Store 1024 12 bit values in a VHDL RAM design, and automatically test it.
This post uses the VHDL RAM design of Michael Kellett on a Zynq. You can store data and read it back.
The design was done out of curiousity. To see how and when Block RAM slices are used. Check the discussions in the comments of MKs post.
Here, I just focus on how the exercise is done with Vivado, Zynq and Pynq.
If this blog proves anything, it's that HDL is very portable.
VHDL Memory component
VHDL source of the memory design is exactly the same as Michael's, except that his block holds 64 12 bit values, mine 1024.
That's why I changed:
- the name from mem_inf_sp_64_12 to mem_inf_sp_1024_12
- the address from STD_LOGIC_VECTOR(5 downto 0) to STD_LOGIC_VECTOR(9 downto 0). It has to fit the 1024 values instead of 64
- the array of data from array (0 to 63) to array (0 to 1023)
Latest source on github: https://gist.github.com/jancumps/72f319d33200af69d823701005e9d9cb
ZYNQ integration between FPGA and ARM / Linux
These signals will come from the ARM / Linux side
- we: write enable. I use a GPIO for that
- en: enable. Also a GPIO
- address: address to read from or write to the memory. 10 MSB of a AXI memory mapped register
- data: data to write. 12 LSB of that same AXI memory mapped register.
- the clock
When a read is requested, the FPGA will send this back to ARM / Linux:
- data: the value present at address when a read is requested. 12 LSB of that same memory mapped register.
Block Design
I've kept it as simple as I could. Just enough to provide the interfaces described in the previous section.
I'm going to skip explaining the mechanics but will show the configs. Every technique has been used before in this blog series.
click to enlarge
The green block is Michael's Memory module. In the block design, I linked all inputs and outputs to the ARM / Linux interfaces.
For the technology used for each signal between ARM and fabric, see the section above.
Configuration
ARM
Zynq AXI for Data and Address register
Zynq GPIO for Enable and Write Enable
Data and address (AXI)
AXI Interconnect
AXI GPIO
Address slice
Isolates the 10 Address bits out of a 32 bit AXI memory mapped register
Write Data slice
Isolates the 12 Data bits out of a 32 bit AXI memory mapped register
Read Data left-padded with 0
Concatenate (left pad) 10 '0' bits to the 12 data bits data, to fill all bits of the 22 bit wide AXI register
The constant 10 '0' bits
Enable and Write Enable (GPIO)
Enable slice
Isolates the Enable bit out of the 64 bit GPIO interface
Write Enable slice
Isolates the Write Enable bit out of the 64 bit GPIO interface
All of these blocks tie to the VHDL memory block. You'll see that the bit width and directions match.
Test Bed
I've tested the code in Python and Pynq.
The test loads a value to each memory location, and then automatically validates integrity.
Load bitstream
from pynq import Overlay overlay = Overlay("memory_ram.bit")
Generate the GPIO API for Enable and Write Enable
# write enable and enable are GPIO pins from pynq import GPIO we = GPIO(GPIO.get_gpio_pin(0), 'out') en = GPIO(GPIO.get_gpio_pin(1), 'out') def disable(): en.write(0) we.write(0) return def write_enable(): en.write(1) we.write(1) return def read_enable(): en.write(1) we.write(0) return
Generate the AXI API for Address and Data
from pynq import MMIO memory_address = overlay.ip_dict['axi_gpio_memory_ram']['phys_addr'] RANGE = 4 memory_register = MMIO(memory_address, RANGE) def memory_write(address, data): # must disable until register is set disable() # mask data for 12 bits maximum memory_register.write(0,address << 12 | (data & 0X0FFF)) write_enable() disable() def memory_read(address): # must disable until register is set disable() memory_register.write(0,address << 12) read_enable() data = memory_register.read() disable() return data
Execute unit tests
This section shows how you can fully automate the validation of a design.
This is useful while developing the FPGA blocks, but also as regression test when you change it.
Every address is filled with a value that can be checked later on.
For the tests, you can choose to test every address, or to spot check every so much positions. I'm testing every 128th address position.
The border conditions, the first and last memory address, are always tested.
address_count = 1024 test_granularity = 128 test_errors = 0 def change_data(data): return 2 * data def test_data(address, data): success = (change_data(address) == data) if(success): print(f'memory at address {address:04}: OK') return True else: print(f'memory at address {address:04}: ERROR') return False disable() # fill all addresses with a different value print(f'fill all {address_count} addresses with a different value') for i in range(0, address_count): memory_write(i, change_data(i)) print(f'validate every {test_granularity} address') print('') print('visual checks:') print('==============') # read a subset back print('first read') for i in range(0, address_count, test_granularity): print(f'memory at address {i:04}: {memory_read(i):04}') print('second read to confirm read isn\'t destructive') # check if no bogus writes for i in range(0, address_count, test_granularity): print(f'memory at address {i:04}: {memory_read(i):04}') print('') print('unit test') print('=========') # check if data matches expectation for i in range(0, address_count, test_granularity): if not (test_data(i,memory_read(i))): test_errors = test_errors + 1 print('test border cases first and last address always') if not (test_data(0,memory_read(0))): test_errors = test_errors + 1 if not (test_data(address_count-1,memory_read(address_count-1))): test_errors = test_errors + 1 if test_errors > 0: print(f'test failed {test_errors} times') else: print('all tests passed')
The test report:
fill all 1024 addresses with a different value
validate every 128 address
visual checks:
==============
first read
memory at address 0000: 0000
memory at address 0128: 0256
memory at address 0256: 0512
memory at address 0384: 0768
memory at address 0512: 1024
memory at address 0640: 1280
memory at address 0768: 1536
memory at address 0896: 1792
second read to confirm read isn't destructive
memory at address 0000: 0000
memory at address 0128: 0256
memory at address 0256: 0512
memory at address 0384: 0768
memory at address 0512: 1024
memory at address 0640: 1280
memory at address 0768: 1536
memory at address 0896: 1792
unit test
=========
memory at address 0000: OK
memory at address 0128: OK
memory at address 0256: OK
memory at address 0384: OK
memory at address 0512: OK
memory at address 0640: OK
memory at address 0768: OK
memory at address 0896: OK
test border cases first and last address always
memory at address 0000: OK
memory at address 1023: OK
all tests passed
The Vivado 2020.1 project and Jupyter code are attached.