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.

 

Pynq - Zync - Vivado series
Add Pynq-Z2 board to Vivado
Learning Xilinx Zynq: port a Spartan 6 PWM example to Pynq
Learning Xilinx Zynq: use AXI with a VHDL example in Pynq
VHDL PWM generator with dead time: the design
Learning Xilinx Zynq: use AXI and MMIO with a VHDL example in Pynq
Learning Xilinx Zynq: port Rotary Decoder from Spartan 6 to Vivado and PYNQ
Learning Xilinx Zynq: FPGA based PWM generator with scroll wheel control
Learning Xilinx Zynq: use RAM design for Altera Cyclone on Vivado and PYNQ
Learning Xilinx Zynq: a Quadrature Oscillator - 2 implementations
Learning Xilinx Zynq: a Quadrature Oscillator - variable frequency
Learning Xilinx Zynq: Hardware Accelerated Software
Automate Repeatable Steps in Vivado
Learning Xilinx Zynq: Try to make my own Accelerated OpenCV Function - 1: Vitis HLS
Learning Xilinx Zynq: Try to make my own Accelerated OpenCV Function - 2: Vivado Block Design
Learning Xilinx Zynq: Logic Gates in Vivado
Learning Xilinx Zynq: Interrupt ARM from FPGA fabric
Learning Xilinx Zynq: reuse and combine components to build a multiplexer