Serial Communication¶
Learning objectives: Two methods for implementing serial communication in an embedded device.
Serial communication refers to techniques used for data transfer between devices or components, where data is transmitted serially, meaning bit by bit over a single line. Data can be transferred in one direction using separate lines for transmission and reception, Alternatively, the same line can be used for both transmission and reception, switching between the two as needed. Additionally, serial communication often uses a clock line to synchronize the data transfer.
The diagram below illustrates the basic idea of asynchronous serial communication. Two devices define their I/O pins for transmission (Tx pin) and reception (Rx pin). The devices are connected so that the transmission of one is received by the other. This way, the bit sequence from the sender (the states of the Tx pin over time) appears on the receiver's Rx pin.
Data is transmitted as a bitstream over the data lines between devices, requiring both ends to understand the message format (the meaning of each bit). This is what is known as the communication protocol. In addition both end should be aware of the data transfer speed. The actual information is transmitted within the message frame of the communication protocol, but additional data, such as error-correction information, may also be included. Serial communication protocols are typically standardized, which allows devices from different manufacturers to communicate. The transfer speed determines the length of time each bit stays on the line. If this is not known by both ends, the receiver cannot properly interpret the bitstream, because it doesn't know where each bit begins and ends and hence do not know when to read the bit value. If we have a clock line the transfer speed is coordinated by this line.
In this course, we won’t delve deeply into the specifics of serial communication. There are many more or less standardized implementations, and we will introduce (and use) the general serial communication protocol UART for SensorTag and the I2C protocol, commonly used with integrated sensors. I2C is very common and fast (clock speeds of 100-400 kHz) in embedded systems.
An alternative to serial communication, previously mentioned, is parallel communication, used in systems like the address, control, and data buses of a computer, where multiple bits are transferred simultaneously over separate lines. While parallel communication is faster, it requires more lines and I/O pins. In contrast, serial communication is more efficient in terms of hardware, and its transfer speeds have significantly improved over time.
Universal Asynchronous Receiver/Transmitter¶
The Universal Asynchronous Receiver/Transmitter (UART) is a serial communication circuit that converts parallel data into serial data for communication with peripheral devices. With UART, we can implement, for example, the well-known RS-232 standard for data transmission. UART is a versatile, slightly older technology, but still commonly used in embedded systems for ASCII/text-based bidirectional communication. UART can also be used for transmitting binary ("numerically encoded") data, such as writing to the program memory of an embedded device during firmware updates.
A typical example of a UART-based peripheral device in embedded systems is a GPS receiver, which transmits coordinates in human-readable NMEA text format. Another example could be a small command interpreter created by the programmer for UART-based serial communication between a PC and an embedded device. Of course, CSV-format data can also be transferred via UART.
Without getting too deep into protocol implementation, here are a few communication parameters that must be set for UART-based serial communication:
- Transfer speed (baud rate). Common speeds are 9600, 19200, 38400, 57600, and 115200 bits per second.
- Number of data bits: always 8 in this course.
- Parity bit: not used in this course.
- Stop bit: always 1 in this course.
Thus, the serial communication parameters are abbreviated as 9600 8n1. It is essential to configure both the sender and receiver with the same parameters to ensure they understand each other. The advantage here is that when the parameters are known, there is no need for a clock line.
Note! When communicating with a workstation, the corresponding speed and settings must be configured for its serial port (COM1, /dev/ttyS, etc.). SensorTag drivers create a logical serial port from a USB connection on the workstation (named XDS110 Class Application/User UART), which can be used to communicate with the device using a terminal program. We will practice this in the lab.
TI-RTOS UART¶
Next, we will go through an example of how to use the RTOS UART library for serial communication.
In the following
serialTask
, we:- First, initialize the serial communication with parameters (9600, 8n1) in the RTOS structure
UART_params
. - Open the connection using the
UART_open
function. The idea is to open the connection once in the task before the infinite loop, keeping the connection open throughout. If the connection needs to be closed for any reason, useUART_Close
. - In the infinite loop, the
UART_Read
function waits for a character to be sent to the device over the serial connection. Here, the parameters are a character variable and the expected length of 1 character. - When the character arrives, the
UART_write
function sends it back through the serial communication, embedding it in a string. The parameters passed to the function are the string itself and its length.
#include <string.h>
#include <ti/drivers/UART.h>
...
// Task function
Void serialTask(UArg arg0, UArg arg1) {
char input;
char echo_msg[30];
// UART library settings
UART_Handle uart;
UART_Params uartParams;
// Initialize serial communication
UART_Params_init(&uartParams);
uartParams.writeDataMode = UART_DATA_TEXT;
uartParams.readDataMode = UART_DATA_TEXT;
uartParams.readEcho = UART_ECHO_OFF;
uartParams.readMode = UART_MODE_BLOCKING;
uartParams.baudRate = 9600; // 9600 baud rate
uartParams.dataLength = UART_LEN_8; // 8
uartParams.parityType = UART_PAR_NONE; // n
uartParams.stopBits = UART_STOP_ONE; // 1
// Open connection to device's serial port defined by Board_UART0
uart = UART_open(Board_UART0, &uartParams);
if (uart == NULL) {
System_abort("Error opening the UART");
}
// Infinite loop
while (1) {
// Receive one character at a time into the input variable
UART_read(uart, &input, 1);
// Send the string back
sprintf(echo_msg, "Received: %c\n", input);
UART_write(uart, echo_msg, strlen(echo_msg));
// Politely sleep for one second
Task_sleep(1000000L / Clock_tickPeriod);
}
}
int main(void) {
...
// Enable serial port in the program
Board_initGeneral();
Board_initUART();
...
return 0;
}
Serial communication via UART on SensorTag is simple, involving the use of read and write functions once the serial connection is opened.
Let’s take a closer look at some members of the
UART_params
structure:readDataMode / writeDataMode
sets the type of data used in serial communication. Options:- Setting
UART_DATA_TEXT
is for human-readable ASCII text, such as CSV-formatted messages. - Setting
UART_DATA_BINARY
is used for transmitting binary data. In this case, the message is not treated as a string but as binary numbers. Typically, a parser function is written to interpret the numeric values. readMode
determines whether serial communication is blocking, with the settingUART_MODE_BLOCKING
. This means the program stops in theUART_read
function until the expected amount of data is available through the serial connection. You may want to return at certain time. For that you can setup theuartParams.readTimeout = number_of_clock_ticks
. When the timeout expires theUART_read
reads the number of bytes read in the buffer.- The UART library also allows implementing serial communication based on interrupts, eliminating the need to stop and wait. Instead, an interrupt signal indicates when data is available. More on this later...
- The
readEcho
setting determines whether the transmitted data is automatically echoed back to the sender. In this example, we don't use automatic echo but handle it programmatically for demonstration purposes.
Finally, the serial connection should be closed using the
UART_Close
function, though in this example, it is not needed because we operate in an infinite loop.Serial Interrupt¶
In the previous section, we discussed how serial port data can be read using the
UART_read
function, where the task waits for data to arrive (blocking). However, this method is not the most efficient way to wait for data from the UART because the task stops. From the MCU’s perspective, UART-based serial communication is slow, so a better approach is to use an alternative method.A better way is to configure the serial communication as non-blocking, where an interrupt is triggered only when new data is available. This is achieved by modifying the UART settings and, of course, creating a handler.
uint8_t uartBuffer[30]; // Receive buffer
// Handler function
static void uartFxn(UART_Handle handle, void *rxBuf, size_t len) {
// We now have the desired amount of characters available
// in the rxBuf array, with a length of len, which we can process as needed.
// Here, we pass them as arguments to another function (for demonstration purposes).
process_data_quickly(rxBuf, len);
// After processing, wait for the next interrupt...
UART_read(handle, rxBuf, 1);
}
static void uartTask(UArg arg0, UArg arg1) {
UART_Handle handle;
UART_Params params;
UART_Params_init(¶ms);
params.baudRate = 9600;
params.readMode = UART_MODE_CALLBACK; // Interrupt-based reception
params.readCallback = &uartFxn; // Handler function
params.readDataMode = UART_DATA_TEXT;
params.writeDataMode = UART_DATA_TEXT;
// Enable UART in the program
handle = UART_open(Board_UART, ¶ms);
if (handle == NULL) {
System_abort("Error opening the UART");
}
// Start waiting for data
UART_read(handle, uartBuffer, 1);
while(1) {
// Infinite loop
}
}
Int main() {
...
Board_initUART();
...
}
In this example, we initialize the RTOS's
UART
library within the task, rather than in the main
function. The idea is to separate serial communication into its own task.The difference from the previous initialization is the new setting
params.readMode = UART_MODE_CALLBACK
, which changes the library's behavior so that the callback function uartFxn
is called whenever data is available. In the function definition, the parameters—receive buffer rxBuf
and the number of received characters len
—allow us to access the received data in the interrupt handler.I2C Bus¶
The I2C serial communication bus is a well-known **binary** serial communication protocol for embedded devices.
The I2C bus operates based on the controller - target (previously known as master/slave) architecture, which has been used in computing since its early days. On the bus, there is always a controlling **controller** device and n number of **target** devices. Here, the controller initiates communication and sets the data transfer rate, which the target devices follow. I2C requires two I/O pins: the clock (Serial Clock Line, SCL) and the data line (Serial Data Line, SDA).
Now, on the I2C bus, devices/components are identified by **addresses**. The address is predetermined by the component manufacturer, and we can find it in the component's datasheet. Sometimes, the manufacturer offers a range of I2C addresses for the component, allowing the user to set the address. This is useful when there are several identical components on the same bus, such as sensors. In one I2C bus, up to 1008 devices can be connected!
I2C **messages** consist generally of three parts: **receiver address**, **register address**, and **data**.
- Receiver address: The length is typically one byte (8 bits).
- Message content: Register address (8 bits), data (n bits).
- The controller sends a command to the one register of the device. The command could be, for example, retrieving certain value generated by a target device. For that it indicates the register which contains that informatin. .
- The master requests data from the slave's data register.
- The slave sends the data to the master. The length of the data field can vary from one to several bytes, depending on the device's protocol.
Below is an example of the frame structure of I2C messages, for reference.
I2C on SensorTag¶
Next, let's look at the usage of the
i2c
library provided by RTOS through a code example. In this example, we read the temperature 10 times from the TMP007 temperature sensor integrated into the SensorTag. As we can see, the sensor’s datasheet is filled with complex information that we don’t need to dive into during the course. However, we are interested in the usage of the sensor’s registers (chapter 7.5 in the datasheet) via the I2C bus. But now we will do this below, based on predefined constants.// 1. Include the I2C library in the program
#include <ti/drivers/I2C.h>
// Task function
Void sensorTask(UArg arg0, UArg arg1) {
uint8_t i;
float temperature;
// RTOS I2C variables and initialization
I2C_Handle i2c;
I2C_Params i2cParams;
// Variable for the I2C message structure
I2C_Transaction i2cMessage;
// Initialize the I2C bus
I2C_Params_init(&i2cParams);
i2cParams.bitRate = I2C_400kHz;
// Open the connection
i2c = I2C_open(Board_I2C_TMP, &i2cParams);
if (i2c == NULL) {
System_abort("Error Initializing I2C\n");
}
// Transmit and receive buffers for I2C messages
uint8_t txBuffer[1]; // Sending one byte
uint8_t rxBuffer[2]; // Receiving two bytes
// I2C message structure
i2cMessage.slaveAddress = Board_TMP007_ADDR;
txBuffer[0] = TMP007_REG_TEMP; // Register address to the transmit buffer
i2cMessage.writeBuf = txBuffer; // Set transmit buffer
i2cMessage.writeCount = 1; // Transmitting 1 byte
i2cMessage.readBuf = rxBuffer; // Set receive buffer
i2cMessage.readCount = 2; // Receiving 2 bytes
while (1) {
// Send the message using I2C_Transfer function
if (I2C_transfer(i2c, &i2cMessage)) {
// Convert the 2-byte data in rxBuffer
// to temperature (formula in the exercises)
temperature = ...;
// Display the temperature value in the console
sprintf(merkkijono,"...",temperature);
System_printf(merkkijono);
System_flush();
}
else {
System_printf("I2C Bus fault\n");
System_flush();
}
// Task goes to sleep!
Task_sleep(1000000 / Clock_tickPeriod);
}
// Close the I2C connection, although the infinite loop never reaches this
I2C_close(i2c);
}
int main(void) {
...
// Include the bus in the program
Board_initI2C();
...
}
Let’s break down the example..
Initialization and Setup¶
Similar to other peripheral components, RTOS also requires its own variables for using the I2C bus. Well, it’s important to keep RTOS happy! Here we introduce the data structure type
I2C_Params
, where the bus settings are placed. Additionally, we have a variable of type I2C_Transaction
, where the I2C messages to be transmitted are created.Note that the variables are inside the task
sensorTask
. The idea is that this task will handle all the I2C communication with the sensors in the program.Void sensorTask(UArg arg0, UArg arg1) {
...
// RTOS I2C variables
I2C_Handle i2c;
I2C_Params i2cParams;
I2C_Transaction i2cMessage;
...
}
Next, the I2C bus is initialized for use in our program in the
main
function with the call to Board_initI2C
.#include <ti/drivers/I2C.h>
...
int main(void) {
...
Board_initI2C();
...
}
The bus is opened in the task using the
I2C_open
call, with the id of the bus where the sensor is connected, in this case, the constant Board_I2C_TMP
. SensorTag also has another I2C bus on a different pin, dedicated to the MPU. More on this later ...Void sensorTask(UArg arg0, UArg arg1) {
...
i2c = I2C_open(Board_I2C_TMP, &i2cParams);
if (i2c == NULL) {
System_abort("Error Initializing I2C\n");
}
...
}
Communication on the I2C Bus¶
In this example, we are requesting temperature measurement values from the TMP007 temperature sensor. Below is the I2C message structure to be created for this purpose:
...
// Send and receive buffers for I2C messages
uint8_t txBuffer[1]; // Sending one byte
uint8_t rxBuffer[2]; // Receiving two bytes
// I2C message structure
i2cMessage.slaveAddress = Board_TMP007_ADDR;
txBuffer[0] = TMP007_REG_TEMP; // Register address which we are sending to the TMP007 -> Place where data we want is located.
i2cMessage.writeBuf = txBuffer; // Set the send buffer
i2cMessage.writeCount = 1; // Send 1 byte
i2cMessage.readBuf = rxBuffer; // Set the receive buffer
i2cMessage.readCount = 2; // Receive 2 bytes
...
First, we define the buffers (i.e., storage locations for messages) as arrays. Later, they are assigned as members of the data structure
i2cMessage
:- Send buffer
txBuffer
, the size of which is specified in the datasheet (or, in our case, the course material). Typically, for read operations, we only need to send the address of the register we want to read. - In this example, we set the register address for the TMP007 sensor’s data register in the send buffer using
txBuffer[0] = TMP007_REG_TEMP;
, a constant provided in the libraries. - Receive buffer
rxBuffer
, where the received I2C message is stored. Similarly, the size of the buffer is indicated in the datasheet. - From the course material, we know that the TMP007 sensor returns the temperature value as a 16-bit number, so the size of the receive buffer is two bytes.
Next, we fill out the I2C message structure. We need to set the buffers for sending the message and receiving the response in the I2C message structure:
i2cMessage.writeBuf = txBuffer; // Set the send buffer
i2cMessage.writeCount = 1; // Send 1 byte
i2cMessage.readBuf = rxBuffer; // Set the receive buffer
i2cMessage.readCount = 2; // Receive 2 bytes
Note! It’s also possible to create I2C messages that don’t receive any data. For instance, some specific commands. In such cases, leave the
readBuf
and readCount
fields undefined.Once the message structure is properly filled out, we can send the actual message to the bus using the
I2C_transfer
function call: if (I2C_transfer(i2c, &i2cMessage)) {
// Convert the 2-byte data in rxBuffer
// to a temperature (formula in the exercises)
temperature = ...;
// Print the temperature value to the console
sprintf(debug_msg,"...",temperature);
System_printf(debug_msg);
System_flush();
}
else {
System_printf("I2C Bus fault\n");
System_flush();
}
...
If the message is successful, the 16-bit value from the sensor’s data register will now be stored in the two bytes of the receive buffer
rxBuffer
. The programmer’s task is to convert the value in rxBuffer
into a temperature. The formula for this is provided in the exercises and later in the course material. If the message fails for some reason, or if another issue occurs, an error message is printed to the console.Closing the I2C Connection¶
There may be a need to close the I2C connection in the program. For example, when we begin using the SensorTag’s second I2C bus later in the project.
...
I2C_close(i2c);
...
If we forget to close the connection and it remains open, using the second bus won’t work because the I2C resources remain allocated to the open connection. We’ll revisit using the second I2C bus in later course material.
Conclusion¶
Serial communication in the embedded world is a considerably complex topic, with various protocols and implementations, but with the information provided here, we can have a basic control the SensorTag sensors being able of reading its data.
In addition to the I2C protocol, SPI is another well-known serial communication protocol used in embedded systems. The choice of protocol depends, of course, on the decisions made by the component manufacturer.
The examples presented here didn’t cover adjusting the settings of the temperature sensor or the calibration of the sensor, because we know that the RTOS firmware driver initializes the sensor for us. Of course, this behavior can be modified with custom code, as long as you know what you’re doing with registers and bit operations.