FreeRTOS 移植到 STM32F407VETX 记录(四)

BMS/BCU FreeRTOS 任务设计指南

从电池控制单元(BCU)的视角,讨论任务划分、优先级分配、栈大小设置的方法论。

基于 STM32F407 + FreeRTOS V11.1.0 实战经验。


一、怎么划分任务 ------ 从功能堆叠到数据流切分

1.1 最常见的错误:按函数名分任务

复制代码
❌ 错误示范:10 个函数 = 10 个任务
  Task1: BootComCheckActivationRequest()
  Task2: ADS1115_AdhesionVol()
  Task3: ADS1115_BusVol()
  Task4: CalcCurrent()
  Task5: CIR_FUN()
  ... 每一个函数一个任务

后果:
  - 10 个 TCB = ~1KB RAM 浪费
  - 10 个栈 = ~60KB RAM 浪费(还没算栈本身的大小)
  - 任务间通信复杂度爆炸(10×10=100 条消息路径)
  - 调度器 10 个任务切来切去,CPU 时间全花在上下文切换上

1.2 正确的切分维度:数据流 + 功能安全级别

BCU 的数据从左到右流动,每个阶段对实时性和安全性的要求不同:

复制代码
传感器/执行器          数据处理              外部系统
─────────────────────────────────────────────────────
                →                    →
   ADC 电压     │    SOC 安时积分      │    HMI 显示屏
   ADS1115 粘连 │    SOH 健康度        │    EMS 能量管理
   DI 状态      │    SOP 功率预测      │    温湿度模块
   DO 控制      │    SOE 能量估算      │
   CIR 绝缘     │    BDS 电池统计      │
                │                    │
  ↑ 物理层      │  ↑ 算法层           │  ↑ 通信层
  I/O 密集      │  CPU 密集            │  可能阻塞
  周期 100ms    │  周期 100ms          │  周期可变
  栈小          │  栈大(矩阵运算)     │  栈中

按这三个阶段切成 5 类任务

复制代码
 ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
 │  SENSORS    │───→│  ALGORITHM  │───→│  PROTOCOLS  │
 │  数据采集    │    │  算法计算    │    │  外部通信    │
 │  优先级 4    │    │  优先级 5    │    │  优先级 3    │
 └──────┬──────┘    └──────┬──────┘    └─────────────┘
        │                  │
        │          ┌───────┴───────┐
        │          │    SAFETY     │  ← 独立的安全路径
        │          │  保护 & 接触器 │
        │          │   优先级 6     │
        │          └───────────────┘
        │
 ┌──────┴──────┐
 │ HOUSEKEEPING│  ← 后台维护
 │  存储 & RTC  │
 │   优先级 1   │
 └─────────────┘

1.3 每类任务的职责边界

SENSORS(数据采集)
复制代码
职责:把物理量变成 cluster 里的数字。只负责"读",不负责"判断"。

包含:
  ✅ ADC 原始值读取 + 偏移/增益校准 + 温度补偿
  ✅ DI 读取 + 软件去抖(3 次确认)
  ✅ 非安全 DO 输出(风扇、状态指示灯)
  ✅ ADS1115 I2C 读、CIR 绝缘检测
  ✅ 单字段原子写入 cluster

不包含:
  ❌ 任何保护逻辑(那是 SAFETY 的事)
  ❌ 算法计算(那是 ALGORITHM 的事)
  ❌ 协议解析(那是 PROTOCOLS 的事)
SAFETY(保护 & 接触器控制)
复制代码
职责:消费 SENSORS 的数据,做保护判断,直接操作接触器 GPIO。

安全原则:决策和执行必须在同一个任务里,中间不能有 IPC。

  ✅ SSM 系统状态机(判断该不该合接触器)
  ✅ ATC 告警控制(告警 LED)
  ✅ FDR 故障诊断 & 记录(抓冻结帧)
  ✅ 接触器 GPIO 直接操控(posRelay/negRelay/preRelay)
  ✅ 过压/过流/过温硬件阈值比对

