Chapter 10 · ARM 嵌入式 C 开发

实战:智能传感器节点

综合运用全课程知识,构建完整的 STM32F4 多传感器采集节点:FreeRTOS 多任务、低功耗模式、Bootloader 概念。

课程进度100%

项目概述

本章构建一个完整的室内环境监测节点,综合运用前9章所有知识点:

硬件配置

  • MCU:STM32F407VGT6(Cortex-M4,168MHz,1MB Flash)
  • 温湿度:DHT22(单总线,PA1)
  • 气压:BMP280(I2C,0x76,I2C1:PB6/PB7)
  • 显示:SSD1306 OLED 128×64(I2C,0x3C)
  • 通信:USART1 115200(PA9/PA10)→ ESP8266/PC
  • 状态:LED×2(PD12 绿,PD14 红)
  • 调试:SWD + UART printf

软件架构

  • FreeRTOS(STM32CubeMX 配置)
  • 4 个任务:传感器/显示/通信/系统管理
  • 2 个队列:传感器数据 + 命令
  • 1 个互斥量:保护 I2C 总线
  • IWDG 看门狗(1 秒超时)
  • STOP 低功耗模式(空闲时)
  • Flash 配置存储(W25Q128 可选)

系统架构图

智能传感器节点 — 软件架构全图 ═══════════════════════════════════════════════════════════════ ┌──────────────────────────────────────────────────────────┐ │ FreeRTOS 调度器 │ │ SysTick = 1ms 优先级抢占 + 同级轮转 │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────▼──────┐ ┌───────▼──────┐ ┌──────▼───────┐ ┌────▼─────┐ │ Task_Sensor │ │ Task_Display │ │ Task_Report │ │ Task_SYS │ │ 优先级 4 │ │ 优先级 3 │ │ 优先级 3 │ │ 优先级 1 │ │ │ │ │ │ │ │ │ │ DHT22 采集 │ │ SSD1306 刷新 │ │ UART 上报 │ │ 看门狗 │ │ BMP280 采集 │ │ 格式化显示 │ │ JSON 打包 │ │ 低功耗 │ │ 2秒周期 │ │ 每次收到更新 │ │ 每5秒上报 │ │ 系统监控 │ └──────┬───────┘ └──────▲───────┘ └──────▲───────┘ └──────────┘ │ │ │ │ ┌──────┴──────┐ ┌──────┴──────┐ │ │ qSensorData │ │ qSensorData │ └────────►│ (深度=5) │──► │ xQueueSend│ │ │ xQueueReceive│ └─────────────┘ └─────────────┘ 共享资源保护: I2C 总线(BMP280+SSD1306 共用)→ xMutex_I2C 互斥量保护 UART 发送缓冲区 → 单任务独享(Task_Report 是唯一发送者) 硬件连接: DHT22 ─── PA1 (单总线,4.7kΩ上拉) BMP280 ─── I2C1 (PB6=SCL, PB7=SDA, 4.7kΩ上拉) SSD1306 ── I2C1 (共享总线,地址0x3C) USART1 ─── PA9(TX) → USB-UART → PC串口助手/ESP8266

关键概念

BMP280 气压传感器
博世出品的高精度气压/温度传感器,I2C 地址 0x76(SDO接GND)或 0x77。测量范围:气压 300-1100 hPa(精度 ±1 hPa),温度 -40~85°C。通过读取 Calib 校准参数并代入补偿公式转换原始值。支持 IIR 滤波器和过采样,可大幅降低噪声。
DHT22 单总线协议
Aosong 的温湿度传感器,单线双向通信(半双工)。主机发起采集:拉低总线 >1ms,再拉高 20~40μs,DHT22 响应低电平 80μs + 高电平 80μs,然后发送 40 bit 数据(湿度16bit + 温度16bit + 校验8bit)。bit "0" = 低50μs+高26~28μs;bit "1" = 低50μs+高70μs。精度:温度 ±0.5°C,湿度 ±2%RH。
低功耗模式 STOP
STM32F4 的 STOP 模式:关闭所有时钟(CPU/外设/AHB/APB),保留 SRAM 和寄存器内容,功耗降至约 1.1mA(vs 正常 168MHz 运行 ~50mA)。可由 EXTI 中断、RTC 闹钟唤醒,唤醒后从 HSI 恢复(需重新配置系统时钟到 168MHz)。与 FreeRTOS 配合:在 Idle 任务钩子中进入 STOP 模式,等待任务的延时 vTaskDelay 到期(由 RTC 唤醒)。
Bootloader 概念
Bootloader 是一段存储在 Flash 起始地址(0x0800_0000)的小程序,上电后首先运行。其职责:① 检查是否有新固件(通过 UART/CAN/USB 接收);② 若有,将新固件写入应用区(如 0x0800_8000 起)并跳转;③ 若无,直接跳转到应用程序。实现固件升级(FOTA/OTA)的基础,是量产产品的必备功能。
STANDBY 模式 vs STOP 模式
STOP 模式:保留 SRAM,唤醒后 CPU 从停止点继续执行(系统时钟需重配)。STANDBY 模式:最低功耗(约 2.8μA),SRAM 不保留,只有后备区域(4KB BKPSRAM)保留,唤醒相当于系统复位。适合深度休眠的电池供电设备,唤醒通过 WKUP 引脚(PA0)或 RTC 闹钟。

