1. 项目背景

这个项目最初并不是为了简单点亮 LED 或让蜂鸣器响一下,而是希望把 FreeRTOS、多外设驱动、任务拆分、状态机、队列通信和嵌入式 UI 组织到一个完整的小型系统中。

从项目里面学到以下知识:

  • 用 FreeRTOS 将输入、显示、传感器、时钟、闹钟和音乐播放拆成独立任务。
  • 用消息队列解耦任务之间的直接调用。
  • 用状态机管理 UI 模式和闹钟生命周期。
  • 用 BSP、Task、App 分层降低代码耦合。
  • 在有限硬件资源下实现时间设置、闹钟设置、贪睡、重复响铃、温湿度显示和低功耗策略。

2. 项目功能

  • 当前时间显示
  • 当前时间手动设置
  • 闹钟时间设置
  • 闹钟开启和关闭
  • 闹钟到点自动响铃
  • PB1 停止响铃
  • PB11 贪睡 5 分钟
  • 重复响铃开关
  • 铃声编号选择和音乐命令队列扩展
  • 旋转编码器调节音量、时间、重复开关和铃声编号
  • OLED 多页面显示
  • DHT11 温湿度采集
  • 响铃超时自动停止
  • 输入事件队列
  • 音乐命令队列
  • OLED 熄屏和 DHT11 降采样
  • 调试页面显示队列丢包计数、任务计数和剩余堆空间

3. 系统总体设计

3.1 硬件架构

项目使用 STM32F103C8T6 作为主控,结合 OLED、DHT11、无源蜂鸣器、旋转编码器和按键完成智能闹钟的完整交互。

模块类别 硬件/接口 功能描述
主控芯片 STM32F103C8T6 系统核心控制器,负责 FreeRTOS 任务调度、外设管理及业务逻辑运行
显示模块 OLED 显示屏 实时显示当前时间、闹钟状态、温湿度信息以及系统调试信息
环境检测模块 DHT11(PA11) 周期性采集环境温度和湿度数据,并显示在 OLED 界面
声音输出模块 无源蜂鸣器(TIM1_CH1 / PA8) 利用 PWM 输出不同频率信号,实现闹钟铃声播放
人机交互模块 旋转编码器(TIM3 Encoder Mode) 用于菜单导航、时间调整、音量调节、铃声切换等参数设置
功能按键1 PB1 控制闹钟启停、关闭响铃等功能
功能按键2 PB11 切换设置页面;长按进入时间设置界面;闹钟响铃时触发贪睡模式
调试模块 PA0 LED 指示系统运行状态,同

在stm32cubemx上的对应引脚关系:

 3.2 软件架构

软件采用三层结构:

  • BSP 驱动层:只关心硬件读写,如 OLED、DHT11、Buzzer、Encoder、Key。
  • Task 任务层:完成周期扫描、数据采集、显示刷新、时钟推进和音乐播放。
  • App 应用层:维护业务状态,如当前时间、闹钟状态、UI 模式、音量和事件分发。

App 层

  •  App_Task      输入事件解释和业务分发
  • Alarm_App     闹钟状态机
  • Clock_App     当前时间维护
  • UI_App        页面模式状态机
  • App_State     音量和活跃状态
  • Input_Event   输入事件队列封装

Task 层

  • Key_Task      按键扫描,生成输入事件
  • Encoder_Task  编码器扫描,生成旋转事件
  • Clock_Task    每秒推进时间,检测闹钟
  • OLED_Task     OLED 页面刷新
  • DHT11_Task    温湿度采集
  • Music_Task    蜂鸣器音乐播放

BSP 层

任务关系图如下:

数据流 说明
Key_Task → InputEventQueue 按键事件发送到输入队列
Encoder_Task → InputEventQueue 编码器事件发送到输入队列
InputEventQueue → App_Task App任务统一处理用户输入
App_Task → Alarm_App 修改闹钟设置
App_Task → Clock_App 修改时间设置
App_Task → UI_App 页面切换
App_Task → App_State 更新系统状态
App_Task → MusicCmdQueue 发送播放音乐命令
Clock_Task → Clock_App 更新时间
Clock_Task → Alarm_App 检查闹钟触发条件
Clock_Task → MusicCmdQueue 闹钟到点发送响铃命令
MusicCmdQueue → BuzzerMusic_Task 蜂鸣器播放控制
DHT11_Task → App_State 更新温湿度数据
OLED_Task ← Clock_App 获取时间数据显示
OLED_Task ← Alarm_App 获取闹钟数据显示
OLED_Task ← UI_App 获取界面状态
OLED_Task ← App_State 获取系统状态与温湿度

这套架构的核心思想是:输入任务只产生事件,应用任务解释事件,业务模块维护状态,显示任务只读取快照并显示。

3.3 FreeRTOS任务配置

系统采用 FreeRTOS 进行任务调度,各任务职责划分如下:

任务名称 功能描述 运行周期 类型
Key_Task 按键扫描与事件生成 10ms 周期任务
Encoder_Task 编码器扫描与事件生成 2ms 周期任务
Clock_Task 时钟推进与闹钟检测 1s 周期任务
OLED_Task OLED页面刷新 100ms 周期任务
DHT11_Task 温湿度采集 1s 周期任务
App_Task 输入事件处理与业务分发 事件驱动 业务任务
BuzzerMusic_Task 铃声播放控制 事件驱动 业务任务

其中 App_Task 作为系统核心控制器,负责解释输入事件并驱动各业务模块运行。

3.4 队列通信设计

系统主要使用消息队列实现任务之间的解耦。

队列名称 数据类型 作用
g_InputEventQueue InputEvent_t 传递按键与编码器输入事件
g_MusicCmdQueue MusicCmd_t 传递音乐播放控制命令

系统的数据流如下:

输入任务 → 输入事件队列 → App_Task → 业务模块

业务模块不直接操作其他任务,而是通过消息队列进行通信。这样能够降低模块耦合度,提高系统可维护性和扩展能力。

3.5 工程目录结构

工程按照 BSP、Task、App 三层进行组织:

Project
│
├── BSP
│   ├── bsp_oled.c
│   ├── bsp_dht11.c
│   ├── bsp_buzzer.c
│   ├── bsp_encoder.c
│   └── bsp_key.c
│
├── App
│   ├── alarm_app.c
│   ├── clock_app.c
│   ├── ui_app.c
│   ├── app_state.c
│   └── input_event.c
│
├── Tasks
│   ├── key_task.c
│   ├── encoder_task.c
│   ├── clock_task.c
│   ├── oled_task.c
│   ├── dht11_task.c
│   └── music_task.c
│
├── Core
│
└── FreeRTOS

这种组织方式使驱动层、任务层和业务层职责清晰,便于后续扩展和维护。

4. 关键模块设计

 4.1 输入事件模块

 功能

输入模块负责把 PB1、PB11 和旋转编码器转换成统一的输入事件。

事件类型包括:

  • `INPUT_EVENT_PB1_SHORT`
  • `INPUT_EVENT_PB11_SHORT`
  • `INPUT_EVENT_PB11_LONG`
  • `INPUT_EVENT_ENCODER_STEP`

实现思路

按键任务周期扫描按键,PB11 采用独立逻辑区分短按和长按。编码器任务以 2ms 周期读取 TIM3 编码器计数,将旋转量投递到输入事件队列。

这里没有让按键任务直接修改闹钟时间,也没有让编码器任务直接修改音量。它们只负责报告“发生了什么输入”。

数据流

发送方

接收方

事件/作用

Key_Task

g_InputEventQueue

发送 PB1 / PB11 按键事件

Encoder_Task

g_InputEventQueue

发送编码器步进(step)事件

g_InputEventQueue

App_Task

输入事件队列传递

App_Task

Alarm_App / Clock_App / UI_App

根据当前 UI 模式进行事件

核心算法

PB11 的长按识别不是继续复用阻塞式 `Key_GetNum()`,而是在任务中直接读取 GPIO 并累计按下时间:

  • 按下超过 800ms,触发长按事件。
  • 松开且按下时间超过 30ms,触发短按事件。
  • 长按已经触发后,松手不再重复触发短按。

