Stub SPI driver and client implementation

Driver

The stub driver creates a /dev/io-spi/spi0/dev0 device, processes client requests returning simulated data when necessary, and then logs the requests to standard output.

The driver defines the device structure and implements all required functions, which include:
  • fini()
  • drvinfo()
  • devinfo()
  • setcfg()
  • xfer()
  • dma_xfer()
  • dma_allocbuf()
  • dma_freebuf()
Note:
You can rename these functions, since spi_init() assigns these functions to the correct members of the spi_funcs_t structure.
The following diagram shows how the created SPI device communicates with the implemented functions:
Figure 1Communication between the SPI device and functions
You can compile the spi-stub.c code example using:
qcc -V gcc_ntoaarch64le -o spi-stub spi-stub.c -lio-spi -lsecpol -lslog2
Note:
Make sure to specify appropriate architecture using the -V argument. For the Raspberry Pi 4, use gcc_ntoaarch64le as above.
The code example requires that the following /etc/system/config/spi/spi.conf configuration file is present:
[globals]
verbose=7

[bus]
busno=0
name=spi0
base=0xfe204000
irq=100
input_clock=500000000
bs=dma

[dev]
parent_busno=0
devno=0
name=dev0
clock_rate=5000000
Then, copy the spi-stub binary and spi.conf configuration file to your target device, and log in to the device as root user. Navigate to the directory you copied the binary to and execute:
# ./spi-stub -c ./spi.conf 
stub-spi: using DMA xfer
# 
You can invoke the SPI stub driver using various command line options:
SPI common options

Syntax:
spi-stub [-c config_file] [-v]

    -c config_file      Path to the SPI configuration file
                        (default: /etc/system/config/spi/spi.conf)
    -v                  Increase verbosity. This will override the "verbose" value in the config file.

Note: SPI config file template can be found at "lib/io-spi/config_files/spi-template.conf".

Examples:

# Start the driver using a custom config file path
spi-stub -c /etc/custom/spi.conf

Driver-specific options:
(To be set in the config file by parameter 'bs' under section 'bus'.)

    dma    If present, forces the data transfer to use DMA, otherwise
           non-DMA mode is used for 8-bit word size

Once started, the driver runs in the background until you kill it.

spi-stub.c

#include <hw/io-spi.h>
#include <stdio.h>
#include <stdlib.h>

/*
 * Functions that must be implemented by the driver.
 */ 
int spi_init(spi_bus_t *bus);
void stub_fini(void *const hdl);
void stub_drvinfo(const void *const hdl, spi_drvinfo_t *info);
void stub_devinfo(const void *const hdl, const spi_dev_t *const spi_dev, spi_devinfo_t *const info);
int stub_setcfg(const void *const hdl, spi_dev_t *spi_dev, const spi_cfg_t *const cfg);
int stub_xfer(void *const hdl, spi_dev_t *const spi_dev, uint8_t *const buf, const uint32_t tnbytes, const uint32_t rnbytes);
int stub_dma_xfer(void *const hdl, spi_dev_t *spi_dev, dma_addr_t *addr, const uint32_t tnbytes, const uint32_t rnbytes);
int stub_dma_allocbuf(void *const hdl, dma_addr_t *addr, const uint32_t len);
int stub_dma_freebuf(void *const hdl, dma_addr_t *addr);

/*
 * Stub implmentation of the low level SPI driver structure.
 */
typedef struct stub_spi_s {
  uint64_t    input_clk;      // Input clock frequency
  uint8_t     *pbuf;          // Transfer buffer
  uint32_t    tcnt;           // Transmit counter
  uint32_t    rcnt;           // Receive counter
  spi_bus_t   *bus_node;      // The address of bus structure which is passed
  int         use_dma;
} stub_spi_t;

