diff --git a/cores/esp32/HardwareSerial.cpp b/cores/esp32/HardwareSerial.cpp index b06e0735a71..ea824fb82b5 100644 --- a/cores/esp32/HardwareSerial.cpp +++ b/cores/esp32/HardwareSerial.cpp @@ -83,6 +83,14 @@ void HardwareSerial::updateBaudRate(unsigned long baud) uartSetBaudRate(_uart, baud); } +// Make sure that the function that the function pointer is pointing to is inside IRAM +void HardwareSerial::setRxInterrupt(void (*arg)(uint8_t, void*), void* user_arg){ + + // Make sure that the previous interrupt_info is not used anymore + uartDisableRxInterrupt(_uart); + uartEnableRxInterrupt(_uart, &interrupt_info, arg, user_arg); +} + void HardwareSerial::end() { if(uartGetDebug() == _uart_nr) { diff --git a/cores/esp32/HardwareSerial.h b/cores/esp32/HardwareSerial.h index e776e939d0b..0fad21ff574 100644 --- a/cores/esp32/HardwareSerial.h +++ b/cores/esp32/HardwareSerial.h @@ -23,6 +23,7 @@ Modified 31 March 2015 by Markus Sattler (rewrite the code for UART0 + UART1 support in ESP8266) Modified 25 April 2015 by Thomas Flayols (add configuration different from 8N1 in ESP8266) Modified 13 October 2018 by Jeroen Döll (add baudrate detection) + Modified 08 januari 2012 by Tim Koers (added RX IRQ handling) Baudrate detection example usage (detection on Serial1): void setup() { Serial.begin(115200); @@ -58,6 +59,7 @@ class HardwareSerial: public Stream void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1, bool invert=false, unsigned long timeout_ms = 20000UL); void end(); void updateBaudRate(unsigned long baud); + void setRxInterrupt(void (*arg)(uint8_t, void*), void* user_arg); int available(void); int availableForWrite(void); int peek(void); @@ -108,6 +110,7 @@ class HardwareSerial: public Stream uart_t* _uart; uint8_t _tx_pin; uint8_t _rx_pin; + uart_interrupt_struct_t* interrupt_info; }; extern void serialEventRun(void) __attribute__((weak)); diff --git a/cores/esp32/esp32-hal-uart.c b/cores/esp32/esp32-hal-uart.c index 7ee23b95c95..570da33b6f2 100644 --- a/cores/esp32/esp32-hal-uart.c +++ b/cores/esp32/esp32-hal-uart.c @@ -38,7 +38,7 @@ static int s_uart_debug_nr = 0; struct uart_struct_t { - uart_dev_t * dev; + uart_dev_t *dev; #if !CONFIG_DISABLE_HAL_LOCKS xSemaphoreHandle lock; #endif @@ -47,6 +47,13 @@ struct uart_struct_t { intr_handle_t intr_handle; }; +struct uart_interrupt_struct_t +{ + void (*func)(uint8_t, void*); + void *user_arg; + uart_t *dev; +}; + #if CONFIG_DISABLE_HAL_LOCKS #define UART_MUTEX_LOCK() #define UART_MUTEX_UNLOCK() @@ -69,23 +76,48 @@ static uart_t _uart_bus_array[3] = { static void uart_on_apb_change(void * arg, apb_change_ev_t ev_type, uint32_t old_apb, uint32_t new_apb); +static uart_interrupt_t *_uart_interrupt_array[3] = {NULL, NULL, NULL}; + static void IRAM_ATTR _uart_isr(void *arg) { uint8_t i, c; BaseType_t xHigherPriorityTaskWoken; uart_t* uart; + uart_interrupt_t *uart_interrupt; + // Loop through all the uart devices for(i=0;i<3;i++){ + + // Get the current uart device uart = &_uart_bus_array[i]; - if(uart->intr_handle == NULL){ + + // Get the interrupt description for this uart device + uart_interrupt = _uart_interrupt_array[i]; + + // If there is no interrupt handle, skip the rest of the for loop's body + if (uart->intr_handle == NULL) { continue; } + + // There are cases where bytes might come in between the time you check and handle the bytes and the time you clear the interrupt. + // In that case you will not get an ISR for those bytes. + // We had that happen and scratched heads for quite some time. + // There was another case that I do not recall at this time as well. + // https://github.com/espressif/arduino-esp32/pull/4656#discussion_r555780523 uart->dev->int_clr.rxfifo_full = 1; uart->dev->int_clr.frm_err = 1; uart->dev->int_clr.rxfifo_tout = 1; - while(uart->dev->status.rxfifo_cnt || (uart->dev->mem_rx_status.wr_addr != uart->dev->mem_rx_status.rd_addr)) { + + // Read until fifo is empty + while (uart->dev->status.rxfifo_cnt || (uart->dev->mem_rx_status.wr_addr != uart->dev->mem_rx_status.rd_addr)) { c = uart->dev->fifo.rw_byte; - if(uart->queue != NULL) { + + // Check if an user defined interrupt handling function is present + if (uart_interrupt != NULL && uart_interrupt->dev->num == uart->num && uart_interrupt->func != NULL) { + // Fully optimized code would not create the queue anymore if an function has been specified as an argument. + (*uart_interrupt->func)(c, uart_interrupt->user_arg); + }else if (uart->queue != NULL) { + // No user function is present, handle as you normally would xQueueSendFromISR(uart->queue, &c, &xHigherPriorityTaskWoken); } } @@ -96,7 +128,20 @@ static void IRAM_ATTR _uart_isr(void *arg) } } -void uartEnableInterrupt(uart_t* uart) +/** + * @brief Enables the UART RX interrupt on the specified UART device. + * + * This function will register the interrupt user function with arguments, to be called when an RX interupt occurs. + * The pointer to the uart_interrupt_t will be deleted when the interrupt is disabled, or another interrupt function is being registered. + * Please check for NULL on the uart_interrupt_t pointer before using it. + + * @param[in] uart The uart device to register the function for. + * @param[out] arg The uart_interrupt description data that is returned when this function succeeds. + * @param[in] func The function to be called when the RX interrupt is fired. (void rx_int(uint8_t c, void* user_arg)) + * @param[in] user_arg The user argument that will be passed to the user interrupt handler. + * + */ +void uartEnableRxInterrupt(uart_t* uart, uart_interrupt_t **arg, void (*func)(uint8_t, void*), void* user_arg) { UART_MUTEX_LOCK(); uart->dev->conf1.rxfifo_full_thrhd = 112; @@ -107,17 +152,48 @@ void uartEnableInterrupt(uart_t* uart) uart->dev->int_ena.rxfifo_tout = 1; uart->dev->int_clr.val = 0xffffffff; + if(arg != NULL){ + (*arg) = malloc(sizeof(uart_interrupt_t)); + (*arg)->func = func; + (*arg)->dev = uart; + (*arg)->user_arg = user_arg; + + if(_uart_interrupt_array[uart->num]){ + free(_uart_interrupt_array[uart->num]); + } + + _uart_interrupt_array[uart->num] = (*arg); + } + esp_intr_alloc(UART_INTR_SOURCE(uart->num), (int)ESP_INTR_FLAG_IRAM, _uart_isr, NULL, &uart->intr_handle); UART_MUTEX_UNLOCK(); } -void uartDisableInterrupt(uart_t* uart) +/** + * @brief Disables the UART RX interrupt on the specified UART device. + * + * This function disables the RX interrupt for the specified UART device. + * This function will delete the uart_interrupt_t* that uartEnableRxInterrupt() creates. + * The pointer to the uart_interrupt_t will be deleted when the interrupt is disabled, or another interrupt function is being registered. + * Please check for NULL on the uart_interrupt_t pointer before using it. + + * @param[in] uart The uart device to register the function for. + * + */ +void uartDisableRxInterrupt(uart_t* uart) { UART_MUTEX_LOCK(); uart->dev->conf1.val = 0; uart->dev->int_ena.val = 0; uart->dev->int_clr.val = 0xffffffff; + // Free uart rx interrupt + if(_uart_interrupt_array[uart->num]){ + free(_uart_interrupt_array[uart->num]); + } + + _uart_interrupt_array[uart->num] = NULL; + esp_intr_free(uart->intr_handle); uart->intr_handle = NULL; @@ -130,7 +206,7 @@ void uartDetachRx(uart_t* uart, uint8_t rxPin) return; } pinMatrixInDetach(rxPin, false, false); - uartDisableInterrupt(uart); + uartDisableRxInterrupt(uart); } void uartDetachTx(uart_t* uart, uint8_t txPin) @@ -148,7 +224,7 @@ void uartAttachRx(uart_t* uart, uint8_t rxPin, bool inverted) } pinMode(rxPin, INPUT); pinMatrixInAttach(rxPin, UART_RXD_IDX(uart->num), inverted); - uartEnableInterrupt(uart); + uartEnableRxInterrupt(uart, NULL, NULL, NULL); } void uartAttachTx(uart_t* uart, uint8_t txPin, bool inverted) @@ -309,7 +385,7 @@ void uartRxFifoToQueue(uart_t* uart) uart->dev->int_ena.frm_err = 1; uart->dev->int_ena.rxfifo_tout = 1; uart->dev->int_clr.val = 0xffffffff; - UART_MUTEX_UNLOCK(); + UART_MUTEX_UNLOCK(); } uint8_t uartRead(uart_t* uart) @@ -320,7 +396,7 @@ uint8_t uartRead(uart_t* uart) uint8_t c; if ((uxQueueMessagesWaiting(uart->queue) == 0) && (uart->dev->status.rxfifo_cnt > 0)) { - uartRxFifoToQueue(uart); + uartRxFifoToQueue(uart); } if(xQueueReceive(uart->queue, &c, 0)) { return c; @@ -336,7 +412,7 @@ uint8_t uartPeek(uart_t* uart) uint8_t c; if ((uxQueueMessagesWaiting(uart->queue) == 0) && (uart->dev->status.rxfifo_cnt > 0)) { - uartRxFifoToQueue(uart); + uartRxFifoToQueue(uart); } if(xQueuePeek(uart->queue, &c, 0)) { return c; diff --git a/cores/esp32/esp32-hal-uart.h b/cores/esp32/esp32-hal-uart.h index e35f05d4cff..ecaade4c372 100644 --- a/cores/esp32/esp32-hal-uart.h +++ b/cores/esp32/esp32-hal-uart.h @@ -51,6 +51,9 @@ extern "C" { struct uart_struct_t; typedef struct uart_struct_t uart_t; +struct uart_interrupt_struct_t; +typedef struct uart_interrupt_struct_t uart_interrupt_t; + uart_t* uartBegin(uint8_t uart_nr, uint32_t baudrate, uint32_t config, int8_t rxPin, int8_t txPin, uint16_t queueLen, bool inverted); void uartEnd(uart_t* uart, uint8_t rxPin, uint8_t txPin); @@ -80,6 +83,9 @@ unsigned long uartDetectBaudrate(uart_t *uart); bool uartRxActive(uart_t* uart); +void uartDisableRxInterrupt(uart_t* uart); +void uartEnableRxInterrupt(uart_t *uart, uart_interrupt_t** arg, void (*func)(uint8_t, void*), void* user_arg); + #ifdef __cplusplus } #endif diff --git a/libraries/ESP32/examples/Serial/Interrupt/Interrupt.ino b/libraries/ESP32/examples/Serial/Interrupt/Interrupt.ino new file mode 100644 index 00000000000..6a46cd511e8 --- /dev/null +++ b/libraries/ESP32/examples/Serial/Interrupt/Interrupt.ino @@ -0,0 +1,77 @@ +/** Tim Koers - 2021 + * + * This sketch shows the usage of a semaphore to receive Serial data. + * You can safely use this in a FreeRTOS environment and use the semaphore from different tasks on different CPU cores. + * However, the InterruptQueue example is much more efficient, since it uses less code. + * This sketch assumes that when Hi is sent, the device returns an 8 byte message. + * + */ + +#define BUFFER_SIZE 8 + +// This semaphore is here to handle the interruption of the loop when the interrupt is running. +SemaphoreHandle_t bufferSemaphore; + +static volatile char inputBuffer[BUFFER_SIZE]; +static volatile size_t inputBufferLength = 0; + +bool messageSent = false; + +// Please keep in mind, since the ESP32 is dual core, +// the interrupt will be running on the same core as the setRxInterrupt function was called on. +static void IRAM_ATTR onSerialRX(uint8_t character, void* user_arg){ + + // Cast the user_arg back to a array + char* buffer = (char*)user_arg; + + BaseType_t xHighPriorityTaskWoken; + + if(xSemaphoreTakeFromISR(bufferSemaphore, &xHighPriorityTaskWoken) == pdTRUE){ + if(inputBufferLength < BUFFER_SIZE){ + buffer[inputBufferLength++] = (char)character; + } + xSemaphoreGiveFromISR(bufferSemaphore, &xHighPriorityTaskWoken); + } +} + +void setup() +{ + bufferSemaphore = xSemaphoreCreateBinary(); + + Serial.begin(115200); + Serial2.begin(115200); + + // The user_arg is expected to be a void pointer (void*), + // so take the reference of the variable, and it to a void* + Serial2.setRxInterrupt(onSerialRX, (void*)&inputBuffer); +} + +void loop() +{ + if(!messageSent){ + Serial.println("Hi"); + messageSent = true; + } + + // Check if the semaphore can be taken and if the inputBuffer length is long enough. + // Please keep in mind, since the variable inputBufferLength is also accessed inside the IRQ handler, + // the semaphore needs to be taken BEFORE you can access that variable. + if(xSemaphoreTake(bufferSemaphore, portMAX_DELAY) == pdTRUE && inputBufferLength == (BUFFER_SIZE - 1)){ + for(size_t i = 0; i < inputBufferLength; i++){ + Serial.write(inputBuffer[i]); + + // Clear the buffer + inputBuffer[i] = '\0'; + } + // Clear the bufferLength + inputBufferLength = 0; + + // Give back the semaphore + xSemaphoreGive(bufferSemaphore); + + // Allow the system to request another data set + messageSent = false; + } + + delay(10); // Wait for 10ms to allow some data to come in +} diff --git a/libraries/ESP32/examples/Serial/InterruptQueue/InterruptQueue.ino b/libraries/ESP32/examples/Serial/InterruptQueue/InterruptQueue.ino new file mode 100644 index 00000000000..945eaebb7ac --- /dev/null +++ b/libraries/ESP32/examples/Serial/InterruptQueue/InterruptQueue.ino @@ -0,0 +1,59 @@ +/** Tim Koers - 2021 + * + * This sketch shows the usage of an queue, to store and read the characters that are sent to the interrupt. + * You can safely use this in a FreeRTOS environment and use the queue from different tasks on different CPU cores. + * This sketch assumes that when Hi is sent, the device returns an 8 byte message. + * + */ + +#define BUFFER_SIZE 8 + +// This queue is here to handle the interruption of the loop when the interrupt is running. +QueueHandle_t bufferQueue; + +bool messageSent = false; + +// Please keep in mind, since the ESP32 is dual core, +// the interrupt will be running on the same core as the setRxInterrupt function was called on. +static void IRAM_ATTR onSerialRX(uint8_t character, void* user_arg){ + + BaseType_t xHighPriorityTaskWoken; + + if(xQueueSendFromISR(bufferQueue, &character, &xHighPriorityTaskWoken) != pdTRUE){ + log_e("IRQ", "Failed to put character onto the queue\n"); + } +} + +void setup() +{ + bufferQueue = xQueueCreate(BUFFER_SIZE * 4, sizeof(char)); // Create a queue that can hold 4 messages of 8-bytes each. + + assert(bufferQueue != NULL); + + Serial.begin(115200); + Serial2.begin(115200); + + Serial2.setRxInterrupt(onSerialRX, NULL); +} + +void loop() +{ + if(!messageSent){ + Serial.println("Hi"); + messageSent = true; + } + + // Check if data in the queue and check if there is a complete message (8-bytes) in the queue + if(!(uxQueueMessagesWaiting(bufferQueue) % BUFFER_SIZE)){ + char c; + // Check if the queue is not empty, 0 % 8 returns 0 instead, so does 24 % 8 + while(uxQueueMessagesWaiting(bufferQueue) > 0){ + // Get the character from the queue, but don't block if someone else is busy with the queue + if(xQueueReceive(bufferQueue, &c, 0) == pdTRUE) + Serial.write(c); + } + + // Allow the system to request another data set + messageSent = false; + } +} diff --git a/libraries/ESP32/examples/Serial/NoInterrupt/NoInterrupt.ino b/libraries/ESP32/examples/Serial/NoInterrupt/NoInterrupt.ino new file mode 100644 index 00000000000..267d6e656e6 --- /dev/null +++ b/libraries/ESP32/examples/Serial/NoInterrupt/NoInterrupt.ino @@ -0,0 +1,27 @@ +/** Tim Koers - 2021 + * + * This sketch shows the 'regular' way of receiving Serial data and using it. + * In Arduino Libraries that communicate with a UART peripheral/device, it is seen many times that a delay is + * added after each sent message in order to wait for the answer of the device. That is very inefficient since, + * if not done properly, it might block the CPU (-core) and the function will take x milliseconds/seconds to return. + * This sketch assumes that when Hi is sent, the device returns an 8 byte message. + * + */ + +void setup() +{ + Serial.begin(115200); + Serial2.begin(115200); +} + +void loop() +{ + Serial.println("Hi"); + // Now wait for the response to arrive + delay(1000); + + // The response MIGHT have arrived, not sure though + if(Serial.available() == 8){ + Serial.print(Serial2.read()); + } +}