这个设计解决了阻塞式按键扫描无法统计按下持续时间的问题。

 4.2 UI 状态机模块

 功能

UI 模式模块负责描述用户当前正在操作哪个页面。

主要模式包括:

枚举值 模式名称 功能说明
UI_MODE_NORMAL 主界面模式 显示当前时间、闹钟状态、温湿度等信息
UI_MODE_SET_ALARM_HOUR 闹钟小时设置 调整闹钟小时数(0~23)
UI_MODE_SET_ALARM_MINUTE 闹钟分钟设置 调整闹钟分钟数(0~59)
UI_MODE_SET_ALARM_REPEAT 闹钟重复设置 设置单次、工作日、每天等重复模式
UI_MODE_SET_ALARM_RINGTONE 闹钟铃声设置 选择蜂鸣器播放的铃声编号
UI_MODE_SET_CLOCK_HOUR 时钟小时设置 手动修改当前时间小时数
UI_MODE_SET_CLOCK_MINUTE 时钟分钟设置 手动修改当前时间分钟数
UI_MODE_DEBUG 调试模式 显示系统状态、任务信息、传感器数据等调试内容

 实现思路

PB11 短按用于闹钟设置流程,PB11 长按用于当前时间设置和调试页面切换。编码器在不同 UI 模式下具有不同含义:

UI模式 页面功能 编码器作用
NORMAL 主界面 调节音量
SET_ALARM_HOUR 闹钟设置 调节闹钟小时
SET_ALARM_MINUTE 闹钟设置 调节闹钟分钟
SET_ALARM_REPEAT 闹钟设置 切换重复开关
SET_ALARM_RINGTONE 闹钟设置 选择铃声编号
SET_CLOCK_HOUR 当前时间设置 调节当前小时
SET_CLOCK_MINUTE 当前时间设置 调节当前分钟

数据流

流程阶段 模块 功能说明
1 Input(输入事件) 接收按键、编码器等输入事件
2 App_Task 统一处理输入事件
3 当前 UI 模式判断 根据 g_CurrentUIMode 选择处理逻辑
4 对应业务模块 执行具体参数修改或界面操作

核心算法

UI 本质上是一个状态机,而不是简单的页面变量。PB11 短按和长按分别对应两条状态转换链:

4.3 闹钟状态机模块

功能

`Alarm_App` 是整个项目的核心业务模块,负责维护闹钟状态、闹钟时间、下一次实际响铃时间、重复开关、贪睡状态和铃声编号。

实现思路

闹钟状态被抽象为 4 个状态:

  • `ALARM_STATE_OFF`:闹钟关闭
  • `ALARM_STATE_ON`:闹钟开启,等待触发
  • `ALARM_STATE_RINGING`:正在响铃
  • `ALARM_STATE_SNOOZING`:贪睡中,等待下一次响铃

状态转换如下:

数据流

Clock_Task
    │
    ▼
Alarm_App_Check(now)
    │
    ▼
到达 nextRingTime ?
    │
   YES
    │
    ▼
AlarmState = RINGING
    │
    ▼
Music_CMD_PostStart()
    │
    ▼
Music Queue
    │
    ▼
BuzzerMusic_Task
    │
    ▼
PWM输出铃声

核心算法

项目中一个很关键的设计是将 `alarmTime` 和 `nextRingTime` 分开:

  •  `alarmTime`:用户设置的原始闹钟时间。
  • `nextRingTime`:系统下一次真正应该响铃的时间。

如果用户设置 07:30,闹钟响后选择贪睡 5 分钟,那么:

alarmTime    = 07:30 nextRingTime = 07:35

这样不会破坏用户设置的原始闹钟时间。贪睡结束后再次响铃,停止响铃时再根据重复开关决定回到 `ON` 还是 `OFF`。

4.4 时钟任务模块

功能

`Clock_Task` 每秒运行一次,负责推进当前时间、更新响铃计时、检测闹钟触发并发送音乐命令。

