《从零搭建无人船控制系统》系列第二篇
第二章从 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. 为什么全用 xTaskCreateStaticFreeRTOS 提供两种创建任务的方式// 动态创建: 从堆分配栈TCB, 简单但有碎片xTaskCreate(entry,name,stack_size,NULL,prio,handle);// 静态创建: 调用者提供栈TCB内存, 无碎片TaskHandle_t handlexTaskCreateStatic(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(inti0;itask_count;i){if(now-tasks[i].last_heartbeat_mstasks[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_distance2.5f;// 原子写入 (对齐的float)g_radar_data.valid_count5;// 原子写入 (uint8_t)// 运动控制任务 (读取者)floatdistg_radar_data.min_distance;// 原子读取对于更复杂的共享代价地图我们用了只增不减策略——唯一一个任务写某个格子只往大的改255 盖 0两个任务同时写也不丢障碍最多多标一个假障碍极端情况下宁保守不激进。7. 踩过的坑坑 1任务栈设太小导致莫名其妙的 HardFaultRTK 任务一开始设了 256 字的栈。NMEA 解析时临时分配了一个 200 字节的局部数组——栈溢出踩到了相邻任务的控制块。现象是系统偶尔重启查了三天找不到原因。Stack canary栈溢出检测救了命。后来所有任务栈至少 512 字。坑 2IWDG 超时设太短一开始设了 3.2 秒。D* Lite 偶尔触发全量重规划3000 节点搜索需要 ~5ms——加上其他任务偶尔超过 3.2 秒。看门狗误复位日志里一切正常系统重启。改 8.2 秒后不再误报。8. 下一章预告第三章串口怎么收数据不丢帧— DMA 环形缓冲 IDLE 中断8 个串口同时收发LiDAR 460800bps 高速数据不丢一帧。DMAMUX 灵活路由、无锁环形缓冲、IDLE 中断帧检测——一文讲透。本文是《从零搭建无人船控制系统》系列第二篇。项目地址[煜坤 YuKun]开源准备中。