优先级必须是所有应用任务中最高的。
这个任务阻塞 = 接触器该断的时候不断 = 安全事故。
ALGORITHM(算法计算)
复制代码
职责:纯 CPU 计算。读 cluster 输入,算完写回 cluster。

  ✅ SOC 安时积分 + 卡尔曼滤波
  ✅ SOH 健康状态估算
  ✅ SOP 功率预测
  ✅ SOE 能量估算
  ✅ BDS 电池数据统计

这个任务的特征:
  - CPU 密集型(SOC 卡尔曼滤波可能跑 10-40ms)
  - 栈最大(float[64] 矩阵临时数组在栈上)
  - 不能阻塞 I/O(纯计算,不碰硬件)
  - 优先级仅次于 SAFETY(保证 dt 精确)
PROTOCOLS(外部通信)
复制代码
职责:和外部系统的 CAN/UART 通信。可以阻塞------不影响安全。

  ✅ HMI 显示屏通信(UART)
  ✅ EMS 能量管理系统(CAN)
  ✅ 温湿度模块协议(UART)

为什么可以阻塞:
  - 即使 CAN 消息超时 500ms,SAFETY 还是独立在跑
  - 协议栈的阻塞隔离在 PROTOCOLS 内,不扩散到其他任务
HOUSEKEEPING(后台维护)
复制代码
职责:不紧急的后台工作。优先级最低。

  ✅ RTC 授时同步
  ✅ DSM 数据存储(写 EEPROM/Flash)
  ✅ BootCom 激活检测

优先级最低意味着:只要有任何其他任务就绪,HOUSEKEEPING 就会被抢占。
Flash 写 500ms?没关系------SAFETY 随时可以抢走 CPU。

1.4 判断"这个函数该放哪个任务"的问自己口诀

复制代码
问 1: 它会阻塞吗?(等 I2C、等 CAN、等 Flash 写)
  是 → 谁被它阻塞了?谁都不受影响就随便放,影响 SAFETY 就隔离出去

问 2: 它是安全的吗?(接触器控制、紧急停机)
  是 → 放 SAFETY,优先级最高,不能等任何东西

问 3: 它算得重吗?(卡尔曼、矩阵、FFT)
  是 → 放 ALGORITHM,给大栈,优先级高但低于 SAFETY

问 4: 别人在不在乎它跑没跑完?
  不在乎 → 放 HOUSEKEEPING,优先级最低

二、优先级怎么设 ------ 不是"重要的高",而是"等不起的高"

2.1 优先级本质:谁不能等,谁优先级就高

复制代码
错误认知:  "这个功能重要 → 优先级高"
正确认知:  "这个任务被延迟的后果严重 → 优先级高"

例子:
  接触器断开延迟 50ms → 500A 短路电流多流了 50ms → 🔴 优先级必须最高
  SOC 更新延迟 200ms → 显示的电量慢了 0.2 秒 → 🟢 优先级可以低一些
  RTC 授时延迟 5 秒 → 时间慢了 5 秒 → 🟢 优先级最低,无所谓

2.2 BCU 任务的推荐优先级表

复制代码
优先级(数值)    任务          被延迟 N ms 的后果
─────────────────────────────────────────────────────────
  7              (保留给极端紧急)
  6              SAFETY         接触器延迟=短路风险      最高
  5              ALGORITHM      SOC dt 误差 < 0.01%      高
  4              SENSORS        采样延迟=数据年龄偏大    中
  3              PROTOCOLS      通信延迟=显示慢半拍      中低
  2              (FreeRTOS Timer)
  1              HOUSEKEEPING   存储延迟=少存几秒数据    低
  0              IDLE           喂狗+WFI                最低

2.3 优先级和周期的关系

优先级和周期是两个维度的概念,不能混为一谈。

复制代码
vTaskDelayUntil(&last, 100ms)  ← 这是周期,决定"多久跑一次"
xTaskCreate(..., priority, ...)  ← 这是优先级,决定"谁先跑"

高优先级 + 长周期:  SAFETY  优先级 6,周期 100ms
                   → 到点立刻运行,抢占所有低优先级任务
                   跑完立刻让出 CPU,等下一个 100ms

低优先级 + 短周期:  不常见,因为频繁打断高优先级任务本身就是问题

2.4 同优先级的时间片