实现思路

当前时间由软件时钟维护,`Clock_Task` 使用 `vTaskDelayUntil()` 保持 1s 周期运行。

数据流

Clock_Task
    │
    ▼
更新时间
    │
    ▼
Alarm_App_Check(now)
    │
    ├── 到达闹钟时间 ──► START
    │
    └── 响铃超时 ─────► STOP
                           │
                           ▼
                   g_MusicCmdQueue
                           │
                           ▼
                   BuzzerMusic_Task

核心算法

响铃超时属于时间事件,而不是输入事件,因此放在 `Clock_Task` 中处理。当前配置中响铃超时时间为 60 秒,到达后自动停止音乐,并根据重复开关决定闹钟后续状态。

4.5 OLED 显示模块

功能

OLED 负责展示当前时间、闹钟时间、闹钟状态、音量、温湿度、设置页面和调试信息。

实现思路

OLED 任务不直接修改业务状态,而是读取 `Clock_App`、`Alarm_App`、`UI_App`、`App_State` 的快照进行显示。这样显示层不会反向污染业务逻辑。

数据流

业务模块
(Clock / Alarm / UI / DHT11)
          │
         ▼

     App_State
          │
         ▼

     OLED_Task
          │
         ▼

   根据UI模式
      刷新页面
          │
         ▼

      OLED显示

核心算法

OLED 页面按 UI 模式分流:

页面类型 页面名称 显示内容
主页面 正常页(Normal Page) 当前时间、闹钟时间/贪睡时间、音量等级、温度、湿度
设置页面 闹钟设置页(Alarm Setting) 闹钟小时、闹钟分钟、重复开关状态、铃声编号
设置页面 当前时间设置页(Clock Setting) 当前时间调整状态、小时设置、分钟设置
运行页面 响铃页(Ringing Page) 响铃提示、停止操作提示、贪睡操作提示
调试页面 调试页(Debug Page) 输入事件丢包数、编码器事件丢包数、音乐命令丢包数、剩余堆空间、任务运行计数

同时,OLED 任务根据最近用户活动时间实现 30 秒无操作熄屏,响铃时强制点亮。

4.6 DHT11 温湿度模块

功能

DHT11 模块负责采集环境温湿度,并在 OLED 正常页面中显示。

实现思路

BSP 层通过 GPIO 开漏输出和输入模式切换完成 DHT11 单总线协议。使用 TIM2 提供微秒级延时,通过高电平持续时间判断数据位 0 或 1。

数据流

DHT11_Task
      │
      ▼
DHT11_Read_Data()
      │
      ▼
温度 / 湿度
      │
      ▼
App_State
      │
      ▼
OLED_Task
      │
      ▼
OLED显示

核心算法

DHT11 的难点在于时序严格。项目中在读取 40bit 数据前会先发送起始信号,然后切换为输入模式等待 DHT11 响应。读取数据位时,通过统计高电平时间判断当前 bit:

- 高电平时间较短,判断为 0。

- 高电平时间较长,判断为 1。

读取完成后通过校验和判断数据是否有效。

4.7 蜂鸣器音乐模块

功能

蜂鸣器模块负责 PWM 输出、音量控制和音乐播放。

实现思路

BSP 层通过 TIM1 PWM 输出不同频率的方波,音乐任务接收 `g_MusicCmdQueue` 中的 START / STOP 命令后播放或停止旋律。

数据流

Clock_Task
      │
      ├────►
      │
App_Task
      │
      ▼

g_MusicCmdQueue
      │
      ▼

BuzzerMusic_Task
      │
      ▼

Buzzer PWM

核心算法

音符本质上是频率和持续时间的组合。播放时根据频率动态修改 TIM1 自动重装载值和比较值,让无源蜂鸣器输出对应音高。

当前工程已经在应用层维护了铃声编号,并在音乐命令结构中加入了 `ringtone` 字段。这说明系统已经具备多铃声扩展接口。当前播放任务侧主要仍是一张旋律表,后续只需要将旋律表扩展为数组,即可按 `ringtone` 选择不同铃声。

