7

Local Bus Interfaces

The communication between an embedded system and other systems in its vicinity is enabled by a few protocols. Most microcontrollers designed for embedded systems support the most common interfaces that control and discipline the access to serial lines. Some of these protocols are so popular that they have become the standard for wired inter-chip communication among microcontrollers, and for controlling electronic devices, such as sensors, actuators, displays, wireless transceivers, and many other peripherals. This chapter describes how these protocols work, specifically focusing on the implementation of the system software, through examples running on the reference platform. In particular, we will cover the following topics in this chapter:

  • Introducing serial communication
  • A UART-based asynchronous serial bus
  • An SPI bus
  • An I2C bus

By the end of this chapter, you will learn how to integrate the common serial communication protocols.

Technical requirements

You can find the code files for this chapter on GitHub at https://github.com/PacktPublishing/Embedded-Systems-Architecture-Second-Edition/tree/main/Chapter7.

Introducing serial communication

All the protocols that we will analyze in this chapter manage the access to a serial bus, which may consist of one or more wires, transporting the information in the form of electrical signals corresponding to logic levels zeros and ones, when associated with specific time intervals. The protocols are different in the way they transmit and receive information on the data bus lines. To transmit a byte, the transceiver encodes it as a bit sequence, which is synchronized with a clock. The logic values of the bit are interpreted by the receiver reading its value on a specific front of the clock, depending on the clock’s polarity.

Each protocol specifies the polarity of the clock and the bit order required to transmit the data, which can start with either the most significant or the least significant bit. For example, a system transmitting the ASCII character D over a serial line regulated by raising clock fronts, with the most significant bit first, would produce a signal such as the following:

Figure 7.1 – The logic levels of the bus on clock raise fronts are interpreted MSB-first into the byte value of 0x44

Figure 7.1 – The logic levels of the bus on clock raise fronts are interpreted MSB-first into the byte value of 0x44

We will now define the characteristics of serial communication interfaces following different standards. In particular, we will indicate the options available for clock synchronization between two endpoints exchanging data; the wiring of the signals to be used, which are specified by each protocol to access the physical media; and finally, the implementation details to program the access to the peripheral, which may differ across different platforms.

Clock and symbol synchronization

In order for the receiving side to understand the message, the clock must be synchronized between the parts. The clock synchronization may be implicit, as in, setting the same data rate to read and write on the bus, or achieved by sharing the clock line from one side using an additional line to explicitly synchronize the transmit data rate. Serial protocols that do not foresee shared clock lines are called asynchronous.

Symbol synchronization should be explicit instead. As we expect to send and receive information in the form of bytes, the beginning of each 8-bit sequence should be marked either using special preamble sequences on the data line or by turning the clock on and off at the right time. The symbol synchronization strategy is defined differently by each protocol.

Bus wiring

The number of lines needed to establish bidirectional communication depends on the specific protocol too. Since one wire can only transport 1 bit of information in one direction at a time, to achieve full-duplex communication, a transceiver should connect to two different wires for transmitting and receiving data. If the protocol supports half-duplex communication, it should provide a reliable mechanism to regulate media access instead and switch between receiving and transmitting data on the same wire.

Important note

The two endpoints must share a common reference ground voltage, which means that it might be required to add one extra wire to connect the ground lines if the devices do not already share a common ground.

Depending on the protocol, devices accessing the bus may either share a similar implementation and act as peers or have different roles assigned when participating in the communication – for example, if a master device is in charge of synchronizing the clock or regulating access to the media.

A serial protocol may foresee communication among more than two devices on the same bus. This may be achieved by using extra slave selection wires, one per slave device sharing the same bus, or by assigning logical addresses to each endpoint, and including the destination address for the communication in the preamble of each transmission. Based on these classifications, an overview of the approach taken by the most popular serial protocols implemented in embedded targets is given in the following table:

The protocols that are detailed in this chapter are only the first three, as they are the most widely used in communicating with embedded peripherals.

Programming the peripherals

Multiple peripherals implementing the protocols described so far are usually integrated into microcontrollers, which means that the associated serial bus can be directly connected to specific pins of the microcontrollers. The peripherals can be enabled through clock gating and controlled by accessing configuration registries mapped in the peripheral region in the memory space. The pins connected to serial buses must also be configured to implement the corresponding alternate function, and the interrupt lines involved should be configured to be handled in the vector table.

Some microcontrollers, including our reference platform, support Direct Memory Access (DMA) to speed up memory operations between the peripheral and the physical RAM. In many cases, this feature is useful to help process the communication data in a shorter time frame and to improve the responsiveness of the system. The DMA controller can be programmed to initiate a transfer operation and trigger an interrupt when it completes.

The interface to control the features relative to each protocol is specific to the functionalities exposed by the peripheral. In the next sections, the interfaces exposed by UART, SPI, and I2C peripherals are analyzed, and code samples tailored to the reference platform are provided as examples of one of the possible implementations for similar device drivers.

UART-based asynchronous serial bus

Historically used for many different purposes, thanks to the simplicity of its asynchronous nature, UART dates back to the origins of computing, and it is still a very popular circuit used in many contexts. Personal computers up to the early 2000s included at least one RS-232 serial port, realized with a UART controller and the transceivers allowing to operate at higher voltages. Nowadays, the USB has replaced serial communication on personal computers, but host computers can still access TTL serial buses using USB-UART peripherals. Microcontrollers have one or more pairs of pins that can be associated with an internal UART controller and connected to a serial bus to configure a bidirectional, asynchronous, full-duplex communication channel toward a device connected to the same bus.

Protocol description

As previously mentioned, asynchronous serial communications rely on implicit synchronization of the bit rate between the transmitter and the receiver in order to guarantee that the data is correctly processed on the receiving end of the communication. If the peripheral clock is fast enough to keep the device running at a high frequency, asynchronous serial communication may be pushed up to several megabits per second.

The symbol synchronization strategy is based on the identification of the beginning of the transmission of every single byte on the wire. When no device is transmitting, the bus is in an idle state.

To initiate the transmission, the transceiver pulls the TX line down to the low logic level, for a period of time that is at least half of the bit sampling period depending on the bit rate. The bits composing the byte being transferred are then translated into logical 0 or 1 values, which are held on the TX line for the time corresponding to each bit, according to the bit rate. After this start condition is easily recognized by the receiver, the bits composing the symbol follow in a specific order, from the least significant bit up to the most significant one.

The number of data bits composing the symbol is also configurable. The default data length of 8 bits allows each symbol to be converted into a byte. At the end of the data, an optional parity bit can be configured to count the number of active bits, as a very simplistic form of a redundant check. The parity bit, if present, can be configured to indicate whether the number of 1 values in the symbol is odd or even. While returning to the idle state, 1 or 2 stop bits must be used to indicate the end of the symbol.

