Még mindig a DDS-ről írok, mivel szabadidőmben mostanában ez foglalkoztat. Ha valaki végigböngészi a Xilinx IP-ket, gyorsan talál egy DDS-t közöttük, mégis inkább úgy voltam vele, hogy megírom a sajátomat. Ennek oka leginkább a Xilinx DDS hiányosságaiban keresendő: például szerettem volna tetszőleges hullámformát generálni, illetve a dither-t ki-be kapcsolni, ahogy azt a generált jelalak megkívánja (ez még a kódban nem szerepel). Eredetileg kicsit több melóra számítottam vele, de mint kiderült, annyira egyszerű, hogy az egész kódot beírhatom ide. Minden részét nem fogom mégsem bemásolni, a generált fájlokat amúgy is jobb, ha mindenki legenerálja magának.
A DDS projekt a következő elemekből épül fel:
- Clock: A Xilinx Clock IP-je építette fel az órajel előállításához szükséges paramétereket. Nekem a panelomon 25MHz-es órajel van, a DA átalakítóm 50MHz-et bír, így egy kétszeres szorzót tettem bele mindössze.
- Ram: Ez is egy IP, mégpedig egy 2048 byte-os memória, amiben a jelalakot tároljuk.
- Random: A dither előállításához szükséges pszeudo-random generátor. Sajnos a Xilinx IP-i közül kivették, ezért meg kellett írni.
- DDSMain: A DDS implementációja.
- testbench: A teszteléshez szükséges jeleket előállító kód.
A forráskódokat innen lehet letölteni.
Nézzük akkor meg a kódokat, először is a véletlen szám generátort:
entity random is
Nincs túlbonyolítva, az órajel minden ciklusára egy 32 bájtos véletlen számot állít elő.
port (
clk : in std_logic;
random_num : out std_logic_vector (31 downto 0) --output vector
);
end random;
architecture Behavioral of random is
Mint az látszik, ez egy egyszerű LFSR, a régi Xilinx IP dokumentációja itt található. Ebben van egy táblázat, amiből kideríthető, hogy 32 bites hossznál melyik biteket célszerű XOR-olni, hogy a vélelen szám generálás a legkésőbb kezdjen ismétlődni.
begin
process(clk)
variable rand_temp : std_logic_vector(31 downto 0) := "10111010111110001011101011111000";
variable temp : std_logic := '0';
begin
if(rising_edge(clk)) then
temp := rand_temp(31) xor rand_temp(21);
temp := temp xor rand_temp(1);
temp := temp xor rand_temp(0);
rand_temp(31 downto 1) := rand_temp(30 downto 0);
rand_temp(0) := temp;
end if;
random_num <= rand_temp;
end process;
end Behavioral;
A DDS kódja:
entity ddsmain is
Fussunk végig a bemeneteken és kimeneteken:
Port ( ClkIn : in STD_LOGIC;
Rst : in STD_LOGIC;
ClkOut : out STD_LOGIC;
DataOut : out STD_LOGIC_VECTOR (7 downto 0);
DataIn : in STD_LOGIC_VECTOR (7 downto 0);
Addr : in STD_LOGIC_VECTOR (3 downto 0);
ManClk : in STD_LOGIC;
WriteClk : in STD_LOGIC;
ClkSel : in STD_LOGIC);
end ddsmain;
- ClkIn: A bemenő órajel, mint említettem 25 MHz
- ManClk: Bemenő órajel, ha nem a 25MHz-es jelet akarjuk használni
- ClkSel: Ezzel választhatunk a ClkIn vagy a ManClk közül
- Rst: Reset
- ClkOut: Kimenő órajel a DA átalakítóhoz
- DataOut: Kimenő adat a DA átalakítóhoz
- DataIn: Bemenő adat a DDS programozásához
- Addr: A programozandó regiszter címe
- WriteClk: A programozáshoz szükséges órajel
A DDS paramétereinek beállítása ahhoz hasonlóan történik, mintha regiszterekbe írnánk az értékeket. Az Addr bemeneten kiválasztjuk a regisztert, a DataIn-be beírjuk az adatot, és a WriteClk ciklusával az adat beíródik.
process (WriteClk, Addr, DataIn)
A következő címek vannak definiálva:
begin
if rising_edge(WriteClk) then
case Addr is
when "0000" => freqbuf(7 downto 0) <= DataIn;
when "0001" => freqbuf(15 downto 8) <= DataIn;
when "0010" => freqbuf(23 downto 16) <= DataIn;
when "0011" => freqbuf(29 downto 24) <= DataIn(5 downto 0); freqreg <= freqbuf;
when "0100" => phasebuf(7 downto 0) <= DataIn;
when "0101" => phasebuf(9 downto 8) <= DataIn(1 downto 0); phasereg <= phasebuf;
when "0110" => ramaddrbuf(7 downto 0) <= DataIn;
when "0111" => ramaddrbuf(10 downto 8) <= DataIn(2 downto 0); ramaddrreg <= ramaddrbuf;
when "1000" => ramdatain <= DataIn;
when "1001" => ramrst <= DataIn(0);
when "1010" => ramwrite(0) <= DataIn(0);
when others => NULL;
end case;
end if;
end process;
- 0000 - 0011: A generálandó frekvencia, 4 bájton ábrázolva. Mivel a DDS fázisakkumulátora 32 bájtos, a frekvencia legfelső két bitjét nem használjuk (nem kapnánk értelmes jelalakot).
- 0100 - 0101: 10 bites fázis regiszter
- 0110 - 0111: 11 bites cím regiszter a RAM írásához
- 1000: A RAM-ba írandó adat
- 1001: A RAM törlése
- 1010: csak a legalsó bitje használt, ha 1, akkor a RAM-ot írjuk, ha 0, akkor olvassuk.
Kicsit előreszaladtunk, ha a kód elejét nézzük, az első lépés a külső modulok példányosítása:
Inst_Clock: Clock PORT MAP(
CLKIN_IN => ClkIn,
CLKIN_IBUFG_OUT => bufo,
CLK0_OUT => clk0,
CLK2X_OUT => clk2
);
Inst_Ram: ram port map (
clka => intclk,
rsta => ramrst,
wea => ramwrite,
addra => ramaddr,
dina => ramdatain,
douta => DataOut
);
Inst_Rnd: random PORT MAP (
clk => clk2,
random_num => random_num
);
Ezzel gyakorlatilag bekötjük belső jelekre a moduljaink lábait.
A következő részben az órajel előállításához szükséges műveletek jönnek:
IBUFG_inst_ManClk : IBUFG
Létrehozunk egy Clock Buffert a ManClk számára is, mert az IP Wizard csak a ClkIn-nek csinálta meg. A Spartan3 kézikőnyvében van leírva, hogy mit miért és hogy kell csinálni.
generic map ( IBUF_DELAY_VALUE => "0", -- Specify the amount of added input delay for buffer, "0"-"16"
IOSTANDARD => "DEFAULT")
port map (
O => manclkbuf, -- Clock buffer output
I => ManClk -- Clock buffer input (connect directly to top-level port)
);
BUFGMUX_inst : BUFGMUX
port map (
O => intclk, -- Clock MUX output
I0 => clk2, -- Clock0 input
I1 => manclkbuf, -- Clock1 input
S => ClkSel -- Clock select input
);
ClkOut <= intclk;
Egy BUFGMUX példány dönti el, hogy a ManClk vagy a ClkIn(x2) bemenetet használjuk órajelnek.
Ezután már csak az a rész van hátra, ahol a DDS jelgenerálás végbemegy:
process (intclk, Rst, ramwrite, freqreg, phasereg, random_num, phasedither, phasecrop, ramaddrreg)
Az első rész a resetet végzi el, utána jön tulajdonképpen a jelgenerálás. Ez nem tér el a DDS-nél már leírt algoritmustól. A fázis akkumulátorhoz hozzáadjuk a frekvencia regisztert minden órajelciklusban, ehhez jön a fázis regiszter, a véletlen szám a ditherhez, végül az egészet akkorára vágjuk, hogy a RAM-ot meg lehessen vele címezni.
begin
if rising_edge(intclk) then
if (Rst = '1') then
phaseacc <= "00000000000000000000000000000000";
phaseout <= "00000000000000000000000000000000";
else
if (ramwrite(0) = '0') then
phaseacc <= phaseacc + freqreg;
phaseout <= phaseacc + (phasereg * "1000000000000000000000");
phasedither <= phaseout + random_num(20 downto 0);
phasecrop <= phasedither(31 downto 21);
ramaddr <= phasecrop;
else
ramaddr <= ramaddrreg;
end if;
end if;
end if;
end process;
A RAM kimenete van a DataOut-ra kötve, így a megcímzett memória tartalma kerül a kimenetre.
Gyakorlatilag a DDS ennyiből áll. Érdemes még egy picit a teszteléshez használt kódon is átfutni, ugyanis ebben megnézhetjük, hogyan kell feltölteni a RAM-ot egy szinusz hullámmal:
signal s: real;
A szinusz hullám feltöltésének menete a következő:
signal s2: real;
signal si: real;
signal intaddr: std_logic_vector(10 downto 0);
...
-- Fill up RAM with sine
DataIn <= "11111111";
Addr <= "1010";
wait for WriteClk_period;
si <= 0.0;
for i in 0 to 2048 loop
intaddr <= conv_std_logic_vector(i,11);
DataIn <= intaddr(7 downto 0);
Addr <= "0110";
wait for WriteClk_period;
DataIn(2 downto 0) <= intaddr(10 downto 8);
DataIn(7 downto 3) <= "00000";
Addr <= "0111";
wait for WriteClk_period;
s <= sin((Math_pi / 1024.0) * si);
s2 <= s * 127.5;
si <= si + 1.0;
DataIn <= conv_std_logic_vector(INTEGER(s2)+127, 8);
Addr <= "1000";
wait for WriteClk_period;
end loop;
DataIn <= "00000000";
Addr <= "1010";
wait for WriteClk_period;
-- Fill End
- Írásra állítjuk a RAM-ot
- Egy ciklussal végigmegyünk a címeken, a "conv_std_logic_vector" segítségével alakíthatjuk a ciklusváltozónkat címmé
- A címet beírjuk a DDS regiszterébe
- A szinuszt a VHDL beépített függvényeivel kiszámoljuk. Ez a rész csak a teszt közben tud futni, az FPGA-ba nem programozható :)
- Az "INTEGER" függvény alakítja egész számmá a "real"-t amiben a szinusz tárolva van, ezt szokás szerint a "conv_std_logic_vector" alakítja nyolc bites számmá, ami a RAM-ba írandó adat.
- Az adatot beírjuk a RAM-ba
- Ha az összes címre beírtuk az adatot, az írás módot kikapcsoljuk.
Gyakorlatilag ennyi az egész. Ha a tesztet lefuttatjuk, nyomon követhetjük, ahogy a szinusz hullámot a DDS létrehozza. Ha a frekvenciát elég alacsonyra vesszük, azt is megnézhetjük, hogyan működik a dither.
Jó szórakozást!