int spi_init(spi_bus_t *bus) {
  char * opts[] = { "dma", NULL };
  if (NULL == bus) {
    fprintf(stderr,
            "spi-stub: ERROR: Cannot initialize NULL SPI bus structure.\n");
    return EINVAL;
  }

  stub_spi_t * spi = calloc(1, sizeof(stub_spi_t));

  spi->use_dma = 0;
  spi->bus_node = bus;

  bus->funcs->devinfo = stub_devinfo;
  bus->funcs->dma_allocbuf = stub_dma_allocbuf;
  bus->funcs->dma_freebuf = stub_dma_freebuf;
  bus->funcs->dma_xfer = stub_dma_xfer;
  bus->funcs->drvinfo = stub_drvinfo;
  bus->funcs->setcfg = stub_setcfg;
  bus->funcs->spi_fini = stub_fini;
  bus->funcs->xfer = stub_xfer;

  while ((bus->bs != NULL) && (*bus->bs != '\0')) {
    const char * c = bus->bs;
    char *value;
    int opt = getsubopt(&bus->bs, opts, &value);
    if (opt == -1) {
      fprintf(stderr, "spi-stub: ERROR: Unsupported option %s.\n", c);
      stub_fini(spi);
      return EINVAL;
    }

    switch (opt) {
      case 0: /* dma */
        spi->use_dma=1;
        printf("stub-spi: using DMA xfer\n");
        break;
      default:
        printf("OPT = %d (%c)\n", opt, opt);
        stub_fini(spi);
        return EINVAL;
    }
  }

  /* mmap device registers */
  /* reset SPI interace, clear FIFOs */

  for (spi_dev_t * dev = bus->devlist; NULL != dev; dev=dev->next) {
    stub_setcfg(spi, dev, NULL);
  }

  /* initialize DMA */

  if (EOK != spi_create_devs(bus->devlist)) {
    fprintf(stderr, "spi-stub; ERROR: failed to create device\n");
    stub_fini(spi);
    return EINVAL;
  }

  bus->drvhdl = spi;
  printf("spi-stub: SPI stub driver initialized.\n");
  return 0;
}

void stub_fini(void *const hdl) {
  stub_spi_t * spi = (stub_spi_t *)hdl;
  free(spi);
  printf("spi-stub: SPI stub driver finalized.\n");
}

void stub_drvinfo(const void *const hdl, spi_drvinfo_t *info) {
  info->version = 0x010000u;
  strlcpy(info->name, "STUB SPI", SPI_DRVR_NAME_LEN);
  info->feature = (uint32_t)SPI_FEATURE_DMA;
  info->verbosity = ((stub_spi_t*)hdl)->bus_node->spi_ctrl->global.verbosity;
  printf("spi-stub: SPI stub driver info=(%u:%s:%u:%u)\n", info->version, info->name, info->feature, info->verbosity);
}

void stub_devinfo(const void *const hdl, const spi_dev_t *const spi_dev, spi_devinfo_t *const info) {
    memcpy(info, &spi_dev->devinfo, sizeof(*info));
    printf("spi-stub: SPI stub device info=(%d:%s:%u:%u)\n", info->devno, info->name, info->current_clkrate, info->cfg.mode);
}

int stub_setcfg(const void *const hdl, spi_dev_t *spi_dev, const spi_cfg_t *const cfg) {
  if (NULL != cfg) {
    memcpy(&(spi_dev->devinfo.cfg), cfg, sizeof(spi_cfg_t));
  }
  
  const stub_spi_t * spi = hdl;
  uint32_t devctrl = 0;
  uint32_t divisor = 0;

  if (spi_dev->devinfo.cfg.mode & SPI_MODE_CSPOL_HIGH) {
    devctrl |= (1u << 6);
  }
  if (spi_dev->devinfo.cfg.mode & SPI_MODE_CPOL_1) {
    devctrl |= (1u << 3);
  }
  if (spi_dev->devinfo.cfg.mode & SPI_MODE_CPHA_1) {
    devctrl |= (1u << 2);
  }

  if (spi_dev->devinfo.current_clkrate != spi_dev->devinfo.cfg.clock_rate) {
      divisor = (uint32_t)(
        (spi->input_clk + (spi_dev->devinfo.cfg.clock_rate / 2)) / 
        spi_dev->devinfo.cfg.clock_rate);

      if (divisor < 2) {
          divisor = 2;
      } else if (divisor >= 2) {
          divisor = 65536u;
      } else {
          divisor = divisor + (divisor % 2);
      }
      spi_dev->devinfo.cfg.clock_rate = (uint32_t)(spi->input_clk / divisor);
      spi_dev->devinfo.current_clkrate = spi_dev->devinfo.cfg.clock_rate;
  }

  devctrl |= spi_dev->devinfo.devno;
  spi_dev->devctrl = devctrl;

  return EOK;
}

