SPI

From Nutwiki
Revision as of 17:03, 27 October 2016 by Harald (Talk | contribs) (1 revision imported)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Description

A new SPI bus interface structure had been introduced since Nut/OS 4.8, which simplifies multithreaded access to SPI devices. This is specifically true for device drivers. For simple SPI access routines, however, the new implementation is slightly more complicated than it was before.

The following example demonstrates how to read the contents of the first 16 registers of the VS1053B audio codec available on the Elektor Internet Radio Board.

Prerequisites

This example is currently limited to the EIR board.

Source Code

<source lang="c">

  1. include <dev/board.h>
  2. include <dev/gpio.h>
  3. include <dev/spibus_gpio.h>
  4. include <dev/spibus.h>
  5. include <dev/vscodec.h>
  1. include <sys/timer.h>
  1. include <stdio.h>
  2. include <io.h>

NUTSPINODE codec_node = { &spiBus0At91, NULL, 150000, 0, 8, 1 };

static int CodecInit(NUTSPINODE *node) {

   int rc;
   
   GpioPinSetLow(VSCODEC0_XRESET_PORT, VSCODEC0_XRESET_BIT);
   GpioPinConfigSet(VSCODEC0_XRESET_PORT, VSCODEC0_XRESET_BIT, GPIO_CFG_OUTPUT);
   GpioPinSetHigh(VSCODEC0_XCS_PORT, VSCODEC0_XCS_BIT);
   GpioPinConfigSet(VSCODEC0_XCS_PORT, VSCODEC0_XCS_BIT, GPIO_CFG_OUTPUT);
   GpioPinSetHigh(VSCODEC0_XDCS_PORT, VSCODEC0_XDCS_BIT);
   GpioPinConfigSet(VSCODEC0_XDCS_PORT, VSCODEC0_XDCS_BIT, GPIO_CFG_OUTPUT);
   NutSleep(VSCODEC0_HWRST_DURATION);
   GpioPinSetHigh(VSCODEC0_XRESET_PORT, VSCODEC0_XRESET_BIT);
   NutSleep(100);
   rc = (*node->node_bus->bus_initnode) (node);
   if (rc == 0) {
       NutEventPost(&node->node_bus->bus_mutex);
   }
   return rc;

}

static int CodecCmd(NUTSPINODE *node, void *cmd, size_t len) {

   int rc;
   rc = (*node->node_bus->bus_alloc) (node, 100);
   if (rc == 0) {
       GpioPinSetLow(VSCODEC0_XCS_PORT, VSCODEC0_XCS_BIT);
       rc = (*node->node_bus->bus_transfer) (node, cmd, cmd, len);
       GpioPinSetHigh(VSCODEC0_XCS_PORT, VSCODEC0_XCS_BIT);
       (*node->node_bus->bus_release) (node);
   }
   return rc;

}

uint16_t CodecReg(NUTSPINODE *node, uint_fast8_t op, uint_fast8_t reg, uint_fast16_t val) {

   uint8_t cmd[4];
   cmd[0] = (uint8_t) op;
   cmd[1] = (uint8_t) reg;
   cmd[2] = (uint8_t) (val >> 8);
   cmd[3] = (uint8_t) val;
   CodecCmd(node, cmd, 4);
   val = cmd[2];
   val <<= 8;
   val |= cmd[3];
   return (uint16_t) val;

}

int main(void) {

   uint32_t baud = 115200;
   uint_fast8_t r;
   NutRegisterDevice(&DEV_DEBUG, 0, 0);
   freopen(DEV_DEBUG_NAME, "w", stdout);
   _ioctl(_fileno(stdout), UART_SETSPEED, &baud);
   puts("\nSPI Test");
   if (CodecInit(&codec_node)) {
       puts("Failed to initialize audio codec node.");
   }
   for (r = 0; r < 16; r++) {
       printf("0x%02X: 0x%04X\n", r, CodecReg(&codec_node, VS_OPCODE_READ, r, 0));
   }
   for (;;) {
       putchar('.');
       NutSleep(1000);
   }
   return 0;

} </source>

