ESP32中ISR工作机制及任务切换异常问题咨询
ESP32 Li-Fi项目ISR任务切换异常问题与疑问解答
我正在做一个基于ESP32的Li-Fi通信项目,用FreeRTOS创建了三个任务:
- 信号接收任务Rx1:负责采集光电信号并转成二进制字符串
- 信号处理任务Rx2:解析Rx1采集的二进制数据并输出字符
- 信号发送任务Tx:将串口输入的字符串转成二进制,控制LED发送光信号
需求是:默认状态下Rx1、Rx2激活,Tx挂起;按下按键后通过ISR切换为Tx激活,Rx1、Rx2挂起。但目前用ISR进行任务切换时出现异常,同时有两个疑问:
- ESP32中ISR(中断服务程序)的工作机制是什么?
- 能否在ISR内部直接挂起或恢复FreeRTOS任务?
项目代码
struct Button { const uint8_t pin; volatile uint8_t numberKeyPresses; }; #define buttonpin 15 #define ledpin 2 #define photopin 34 #define threshold 100 #define buffersize 16000 #define interval 1 //interval in milliseconds TaskHandle_t Rx1_handle; TaskHandle_t Rx2_handle; TaskHandle_t Tx_handle; Button button1 = {buttonpin, 0}; volatile bool pressed = false; void IRAM_ATTR isr() { static uint32_t lastPressTime = 0; // Store the last press time to debounce the button uint32_t currentTime = millis(); // Get the current time // Debounce logic to avoid multiple triggers in quick succession if (currentTime - lastPressTime > 400) { // 200ms debounce time lastPressTime = currentTime; // Increment the press count (wrap around after 2 presses, for example) button1.numberKeyPresses = (button1.numberKeyPresses + 1) % 2; // 2 total presses before wrapping around // Perform actions based on the number of presses switch (button1.numberKeyPresses) { case 0: // First press (or after 5 presses) vTaskResume(Rx1_handle); vTaskResume(Rx2_handle); vTaskSuspend(Tx_handle); // xTaskCreatePinnedToCore(Rx1, "Signal Recieving", 100000, NULL, 1, &Rx1_handle, 1); // xTaskCreatePinnedToCore(Rx2, "Signal Processing", 10000, NULL, 1, &Rx2_handle, 0); // vTaskDelete(Tx_handle); break; case 1: // Second press // Your custom logic for 2nd press, e.g., alternate functionality vTaskSuspend(Rx1_handle); vTaskSuspend(Rx2_handle); vTaskResume(Tx_handle); // vTaskDelete(Rx1_handle); // vTaskDelete(Rx2_handle); // xTaskCreatePinnedToCore(Tx, "Signal Transmitting", 10000, NULL, 1, &Tx_handle, 1); break; default: break; } } } void setup() { Serial.begin(115200); pinMode(ledpin, OUTPUT); // Testing if LED is Working digitalWrite(ledpin, HIGH); delay(500); digitalWrite(ledpin, LOW); pinMode(buttonpin, INPUT_PULLUP); attachInterrupt(button1.pin, isr, RISING); xTaskCreatePinnedToCore( Rx1, // Function to implement the task "Signal Recieving", // Name of the task 10000, // Stack size in words NULL, // Task input parameter 1, // Priority of the task &Rx1_handle, // Task handle 0 // Core where the task should run (Core 0) ); xTaskCreatePinnedToCore( Rx2, // Function to implement the task "Signal Recieving", // Name of the task 10000, // Stack size in words NULL, // Task input parameter 1, // Priority of the task &Rx2_handle, // Task handle 1 // Core where the task should run (Core 0) ); xTaskCreatePinnedToCore( Tx, // Function to implement the task "Signal Transmitting", // Name of the task 10000, // Stack size in words NULL, // Task input parameter 1, // Priority of the task &Tx_handle, // Task handle 1 // Core where the task should run (Core 0) ); vTaskResume(Rx1_handle); vTaskResume(Rx2_handle); vTaskSuspend(Tx_handle); } String binData_rx = ""; char binaryToChar(String binary) { int value = 0; for (int i = 0; i < 8; i++) { value <<= 1; // Shift left to make space for the next bit if (binary.charAt(i) == '1') { value = value | 1; // Set the least significant bit if it's '1' } } return (char)value; // Return the resulting character } void processData() { int indx = binData_rx.indexOf("00000010"); // Serial.println(indx); binData_rx.remove(0, indx+8); while(binData_rx.length()) { String subData = binData_rx.substring(0, 8); binData_rx.remove(0, 8); if(subData == "00000011") break; char ch = binaryToChar(subData); // processedData += ch; Serial.print(ch); vTaskDelay(interval * 8 / portTICK_PERIOD_MS); //in milliseconds // vTaskDelay(interval * 8 ); // in microseconds } binData_rx.clear(); Serial.println(""); } void Rx1(void *parameter) { // Recieves Signal Serial.println("Signal Recieving"); for(;;) { int value = analogRead(photopin); if(value > threshold) binData_rx += '0'; else binData_rx += '1'; vTaskDelay(interval / portTICK_PERIOD_MS); // in milli seconds // vTaskDelay(interval); // in micro seconds } } void Rx2(void *parameter) { // Processes the Signal Serial.println("Singal Processing"); for(;;) { if(binData_rx.length() > buffersize) { binData_rx.remove(0, 15900); } else if(binData_rx.indexOf("00000010") > -1) { binData_rx.remove(0, binData_rx.indexOf("00000010")); vTaskDelay(interval * 10 / portTICK_PERIOD_MS); // in milli seconds // vTaskDelay(interval * 10); // in micro seconds processData(); } vTaskDelay(500 / portTICK_PERIOD_MS); // in milli seconds // vTaskDelay(500 * 1000); // in micro seconds } } // Tx part String stringToBinary(String str) { String output = ""; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); // Print the binary representation of each character for (int j = 7; j >= 0; j--) { output += String((c >> j) & 1); } // output += " "; } return output; } void Tx(void *parameter) { Serial.println("Signal Transmitting"); for(;;) { if(Serial.available() > 0) { String input = Serial.readStringUntil('\n'); String binData_tx = "00000010"+stringToBinary(input)+"0000001100000011"; //Serial.println(2); Serial.print( binData_tx); for(int i=0; i<binData_tx.length(); i++) { if(binData_tx.charAt(i) == '1') { digitalWrite(ledpin, HIGH); //delayMicroseconds(interval); } else { digitalWrite(ledpin, LOW); //delayMicroseconds(interval); } // digitalWrite(ledPin, (binData[i] == '1') ? HIGH : LOW); delay(interval); // delay of 200ms } binData_tx.clear(); Serial.println(); digitalWrite(ledpin, LOW); } } } void loop() { }
问题解答
一、ESP32 ISR工作机制
ESP32是双核Xtensa LX6处理器,每个核心拥有独立的中断控制器,ISR核心特性如下:
- 高优先级执行:ISR会打断当前运行的任务,优先执行,完成后才回到被打断的上下文。
- 受限运行环境:ISR不能调用阻塞函数(如
delay()、vTaskDelay())或动态内存分配操作,因为这些操作依赖FreeRTOS调度器,而ISR运行时调度器处于暂停状态。 - IRAM存储要求:ISR函数必须用
IRAM_ATTR修饰,将代码放在指令RAM中,避免从SPI Flash加载代码导致延迟或异常。 - 支持中断嵌套:高优先级ISR可以打断低优先级ISR,具体优先级可通过API配置。
二、ISR内部能否挂起/恢复FreeRTOS任务?
可以,但需遵循FreeRTOS的中断安全规则:
- 挂起任务:
vTaskSuspend()本身是中断安全的,它仅修改任务状态,不会触发调度,可直接在ISR中调用。 - 恢复任务:必须使用中断安全版本
xTaskResumeFromISR(),因为恢复任务可能触发上下文切换,需要通过该API通知调度器,并在必要时调用portYIELD_FROM_ISR()主动触发调度。
三、代码异常原因及修复
1. 核心问题
- 直接在ISR中调用非中断安全的
vTaskResume(),可能导致调度器状态异常。 - 使用
millis()获取时间,依赖系统定时器中断,在ISR中调用可能导致时间计数不准确。
2. 修复后的ISR代码
void IRAM_ATTR isr() { static TickType_t lastPressTick = 0; TickType_t currentTick = xTaskGetTickCountFromISR(); // 防抖:400ms转换为系统tick数 if (currentTick - lastPressTick > 400 / portTICK_PERIOD_MS) { lastPressTick = currentTick; button1.numberKeyPresses = (button1.numberKeyPresses + 1) % 2; switch (button1.numberKeyPresses) { case 0: vTaskResume(Rx1_handle); vTaskResume(Rx2_handle); vTaskSuspend(Tx_handle); break; case 1: vTaskSuspend(Rx1_handle); vTaskSuspend(Rx2_handle); // 使用中断安全版本恢复任务 BaseType_t xHigherPriorityTaskWoken = pdFALSE; xTaskResumeFromISR(Tx_handle, &xHigherPriorityTaskWoken); // 若需要切换上下文,通知调度器 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); break; default: break; } } }
3. 额外优化建议
- 共享资源保护:
binData_rx是Rx1和Rx2的共享变量,需用互斥锁(xSemaphoreHandle)保护,避免并发访问导致数据损坏。 - 替换阻塞函数:Tx任务中的
delay()应改为vTaskDelay(),符合FreeRTOS任务调度逻辑。 - 调整任务优先级:给Rx1设置更高优先级,确保信号采集不丢失;Tx任务优先级可与Rx1相同或略低。
内容的提问来源于stack exchange,提问作者curious_boy
相关产品推荐
相关产品推荐