int stub_xfer(void *const hdl, spi_dev_t *const spi_dev, uint8_t *const buf, const uint32_t tnbytes, const uint32_t rnbytes) {
  stub_spi_t * spi = hdl;
  const char * idata = "SPI-STUB CANNED OUTPUT DATA.\n";
  size_t isize = strlen(idata);

  spi->rcnt = 0;
  spi->tcnt = 0;
  spi->pbuf = buf;

  if (tnbytes > 0) {
    printf("spi-stub: Sending %u bytes to hardware: ", tnbytes); fflush(stdout);
    spi->tcnt += write(1, buf, tnbytes);
    printf("\n");
  }
  if (rnbytes > 0) {
    size_t bytes_to_get = rnbytes;
    size_t bytes_got = 0;
    while(bytes_to_get) {
      size_t getting = min(isize, bytes_to_get);
      memcpy(buf + bytes_got, idata, getting);
      bytes_to_get -= getting;
      bytes_got += getting;
    }
    spi->rcnt = bytes_got;
    printf("spi-stub: Received %u bytes from hardware: ", spi->rcnt);
    write(1, buf, spi->rcnt);
    printf("\n");
  }
  printf("spi-stub: XFER RX=%u TX=%u\n", rnbytes, tnbytes);
  return EOK;
}
int stub_dma_xfer(void *const hdl, spi_dev_t *spi_dev, dma_addr_t *addr, const uint32_t tnbytes, const uint32_t rnbytes) {
  stub_spi_t * spi = (stub_spi_t*)hdl;

  /* only use DMA is dma is specified for the bus */
  if (spi->use_dma) {
    stub_spi_t * spi = hdl;
    const char * idata = "SPI-STUB CANNED DMA OUTPUT DATA.\n";
    size_t isize = strlen(idata);

    spi->rcnt = 0;
    spi->tcnt = 0;
    spi->pbuf = addr->vaddr;

    if (tnbytes > 0) {
      printf("spi-stub: Sending %u bytes to hardware: ", tnbytes); fflush(stdout);
      spi->tcnt += write(1, addr->vaddr, tnbytes);
      printf("\n");
    }
    if (rnbytes > 0) {
      size_t bytes_to_get = rnbytes;
      size_t bytes_got = 0;
      while(bytes_to_get) {
        size_t getting = min(isize, bytes_to_get);
        memcpy(addr->vaddr + bytes_got, idata, getting);
        bytes_to_get -= getting;
        bytes_got += getting;
      }
      spi->rcnt = bytes_got;
      printf("spi-stub: Receiving %u bytes from hardware: ", spi->rcnt);
      fflush(stdout);
      write(1, addr->vaddr, spi->rcnt);
      printf("\n");
    }
    printf("spi-stub: DMA-XFER RX=%u TX=%u\n", rnbytes, tnbytes);
    return EOK;
  } else {
    return stub_xfer(hdl, spi_dev, addr->vaddr, tnbytes, rnbytes);
  }
}