Details

Initializing stdout

<source lang="c"> uint32_t baud = 115200;

NutRegisterDevice(&DEV_DEBUG, 0, 0); freopen(DEV_DEBUG_NAME, "w", stdout); _ioctl(_fileno(stdout), UART_SETSPEED, &baud); puts("\nSPI Test"); </source>

This is the typical initialization of the Nut/OS debug output device, which redirects stdout to the serial RS-232 port.

SPI node structure

To use the SPI bus controller, we need to set up an SPI node structure.

<source lang="c"> struct _NUTSPINODE {

   NUTSPIBUS *node_bus;
   void *node_stat;
   uint_fast32_t node_rate;
   uint_fast16_t node_mode;
   uint_fast8_t node_bits;
   uint_fast8_t node_cs;

}; </source>

Member Description EIR Setting
node_bus Pointer to the bus controller driver, e.g. spiBus0At91, spiBus0Avr, spiBus0Gpio etc. spiBus0At91
node_stat Optional pointer to the node's local data. NULL (not used)
node_rate SPI clock rate. 150000 (~12.288 MHz / 8)
node_mode SPI mode. 0
node_bits Number of data bits / transfer. 8
node_cs Chip select index. Most bus controllers currently support four chips. 1

The initialized structure variable codec_node is

<source lang="c"> NUTSPINODE codec_node = { &spiBus0At91, NULL, 150000, 0, 8, 1 }; </source>

Initializing the hardware

In a next step the main routine calls CodecInit() to initialize the hardware.

<source lang="c"> if (CodecInit(&codec_node)) {

   puts("Failed to initialize audio codec node.");

} </source>

CodecInit() is a local function.

<source lang="c"> static int CodecInit(NUTSPINODE *node) {

   int rc;
   
   GpioPinSetLow(VSCODEC0_XRESET_PORT, VSCODEC0_XRESET_BIT);
   GpioPinConfigSet(VSCODEC0_XRESET_PORT, VSCODEC0_XRESET_BIT, GPIO_CFG_OUTPUT);
   GpioPinSetHigh(VSCODEC0_XCS_PORT, VSCODEC0_XCS_BIT);
   GpioPinConfigSet(VSCODEC0_XCS_PORT, VSCODEC0_XCS_BIT, GPIO_CFG_OUTPUT);
   GpioPinSetHigh(VSCODEC0_XDCS_PORT, VSCODEC0_XDCS_BIT);
   GpioPinConfigSet(VSCODEC0_XDCS_PORT, VSCODEC0_XDCS_BIT, GPIO_CFG_OUTPUT);
   NutSleep(VSCODEC0_HWRST_DURATION);
   GpioPinSetHigh(VSCODEC0_XRESET_PORT, VSCODEC0_XRESET_BIT);
   NutSleep(100);
   rc = (*node->node_bus->bus_initnode) (node);
   if (rc == 0) {
       NutEventPost(&node->node_bus->bus_mutex);
   }
   return rc;

} </source>

After reset almost all I/O ports of the microcontroller are preset as inputs. We must configure all output lines first.

We could have used low level port I/O, but that would require to implement a specific initialization routine for each target family. Using the Nut/OS GPIO library module is somewhat slower and produces more code, but it works on all platforms.

All our three output GPIO lines are low active:

  • VSCODEC0_XRESET: Codec reset line
  • VSCODEC0_XCS: Codec command line select
  • VSCODEC0_XDCS: Codec data line select

The actual hardware ports and bit numbers are configured in the Nut/OS Configurator (see screen shot below). Therefore, no code change is required when building the sample for different hardware layouts.

320px

Note, that only the two chip selects and the reset line are directly controlled. The SPI port is configured when calling bus_initnode() of the bus controller driver.