5. 核心技术分析

5.1 为什么使用事件队列,而不是任务互相调用

如果按键任务直接修改闹钟时间,编码器任务直接修改音量,OLED 任务再读取这些变量,系统很快会变成“到处都是 if 和全局变量”的结构。

本项目采用输入事件队列:

输入采集任务 -> 输入事件队列 -> App_Task -> 业务模块

好处是:

  • 输入采集和业务解释解耦。
  • 同一个输入在不同 UI 模式下可以表达不同含义。
  • 业务状态集中在 App 层,便于维护。
  • 事件投递失败可以计数,便于调试。

5.2 为什么闹钟必须使用状态机

闹钟不是简单的“时间到了就响”。真实闹钟至少包含开启、关闭、响铃、贪睡、重复、超时停止等多个状态。如果只用多个布尔变量,会很容易出现矛盾状态。

例如

isAlarmOn = 1

isRinging = 1

isSnoozing = 1

这些变量组合起来含义并不清晰。状态机能明确约束系统在同一时刻只能处于一个主状态,并且所有状态转换都有触发条件。

5.3 为什么要区分 alarmTime 和 nextRingTime

支持贪睡后,用户设置值和系统运行时临时值必须分离。

如果贪睡时直接修改 `alarmTime`,用户原本设置的 07:30 会被改成 07:35。这样下一天重复响铃时,闹钟时间就不再是用户想要的时间。

因此:

  • 用户配置保存在 `alarmTime`。
  • 贪睡后的实际触发时间保存在 `nextRingTime`。

这个设计体现了嵌入式工程中很重要的思想:配置值和运行时状态不要混在一起。

5.4 为什么显示层只读快照

OLED 任务只负责显示,不负责修改状态。比如它可以显示当前处于 `RINGING`,但不能自己停止闹钟。

这样做的好处是:

  1. 显示刷新频率可以独立调整。
  2. 业务状态不会被显示逻辑意外修改。
  3. 后续更换显示屏或 UI 框架时,不需要重写闹钟业务逻辑。

5.5 低功耗策略为什么先做 Sleep 级优化

当前项目的时间由 FreeRTOS 软件时钟推进。如果直接进入 STOP 模式,在没有 RTC 或低功耗唤醒源配合的情况下,SysTick 会受影响,闹钟时间可能不准。

因此当前更合适的策略是先做温和低功耗:

  • 30 秒无操作后 OLED 熄屏。
  • OLED 熄屏后 DHT11 从 1 秒采样一次降低到 5 秒检查一次。
  • 保留 FreeRTOS 调度和软件时钟,避免破坏闹钟核心功能。

源码中已经开启了 `configUSE_IDLE_HOOK`,并定义了 `APP_LOW_POWER_IDLE_SLEEP`,后续可以补充自定义 Idle Hook,在空闲时执行 `__WFI()`。这一步需要结合功耗测试验证,不能只为了低功耗而牺牲时间准确性。

6. 项目难点

难点 1:DHT11 读取不稳定

问题

DHT11 驱动调试时经常读取失败,表面上看像是代码细节错误。

原因分析

DHT11 是单总线时序器件,对微秒级延时、GPIO 输入输出模式切换和中断干扰都比较敏感。如果任务栈不足、延时不准确或读取过程中被打断,都可能导致校验失败。

解决方案

项目中使用 TIM2 实现微秒级延时,读取时根据高电平持续时间判断 bit,并通过校验和验证数据。在读取关键时序段临时关闭中断,减少时序被打断的概率。

最终效果

DHT11 数据可以被周期性采集,并在 OLED 页面显示温湿度。熄屏后任务降低采样频率,减少无意义的传感器访问。

难点 2:PB11 既要短按又要长按

问题

PB11 需要同时承担闹钟设置、当前时间设置、调试页面切换和响铃时贪睡等功能。直接复用阻塞式按键读取函数会导致无法准确识别长按。

原因分析

原始按键函数内部会等待按键释放,这种阻塞方式适合简单短按,但不适合统计“按住了多久”。一旦任务阻塞,就很难做长按计时和事件分发。

