1. 项目背景与核心需求
最近在做一个基于FreeRTOS的空气质量检测仪项目,这个设备需要实时监测PM2.5、温湿度、TVOC等环境参数,并通过OLED屏幕直观展示数据。不同于简单的传感器数据采集,这个项目最大的挑战在于如何在一个资源受限的嵌入式系统中实现多任务协同工作,同时保证数据的实时性和准确性。
选择FreeRTOS作为操作系统内核,主要看中它的轻量级特性和优秀的实时性能。在STM32F103C8T6这样的Cortex-M3内核MCU上,FreeRTOS内核仅占用6-10KB的ROM空间,却能提供完整的任务调度、内存管理和IPC机制。对于需要同时处理传感器数据采集、数据处理、显示刷新和通信的空气质量检测仪来说,这种资源占用与功能完备性的平衡正是我们需要的。
2. 硬件架构设计
2.1 核心硬件选型
主控芯片选择了STM32F103C8T6,也就是我们常说的"蓝莓派"最小系统板。这款芯片具有:
- 72MHz主频的Cortex-M3内核
- 64KB Flash + 20KB RAM
- 丰富的外设接口(3xUSART, 2xSPI, 2xI2C)
- 价格低廉(约10元/片)
传感器部分采用了以下模块:
- PMS5003激光粉尘传感器(I2C接口)
- SHT30温湿度传感器(I2C接口)
- CCS811 TVOC传感器(I2C接口)
- 0.96寸OLED显示屏(I2C接口)
硬件设计心得:所有传感器都采用I2C接口,这样只需要2个GPIO(SCL/SDA)就可以连接多个设备,大大节省了IO资源。但要注意I2C设备的地址不能冲突,必要时可以通过地址跳线或软件修改地址。
2.2 电源管理设计
考虑到设备可能需要电池供电,我们特别设计了低功耗方案:
- 主控芯片工作在72MHz全速模式
- 传感器采用分时供电策略
- OLED屏幕在不刷新时进入休眠模式
- 整体待机电流控制在15mA以下
电源管理的关键代码片段:
c复制void Sensor_PowerOn(uint8_t sensor_type) {
switch(sensor_type) {
case SENSOR_PM25:
HAL_GPIO_WritePin(PM25_PWR_GPIO_Port, PM25_PWR_Pin, GPIO_PIN_SET);
vTaskDelay(50); // 等待传感器稳定
break;
case SENSOR_TVOC:
HAL_GPIO_WritePin(TVOC_PWR_GPIO_Port, TVOC_PWR_Pin, GPIO_PIN_SET);
vTaskDelay(100); // CCS811需要更长启动时间
break;
// 其他传感器类似
}
}
3. FreeRTOS任务设计
3.1 任务划分与优先级设置
根据功能需求,我们将系统划分为以下几个任务:
| 任务名称 | 优先级 | 堆栈大小 | 主要功能 |
|---|---|---|---|
| Sensor_Collect | 3 | 512字节 | 传感器数据采集 |
| Data_Process | 2 | 1024字节 | 数据滤波与校准 |
| Display_Refresh | 1 | 768字节 | OLED界面刷新 |
| System_Monitor | 4 | 256字节 | 系统状态监控 |
任务创建代码示例:
c复制void StartDefaultTask(void const * argument) {
// 创建传感器采集任务
xTaskCreate(SensorCollect_Task, "Sensor_Collect",
512, NULL, 3, &hSensorCollect);
// 创建数据处理任务
xTaskCreate(DataProcess_Task, "Data_Process",
1024, NULL, 2, &hDataProcess);
// 其他任务类似
}
3.2 任务间通信机制
系统使用了多种IPC机制实现任务同步和数据传递:
-
队列(Queue):用于传感器原始数据的传递
- 创建了一个长度10的队列,每个元素是包含所有传感器数据的结构体
c复制typedef struct { float pm25; float temperature; float humidity; uint16_t tvoc; } SensorData_t; QueueHandle_t xSensorDataQueue = xQueueCreate(10, sizeof(SensorData_t)); -
信号量(Semaphore):用于控制显示刷新频率
- 使用二进制信号量确保每100ms刷新一次界面
c复制
SemaphoreHandle_t xDisplaySemaphore = xSemaphoreCreateBinary(); -
事件组(Event Group):用于系统状态通知
- 定义了几个关键事件位:
c复制#define DATA_READY_BIT (1 << 0) #define ALARM_TRIGGER_BIT (1 << 1) #define LOW_POWER_BIT (1 << 2)
实际开发中发现,FreeRTOS的队列在传递较大结构体时可能会引起内存碎片问题。后来改为使用指针传递,并在接收任务中复制数据,显著改善了内存使用效率。
4. 传感器数据采集与处理
4.1 多传感器协同采集
由于所有传感器共用I2C总线,必须设计合理的采集时序:
- PMS5003粉尘传感器:每2秒读取一次
- SHT30温湿度传感器:每5秒读取一次
- CCS811 TVOC传感器:每10秒读取一次
采集任务的主要逻辑:
c复制void SensorCollect_Task(void *pvParameters) {
SensorData_t sensorData;
TickType_t xLastWakeTime = xTaskGetTickCount();
while(1) {
// 获取PM2.5数据
if(xTaskGetTickCount() - lastPM25Time >= 2000) {
PM25_ReadData(&sensorData.pm25);
lastPM25Time = xTaskGetTickCount();
}
// 类似处理其他传感器
// 发送到数据处理队列
xQueueSend(xSensorDataQueue, &sensorData, portMAX_DELAY);
// 严格周期执行
vTaskDelayUntil(&xLastWakeTime, 100);
}
}
4.2 数据滤波算法
传感器数据通常存在噪声,我们实现了多种滤波算法:
-
滑动平均滤波:用于PM2.5数据
c复制#define FILTER_SIZE 5 float PM25_Filter(float newValue) { static float buffer[FILTER_SIZE] = {0}; static uint8_t index = 0; float sum = 0; buffer[index] = newValue; index = (index + 1) % FILTER_SIZE; for(int i=0; i<FILTER_SIZE; i++) { sum += buffer[i]; } return sum / FILTER_SIZE; } -
卡尔曼滤波:用于温湿度数据
(实现较复杂,此处省略具体代码) -
异常值剔除:当数据突变超过阈值时,保留上次有效值
调试中发现,PMS5003传感器在刚上电时会有30秒左右的预热期,这段时间的数据波动较大。我们在代码中增加了传感器状态检测,预热期间的数据会特别标记,不参与平均值计算。
5. 用户界面设计
5.1 OLED显示布局
使用U8g2图形库驱动OLED,设计了三个主要界面:
-
主界面:实时显示所有传感器数据
code复制------------------- | 室内环境监测 | | PM2.5: 12μg/m³ | | 温度: 25.6℃ | | 湿度: 45%RH | | TVOC: 125ppb | ------------------- -
趋势图界面:显示最近1小时的数值变化曲线
-
设置界面:可调整报警阈值等参数
界面刷新任务的关键代码:
c复制void DisplayRefresh_Task(void *pvParameters) {
while(1) {
// 等待信号量触发刷新
xSemaphoreTake(xDisplaySemaphore, portMAX_DELAY);
u8g2_ClearBuffer(&u8g2);
switch(currentScreen) {
case MAIN_SCREEN:
DrawMainScreen();
break;
case TREND_SCREEN:
DrawTrendScreen();
break;
// 其他界面
}
u8g2_SendBuffer(&u8g2);
}
}
5.2 界面切换逻辑
通过按键中断实现界面切换:
c复制void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin == KEY_Pin) {
// 在事件组中设置界面切换标志
xEventGroupSetBits(xSystemEvents, SCREEN_CHANGE_BIT);
}
}
实际使用中发现,直接在中回调中操作FreeRTOS对象可能引发问题。后来改为在回调中设置标志,由专门的任务处理界面切换,稳定性大幅提升。
6. 系统优化与调试
6.1 内存优化技巧
在仅有20KB RAM的STM32F103上,内存管理至关重要:
- 使用FreeRTOS的heap_4内存管理方案
- 精确控制每个任务的堆栈大小
- 启用栈溢出检测
c复制#define configCHECK_FOR_STACK_OVERFLOW 2 - 使用静态分配创建任务和队列
c复制StaticTask_t xTaskBuffer; StackType_t xStack[512]; xTaskCreateStatic(SensorCollect_Task, "Sensor_Collect", 512, NULL, 3, xStack, &xTaskBuffer);
6.2 实时性保障措施
- 合理设置任务优先级
- 关键代码段禁用中断
c复制taskENTER_CRITICAL(); // 关键代码 taskEXIT_CRITICAL(); - 使用vTaskDelayUntil确保严格周期执行
- 监控CPU使用率
c复制void SystemMonitor_Task(void *pvParameters) { while(1) { float usage = 100.0 * (1.0 - (float)uxTaskGetIdleTime() / 1000); // 记录或处理CPU使用率数据 vTaskDelay(1000); } }
7. 常见问题与解决方案
7.1 I2C总线冲突
现象:多个传感器同时使用时,I2C通信偶尔失败。
解决方案:
- 为每个I2C操作添加互斥锁
c复制SemaphoreHandle_t xI2CSemaphore = xSemaphoreCreateMutex(); void Safe_I2C_Read(uint8_t devAddr, uint8_t regAddr, uint8_t *data, uint8_t len) { xSemaphoreTake(xI2CSemaphore, portMAX_DELAY); HAL_I2C_Mem_Read(&hi2c1, devAddr, regAddr, 1, data, len, 100); xSemaphoreGive(xI2CSemaphore); } - 增加重试机制
- 降低I2C时钟频率(从400kHz降到100kHz)
7.2 显示闪烁问题
现象:OLED刷新时出现闪烁。
解决方案:
- 使用双缓冲机制
- 优化刷新流程:
c复制void DrawMainScreen(void) { u8g2_ClearBuffer(&u8g2); // 绘制所有元素 u8g2_SendBuffer(&u8g2); } - 确保刷新周期稳定(100ms)
7.3 传感器数据异常
现象:偶尔读取到明显不合理的数据。
解决方案:
- 增加数据校验(如CRC检查)
- 实现软件滤波算法
- 添加传感器状态检测
- 异常数据自动重采
8. 项目扩展方向
目前的基础功能已经实现,还可以考虑以下扩展:
- 无线传输功能:添加ESP8266模块,将数据上传到云平台
- 历史数据存储:使用SPI Flash存储历史数据
- 语音提示:通过PWM驱动蜂鸣器实现超标报警
- 低功耗优化:实现真正的低功耗模式,延长电池寿命
添加WiFi功能的示例代码结构:
c复制void WiFi_Upload_Task(void *pvParameters) {
while(1) {
SensorData_t data;
if(xQueueReceive(xSensorDataQueue, &data, 100) == pdTRUE) {
char json[128];
sprintf(json, "{\"pm25\":%.1f,\"temp\":%.1f}", data.pm25, data.temperature);
ESP8266_SendData(json);
}
vTaskDelay(1000);
}
}
在开发这个空气质量检测仪的过程中,最大的收获是对FreeRTOS多任务系统的深入理解。特别是在资源受限的环境下,如何平衡实时性、内存占用和功能完整性,这些经验对后续的嵌入式开发项目都有很大帮助。