Finally we initialize a simple mutex semaphore, using NutEventPost().

Reading codec registers

The application can then make use of another local function named CodecReg(), that allows to read from or write to registers of the VS1053 audio codec. Please refer to the VS1053 datasheet about how to use the SPI command interface and the chip's register layout.

Here is the code of the register access function.

<source lang="c"> uint16_t CodecReg(NUTSPINODE *node, uint_fast8_t op, uint_fast8_t reg, uint_fast16_t val) {

   uint8_t cmd[4];
   /* Assemble command buffer. */
   cmd[0] = (uint8_t) op;
   cmd[1] = (uint8_t) reg;
   cmd[2] = (uint8_t) (val >> 8);
   cmd[3] = (uint8_t) val;
   CodecCmd(node, cmd, 4);
   val = cmd[2];
   val <<= 8;
   val |= cmd[3];
   return (uint16_t) val;

} </source>

To read the current value of status register (index 0x1), use

<source lang="c"> val = CodecReg(&codec_node, VS_OPCODE_READ, 0x1, 0); </source>

The register content is returned as a function result, while the last parameter, the value to write into the register, is ignored.

To set the integrated amplifier to -18db on both channels, we must write 0x2424 to register 0xB. Again, check the VS1053 datasheet about how to set the amplifier's attenuation. The related call is

<source lang="c"> CodecReg(&codec_node, VS_OPCODE_WRITE, 0xB, 0x2424); </source>

When writing, we can ignore the return value.

Our main routine call CodecReg() in a loop to read the contents of the first 16 registers and print them on stdout.

<source lang="c"> uint_fast8_t r;

for (r = 0; r < 16; r++) {

   printf("0x%02X: 0x%04X\n", r, CodecReg(&codec_node, VS_OPCODE_READ, r, 0));

} </source>

Using the bus controller driver

The last local function not touched so far is CodecCmd(), which is called by CodecReg().

<source lang="c"> static int CodecCmd(NUTSPINODE *node, void *cmd, size_t len) {

   int rc;
   rc = (*node->node_bus->bus_alloc) (node, 100);
   if (rc == 0) {
       GpioPinSetLow(VSCODEC0_XCS_PORT, VSCODEC0_XCS_BIT);
       rc = (*node->node_bus->bus_transfer) (node, cmd, cmd, len);
       GpioPinSetHigh(VSCODEC0_XCS_PORT, VSCODEC0_XCS_BIT);
       (*node->node_bus->bus_release) (node);
   }
   return rc;

} </source>

This is the core routine to access the audio codec via the bus controller interface. And it may be the most confusing part of our sample.

<source lang="c"> int rc;

rc = (*node->node_bus->bus_alloc) (node, 100); if (rc == 0) {

 ...
 (*node->node_bus->bus_release) (node);

} </source>

It tries to allocate the SPI bus by calling the controller's bus_alloc routine. On success (0 is returned) the SPI bus is available for transfers and then has to be released by calling the bus_release function. The transfer itself is done by calling bus_transfer() once or even several times.

<source lang="c"> rc = (*node->node_bus->bus_transfer) (node, cmd, cmd, len); </source>

The first parameter is a pointer to the SPI node structure, followed by a pointer to the transmit and a pointer to the receive buffer. As in our case, both may point to the same buffer, in which case its content is sent out and replaced by the incoming bytes. The last parameter specifies the number of bytes to transmit.

Output

Contents of the VS1053 registers.

SPI Test
0x00: 0x4800
0x01: 0x0048
0x02: 0x0000
0x03: 0x0000
0x04: 0x0000
0x05: 0x1F40
0x06: 0x0000
0x07: 0x0000
0x08: 0x0000
0x09: 0x0000
0x0A: 0x0000
0x0B: 0x0000
0x0C: 0x0000
0x0D: 0x0000
0x0E: 0x0000
0x0F: 0x0000
0x01: 0x0048
......

See also

External Links