解决方案

在 `Key_Task` 中直接扫描 PB11 GPIO,使用计时变量累计按下时间:

  • 超过 800ms 触发长按。
  • 松开且超过消抖时间触发短按。
  • 长按触发后不再重复触发短按。

最终效果

PB11 可以稳定区分短按和长按,并能在不同状态下表达不同含义:正常页面下切换设置流程,响铃状态下触发贪睡。

难点 3:编码器残留输入影响模式切换

问题

频繁切换 UI 模式时,编码器计数可能残留,导致刚进入新页面就误调音量、闹钟时间或当前时间。

原因分析

编码器任务持续读取 TIM3 计数,UI 模式切换是另一个事件。当模式刚变化时,旧模式下产生但尚未消费的 step 可能在新模式下被解释,造成错误修改。

解决方案

在 `App_Task` 中引入模式切换保护窗口。每次 UI 模式变化后,短时间内忽略编码器事件,等待残留输入自然清空。

最终效果

模式切换和编码器调参之间的边界更清晰,避免了“没有转编码器,数值却变化”的交互 bug。

难点 4:任务之间职责容易混乱

问题

功能增加后,按键、OLED、闹钟和蜂鸣器逻辑容易互相调用,代码会快速失控。

原因分析

简单 demo 阶段可以把所有逻辑写在一个任务中,但闹钟加入贪睡、重复、铃声、当前时间设置后,业务关系复杂度明显上升。

解决方案

将系统拆为 BSP、Task、App 三层:

  • BSP 层只读写硬件。
  • Task 层负责周期行为和事件产生。
  • App 层维护业务状态和状态机。

任务之间通过队列传递事件,不直接修改其他任务内部变量。

最终效果

代码结构更接近真实嵌入式项目,新增功能时主要扩展状态机和事件分发逻辑,而不是在各个任务里到处补条件判断。

7. 项目成果展示

7.1 OLED 页面效果

系统包含多个 OLED 页面:

正常页面:当前时间、闹钟时间、闹钟状态、音量、温湿度、编码器状态。

闹钟设置页面:小时、分钟、重复开关、铃声编号。

当前时间设置页面:当前小时和分钟。

响铃页面:显示响铃提示、PB1 停止、PB11 贪睡。

调试页面:显示输入事件丢包、编码器事件丢包、音乐命令丢包和剩余堆空间。

7.2 运行行为

系统启动后,时钟从默认时间开始运行,闹钟模块初始化为开启状态。用户可以通过 PB11 进入设置流程,通过编码器调整参数,通过 PB1 开启或关闭闹钟。

当当前时间到达 `nextRingTime` 时,闹钟进入 `RINGING` 状态并发送音乐播放命令。响铃过程中:

  • PB1:停止响铃。
  • PB11:进入 5 分钟贪睡。
  • 60 秒无操作:自动停止响铃。

7.3 调试与可靠性指标

项目没有在代码中固化具体性能测试报告,因此不虚构响应时间或 CPU 占用率。当前可展示的工程化观测指标包括:

  • `g_InputEventDropCount`:按键事件队列丢包计数。
  • `g_EncoderEventDropCount`:编码器事件队列丢包计数。
  • `g_MusicCmdDropCount`:音乐命令队列丢包计数。
  • `xPortGetFreeHeapSize()`:剩余堆空间。
  • 任务计数器:观察 Key、PB1、PB11、OLED 等任务运行状态。

这些指标可以直接显示在 OLED 调试页上,用于判断系统是否存在队列拥塞、任务卡死或堆空间不足。

8. 项目总结

8.1 技术收获

这个项目覆盖了 STM32 HAL、GPIO、定时器 PWM、TIM 编码器模式、DHT11 单总线时序、OLED 显示、FreeRTOS 任务、队列、互斥量和软件时钟等知识点。

其中最重要的不是某一个外设怎么驱动,而是理解了:外设只是能力,真正决定项目质量的是任务如何拆、状态如何管、事件如何流动。

