Driver

In Chapter 2 - Embedded software some basics on the embedded software has been given. A bit more general information is explained using the communicator as example.

Hardware mapping

As chip designer for the communicator, you are the one that knows how you wired up the Memory-Mapped IO. In this example the mapping below is assumed.

MMIO map

The image above shows the use of 4 registers. Slv_reg0 and slv_reg1 are outgoing registers while slv_reg2 and slv_reg3 are incoming registers. The data_out signal is mapped directly to slv_reg2 and, similarly, the data_in signal is driven by slv_reg0. Connecting up the data-portion is straightforward.

The control-portion of the design sometimes requires a bit more attention. Typically this is done through a command register (CR) (from the processor towards the hardware) and a status register (SR) (from the hardware to the processor). As the design is becoming more complex, a single 32-bit vector could not fit all the relevant signals and more registers can be required. In this example slv_reg1 acts as CR while slv_reg3 act as a SR.

According to the requirements, the I2C_slave component signals valid output data through a short pulse on the data_out_valid signals. As it is very likely that the software will miss this pulse, the example adds a set-reset-flipflop. This way there is an acknowledgement of the fact there is data present.

Image created with wavedrom.com

The result of the SRFF is always stored in slv_reg3.

Driver

Memory map

With the hardware design fixed, it is necessary to create a driver This is small piece of software that provides a certain API to the user. Because the user does not know the hardware mapping is done, this driver has to be provided by the supplier of the hardware, also known as: YOU.

While creating the blockdesign, there is Address Editor in anther Vivado tab.

Address Editor in Vivado

When moving to SDK for the software development, the complete memory map of the processor can be consulted. This shows the segmentation of the entire memory space. As can be seen in the example, both the instantiated IP components that you designed are taken up in this map.

As elaborated on in 2.2 (embedded software), the registers that are used in our approach are mapped to the start of the allocated memory space. It is important to remember that every slv_reg occupies 4 memory addresses.

You might have noticed, however, that mapping of a certain component in the entire memory map is not always identical. Sometimes, a certain component has another start address. This annoying fact has to be solved by driver, next to providing the API. Luckily the xparameters.h file is generated by the design tools and a define is made. The name of this define is fixed and this you can rely on to write the driver.

Address map in SDK (as found in the .hdf file)

Installing some defines

In this example these defines are present in the xparameters.h.

...
/* Definitions for peripheral COMMUNICATOR_V1_0_0 */
#define XPAR_COMMUNICATOR_V1_0_0_BASEADDR 0x43C10000
#define XPAR_COMMUNICATOR_V1_0_0_HIGHADDR 0x43C1FFFF
...

To make the our lives easier we can extend these defines in our own code.

#define XMASCOMM_BASEADDRESS      XPAR_COMMUNICATOR_V1_0_0_BASEADDR
#define XMASCOMM_REG0_ADDRESS     (XMASCOMM_BASEADDRESS + 0*4)
#define XMASCOMM_REG1_ADDRESS     (XMASCOMM_BASEADDRESS + 1*4)
#define XMASCOMM_REG2_ADDRESS     (XMASCOMM_BASEADDRESS + 2*4)
#define XMASCOMM_REG3_ADDRESS     (XMASCOMM_BASEADDRESS + 3*4)

With these defines installed, the software developer does not have to bother himself/herself with these increments of four (as long as the processor is a 32-bitter). A typical thing you see in drivers is also the mapping of certain bits. Applying this to the example could look like:

#define XMASCOMM_SR_RXAVAILABLE   0x00000001U
#define XMASCOMM_CR_TXSEND        0x00000001U
#define XMASCOMM_CR_RXCONFIRM     0x00000002U

In Chapter 2 the functions Xil_In32() and Xil_Out32() are explained. However, with a tiny bit of C-magic, the code can become more readable.

#define XMASCOMM_FPGA2EXT         (*(volatile u32 *) XMASCOMM_REG0_ADDRESS)
#define XMASCOMM_CR               (*(volatile u32 *) XMASCOMM_REG1_ADDRESS)
#define XMASCOMM_EXT2FPGA         (*(volatile u32 *) XMASCOMM_REG2_ADDRESS)
#define XMASCOMM_SR               (*(volatile u32 *) XMASCOMM_REG3_ADDRESS)

