FPGA: Making Waves




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.

In the last blog I described a simple circuit using a low-cost 12-bit SPI DAC from Microchip, an MCP4821.

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;
 wave_test_stuff: process (clk_in)
   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';
     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;
     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");
 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++) {
  /* close output file */



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



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