8.2 架构收获

项目从最初的蜂鸣器播放 demo,逐步演进为 BSP、Task、App 分层结构。这个过程说明,当功能变多后,不能靠到处加 if 维护系统,而要先抽象状态,再设计状态转换,最后决定哪个任务负责触发。

8.3 工程收获

项目中遇到过栈空间不足、OLED 共享资源竞争、按键长按识别、编码器残留输入、DHT11 时序敏感、队列消息结构升级等问题。这些问题都不是单纯语法错误,而是典型的嵌入式工程问题。

解决它们的过程训练了几个重要习惯:

  • 出现运行异常时,要优先考虑栈、堆、任务阻塞和外设时序。
  • 共享资源要用互斥量或明确所有权保护。
  • 任务通信优先使用队列或事件,不随意改其他任务变量。
  • 配置值和运行时状态要分开。
  • 显示层只读状态,不直接修改业务。

8.4 个人心得

这个项目的动机很简单:就是高中时期用过的一款闹钟,功能和现在这个项目非常接近。后来学习 RTOS 时,我意识到不能等到把整个 RTOS 的所有底层原理都完全学透之后,才开始做一个完整项目。真正的工程思维不是先把所有知识点孤立学完,而是在实操中不断遇到问题、定位问题、修正设计。

项目最开始是从 STM32CubeMX 配置和点亮 LED 起步的,随后逐步编写 OLED、按键、LED、电位器等 BSP 驱动。后来为了实现音乐播放,又引入了蜂鸣器;为了增加环境感知能力,又加入了 DHT11 温湿度传感器。DHT11 的调试花了很长时间,最初一直以为是代码细节问题,后来才发现更关键的是堆栈资源和任务配置问题。这次经历让我意识到,嵌入式调试不能只盯着某一行代码,堆、栈、任务调度、外设时序同样是排查重点。

开发工具上,项目也从单纯依赖 Keil,逐渐切换到 VS Code 编写代码、Keil 调试和烧写的方式。这个变化本身也是一次工程习惯的调整:写代码、管理文件、阅读工程结构和调试下载可以由不同工具承担,不必把所有工作都限制在同一个 IDE 里。

在整个开发过程中,堆栈空间问题出现过多次:写早期任务时遇到过,调 DHT11 时遇到过,后面加入贪睡等功能时也再次遇到过。这让我形成了一个很重要的排错习惯:当新增功能后编译没有报错,但单片机运行异常时,不能只怀疑代码细节,还要优先检查任务栈、系统堆、队列大小、任务阻塞和外设时序。

最终学到的核心知识可以概括为四点:

  1. 状态机是防止复杂逻辑混乱的关键。功能越多,越需要先画状态和状态转换,而不是直接写判断条件。
  2. 堆栈空间是 FreeRTOS 项目中非常重要的排查方向。运行异常不一定是语法问题,也可能是资源配置问题。
  3. 任务之间要有明确的保护边界。共享状态要通过临界区、互斥量或封装接口访问,不能随意跨任务改变量。
  4. 任务之间要通过队列、事件或命令通信。输入任务、应用任务、音乐任务各自负责自己的事情,系统才不会因为功能增加而失控。

回头看,这个项目真正训练的不是“会不会驱动某个外设”,而是如何把多个外设、多个任务、多个状态组织成一个还能继续扩展和维护的嵌入式系统。

8.5 后续优化方向

后续可以继续优化

  1. 增加 RTC,用硬件时间替代软件时钟。
  2. 增加 STOP 或 Standby 低功耗模式,并通过 RTC Alarm 或 EXTI 唤醒。
  3. 完善多铃声播放任务,让 `ringtone` 真正选择不同旋律表。
  4. 增加 Flash 参数保存,掉电后保留闹钟时间、音量、重复开关和铃声编号。
  5. 增加蓝牙或串口配置功能。
  6. 增加更完整的任务运行统计和栈水位检测。
  7. 优化 OLED 页面交互,让设置流程更接近真实产品。

9.代码实现

具体代码在github上
fish-eat-no/RTOS_Clock

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