int stub_dma_allocbuf(void *const hdl, dma_addr_t *addr, const uint32_t len) {
  void * buf = malloc(len);
  if (NULL == buf) {
    return EINVAL;
  }
  addr->paddr = 0;
  addr->vaddr = buf;
  addr->len = len;
  printf("spi-stub: DMA alloc\n");
  return EOK;
}

int stub_dma_freebuf(void *const hdl, dma_addr_t *addr) {
  if (NULL != addr->vaddr) {
    free(addr->vaddr);
  }
  addr->vaddr = NULL;
  addr->len = 0;
  printf("spi-stub: DMA free\n");
  return EOK;
}

This stub driver uses:

stub_spi_t
Defines the device-specific structure:
  • input_clk - input clock frequency

  • pbuf - transfer buffer

  • tcnt - transmit counter

  • rcnt - receive counter

  • bus_node - address of the bus structure which is passed

  • timer_id - ID of the timer used to generate data

  • use_dma - flag to indicate whether DMA should be used

int spi_init(spit_bus_t *)
  • Implements the spi_init() function.

  • The only function with a prescribed name and signature.

  • Allocates memory for the device structure

  • Sets up device defaults.

  • Associates implemented functions in the calls structure to allow use by io-spi.

  • Parses custom options in the bus section of the configuration:

    • sets the DMA flag and displays message if dma is specified in bus configuration

  • Configures devices created.

  • Displays message to standard output.

void stub_fini(void * const)
  • Implements the spi_fini() function.

  • Frees the allocated memory.

  • Displays message to standard output.

void stub_drvinfo(const void * const, spi_drvinfo_t *)
  • Implements the dvrinfo() function.

  • Displays message to standard output.

  • Returns (through the info output parameter):

    • driver version

    • driver name

    • feature

    • mode

void stub_devinfo(const void * const, spi_dev_t * const, spi_devinfo_t * const)
  • Implements the devinfo() function.

  • Displays message to standard output.

  • Returns (through the info output parameter):

    • device number

    • device name

    • current clock rate

    • device mode

int stub_setcfg(const void * const, spi_dev_t *, const spi_cfg_t * const)
  • Implements the setcfg() function.

  • Configures device:

    • saves passed-in device configuration

    • re-calculates devctrl flags

    • sets new clock rate

int stub_xfer(void * const, spi_dev_t *, uint_t * const, const uint32_t, const uint32_t)
  • Implements the xfer() function.

  • Transfers specified amount of data from the passed in buffer to the hardware device:

    • displays the amount of bytes being sent.

    • writes all bytes to standard output.

  • Transfers specified amount of data from the hardware device to the passed in buffer:

    • iterates through canned data and copies it to the buffer.

    • displays the amount of bytes being received.

    • writes all bytes to standard output.

  • Displays message containing number of bytes received and transmitted to standard output.

int stub_dma_xfer(void * const, spi_dev_t *, dma_addr_t *, const uint32_t, const uint32_t)
  • Implements the dma_xfer() function.

  • Checks if DMA is being used, and if not uses stub_xfer() to complete the transfer; otherwise,

  • Transfers specified amount of data from the passed in DMA buffer to the hardware device:

    • displays the amount of bytes being sent.

    • writes all bytes to standard output.

  • Transfers specified amount of data from the hardware device to the passed in DMA buffer:

    • iterates through canned data and copies it to the DMA buffer.

    • displays the amount of bytes being received.

    • writes all bytes to standard output.

  • Displays message containing number of bytes received and transmitted to standard output.

int stub_dma_allocbuf(void * const, dma_addr_t *, const uint32_t)
  • Implements the dma_allocbuf() function.

  • Allocates specified amount of memory for the DMA buffer.

  • Displays message to standard output.

int stub_dma_freebuf(void * const, dma_addr_t *)
  • Implements the dma_feebuf() function.

  • Frees the memory allocated for the DMA buffer.

  • Displays message to standard output.

Client

The simple client below connects to the device created and managed by the SPI stub driver and uses devctl() to issue requests to the driver, as shown in the diagram below:
Figure 2Communication between the stub client and driver

