/**
* ESP32 Sample and Process Solution
*
* Sample ADC in an ISR, process on one core, handle CLI on the other.
*
* Note: Changes from original, single-core version are marked with a
* "%%%" in the comment.
*
* Date: March 3, 2021
* Author: Shawn Hymel
* License: 0BSD
*/
// You'll likely need this on vanilla FreeRTOS
//#include semphr.h
// Core definitions (assuming you have dual-core ESP32)
// %%% We can use both cores now
static const BaseType_t pro_cpu = 0;
static const BaseType_t app_cpu = 1;
// Settings
static const char command[] = "avg"; // Command
static const uint16_t timer_divider = 8; // Divide 80 MHz by this
static const uint64_t timer_max_count = 1000000; // Timer counts to this value
static const uint32_t cli_delay = 10; // ms delay
enum { BUF_LEN = 10 }; // Number of elements in sample buffer
enum { MSG_LEN = 100 }; // Max characters in message body
enum { MSG_QUEUE_LEN = 5 }; // Number of slots in message queue
enum { CMD_BUF_LEN = 255}; // Number of characters in command buffer
// Pins
static const int adc_pin = A0;
// Message struct to wrap strings for queue
typedef struct Message {
char body[MSG_LEN];
} Message;
// Globals
static hw_timer_t *timer = NULL;
static TaskHandle_t processing_task = NULL;
static SemaphoreHandle_t sem_done_reading = NULL;
static portMUX_TYPE spinlock = portMUX_INITIALIZER_UNLOCKED;
static QueueHandle_t msg_queue;
static volatile uint16_t buf_0[BUF_LEN]; // One buffer in the pair
static volatile uint16_t buf_1[BUF_LEN]; // The other buffer in the pair
static volatile uint16_t* write_to = buf_0; // Double buffer write pointer
static volatile uint16_t* read_from = buf_1; // Double buffer read pointer
static volatile uint8_t buf_overrun = 0; // Double buffer overrun flag
static float adc_avg;
//****************************************************************************
*
// Functions that can be called from anywhere (in this file)
// Swap the write_to and read_from pointers in the double buffer
// Only ISR calls this at the moment, so no need to make it thread-safe
void IRAM_ATTR swap() {
volatile uint16_t* temp_ptr = write_to;
write_to = read_from;
read_from = temp_ptr;
}
//****************************************************************************
*
// Interrupt Service Routines (ISRs)
// This function executes when timer reaches max (and resets)
void IRAM_ATTR onTimer() {
static uint16_t idx = 0;
BaseType_t task_woken = pdFALSE;
// If buffer is not overrun, read ADC to next buffer element. If buffer is
// overrun, drop the sample.
if ((idx < BUF_LEN) && (buf_overrun == 0)) {
write_to[idx] = analogRead(adc_pin);
idx++;
}
// Check if the buffer is full
if (idx >= BUF_LEN) {
// If reading is not done, set overrun flag. We don't need to set this
// as a critical section, as nothing can interrupt and change either value.
if (xSemaphoreTakeFromISR(sem_done_reading, &task_woken) == pdFALSE) {
buf_overrun = 1;
}
// Only swap buffers and notify task if overrun flag is cleared
if (buf_overrun == 0) {
// Reset index and swap buffer pointers
idx = 0;
swap();
// A task notification works like a binary semaphore but is faster
vTaskNotifyGiveFromISR(processing_task, &task_woken);
}
}
// Exit from ISR (Vanilla FreeRTOS)
//portYIELD_FROM_ISR(task_woken);
// Exit from ISR (ESP-IDF)
if (task_woken) {
portYIELD_FROM_ISR();
}
}
//****************************************************************************
*
// Tasks
// Serial terminal task
void doCLI(void *parameters) {
Message rcv_msg;
char c;
char cmd_buf[CMD_BUF_LEN];
uint8_t idx = 0;
uint8_t cmd_len = strlen(command);
// Clear whole buffer
memset(cmd_buf, 0, CMD_BUF_LEN);
// Loop forever
while (1) {
// Look for any error messages that need to be printed
if (xQueueReceive(msg_queue, (void *)&rcv_msg, 0) == pdTRUE) {
Serial.println(rcv_msg.body);
}
// Read characters from serial
if (Serial.available() > 0) {
c = Serial.read();
// Store received character to buffer if not over buffer limit
if (idx < CMD_BUF_LEN - 1) {
cmd_buf[idx] = c;
idx++;
}
// Print newline and check input on 'enter'
if ((c == '\n') || (c == '\r')) {
// Print newline to terminal
Serial.print("\r\n");
// Print average value if command given is "avg"
cmd_buf[idx - 1] = '\0';
if (strcmp(cmd_buf, command) == 0) {
Serial.print("Average: ");
Serial.println(adc_avg);
}
// Reset receive buffer and index counter
memset(cmd_buf, 0, CMD_BUF_LEN);
idx = 0;
// Otherwise, echo character back to serial terminal
} else {
Serial.print(c);
}
}
// Don't hog the CPU. Yield to other tasks for a while
vTaskDelay(cli_delay / portTICK_PERIOD_MS);
}
}
// Wait for semaphore and calculate average of ADC values
void calcAverage(void *parameters) {
Message msg;
float avg;
// Start a timer to run ISR every 100 ms
// %%% We move this here so it runsin core 0
timer = timerBegin(0, timer_divider, true);
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, timer_max_count, true);
timerAlarmEnable(timer);
// Loop forever, wait for semaphore, and print value
while (1) {
// Wait for notification from ISR (similar to binary semaphore)
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// Calculate average (as floating point value)
avg = 0.0;
for (int i = 0; i < BUF_LEN; i++) {
avg += (float)read_from[i];
//vTaskDelay(105 / portTICK_PERIOD_MS); // Uncomment to test overrun flag
}
avg /= BUF_LEN;
// Updating the shared float may or may not take multiple isntructions, so
// we protect it with a mutex or critical section. The ESP-IDF critical
// section is the easiest for this application.
portENTER_CRITICAL(&spinlock);
adc_avg = avg;
portEXIT_CRITICAL(&spinlock);
// If we took too long to process, buffer writing will have overrun. So,
// we send a message to be printed out to the serial terminal.
if (buf_overrun == 1) {
strcpy(msg.body, "Error: Buffer overrun. Samples have been dropped.");
xQueueSend(msg_queue, (void *)&msg, 10);
}
// Clearing the overrun flag and giving the "done reading" semaphore must
// be done together without being interrupted.
portENTER_CRITICAL(&spinlock);
buf_overrun = 0;
xSemaphoreGive(sem_done_reading);
portEXIT_CRITICAL(&spinlock);
}
}
//****************************************************************************
*
// Main (runs as its own task with priority 1 on core 1)
void setup() {
// Configure Serial
Serial.begin(115200);
// Wait a moment to start (so we don't miss Serial output)
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Sample and Process Demo---");
// Create semaphore before it is used (in task or ISR)
sem_done_reading = xSemaphoreCreateBinary();
// Force reboot if we can't create the semaphore
if (sem_done_reading == NULL) {
Serial.println("Could not create one or more semaphores");
ESP.restart();
}
// We want the done reading semaphore to initialize to 1
xSemaphoreGive(sem_done_reading);
// Create message queue before it is used
msg_queue = xQueueCreate(MSG_QUEUE_LEN, sizeof(Message));
// Start task to handle command line interface events. Let's set it at a
// higher priority but only run it once every 20 ms.
xTaskCreatePinnedToCore(doCLI,
"Do CLI",
1024,
NULL,
2,
NULL,
app_cpu);
// Start task to calculate average. Save handle for use with notifications.
// %%% We set this task to run in Core 0
xTaskCreatePinnedToCore(calcAverage,
"Calculate average",
1024,
NULL,
1,
&processing_task,
pro_cpu);
// Delete "setup and loop" task
vTaskDelete(NULL);
}
void loop() {
// Execution should never get here
}