第二章:从 main.c 到 FreeRTOS — 嵌入式实时系统启动全流程
上一章选了芯片,这一章让它跑起来。看一个无人船主控怎么从加电到所有任务就绪。
1. 启动流程:200ms 内发生了什么?
加电 → 复位向量(0x08000000) → startup 汇编 → SystemInit (HSE 25MHz → PLL 480MHz, ~5ms) → __main (RW初始化 + ZI清零, ~1ms) → main() ├── bsp_init() ← 所有外设初始化 (~100ms) ├── path_planner_init() ← 代价地图 + 规划器 ├── radar_task_init() ← 创建雷达采集任务 ├── lidar_task_init() ← 创建 LiDAR 采集任务 ├── ... 15+ 个任务创建 ... └── vTaskStartScheduler() ← 永不返回200ms 后,15 个 FreeRTOS 任务同时运行。Linux 加电到飞控就绪要 5-10 秒——这 10 秒差在海上可能就是 20 米的漂移距离。
2. bsp_init:一个函数统治所有硬件
voidbsp_init(void){HAL_Init();// ① HAL库基础初始化SystemClock_Config();// ② HSE 25MHz → PLL → SYSCLK 480MHzMPU_Config();// ③ 内存保护: DTCM不缓存, DMA区域Device// ④ GPIO + 端口控制: 12路P口+13路T口+6路R路继电器MX_GPIO_Init();port_ctrl_init();// ⑤ 通信接口: 按依赖顺序初始化MX_DMA_Init();// DMAMUX 先配好MX_USART_Init();// 8路串口 (依赖DMA)MX_SPI1_Init();// SPI FlashMX_I2C4_Init();// INA219电流传感器MX_FDCAN_Init();// 2路CAN (推进器+飞控)// ⑥ 外设MX_TIM_Init();// PWM定时器MX_ADC1_Init();// 电池电压检测MX_ETH_Init();// 以太网 (lwIP)// ⑦ 中间件MX_FreeRTOS_Init();// heap_5 内存池配置w25q_init();// SPI Flash 参数存储}关键原则:按依赖顺序。DMA 必须在串口之前初始化(串口依赖 DMA),堆在任务创建之前初始化。顺序乱一个,后面全挂。
3. 为什么全用 xTaskCreateStatic?
FreeRTOS 提供两种创建任务的方式:
// 动态创建: 从堆分配栈+TCB, 简单但有碎片xTaskCreate(entry,"name",stack_size,NULL,prio,&handle);// 静态创建: 调用者提供栈+TCB内存, 无碎片TaskHandle_t handle=xTaskCreateStatic(entry,"name",stack_size,NULL,prio,task_stack,// ← 你自己分配好的栈内存&task_tcb// ← 你自己分配好的任务控制块);整个项目 20 个任务,全部用静态创建。原因:
1. 确定性
动态分配失败 → 返回 NULL → 任务没创建 → 船出去了才发现 IMU 没在跑。静态分配的栈大小编译期确定,启动时要么全成功要么全失败(立即 Error_Handler)。
2. 零碎片pvPortMalloc和vPortFree用多了会产生内存碎片——256 字节空闲但分在 4 个不连续的 64 字节块里,申请 200 字节就失败。船可能连续跑几十个小时,堆碎片累积是不可接受的。
3. 可审计
每个任务的栈用SECTION_DTCM标记,编译后.map文件一眼看到谁用了多少:
lidar_task_stack 0x20003d60 3072 B DTCM ← 精确可查 radar_task_stack 0x20003560 2048 B DTCM4. 优先级怎么分配?
优先级 7 (最高): IWDG Monitor — 看门狗, 其他任务都靠它保命 优先级 6: Motion Control — 50Hz 控制回路, 不能有任何延迟 优先级 5: Thruster/PWM — 推进器输出, 慢了船就失控 优先级 4: IMU/RTK/Radar/LiDAR/RC — 传感器采集, 实时但可容一两帧延迟 优先级 3: Power/Host/IoT — 电源管理+通信, 低频任务 优先级 2: Ship State — 遥测打包, 最不紧急核心思想:谁离推进器越近,谁优先级越高。控制回路延迟 20ms → 船偏航 → 可能要撞。遥测延迟 1 秒 → 地面站数据卡一下 → 没关系。就算所有任务都忙着,高优先级总能抢占。
5. 看门狗怎么监视所有任务?
用 STM32 的 IWDG(独立看门狗)——它是一个硬件定时器,8.2 秒后自动复位。正常情况每个任务定期"喂狗"(清计数器),超过 8.2 秒没喂就系统复位。
但一个 IWDG 只能监控一个心跳。怎么监控 20 个任务?
// iwdg_task_monitor.ctypedefstruct{TaskHandle_t handle;uint32_tlast_heartbeat_ms;// 上次心跳时间uint32_ttimeout_ms;// 超时阈值}MonitoredTask;// 每个任务在自己的循环里:IWDG_MonitorTask_Heartbeat(xTaskGetCurrentTaskHandle());// 看门狗监控任务 (优先级最高, 1Hz):voidIWDG_MonitorTask_Run(void){for(inti=0;i<task_count;i++){if(now-tasks[i].last_heartbeat_ms>tasks[i].timeout_ms){// 有任务挂了 → 记录日志 → 重启LOG_CRITICAL("Task %s hung, rebooting...",tasks[i].name);HAL_IWDG_Refresh(&hiwdg);// 喂最后一次// 停止喂狗 → IWDG溢出 → 硬件复位}}HAL_IWDG_Refresh(&hiwdg);// 全都正常 → 正常喂狗}关键设计:不是每个任务自己喂狗(那样一个任务挂了其他任务还能喂、永远不复位),而是专门的监控任务统一喂狗——任何一个任务心跳超时,监控任务停止喂狗 → 系统复位。
6. 任务怎么互不踩踏?
我们用全局数据结构体g_xxx_data在不同任务间共享数据。对于写入者单一的简单场景,Cortex-M7 的单字节写入是原子的:
// 雷达采集任务 (写入者)g_radar_data.min_distance=2.5f;// 原子写入 (对齐的float)g_radar_data.valid_count=5;// 原子写入 (uint8_t)// 运动控制任务 (读取者)floatdist=g_radar_data.min_distance;// 原子读取对于更复杂的共享(代价地图),我们用了"只增不减"策略——唯一一个任务写某个格子,只往大的改(255 盖 0),两个任务同时写也不丢障碍,最多多标一个假障碍(极端情况下宁保守不激进)。
7. 踩过的坑
坑 1:任务栈设太小导致莫名其妙的 HardFault
RTK 任务一开始设了 256 字的栈。NMEA 解析时临时分配了一个 200 字节的局部数组——栈溢出,踩到了相邻任务的控制块。现象是"系统偶尔重启,查了三天找不到原因"。Stack canary(栈溢出检测)救了命。后来所有任务栈至少 512 字。
坑 2:IWDG 超时设太短
一开始设了 3.2 秒。D* Lite 偶尔触发全量重规划(3000 节点搜索),需要 ~5ms——加上其他任务,偶尔超过 3.2 秒。看门狗误复位,日志里"一切正常,系统重启"。改 8.2 秒后不再误报。
8. 下一章预告
第三章:串口怎么收数据不丢帧?— DMA + 环形缓冲 + IDLE 中断
8 个串口同时收发,LiDAR 460800bps 高速数据,不丢一帧。DMAMUX 灵活路由、无锁环形缓冲、IDLE 中断帧检测——一文讲透。
本文是《从零搭建无人船控制系统》系列第二篇。项目地址:[煜坤 YuKun](开源准备中)。