BMS 一般不推荐同优先级任务------抢占式调度的优势就在于"紧急的事情立即响应"。如果两个任务优先级相同且都就绪,FreeRTOS 会按时间片(1 tick = 1ms)轮流切换,这不符合 BMS 对确定性时延的要求。

结论:BMS 的每个应用任务给一个独特优先级。不要把优先级当"重要性标签"------多一个优先级多不了几字节 RAM。


三、任务栈大小怎么设 ------ 先估后测,测完再调

3.1 单位陷阱

c 复制代码
xTaskCreate(Task, "name", 1024, ...)
                           ↑
                    单位是 words,不是 bytes!

Cortex-M4: 1 word = 4 bytes
1024 words = 4096 bytes = 4KB

3.2 栈消耗的构成

复制代码
栈顶(高地址)
│
├─ 硬件自动压栈 (exception entry)
│   R0, R1, R2, R3, R12, LR, PC, xPSR    = 8 words    (32B)
│
├─ FreeRTOS 手动压栈 (PendSV context switch)
│   R4, R5, R6, R7, R8, R9, R10, R11      = 8 words    (32B)
│   EXC_RETURN                             = 1 word     (4B)
│
├─ FPU 惰性压栈 (如果任务用过浮点)
│   S0 - S31 (32 个单精度浮点寄存器)        = 32 words   (128B)
│   FPSCR                                  = 1 word     (4B)
│
├─ 中断嵌套 (最坏情况)
│   每层 ISR 嵌套 = 8 words (整数) + 33 words (FPU)
│   BMS 典型 2 层: ~50 words
│
├─ 任务自己的局部变量
│   函数调用深度 × 每层局部变量数组
│   SOC_FUN → CalcSOC → KalmanFilter → MatrixInverse
│   4 层 × 每层可能开 float[32]
│
├─ 编译器生成的栈帧 (prologue/epilogue/spilling)
│
└─ 栈底(低地址)

3.3 初始估算

复制代码
任务           特征                                     初始栈
────────────────────────────────────────────────────────────────
SENSORS       I2C 读 ADS1115 (3层调用深)                 1024 words (4KB)
              局部变量: uint16_t raw[2] 等小数组
              无浮点密集运算

SAFETY        SSM 状态机 (可能有大的 switch-case)         2048 words (8KB)
              FDR 冻结帧结构体 (可能 20+ 字段)
              不做 CPU 密集运算

ALGORITHM     SOC 卡尔曼滤波 (4-5 层调用深)              8192 words (32KB)
              局部变量: float P[16][16] 协方差矩阵
                        float K[16]    卡尔曼增益
                        float x[16]    状态向量
              → 16×16×4 = 1KB 一个矩阵就吃这么多

PROTOCOLS     UART/CAN 收发 buffer                      2048 words (8KB)
              协议解析状态机
              DMA 缓冲区 (可能在全局区,不在栈上)

HOUSEKEEPING  BootCom + RTC + DSM                        1024 words (4KB)
              EEPROM 写 buffer (可能 256B)
              浅调用深度

3.4 运行时测量

uxTaskGetStackHighWaterMark() 获取历史极值------这个值从任务创建以来只降不升:

c 复制代码
void vSensorsTask(void *pvParameters)
{
    UBaseType_t uxFree;                  // 声明变量

    for (;;)
    {
        // ... 所有采集函数 ...

        uxFree = uxTaskGetStackHighWaterMark(NULL);
        // uxFree 是还剩下多少 words 没被用过
        // 这个值从创建以来只降不升 → 记录的是最坏情况
    }
}

必须实测的最坏工况(否则测到的是假的):

任务 最坏工况 怎么触发
SENSORS 所有 ADS1115 通道轮流读 + CIR 检测 正常运行即可
SAFETY SSM + FDR 冻结帧 (full state machine) 注入故障信号
ALGORITHM SOC 全量卡尔曼滤波 + BDS 全量统计 SOC 初始值偏差大,滤波器收敛期
PROTOCOLS EMS 发来完整的一帧最长数据 CAN 工具模拟
HOUSEKEEPING DSM 写 EEPROM 最长一帧数据 发送存储命令

