Hi there,
I'm now building a thermography camera which uses MLX90640, Seeed Studio XIAO ESP32S3, and 2.4" TFT LCD with touch sensor and SD card I/F.
Here is a block diagram:
1st progress
Two functions are running on the ESP32.
ProcessInput()
is to acquire the thermal image sent from the MLX90640 via I2C.
And the other ProcessOutput()
is to interpolate the thermal image to make resolution higher and display it in color.
These functions run sequentially in loop()
on ESP32 Core 1 and everything works fine like as follows:
2nd progress
So as a next step, to speed up the frame rate, I rewrote the program so that ProcessInput()
runs on core 1 as Task1
and ProcessOutput()
runs on core 0 as Task2
. And those are connected with double buffers and some synchronous handshaking.
The handshaking between two tasks is using a message queue and a counting semaphore. The expected effect is expressed in a timing diagram as follows:
(Edit: Added "Core 0" and "Core 1" to each time line.)
(Edit: If you can not understand my poor diagram, please refer to Multiple buffering - Wikipedia)
I think it will be easier to understand if you actually see my program, which I will show it at the end.
Result
When running on multiple cores, I found that the processing time of ProcessInput()
increased, resulting in no improvement in frame rate at all
ProcessInput() [ms] | ProcessOutput() [ms] | Frame rate [Hz] | |
---|---|---|---|
Single-core sequential processing | 74 | 52 | 7.9 |
Multi-cores parallel processing | 127 | 53 | 7.9 |
Note that 7.9 ≒ 1000 / (74 + 152) in the 1st column, and 7.9 ≒ 1000 / 127 in the 2nd column.
Questions
I suspect that I2C and SPI share one APB (Advanced Peripheral Bus) and there is a conflict, which makes the I2C slower. This is based on ESP32 Technical Reference Manual, or the following thread:
So my 1st question is as stated in the title, or is my suspicion correct, or is there any other reasons?
Then my 2rd question is whether there is a way to improve the frame rate in my camera system.
And my last question is whether my multi-tasking programming is good or bad.
I'm hoping for advice from someone more knowledgeable.
Thanks in advance
A simple sketch for observing handshaking and processing time in multi-tasking.
SyncTasks.ino
The ENA_MULTITASKING
setting switches between single tasking and multitasking.
#include <Arduino.h>
/*=============================================================
* Step 1: Select whether to multitask or not
*=============================================================*/
#define ENA_MULTITASKING false
/*=============================================================
* Step 2: Configure expected processing time
*=============================================================*/
#define RANDOMIZE false
#if RANDOMIZE
#define PROCESS(x) delay(random(x))
#else
#define PROCESS(x) delay(x)
#endif
#define PROCESSING_TIME_INPUT 1000
#define PROCESSING_TIME_OUTPUT 2000
// Function prototype defined in multitasking.cpp
void task_setup(void (*task1)(uint8_t), void (*task2)(uint8_t, uint32_t, uint32_t));
void ProcessInput(uint8_t bank) {
PROCESS(PROCESSING_TIME_INPUT);
}
void ProcessOutput(uint8_t bank, uint32_t inputStart, uint32_t inputFinish) {
static uint32_t prevFinish;
uint32_t outputStart = millis();
PROCESS(PROCESSING_TIME_OUTPUT);
uint32_t outputFinish = millis();
Serial.printf("Input: %d\nOutput: %d\nCycle: %d\n",
(inputFinish - inputStart ),
(outputFinish - outputStart),
(outputFinish - prevFinish )
);
prevFinish = outputFinish;
}
void setup() {
Serial.begin(115200);
// Start tasks
#if ENA_MULTITASKING
void task_setup(void (*task1)(uint8_t), void (*task2)(uint8_t, uint32_t, uint32_t));
task_setup(ProcessInput, ProcessOutput);
#endif
}
void loop() {
#if ENA_MULTITASKING
delay(1000);
#else
uint32_t inputStart = millis();
ProcessInput(0);
ProcessOutput(0, inputStart, millis());
#endif
}
multitasking.cpp
In multitasking.cpp
, not only the bank number processed by ProcessInput()
but also the processing time is stored in the message queue and passed to ProcessOutput()
.
Then, ProcessOutput()
monitors each processing time and frame rate.
Also, if you set each task to run on the same core, it will behave the same as single tasking.
#include <Arduino.h>
#define TASK1_CORE 1
#define TASK2_CORE 0
#define TASK1_PRIORITY 2
#define TASK2_PRIORITY 1
// Message queue sent from task 1 to task 2
typedef struct {
uint8_t bank; // Exclusive bank numbers for Task 1 and Task 2
uint32_t start; // Task 1 start time
uint32_t finish; // Task 1 Finish Time
} MessageQueue_t;
// Define two tasks on the core
void Task1(void *pvParameters);
void Task2(void *pvParameters);
// Define pointers to the tasks
static void (*Process1)(uint8_t bank);
static void (*Process2)(uint8_t bank, uint32_t start, uint32_t finish);
// Message queues and semaphores for handshaking
static TaskHandle_t taskHandle[2];
static QueueHandle_t queHandle;
static SemaphoreHandle_t semHandle;
#define HALT() { for(;;) delay(1000); }
// The setup function runs once when press reset or power on the board
void task_setup(void (*task1)(uint8_t), void (*task2)(uint8_t, uint32_t, uint32_t)) {
// Pointers to the tasks to be executed.
Process1 = task1;
Process2 = task2;
// To process tasks in parallel, the semaphore must have an initial count of 1
semHandle = xSemaphoreCreateCounting(1, TASK1_CORE != TASK2_CORE ? 1 : 0);
queHandle = xQueueCreate(1, sizeof(MessageQueue_t));
// Check if the queue or the semaphore was successfully created
if (queHandle == NULL || semHandle == NULL) {
Serial.println("Can't create queue or semaphore.");
HALT();
}
// Set up sender task in core 1 and start immediately
xTaskCreatePinnedToCore(
Task1, "Task1",
8192, // The stack size
NULL, // Pass reference to a variable describing the task number
TASK1_PRIORITY, // priority
&taskHandle[0], // Pass reference to task handle
TASK1_CORE
);
// Set up receiver task on core 0 and start immediately
xTaskCreatePinnedToCore(
Task2, "Task2",
8192, // The stack size
NULL, // Pass reference to a variable describing the task number
TASK2_PRIORITY, // priority
&taskHandle[1], // Pass reference to task handle
TASK2_CORE
);
}
/*--------------------------------------------------*/
/*------------------- Handshake --------------------*/
/*--------------------------------------------------*/
uint8_t SendQueue(uint8_t bank, uint32_t start, uint32_t finish) {
MessageQueue_t queue = {
bank, start, finish
};
if (xQueueSend(queHandle, &queue, portMAX_DELAY) == pdTRUE) {
// Serial.println("Give queue: " + String(queue.bank));
} else {
Serial.println("unable to send queue");
}
return !bank;
}
MessageQueue_t ReceiveQueue() {
MessageQueue_t queue;
if (xQueueReceive(queHandle, &queue, portMAX_DELAY) == pdTRUE) {
// Serial.println("Take queue: " + String(queue.bank));
} else {
Serial.println("Unable to receive queue.");
}
return queue;
}
void TakeSemaphore(void) {
if (xSemaphoreTake(semHandle, portMAX_DELAY) == pdTRUE) {
// Serial.println("Take semaphore.");
} else {
Serial.println("Unable to take semaphore.");
}
}
void GiveSemaphore(void) {
if (xSemaphoreGive(semHandle) == pdTRUE) {
// Serial.println("Give semaphore.");
} else {
Serial.println("Unable to give semaphore.");
}
}
/*--------------------------------------------------*/
/*--------------------- Tasks ----------------------*/
/*--------------------------------------------------*/
void Task1(void *pvParameters) {
uint8_t bank = 0;
while (true) {
uint32_t start = millis();
// some process
Process1(bank);
// Serial.println(millis() - start);
bank = SendQueue(bank, start, millis());
TakeSemaphore();
}
}
void Task2(void *pvParameters) {
while (true) {
MessageQueue_t queue = ReceiveQueue();
GiveSemaphore();
// some process
Process2(queue.bank, queue.start, queue.finish);
}
}
Thanks for reading this long post.