Introduction
I have a small Brevia 2 development board [1] from Lattice Semiconductor featuring one of their XP2- family FPGAs. I'm using it to explore, in a very simple, basic kind of way, digital signal generation and processing. I'm using VHDL to describe the digital logic that I'm designing. I tested it by throwing together some VHDL to drive the DAC data with the output of a counter, generating a simple, linear ramp as a waveform. This time I'm going to have a go at generating a sine wave and, to fascilitate that, I'll be using one of the block memories on the FPGA as a look-up table.
Since I've not used the Lattice FPGAs before, it will be a useful introduction in to how they handle their proprietory elements (multipliers, block memories, PLLs, etc) in the Diamond design suite.
Using a Block Memory as a ROM
This particular FPGA has nine EBRs [Embedded Block RAMs]. Each EBR can have its contents initialised at start-up from the bitstream that configures the FPGA. If the write signal is disabled, the device will then operate as a ROM [Read-Only Memory] (actually, almost like a ROM: the applications material points out that the set-up and hold times on the address and other signals need to be observed to avoid corruption of the contents, which is something to bear in mind).
Design suites need a way of connecting to such components. Previously I'd worked with Xilinx Spartan devices, using Web ISE as the design environment, and there it can simply be done by instantiating the component in the VHDL source (starting with a supplied template) and letting the software sort it all out in the background. With Diamond, it turned out that there is an intermediate step where a utility within Diamond, called IPExpress, allows primitives like memory elements to be selected and configured, and a module to be automatically created to include in the project. Once the module is available, the element can then be simply declared as a component in the VHDL and the connections achieved through an instantiation statement.
Here's a view of my module in IPExpress:
After selecting ROM from the list, putting in a filename for the module and selecting the language (VHDL in my case), clicking on the customize button brought up this dialog box:
That should have been straightforward too, except that I got an error on loading the Memory File that I'd generated with a sine table in it. Here's what the Lattice documentation gives for the format:
Unfortunately it's wrong. There is no header line and it should just be a list of values, one on each line. Including the 'Memory Size ...' part in the grey box looks like it was someone messing up the formatting of the Technical Note. Once I'd puzzled that one out it was all plain sailing.
Trying It Out
I adapted my original VHDL so that the counter connected to the ROM address lines and the data from the ROM went to the shift register and this was the nice result from the DAC:
Unfortunately, it's not quite so nice at the lowest point, though I already knew that would be the case from doing the last blog.
Here the sine shape is compromised by the way the op-amp buffer in the DAC behaves near ground. Not only does it limit the lowest value, distorting the shape of the waveform, but also it seems to take its time lifting off again and the noise rejection seems much poorer (the 'scope is set to a one second persistence time).
The Code
Here's the VHDL
------------------------------------------------------------------ -- ***** Waves_Sine ***** -- -- Test of MCP4821 DAC with sine wave from look-up table -- ------------------------------------------------------------------ -- JC 20th September 2019 -- ------------------------------------------------------------------ -- Rev Date Comments -- -- 01 20-Sept-2019 -- ------------------------------------------------------------------ library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity wave_test_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_test_con; architecture arch_wave_test of wave_test_con is signal spi_send, spi_ncs, spi_ncs_del: std_logic; signal waveform_count: std_logic_vector (11 downto 0); 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 sine_value: std_logic_vector (12 downto 0); component sine_rom port ( Address: in std_logic_vector(9 downto 0); OutClock: in std_logic; OutClockEn: in std_logic; Reset: in std_logic; Q: out std_logic_vector(11 downto 0)); end component; begin wave_test_stuff: process (clk_in) begin if (clk_in'event and clk_in='1') then --- 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 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; --- 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; --- waveform count - this in now going to be the address for the look-up table if (spi_send = '1') then waveform_count(11 downto 0) <= waveform_count(11 downto 0) + 1; end if; --- spi output shift register if (spi_send = '1') then --- load... spi_output_sr(11 downto 0) <= sine_value(11 downto 0); --- dac data from look-up table 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; --- 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_test_stuff; sine_rom1: sine_rom port map(Address => waveform_count(9 downto 0), OutClock => clk_in, OutClockEn => '1', Reset => '0', Q => sine_value(11 downto 0)); end arch_wave_test;
and here's the simple C program I used to generate the sine values for the memory initialisation file
/****************************************************************/ /* sintab.c */ /* Data values for sine look-up table */ /* for Lattice FPGA ROM initialisation */ /* Output file is called sineTable.mem */ /* 13th April 2018 Jon Clift */ /*--------------------------------------------------------------*/ /* Rev Date Comments */ /* 1.0 18/09/2019 */ /****************************************************************/ #include <stdio.h> #include <conio.h> #include <string.h> #include <dos.h> #include <errno.h> #include <math.h> // variables FILE *handle; char temp_string[256]; unsigned int sineTable[1024]; // main routine void main(int argc,char *argv[]) { int i,j; /* print banner */ printf("\n--- sinetab DOS utility program V1.0 ---\n"); printf("Builds sine table for Lattice FPGA ROM.\n"); /* generate sine table in array */ for(i=0;i<1024;i++) { sineTable[i]=(unsigned int) (((sin((2 * 3.1415926/1024)*i)) * 2048) + 2048); } /* open output file */ if((handle=fopen("sinetab.mem","wt"))==NULL) { printf("Failed to open output file.\n"); _fcloseall(); } else { /* write header */ // fprintf(handle,"// Auto-generated by memint 09/19/2019 16:41:21\n"); // fprintf(handle,"#Format=Hex\n"); // fprintf(handle,"#Depth=512\n"); // fprintf(handle,"#Width=12\n"); // fprintf(handle,"#AddrRadix=3\n"); // fprintf(handle,"#DataRadix=3\n"); // fprintf(handle,"#Data\n"); /* write table to file */ for (i=0;i<1024;i++) { fprintf(handle,"%03X\n",sineTable[i]); } /* close output file */ fclose(handle); printf("Done.\n"); } }
Note that the values are unsigned binary in a form suitable for throwing directly at the DAC. Later I'll probably move to a better representation for doing arithmetic with.
I now need to think what I go on and do with my new-found capability to generate and manipulate sine waves.
If you found this interesting and would like to see more blogs I've written, a list can be found here: jc2048 Blog Index
[1] http://www.latticesemi.com/en/Products/DevelopmentBoardsAndKits/LatticeXP2Brevia2DevelopmentKit.aspx |
Top Comments