SPI driver implementation

This section describes how to implement an SPI driver for QNX. The example functions below are simple examples and shouldn't be copied literally.

For more information on the SPI framework, including its essential functions, refer to the SPI Framework Technote in the SDP 8.0 documentation.

Define a structure

Before making the functions, you need to define a structure that holdsĀ all necessary state information. The structure may look like this:

typedef struct {
    uint64_t    pbase;          // Physical base address
    uintptr_t   vbase;          // Virtual base address
    uint32_t    irq;            // Interrupt number
    int         iid;
    uint64_t    input_clk;      // Input clock frequency
    uint8_t     *pbuf;          // Transfer buffer
    uint32_t    tnbytes;        // Transmit bytes
    uint32_t    rnbytes;        // Receive bytes
    uint32_t    xlen;           // Full transaction length requested by client
    uint32_t    tcnt;           // Transmit counter
    uint32_t    rcnt;           // Receive counter
    uint32_t    dwidth;         // Data width. 1 = 8bits/byte, 2 = 16bits/2 bytes, 4 = 32bits/4 bytes
    spi_ctrl_t  *spi_ctrl;      // The address of spi_ctrl structure
    spi_bus_t   *bus_node;      // The address of bus structure which is passed in by spi_init()
} your_spi_t;

Clean up the driver

The following code sample shows how to implement the driver cleanup function:

void your_fini(void *const hdl) {
    your_spi_t *const spi = hdl;

    if (spi == NULL) {
        return;
    }

    // 1. Disable hardware
    if (spi->vbase != (uintptr_t)MAP_FAILED) {
        // Disable SPI controller
        out32(spi->vbase + YOUR_SPI_CONTROL_REG, 0);

        // Unmap registers
        munmap_device_memory((void *)spi->vbase, YOUR_SPI_REGLEN);
    }

    // 2. Free allocated memory
    free(spi);
}

Set up a config driver

You need a setup config driver function to set the configuration of when the device driver starts, or when the application sets the configuration, which may look something like this:

int your_setcfg(const void *const hdl, spi_dev_t *spi_dev,const spi_cfg_t *const cfg)
{
    const your_spi_t *const spi = hdl;
    uintptr_t     const base = spi->vbase;
    uint32_t      devctrl = 0;

    /* Check hardware limitations for word width */
    switch ((cfg->mode & SPI_MODE_WORD_WIDTH_MASK)) {
        case SPI_MODE_WORD_WIDTH_32:
        case SPI_MODE_WORD_WIDTH_8:
            break;
        default:
            spi_slogf(_SLOG_ERROR, "%s: %d-bit word width is not supported by this controller",
                                __func__, cfg->mode & SPI_MODE_WORD_WIDTH_MASK);
            return EINVAL;
    }

    /* Gather default CS and clock polarity related configuration */
    if (cfg->mode & SPI_MODE_CSPOL_HIGH) {
        devctrl |= (SPI_CS_CSPOL_HIGH << SPI_CS_CSPOL);   /* CS Polarity: CS active high */
    }

    if (cfg->mode & SPI_MODE_CPOL_1) {
        devctrl |= (SPI_CS_CPOL_HIGH << SPI_CS_CPOL);     /* Clock Polarity: Rest state of clock high */
    }

    if (cfg->mode & SPI_MODE_CPHA_1) {
        devctrl |= (SPI_CS_CPHA_BEGIN << SPI_CS_CPHA);    /* Clock Phase: SCLK beginning of data bit */
    }

    /* Set SPI_CLK only when it is different than current clock rate */
    if (spi_dev->devinfo.current_clkrate != cfg->clock_rate) {
        uint32_t actual_rate = spi_set_clock(spi, cfg, base);
        if (actual_rate == 0) {
            spi_slogf(_SLOG_ERROR, "%s: Clock configuration failed", __func__);
            return EINVAL;
        }

        /* Update current clock rate with actual achieved rate */
        cfg->clock_rate = actual_rate;
        spi_dev->devinfo.current_clkrate = actual_rate;
    } else {
        spi_slogf(_SLOG_DEBUG2, "%s: Device clock rate is already set to %d", __func__, cfg->clock_rate);
    }

    /* Add Chip Select/Physical device ID */
    devctrl |= spi_dev->devinfo.devno;

    /* Update the SPI device devctrl reg value */
    spi_dev->devctrl = devctrl;

    spi_slogf(_SLOG_DEBUG1, "%s: sclk=%d, devctrl=0x%x", __func__,
              cfg->clock_rate, devctrl);

    return EOK;
}

/*
 * NOTE: The following function must be implemented according to your specific hardware:
 * - spi_set_clock(): Set the SPI clock rate
 */

Exchange data

You need a function to exchange data between the master and the slave. The function may look something like this:

int your_xfer(void *const hdl, spi_dev_t *const spi_dev, uint8_t *const buf, const uint32_t tnbytes, const uint32_t rnbytes)
{
    your_spi_t *const spi = hdl;
    const uintptr_t base = spi->vbase;
    const spi_cfg_t *const cfg = &spi_dev->devinfo.cfg;
    int status = EOK;

    /* Set transfer parameters based on operation type */
    if (tnbytes == 0) {
        /* Read only */
        spi->tnbytes = rnbytes;
        spi->rnbytes = rnbytes;
    } else if (rnbytes == 0) {
        /* Write only */
        spi->tnbytes = tnbytes;
        spi->rnbytes = tnbytes;
    } else {
        /* Bidirectional transaction */
        spi->tnbytes = tnbytes;
        spi->rnbytes = rnbytes;
    }

    /* Capture max transaction length requested by client */
    spi->xlen = max(spi->tnbytes, spi->rnbytes);

    /* Setup transfer parameters */
    spi->dwidth = ((cfg->mode & SPI_MODE_WORD_WIDTH_MASK) >> 3);
    spi->rcnt = 0;
    spi->tcnt = 0;
    spi->pbuf = buf;

    /* Reset the SPI interface
     * Clear both FIFOs
     */
    out32((base + YOUR_SPI_CS), YOUR_SPI_RESET);
    out32((base + YOUR_SPI_CS), YOU_SPI_FIFO_CLEAR);

    /* Enable SPI transfer */
    spi_enable(spi, spi_dev->devctrl);

    /* Fill TX FIFO with data */
    while ((spi->tcnt < spi->tnbytes) && (spi->tcnt < YOUR_SPI_FIFOLEN)) {
        spi_write_fifo(spi, *(spi->pbuf + spi->tcnt));
        spi->tcnt += spi->dwidth;
    }

    /* Wait for transfer completion */
    status = spi_wait_complete(spi);

    /* Disable SPI */
    spi_disable(spi);

    return status;
}

/*
* NOTE: The following functions must be implemented according to your specific hardware:
* - spi_enable(): Enable SPI
* - spi_write_fifo(): Write a byte to the TX FIFO
* - spi_wait_complete(): Wait for transfer completion with appropriate timeout
* - spi_disable(): Disable SPI controller and interrupts
*/

Initialize the driver

Finally, after making the essential functions to make the driver, you can integrate these functions in the spi_init() function to make the driver. This spi_init() function is called when the driver is started by the user. The function may look something like this:

static int init_device(your_spi_t *spi)
{
    int             status;
    uintptr_t       base;
    spi_bus_t       *const bus = spi->bus_node;
    spi_dev_t       *spi_dev = bus->devlist;

    /*
     * Map in SPI registers
     */
    base = (uintptr_t)mmap(NULL, YOUR_SPI_REGLEN, PROT_READ | PROT_WRITE | PROT_NOCACHE, MAP_PHYS | MAP_SHARED, NOFD, (off_t)spi->pbase);
    if (base == (uintptr_t)MAP_FAILED) {
        spi_slogf(_SLOG_ERROR, "%s: Failed to map SPI registers(%s)", __func__, strerror(errno));
        return errno;
    }

    spi->vbase = base;

    /* Reset the SPI interface
     * Clear both FIFOs
     */
    out32((base + YOUR_SPI_CS), YOUR_SPI_RESET);
    out32((base + YOUR_SPI_CS), YOU_SPI_FIFO_CLEAR);

    /*
     * Initial device configuration with defaults from config file
     */
    while (spi_dev != NULL) {
        status = your_setcfg(spi, spi_dev, &spi_dev->devinfo.cfg);
        if (status != EOK) {
            spi_slogf(_SLOG_ERROR, "%s: your_setcfg failed", __func__);
            return status;
        }
        spi_dev = spi_dev->next;
    }

    /* Attach SPI interrupt */
    spi->iid = InterruptAttachEvent(spi->irq, &bus->evt, _NTO_INTR_FLAGS_TRK_MSK);

    return status;
}


int spi_init(spi_bus_t *bus)
{
    your_spi_t *spi = NULL;
    int           status = EOK;

    if (bus == NULL) {
        spi_slogf(_SLOG_ERROR, "%s: SPI bus structure is NULL!", __func__);
        return EINVAL;
    }

    spi = calloc(1, sizeof(your_spi_t));

    /* Save spi_ctrl to driver structure */
    spi->spi_ctrl = bus->spi_ctrl;
    spi->bus_node = bus;

    /* Get other SPI driver functions */
    bus->funcs->spi_fini = your_fini;
    bus->funcs->drvinfo  = your_drvinfo;
    bus->funcs->devinfo = your_devinfo;
    bus->funcs->setcfg = your_setcfg;
    bus->funcs->xfer  = your_xfer;
    bus->funcs->dma_xfer  = NULL;
    bus->funcs->dma_allocbuf = NULL;
    bus->funcs->dma_freebuf = NULL;

    /* Get controller parameters */
    spi->pbase = bus->pbase;
    spi->irq = bus->irq;
    spi->input_clk = bus->input_clk;

    /* Update SPI transaction buffer boundary */
    bus->buf_max = SPI_BUF_MAX_64K;

    spi->iid = -1;
    spi->vbase = (uintptr_t)MAP_FAILED;

    /* Optional: Process custom arguments to your driver to override the defaults */
    if (bus->bs != NULL) {
        status = process_args(spi, bus->bs);
        if (status != EOK) {
            spi_slogf(_SLOG_ERROR, "%s: process_args failed", __func__);
            your_fini(spi);
            return status;
        }
    }

    /* Init SPI device */
    status = init_device(spi);
    if (status != EOK) {
        spi_slogf(_SLOG_ERROR, "%s: init_device failed: %s", __func__, strerror(status));
        your_fini(spi);
        return status;
    }

    /*
     * Create SPI chip select devices
     */
    status = spi_create_devs(bus->devlist);
    if (status != EOK) {
        spi_slogf(_SLOG_ERROR, "%s: spi_create_devs for %s failed", __func__, bus->devlist->devinfo.name);
        your_fini(spi);
        return status;
    }

    /* Save driver structure to drvhdl */
    bus->drvhdl = spi;

    return status;
}

/*
* NOTE: Implement the following function if want your driver to have optional parameters
* - process_args(): Process custom arguments to your driver to override the defaults
*/
Page updated: