【实战】智慧农场项目
本文档面向已经学过 STM32 外设基础、了解 FreeRTOS 基本概念,但从未做过完整 RTOS 实战工程的学习者。
下载例程代码: 下载代码(CLion / VSCode) 下载代码(CubeIDE) 下载代码(keil)
一、项目整体介绍
1.1 这个项目是做什么的
智慧农场项目是一个完整的嵌入式监测与控制系统,运行在 STM32F103 单片机上,使用 FreeRTOS 实时操作系统进行任务调度。
系统能够:
- 实时监测: 温度、湿度、土壤湿度、光照强度、降雨量等环境参数
- 自动控制: 根据土壤湿度自动启停水泵
- 报警提示: 环境参数超出安全范围时触发蜂鸣器和蓝牙报警
- 人机交互: 通过 OLED 显示屏、按键和旋钮进行参数设置和状态查看
1.2 为什么选择"智慧农场"作为实战项目
在真实的嵌入式项目中,单一功能的 demo和完整的产品级系统之间存在巨大鸿沟。智慧农场这 个场景恰好能填平这个鸿沟,因为它具备真实产品的典型特征:
✅ 多传感器融合: 涉及 I2C(AHT20)、ADC(土壤湿度、光照、降雨量)等多种外设
✅ 实时性要求不同: 传感器采集需要周期执行,用户输入需要快速响应,报警需要及时处理
✅ 任务间协作: 传感器数据要传递给显示任务,报警消息要传递给蓝牙任务
✅ 资源竞争: OLED 和 AHT20 共用 I2C1 总线,需要互斥访问
✅ 用户交互: 有完整的菜单系统、参数编辑逻辑
这正是真实嵌入式项目的典型特征。
1.3 学完这个项目你能掌握什么
不只是函数调用,而是工程思维:
- 如何将一个复杂系统拆分成多个任务
- 如何设计任务的优先级和执行周期
- 任务之间如何安全地交换数据
- 如何避免资源竞争导致的死锁和数据错乱
- 如何处理周期性任务和事件驱动任务
二、系统整体架构设计
2.1 系统由哪些模块组成
从硬件模块看,系统包含:
| 模块 | 功能 | 通信接口 |
|---|---|---|
| AHT20 | 温湿度传感器 | I2C1 |
| OLED | 显示屏 | I2C1 |
| 土壤湿度传感器 | 检测土壤湿度 | ADC1 |
| 光照传感器 | 检测光照强度 | ADC2 |
| 降雨量传感器 | 检测降雨量 | ADC |
| 水泵 | 自动灌溉 | GPIO |
| 蜂鸣器 | 报警提示 | GPIO + 定时器 |
| 按键 | 用户输入 | GPIO |
| 旋钮 | 参数调节 | GPIO |
| 蓝牙模块 | 无线通信 | UART3 + DMA |
硬件连接说明
本项目所有外设模块均通过杜邦线连接到STM32学习板的相应接口。

图 2.1: 智慧农场项目硬件连接示意图
上图展示了所有模块与STM32学习板的连接方式。如果你手头有实物,可以按照上图进行接线。
关键连接说明:
-
I2C1 总线连接(共享总线,需要特别关注):
- OLED 显示屏: SCL → PB6, SDA → PB7
- AHT20 温湿度传感器: SCL → PB6, SDA → PB7
- ⚠️ 注意: 两个设备共用 I2C1 总线,这就是为什么代码中需要使用互斥锁
i2c1Mutex来保护总线访问
-
ADC 传感器连接:
- 光照传感器: AO → PB1(ADC1_IN9)
- 土壤湿度传感器: AO → PA0(ADC2_IN0)
- 降雨量传感器: ADC 输入引脚请参考具体硬件配置
-
GPIO 输入设备:
- KEY1 按键: PB12(页面切换)
- KEY3 按键: PB15(编辑模式切换)
- 旋钮(编码器): CH1 → PA8, CH2 → PA9(使用 TIM1 编码器模式)
-
输出设备:
- 水泵继电器: PA12(低电平触发)
- 蜂鸣器: PB9(TIM4_CH4, PWM 控制)
-
UART3 蓝牙模块:
- TX: PB10
- RX: PB11
- 使用 DMA 方式传输,提高效率
电源连接:
- 所有模块 VCC 接 3.3V 或 5V(根据模块规格)
- 所有模块 GND 接 STM32 GND
- ⚠️ 注意: 务必保证共地,否则通信会失败
从软件任务看,系统划分为 4 个任务 + 1 个定时器:
┌─────────────────────────────────────────────────┐
│ FreeRTOS 调度器 │
├─────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ SensorTask │ │ InputTask │ │
│ │ 优先级:普通 │ │ 优先级:高 │ │
│ │ 周期:1000ms │ │ 周期:10ms │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ScreenTask │ │ BLETask │ │
│ │ 优先级:低 │ │ 优先级:低 │ │
│ │ 周期:10ms │ │ 事件驱动 │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────┐ │
│ │ BeepTimer (软件定时器) │ │
│ │ 周期:500ms │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────┘
2.2 这是一个"任务驱动"的系统
为什么不用 while(1) 全部写在 main 里?
在裸机编程中,你可能习惯这样写:
int main() {
// 初始化所有外设
AHT20_Init();
OLED_Init();
// ...
while(1) {
// 读取传感器
AHT20_Read(&temp, &humi);
soilMoisture = SoilMoisture_Get();
// ...
// 检查报警
if (temp > MAX_TEMP) {
Beep_on();
}
// ...
// 刷新显示
OLED_ShowTemp(temp);
OLED_ShowHumi(humi);
// ...
// 检测按键
if (KEY1_Pressed()) {
// 切换页面
}
// ...
delay_ms(10);
}
}
这种写法在实际项目中会遇到什么问题?
❌ 问题 1: 代码耦合严重
- 传感器读取、显示刷新、按键检测全混在一起
- 修改显示逻辑可能会影响传感器采集
❌ 问题 2: 实时性无法保证
- OLED 刷新需要时间(通过 I2C 发送整屏数据)
- 在刷新 OLED 期间,按键检测被阻塞,用户会感觉"按键不灵敏"
- 传感器采集也被延迟,数据时效性变差
❌ 问题 3: 难以扩展
- 要添加蓝牙功能,需要在主循环里加一堆代码
- 主循环变得越来越长,越来越难维护
❌ 问题 4: 资源竞争无保护
- AHT20 和 OLED 共用 I2C1,交替访问时可能出现数据错乱
- 需要手动管理互斥访问,容易出错
FreeRTOS 如何解决这些问题?
FreeRTOS 通过任务调度让每个功能模块独立运行:
// 传感器任务:每秒执行一次
void SensorTask() {
while(1) {
// 采集数据
AHT20_Read(&temp, &humi);
// 检查报 警
osDelay(1000);
}
}
// 显示任务:每10ms执行一次
void ScreenTask() {
while(1) {
// 刷新显示
OLED_Refresh();
osDelay(10);
}
}
// 输入任务:每10ms执行一次
void InputTask() {
while(1) {
// 检测按键
if (KEY1_Pressed()) {
// 切换页面
}
osDelay(10);
}
}
每个任务都是独立的执行流,FreeRTOS 调度器会根据优先级自动切换任务执行:
- InputTask 优先级最高,按键检测不会被其他任务阻塞
- ScreenTask 优先级较低,不会影响传感器采集和报警检测
- SensorTask 每秒执行一次,保证数据及时更新
这就是任务驱动的核心思想:关注点分离 + 调度器管理。