Let’s quickly break this down for those whose C-skills are a bit rusty. The define XMASCOMM_SR make sure that, everywhere in the code this define is substituted by (*(volatile u32 *) XMASCOMM_REG0_ADDRESS). XMASCOMM_REG0_ADDRESS contains the address of address 0. This value is type-cast to an unsigned 32-bit pointer. The keyword volatile states that the content of a variable can also be altered from another source. This is important !! Otherwise the optimisation of the C-compiler might optimise-out certain lines of C-code.

The attentive reader could now spot that (volatile u32 *) XMASCOMM_REG0_ADDRESS is explained. However, in the define is another (* … ) encapsulating all this. This is used to dereference the pointer that was created.

A TL;DR version: with these defines you can write x = XMASCOMM_SR;, or XMASCOMM_CR = 12;.

Earlier the following C-code was shown: Xil_Out32(XMAS_LIGHT_CR, 0x00000001); Note that this lines sets the LSB of the CR to one, but it also reset all other 31 bits to 0 !!

A better way of setting the LSB of the CR would be: XMASCOMM_CR |= 0x1; Using the full power of the defines that were made, results in the more general: XMASCOMM_CR |= XMASCOMM_CR_TXSEND;

Starting the driver

Let us assume the following two C-files:

/*
 * xmascomm_driver.h
 */

#include "xparameters.h"
#include "xil_io.h"

#define XMASCOMM_BASEADDRESS      XPAR_COMMUNICATOR_V1_0_0_BASEADDR
#define XMASCOMM_REG0_ADDRESS     (XMASCOMM_BASEADDRESS + 0*4)
#define XMASCOMM_REG1_ADDRESS     (XMASCOMM_BASEADDRESS + 1*4)
#define XMASCOMM_REG2_ADDRESS     (XMASCOMM_BASEADDRESS + 2*4)
#define XMASCOMM_REG3_ADDRESS     (XMASCOMM_BASEADDRESS + 3*4)
#define XMASCOMM_REG4_ADDRESS     (XMASCOMM_BASEADDRESS + 4*4)

#define XMASCOMM_FPGA2EXT         (*(volatile u32 *) XMASCOMM_REG0_ADDRESS)
#define XMASCOMM_CR               (*(volatile u32 *) XMASCOMM_REG1_ADDRESS)
#define XMASCOMM_EXT2FPGA         (*(volatile u32 *) XMASCOMM_REG2_ADDRESS)
#define XMASCOMM_SR               (*(volatile u32 *) XMASCOMM_REG3_ADDRESS)
#define XMASCOMM_MSGCOUNTER       (*(volatile u32 *) XMASCOMM_REG4_ADDRESS)

#define XMASCOMM_SR_RXAVAILABLE   0x00000001U
#define XMASCOMM_CR_TXSEND        0x00000001U
#define XMASCOMM_CR_RXCONFIRM     0x00000002U


void xmascomm_send_command(uint32_t data);
uint32_t xmascomm_wait_for_command(void);
uint32_t xmascomm_check_received(void);
uint32_t xmascomm_fetch_received(void);
void xmascomm_acknowledge_rx(void);


  
/*
 * xmascomm_driver.c
 */

#include "xmascomm_driver.h"

void xmascomm_send_command(uint32_t data) {
  XMASCOMM_FPGA2EXT = data;
  XMASCOMM_CR |= XMASCOMM_CR_TXSEND;
  XMASCOMM_CR &= ~(XMASCOMM_CR_TXSEND);
}

uint32_t xmascomm_wait_for_command(void) {
  uint32_t rx;

  while(! xmascomm_check_received());
  rx = xmascomm_fetch_received();
  xmascomm_acknowledge_rx();

  return rx;
}


uint32_t xmascomm_check_received(void) {
  return (XMASCOMM_SR & XMASCOMM_SR_RXAVAILABLE);
}

uint32_t xmascomm_fetch_received(void) {
  return (XMASCOMM_EXT2FPGA);
}

void xmascomm_acknowledge_rx(void) {
  XMASCOMM_CR |= XMASCOMM_CR_RXCONFIRM;
  XMASCOMM_CR &= ~(XMASCOMM_CR_RXCONFIRM);
}
  

Although this is not yet optimal, the code above gives some idea on how a driver is constructed. For the sake of completeness, it is mentioned that these C-files can also be include in the IP core. When you make the IP core available, it then includes the hardware and the accompanying driver.

The API provides the user of the IP core with some nice function calls. However, this API should be documented or the user will not know which nice function calls he/she can use. Don’t forget !!