下一步怎么做:从能动到可控的完整路线图
🔄 上一章回顾
上一章我们把镜头从一条命令拉远到整个仓库,沿着 Git 历史和源码盘点了项目已经完成的部分、留好接口但还没填血肉的部分、以及等待实机验证的部分。现在我们知道了项目走到哪一步,该规划接下来怎么走了。
🎯 本章你会学到
- 如何在 Keil 中建立一次可复现的编译构建
- 通信验证的步骤和常见问题排查
- 四个电机通道的逐一测试方法
- 安全停止机制(运动许可)的设计思路
- PID 控制的基本原理——让电机真正按目标速度运转
- 航向、操作机构的接入顺序
从已经走通的路继续向前
故事开始的时候,上位机只发来一条简简单单的 START_MOTION。现在我们已经知道,这条命令会进入 USART1,经过协议解析和命令分发,再由 motion_start_motion() 把 PWM 3000 写到四个电机通道。那一刻四个轮子应该开始转动——如果接线正确的话。
这条链路看起来很短,但它证明了新架构的主干已经形成:上位机说得清楚,下位机听得懂,协议层没有丢包,运动层能把数字变成电机输出。就像盖房子时先搭好承重墙,墙稳了,后面才能放心地砌砖、装窗、布线。
接下来的工作适合像搭桥一样进行。先确认脚下这一段能够稳定承重,再向前接下一段。每接通一个功能,就留下编译、通信和实机证据。这样遇到问题时,可以很快判断故障发生在协议层、任务调度、运动计算、接线还是机械结构上,而不至于一上来就"整车不动"然后无处下手。
下面这张图是整个后续开发的大致顺序。你可以把它当成一份路线图,不一定要严格线性执行,但建议大体按这个方向推进:
先让构建和通信稳定,再让车轮听话,然后加上安全和反馈,最后接入操作机构。
先认识工程的几个入口
第一次打开大型嵌入式仓库,很容易在几百个文件之间迷路。文件夹一层套一层,文件名看起来都差不多,函数名又特别长。
其实对于初学者来说,最有效的阅读方式不是从第一行开始顺序读,而是沿着"一条命令到底怎么被处理"的实际路径去读。想象一下:上位机发了一个 START_MOTION,它像一封信,从串口进来,经过好几道门,最后变成电机输出。我们只需要跟着这封信走,就能把仓库里最核心的几条线索串起来。
| 入口 | 阅读时关注的内容 | 为什么要看这里 |
|---|---|---|
USER/WHEELTEC.uvprojx |
芯片型号、编译器版本、目标名、源码分组和输出设置 | 确认工程能在你的电脑上打开和编译 |
USER/main.c |
systemInit()、FreeRTOS 启动和 app_task 的创建 |
了解程序上电后第一件事做什么 |
USER/system.c |
USART1、PWM、ICM20948、使能开关和四路编码器初始化 | 知道硬件外设在哪里被配置 |
LOWER_CONTROLLER/APP/app_main.c |
字节怎样进入协议解析器 | 跟踪"收到串口数据后发生了什么" |
LOWER_CONTROLLER/APP/command_dispatcher.c |
17 条命令目前各自走向哪里 | 看到每条命令对应哪个处理函数 |
LOWER_CONTROLLER/BOARD/ |
USART1、115200、PWM 3000 和 A/D/B/C 轮位映射 | 确认硬件参数和轮位配置 |
LOWER_CONTROLLER/MOTION/motion_controller.c |
平移、旋转、停止和电机方向引脚 | 追踪运动命令如何变成电机输出 |
拿到仓库后,建议先双击 USER/WHEELTEC.uvprojx 打开 Keil,看看工程的"目录树"长什么样。注意芯片是不是 STM32F407VE,目标名是不是 Mec_Car,源码有没有按 HARDWARE、USER、LOWER_CONTROLLER 这样分组。这一步不需要编译,只是熟悉战场。
然后打开 main.c,找到 systemInit() 和 FreeRTOS 的任务创建代码。这里回答的问题是:程序上电后第一件事做什么?哪些任务在跑?app_task 是我们自己业务逻辑的入口,相当于整个下位机程序的"前台接待"。
再往里面走,system.c 负责把各种硬件初始化好:串口、PWM、IMU、编码器、使能开关。可以把它理解为"开机自检和硬件准备工作"。
真正处理命令的地方在 LOWER_CONTROLLER/APP/。app_main.c 是接待处,command_dispatcher.c 是分发窗口,motion_controller.c 是执行车间。
遇到"为什么车轮这样转"的问题,可以从 command_dispatcher.c 跟到 motion_controller.c,再看 board_motion_config.h 和 HARDWARE/motor.h。遇到"为什么收不到命令"的问题,可以从 board_uart_config.h 跟到 uart_irq.c、transport_uart.c 和协议解析器。
跟着数据走
嵌入式代码最怕"不知道从哪里看起"。只要记住跟着数据走这四个字,沿着数据流寻找答案,阅读范围会很快缩小,大部分问题都能定位到具体文件。
建立一次可以复现的构建
在继续加功能之前,第一件该做的事是:确保当前代码能稳定编译,并且知道编译出来的是什么。
💡 零基础小课堂:几个关键词
- IDE(集成开发环境,Integrated Development Environment)——专门用来写代码的软件,就像 Word 是专门写文档的。Keil µVision 就是嵌入式开发最常用的一款 IDE。
- 编译(compile)——把人写的 C 代码翻译成芯片能看懂的机器语言。就像把中文翻译成英文,翻译完才能"给外国人看"。
- 烧录(flash/program)——把编译好的程序写进芯片,就像拷文件到 U 盘。芯片断电后程序不会丢失。
USER/WHEELTEC.uvprojx 是现有的 Keil µVision 工程。目标名为 Mec_Car,芯片配置为 STM32F407VE。工程记录的编译器是 ARMCC 5.06 update 7,设备包是 Keil.STM32F4xx_DFP.3.1.1。
确保工具版本一致
同一份工程在不同版本的编译器上可能报错。很多初学者遇到"别人电脑能编译,我电脑报错"的情况,往往就是编译器或设备包版本不一致。安装 Keil 后,请核对你的 ARMCC 版本和设备包版本,保持和工程文件记录的一致。
构建输出和自动化小工具
工程把输出目录设为 OBJ/,输出名为 Mec_Car,并开启 HEX 生成。构建完成后,有几个自动运行的小工具会帮你处理产物:
fromelf:Keil 自带的转换工具,从OBJ/Mec_Car.axf生成仓库根目录下的Mec_Car.bin。copyhex.bat:把生成的 HEX 文件复制到根目录,方便烧录工具找到。hexbinHandle.bat:构建前执行,清理旧的 HEX 和 BIN,避免把上一次产物误当成当前结果。
这些小工具不需要你手动运行,Keil 在编译流程中会自动调用它们。
AXF、HEX、BIN 三种文件的区别
编译会生成几种不同格式的文件,它们的用途各不相同:
- AXF:带调试信息的可执行文件,体积最大,仿真和调试时用到。可以理解为"开发者版本"。
- HEX:文本格式的烧录文件,FlyMcu 等工具常用这种格式。里面包含了地址和数据,烧录工具能自动识别。
- BIN:纯二进制机器码,没有地址信息,需要配合起始地址使用。体积最小,适合特定烧录场景。
仓库还保存了 Windows 程序 FlyMcu2188.exe,用于把固件烧进单片机。源码工程使用指南.pdf 讲到 target 选择、全量编译和 HEX 产物;FlyMcu 的端口、波特率、芯片选项和 BOOT 状态将在现场烧录时确认并写入记录。
第一次构建的目标
第一次构建的目标很朴素,但非常有价值:让 HEAD fc3ef5d 在指定工具链中完整编译,并保存构建记录。建议用一个简单的文本文件记录这些信息:
日期:2026-07-14 # 构建日期
提交:fc3ef5d # Git 提交号,方便回溯
编译器:ARMCC 5.06 update 7 # 编译器版本
错误:0 # 编译错误数量(必须为 0)
警告:3(记录具体内容) # 警告可以暂时忽略,但要记录
产物:Mec_Car.hex, Mec_Car.bin # 确认生成了这两个文件
产物被 .gitignore 忽略很正常,构建记录仍可以单独保存。这样,后续源码变化造成编译问题时,会有一个明确的可用基线——你可以随时回退到这个提交,确认"这个版本至少是能编译的"。
在车轮接触地面前验证通信
电机转起来之前,强烈建议先把通信链路跑通。原因很简单:如果后面车轮不转,你得先确定是电机的问题,还是命令根本没送到单片机。
通信测试可以让机器人保持静止,只看不摸。USART1 的当前配置是 115200 波特率,协议帧头为 0xAA 0x55,payload version 是 20260612。这几个数字先记住:
- 115200:波特率,表示每秒传输多少位,上下位机必须一致。
0xAA 0x55:帧头,相当于一封信的信封颜色,告诉单片机"后面跟的是正式数据"。20260612:协议版本号,上下位机要匹配,否则可能出现"你发的是新格式,我按老格式解析"的问题。
三条必测命令
TESTS/ 目前只有 README。后续加入的第一个上位机脚本可以依次发送下面这三条命令:
PING:确认收发通道是通的,单片机还活着。GET_PAYLOAD_VERSION:确认上下位机协议版本一致。TEST_COMMAND:确认一个 32 位整数能否完整往返,检查大小端、字节序和 CRC。
核对 ACK 的原命令序号、命令 ID 和状态,再核对版本或测试 telemetry。
故意制造错误
通信能跑通还不够,还得确认它能正确拒绝错误数据。可以故意加入错误帧:
- 错误 CRC:看解析器会不会丢掉坏帧。
- 错误参数长度:看分发器会不会回复
INVALID_ARGS。 - 未知命令:看分发器会不会回复
INVALID_COMMAND。
这一步像先打电话确认双方语言一致。只要 PING、版本和测试值都能稳定往返,接下来的电机问题就可以集中到运动层与硬件层,而不是在串口和协议上反复绕圈。
💡 零基础小课堂:冒烟测试
冒烟测试(smoke test) 这个名字来自早期电子工程师给新电路板通电时,先看它会不会冒烟。如果通电后没冒烟,说明至少没有短路或严重错误,可以继续做下一步测试。在软件里,冒烟测试指的是"运行最基本的几个功能,确认系统没有严重故障"。我们上面的 PING + 版本 + 测试值三步,就是一次通信层的冒烟测试。
⚠️ 第一次调试最常见的问题
如果通信测试跑不通,先检查这四个最常见的原因:
- 波特率不一致——上位机和下位机的波特率设置必须完全相同(当前是 115200)
- TX/RX 接反——上位机的 TX 要接下位机的 RX,反过来也一样,这是最容易犯的接线错误
- GND 没有共地——上位机和下位机的 GND 必须连在一起,否则电平参考不同,通信会乱码
- USB 转串口驱动没装——电脑需要安装对应芯片(如 CH340、CP2102)的驱动程序
让四个通道依次说话
通信没问题之后,再来碰电机。但正式测试整车方向前,车轮适合先悬空。原因很简单:如果某个通道方向反了、PWM 写错通道了,或者某个轮子接线松了,悬空时最多是空转,不会把机器人一下子撞出去。
motion_test_motor_channels_loop() 已经提供一个通道测试:A、B、C、D 分别以 PWM 1200 运行一秒,然后全部停止两秒。当前主任务没有调用它,调试时需要用临时测试入口接入,验证结束后恢复 app_task。
⚠️ 临时测试代码一定要删掉!
很多人调试时为了省事,把测试入口长期留在 app_task 里,结果正式联调时机器人一上电就自转,找半天才发现是调试代码没删。养成"加进去 → 验证完 → 立刻删掉或注释掉"的习惯,能避免很多奇怪的问题。
观察记录应同时写下"哪个接口获得输出"和"车身哪个轮位转动"。当前配置期望:
- 左前轮接 A
- 左后轮接 D
- 右前轮接 B
- 右后轮接 C
轮位正确后再核对转向。左后和右前使用 -1 方向系数。这个方向系数是什么意思呢?简单说,就是告诉电机"你的正转对车身来说是反转"。因为四个电机的安装方向不同,即使 PWM 都是正的,车身也未必往同一个方向走。通过给某些通道乘以 -1,可以让"前进命令"真正产生前进效果。
接线变化写入 board_motion_config.h,运动层继续使用左前、左后、右前、右后的逻辑顺序。这样即使以后换电机、换线序,也只需要改配置文件,不需要动运动控制的核心算法。
四个通道逐一确认后,可以发送 START_MOTION 的前进、后退、左移、右移,再发送两种 START_ROTATION。每次动作后都发送 STOP_MOTION,确认 PWM 和方向引脚清零。当前三种 gear 都会得到固定 PWM 3000,因此这轮测试关注方向组合和停止效果,而不是速度大小。
做好记录
建议把每一步的结果都拍照或录视频,同时在记录表里写下:命令、期望动作、实际动作、是否通过。这些记录在后面调闭环和整定参数时会非常有用。
先把停止能力接牢
在让机器人跑得快之前,必须先让它停得稳。这是机器人开发里最容易被新手忽视、但又最重要的一条原则。
车轮落地前,安全链需要从"收到停止命令就停"扩展到"通信异常和任务异常也能停"。使能开关 PE4 已经初始化,下一步是让 APP 读取它并维护统一的运动许可。
💡 什么是"运动许可"?
你可以把它理解成一个总闸门——只有当所有条件都满足时,闸门才打开,电机才会响应运动命令。这些条件包括:
- 使能开关已打开(硬件层面)
- 通信正常(最近收到过有效命令)
- 没有检测到故障
一旦任何一个条件不满足,闸门立刻关闭,所有输出被强制清零。所有底盘和执行机构入口都读取这份许可,许可撤销时进入同一个安全停止函数。
命令超时检查
命令超时需要周期性检查最后一条有效运动命令的时间。当前 app_task 使用:
// 等待串口收到下一个字节
// portMAX_DELAY 表示"永远等待,直到有数据进来"
transport_uart_receive_byte(&byte, portMAX_DELAY);
portMAX_DELAY 会一直等待下一个字节。也就是说,如果没有数据进来,这个任务会一直挂在这里,没法去做别的事情——自然也没法检查"是不是太久没收到命令了"。
要加入超时检查,有两种思路:
- 让
receive_byte带超时:比如改成等待 100ms,超时后去检查时间,再回来继续等。改动小,但会频繁唤醒任务。 - 新建安全任务/软件定时器:独立的安全任务专门负责检查超时、读取使能开关。职责更清晰,安全逻辑独立,但要多维护一个任务。
无论采用哪种方式,通信安静时也要有代码醒来检查时间。超时长度由上位机发送周期和实车制动测试决定,并写入板级配置和通信约定。建议初始值设得宽松一点,比如 500ms 到 1s,后续根据实际通信周期收紧。
看门狗和复位
看门狗随后负责发现任务卡死——还记得第 5 章讲过的看门狗吗?正常任务定期"喂狗",关键任务停止运行时由硬件复位。启动后的默认电机状态、复位原因记录和复位后的输出清零也应一起验证。
完成这一步后,串口拔掉、上位机退出、使能关闭和任务停顿都能得到可观察的安全结果。
用编码器把档位变成真实速度
到目前为止,gear 参数虽然存在,但其实还没真正派上用场。当前 PWM 3000 让车轮获得固定驱动,不管是 gear = 1 还是 gear = 3,输出都一样。这显然不是最终想要的行为。
从开环到闭环
编码器接入后,可以为微速、慢速和快速档位定义目标轮速,再周期性读取 A、B、C、D 的增量。控制器比较目标与测量值,逐步调整 PWM——这就是速度闭环。
闭环控制听起来有点抽象,但其实很好理解。想象你在开车,目标是保持 60km/h。你一边看仪表盘(编码器反馈),一边踩油门或刹车(调整 PWM),让速度尽量接近目标。
那么,谁来当这个"自动司机"呢?答案就是 PID 控制器。
💡 零基础小课堂:PID 控制
PID 是三个英文单词的首字母:Proportional(比例)、Integral(积分)、Derivative(微分)。不用被数学名词吓到——它本质上就是"定速巡航"的三种调整策略:
- P = 比例:差距越大,调整越用力。当前速度和目标差了很多?那就大力踩油门。差得不多?轻踩一下就好。
- I = 积分:如果差距持续存在,慢慢加大调整力度。比如你一直差 2km/h 追不上,I 会说"光靠 P 不够,我再多加一点油"。
- D = 微分:如果差距正在缩小,提前减少调整,避免冲过头。比如速度正快速接近目标,D 会说"别再猛踩了,马上就到了,该松油门了"。
三者合力,就能让电机既快速响应、又不剧烈震荡地跟上目标速度。慢慢来,PID 一开始调不好很正常,这需要反复实验。
关键参数
要让 PID 工作,先得搞清楚几个关键参数:
- 编码器每圈脉冲数:决定一个脉冲代表多少角度。
- 减速比:电机轴转多少圈,轮子才转一圈。
- 轮径:轮子周长决定走一米需要转多少圈。
- 读取周期:多久读一次编码器,直接影响速度计算和控制频率。
- 正负方向:编码器反馈的正负要和电机方向对应上。
数据单位统一后,SENSORS 对外提供轮速,MOTION 管理目标和控制器。原厂 BALANCE/ 中保留了 PI 控制思路,可以作为参考;新的初始化已经停用原厂 PI 参数,比赛底盘需要结合当前机械结构重新测量和整定。
整定 PID 参数
整定(tuning) 是什么意思?你可以把它想象成调收音机频道——慢慢转旋钮,找到声音最清晰的位置。PID 的三个参数(P、I、D)就像三个旋钮,需要你一边调一边观察效果,找到最合适的组合。
整定时不要心急,建议按这个步骤来:
- 先用一个轮子悬空做单轴测试
- 观察阶跃响应(step response)——突然给电机一个目标速度(比如从 0 跳到 200 RPM),看它多快能跟上、有没有剧烈震荡、稳态误差大不大
- 单轴稳定了再上整车
- 整车稳定了再加负载
从闭环到精确运动
有了稳定轮速,按时间移动可以使用 FreeRTOS 时钟结束动作,按距离移动可以累计编码器里程。持续动作与精确动作适合由运动状态机来管理。
还记得第 3 章讲过的状态机吗?运动状态机的思路是一样的——可以想成一张写着"空闲、运行、减速、完成、故障"的流程牌。命令分发器只负责接单和启动状态,运动任务持续执行,新的 STOP_MOTION 仍能随时被接收。
状态机的好处是:同样一条 START_MOTION,在不同状态下可能有不同处理。比如正在运行时收到新命令,可以选择覆盖、排队或拒绝;发生故障时自动进入安全状态。这样代码结构清晰,调试时也更容易定位"卡在哪一步"。
再补航向和操作机构
底盘能跑、能停、能按速度跑之后,再往上加精度和操作机构。
航向控制
按角度旋转和 GET_HEADING 需要 ICM20948 提供稳定朝向。还记得第 5 章介绍过的 IMU(惯性测量单元)吗?这里主要用它的陀螺仪和磁力计来算航向角。
开发时先记录静止零漂——就是机器人不动的时候,IMU 输出的角度还在慢慢变化(第 5 章也提到过这个问题)。这个漂移如果太大,会导致积分出来的航向角越来越不准。可以通过校准、滤波或者定期用编码器修正来缓解。
验证步骤:先确认静止零漂在可接受范围内,再验证缓慢旋转一周时角度是否连续,随后把 heading 接入 SENSORS 和 telemetry。
编码器负责看轮子走了多少,IMU 负责看车身朝向怎样,两种反馈合在一起可以提高直线和旋转控制的可靠性。比如走直线时,IMU 可以检测车身有没有偏航,然后自动修正左右轮的速度差。
操作机构
升降、夹爪和推杆适合在底盘安全链稳定后接入。为什么呢?因为操作机构往往涉及机械限位、电机堵转、夹持力控制等问题,如果底盘本身还动不动乱跑,调试执行机构会非常危险和混乱。
建议按这个顺序来:
- 每个机构先完成单独的手动低速动作。
- 再加入限位读取、动作超时和停止。
- 最后连接六条已有协议命令。
目标高度、方向和开合状态已经有生成的参数结构,新的 ACTUATORS 层负责把这些参数变成受保护的机械动作。所谓"受保护",就是要有超时、限位、故障检测,不能一个命令下去就无脑执行到底。
整车联调记录
整车联调时,每条记录都带上 Git 提交号、固件版本、板卡与接线、发送命令、收到的 ACK/telemetry 和实际动作。构建日志证明代码进入固件,串口抓包证明命令完成往返,轮位记录和视频证明机械动作符合预期。证据连成一条线,项目的完成状态也会自然变得清楚。
小结与下一步
那条最初的 START_MOTION 已经从一个协议名称走到四路电机输出接口。它走通了一条最小的端到端链路,也把我们带到了继续开发的起点。
继续沿着构建、验证、反馈和安全这条路线前进,它会逐渐从"写下电机输出"成长为"让机器人知道自己在做什么,并且能够可靠地停下来"。
对于刚接手这个仓库的你来说,不需要一次性吃透所有文件。建议下一阶段的阅读顺序是:
- 先打开 Keil 工程,确认能编译当前提交。
- 写一个最小的上位机脚本,把 PING、版本和测试命令跑通。
- 悬空测试四个电机通道,确认轮位和方向。
- 加入超时和使能检查,让停止变得可靠。
- 接入编码器,把 PWM 开环变成速度闭环。
- 最后接入 IMU 航向和操作机构。
每一步都留下记录,每步之间都有可验证的证据。这样即使你是第一次面对这样的大型电控仓库,也能稳扎稳打地把机器人做完整。
本章新词
| 术语 | 英文 | 一句话解释 |
|---|---|---|
| IDE | Integrated Development Environment | 专门用来写代码的软件,Keil 就是一款嵌入式 IDE |
| 编译 | compile | 把人写的 C 代码翻译成芯片能执行的机器语言 |
| 烧录 | flash / program | 把编译好的程序写进芯片,就像拷文件到 U 盘 |
| AXF | ARM Executable Format | 带调试信息的可执行文件,仿真时使用 |
| HEX | Intel HEX | 文本格式的烧录文件,包含地址和数据 |
| BIN | Binary | 纯二进制机器码,体积最小 |
| 冒烟测试 | smoke test | 运行最基本的功能,确认系统没有严重故障 |
| 运动许可 | motion permit | 一个总闸门,所有条件都满足时才允许电机运转 |
| PID 控制 | Proportional-Integral-Derivative | 通过比例、积分、微分三种策略让电机跟上目标速度 |
| 阶跃响应 | step response | 突然给电机一个目标速度,观察它多快能跟上 |
| 整定 | tuning | 调整 PID 参数使控制效果最优,像调收音机找最清晰频道 |
| 零漂 | zero drift | 传感器静止时输出值仍在缓慢变化的现象 |
| 状态机 | state machine | 按预设流程在不同状态间切换的程序结构 |