CHAPTER 12 / FIELD GUIDE
FreeRTOS:任务、队列与中断
让慢屏幕不拖住采样。
先给数据明确的主人
采样任务拥有 ADC,显示任务拥有 OLED。采样把数值放进队列,显示取出副本。这样避免两个任务同时操作同一个驱动,也不用让显示任务读一个不断被修改的全局对象。
一个长度为 1 的队列适合“只关心最新值”:新的样本覆盖旧样本。按键事件、交易记录这类不能丢的数据应使用事件队列,并明确队列满了如何处理。
实验工程 · 08_tasks
采样任务约 20 ms 一次;显示任务按较慢节奏读取最新值。vTaskDelayUntil 让周期基于计划时刻,而非上次工作结束时刻。
打开 main.c · 查看公共驱动 lab.c · 下载全部工程
#include "lab.h"
#include "freertos/queue.h"
#include "esp_log.h"
static QueueHandle_t samples;
static void sample_task(void *arg) {
(void)arg;
adc_init();
TickType_t next=xTaskGetTickCount();
for(;; ) {
int raw=adc_read_raw();
xQueueOverwrite(samples,&raw);
vTaskDelayUntil(&next,pdMS_TO_TICKS(20));
}
}
static void display_task(void *arg) {
(void)arg;
oled_init();
int raw;
for(;; ) {
if(xQueueReceive(samples,&raw,portMAX_DELAY)==pdTRUE) {
oled_clear();
oled_number(28,16,(unsigned)raw,4);
oled_show();
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void app_main(void) {
samples=xQueueCreate(1,sizeof(int));
assert(samples);
BaseType_t a=xTaskCreate(sample_task,"sample",4096,NULL,5,NULL);
assert(a==pdPASS);
BaseType_t b=xTaskCreate(display_task,"display",4096,NULL,3,NULL);
assert(b==pdPASS);
ESP_LOGI("tasks","two tasks created");
}三种机制的分工
| 机制 | 适合 | 不适合 |
|---|---|---|
| 轮询 | 按键、低速状态检查 | 高频窄脉冲计数 |
| 任务 | 正常业务逻辑、可能等待的 I/O | 假设优先级高就永远不会超时 |
| 中断 | 快速记录事件,通知任务 | 刷屏、长日志、阻塞等待 |
中断打断正常执行,需要简短。需要把事件交给任务时,使用对应 FromISR API,而不是普通阻塞接口。编码器计数优先使用硬件 PCNT,不必为每个脉冲都运行复杂中断逻辑。
volatile 不能替代互斥锁或队列;它不使“读、修改、写”变成原子操作。多核下更不能依赖“应该不会刚好同时发生”。ESP-IDF 的任务栈大小参数以字节计。
动手验证
- 运行 08_tasks,旋转电位器确认显示更新。
- 把显示等待增大到 1 秒,采样任务保持 20 ms。
- 在采样任务每 50 次打印一次计数,确认没有随显示等待变成每秒一次采样。
- 不在每次采样中打印,避免日志本身改变测量对象。
停下来想一想
队列长度为 1,显示任务读取太慢时会看到每一个中间值吗?
查看推导与答案
不会,xQueueOverwrite 刻意保留最新值。这适合仪表读数,不适合必须逐条处理的按钮事件。数据语义决定队列策略。
本章验收
本章一手资料
卡住时可以直接向我提问:附上章节号、工程名、接线和第一条错误日志。