3.5 根据测量值调栈

复制代码
uxFree (words)    判断
────────────────────────────────────────────
 < 50             立即翻倍,紧急------离栈溢出只差一口气
  50 - 100        危险 ------ 翻倍或 ×1.5
 100 - 300        刚好 ------ 保持当前值
 300 - 500        舒适 ------ 可以适当缩减
 > 500            浪费 ------ 减半释放堆空间

3.6 FPU 项目额外注意

复制代码
无 FPU 的任务:
  栈帧 = 8 (硬件) + 9 (手动) = 17 words (68B)

有 FPU 且使用过的任务:
  栈帧 = 8 + 9 + 33 (FPU) = 50 words (200B)

编译器标志 -mfloat-abi=hard 意味着:
  即使你的代码里完全没有 float,编译器也可能用 FPU 寄存器
  来做 memcpy 优化 ------ 所以栈大小要按"可能会用 FPU"来估

四、一张图总结

复制代码
                        数据流 →
  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
  │ SENSORS  │   │ SAFETY   │   │ALGORITHM │   │PROTOCOLS │
  │          │   │          │   │          │   │          │
  │ 采集物理量│   │ 保护逻辑  │   │ SOC/SOH  │   │ CAN/UART │
  │ 写入cluster│   │ 控制接触器│   │ 纯计算    │   │ 可能阻塞  │
  │          │   │          │   │          │   │          │
  │ Prio 4   │   │ Prio 6   │   │ Prio 5   │   │ Prio 3   │
  │ 堆 4KB   │   │ 堆 8KB   │   │ 堆 32KB  │   │ 堆 8KB   │
  │ 100ms    │   │ 100ms    │   │ 100ms    │   │ 100ms    │
  └──────────┘   └──────────┘   └──────────┘   └──────────┘
                                                   │
  ┌──────────┐                                     │
  │HOUSEKEEP │   ← 最低优先级,后台杂务              │
  │ RTC+存储 │                                     │
  │ Prio 1   │                                     │
  │ 堆 4KB   │                                     │
  │ 500ms    │                                     │
  └──────────┘                                     │
       ↑                                           │
       └─── IDLE (Prio 0) ─── 喂 IWDG + WFI ──────┘

核心原则三条

  1. 按数据流 + 安全级别切任务,不按函数数量。功能类似、安全级别相同 → 合并。安全关键 → 隔离。
  2. 优先级 = 被延迟的后果严重度。接触器延迟 → 最高。存储延迟 → 最低。
  3. 栈大小 = 先估算 → 实测最坏工况 → 留 20% 余量 。用 uxTaskGetStackHighWaterMark 看历史极值,不是看当前值。
相关推荐
十月的皮皮12 小时前
stm20260720-从新手 C 到量产 STM32 工程:程序设计推导指南
c语言·开发语言·stm32·stm32cubemx·hal库
开发笔记-阿牛13 小时前
解析一款有意思的产品||CK6159A语音芯片设计的儿童专注力训练机-舒尔特玩具
stm32·单片机·嵌入式硬件·音频
搞全栈小苏15 小时前
SIMetrix_simplis8.4安装教程——附安装包
stm32·单片机·嵌入式硬件·电力电子
qq_5739678216 小时前
STM32控制器
stm32·单片机·嵌入式硬件
某林21217 小时前
履带小车底盘stm32F103RCT6开发
人工智能·stm32·单片机·嵌入式硬件·架构·人机交互·ros2
做萤石二次开发的哈哈18 小时前
萤石 ERTC Windows SDK 包含哪些类型定义?
stm32·单片机·嵌入式硬件
亿道电子Emdoor18 小时前
【Aitium】PCB导出DXF文件出现eDuplicateRecordName报错
stm32·单片机·嵌入式硬件·altium
巴巴媛6661 天前
STM32学习笔记【38.DHT11实验】
笔记·stm32·学习
深圳市泰凌微电子1 天前
HK航顺 HK32F407RGT7替STM32F405RGT6 微控制器32位MCU
stm32·单片机·嵌入式硬件
小心亦新2 天前
STM32学习16--定时器输出比较2直流电机
stm32·嵌入式硬件·学习