Using FreeRTOS with the Raspberry Pi Pico and AWS IoT ExpressLink: Part 5

By Daniel Gross

Senior Developer Advocate

Amazon Web Services

February 08, 2023

Blog

Using FreeRTOS with the Raspberry Pi Pico and AWS IoT ExpressLink: Part 5

In this blog, we will explore using FreeRTOS with the Raspberry Pi Pico and AWS IoT ExpressLink.

As a continuation of the series where we have been exploring writing embedded applications for the Raspberry Pi Pico using FreeRTOS, this blog will show how to add cloud connectivity to an embedded application through the use of an AWS IoT ExpressLink connectivity module. By adding cloud connectivity, we can turn our embedded device into an Internet of Things (IoT) device. We will explain what AWS IoT ExpressLink is, how to use it, and why it is a great match for the Raspberry Pi Pico and FreeRTOS in an IoT design. This will be demonstrated with sample code utilizing the Raspberry Pi Pico SDK.

As covered previously, the Raspberry Pi Pico is based on the RP2040, a dual-core Arm-based microcontroller. FreeRTOS is a real-time operating system that includes useful features like multitasking, queues, message buffers, semaphores, and symmetric multiprocessing (SMP). So, what is AWS IoT ExpressLink? AWS IoT ExpressLink is a module that includes both hardware and software with networking built-in for cloud connectivity. It is designed to be paired with a host processor like the RP2040 to run IoT applications. The Raspberry Pi Pico is an excellent choice to use with AWS IoT ExpressLink because it does not come with onboard networking. AWS IoT ExpressLink modules are produced by several vendors including Espressif, Infineon, Realtek, and u-blox based on a common specification. The specification defines the hardware and software interfaces, as well as security requirements to connect to AWS securely. For this blog, any WiFi-based AWS IoT ExpressLink development kit is suitable, so use whichever is available to you from this list.

Starting with the hardware, all AWS IoT ExpressLink modules have a serial UART interface. The Raspberry Pi Pico has two UARTs available, so we will use one of the them to connect an AWS IoT ExpressLink module, making the Raspberry Pi Pico our host processor. Depending on the module you choose, the wiring may differ slightly. Whichever module is used, there will be an RX pin to receive data and a TX pin to send data. There will also be pins to connect for ground and 5v or 3.3v power, which are both supported by the Raspberry Pi Pico. Consult the Raspberry Pi Pico datasheet and the module’s data sheet to make the correct connections. Note, there are also three optional pins for reset, wake, and event functions defined by the AWS IoT ExpressLink specification which we will not cover in this blog.

Here is a wiring example:

Once an AWS IoT ExpressLink module is physically connected to the Raspberry Pi Pico, how do we use it? The specification defines an AT command set that can be used over the serial UART connection, which is our API into the functions supported by AWS IoT ExpressLink. Remember dial-up modems, circa 1998? Well, the AWS IoT ExpressLink AT command set is a concise and cloud-centric version of AT commands that look similar to the commands used with modems. For example, the command ‘AT+CONNECT’ will establish a networking connection to AWS IoT Core so that the embedded device can interact with AWS cloud services. In this way, a host application running on the Raspberry Pi Pico can use the connected module as a peripheral to send and receive data as needed for the IoT device.

In practice, how do we write an embedded application for the Raspberry Pi Pico to send AT commands and receive data over the UART? With the Raspberry Pi Pico SDK, there are a number of UART-related functions available that we can call to initialize and use the UART within an application for the Raspberry Pi Pico. We can also use FreeRTOS features covered previously in this blog series for our application. The Raspberry Pi Pico SDK gives us the ability to define an interrupt handler function for when data is received over the UART, which is a good event-driven design technique as covered in Part 3. To demonstrate how this works, we can start with the sample code below in C as a basic example. This sample code shows how to setup and send data over the UART, then wait for the UART to fill a buffer in order to capture a response. Notice how the ‘on_uart_rx()’ handler and the ‘setup_uart()’ function utilize various API calls from the Raspberry Pi Pico SDK. Additional explanation of the code block will follow.

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include "pico/stdlib.h"

#include "FreeRTOS.h"

#include "task.h"

#include "semphr.h"

 