A stop bit is transmitted by pulling the signal high for the entire duration of a bit transmission, marking the end of the current symbol, and forcing the receiver to initiate receiving the next one. A 1-stop bit is the most used default; the 1.5- and 2-stop bit settings provide a longer inter-symbol idling interval, which was useful in the past to communicate with slower, less responsive hardware but is rarely used today.

The two endpoints must be aware of these settings before initiating the communication. Serial controllers do not normally support the dynamic detection of the symbol rate or of any of the settings from the device connected to the other end, and, for this reason, the only way to successfully attempt any serial communication is to program both devices on the bus using the same well-known settings. As a recap, these settings are as follows:

  • The bit rate, expressed in bits per second
  • The number of data bits in each symbol (typically 8)
  • The meaning of parity bit, if present (O is odd, E is even, and N is not present)
  • The number of stop bits

Additionally, the sender must be configured to send 1, 1.5, or 2 stop bits at the end of each transmission. 1.5 and 2 stop bits were more widely used in the past to synchronize communication with ancient electromechanical devices. Nowadays, parity checks and stop bits greater than 1 are not needed for communications using modern transceivers and are rarely used.

This group of settings is often abbreviated into something such as 115200-8-N-1 or 38400-8-O-2 to indicate, respectively, a 115.2 Kbps serial line with 8 data bits per symbol, no parity and 1 stop bit, and a 38400 line with the same data bits, odd parity, and 2 stop bits.

Programming the controller

Development boards usually provide multiple UARTs, and our reference, the STM32F407, is not an exception. According to the manual, UART3 can be associated with the PD8 (TX) and PD9 (RX) pins, which we will use in this example. The code needed to turn on the clock for the D GPIO group and set the 8 and 9 pins in alternate mode, with an alternate function of 7, is as follows:

#define AHB1_CLOCK_ER (*(volatile uint32_t *)(0x40023830))
#define GPIOD_AHB1_CLOCK_ER (1 << 3)
#define GPIOD_BASE 0x40020c00
#define GPIOD_MODE (*(volatile uint32_t *)(GPIOD_BASE + 0x00))
#define GPIOD_AFL (*(volatile uint32_t *)(GPIOD_BASE + 0x20))
#define GPIOD_AFH (*(volatile uint32_t *)(GPIOD_BASE + 0x24))
#define GPIO_MODE_AF (2)
#define UART3_PIN_AF (7)
#define UART3_RX_PIN (9)
#define UART3_TX_PIN (8)
static void uart3_pins_setup(void)
{
  uint32_t reg;
  AHB1_CLOCK_ER |= GPIOD_AHB1_CLOCK_ER;
  reg = GPIOD_MODE & ~ (0x03 << (UART3_RX_PIN * 2));
  GPIOD_MODE = reg | (2 << (UART3_RX_PIN * 2));
  reg = GPIOD_MODE & ~ (0x03 << (UART3_TX_PIN * 2));
  GPIOD_MODE = reg | (2 << (UART3_TX_PIN * 2));
  reg = GPIOD_AFH & ~(0xf << ((UART3_TX_PIN - 8) * 4));
  GPIOD_AFH = reg | (UART3_PIN_AF << ((UART3_TX_PIN - 8) *
     4));
  reg = GPIOD_AFH & ~(0xf << ((UART3_RX_PIN - 8) * 4));
  GPIOD_AFH = reg | (UART3_PIN_AF << ((UART3_RX_PIN - 8) *
     4));
}

The device has its own clock-gating configuration bit in the APB1_CLOCK_ER register, at position 18:

#define APB1_CLOCK_ER (*(volatile uint32_t *)(0x40023840))
#define UART3_APB1_CLOCK_ER_VAL (1 << 18)

Each UART controller can be accessed using registers mapped in the peripheral region, with fixed offsets from the UART controller base address:

  • UART Status Register (SR): A read-only register containing status flags, with an offset equal to 0
  • UART_Data Register (DR): A read/write data register, with an offset equal to 4
  • UART Bit Rate Register (BRR): This sets the clock divisor to obtain the desired bit rate, offset equal to 8
  • UART Configuration Registers (CRxs): One or more UART_CRx registers at offset 12, to set the serial port parameters, enable interrupts and DMA, and enable and disable the transceiver

In this example, we define shortcut macros to access the following registers for UART3:

#define UART3 (0x40004800)
#define UART3_SR (*(volatile uint32_t *)(UART3))
#define UART3_DR (*(volatile uint32_t *)(UART3 + 0x04))
#define UART3_BRR (*(volatile uint32_t *)(UART3 + 0x08))
#define UART3_CR1 (*(volatile uint32_t *)(UART3 + 0x0c))
#define UART3_CR2 (*(volatile uint32_t *)(UART3 + 0x10))

We define the positions in the corresponding bit fields:

#define UART_CR1_UART_ENABLE (1 << 13)
#define UART_CR1_SYMBOL_LEN (1 << 12)
#define UART_CR1_PARITY_ENABLED (1 << 10)
#define UART_CR1_PARITY_ODD (1 << 9)
#define UART_CR1_TX_ENABLE (1 << 3)
#define UART_CR1_RX_ENABLE (1 << 2)
#define UART_CR2_STOPBITS (3 << 12)
#define UART_SR_TX_EMPTY (1 << 7)

The uart3_pins_setup helper function can be called at the beginning of the initialization function to set up the pin. The function accepts arguments to set the bit rate, parity bit, and stop bits on the UART3 port:

int uart3_setup(uint32_t bitrate, uint8_t data,
char parity, uint8_t stop)
{
  uart3_pins_setup();

The device is turned on:

  APB1_CLOCK_ER |= UART3_APB1_CLOCK_ER_VAL;

In the CR1 configuration register, the bit to enable the transmitter is set:

  UART3_CR1 |= UART_CR1_TX_ENABLE;

UART_BRR is set to contain the divisor between the clock speed and the desired bit rate:

  UART3_BRR = CLOCK_SPEED / bitrate;

Our function also accepts a character to indicate the desired parity. The options are O or E for odd or even. Any other character will keep the parity disabled:

  /* Default: No parity */
  UART3_CR1 &= ~(UART_CR1_PARITY_ENABLED | 
      UART_CR1_PARITY_ODD);
   switch (parity) {
       case 'O':
           UART3_CR1 |= UART_CR1_PARITY_ODD;
           /* fall through to enable parity */
       case 'E':
           UART3_CR1 |= UART_CR1_PARITY_ENABLED;
           break;
}

The number of stop bits is set according to the parameter. The configuration is stored using 2 bits of the register, with a value of 0 meaning 1 stop bit, and a value of 2 meaning 2:

  reg = UART3_CR2 & ~UART_CR2_STOPBITS;
  if (stop > 1)
    UART3_CR2 = reg | (2 << 12);

The configuration is now complete. The UART can be turned on to initiate transmissions:

  UART3_CR1 |= UART_CR1_UART_ENABLE;
  return 0;
}

