Ther are so many variants of Arduino boards that is based on ESP32 chip.
I wonder whether there is any module that supports RTOS.
If you buy a common ESP32 module, and install the ESP32 boards by Espressif (the manufacturer of the ESP32) in the Arduino IDE, then the Arduino layer is running on top of FreeRTOS. So the FreeRTOS is already fully running, and you can use it in your sketch.
If you prefer Mbed, then try a Raspberry Pi Pico. The Arduino layer is running on top of Mbed, and everything from Mbed can be used.
By the way, the Wokwi simulator simulates both the ESP32 and the Raspberry Pi Pico with FreeRTOS and Mbed and everything: https://wokwi.com/
That means if I did not use Espressif tool, instead just use Arduino IDE to install Esp32 drivers, then what I get is only single-thread application with hardware interrupts, isn't it?
I am asking this because I found WiFi client.write takes a long time (>400us) to return.
I wonder why not make it non-blocking and assign a callback interrupt for it, which can free up MCU to do other stuff when wifi is in transmission.
I'm not sure what the problem is. Why is that slow ? I will write down a few notes, maybe that is helpful.
The classic ESP32 has two cores. All the WiFi runs on Core0 and the Arduino sketch runs as a task on Core1.
When Core0 is involved and when WiFi communication is involved, then it can not be predicted how long it takes. I assume that writing a block of data is faster than writing many single bytes.
I don't know if there is a callback function. You can create a new task for something that needs to keep running, to avoid waiting for a WiFi function to return.
Beside the Arduino reference, you now have also the Espressif documentation: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html
The Arduino layer for the ESP32 is also documentated: https://docs.espressif.com/projects/arduino-esp32/en/latest/getting_started.html
Did you know that when you create tasks in the sketch, that the loop() still runs as a task as well ? Some forget that.
Some try to start the scheduler in setup(), but there is already a fully running FreeRTOS under the Arduino layer.
Don't run tasks on Core0, it can mess up the WiFi tasks.
Some think that using vTaskDelay()
is better than the Arduino delay()
. But the Arduino delay()
is of course translated to vTaskDelay()
to keep the multitasking system running smooth. When Arduino is on top of Mbed, then delay()
is translated as well.
An interrupt in the Arduino layer is a real interrupt. It needs something special, see the ARDUINO_ISR_ATTR
here: https://docs.espressif.com/projects/arduino-esp32/en/latest/api/gpio.html
Don't ask me about the ESP32 in I2C Slave mode I kind of works. It seems as if the classic ESP32 processor hardware was not designed for a I2C Slave mode. Newer variants of the ESP32 don't have that problem.
The ESP32 build environment for the Arduino layer has been upgraded from version 2 to version 3 recently: https://docs.espressif.com/projects/arduino-esp32/en/latest/migration_guides/2.x_to_3.0.html
There are still libraries that needs to be updated. You might encounter a library that does not work with version 3.
Here are many tutorials: https://randomnerdtutorials.com/
thx a lot. that's a lot info for me to digest.
In my test code, I just put in the loop a delayMicroseconds function and a WiFi client.write function if WiFi client is connected.
Before client.write I record current system time by calling micros(); then call it again after client.write returns. That's how I get the delay.
My Nano ESP32 runs as a server, and the receiving end is a computer in the same WiFi network. During the test, once they're connected, I observe that the connection remains stable. There is no need to reconnect, so delay is not caused by that.
ESP32-C3 is single core.
But the Arduino Nano ESP32 is a "ESP32-S3"
Wikipedia has a nice list of the variants: https://en.wikipedia.org/wiki/ESP32#ESP32-xx_family
I don't understand how this new task can avoid waiting for Client.write to return, if it's a blocking one. Do you mean that even if Client.write is in the middle of running, MCU is able to run the task?
Actually my case is I need MCU to respond to a pin falling event, which is triggered by an external hardware.
Originally I thought MCU is not able to do that when it's running a blocking function.
FreeRTOS is a full pre-emptive multitasking system. It does not matter how many cores there are or if a task is blocking, another task will still run. Try it.
This is my test, trying to create blocking code (it never blocks):
// Question: How pre-emptive is FreeRTOS ?
// Answer : Fully pre-emptive in every way
// This Wokwi project: https://wokwi.com/projects/403090705288099841
const int pinLedZero = 14;
const int pinLedOne = 27;
const int pinLedTwo = 26;
const int pinLedThree = 25;
volatile unsigned long counterOne;
bool stateOne = false;
void setup()
{
Serial.begin(115200);
Serial.println("Starting");
pinMode(pinLedZero, OUTPUT);
// Not only the priority can be set,
// some systems have a extra "portPRIVILEGE_BIT"
xTaskCreate(taskOne, "One" , 1000, NULL, 1, NULL);
xTaskCreate(taskTwo, "Two" , 1000, NULL, 1, NULL);
xTaskCreate(taskThree, "Three", 1000, NULL, 1, NULL);
}
void loop()
{
Serial.print("Task Zero: counter of task One is = ");
Serial.println(counterOne);
delay(1000);
digitalWrite(pinLedZero, HIGH);
delay(200);
digitalWrite(pinLedZero, LOW);
}
void taskOne(void * pvParameters)
{
pinMode(pinLedOne, OUTPUT);
// Blocking code ! No delay, no wait.
// Comment out the digitalWrite to be 100% sure.
// The other tasks run as well.
for(;;)
{
counterOne++;
if(counterOne % 500000UL == 0)
{
digitalWrite(pinLedOne, stateOne ? HIGH : LOW);
stateOne = !stateOne;
}
}
}
void taskTwo(void * pvParameters)
{
pinMode(pinLedTwo, OUTPUT);
for(;;)
{
digitalWrite(pinLedTwo, HIGH);
delay(1000); // delay calls vTaskDelay
digitalWrite(pinLedTwo, LOW);
delay(1000); // delay calls vTaskDelay
}
}
void taskThree(void * pvParameters)
{
pinMode(pinLedThree, OUTPUT);
for(;;)
{
digitalWrite(pinLedThree, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS);
digitalWrite(pinLedThree, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
Try it in Wokwi:
Thx for enlightening me.
I never thought it was so easy to run FreeRTOS in Arduino. I will give it a try to use your code and see how I can apply to my case.
Just for fun, I replaced the classic ESP32 with a ESP32-C3 single core RISC-V: https://wokwi.com/projects/403103153803776001
That is the beauty of a multitasking system, you won't notice the single core.
what is the default priority value for the loop() task?
Try:
Serial.println(uxTaskPriorityGet(NULL));
It returns 1 when printed from within the loop().
That means that it is a normal task, not a idle task.
Priority 1 is also the default priority for the "main" functon when the Arduino layer is not used.
I created a task and put my WiFi transmission code inside it. Unfortunately it makes the firmware crash: when running into the task, firmware restarts.
xTaskCreate(WiFi_tx, "WiFi_tx", 1000, NULL, 1, NULL);
The task called WiFi_tx is created in setup(), which runs WiFi_tx function.
void WiFi_tx(void * pvParameters) {
Serial.println("WiFi_tx starting ...");
for(;;) {
Serial.print("!");
if(packet_ready) {
Serial.println(wifi_pkt_num);
#if ENABLE_WIFI
if (!wifi_client.connected()) {
wifi_client = wifi_server.available();
if(wifi_client) {
Serial.println("Client Connected");
wifi_client.write(DataPack[wifi_pkt_num], DATAPACKSIZE);
}
} else {
wifi_delay_us = micros();
DataPack[wifi_pkt_num][3]++;
wifi_client.write(DataPack[wifi_pkt_num], DATAPACKSIZE);
Serial.println(wifi_pkt_num);
wifi_delay_us = micros() - wifi_delay_us;
}
#endif
packet_ready = false;
} else {
delayMicroseconds(5);
}
}
}
That packet_ready flag is set in the main loop when some condition is met.
I don't know if that has to do with the task. It is hard to tell without knowing the rest of the code. It could be that "wifi_client" is no longer valid or that "wifi_client.write()" has the wrong parameters.
wifi_client is a global variable.
This piece of code runs well when it is put in the main loop.
/*
* Change log
* 12/07/2024 v1.0
* WiFi_tx crashes the fw.
*
* Use task threads to perform two actions:
* 1. In main loop, read data from ADS1299 through SPI when data_ready flag to true.
* 2. ADS1299 data ready pin to trigger interrupt, and set data_ready flag to true.
* 3. Create a task to do WiFi tx when one pkt is ready.
*
*/
#include <SPI.h>
#include <WiFi.h>
#include "ADS1299.h"
//#define MOCK_DATA 1
#define SHOW_DATA 0
#define CHECK_SAMPLE_DELAY 1
#define ENABLE_WIFI 0
#define TX_RX_BUF_LENGTH 28u /**< SPI transaction buffer length. */
#define NUMBER_of_CHANNELS 2
#define NUMBER_of_SAMPLELS 20
#define DATA_PKT_BUF_SIZE 5
const int pinLedTwo = LED_GREEN;
/* Head_Size: 6, data_size: 3, 2 Channels & 50 samples in one packet, 3 byte of checksum*/
#define DATAPACKSIZE (6 + 3 * NUMBER_of_CHANNELS * NUMBER_of_SAMPLELS + 3)
static uint8_t m_tx_data[TX_RX_BUF_LENGTH] = {0}; /**< A buffer with data to transfer. */
static uint8_t m_rx_data[TX_RX_BUF_LENGTH] = {0}; /**< A buffer for incoming data. */
uint16_t dtpk_ptr = 6;
uint8_t data_pkt_num = 0;
uint8_t wifi_pkt_num = 0;
uint8_t DataPack[DATA_PKT_BUF_SIZE][DATAPACKSIZE];
WiFiServer wifi_server(80);
WiFiClient wifi_client;
WiFiUDP wifi_udp;
const int ads1299_cs = 10;
const int ads1299_reset = 8;
const int ads1299_data_ready = 9;
bool data_ready;
bool packet_ready;
uint8_t mock_seq = 0;
uint8_t devId = 0;
int32_t cnvt_data = 0;
uint8_t checksum = 0;
unsigned long wifi_delay_us;
unsigned long spi_read_data_delay_us;
unsigned long interrupt_gap_delay_us;
unsigned long prev_interrupt_time;
uint8_t itrpt_seq = 0;
const char* ssid_N12_18 = "iBMAAR-CDE6";
const char* password_N12_18 = "fvwra36040";
const char* ssid_S10_19 = "edimax_2.4G_16A229";
const char* password_S10_19 = "56zlab78";
IPAddress broadcastIP(192,168,0,255);
void setup() {
// put your setup code here, to run once:
pinMode(LED_RED, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(ads1299_cs, OUTPUT);
pinMode(ads1299_reset, OUTPUT);
pinMode(ads1299_data_ready, INPUT);
attachInterrupt(digitalPinToInterrupt(ads1299_data_ready), data_ready_isr, FALLING);
digitalWrite(ads1299_reset, LOW);
delay(10);
digitalWrite(ads1299_cs, HIGH);
delay(10);
Serial.begin(115200);
delay(1000); // wait for a second
for(uint8_t i = 0; i < 3; i++) {
Serial.print(i);
Serial.println("\t\t *** Hello Nano ESP32");
delay(2000);
}
WIFIsetup(ssid_N12_18, password_N12_18);
//xTaskCreate(WiFi_tx, "WiFi_tx", 1000, NULL, 1, NULL);
delay(500); // wait for half a second
SPI.begin();
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE1));
init_ads1299();
packet_ready = false;
data_ready = false;
delay(500); // wait for half a second
xTaskCreate(spi_read_ads1299, "spi_read_ads1299", 1000, NULL, 1, NULL);
}
void spi_read_ads1299(void * pvParameters)
{
Serial.println("task spi_read_ads1299 running ...");
while(true)
{
if(data_ready) {
Serial.print(dtpk_ptr);
Serial.print(".");
data_ready = false;
ADS1299ReadData();
#if 0
Serial.print(m_rx_data[4]);
Serial.print(",");
Serial.print(m_rx_data[5]);
Serial.print(",");
Serial.println(m_rx_data[6]);
#endif
} else {
delayMicroseconds(1);
}
}
}
void taskTwo(void * pvParameters)
{
pinMode(pinLedTwo, OUTPUT);
for(;;)
{
digitalWrite(pinLedTwo, HIGH);
delay(1000); // delay calls vTaskDelay
digitalWrite(pinLedTwo, LOW);
delay(1000); // delay calls vTaskDelay
}
}
void WiFi_tx(void * pvParameters) {
Serial.println("WiFi_tx starting ...");
for(;;) {
Serial.print("!");
if(packet_ready) {
Serial.println(wifi_pkt_num);
#if ENABLE_WIFI
if (!wifi_client.connected()) {
wifi_client = wifi_server.available();
if(wifi_client) {
Serial.println("Client Connected");
wifi_client.write(DataPack[wifi_pkt_num], DATAPACKSIZE);
}
} else {
wifi_delay_us = micros();
DataPack[wifi_pkt_num][3]++;
wifi_client.write(DataPack[wifi_pkt_num], DATAPACKSIZE);
Serial.println(wifi_pkt_num);
wifi_delay_us = micros() - wifi_delay_us;
}
#endif
packet_ready = false;
} else {
delayMicroseconds(5);
}
}
}
void data_ready_isr() {
interrupt_gap_delay_us = micros() - prev_interrupt_time;
itrpt_seq++;
prev_interrupt_time = micros();
data_ready = true;
//if(itrpt_seq == 0) {
// Serial.print(interrupt_gap_delay_us);
// Serial.print(" ");
//}
}
void loop() {
if(packet_ready) {
Serial.println(wifi_pkt_num);
#if ENABLE_WIFI
if (!wifi_client.connected()) {
wifi_client = wifi_server.available();
if(wifi_client) {
Serial.println("Client Connected");
wifi_client.write(DataPack[wifi_pkt_num], DATAPACKSIZE);
}
} else {
wifi_delay_us = micros();
DataPack[wifi_pkt_num][3]++;
wifi_client.write(DataPack[wifi_pkt_num], DATAPACKSIZE);
Serial.println(wifi_pkt_num);
wifi_delay_us = micros() - wifi_delay_us;
}
#endif
packet_ready = false;
} else {
delayMicroseconds(5);
}
}
void Blink_Led() {
delay(1000);
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite(LED_RED, LOW);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
digitalWrite(LED_RED, HIGH);
delay(1000);
}
void WIFIsetup(const char* ssid, const char* password) {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.println("...");
}
wifi_server.begin();
Serial.print("WiFi connected with IP:");
Serial.println(WiFi.localIP());
for(uint8_t i = 0; i < DATA_PKT_BUF_SIZE; i++) {
DataPack[i][0] = 0xAA;
DataPack[i][1] = NUMBER_of_SAMPLELS;
DataPack[i][2] = NUMBER_of_CHANNELS;
}
}
void ADS1299WriteReg(uint8_t address, uint32_t data) {
...
}
void ADS1299WriteCMD(uint8_t cmd, uint8_t data)
{
...
}
void ADS1299_Read_DevId() {
...
}
// DS: Data Sheet
void init_ads1299() {
...
}
void ADS1299ReadData() {
m_tx_data[0] = 0x12;
digitalWrite(ads1299_cs, LOW); // pull CS low to start SPI com
delayMicroseconds(1);
m_rx_data[0] = SPI.transfer(m_tx_data[0]);
delayMicroseconds(1);
for(uint8_t i = 1; i <= (NUMBER_of_CHANNELS + 1) * 3; i++) {
m_rx_data[i] = SPI.transfer(m_tx_data[i]);
delayMicroseconds(1);
}
digitalWrite(ads1299_cs, HIGH); // pull CS High to stop SPI com
#if SHOW_DATA
uint32_t data = 0;
data |= m_rx_data[4] << 16; // read top 8 bits data
data |= m_rx_data[5] << 8; // read middle 8 bits data
data |= m_rx_data[6] & 0xFF;
cnvt_data = data;
if ((data & 0x00800000) > 0) {
cnvt_data |= 0xFF000000;
} else {
cnvt_data &= 0x00FFFFFF;
}
//float f_data = cnvt_data/0x7fffffff;
//Serial.print(itrpt_seq);
//Serial.print(",");
//Serial.println(interrupt_gap_delay_us);
#endif
#if MOCK_DATA // send sequence number
DataPack[data_pkt_num][dtpk_ptr] = mock_seq++;
DataPack[data_pkt_num][dtpk_ptr + 3] = mock_seq++;
dtpk_ptr += 6;
#else
DataPack[data_pkt_num][dtpk_ptr] = itrpt_seq;
#if CHECK_SAMPLE_DELAY
DataPack[data_pkt_num][dtpk_ptr + 3] = interrupt_gap_delay_us;//interrupt_gap_delay_us
DataPack[data_pkt_num][dtpk_ptr + 4] = (interrupt_gap_delay_us >> 8); //wifi_delay_us
DataPack[data_pkt_num][dtpk_ptr + 5] = (interrupt_gap_delay_us >> 16);
#else
DataPack[data_pkt_num][dtpk_ptr + 3] = m_rx_data[9];
DataPack[data_pkt_num][dtpk_ptr + 4] = m_rx_data[8];
DataPack[data_pkt_num][dtpk_ptr + 5] = m_rx_data[7];
#endif
dtpk_ptr += 6;
//checksum += (m_rx_data[4] + m_rx_data[5] + m_rx_data[6] + m_rx_data[7] + m_rx_data[8] + m_rx_data[9]);
if(dtpk_ptr == DATAPACKSIZE - 3) {
dtpk_ptr = 6;
wifi_pkt_num = data_pkt_num++;
data_pkt_num = data_pkt_num % DATA_PKT_BUF_SIZE;
Serial.print(wifi_pkt_num);
Serial.print(",");
Serial.println(data_pkt_num);
//packet_ready = true;
}
#endif
}
In this code, I have another task, which reads data from ADS1299. It also makes the firmware crash.
Basically I have two tasks to perform: T1, to read data from ADS1299 when its data ready pin goes high to low; and fills the data in a WiFi packet. T2, when packet is full, use WiFi client to send it out.
I tried 2 methods: (1) put T1 in main loop, and put T2 in a created task; (2) put T2 in main loop, and put T1 in a created task. In both cases, the firmware crashed in the created task.
In FreeRTOS protcol, you have to use mutex/semaphore to be sure that the same resource is not being accessed simultaneously by two tasks.
The DataPacket is the only resource, which both tasks access. That's why I have a simple queue to manage them. The data filling method always works on one indexed by data_pkt_num, and the WiFi client transmit the one indexed by wifi_pkt_num. (wifi_pkt_num = data_pkt_num - 1). If the data sampling rate is not extremely fast, I guess it's safe.
You have two tasks running concurrently and asynchronously. You need to acquire mutex first to access the queue and then release it for the other task to acquire.