#define BUFF_SIZE        4096     // 4K

#define TRIES_MS         60000    // 60 secs

#define IRQ_WAIT_US      10000    // 10 millis

#define NUM_CMDS          2        // # of cmds in cmds array

 

static char cmds[NUM_CMDS][70] = {

         "AT+CONF? Certificate pem\n",

         "AT+CONF? ThingName\n",

 

         //"AT+CONF Endpoint=xxxx\n",

         //"AT+CONF SSID=xxxx\n",

         //"AT+CONF Passphrase=xxxx\n",

 

         //"AT+CONF Topic1=pico\n",

         //"AT+CONNECT\n"

};

static int buff_idx = 0;

static uint8_t buffer[BUFF_SIZE];

static absolute_time_t irq_timestamp;

static SemaphoreHandle_t print_mutex;

static SemaphoreHandle_t expresslink_mutex;

 

void on_uart_rx() {

         irq_timestamp = get_absolute_time();

         while (uart_is_readable(uart1)) {

                  int c = uart_getc(uart1);

                  if (buff_idx < BUFF_SIZE) {

                           buffer[buff_idx++] = c;

                  } // drain the UART if buffer is full

         }

}

 

void setup_uart() {

         uart_init(uart1, 115200);

         gpio_set_function(4, GPIO_FUNC_UART); // 4 is TX

         gpio_set_function(5, GPIO_FUNC_UART); // 5 is RX

         uart_set_hw_flow(uart1, false, false);

         uart_set_format(uart1, 8, 1, UART_PARITY_NONE);

         uart_set_fifo_enabled(uart1, false);

         irq_set_exclusive_handler(UART1_IRQ, on_uart_rx);

         irq_set_enabled(UART1_IRQ, true);

         uart_set_irq_enables(uart1, true, false);

}

 

void vResetBuffer() {

         memset(buffer,'\0', BUFF_SIZE);

         buff_idx = 0;

}

 

void vTransmit(char *send, char *response) {

         xSemaphoreTake(expresslink_mutex, portMAX_DELAY);

         vResetBuffer();

         uart_puts(uart1, send);

         int tries = TRIES_MS;

         while (strlen(buffer) == 0 && tries > 0) {

                  busy_wait_ms(1); // wait for first byte in buffer

                  --tries;

         }

         while (absolute_time_diff_us(irq_timestamp, get_absolute_time()) < IRQ_WAIT_US) {

                  busy_wait_ms(1); // wait for timeout from last UART interrupt

         }

         strncpy(response, (char*)buffer, BUFF_SIZE);

         xSemaphoreGive(expresslink_mutex);

}

 

void vSafePrint(char *out) {

         xSemaphoreTake(print_mutex, portMAX_DELAY);

         puts(out);

         xSemaphoreGive(print_mutex);

}

 

void vPrintAndTransmit(char *send) {

         char *response = malloc(BUFF_SIZE);

         vSafePrint(send);

         vTransmit(send, response);

         vSafePrint(response);

         free(response);

}

 

void vTaskExpressLink() {

         for (int i = 0; i < NUM_CMDS; i++) {

                  vPrintAndTransmit(cmds[i]);

                  vTaskDelay(4000);

         }

         char send[64];

         for (;;) {      

                  sprintf(send, "AT+SEND1 {\"timestamp\": \"%d\"}\n", get_absolute_time());

                  vPrintAndTransmit(send);

                  vTaskDelay(20000);      

         }

}

 

void main() {

         stdio_init_all();

         busy_wait_ms(2000);

         setup_uart();

         print_mutex = xSemaphoreCreateMutex();

         expresslink_mutex = xSemaphoreCreateMutex();

         xTaskCreate(vTaskExpressLink, "ExpressLink Task", 256, NULL, 1, NULL);

         vTaskStartScheduler();

}

The code above is a starting point to get an AWS IoT ExpressLink module working over UART on the Raspberry Pi Pico. You can follow the setup, build, and flash procedure from Part 1 of this series to run it on your own Raspberry Pi Pico board, plus view the terminal output. Before explaining more about the sample code itself, it is worth mentioning that the code can be run in 3 different ways depending on the AT commands that are commented out or uncommented in the ‘cmds’ array. Because AWS IoT ExpressLink modules have built-in cloud connectivity, any module used for the first time must be properly configured for networking. There are a few steps for configuring the module, which can all be accomplished with the sample application.

First, every AWS IoT ExpressLink module comes pre-provisioned with a certificate and unique identifier that can be used to connect securely to AWS. An X.509 certificate is required to establish a TLS connection with mutual authentication between device and cloud. The first time you run the program above, the AT commands ‘AT+CONF? Certificate pem\n’ and ‘AT+CONF? ThingName\n’ will extract those values from the AWS IoT ExpressLink module and print them to the terminal. These values can then be used to register the module as a ‘Thing’ in AWS IoT Core. Using the AWS IoT Console requires an AWS account to access. There is no cost as long as you stay within the free tier for usage, which will not be an issue with this example. As a side note, another method for connecting a device to AWS IoT Core without an account is through AWS Quick Connect, but the current demos do not include the Raspberry Pi Pico. Go ahead and run the application, taking note of the output in the terminal, namely the Certificate and ThingName values. Then, follow steps 5-13 in these instructions to register your AWS IoT ExpressLink module. You can skip the steps requiring AT commands because our application is performing those functions. Be sure to copy your endpoint which will be used in the next step.

Now that your AWS IoT ExpressLink module is registered and you have an endpoint, you can comment out the first 2 AT commands in the ‘cmds’ array and uncomment the next 3 commands. Also, change NUM_CMDS to 3. At this point, you will have to fill in the proper values of xxxx for ‘AT+CONF Endpoint=xxxx\n’, ‘AT+CONF SSID=xxxx\n’, and ‘AT+CONF Passphrase=xxxx\n’. Use the endpoint value captured from the last step and then enter the WiFi credentials for your local WiFi access point. All these values will remain on the device. The newline characters (‘\n’) must be kept intact in order for the commands to work properly. Build, flash, and run the application again. This will configure your AWS IoT ExpressLink module for networking once you verify the commands were successfully executed from the terminal output. Example output:

AT+CONF Endpoint=a1iy96edudozia-ats.iot.us-west-2.amazonaws.com

OK

AT+CONF SSID=MySSID

OK

AT+CONF Passphrase=MyPassphrase

OK

AT+SEND1 {"timestamp": "17059138"}

...

Finally, change the ‘cmd’ array in the sample code one last time to uncomment ‘AT+CONF Topic1=pico\n’ and ‘AT+CONNECT\n’ while commenting out all other commands. Also, change NUM_CMDS back to 2. Build and flash a 3rd time and the application will then send data to AWS IoT Core in the form of timestamps generated from the Raspberry Pi Pico SDK with ‘get_absolute_time()’. You can verify data is being sent to AWS IoT Core by logging back into the AWS IoT Console. In the navigation pane, choose Test and then MQTT Test Client. In Subscribe to a topic, enter #, and then choose Subscribe. You should see output like this in the console:

We have shown a quick way to get up and running with an AWS IoT ExpressLink module on the Raspberry Pi Pico. The code is minimal and does not require any non-standard libraries. The ‘vTransmit()’ function relies on specific timing events to receive a general multiline response in the buffer from the UART. This works nicely as long as we do not experience any unexpected behavior. For a more robust implementation, the code could be expanded to check that the values received from the UART terminate in the correct number of newlines expected, as well as result in ‘OK’ or ‘ERR’ as the standard responses defined for an AWS IoT ExpressLink module.

This blog should give you the building blocks for using the Raspberry Pi Pico with FreeRTOS and AWS IoT ExpressLink. Dig in and experiment with how UART works with the Raspberry Pi Pico SDK, and learn more about the AT command set available with AWS IoT ExpressLink. The next blog will cover more details about messaging, including subscribing to topics with MQTT, and how to capture data sent from the cloud to the Raspberry Pi Pico through AWS IoT ExpressLink. Stay tuned for more.

Daniel Gross is a Senior Developer Advocate at Amazon Web Services (AWS). Focused on FreeRTOS and embedded devices interacting with cloud services, Daniel is passionate about delivering a great developer experience for IoT builders.

More from Daniel