DHT22 驱动实现

/**
 * DHT22 单总线驱动
 * 需要精确的微秒延时,使用 DWT 计数器实现
 */
#define DHT22_PIN   GPIO_PIN_1
#define DHT22_PORT  GPIOA

/* DWT 微秒延时初始化(Cortex-M4 调试计数器)*/
void DWT_Delay_Init(void) {
  CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  DWT->CYCCNT = 0;
  DWT->CTRL  |= DWT_CTRL_CYCCNTENA_Msk;
}

void delay_us(uint32_t us) {
  uint32_t start   = DWT->CYCCNT;
  uint32_t cycles  = us * (SystemCoreClock / 1000000);
  while ((DWT->CYCCNT - start) < cycles);
}

static void DHT22_SetOutput(void) {
  GPIO_InitTypeDef g = {DHT22_PIN, GPIO_MODE_OUTPUT_OD, GPIO_NOPULL, GPIO_SPEED_FREQ_HIGH, 0};
  HAL_GPIO_Init(DHT22_PORT, &g);
}
static void DHT22_SetInput(void) {
  GPIO_InitTypeDef g = {DHT22_PIN, GPIO_MODE_INPUT, GPIO_PULLUP, GPIO_SPEED_FREQ_HIGH, 0};
  HAL_GPIO_Init(DHT22_PORT, &g);
}

typedef struct { float temp; float humi; uint8_t ok; } DHT22_Data_t;

DHT22_Data_t DHT22_Read(void) {
  DHT22_Data_t result = {0};
  uint8_t data[5] = {0};

  /* 主机发起:拉低 1.5ms,再释放(拉高)*/
  DHT22_SetOutput();
  HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_RESET);
  HAL_Delay(2);
  HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_SET);
  delay_us(30);
  DHT22_SetInput();

  /* 等待 DHT22 响应低电平(约80μs)*/
  uint32_t timeout = HAL_GetTick() + 10;
  while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) && HAL_GetTick() < timeout);
  while (!HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) && HAL_GetTick() < timeout);
  while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN) && HAL_GetTick() < timeout);

  /* 读取 40 bit 数据 */
  for (int i = 0; i < 40; i++) {
    while (!HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN));   /* 等待高电平 */
    delay_us(40);  /* 40μs 后采样:>28μs = 1,<28μs = 0(因为0只有26-28μs高)*/
    if (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN))
      data[i / 8] |= (1 << (7 - i % 8));
    while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN));    /* 等待低电平 */
  }

  /* 校验(前4字节之和 = 第5字节)*/
  if ((data[0]+data[1]+data[2]+data[3]) == data[4]) {
    result.humi = ((data[0] << 8) | data[1]) / 10.0f;
    result.temp = (((data[2] & 0x7F) << 8) | data[3]) / 10.0f;
    if (data[2] & 0x80) result.temp = -result.temp;  /* 负温度 */
    result.ok = 1;
  }
  return result;
}

低功耗 STOP 模式实现

/**
 * 在 FreeRTOS Idle 任务钩子中进入 STOP 模式
 * 配置:FreeRTOSConfig.h 中 configUSE_IDLE_HOOK = 1
 */
void vApplicationIdleHook(void) {
  /* 进入 STOP 模式,降低功耗 */
  /* 由 RTC Alarm 或 EXTI 唤醒(SysTick 在 STOP 中停止!)*/
  HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);

  /* 唤醒后:时钟回到 HSI 16MHz,需要重新配置 PLL */
  SystemClock_Config();   /* 恢复 168MHz */
}

/**
 * 深度低功耗:STANDBY 模式(仅电池供电设备)
 * 唤醒后相当于复位,SRAM 数据丢失
 * 使用 RTC 每分钟唤醒一次,采集数据后再次进入 STANDBY
 */
void Enter_Standby_Mode(uint32_t wake_after_sec) {
  /* 配置 RTC 闹钟 N 秒后唤醒 */
  RTC_AlarmTypeDef sAlarm = {0};
  /* ... 省略 RTC 配置细节 ... */

  /* 清除 WU 标志,使能 WU 引脚 */
  __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
  HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);

  /* 进入 STANDBY(永不返回,只有复位/唤醒才能退出)*/
  HAL_PWR_EnterSTANDBYMode();
}

/* 功耗对比(STM32F407,3.3V 供电):*/
/* 正常运行 168MHz   : ~50mA = ~165mW  */
/* SLEEP 模式(停CPU): ~25mA = ~82mW   */
/* STOP 模式         : ~1.1mA = ~3.6mW */
/* STANDBY 模式      : ~2.8μA = ~9.2μW */

Bootloader 基础概念与跳转

