How to create a IP-core driver#
This tutorial creates a software driver for the AXI Test IP. Only a part of the functionality of AXI Test IP is implemented here, i.e., the multiplication of two integer values on the PL. The driver takes two integer values as input, writes them to the PL, and reads the result. The implementation is done in a way that implements the driver and the Unit tests (Ceedling) in parallel.
Note
This example is purely for understanding how to write the driver. Using the PL for simple integer multiplication is much slower than just doing it on the PS.
Tip
Sometimes, Ceedling fails to build, and unclear errors are present. Try to use ceedling clean
to delete temporary files of Ceedling or commenting out the last test, running ceedling clean`, running all tests again, and comment in the last test again. If this does not work, run ceedling clobber
to delete more temporary files and run the tests again.
Video#
Setup#
Open VS Code with VS Code Remote Container
Open a new terminal (
Terminal -> New Terminal
)Change the directory of the terminal by typing:
cd vitis/software/Baremetal
Create a new software module
uz_myIP_hw
and specify the pathIP_Cores/uz_myIP/
:
ceedling module:create[IP_Cores/uz_myIP/uz_myIP_hw]
Ceedling creates the files:
uz_myIP_hw.c
invitis/software/Baremetal/src/IP_Cores/uz_myIP/
uz_myIP_hw.h
invitis/software/Baremetal/src/IP_Cores/uz_myIP/
test_uz_myIP_hw.c
invitis/software/Baremetal/test/IP_Cores/uz_myIP/
Create the file
uz_myIP_hwAddresses.h
invitis/software/Baremetal/src/IP_Cores/uz_myIP/
and add an include guard to it (#pragma once
in first line)Open
uz_axi_testIP_addr.h
inultrazohm_sw/ip_cores/AXI_testIP/uz_axi_testIP/ipcore/uz_axi_testIP_v1_0/include
Copy all defines in this file
Paste the defines of
uz_axi_testIP_addr.h
intouz_myIP_hwAddresses.h
Open
test_uz_myIP_hw.c
and add:
#include "test_assert_with_exception.h"
#include "mock_uz_AXI.h" // Tells Ceedling to create mock versions of the functions in uz_AXI (e.g., _Expect)
#include "uz_myIP_hwAddresses.h"
#define TEST_BASE_ADDRESS 0x00000000F // random hex value that represents a fictional base address
Rename the existing test
test_uz_myIP_hw_NeedToImplement
totest_uz_myIP_hw_write_to_A
Run the tests by typing
ceedling test:all
in the terminalThe tests pass, but a message a ignore message is printed
--------------------
IGNORED TEST SUMMARY
--------------------
[test_uz_myIP_hw.c]
Test: test_uz_myIP_hw_write_to_A
At line (21): "Need to Implement uz_myIP_hw"
This means that the test builds, but no test is implemented for the new IP-Core.
IP-Core driver: Hardware registers#
To multiply two variables \(C=A \cdot B\) of type int32_t
, the driver has to write \(A\) and \(B\) from the PS to the PL by AXI in the correct registers and read back the result \(C\) from the PL to the PS.
In this section, we write a software module (uz_myIP_hw
) that only deals with writing and reading the hardware registers of the IP-Core such that software modules can use the module without having to know the hardware addresses.
See AXI Test IP for the available hardware registers of the IP-Core.
The addresses of the registers are listed in uz_myIP_hwAddresses.h
.
Read/write operations on AXI are done by using the Hardware Abstraction Layer.
Therefore, we expect that the driver first has to call the function uz_axi_write_int32
with the register address of \(A\) and an integer as arguments.
Write the test for this behavior:
Delete the line
TEST_IGNORE_MESSAGE("Need to Implement uz_myIP_hw");
and add a first test intest_uz_myIP_hw.c
.
1void test_uz_myIP_hw_write_to_A(void)
2{
3 int a=-10;
4 // Test passes if uz_axi_write_int32 is called once with these arguments
5 uz_axi_write_int32_Expect(TEST_BASE_ADDRESS+A_int32_Data_uz_axi_testIP,a);
6 uz_myIP_hw_write_A(TEST_BASE_ADDRESS,a);
7}
Run the tests (type
ceedling test:all
in terminal)Tests fail with a warning that
uz_myIP_hw_write_to_A
has an implicit declarationDeclare the required functions to read and write from the IP-core in
uz_myIP_hw.h
1#ifndef UZ_MYIP_HW_H
2#define UZ_MYIP_HW_H
3#include <stdint.h>
4void uz_myIP_hw_write_A(uint32_t base_address,int32_t A);
5void uz_myIP_hw_write_B(uint32_t base_address,int32_t B);
6int32_t uz_myIP_hw_read_C(uint32_t base_address);
7#endif // UZ_MYIP_HW_H
Run the tests. They will fail due to undefined references to
uz_myIP_hw_write_A
Implement the write function in
uz_myIP_hw.c
1#include "uz_myIP_hw.h"
2#include "uz_myIP_hwAddresses.h"
3#include "../../uz/uz_AXI.h"
4
5void uz_myIP_hw_write_A(uint32_t base_address,int32_t A){
6 uz_axi_write_int32(base_address+A_int32_Data_uz_axi_testIP,A);
7}
Run the tests. They will pass
Currently, we only test the good case in which everything works as expected. However, we need to protect against some basic errors.
Add a test that protects against calling the write function without a valid base address:
1void test_uz_myIP_hw_write_to_A_with_zero_base_address(void)
2{
3 int a=-10;
4 // Tell the test that we do not care how often this function is called
5 uz_axi_write_int32_Ignore();
6 // Test passes if an assert fails in the function under test
7 TEST_ASSERT_FAIL_ASSERT(uz_myIP_hw_write_A(0,a))
8}
Run the tests, they fail with the following message because we expected that an Assertions fires in
uz_myIP_hw_write_A
to prevent calling the function with base address0
:
FAILED TEST SUMMARY
-------------------
[test_uz_myIP_hw.c]
Test: test_uz_myIP_hw_write_to_A_with_zero_base_address
At line (31): "Code under test did not assert"
Add the following to
uz_myIP_hw.c
1#include "uz_myIP_hw.h"
2#include "uz_myIP_hwAddresses.h"
3#include "../../uz/uz_AXI.h"
4#include "../../uz/uz_HAL.h"
5
6void uz_myIP_hw_write_A(uint32_t base_address,int32_t A){
7 uz_assert_not_zero(base_address);
8 uz_axi_write_int32(base_address+A_int32_Data_uz_axi_testIP,A);
9}
Run the tests. They pass. Note that this assertion only prevents calling the function with
base_address == 0
, e.g., if a struct initializer automatically initialized the variable. The function still can be called with a wrong base address!We can now write \(A\) to the IP-Core and have a test that ensures that we write to the correct addresses. Next step: do the same for \(B\):
Warning
It is tempting to copy & paste everything here - be careful to get all addresses, functions, and variable names right!
Write a test that checks that
uz_myIP_hw_write_B
writes to the correct address and a test that prevents calls withbase_address == 0
:
1void test_uz_myIP_hw_write_to_B(void)
2{
3 int b=100;
4 uz_axi_write_int32_Expect(TEST_BASE_ADDRESS+B_int32_Data_uz_axi_testIP,b);
5 uz_myIP_hw_write_B(TEST_BASE_ADDRESS,b);
6}
Run the test. It does not compile since
uz_myIP_hw_write_B
is not implemented. Add the implementation inuz_myIP_hw.c
:
1void uz_myIP_hw_write_B(uint32_t base_address,int32_t B){
2uz_assert_not_zero(base_address);
3uz_axi_write_int32(base_address+B_int32_Data_uz_axi_testIP,B);
4}
Run the test. It passes. We already implemented the assert for the base address in this case. Make sure to add the test for this:
1void test_uz_myIP_hw_write_to_B_with_zero_base_address(void)
2{
3 int b=2;
4 uz_axi_write_int32_Ignore();
5 TEST_ASSERT_FAIL_ASSERT(uz_myIP_hw_write_B(0,b))
6}
Run the test, it passes.
To get the result of the multiplication, read the register C. Create a test for this.
uz_axi_read_int32_ExpectAndReturn
creates a mock for the functionuz_axi_read_int32
that returnsc
if it is called. Furthermore, we test that the right value is returned formuz_myIP_hw_read_C
:
1void test_uz_myIP_hw_read_from_C(void)
2{
3 int c=101230;
4 uz_axi_read_int32_ExpectAndReturn(TEST_BASE_ADDRESS+C_int32_Data_uz_axi_testIP,c);
5 int c_readback=uz_myIP_hw_read_C(TEST_BASE_ADDRESS);
6 TEST_ASSERT_EQUAL_INT(c,c_readback);
7}
Run the test, this does not compile since there is no implementation of
uz_myIP_hw_read_C
. Add it touz_myIP_hw.c
:
1int32_t uz_myIP_hw_read_C(uint32_t base_address){
2
3}
Run the test. The test fails since
uz_myIP_hw_read_C
did not return the right value.Implement a real version of
uz_myIP_hw_read_C
:
1int32_t uz_myIP_hw_read_C(uint32_t base_address){
2return (uz_axi_read_int32(base_address+C_int32_Data_uz_axi_testIP));
3}
Run the tests. They will pass now.
Add a test for the missing assert:
1void test_uz_myIP_hw_read_C_with_zero_base_address(void)
2{
3 int c=123;
4 // Ignores how often the read function is called and returns (c)
5 uz_axi_read_int32_IgnoreAndReturn(c);
6 TEST_ASSERT_FAIL_ASSERT(uz_myIP_hw_read_C(0));
7}
Run the test, the test fails with
Code under test did not assert
Add
1int32_t uz_myIP_hw_read_C(uint32_t base_address){
2uz_assert_not_zero(base_address);
3return (uz_axi_read_int32(base_address+C_int32_Data_uz_axi_testIP));
4}
Run the tests. All tests will pass!
IP-Core driver: User software#
Recall that we use the AXI Test IP to calculate \(C=A \cdot B\). Until now, we created an abstraction layer for the hardware registers. We implement the actual function of the driver in the following.
Type in the terminal:
ceedling module:create[IP_Cores/uz_myIP/uz_myIP]
Create the interface of the IP-Core driver in
uz_myIP.h
. Notice how the interface is focused on the user: We only have to initialize the module and use the hardware calculation \(C=A \cdot B\) without knowledge about hardware registers and addresses. We use Doxygen to document the interface. Type/**
above a function, struct or typedef you want to comment and press enter, VSCode will auto-generate the doxygen boiler plate. We only use doxygen comments for the interface (in the.h
file) and later include these in the sphinx documentation.
1#ifndef UZ_MYIP_H
2#define UZ_MYIP_H
3#include <stdint.h>
4
5/**
6 * @brief Data type for object myIP
7 *
8 */
9typedef struct uz_myIP_t uz_myIP_t;
10
11/**
12 * @brief Configuration struct for myIP
13 *
14 */
15struct uz_myIP_config_t{
16 uint32_t base_address; /**< Base address of the IP-Core */
17 uint32_t ip_clk_frequency_Hz; /**< Clock frequency of the IP-Core */
18};
19
20/**
21 * @brief Initializes an instance of the myIP driver
22 *
23 * @param config Configuration values for the IP-Core
24 * @return Pointer to initialized instance
25 */
26uz_myIP_t* uz_myIP_init(struct uz_myIP_config_t config);
27
28/**
29 * @brief Calculates C=A*B
30 *
31 * @param self Pointer to IP-Core instance that was initialized with init function
32 * @param A First factor
33 * @param B Second factor
34 * @return Product of A times B
35 */
36int32_t uz_myIP_multiply(uz_myIP_t* self, int32_t A, int32_t B);
37
38#endif // UZ_MYIP_H
Run Ceedling, the tests will pass but the test for
uz_myIP
is ignored.Open the file
uz_myIP.c
insrc/IP_Cores/uz_myIP/
.Use the allocation VSCode snippet for the static memory allocation boiler plate code (see Static memory allocation for details). If you use VS Code Remote Container, you can use the snippet by typing
allocator
in the file. Alternatively copy the following code.
1#include "../../uz/uz_global_configuration.h"
2#if UZ_MYIP_MAX_INSTANCES > 0U
3#include <stdbool.h>
4#include "../../uz/uz_HAL.h"
5#include "uz_myIP.h"
6
7struct uz_myIP_t {
8 bool is_ready;
9};
10
11static uint32_t instance_counter = 0U;
12static uz_myIP_t instances[UZ_MYIP_MAX_INSTANCES] = { 0 };
13
14static uz_myIP_t* uz_myIP_allocation(void);
15
16static uz_myIP_t* uz_myIP_allocation(void){
17 uz_assert(instance_counter < UZ_MYIP_MAX_INSTANCES);
18 uz_myIP_t* self = &instances[instance_counter];
19 uz_assert_false(self->is_ready);
20 instance_counter++;
21 self->is_ready = true;
22 return (self);
23}
24
25uz_myIP_t* uz_myIP_init() {
26 uz_myIP_t* self = uz_myIP_allocation();
27 return (self);
28}
29#endif
Open
uz_global_configuration.h
if you already renamed the sample configuration. If not, see Global configuration.Add
#define UZ_MYIP_MAX_INSTANCES 5U
touz_global_configuration.h
inside the test ifdef (at the bottom of the file). We can now use up to 5 instances of the IP-core driver for five different instances of the IP-Core in the tests.Add the following code to
test_uz_myIP.c
. We isolate the testing by using a mock version of our already implementeduz_myIP_hw
.
1#include "test_assert_with_exception.h"
2#include "uz_myIP.h"
3#include "mock_uz_myIP_hw.h" // Mock the _hw functions to isolate testing
4#include <stdint.h>
5
6#define TEST_BASE_ADDRESS 0x0000000A
7#define TEST_IP_CORE_FRQ 100000000U
Change the implementation of
uz_myIP_init
inuz_myIP.c
to match the interface inuz_myIP.h
1uz_myIP_t* uz_myIP_init(struct uz_myIP_config_t config){
2 uz_myIP_t* self = uz_myIP_allocation();
3 return (self);
4}
Run the tests, all tests pass, but
uz_myIP_test
is ignored.Start writing a test for the multiplication \(C=A \cdot B\) by initializing an instance of the IP-Core driver:
1void test_uz_myIP_test_A_times_B_equals_C(void)
2{
3 struct uz_myIP_config_t config={
4 .base_address= TEST_BASE_ADDRESS,
5 .ip_clk_frequency_Hz=TEST_IP_CORE_FRQ
6 };
7 uz_myIP_t *instance = uz_myIP_init(config);
8}
Run the tests, they will pass but a warning about unused variables
config
andinstance
is shown.Add to the test:
1 void test_uz_myIP_test_A_times_B_equals_C(void)
2 {
3 struct uz_myIP_config_t config={
4 .base_address= TEST_BASE_ADDRESS,
5 .ip_clk_frequency_Hz=TEST_IP_CORE_FRQ
6 };
7 uz_myIP_t *instance = uz_myIP_init(config);
8 int32_t a = -10;
9 int32_t b = 200;
10 uz_myIP_hw_write_A_Expect(TEST_BASE_ADDRESS, a);
11 uz_myIP_hw_write_B_Expect(TEST_BASE_ADDRESS, b);
12 uz_myIP_hw_read_C_ExpectAndReturn(TEST_BASE_ADDRESS, a * b);
13 int32_t c = uz_myIP_multiply(instance, a, b);
14 TEST_ASSERT_EQUAL_INT32(a * b, c);
15 }
Run the tests, we have a linker error since
uz_myIP_multiply
is not implemented yet.Add
#include "uz_myIP_hw.h"
touz_myIP.c
and implement the calls to the hardware registers.
1int32_t uz_myIP_multiply(uz_myIP_t* self, int32_t A, int32_t B){
2uz_assert(self->is_ready);
3uz_myIP_hw_write_A(self->config.base_address,A);
4uz_myIP_hw_write_B(self->config.base_address,B);
5return (uz_myIP_hw_read_C(self->config.base_address));
6}
Run the tests, we have several errors since we have no struct member
config
. Add config to the structuz_myIP_t
:
1struct uz_myIP_t {
2 bool is_ready;
3 struct uz_myIP_config_t config;
4};
Run the tests, they fail since
uz_myIP_hw_write_A
is not called with the correct base address.Assign the passed config value to the instance in
uz_myIP_init()
:
1uz_myIP_t* uz_myIP_init(struct uz_myIP_config_t config){
2 uz_myIP_t* self = uz_myIP_allocation();
3 self->config=config;
4 return (self);
5}
Run the tests, they pass!
Add a test to prevent calling init without initialization of the base address:
1void test_uz_myIP_fail_assert_if_base_address_is_zero(void)
2{
3 struct uz_myIP_config_t config={
4 .ip_clk_frequency_Hz=TEST_IP_CORE_FRQ
5 };
6 TEST_ASSERT_FAIL_ASSERT(uz_myIP_init(config) );
7}
Test fails, add
uz_assert_not_zero(config.base_address);
touz_myIP_init
before the allocation is done.Run the test again, it passes now.
Repeat for
ip_clk_frequency_Hz
. Adduz_assert_not_zero(config.ip_clk_frequency_Hz);
touz_myIP_init
and the following test:
1void test_uz_myIP_fail_assert_if_ip_frequency_is_zero(void)
2{
3 struct uz_myIP_config_t config={
4 .base_address=TEST_BASE_ADDRESS
5 };
6 TEST_ASSERT_FAIL_ASSERT(uz_myIP_init(config) );
7}
Add a test to prevent calling
uz_myIP_multiply()
with a NULL pointer
void test_uz_myIP_fail_assert_if_multiply_is_called_with_NULL_pointer(void)
{
TEST_ASSERT_FAIL_ASSERT(uz_myIP_multiply(NULL, 5, 1));
}
Add an assertion to prevent calls with NULL pointer:
int32_t uz_myIP_multiply(uz_myIP_t* self, int32_t A, int32_t B){
uz_assert_not_NULL(self);
uz_assert(self->is_ready);
uz_myIP_hw_write_A(self->config.base_address,A);
uz_myIP_hw_write_B(self->config.base_address,B);
return (uz_myIP_hw_read_C(self->config.base_address));
}
We now have a working and fully tested driver for our IP-Core!
Warning
While we tested our functions with different error cases and made sure they behave as expected, we omitted the fact that the multiplication can overflow. This is especially tricky in this case since the multiplication is implemented in hardware. Thus the rules for C do not apply to it. There are two ways to handle this: implement the hardware multiplication to saturate on overflow or check if the multiplication will overflow before writing to the PL. The way AXI Test IP is implemented will wrap on overflow, i.e., 2147483647*2 will be a negative value. Keep this concept in mind for real IP-Cores that you implement. Additionally, prevent the software driver from writing values that are out of range to the IP-Core, e.g., if the register only uses 10 bit. Note that the AXI data width is always 32-bit.
Integration in Vitis#
Open Vitis
(if not already done) Run the tcl script to generate the workspace (Tcl Scripts)
Navigate to the Baremetal code
Create the file
uz_myIP_testebench.h
in theinclude
folder
1#pragma once
2
3void uz_myIP_testbench(void);
Create the file
uz_myIP_testebench.c
in thesw
folder. Note how the code is basically the same as the testtest_uz_myIP2_test_A_times_B_equals_C
without the assertions and ceedling function calls.
1#include "../include/uz_myIP_testbench.h"
2#include "../uz/uz_HAL.h"
3#include "../IP_Cores/uz_myIP/uz_myIP.h"
4#include "xparameters.h"
5
6void uz_myIP_testbench(void){
7 struct uz_myIP_config_t config={
8 .base_address= XPAR_UZ_AXI_TESTIP_0_BASEADDR,
9 .ip_clk_frequency_Hz=100000000U
10 };
11 uz_myIP_t *instance = uz_myIP_init(config);
12 int32_t a = -10;
13 int32_t b = 200;
14 int32_t c = uz_myIP_multiply(instance, a, b);
15 uz_printf("Hardware multiply: %i, Software multiply: %i\n", c, a*b);
16 if (c==a*b){
17 uz_printf("Success: hardware and software multiply are equal! \n");
18 }else{
19 uz_printf("Fail: hardware and software multiply are NOT equal! \n");
20 }
21
22 while(1){
23 // do nothing and loop forever
24 }
25}
Add
#define UZ_MYIP_MAX_INSTANCES 1U
betweenifndef TEST
and the first#endif
to use one instance of the module in the software.Build the software.
Include
#include "include/uz_myIP_testbench.h"
inmain.c
(Baremetal R5) and calluz_myIP_testbench();
before the ISR is initialized!Connected the serial port to the Vitis Serial Terminal
Run the UltraZohm. The success message should be printed to the Vitis Serial Terminal.