Previous blogs:
FPGA: Making Waves
Introduction
In the last blog, I looked at computing a sinewave using a CORDIC method. It was implemented in a iterated bit-serial fashion to keep the hardware usage low, though it turned out to be more complex than I had anticipated because of some of the fiddly control necessary.
This time I'm going to add a passive analogue reconstruction filter to the output. At the same time, I'll have to do a bit of work on my phase accumulator. In the last blog, I just had it running from -pi/2 to +pi/2 to generate half of the sine, but now I need it to run continuously and generate the actual sinewave.
The Circuit
Here's the circuit for the filter I used:
In case you're interested, today's apple is a Cox's Orange Pippin. This one lived up to its name and was a nice orange and red colour.
For the filter, I chose a 5th-order Butterworth low-pass response and designed it by finding the normalised (to 1Hz and 1ohm) capacitor and inductor values in a book and then scaling them appropriately. [I actually worked it out somewhat backwards, because I only had a very limited range of chokes, so I looked for which inductor would give a fairly sensible resistor value for the cut-off frequency I wanted (25kHz) and then calculated the capacitors on the basis I could probably piece them together from parts that I had.]
Here it is built on my prototyping board.
You can see that I've paralleled-up capacitors to get the value I wanted. Happily, I found some old 10nF parts that actually measured 9nF and which I could parallel to give 18nF.
Simulated Response
Here's what a simulator gives me for the frequency and phase responses for the component values I've chosen:
Phase Accumulator
I wasn't too sure what to do with the phase accumulator. Working with the interval -pi/2 to pi/2 is awkward, though I suppose there is an argument that, using radians, frequencies then work out neatly and directly. But in an FPGA, it seemed like it might be simpler to scale the input so that I could use binary modulo arithmetic, the modulo arithmetic giving me the wrap-around of the phase for free.
The CORDIC approach I'm using here operates with simple additions and subtractions to iterate to the required angle, so I don't need to scale the input phase from the binary range used by the accumulator to the radian range used by the CORDIC processor, instead I can bake the scale factor into the coefficients and save having the multiply. Effectively, the CORDIC is then doing the multiply for me.
The other thing I have to arrange is to achieve the symmetry to get the other half of the sine which would conflict with simply using modulo arithmetic where I'd just have one half repeating. After a bit of head scratching, I realised that I could operate the binary accumulator over the interval -2 to +2 and use an exclusive or of the top two bits to control whether to use the value directly or to invert the lower part of it. That seems to work, though I haven't convinced myself that it is strictly correct.
The VHDL
------------------------------------------------------------------ -- ***** waves_cordic_sine ***** -- -- 15-bit CORDIC sine calculation in serial form (slow!) -- -- coefficients now normalised for +1 to -1 range -- -- rather than +pi/2 to -pi/2 -- -- to allow for binary modulo arithmetic on phase accumulator -- ------------------------------------------------------------------ -- JC 4th November 2019 -- ------------------------------------------------------------------ -- Rev Date Comments -- -- 01 04-Nov-2019 -- ------------------------------------------------------------------ library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity wave_cordic_con is port( clk_in: in std_logic; --- system clock in (50 MHz oscillator) --- DAC connections mcp4821_ncs: out std_logic; --- DAC cs mcp4821_sck: out std_logic; --- DAC sck mcp4821_sdi: out std_logic; --- DAC sdi mcp4821_nldac: out std_logic; --- DAC load --- misc control signals on evaluation board that it might be good to hold at fixed levels spi_cs: out std_logic; --- hold_n: out std_logic; --- sram_cen: out std_logic; --- sram_oen: out std_logic; --- sram_wen: out std_logic; --- uart_tx: out std_logic); --- end wave_cordic_con; architecture arch_wave_cordic of wave_cordic_con is signal start_count: std_logic_vector (1 downto 0); signal start_reset: std_logic; signal spi_send, spi_send_plus_one, spi_ncs, spi_ncs_del: std_logic; signal spi_output_sr_bit_count: std_logic_vector (5 downto 0); signal spi_output_sr: std_logic_vector (15 downto 0); signal interval_count: std_logic_vector (8 downto 0); signal acc_count: std_logic_vector (7 downto 0); signal cordic_reset,cordic_enable, cordic_dir: std_logic; signal cordic_count: std_logic_vector (7 downto 0); signal adder_out: std_logic_vector (31 downto 0); signal add_sub_1_out, add_sub_2_out, add_sub_3_out: std_logic_vector (0 downto 0); signal mux_select: std_logic_vector (3 downto 0); signal mux_count: std_logic_vector (3 downto 0); signal sine_tap_value, cos_tap_value: std_logic_vector (0 downto 0); signal theta: std_logic_vector (15 downto 0); signal phase_acc: std_logic_vector (31 downto 0); signal phase_increment: std_logic_vector (31 downto 0); signal sine_value: std_logic_vector (15 downto 0); signal cos_value: std_logic_vector (15 downto 0); signal angle_value: std_logic_vector (15 downto 0); signal osc_reset: std_logic_vector (1 downto 0); signal rom_out: std_logic_vector (0 downto 0); signal carry_1_in, carry_2_in, carry_3_in: std_logic; signal carry_1_out, carry_2_out, carry_3_out: std_logic; signal sine_sign_extend, cos_sign_extend: std_logic; signal sine_select: std_logic; --- declare the adder/subractor and the table modules as components component add_sub_module is port ( DataA: in std_logic_vector(0 downto 0); DataB: in std_logic_vector(0 downto 0); Cin: in std_logic; Add_Sub: in std_logic; Result: out std_logic_vector(0 downto 0); Cout: out std_logic); end component; component rom_module is port ( Address: in std_logic_vector(7 downto 0); Q: out std_logic_vector(0 downto 0)); end component; component add_module is port ( DataA: in std_logic_vector(31 downto 0); DataB: in std_logic_vector(31 downto 0); Result: out std_logic_vector(31 downto 0)); end component; begin wave_cordic_stuff: process (clk_in) begin if (clk_in'event and clk_in='1') then --- start reset if (start_count(1 downto 0) /= b"11") then start_count(1 downto 0) <= start_count(1 downto 0) + 1; end if; if (start_count(1 downto 0) = b"10") then start_reset <= '1'; else start_reset <= '0'; end if; --- interval_count counts at the clock rate (50MHz) --- counts down by 500, so spi_send occurs every 10us (100kHz) if (interval_count(8 downto 0) = b"000000000") then --- if zero interval_count(8 downto 0) <= b"111110011"; --- preset to 499 spi_send <= '1'; else interval_count(8 downto 0) <= interval_count(8 downto 0) - 1; --- count down spi_send <= '0'; end if; spi_send_plus_one <= spi_send; --- spi_send delayed by one clock --- spi ncs goes low when triggered by spi_send, goes hi again when bitcount reaches 31 if (spi_send = '1') then spi_ncs <= '0'; elsif (spi_output_sr_bit_count(5 downto 0) = b"111111") then spi_ncs <= '1'; end if; spi_ncs_del <= spi_ncs; --- cordic reset occurs at end of spi cs --- this starts the CORDIC calculation for the next sine if (spi_ncs = '1' and spi_ncs_del = '0') then cordic_reset <= '1'; else cordic_reset <= '0'; end if; --- cordic enable and cordic count --- there are 15 cycles of 16 bits, so count goes to 240 --- low half of count is the bit, high part is the cycle if (cordic_reset = '1') then cordic_enable <= '1'; --- elsif (cordic_count(7 downto 0) = b"00001111") then elsif (cordic_count(7 downto 0) = b"11011111") then cordic_enable <= '0'; end if; if (cordic_enable = '1') then cordic_count(7 downto 0) <= cordic_count(7 downto 0) + 1; else cordic_count(7 downto 0) <= b"00000000"; end if; --- spi output bit count only counts when enable spi cs is low if (spi_ncs = '0') then spi_output_sr_bit_count(5 downto 0) <= spi_output_sr_bit_count(5 downto 0) + 1; else spi_output_sr_bit_count(5 downto 0) <= b"000000"; end if; --- phase accumulator --- addition of increment done just before CORDIC calculation --- S1.30 if (start_reset = '1') then phase_acc(31 downto 0) <= b"0_0_000000000000000000000000000000"; --- at start set to zero elsif(spi_send = '1') then phase_acc(31 downto 0) <= adder_out(31 downto 0); --- store addition of increment to current phase end if; --- on next cycle after spi_send, theta is derived from the phase accumulator. --- values between -1 and +1 are unchanged, outside that, they fold in --- to get the symmetry on the waveform if(spi_send_plus_one = '1') then if((phase_acc(31) xor phase_acc(30)) = '0') then theta(15) <= phase_acc(31); theta(14 downto 0) <= phase_acc(30 downto 16); else theta(15) <= phase_acc(31); theta(14 downto 0) <= not phase_acc(30 downto 16); end if; end if; --- CORDIC algorithm --- Implemented as bit-serial arithmetic (this is my choice for minimal hardware - not inherent in method). --- Each variable gets rotated out of shift register, with single bit at a time being presented --- to a 1-bit adder/subtractor, and the result shifted back in the other end. --- Cos start value is prescaled to save having to compensate with a --- multiplication at the end. --- angle_value is preset to the required phase, the algorithm then aims for an angle value of zero. --- Which side of zero we are on is easy to test for as it just entails looking at the resulting --- angle_value sign bit and using that to control whether to add or subtract on the next iteration. if (cordic_reset = '1') then sine_value(15 downto 0) <= b"0_0_00000000000000"; --- sin starts at 0 cos_value(15 downto 0) <= b"0_1_00110110111000"; --- cos = 1/An (An is gain for n iterations) angle_value(15 downto 0) <= theta(15 downto 0); elsif (cordic_enable = '1') then cos_value(14 downto 0) <= cos_value(15 downto 1); --- rotate right through add/sub cos_value(15) <= add_sub_1_out(0); sine_value(14 downto 0) <= sine_value(15 downto 1); --- rotate right through add/sub sine_value(15) <= add_sub_2_out(0); angle_value(14 downto 0) <= angle_value(15 downto 1); --- rotate right through add/sub angle_value(15) <= add_sub_3_out(0); end if; if (cordic_reset = '1') then cos_sign_extend <= '0'; sine_sign_extend <= '0'; cordic_dir <= theta(15); --- initial direction depends on sign of angle carry_1_in <= not theta(15); --- sense of initial carry depends on whether next cycle is add or subtract carry_2_in <= theta(15); carry_3_in <= not theta(15); elsif (cordic_count(3 downto 0) = b"1111") then --- at end of word cos_sign_extend <= add_sub_1_out(0); --- sign of result is stored for next cycle sine_sign_extend <= add_sub_2_out(0); --- ... for use by sign-extension cordic_dir <= add_sub_3_out(0); --- sign of angle determines add/sub for next cycle carry_1_in <= not add_sub_3_out(0); --- set to no carry or no borrow at start carry_2_in <= add_sub_3_out(0); carry_3_in <= not add_sub_3_out(0); else carry_1_in <= carry_1_out; --- whilst rotating, carry is from the last bit-add or bit-subtract carry_2_in <= carry_2_out; carry_3_in <= carry_3_out; end if; if (cordic_reset = '1') then mux_count <= b"0000"; --- elsif (cordic_count(3 downto 0) = b"1111") then mux_count <= cordic_count(7 downto 4) + 1; --- elsif (mux_count(3 downto 0) /= b"1111") then mux_count <= mux_count + 1; end if; if (cordic_reset = '1') then mux_select <= b"0000"; --- s/r start 'tap' for next cycle elsif (cordic_reset = '1' or cordic_count(3 downto 0) = b"1111") then mux_select <= cordic_count(7 downto 4) + 1; --- s/r start 'tap' for next cycle elsif (mux_count(3 downto 0) = b"1111") then mux_select <= b"1111"; --- ...selects sign extend instead for rest end if; --- spi output shift register if (spi_send = '1') then --- load... spi_output_sr(10 downto 0) <= sine_value(14 downto 4); --- use sine value spi_output_sr(11) <= not sine_value(15); --- inverse of sign bit spi_output_sr(15 downto 12) <= b"0011"; --- dac control bits elsif (spi_ncs = '0' and spi_output_sr_bit_count(1 downto 0) = b"11") then --- shift... spi_output_sr(15 downto 1) <= spi_output_sr(14 downto 0); --- the register contents spi_output_sr(0) <= '0'; end if; end if; --- multiplexers to tap the sine and cos shift registers --- (as well as doing the shift, these deal with the sign-extend --- for 2's complement arithmetic) case mux_select(3 downto 0) is when b"0000" => sine_tap_value(0) <= sine_value(0); when b"0001" => sine_tap_value(0) <= sine_value(1); when b"0010" => sine_tap_value(0) <= sine_value(2); when b"0011" => sine_tap_value(0) <= sine_value(3); when b"0100" => sine_tap_value(0) <= sine_value(4); when b"0101" => sine_tap_value(0) <= sine_value(5); when b"0110" => sine_tap_value(0) <= sine_value(6); when b"0111" => sine_tap_value(0) <= sine_value(7); when b"1000" => sine_tap_value(0) <= sine_value(8); when b"1001" => sine_tap_value(0) <= sine_value(9); when b"1010" => sine_tap_value(0) <= sine_value(10); when b"1011" => sine_tap_value(0) <= sine_value(11); when b"1100" => sine_tap_value(0) <= sine_value(12); when b"1101" => sine_tap_value(0) <= sine_value(13); when b"1110" => sine_tap_value(0) <= sine_value(14); when b"1111" => sine_tap_value(0) <= sine_sign_extend; when others => sine_tap_value(0) <= sine_sign_extend; end case; case mux_select(3 downto 0) is when b"0000" => cos_tap_value(0) <= cos_value(0); when b"0001" => cos_tap_value(0) <= cos_value(1); when b"0010" => cos_tap_value(0) <= cos_value(2); when b"0011" => cos_tap_value(0) <= cos_value(3); when b"0100" => cos_tap_value(0) <= cos_value(4); when b"0101" => cos_tap_value(0) <= cos_value(5); when b"0110" => cos_tap_value(0) <= cos_value(6); when b"0111" => cos_tap_value(0) <= cos_value(7); when b"1000" => cos_tap_value(0) <= cos_value(8); when b"1001" => cos_tap_value(0) <= cos_value(9); when b"1010" => cos_tap_value(0) <= cos_value(10); when b"1011" => cos_tap_value(0) <= cos_value(11); when b"1100" => cos_tap_value(0) <= cos_value(12); when b"1101" => cos_tap_value(0) <= cos_value(13); when b"1110" => cos_tap_value(0) <= cos_value(14); when b"1111" => cos_tap_value(0) <= cos_sign_extend; when others => cos_tap_value(0) <= cos_sign_extend; end case; --- phase increment for testing phase_increment(31 downto 0) <= b"0_00_01100110011001100110011001100"; --- --- wiring the DAC I/O pins mcp4821_ncs <= spi_ncs; mcp4821_sck <= spi_output_sr_bit_count(1); mcp4821_sdi <= spi_output_sr(15); mcp4821_nldac <= '0'; --- hold these device control pins at a fixed level to stop them flapping around spi_cs <= '1'; hold_n <= '1'; sram_cen <= '1'; sram_oen <= '1'; sram_wen <= '1'; uart_tx <= '1'; end process wave_cordic_stuff; --- instantiate the adder-subtractors, adder, and the ROM table holding the inverse tangent values add_sub_1: add_sub_module --- cosine port map( DataA => cos_value(0 downto 0), DataB => sine_tap_value(0 downto 0), Cin => carry_1_in, Add_Sub => cordic_dir, Result => add_sub_1_out(0 downto 0), Cout => carry_1_out); add_sub_2: add_sub_module --- sine port map( DataA => sine_value(0 downto 0), DataB => cos_tap_value(0 downto 0), Cin => carry_2_in, Add_Sub => not cordic_dir, Result => add_sub_2_out(0 downto 0), Cout => carry_2_out); add_sub_3: add_sub_module ---angle port map( DataA => angle_value(0 downto 0), DataB => rom_out(0 downto 0), Cin => carry_3_in, Add_Sub => cordic_dir, Result => add_sub_3_out(0 downto 0), Cout => carry_3_out); rom_module1: rom_module --- inverse-tangent table (bit serial, 256 bits, lsb first) port map( Address => cordic_count(7 downto 0), Q => rom_out(0 downto 0)); add_1: add_module --- phase accumulator calculation port map( DataA => phase_acc(31 downto 0), DataB => phase_increment(31 downto 0), Result => adder_out(31 downto 0)); end arch_wave_cordic;
This is the ROM table holding the inverse tangent values. It's a 256x1 ROM and the first bit out is the lsb one.
1 0 1 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 1 0 1 0 1 1 0 0 1 0 0 0 1 0 0 1 1 1 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1 1 0 0 1 0 1 0 0 0 0 0 1 1 0 1 1 0 0 1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 1 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Testing
Here's how it looks on the oscilloscope. The blue trace is the output of the DAC, the yellow the filtered waveform. It was supposed to be 10kHz - I forgot about the extra factor of two that I had built in.
The amplitude is diminished slightly over the gain of x0.5 we'd expect from the equal-value resistors at each end of the filter. That's probably the sinc roll-off from the first-order hold response of the DAC which I haven't tried to compensate for, either in the design of the analogue filter or by filtering digitally with the inverse response in the FPGA. I'll add it to the list for future blogs.
Out of curiosity, I tried the filtered sine with the 'scope's FFT. That shows a fair bit of 3rd harmonic and some 2nd, so it might be that the way I'm folding the accumulator values isn't quite right.
Update 9th November
I've been looking at the phase accumulator and I did get it wrong. To get the symmetry, I should have complemented and added one rather than just complemented. It didn't make much difference in practice, though (because of the large, 32-bit phase accumulator). With my 5kHz waveform, a difference of one over half the cycle is next to nothing and doesn't notice at all in the end result. But, correcting that showed up a more subtle problem.
I thought I'd have a look at generating a really slow waveform. I'd already done some calculations for a 2Hz sinewave, so I put in the phase increment and this was the result:
Hmm, got that wrong! My calculations had been for the fast cordic variant, with a sample rate of 50Msps, and not the 100ksps I'm using with the SPI DAC, so the end result was this sine with period of over 16 minutes. I let it run for a whole cycle so that you could see the trace. It does show at least one advantage of a digitally-generated waveform: that result would be extremely hard to achieve with an analogue oscillator.
What caught my attention was what appears to be a glitch. Well, it looks like a glitch, but if I increase the size of the phase increment and generate a 0.25Hz waveform, we can see that it occurs in the same place every cycle.
My tests for whether to leave the phase alone, or do the complement and add one, needed an additional test so that they get the very negative-most value right (it needs to be treated as a special case, unfortunately).
I now have this for the 0.25Hz waveform
That's quite mesmerising to watch, a bit like looking at a pendulum swinging back and forth as it gets drawn on the righthand edge of the oscilloscope screen.
There's been next to no change of the spectrum, so it doesn't look like the distortion is coming from the phase calculations.
Here's the reworked code, in case you're interested in looking at it. No warranty. No guarantee. No fitness for any purpose, and all that kind of thing.
---------------------------------------------------------------------- -- ***** waves_cordic_sine ***** -- -- 15-bit CORDIC sine calculation in serial form (slow!) -- -- coefficients now normalised for +1 to -1 range -- -- rather than +pi/2 to -pi/2 -- -- to allow for binary modulo arithmetic on phase accumulator -- ---------------------------------------------------------------------- -- JC 4th November 2019 -- ---------------------------------------------------------------------- -- Rev Date Comments -- -- 01 04-Nov-2019 -- -- 02 09-Nov-2019 Reworked derivation of theta from phase_acc -- ---------------------------------------------------------------------- library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity wave_cordic_con is port( clk_in: in std_logic; --- system clock in (50 MHz oscillator) --- DAC connections mcp4821_ncs: out std_logic; --- DAC cs mcp4821_sck: out std_logic; --- DAC sck mcp4821_sdi: out std_logic; --- DAC sdi mcp4821_nldac: out std_logic; --- DAC load --- misc control signals on evaluation board that it might be good to hold at fixed levels spi_cs: out std_logic; --- hold_n: out std_logic; --- sram_cen: out std_logic; --- sram_oen: out std_logic; --- sram_wen: out std_logic; --- uart_tx: out std_logic); --- end wave_cordic_con; architecture arch_wave_cordic of wave_cordic_con is signal start_count: std_logic_vector (1 downto 0); signal start_reset: std_logic; signal spi_send, spi_send_plus_one, spi_ncs, spi_ncs_del: std_logic; signal spi_output_sr_bit_count: std_logic_vector (5 downto 0); signal spi_output_sr: std_logic_vector (15 downto 0); signal interval_count: std_logic_vector (8 downto 0); signal acc_count: std_logic_vector (7 downto 0); signal cordic_reset,cordic_enable, cordic_dir: std_logic; signal cordic_count: std_logic_vector (7 downto 0); signal adder_out: std_logic_vector (31 downto 0); signal add_sub_1_out, add_sub_2_out, add_sub_3_out: std_logic_vector (0 downto 0); signal mux_select: std_logic_vector (3 downto 0); signal mux_count: std_logic_vector (3 downto 0); signal sine_tap_value, cos_tap_value: std_logic_vector (0 downto 0); signal theta: std_logic_vector (15 downto 0); signal phase_acc: std_logic_vector (31 downto 0); signal phase_increment: std_logic_vector (31 downto 0); signal sine_value: std_logic_vector (15 downto 0); signal cos_value: std_logic_vector (15 downto 0); signal angle_value: std_logic_vector (15 downto 0); signal osc_reset: std_logic_vector (1 downto 0); signal rom_out: std_logic_vector (0 downto 0); signal carry_1_in, carry_2_in, carry_3_in: std_logic; signal carry_1_out, carry_2_out, carry_3_out: std_logic; signal sine_sign_extend, cos_sign_extend: std_logic; signal sine_select: std_logic; --- declare the adder/subractor and the table modules as components component add_sub_module is port ( DataA: in std_logic_vector(0 downto 0); DataB: in std_logic_vector(0 downto 0); Cin: in std_logic; Add_Sub: in std_logic; Result: out std_logic_vector(0 downto 0); Cout: out std_logic); end component; component rom_module is port ( Address: in std_logic_vector(7 downto 0); Q: out std_logic_vector(0 downto 0)); end component; component add_module is port ( DataA: in std_logic_vector(31 downto 0); DataB: in std_logic_vector(31 downto 0); Result: out std_logic_vector(31 downto 0)); end component; begin wave_cordic_stuff: process (clk_in) begin if (clk_in'event and clk_in='1') then --- start reset if (start_count(1 downto 0) /= b"11") then start_count(1 downto 0) <= start_count(1 downto 0) + 1; end if; if (start_count(1 downto 0) = b"10") then start_reset <= '1'; else start_reset <= '0'; end if; --- interval_count counts at the clock rate (50MHz) --- counts down by 500, so spi_send occurs every 10us (100kHz) if (interval_count(8 downto 0) = b"000000000") then --- if zero interval_count(8 downto 0) <= b"111110011"; --- preset to 499 spi_send <= '1'; else interval_count(8 downto 0) <= interval_count(8 downto 0) - 1; --- count down spi_send <= '0'; end if; spi_send_plus_one <= spi_send; --- spi_send delayed by one clock --- spi ncs goes low when triggered by spi_send, goes hi again when bitcount reaches 31 if (spi_send = '1') then spi_ncs <= '0'; elsif (spi_output_sr_bit_count(5 downto 0) = b"111111") then spi_ncs <= '1'; end if; spi_ncs_del <= spi_ncs; --- cordic reset occurs at end of spi cs --- this starts the CORDIC calculation for the next sine if (spi_ncs = '1' and spi_ncs_del = '0') then cordic_reset <= '1'; else cordic_reset <= '0'; end if; --- cordic enable and cordic count --- there are 15 cycles of 16 bits, so count goes to 240 --- low half of count is the bit, high part is the cycle if (cordic_reset = '1') then cordic_enable <= '1'; --- elsif (cordic_count(7 downto 0) = b"00001111") then elsif (cordic_count(7 downto 0) = b"11011111") then cordic_enable <= '0'; end if; if (cordic_enable = '1') then cordic_count(7 downto 0) <= cordic_count(7 downto 0) + 1; else cordic_count(7 downto 0) <= b"00000000"; end if; --- spi output bit count only counts when enable spi cs is low if (spi_ncs = '0') then spi_output_sr_bit_count(5 downto 0) <= spi_output_sr_bit_count(5 downto 0) + 1; else spi_output_sr_bit_count(5 downto 0) <= b"000000"; end if; --- phase accumulator --- addition of increment done just before CORDIC calculation --- S1.30 if (start_reset = '1') then phase_acc(31 downto 0) <= b"0_0_000000000000000000000000000000"; --- at start set to zero elsif(spi_send = '1') then phase_acc(31 downto 0) <= adder_out(31 downto 0); --- store addition of increment to current phase end if; --- on next cycle after spi_send, theta is derived from the phase accumulator. --- values between -1 and +1 are unchanged, outside that, they fold in --- to get the symmetry on the waveform if(spi_send_plus_one = '1') then if(phase_acc(31 downto 16) = b"1000000000000000") then theta(15) <= not phase_acc(31); else theta(15) <= phase_acc(31); end if; if((phase_acc(31) xor phase_acc(30)) = '0') then theta(14 downto 0) <= phase_acc(30 downto 16); else theta(14 downto 0) <= (not phase_acc(30 downto 16)) + 1; end if; end if; --- CORDIC algorithm --- Implemented as bit-serial arithmetic (this is my choice for minimal hardware - not inherent in method). --- Each variable gets rotated out of shift register, with single bit at a time being presented --- to a 1-bit adder/subtractor, and the result shifted back in the other end. --- Cos start value is prescaled to save having to compensate with a --- multiplication at the end. --- angle_value is preset to the required phase, the algorithm then aims for an angle value of zero. --- Which side of zero we are on is easy to test for as it just entails looking at the resulting --- angle_value sign bit and using that to control whether to add or subtract on the next iteration. if (cordic_reset = '1') then sine_value(15 downto 0) <= b"0_0_00000000000000"; --- sin starts at 0 cos_value(15 downto 0) <= b"0_1_00110110111000"; --- cos = 1/An (An is gain for n iterations) --- cos_value(15 downto 0) <= b"0_0_10011011011100"; --- cos = 1/An (An is gain for n iterations) angle_value(15 downto 0) <= theta(15 downto 0); elsif (cordic_enable = '1') then cos_value(14 downto 0) <= cos_value(15 downto 1); --- rotate right through add/sub cos_value(15) <= add_sub_1_out(0); sine_value(14 downto 0) <= sine_value(15 downto 1); --- rotate right through add/sub sine_value(15) <= add_sub_2_out(0); angle_value(14 downto 0) <= angle_value(15 downto 1); --- rotate right through add/sub angle_value(15) <= add_sub_3_out(0); end if; if (cordic_reset = '1') then cos_sign_extend <= '0'; sine_sign_extend <= '0'; cordic_dir <= theta(15); --- initial direction depends on sign of angle carry_1_in <= not theta(15); --- sense of initial carry depends on whether next cycle is add or subtract carry_2_in <= theta(15); carry_3_in <= not theta(15); elsif (cordic_count(3 downto 0) = b"1111") then --- at end of word cos_sign_extend <= add_sub_1_out(0); --- sign of result is stored for next cycle sine_sign_extend <= add_sub_2_out(0); --- ... for use by sign-extension cordic_dir <= add_sub_3_out(0); --- sign of angle determines add/sub for next cycle carry_1_in <= not add_sub_3_out(0); --- set to no carry or no borrow at start carry_2_in <= add_sub_3_out(0); carry_3_in <= not add_sub_3_out(0); else carry_1_in <= carry_1_out; --- whilst rotating, carry is from the last bit-add or bit-subtract carry_2_in <= carry_2_out; carry_3_in <= carry_3_out; end if; if (cordic_reset = '1') then mux_count <= b"0000"; --- elsif (cordic_count(3 downto 0) = b"1111") then mux_count <= cordic_count(7 downto 4) + 1; --- elsif (mux_count(3 downto 0) /= b"1111") then mux_count <= mux_count + 1; end if; if (cordic_reset = '1') then mux_select <= b"0000"; --- s/r start 'tap' for next cycle elsif (cordic_reset = '1' or cordic_count(3 downto 0) = b"1111") then mux_select <= cordic_count(7 downto 4) + 1; --- s/r start 'tap' for next cycle elsif (mux_count(3 downto 0) = b"1111") then mux_select <= b"1111"; --- ...selects sign extend instead for rest end if; --- spi output shift register if (spi_send = '1') then --- load... spi_output_sr(10 downto 0) <= sine_value(14 downto 4); --- use sine value spi_output_sr(11) <= not sine_value(15); --- inverse of sign bit spi_output_sr(15 downto 12) <= b"0011"; --- dac control bits elsif (spi_ncs = '0' and spi_output_sr_bit_count(1 downto 0) = b"11") then --- shift... spi_output_sr(15 downto 1) <= spi_output_sr(14 downto 0); --- the register contents spi_output_sr(0) <= '0'; end if; end if; --- multiplexers to tap the sine and cos shift registers --- (as well as doing the shift, these deal with the sign-extend --- for 2's complement arithmetic) case mux_select(3 downto 0) is when b"0000" => sine_tap_value(0) <= sine_value(0); when b"0001" => sine_tap_value(0) <= sine_value(1); when b"0010" => sine_tap_value(0) <= sine_value(2); when b"0011" => sine_tap_value(0) <= sine_value(3); when b"0100" => sine_tap_value(0) <= sine_value(4); when b"0101" => sine_tap_value(0) <= sine_value(5); when b"0110" => sine_tap_value(0) <= sine_value(6); when b"0111" => sine_tap_value(0) <= sine_value(7); when b"1000" => sine_tap_value(0) <= sine_value(8); when b"1001" => sine_tap_value(0) <= sine_value(9); when b"1010" => sine_tap_value(0) <= sine_value(10); when b"1011" => sine_tap_value(0) <= sine_value(11); when b"1100" => sine_tap_value(0) <= sine_value(12); when b"1101" => sine_tap_value(0) <= sine_value(13); when b"1110" => sine_tap_value(0) <= sine_value(14); when b"1111" => sine_tap_value(0) <= sine_sign_extend; when others => sine_tap_value(0) <= sine_sign_extend; end case; case mux_select(3 downto 0) is when b"0000" => cos_tap_value(0) <= cos_value(0); when b"0001" => cos_tap_value(0) <= cos_value(1); when b"0010" => cos_tap_value(0) <= cos_value(2); when b"0011" => cos_tap_value(0) <= cos_value(3); when b"0100" => cos_tap_value(0) <= cos_value(4); when b"0101" => cos_tap_value(0) <= cos_value(5); when b"0110" => cos_tap_value(0) <= cos_value(6); when b"0111" => cos_tap_value(0) <= cos_value(7); when b"1000" => cos_tap_value(0) <= cos_value(8); when b"1001" => cos_tap_value(0) <= cos_value(9); when b"1010" => cos_tap_value(0) <= cos_value(10); when b"1011" => cos_tap_value(0) <= cos_value(11); when b"1100" => cos_tap_value(0) <= cos_value(12); when b"1101" => cos_tap_value(0) <= cos_value(13); when b"1110" => cos_tap_value(0) <= cos_value(14); when b"1111" => cos_tap_value(0) <= cos_sign_extend; when others => cos_tap_value(0) <= cos_sign_extend; end case; --- phase increment for testing --- phase_increment(31 downto 0) <= b"0_00_01100110011001100110011001100"; --- 5kHz phase_increment(31 downto 0) <= b"0_00_00000000000000010100111110001"; --- 0.25Hz --- wiring the DAC I/O pins mcp4821_ncs <= spi_ncs; mcp4821_sck <= spi_output_sr_bit_count(1); mcp4821_sdi <= spi_output_sr(15); mcp4821_nldac <= '0'; --- hold these device control pins at a fixed level to stop them flapping around spi_cs <= '1'; hold_n <= '1'; sram_cen <= '1'; sram_oen <= '1'; sram_wen <= '1'; uart_tx <= '1'; end process wave_cordic_stuff; --- instantiate the adder-subtractors, adder, and the ROM table holding the inverse tangent values add_sub_1: add_sub_module --- cosine port map( DataA => cos_value(0 downto 0), DataB => sine_tap_value(0 downto 0), Cin => carry_1_in, Add_Sub => cordic_dir, Result => add_sub_1_out(0 downto 0), Cout => carry_1_out); add_sub_2: add_sub_module --- sine port map( DataA => sine_value(0 downto 0), DataB => cos_tap_value(0 downto 0), Cin => carry_2_in, Add_Sub => not cordic_dir, Result => add_sub_2_out(0 downto 0), Cout => carry_2_out); add_sub_3: add_sub_module ---angle port map( DataA => angle_value(0 downto 0), DataB => rom_out(0 downto 0), Cin => carry_3_in, Add_Sub => cordic_dir, Result => add_sub_3_out(0 downto 0), Cout => carry_3_out); rom_module1: rom_module --- inverse-tangent table (bit serial, 256 bits, lsb first) port map( Address => cordic_count(7 downto 0), Q => rom_out(0 downto 0)); add_1: add_module --- phase accumulator calculation port map( DataA => phase_acc(31 downto 0), DataB => phase_increment(31 downto 0), Result => adder_out(31 downto 0)); end arch_wave_cordic;
(Sorry the formatting is a bit of a mess. I'm copying from the Diamond editor window, but obviously tabs and whatnot get lost or end up with other default values.)
phase_acc is the logic vector that accumulates the phase. The most significant part (considered as sign, one whole bit, and the rest as fraction - so an interval of -2 to +2) gets mangled (for the symmetry) into theta which is the phase given to the CORDIC routine. Theta is then over the interval -1 to +1, represented as a two's complement, fractional binary number.
As far as the CORDIC routines are concerned, they're still working over the interval -pi/2 to +pi/2 radians (-90 to +90 degrees) because I've scaled the inverse tangent values appropriately to compensate for the smaller range [I can do that because the angle operation is linear].
Conclusion
The big problem I'm finding with all this is that there are too many interesting topics to explore. If you're working, you naturally keep a tight focus on the spec and where you're off to, but here I'm rushing off in all directions and trying to do too many things at once. I can't quite decide what's next. Possibly fast CORDIC, though I'm tempted to look at either random sequences or computation of logs/exponentials.
[1] http://www.latticesemi.com/en/Products/DevelopmentBoardsAndKits/LatticeXP2Brevia2DevelopmentKit.aspx |
Top Comments