Flash 地址空间分配(Bootloader + Application) ═══════════════════════════════════════════════════════════════ 0x0807_FFFF ┐ │ Application 区(Flash 扇区2-11) │ 用户应用程序(main, FreeRTOS, 驱动等) │ 大小:~1MB - 32KB = ~992KB 0x0800_8000 ─┤ ← Application 入口(中断向量表起始) │ │ Bootloader 区(Flash 扇区0-1) │ 大小:2×16KB = 32KB │ 功能:检查UART/CAN是否有新固件 │ 有则接收并写入Application区 │ 无则跳转到Application 0x0800_0000 ─┘ ← 上电复位后,CPU 从此处开始执行 跳转流程: ① Bootloader 读 0x0800_8000(应用程序 MSP 栈顶地址) ② 检查是否是合法地址(在 SRAM 范围内) ③ 设置 MSP(主栈指针) ④ 读 0x0800_8004(复位中断向量),跳转执行
/**
 * Bootloader 跳转到应用程序
 * APP_ADDR = 0x08008000(Application 在 Flash 的起始地址)
 * 应用程序的链接脚本中 FLASH origin 也要改为 0x08008000
 * STM32CubeMX → SCB->VTOR = APP_ADDR(重定向中断向量表)
 */
#define APP_ADDR  0x08008000U

typedef void (*pFunction)(void);

void Bootloader_JumpToApp(void) {
  uint32_t app_sp    = *((uint32_t*) APP_ADDR);         /* 应用栈顶 */
  uint32_t app_reset = *((uint32_t*)(APP_ADDR + 4));    /* 应用复位向量 */

  /* 检查栈顶地址是否在 SRAM 范围内 */
  if ((app_sp & 0xFF000000) != 0x20000000) {
    /* 应用程序不存在或损坏,保持在 Bootloader */
    return;
  }

  /* 关闭所有中断,防止跳转后中断打入 Bootloader 的 ISR */
  __disable_irq();
  HAL_DeInit();

  /* 重定向中断向量表到应用程序地址 */
  SCB->VTOR = APP_ADDR;

  /* 设置主栈指针 */
  __set_MSP(app_sp);

  /* 跳转!*/
  pFunction jump = (pFunction)app_reset;
  jump();  /* 永不返回 */
}

/* Bootloader main 逻辑(简化)*/
int main(void) {
  HAL_Init();
  SystemClock_Config();
  USART1_Init();

  /* 等待 500ms,若收到升级命令进入升级流程 */
  if (Bootloader_CheckUpdate()) {
    Bootloader_ReceiveFirmware();   /* Ymodem/XMODEM 或自定义协议 */
  }

  Bootloader_JumpToApp();   /* 跳转到应用 */
  while(1);  /* 永不到达 */
}

完整项目数据上报格式

/* Task_Report:每 5 秒打包一次 JSON 上报 */
void Task_Report(void *pvParams) {
  SensorData_t data;
  char json_buf[128];
  static uint32_t seq = 0;

  for (;;) {
    /* 等待最新数据(超时 6 秒,超时则上报错误)*/
    if (xQueuePeek(qSensorData, &data, pdMS_TO_TICKS(6000)) == pdPASS) {
      snprintf(json_buf, sizeof(json_buf),
        "{\"seq\":%lu,\"t\":%.1f,\"h\":%.1f,\"p\":%.1f,\"ts\":%lu}\r\n",
        seq++, data.temperature, data.humidity,
        data.pressure / 100.0f,   /* Pa → hPa */
        data.timestamp);
    } else {
      snprintf(json_buf, sizeof(json_buf),
        "{\"seq\":%lu,\"err\":\"sensor_timeout\"}\r\n", seq++);
    }
    UART_DMA_Send((uint8_t*)json_buf, strlen(json_buf));
    vTaskDelay(pdMS_TO_TICKS(5000));
  }
}
项目工程检查清单

① STM32CubeMX 中开启所有用到的外设时钟(RCC → AHB/APB Enable)
② DMA Stream/Channel 对照手册确认无误
③ FreeRTOS 优先级:configMAX_PRIORITIES=8;ISR 优先级 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY
④ 所有共享变量加 volatile,跨任务共享资源用 Mutex 保护
⑤ 每个任务用 uxTaskGetStackHighWaterMark() 验证栈充足(剩余 > 20 字)
⑥ 低功耗模式唤醒后重新调用 SystemClock_Config() 恢复 168MHz
⑦ Bootloader 与 App 的 Flash 起始地址在各自的链接脚本(.ld)和 VTOR 中一致

课程总结:嵌入式 C 开发知识图谱

基础层:架构(Cortex-M4)→ 存储器映射 → 寄存器操作 → GPIO / 时钟
中断层:NVIC → 优先级 → EXTI → ISR 规范 → 临界区
外设层:定时器/PWM → UART → I2C → SPI → ADC/DAC
高效层:DMA(零拷贝)→ 双缓冲 → 环形缓冲区
系统层:FreeRTOS → 任务/队列/信号量 → 低功耗 → Bootloader

嵌入式开发的核心思维:理解硬件,用软件控制硬件,用系统协调软件。