The client parses command line for the device number, opens the device for reading and writing, performs all specified commands, and closes the device. For simplicity, only these operations are supported by the code below:

You can compile the spi-stub-clent.c code example using:
qcc -V gcc_ntoaarch64le -o spi-stub-client spi-stub-client.c
Note:
Make sure to specify appropriate architecture using the -V argument. For the Raspberry Pi 4, use gcc_ntoaarch64le as above.
Then, copy the spi-stub-client binary to your target device, and log in to the device as root user. Navigate to the directory you copied the binary to and execute:
# ./spi-stub-client -n 0 -d 0 -x "This is test data that is 45 characters long."
spi-stub-client: Using device /dev/io-spi/spi0/dev0
spi-stub: Sending 45 bytes to hardware: This is test data that is 45 characters long.
spi-stub: Receiving 45 bytes from hardware: SPI-STUB CANNED DMA OUTPUT DATA.
SPI-STUB CAN
spi-stub: DMA-XFER RX=45 TX=45
Got 45 bytes of data: "SPI-STUB CANNED DMA OUTPUT DATA.
SPI-STUB CAN"
spi-stub-client: Closing device.
# ./spi-stub-client -n 0 -d 0 -i                                                 
spi-stub-client: Using device /dev/io-spi/spi0/dev0
spi-stub: SPI stub driver info=(65536:STUB SPI:2147483648:7)
spi-stub-client: DRIVER=(65536:STUB SPI:2147483648:7)
spi-stub-client: Closing device.
# ./spi-stub-client -n 0 -d 0 -I 
spi-stub-client: Using device /dev/io-spi/spi0/dev0
spi-stub: SPI stub device info=(0:dev0:0:1032)
spi-stub-client: DEVICE=(0:dev0:0:1032)
spi-stub-client: Closing device.
#
You can invoke the SPI client using various command line options:
SPI stub client

spi-stub-client [options]

Options:
-n busno        SPI bus number.  Bus number to append to bus name
                prefix /dev/io-spi/spi.  (Default: 0)
-d devno        SPI device number.  Device number to append to bus name and
                device prefix /dev/io-spi/spi0/dev. (Default: 0)
-i              Retrive driver information - version, name ,feature, and
                verbosity.
-I              Retrieve device information - number, name, current clockrate,
                and mode.
-x xfer_string  Exchange data.  Send the xfer_string to the driver and recive
                the same amount of bytes back.

Example:
spi-stub-client -n 0 -d 0 -x THIS_IS_A_TEST

spi-stub-client.c

#include <errno.h>
#include <fcntl.h>
#include <hw/io-spi.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define STRING_LEN 200u