Serial data can now be transmitted on PD8 simply by copying one byte at a time on the UART_DR register.

Hello world!

One of the most useful functions when developing an embedded system is to convert one of the available UARTs into a logging port, where debug messages and other information produced during the execution can be read on the host computer using a serial-to-USB converter:

Figure 7.2 – The host is connected to the serial port of the target platform using a converter

Figure 7.2 – The host is connected to the serial port of the target platform using a converter

The UART logic includes FIFO buffers in both directions. The transmit FIFO is fed by writing on the UART_DR register. To actually output data on the UART TX line in polling mode, we choose to check that the FIFO is empty before writing each character, to ensure that no more than one character is put in the FIFO at a time. When the FIFO is empty, the bit associated with the TX_FIFO_EMPTY flag in UART3_SR is set to 1 by the device. The following function shows how to transmit an entire string of characters passed as an argument, waiting for the FIFO to empty after every byte:

void uart3_write(const char *text)
{
  const char *p = text;
  int i;
  volatile uint32_t reg;
  while(*p) {
    do {
      reg = UART3_SR;
    } while ((reg & UART_SR_TX_EMPTY) == 0);
    UART3_DR = *p;
    p++;
  }
}

In the main program, it is possible to call this function with a pre-formatted, NULL-terminated string:

#include "system.h"
#include "uart.h"
void main(void) {
  flash_set_waitstates();
  clock_config();
  uart3_setup(115200, 8, 'N', 1);
  uart3_write("Hello World!
");
  while(1)
    WFI();
}

If the host is connected to the other endpoint of the serial bus, as a result, we can visualize the Hello World! message using a serial terminal program, such as minicom, on the host.

By capturing the output of the PD8 pin, used as UART_TX on the target, and setting the right option for the serial decoding, we have a better idea of how the serial flow is parsed on the receiving side. The logic analyzer can show how the data bits are sampled after every start condition, and reveal the ASCII character associated with the byte on the wire. Logic analyzer tools are usually capable of decoding the bits captured on the wire, and showing each byte transmitted back into its ASCII format. This feature offers a quick and accurate way to verify that our serial communication is compliant, the time in between the consecutive bits respects the selected baud rate, and the content on the wire matches the data sent to the UART transceiver, as in the following figure, which shows our embedded target sending “Hello” from the string and passing it to the uart3_write function.

Figure 7.3 – Screenshot of the logic analyzer tool showing the first 5 bytes sent by the example to the host using UART3

Figure 7.3 – Screenshot of the logic analyzer tool showing the first 5 bytes sent by the example to the host using UART3

newlib printf

Writing pre-formatted strings is not the most ideal API for accessing a serial port to provide debugging messages. Application developers would most certainly prefer if the system exposed a standard C printf function. When the toolchain includes an implementation of a standard C library, it usually gives you the possibility to connect the standard output of the main program to a serial interface. Luckily enough, the toolchain in use for the reference platform allows us to link to newlib functions. Similar to what we did in Chapter 5, Memory Management, using the malloc and free functions from newlib, we provide a backend function called _write(), which gets the output redirected from the string formatted by all the calls to printf(). The _write function implemented here will receive all the strings pre-formatted by printf():

int _write(void *r, uint8_t *text, int len)
{
  char *p = (char *)text;
  int i;
  volatile uint32_t reg;
  text[len - 1] = 0;
  while(*p) {
    do {
       reg = UART3_SR;
    } while ((reg & UART_SR_TX_EMPTY) == 0);
    UART3_DR = *p;
    p++;
  }
  return len;
}

So, in this case, linking with newlib allows us to use printf to produce messages, including its variance-argument parsing, as in this example main() function:

#include <stdio.h>
#include "system.h"
#include "uart.h"
void main(void) {
  char name[] = "World";
  flash_set_waitstates();
  clock_config();
  uart3_setup(115200, 8, 'N', 1);
  printf("Hello %s!
", name);
  while(1)
    WFI();

This second example will produce the same output as the first one, but this time, using the printf function from newlib.

Receiving data

To enable the receiver on the same UART, the initialization function should also turn on the receiver using the corresponding switch in the UART_CR1 register:

UART3_CR1 |= UART_CR1_TX_ENABLE | UART_CR1_RX_ENABLE;

This ensures that the receiving side of the transceiver is enabled too. To read data in polling mode, blocking until a character is received, we can use the following function, which will return the value of the byte read:

char uart3_read(void)
{
  char c;
  volatile uint32_t reg;
  do {
    reg = UART3_SR;
  } while ((reg & UART_SR_RX_NOTEMPTY) == 0);
  c = (char)(UART3_DR & 0xff);
  return c;
}

This way, we can, for example, echo back to the console each character received from the host:

void main(void) {
  char c[2];
  flash_set_waitstates();
  clock_config();
  uart3_setup(115200, 8, 'N', 1);
  uart3_write("Hello World!
");
  while(1) {
    c[0] = uart3_read();
    c[1] = 0;
    uart3_write(c);
    uart3_write("
");
  }
}

Interrupt-based input/output

The examples in this section are based on polling the status of the UART by continuously checking the flags of UART_SR. The write operation contains a busy loop that can spin for several milliseconds, depending on the length of the string. Even worse, the read function presented earlier spins within a busy loop until there is data to read from the peripheral, which means that the whole system is hanging until new data is received. In a single-thread embedded system, returning to the main loop with the shortest latency possible is important to keep the system responsive.

The correct way to perform UART communication without blocking is by using the interrupt line associated with the UART to trigger actions based on the event received. UART can be configured to raise the interrupt signal upon multiple types of events. As we have seen in the previous examples, to regulate input and output operations, we are interested in particular in two specific events:

  • A TX FIFO empty event, allowing more data to be transmitted
  • A RX FIFO not-empty event, signaling the presence of newly received data

The interrupt for these two events can be enabled by setting the corresponding bits in UART_CR1. We define two helper functions with the purpose of turning interrupts on and off, independently:

#define UART_CR1_TXEIE (1 << 7)
#define UART_CR1_RXNEIE (1 << 5)
static void uart3_tx_interrupt_onoff(int enable)
{
  if (enable)
    UART3_CR1 |= UART_CR1_TXEIE;
  else
    UART3_CR1 &= ~UART_CR1_TXEIE;
}
static void uart3_rx_interrupt_onoff(int enable)
{
  if (enable)
    UART3_CR1 |= UART_CR1_RXNEIE;
  else
    UART3_CR1 &= ~UART_CR1_RXNEIE;
}

A service routine can be associated with the interrupt events, and then check the flags in UART_SR to identify the cause of the interrupt:

void isr_uart3(void)
{
  volatile uint32_t reg;
  reg = UART3_SR;
  if (reg & UART_SR_RX_NOTEMPTY) {
     /* Receive a new byte */
  }
  if ((reg & UART_SR_TX_EMPTY)
  {
     /* resume pending transmission */
  }
}

The implementation of the interrupt routine depends on the specific system design. An RTOS may decide to multiplex access to the serial port to multiple threads and wake up threads waiting to access the resource. In a single-thread application, it is possible to add intermediate system buffers to provide non-blocking calls, which return immediately after copying the data from the receiving buffer, or to the transmitting one. The interrupt service routine fills the receiving buffer with new data from the bus and transmits the data from the pending buffer. Using appropriate structures, such as circular buffers to implement system input and output queues, ensures that the use of the memory assigned is optimized.

SPI bus

The SPI bus provides a different approach, based on master-slave communication. As the name suggests, the interface was initially designed to control peripherals. This is reflected in the design, as all the communication is always initiated by the master on the bus. Thanks to the full-duplex pin configuration and the synchronized clock, it may be much faster than asynchronous communication, due to the better robustness to clock skews between the systems sharing the bus. An SPI is widely used as a communication protocol for several different devices, due to its simple logic and the flexibility given by the fact that the slave does not have to be preconfigured to communicate at a predefined speed that matches the one on the master. Multiple peripherals can share the same bus, as long as media access strategies are defined. A common way for a master to control one peripheral at a time is by using separate GPIO lines to control the slave selection, although this does require an additional wire for each slave.

Protocol description

The configuration of the SPI transceiver is very flexible. Usually, a transceiver on a microcontroller is able to act as a master as well as a slave. A few predefined settings must be known in advance and shared between the master and all the slaves on the same bus:

  • The clock polarity, indicating whether the clock tick corresponds to a raising or a falling edge of the clock
  • The clock phase, indicating whether the clock idle position is high or low
  • The length of the data packet, any value between 4 and 16 bits
  • The bit order, indicating whether the data is transmitted starting from the most significant bit or the least significant bit

Since the clock is synchronous and imposed by the master at all times, the SPI does not have a predefined frequency of operation, although using too high a speed might not work with all peripherals and microcontrollers.

SPI communication toward a slave is disabled until the master initiates a transaction. At the beginning of each transaction, the master selects the slave by activating its slave-select line:

Figure 7.4 – An additional signal may be used to select a specific slave on the bus

Figure 7.4 – An additional signal may be used to select a specific slave on the bus

To initiate the communication, the master must activate the clock, and may send a command sequence to the slave on the MOSI line. When the clock is detected, the slave can immediately start transferring bytes in the opposite direction using the MISO line.

Even if the master has finished transmitting, it must comply with the protocol implemented by the slave and permit it to reply by keeping the clock alive for the duration of the transaction. The slave is given a predefined number of byte slots to communicate with the master.

In order to keep the clock alive even when there is no data to transfer to the slave, the master can keep sending dummy bytes through the MOSI line, which are ignored by the slave. In the meantime, the slave is allowed to send data through the MISO line, as long as the master ensures that the clock keeps running. Unlike UART, in the master-slave communication model implemented in the SPI, the slaves can never spontaneously initiate SPI communication, as the master is the only device on the bus allowed to transmit a clock. Each SPI transaction is self-contained, and at the end, the slave is deselected by turning off the corresponding slave-select signal.

Programming the transceiver

On the reference board, an accelerometer is connected as a slave to the SPI1 bus, so we can examine how to implement the master side of the communication on the microcontroller by configuring the transceiver and executing a bidirectional transaction toward the peripheral.

The SPI1 bus has its configuration registers mapped in the peripherals region:

#define SPI1 (0x40013000)
#define SPI1_CR1 (*(volatile uint32_t *)(SPI1))
#define SPI1_CR2 (*(volatile uint32_t *)(SPI1 + 0x04))
#define SPI1_SR (*(volatile uint32_t *)(SPI1 + 0x08))
#define SPI1_DR (*(volatile uint32_t *)(SPI1 + 0x0c))

The peripheral exposes a total of four registers:

  • Two bit-field configuration registers
  • One status register
  • One bidirectional data register

It is clear that the interface is similar to that of the UART transceiver, as the configuration of the communication parameters goes through the SPI_CRx registers, the status of the FIFO can be monitored by looking at SPI_SR, and SPI_DR can be used to read and write data to the serial bus.

The value for the configuration register CR1 contains the following:

  • The clock phase, 0 or 1, in bit 0
  • The clock polarity in bit 1
  • The SPI master mode flag in bit 2
  • The bit rate scaling factor in bits 3-5
  • The SPI enable flag in bit 6
  • Other configuration parameters, such as the word length, LSB-first, and other flags, which will not be used in this example, as the default will be kept for these parameters

The CR2 configuration register contains the flags to enable the interrupt events and the DMA transfers, as well as the Slave Select Output Enable (SSOE) flag, which is relevant in this example.

The SPI1_SR status register is similar to the UART status register in the previous section, as it contains flags to determine whether the transmit FIFO is empty, and when the FIFO on the receiving side is not empty, to regulate the phases of the transfer.

The bits corresponding to the flags that are used in this example are defined as follows:

#define SPI_CR1_MASTER (1 << 2)
#define SPI_CR1_SPI_EN (1 << 6)
#define SPI_CR2_SSOE (1 << 2)
#define SPI_SR_RX_NOTEMPTY (1 << 0)
#define SPI_SR_TX_EMPTY (1 << 1)

The RCC controls the clock and the reset lines toward the SPI1 transceiver connected to the APB2 bus:

#define APB2_CLOCK_ER (*(volatile uint32_t *)(0x40023844))
#define APB2_CLOCK_RST (*(volatile uint32_t
      *)(0x40023824))
#define SPI1_APB2_CLOCK_ER_VAL (1 << 12)

The transceiver can be reset by sending a reset pulse from the RCC:

static void spi1_reset(void)
{
   APB2_CLOCK_RST |= SPI1_APB2_CLOCK_ER_VAL;
   APB2_CLOCK_RST &= ~SPI1_APB2_CLOCK_ER_VAL;
}

The PA5, PA6, and PA7 pins can be associated with the SPI1 transceiver by setting the appropriate alternate function:

#define SPI1_PIN_AF 5
#define SPI1_CLOCK_PIN 5
#define SPI1_MOSI_PIN 6
#define SPI1_MISO_PIN 7
static void spi1_pins_setup(void)
{
  uint32_t reg;
  AHB1_CLOCK_ER |= GPIOA_AHB1_CLOCK_ER;
  reg = GPIOA_MODE & ~(0x03 << (SPI1_CLOCK_PIN * 2));
  reg &= ~(0x03 << (SPI1_MOSI_PIN));
  reg &= ~(0x03 << (SPI1_MISO_PIN));
  reg |= (2 << (SPI1_CLOCK_PIN * 2));
  reg |= (2 << (SPI1_MOSI_PIN * 2)) | (2 << (SPI1_MISO_PIN
      *2))
  GPIOA_MODE = reg;
  reg = GPIOA_AFL & ~(0xf << ((SPI1_CLOCK_PIN) * 4));
  reg &= ~(0xf << ((SPI1_MOSI_PIN) * 4));
  reg &= ~(0xf << ((SPI1_MISO_PIN) * 4));
  reg |= SPI1_PIN_AF << ((SPI1_CLOCK_PIN) * 4);
  reg |= SPI1_PIN_AF << ((SPI1_MOSI_PIN) * 4);
  reg |= SPI1_PIN_AF << ((SPI1_MISO_PIN) * 4);
  GPIOA_AFL = reg;
}

The additional pin connected to the “chip select” line of the accelerometer is PE3, which is configured as output, with a pull-up internal resistor. The logic of this pin is active low so that a logical zero will turn the chip on:

#define SLAVE_PIN 3
static void slave_pin_setup(void)
{
  uint32_t reg;
  AHB1_CLOCK_ER |= GPIOE_AHB1_CLOCK_ER;
  reg = GPIOE_MODE & ~(0x03 << (SLAVE_PIN * 2));
  GPIOE_MODE = reg | (1 << (SLAVE_PIN * 2));
  reg = GPIOE_PUPD & ~(0x03 << (SLAVE_PIN * 2));
  GPIOE_PUPD = reg | (0x01 << (SLAVE_PIN * 2));
  reg = GPIOE_OSPD & ~(0x03 << (SLAVE_PIN * 2));
  GPIOE_OSPD = reg | (0x03 << (SLAVE_PIN * 2));
}

The initialization of the transceiver begins with the configuration of the four pins involved. The clock gate is then activated, and the transceiver receives a reset via a pulse through the RCC:

void spi1_setup(int polarity, int phase)
{
  spi1_pins_setup();
  slave_pin_setup();
  APB2_CLOCK_ER |= SPI1_APB2_CLOCK_ER_VAL;
  spi1_reset();

The default parameters (MSB-first, 8-bit word length) are left untouched. The bit rate scaling factor of this controller is expressed in powers of 2, starting with 2 corresponding to a bit field value of 0, and doubling at each increment. A generic driver should calculate the correct scaling factor, according to the desired clock rate and the peripheral clock frequency. In this simple case, we enforce a hardcoded scaling factor of 64, corresponding to the value 5.

SPI1_CR1 is then set as follows:

  SPI1_CR1 = SPI_CR1_MASTER | (5 << 3) | (polarity << 1) | 
      (phase << 0);

Finally, we set the bit corresponding to the SSOE flag in SPI1_CR2, and the transceiver is enabled:

  SPI1_CR2 |= SPI_CR2_SSOE;
  SPI1_CR1 |= SPI_CR1_SPI_EN;
}

Read and write operations can now begin, as both the master and slave SPI controllers are ready to perform the transactions.

SPI transactions

The read and write functions represent the two different phases of the SPI transaction. Most SPI slave devices are capable of communicating using a full-duplex mechanism so that bytes are exchanged in both directions while the clock is active. During each interval, a byte is transmitted in both directions, using the MISO and MOSI lines independently.

A common strategy, implemented by many slaves, consists of accessing registers for read and write operations in the slave devices, by using well-known command handles that are documented in the device’s datasheet.

The STM32F407DISCOVERY board has an accelerometer connected to the SPI1 bus, which responds to predefined commands accessing specific registers in the device memory for reading or writing. In these cases, the read and write operations are performed sequentially: during the first interval, the master transmits the command handle, while the device has nothing to transmit, then the actual bytes are transmitted in either direction at subsequent intervals.

The example operation described here consists of reading the WHOAMI register in the accelerometer, using the 0x8F command handle. The peripheral should respond with 1 byte containing the 0x3B value, which correctly identifies the device and proves that the SPI communication is working correctly. However, during the transmission of the command byte, the device has nothing to transmit yet, so the result of the first read operation can be discarded. Similarly, after sending the command, the master has nothing else to communicate to the slave, so it outputs a 0xFF value on the MOSI line while reading the byte transmitted by the slave through the MISO line at the same time.

The steps to perform to successfully perform a 1-byte read on this specific device are as follows:

  1. Turn on the slave by pulling down the slave-select signal.
  2. Send a byte containing the code for the 1-byte read operation.
  3. Send 1 dummy byte while the slave transfers the reply using the clock.
  4. Read back the value transferred from the slave during the second interval.
  5. Turn off the slave by pulling the slave-select signal back up.

To do so, we define blocking read and write functions as follows:

uint8_t spi1_read(void)
{
  volatile uint32_t reg;
  do {
    reg = SPI1_SR;
  } while ((reg & SPI_SR_RX_NOTEMPTY) == 0);
  return (uint8_t)SPI1_DR;
}
void spi1_write(const char byte)
{
  int i;
  volatile uint32_t reg;
  SPI1_DR = byte;
  do {
    reg = SPI1_SR;
  } while ((reg & SPI_SR_TX_EMPTY) == 0);
}

The read operation waits until the RX_NOTEMPTY flag is enabled on SPI1_SR before transferring the contents of the data register. The transmit function instead transfers the value of the byte to transmit onto the data register, and then polls for the end of the operation by waiting for the TX_EMPTY flag.

The two operations can now be concatenated. The master has to explicitly send 2 data bytes in total, so our main application can query the accelerometer identification register by doing the following:

 slave_on();
 spi1_write(0x8F);
 b = spi1_read();
 spi1_write(0xFF);
 b = spi1_read();
 slave_off();

This is what happens on the bus:

  • During the first write, the command 0x8F is sent to MOSI.
  • The value read using the first spi1_read function is the dummy bit that the slave has put into MISO while listening for the incoming command. The value obtained has no meaning in this particular case – therefore, it is discarded.
  • The second write puts the dummy bit on the MOSI line, as the master does not have anything else to transmit. This forces the clock generation for the second byte, which is needed by the slave to reply to the command.
  • The second read processes the reply transferred using the MISO line during the write of the dummy byte from the master. The value obtained in this second transaction is a valid reply from the slave, according to the description of the command in the documentation.

Looking at the serial transaction with the logic analyzer, we can clearly distinguish the two phases, and the alternate relevant content – first, on MOSI to transmit the command, and then on MISO to receive the reply:

Figure 7.5 – A bidirectional SPI transaction, containing a request from the master and a reply from the slave (from top to bottom: SPI1_MISO, SPI1_MOSI, SLAVE_SELECT, and SPI1_CLOCK)

Figure 7.5 – A bidirectional SPI transaction, containing a request from the master and a reply from the slave (from top to bottom: SPI1_MISO, SPI1_MOSI, SLAVE_SELECT, and SPI1_CLOCK)

Once again, using blocking operations with a busy loop is a very bad practice. The reason why it is shown here is to explain the primitive operations needed to successfully complete bidirectional SPI transactions. In a real embedded system, it is always recommended to use interrupt-based transfers to ensure that the CPU is not busy looping while waiting for the transfer to complete. SPI controllers provide interrupt signals to indicate the state of the FIFO buffers of the controller, in order to synchronize the SPI transaction with the actions required upon data transfers in either direction.

Interrupt-based SPI transfers

The interface to enable the interrupt for the SPI transceiver is in fact very similar to that of UART as seen in the previous section. In order for non-blocking transactions to be correctly implemented, they have to be split between their read and write phases to allow events to trigger the associated actions.

Setting these two bits in the SPI1_CR2 register will enable the interrupt trigger upon an empty transmit FIFO and a non-empty receive FIFO, respectively:

#define SPI_CR2_TXEIE (1 << 7)
#define SPI_CR2_RXNEIE (1 << 6)

The associated service routine, included in the interrupt vector, can still peek at the values in SPI1_SR to advance the transaction to the next phase:

void isr_spi1(void)
{
  volatile uint32_t reg;
  reg = SPI1_SR;
  if (reg & SPI_SR_RX_NOTEMPTY) {
    /* End of transmission: new data available on MISO*/
  }
  if ((reg & SPI_SR_TX_EMPTY)
  {
    /* End of transmission: the TX FIFO is empty*/
  }
}

Once again, the implementation of the top half of the interrupt is left to the reader, as it depends on the API that the system is required to implement, the nature of the transactions, and their impact on the responsiveness of the system. Short, high-speed SPI transactions, however, may be short and scattered in time so that even implementing blocking operations has a smaller influence on the system latency.

I2C bus

The third serial communication protocol analyzed in this chapter is I2C. From the communication strategy point of view, this protocol shares some similarities with SPI. However, the default bit rate for I2C communication is much lower, as the protocol privileges lower-power consumption over throughput.

The same two-wire bus can accommodate multiple participants, both masters and slaves, and there is no need for extra signals to physically select the slave of the transaction, as slaves have fixed logic addresses assigned:

Figure 7.6 – I2C bus with three slaves and external pull-up resistors

Figure 7.6 – I2C bus with three slaves and external pull-up resistors

One wire transports the clock generated by the master, and the other is used as a bidirectional synchronous data path. This is possible thanks to the unique mechanism of arbitration of the channel, which relies on the electronic design of the transceivers and may deal with the presence of multiple masters on the same bus in a very clean way.

The two signals must be connected to the high-level voltage of the bus (typically 3.3 V) using pull-up resistors. The controllers never drive the signal high and instead, they let it float to its default value imposed by the pull-ups while transmitting ones. As a consequence, logic level zero is always dominant; if any of the devices connected to the bus enforce a zero by pulling the line down, all the devices will read the line as low, no matter how many other senders are keeping the logic level 1 on the bus. This allows the bus to be controlled by multiple transceivers at the same time, and transmit operations can be coordinated by initiating new transactions only when the bus becomes available. In this section, we will see an introduction to the protocol, in order to introduce the software tools used to manage the I2C controller peripherals. More information on the I2C bus communication and the related documentation can be found at https://www.i2c-bus.org/.

Protocol description

The synchronization between the master and slave is achieved by a recognizable START condition and a STOP condition, which determine the beginning and the end of a transaction, respectively. The bus is initially idle, with both signals at the high logic state when all the participants are idling.

The START condition is the only case when SDA is pulled low before SCL by the master. The special condition communicates to slaves and other masters on the bus that a transaction is initiated. A STOP condition can be identified by the SDA transaction from a low to a high level, while the SCL remains high. After a STOP condition, the bus is idle again, and initiating communication is only possible if a new START condition is transmitted.

A master sends a START condition by pulling SDA and SCL low in this order. A frame is composed of nine clock periods. After the edge of each clock pulse is raised, the level of SDA does not change until the clock is low again. This allows us to transmit 1 frame of 8 bytes in the first 8 clock raise fronts. During the last clock pulse, the master does not drive the SDA line, which is then held high by the pull-up resistor. Any receiver that wants to acknowledge the reception of the frame can drive the signal low. This condition on the ninth clock pulse is known as ACK. If no receiving device acknowledges the frame, SDA remains high, and the sender understands that the frame did not reach the intended destination:

Figure 7.7 – A single-byte I2C transaction on the bus, with the correct START and STOP conditions and the ACK flag set by the receiver

Figure 7.7 – A single-byte I2C transaction on the bus, with the correct START and STOP conditions and the ACK flag set by the receiver

A transaction consists of two or more frames and is always initiated by a device operating in master mode. The first frame of each transaction is called the address frame and contains the address and the mode for the next operation. All the subsequent frames in the transaction are data frames, containing 1 byte each. The master decides how many frames compose the transaction and the direction of the data transfer by keeping the transaction active for the desired amount of frames before enforcing a STOP condition.

Slave devices have fixed 7-bit addresses where they can be contacted using the bus. A slave that notices a START condition on the bus must listen for the address frame and compare it with its address. If the address matches, the address frame must be acknowledged by pulling the SDA line low during the ninth clock pulse within the transmission of the frame.

Data is always transferred with the leading Most Significant Bit (MSB), and the format for the address frame is the following:

Figure 7.8 – Format of the address frame containing a destination 7-bit address and the R/W̅ flag

Figure 7.8 – Format of the address frame containing a destination 7-bit address and the R/ flag

The preceding diagram shows the format used by the address frame. The R/W̅ bit is set by the master to indicate the direction of the transaction. R/W̅ reads as read, not write, meaning that a value of 0 indicates a write operation, and a value of 1 indicates a read operation. Depending on the value of this bit, the data bytes following the transactions are either flowing toward the slave (a write operation) or from the selected slave to the master (a read operation). In a read operation, the direction of the ACK bit is also inverted for the data frames following the selection of the slave, and the master is supposed to acknowledge each frame received within the transaction. The master can decide to abort the transmission at any time by not pulling down the ACK bit on the last frame, and enforcing a STOP condition afterward.

The transaction continues after the transfer of the address frame, and the data can be transferred using subsequent data frames, each containing 1 byte, that can be acknowledged by the receiver. If the value of the R/W̅ bit in the address frame is set to 0, the master intends to initiate a write operation. Once the slave has acknowledged the address frame by recognizing itself as the destination, it is ready to receive data, and acknowledges data frames, until the master sends the STOP condition.

The I2C protocol specifies that if a START condition is repeated at the end of a transaction, instead of sending the STOP condition, a new transaction can be started right away without setting the bus to its idling state. A repeated START condition ensures that two or more transactions can be performed on the same bus without interruptions, for example, preventing another master from starting a communication between them.

A less popular format foresees 10-bit addresses for the slaves. 10-bit addresses are an extension of the standard, introduced at a later time, that provide compatibility with 7-bit addressable devices on the same bus. The address is selected using 2 consecutive frames, and the first 5 bits, A6-A2, in the first frame are set to 11110 to indicate the selection of a 10-bit address. As per the protocol specification, addresses starting with 0000 or 1111 are reserved and must not be used by slaves. In the 10-bit format, the most significant 2 bits are contained in A1 and A0 of the first frame, while the second frame contains the remaining 8 bits. The R/W̅ bit keeps its position in the first frame. This addressing mechanism is not very common, as only a few slave devices support it.

Clock stretching

We have observed that the master is the only one driving the SCL signal during I2C transactions. This is always true, except when the slave is not yet ready to transmit the requested data from the master. In this particular case, the slave may decide to delay the transaction by keeping the clock line pulled low, which results in the transaction being put on hold. The master recognizes its inability to oscillate the clock, as releasing the SCL signal to a floating state does not result in a change to a high logic level on the bus. The master will keep trying to release the SCL signal to its natural high position until the requested data is finally available on the slave, which eventually releases the hold on the line.

The transmission can now resume after being kept on hold for an indefinite amount of time, and the master is still expected to produce the nine clock pulses to conclude the transmission. Because no more frames are expected within this transaction, the master does not pull the ACK bit low in the end, and sends the STOP condition instead to correctly complete the transaction:

Figure 7.9 – I2C read transaction with the reply frame delayed by the slave using the clock-stretching technique

Figure 7.9 – I2C read transaction with the reply frame delayed by the slave using the clock-stretching technique

Even though not all devices support clock stretching, this mechanism is useful to complete transactions when the requested data is slightly late. Clock stretching is a very unique feature of I2C, making it a very versatile protocol to communicate with sensors and other input peripherals. Clock stretching is very important to communicate with slower devices that cannot provide the values to complete the transaction in time. It is advisable that this feature is correctly supported by a master device that is designed to communicate with generic I2C slaves. On the slave side, to enforce clock stretching, the device must provide a hardware configuration that allows us to keep the SCL line at a logical low value until it is ready again. This means that the SCL line must be bidirectional in this particular case, and the slave should be designed to access it to enforce a pull-down to keep the transaction alive while preparing the transfer of the next frame.

Multiple masters

I2C offers a deterministic mechanism to detect and react to the presence of multiple masters on the bus, which is, again, based on the electrical property of the SDA line.

Before initiating any communication, the master ensures that the bus is available by sensing the SDA and SCL lines. The way the START condition is designed can already rule out most of the conflicts. Concurrent start conditions can be interrupted whenever the SDA line is sensed low in the initial grace time between the two edges. This mechanism alone does not prevent two I2C masters from accessing the channel at the same time, because conflicts are still possible due to the propagation time of the signal across the wire.

Two master devices that initiate a transaction at the same time continuously compare the status of the line, after each bit has been transmitted. In the case of two masters perfectly synchronized for two different transmissions, the first bit with a different value on the two sources will only be noticed by the master transmitting a 1 value, because the expected value is not reflected by the actual line status. That master aborts the transaction immediately, and the transmitter can detect the error as a conflict on the network, which, in this context, means that the arbitration was lost in favor of another master. Meanwhile, the other master will not notice anything, and neither will the slaves, because the transaction will continue despite the silently contended bus line.

Programming the controller

Microcontrollers may provide one or more I2C controllers on board that can be bound to specific pins using alternate functions. On our reference board, to enable the I2C1 bus, we activate the clock gating and start the initialization procedure by accessing the control, data, and status register mapped in the peripheral memory region:

#define APB1_CLOCK_ER (*(volatile uint32_t *)(0x40023840))
#define APB1_CLOCK_RST (*(volatile uint32_t *)(0x40023820))
#define I2C1_APB1_CLOCK_ER_VAL (1 << 21)

The I2C1 controller on the STM32F407 is associated with pins PB6 and PB9 when they are configured with the AF 4 alternate function:

#define I2C1_PIN_AF 4
#define I2C1_SCL 6
#define I2C1_SDA 9
#define GPIO_MODE_AF (2)
static void i2c1_pins_setup(void)
{
  uint32_t reg;
  AHB1_CLOCK_ER |= GPIOB_AHB1_CLOCK_ER;
  /* Set mode = AF */
  reg = GPIOB_MODE & ~(0x03 << (I2C1_SCL * 2));
  reg &= ~(0x03 << (I2C1_SDA * 2));
  GPIOB_MODE = reg | (2 << (I2C1_SCL * 2)) | 
      (2 << (I2C_SDA * 2));
  /* Alternate function: */
  reg = GPIOB_AFL & ~(0xf << ((I2C1_SCL) * 4));
  GPIOB_AFL = reg | (I2C1_PIN_AF << ((I2C1_SCL - 8) * 4));
  reg = GPIOB_AFH & ~(0xf << ((I2C1_SDA - 8) * 4));
  GPIOB_AFH = reg | (I2C1_PIN_AF << ((I2C1_SDA - 8) * 4));
}

The initialization function accesses the configuration registers of the I2C controller, mapped in the peripheral region. After the pin configuration and the RCC startup sequence, the transceiver speed is calibrated by using the frequency of the APB1 bus clock, in MHz. When the clocks are calibrated, the transceiver is enabled by setting a bit in the CR1 register. The parameters used here configure the master bus clock to run at 400 kHz. While the default setting for the protocol foresees a clock of 100 kHz, the 400 kHz option was added later on, and is now supported by many devices:

#define I2C1 (0x40005400)
#define APB1_SPEED_IN_MHZ (42)
#define I2C1_CR1 (*(volatile uint32_t *)(I2C1))
#define I2C1_CR2 (*(volatile uint32_t *)(I2C1 + 0x04))
#define I2C1_OAR1 (*(volatile uint32_t *)(I2C1 + 0x08))
#define I2C1_OAR2 (*(volatile uint32_t *)(I2C1 + 0x0c))
#define I2C1_DR (*(volatile uint32_t *)(I2C1 + 0x10))
#define I2C1_SR1 (*(volatile uint32_t *)(I2C1 + 0x14))
#define I2C1_SR2 (*(volatile uint32_t *)(I2C1 + 0x18))
#define I2C1_CCR (*(volatile uint32_t *)(I2C1 + 0x1c))
#define I2C1_TRISE (*(volatile uint32_t *)(I2C1 + 0x20))
#define I2C_CR2_FREQ_MASK (0x3ff)
#define I2C_CCR_MASK (0xfff)
#define I2C_TRISE_MASK (0x3f)
#define I2C_CR1_ENABLE (1 << 0)
void i2c1_setup(void)
{
  uint32_t reg;
  i2c1_pins_setup();
  APB1_CLOCK_ER |= I2C1_APB1_CLOCK_ER_VAL;
  I2C1_CR1 &= ~I2C_CR1_ENABLE;
  i2c1_reset();
  reg = I2C1_CR2 & ~(I2C_CR2_FREQ_MASK);
  I2C1_CR2 = reg | APB1_SPEED_IN_MHZ;
  reg = I2C1_CCR & ~(I2C_CCR_MASK);
  I2C1_CCR = reg | (APB1_SPEED_IN_MHZ * 5);
  reg = I2C1_TRISE & ~(I2C_TRISE_MASK);
  I2C1_TRISE = reg | APB1_SPEED_IN_MHZ + 1;
  I2C1_CR1 |= I2C_CR1_ENABLE;
}

From this moment on, the controller is ready to be configured and used, either in master or slave mode. Data can be read and written using I2C1_DR, in the same way as SPI and UART. The main difference here is that, for a master I2C device, the START and STOP conditions must be manually triggered by setting the corresponding values in the I2C1_CR1 register. Functions such as the following are intended for this purpose:

static void i2c1_send_start(void)
{
  volatile uint32_t sr1;
  I2C1_CR1 |= I2C_CR1_START;
  do {
    sr1 = I2C1_SR1;
  } while ((sr1 & I2C_SR1_START) == 0);
}
static void i2c1_send_stop(void)
{
  I2C1_CR1 |= I2C_CR1_STOP;
}

At the end of each condition, the bus must be tested for possible errors or abnormal events. The combination of the flags in I2C1_CR1 and I2C1_CR2 must reflect the expected status for the transaction to continue, or it must be gracefully aborted in the case of timeouts or unrecoverable errors.

Due to the complexity caused by the high number of events possible during the setup of the transaction, it is necessary to implement a complete state machine that keeps track of the phases of the transmission to use the transceiver in master mode.

As a demonstration of basic interactions with the transceiver, we can write a sequential interaction with the bus, but a real-life scenario would require us to keep track of the state of each transaction and react to the many scenarios possible within the combination of the flags contained in I2C1_SR1 and I2C1_SR2. This sequence initiates a transaction toward an I2C slave with an address of 0x42, and if the slave responds, it sends 2 bytes with values of 0x00 and 0x01, respectively. The only purpose of this sequence is to show the interaction with the transceiver, and it does not recover from any of the possible errors. At the beginning of the transaction, we zero the flags related to the ACK or the STOP condition, and we enable the transceiver using the lowest bit in CR1:

void i2c1_test_sequence(void)
{
  volatile uint32_t sr1, sr2;
  const uint8_t address = 0x42;
  I2C1_CR1 &= ~(I2C_CR1_ENABLE | I2C_CR1_STOP |
      I2C_CR1_ACK);
  I2C1_CR1 |= I2C_CR1_ENABLE;

To ensure that no other master is occupying the bus, the procedure hangs until the busy flag is cleared in the transceiver:

  do {
    sr2 = I2C1_SR2;
  } while ((sr2 & I2C_SR2_BUSY) != 0);

A START condition is sent, using the function defined earlier, which will also wait until the same START condition appears on the bus:

  i2c1_send_start();

The destination address is set to the highest 7 bits of the byte we are about to transmit. The lowest bit is off as well, indicating a write operation. To proceed after a correct address selection that has been acknowledged by the receiving slave, two flags must be set in I2C1_SR2, indicating that the master mode has been selected and the bus is still taken:

  I2C1_DR = (address << 1);
  do {
    sr2 = I2C1_SR2;
  } while ((sr2 & (I2C_SR2_BUSY | I2C_SR2_MASTER)) !=
          (I2C_SR2_BUSY | I2C_SR2_MASTER));

The data communication with the slave has now been initiated, and the 2 data bytes can be transmitted. The TX FIFO EMPTY event indicates when each byte has been transferred within a frame in the transaction:

  I2C1_DR = (0x00);
  do {
    sr1 = I2C1_SR1;
  } while ((sr1 & I2C_SR1_TX_EMPTY) != 0);
  I2C1_DR = (0x01);
  do {
    sr1 = I2C1_SR1;
  } while ((sr1 & I2C_SR1_TX_EMPTY) != 0);

Finally, the STOP condition is set, and the transaction is over:

  i2c1_send_stop();
}

Interrupt handling

The event interface of the I2C controller on the reference target is complex enough to provide two separate interrupt handlers for each transceiver. The suggested implementation for a generic I2C master includes a proper interrupt setup and the definition of all the combinations between states and events. The I2C controller can be configured to associate interrupts with all the relevant events happening on the bus, allowing for the fine-tuning of specific corner cases, and a more-or-less complete implementation of the I2C protocol.

That brings us to the end of this chapter.

Summary

This chapter has given us the necessary information to start programming system support for the most popular local bus communication interfaces available on embedded targets. Accessing peripherals and other microcontrollers in the same geographical location is one of the typical requirements of embedded systems interacting with sensors, actuators, and other devices in proximity of the embedded system.

Several implementations providing a higher level of abstraction to the transceivers analyzed here already exist. The serial communication protocols covered in this chapter, namely UART, SPI, and I2C, are usually accessible through drivers that are part of the board support kit and do not need to be reimplemented from scratch. This chapter, however, purposely focused on studying the behavior of the components from the closest possible point of view, to better understand the interface provided by the hardware manufacturer, and possibly provide the tools to design new ways of accessing the interfaces, tailored or optimized, for a specific platform or scenario, while also understanding the choices behind some of the protocol design characteristics.

In the next chapter, we will describe the mechanisms used to reduce the power consumption of embedded systems by studying the low-power and ultra-low-power features present in modern embedded devices.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset