feat(uart): simplifies UART example based on MODBUS standard (#11309)

* feat(uart): simplifies UART example based on MODBUS standard

* fix(uart): fixes a uart example typo

* feat(uart): replaces UART0 by Serial0 in the code

* ci(pre-commit): Apply automatic fixes

* fix(uart): typo error message in commentary

---------

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
Sugar Glider 2025-04-29 02:58:10 -03:00 committed by GitHub
parent 16fcdeb0be
commit d63b876f93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -5,20 +5,20 @@
void HardwareSerial::onReceive(OnReceiveCb function, bool onlyOnTimeout = false) void HardwareSerial::onReceive(OnReceiveCb function, bool onlyOnTimeout = false)
It is possible to register an UART callback function that will be called It is possible to register an UART callback function that will be called
every time that UART receives data and an associated interrupt is generated. every time that UART receives data and an associated UART interrupt is generated.
In summary, HardwareSerial::onReceive() works like an RX Interrupt callback, that can be adjusted In summary, HardwareSerial::onReceive() works like an RX Interrupt callback, that
using HardwareSerial::setRxFIFOFull() and HardwareSerial::setRxTimeout(). can be adjusted using HardwareSerial::setRxFIFOFull() and HardwareSerial::setRxTimeout().
OnReceive will be called, while receiving a stream of data, when every 120 bytes are received (default FIFO Full), In case that <onlyOnTimeout> is not changed or it is set to <false>, the callback function is
which may not help in case that the application needs to get all data at once before processing it. executed whenever any event happens first (FIFO Full or RX Timeout).
Therefore, a way to make it work is by detecting the end of a stream transmission. This can be based on a protocol OnReceive will be called when every 120 bytes are received(default FIFO Full),
or based on timeout with the UART line in idle (no data received - this is the case of this example). or when RX Timeout occurs after 1 UART symbol by default.
In some cases, it is necessary to wait for receiving all the data before processing it and parsing the This example demonstrates a way to create a String with all data received from UART0 only
UART input. This example demonstrates a way to create a String with all data received from UART0 and after RX Timeout. This example uses an RX timeout of about 3.5 Symbols as a way to know
signaling it using a Mutex for another task to process it. This example uses a timeout of 500ms as a way to when the reception of data has finished.
know when the reception of data has finished. In order to achieve it, the sketch sets <onlyOnTimeout> to <true>.
The onReceive() callback is called whenever the RX ISR is triggered. The onReceive() callback is called whenever the RX ISR is triggered.
It can occur because of two possible events: It can occur because of two possible events:
@ -34,90 +34,73 @@
2- UART RX Timeout: it happens, based on a timeout equivalent to a number of symbols at 2- UART RX Timeout: it happens, based on a timeout equivalent to a number of symbols at
the current baud rate. If the UART line is idle for this timeout, it will raise an interrupt. the current baud rate. If the UART line is idle for this timeout, it will raise an interrupt.
This time can be changed by HardwareSerial::setRxTimeout(uint8_t rxTimeout) This time can be changed by HardwareSerial::setRxTimeout(uint8_t rxTimeout).
<rxTimeout> is bound to the clock source.
In order to use it properly, ESP32 and ESP32-S2 shall set the UART Clock Source to APB.
When any of those two interrupts occur, IDF UART driver will copy FIFO data to its internal When any of those two interrupts occur, IDF UART driver will copy FIFO data to its internal
RingBuffer and then Arduino can read such data. At the same time, Arduino Layer will execute RingBuffer and then Arduino can read such data. At the same time, Arduino Layer will execute
the callback function defined with HardwareSerial::onReceive(). the callback function defined with HardwareSerial::onReceive().
<bool onlyOnTimeout> parameter (default false) can be used by the application to tell Arduino to <bool onlyOnTimeout> parameter can be used by the application to tell Arduino to only execute
only execute the callback when the second event above happens (Rx Timeout). At this time all the callback when Rx Timeout happens, by setting it to <true>.
received data will be available to be read by the Arduino application. But if the number of At this time all received data will be available to be read by the Arduino application.
received bytes is higher than the FIFO space, it will generate an error of FIFO overflow. The application shall set an appropriate RX buffer size using
In order to avoid such problem, the application shall set an appropriate RX buffer size using
HardwareSerial::setRxBufferSize(size_t new_size) before executing begin() for the Serial port. HardwareSerial::setRxBufferSize(size_t new_size) before executing begin() for the Serial port.
*/
// this will make UART0 work in any case (using or not USB) MODBUS timeout of 3.5 symbol is based on these documents:
#if ARDUINO_USB_CDC_ON_BOOT https://www.automation.com/en-us/articles/2012-1/introduction-to-modbus
#define UART0 Serial0 https://minimalmodbus.readthedocs.io/en/stable/serialcommunication.html
#else */
#define UART0 Serial
#endif
// global variable to keep the results from onReceive() // global variable to keep the results from onReceive()
String uart_buffer = ""; String uart_buffer = "";
// a pause of a half second in the UART transmission is considered the end of transmission. // The Modbus RTU standard prescribes a silent period corresponding to 3.5 characters between each
const uint32_t communicationTimeout_ms = 500; // message, to be able to figure out where one message ends and the next one starts.
const uint32_t modbusRxTimeoutLimit = 4;
const uint32_t baudrate = 19200;
// Create a mutex for the access to uart_buffer // UART_RX_IRQ will be executed as soon as data is received by the UART and an RX Timeout occurs
// only one task can read/write it at a certain time // This is a callback function executed from a high priority monitor task
SemaphoreHandle_t uart_buffer_Mutex = NULL; // All data will be buffered into RX Buffer, which may have its size set to whatever necessary
// UART_RX_IRQ will be executed as soon as data is received by the UART
// This is a callback function executed from a high priority
// task created when onReceive() is used
void UART0_RX_CB() { void UART0_RX_CB() {
// take the mutex, waits forever until loop() finishes its processing while (Serial0.available()) {
if (xSemaphoreTake(uart_buffer_Mutex, portMAX_DELAY)) { uart_buffer += (char)Serial0.read();
uint32_t now = millis(); // tracks timeout
while ((millis() - now) < communicationTimeout_ms) {
if (UART0.available()) {
uart_buffer += (char)UART0.read();
now = millis(); // reset the timer
}
}
// releases the mutex for data processing
xSemaphoreGive(uart_buffer_Mutex);
} }
} }
// setup() and loop() are functions executed by a low priority task // setup() and loop() are functions executed by a low priority task
// Therefore, there are 2 tasks running when using onReceive() // Therefore, there are 2 tasks running when using onReceive()
void setup() { void setup() {
UART0.begin(115200); // Using Serial0 will work in any case (using or not USB CDC on Boot)
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2
// creates a mutex object to control access to uart_buffer // UART_CLK_SRC_APB will allow higher values of RX Timeout
uart_buffer_Mutex = xSemaphoreCreateMutex(); // default for ESP32 and ESP32-S2 is REF_TICK which limits the RX Timeout to 1
if (uart_buffer_Mutex == NULL) { // setClockSource() must be called before begin()
log_e("Error creating Mutex. Sketch will fail."); Serial0.setClockSource(UART_CLK_SRC_APB);
while (true) { #endif
UART0.println("Mutex error (NULL). Program halted."); // the amount of data received or waiting to be proessed shall not exceed this limit of 1024 bytes
delay(2000); Serial0.setRxBufferSize(1024); // default is 256 bytes
} Serial0.begin(baudrate); // default pins and default mode 8N1 (8 bits data, no parity bit, 1 stopbit)
} // set RX Timeout based on UART symbols ~ 3.5 symbols of 11 bits (MODBUS standard) ~= 2 ms at 19200
Serial0.setRxTimeout(modbusRxTimeoutLimit); // 4 symbols at 19200 8N1 is about 2.08 ms (40 bits)
UART0.onReceive(UART0_RX_CB); // sets the callback function // sets the callback function that will be executed only after RX Timeout
UART0.println("Send data to UART0 in order to activate the RX callback"); Serial0.onReceive(UART0_RX_CB, true);
Serial0.println("Send data using Serial Monitor in order to activate the RX callback");
} }
uint32_t counter = 0; uint32_t counter = 0;
void loop() { void loop() {
// String <uart_buffer> is filled by the UART Callback whenever data is received and RX Timeout occurs
if (uart_buffer.length() > 0) { if (uart_buffer.length() > 0) {
// signals that the onReceive function shall not change uart_buffer while processing // process the received data from Serial - example, just print it beside a counter
if (xSemaphoreTake(uart_buffer_Mutex, portMAX_DELAY)) { Serial0.print("[");
// process the received data from UART0 - example, just print it beside a counter Serial0.print(counter++);
UART0.print("["); Serial0.print("] [");
UART0.print(counter++); Serial0.print(uart_buffer.length());
UART0.print("] ["); Serial0.print(" bytes] ");
UART0.print(uart_buffer.length()); Serial0.println(uart_buffer);
UART0.print(" bytes] "); uart_buffer = ""; // reset uart_buffer for the next UART reading
UART0.println(uart_buffer);
uart_buffer = ""; // reset uart_buffer for the next UART reading
// releases the mutex for more data to be received
xSemaphoreGive(uart_buffer_Mutex);
}
} }
UART0.println("Sleeping for 1 second..."); delay(1);
delay(1000);
} }