int main(int argc, char * argv[]) {
    char device_name[STRING_LEN];
    unsigned bus = 0;
    unsigned device = 0;
    int c;
    int fd;
    int done = 0;
    int status = EOK;

    spi_drvinfo_t driver_info;
    spi_devinfo_t device_info;
    spi_xchng_t * spi_xchng;
    int data_size;

    // parser common option only
    while (-1 != (c = getopt(argc, argv, ":n:d:iIx:"))) {
        switch(c) {
            case ':':
                fprintf(stderr, "spi-stub-client: Missing bus or device number.\n");
                return 1;
            case 'n':
                bus = strtoul(optarg, &optarg, 0);
                break;
            case 'd':
                device = strtoul(optarg, &optarg, 0);
                break;
            default:
                break;
        }
    }

    snprintf(device_name, STRING_LEN, "/dev/io-spi/spi%d/dev%d", bus, device);

    // open the bus
    printf("spi-stub-client: Using device %s\n", device_name);
    if(-1 == (fd = open(device_name, O_RDWR))) {
        perror("open");
        return 1;
    }

    // reset getopt and run commands in order specified
    optind = 1;
    while (EIO != status && -1 != (c = getopt(argc, argv, "n:d:iIx:"))) {
        switch(c) {

            // DRIVER_INFO
            case 'i':
                done = !0;
                if (EIO == (status = devctl(fd, DCMD_SPI_GET_DRVINFO, &driver_info, sizeof(spi_drvinfo_t), NULL))) {
                    fprintf(stderr, "spi-stub-client: Failed to get driver info.\n");
                } else {
                    printf("spi-stub-client: DRIVER=(%u:%s:%u:%u)\n", driver_info.version, driver_info.name, driver_info.feature, driver_info.verbosity);
                }
                break;

            // DEVICE_INFO
            case 'I':
                done = !0;
                if (EIO == (status = devctl(fd, DCMD_SPI_GET_DEVINFO, &device_info, sizeof(spi_devinfo_t), NULL))) {
                    fprintf(stderr, "spi-stub-client: Failed to get device info.\n");
                } else {
                    printf("spi-stub-client: DEVICE=(%d:%s:%u:%u)\n", device_info.devno, device_info.name, device_info.current_clkrate, device_info.cfg.mode);
                }
                break;

            // XFER
            case 'x':
                done = !0;
                data_size = strlen(optarg);
                spi_xchng = malloc(sizeof(spi_xchng_t) + data_size + 1);
                if (NULL == spi_xchng) {
                    perror("malloc");
                    return 1;
                }
                spi_xchng->nbytes = data_size;
                strncpy((char*)&spi_xchng->data, optarg, data_size);

                if (EOK != (status = devctl(fd, DCMD_SPI_DATA_XCHNG, spi_xchng, sizeof(spi_xchng_t) + data_size, NULL))) {
                    fprintf(stderr, "spi-stub-client: Failed to xchange data \"%s\".\n", optarg);
                    free(spi_xchng);
                } else {
                    spi_xchng->data[data_size] = 0;
                    printf("Got %d bytes of data: \"%s\"\n", spi_xchng->nbytes, (char*)&spi_xchng->data);
                }

            // ignore parsed already
            case 'n':
            case 'd':
                break;
            default:
                fprintf(stderr, "spi-stub-client: Unknown option %c.\n", c);
                return 1;
        }
    }

    if (!done) {
        fprintf(stderr, "spi-stub-client: No command specied.\n");
        status = EIO;
    }
    
    printf("spi-stub-client: Closing device.\n");
    close(fd);

    if(EIO == status) {
        return 1;
    }
    return 0;

}

The above example assumes that the spi-stub driver was started using the ./spi-stub -c spi.conf command in the same shell session, which allows the driver and client output to be interleaved, with the content of spi.conf, as given above.

  1. Client is invoked with arguments:
    • -n 0 -d 0 - Use /dev/io-spi/spi0/dev0 device.
    • -x "This is test dsata that is 45 characters long." - Send the string specified by argument to the driver and receive 45 bytes back.
  2. Client opens the /dev/io-spi/spi0/dev0 device.
  3. Clients initiates the data transfer and driver sends the data to hardware.
  4. Driver receives data from hardware, in this case canned data.
  5. Data contains new-line "\n" character for the sake of presentation.
  6. Driver displays number of bytes sent and received.
  7. Client displays the data received, including the new-line character.

  8. Client closes the connection to the device.
  9. Client is invoked again with arguments:
    • -n 0 -d 0 - use /dev/io-spi/spi0/dev0 device
    • -i - get the driver information
  10. Client opens the /dev/io-spi/spi0/dev0 device.
  11. Driver displays and returns the driver information.
  12. Client displays the driver information.
  13. Client closes the connection to the device.
  14. Client is invoked again with arguments:
    • -n 0 -d 0 - use /dev/io-spi/spi0/dev0 device
    • -I - get the device information
  15. Client opens the /dev/io-spi/spi0/dev0 device.
  16. Driver displays and returns the device information.
  17. Client displays the driver information.
  18. Client closes the connection to the device.
Page updated: