简介:成都信息工程大学风信子战队RoboMaster 2021赛季步兵电控代码,面向电子设计竞赛、嵌入式开发及机器人控制学习者,尤其适合有一定STM32基础、希望从完整电控工程入手研究步兵机器人逻辑的进阶开发者。压缩包共180个文件,包含98个头文件与65个C源文件,覆盖HAL库定时器、串口、SPI、CAN、Flash等底层驱动,另有系统架构图、CubeMX配置工程及编译调试配置文件,可据此梳理整机嵌入式软件框架。整体体积仅1.23MB,代码结构紧凑、便于移植与二次开发。目前已有41人学习,资源通过严格测试可直接运行,可用作毕业设计、课程项目或竞赛实训的参照原型。对于关注电控分层、模块解耦或云台与底盘通信的读者,这份源码提供了良好的工程范本,可在其基础上修改扩展,快速验证自己的控制策略。
1. 风信子战队 2021 赛季步兵电控代码解析:先讲清楚步兵身上有几套系统
先泼一盆冷水:步兵电控代码的难点不在某一行算法写得有多巧妙,而在于底盘移动、云台稳定、弹丸发射三套系统同时在主控上争抢同一批外设资源。成都信息工程大学风信子战队 2021 赛季放出的这份步兵电控代码,正好是能对照着搭工程骨架的样本。你从 zip 包解压后先看到的不是技巧,而是 2021 赛季规则下步兵必须解决的三个问题:全向底盘怎么走位,云台俯仰偏航怎么稳住,12mm 弹丸怎么在热量限制内连续打出去。本文按“解包结构、CAN 通信、控制逻辑、落地排查”四层推进,适合已经在 CubeMX 里点过灯、想上整车验证的电控新人,也适合把别人项目嫁接回自己底盘的熟手。
2. 解开 .zip 包:工程目录、Keil 环境与 1kHz 控制任务的挂载
2.1 解压后的工程结构:BSP 与 APP 的分工边界
先解决最实际的问题:这份 zip 怎么解开、解完看什么。用 7-Zip 解压到一个纯英文路径,这一步比大多数人想的重要。Keil MDK 对中文字符路径的支持一直不稳定,工程文件放在“桌面/新建文件夹(2)”这类路径下,编译时经常出现头文件找不到、objects 目录无法创建的问题。如果解压时报 “error read zip archive”,先别急着换工具——这是文件下载不完整或传输过程中丢包导致的,重新下载一次通常就能解决。解压后先看一级目录,RoboMaster 步兵工程几乎共用一套分层方式:
USER/ ├── main.c ├── usbd_desc.c ├── usbd_cdc_if.c BSP/ ├── bsp_can.c ├── bsp_uart.c ├── bsp_imu.c └── bsp_pwm.c APP/ ├── chassis_task.c ├── gimbal_task.c ├── shoot_task.c ├── robot_control_task.c └── protocol.c Driver/ └── CMSIS/ MDK-ARM/ └── CFU_HF32.uvprojx这个结构能不能跑起来,关键看依赖方向。BSP 层只做外设封装:bsp_can.c 提供 CAN 初始化、发送和接收回调,不直接参与控制逻辑;APP 层是真正的控制任务,调用 BSP 接口而不是直接操作寄存器。这么做的好处是换主控板只改 BSP,PID 和状态机代码可以原样带走。如果你解压的代码里还有 FreeRTOS 或 osal 目录,说明工程跑在 RTOS 上,控制任务是一个独立 task;没有的话就是裸机程序,靠定时器中断驱动。区分方式很简单:打开 main.c,找有没有 osKernelStart 或 vTaskStartScheduler 调用。
2.2 MDK 构建与下载:最小烧录闭环
打开 MDK-ARM 目录下的 .uvprojx,推荐用 Keil MDK 5.25 以上的版本。打开后先做两件事:第一,在 Options for Target 里确认 Device 选的芯片型号和实际板卡一致,芯片不对时会烧进去直接跑飞;第二,在 C/C++ 选项的 Define 里确认包含 STM32F407xx 这类与芯片对应的宏。报 “cannot open source input file stm32f4xx_hal.h” 时,把 BSP、APP、Driver 目录加到 Include Paths 就能解决。
下载器配置按 ST-Link 和 DAP-Link 有细微差异,但步骤一致:
Options for Target -> Debug -> 选择 ST-Link Debugger -> Settings -> Flash Download -> 确认 Programming Algorithm 列表非空如果报 “Flash Download failed - Cortex-M4”,先查 SWDIO、SWCLK、GND 三根线是否和板子共地,再把 Settings 里的 Max Clock 从 4MHz 降到 1MHz。这一步能修好八成下载失败。所有设置做完,按 F8 烧录,能看到 Download 进度条走完就说明工程本身没问题了。
2.3 主循环与 1kHz 控制中断:频率怎么分配
打开 USER/main.c,步兵主控的启动流程几乎都一样:先初始化系统时钟、GPIO、CAN、串口、定时器,再初始化控制任务,最后进 while(1) 死循环。核心控制不放在主循环里,而是放在定时器中断里以 1kHz 频率执行。原因是控制回路必须严格定时,而主循环里每次迭代耗时不稳定,会造成 PID 周期抖动。典型代码结构如下:
int main(void) { HAL_Init(); SystemClock_Config(); // 168MHz 主频 MX_GPIO_Init(); MX_CAN1_Init(); MX_CAN2_Init(); MX_USART1_Init(); // 调试串口 MX_UART4_Init(); // 裁判系统串口 MX_TIM7_Init(); // 1kHz 控制定时器 chassis_init(); gimbal_init(); shoot_init(); // 三个控制任务初始化 HAL_TIM_Base_Start_IT(&htim7); // 启动 1kHz 中断 while (1) { referee_task(); // 低速:裁判系统报文 debug_task(); // 低速:调试数据发送 HAL_Delay(2); } }这段代码体现了步兵电控的核心设计:控制任务必须严格定时,所以挂在定时器中断里;裁判系统解析和调试发送允许抖动,放在主循环轮询。HAL_TIM_PeriodElapsedCallback 里会依次调用 chassis_task、gimbal_task、shoot_task 三个高频函数,它们内部各自有状态机,由遥控器拨杆和裁判系统数据驱动。理解这一层之后,整份工程的阅读顺序就清楚了:先看 main.c 里开了哪些外设,再进 APP 看三套任务怎么分工。
提示:如果发现云台有规律地抖动,而 PID 参数怎么调都不解决问题,先检查 1kHz 中断是否被其他中断阻塞到毫秒级,比如 UART 中断里做了大量解析。
3. 步兵 CAN 总线通信:M3508、GM6020 的帧格式与接收中断
3.1 C620 电调的 0x200 指令帧与 0x201–0x204 反馈帧
步兵底盘四个轮子基本是 M3508 电机配 C620 电调,电调和主控之间走 CAN。C620 的通信协议已经很固定:主控向 0x200 标准帧写 8 字节,每两个字节对应一个电机的目标转速,16 位有符号数,单位 rpm。四个电机目标在同一帧里,所以每 1ms 只发一次 CAN 就能控制整个底盘。反馈在 0x201 到 0x204 四个独立 ID 上,每帧 8 字节:
| 字节 | 内容 | 类型 | 说明 |
|---|---|---|---|
| 0-1 | 转子位置编码值 | uint16_t | 0~8191 原始读数 |
| 2-3 | 转速 | int16_t | 单位 rpm |
| 4-5 | 实际转矩电流 | int16_t | 与力矩关联 |
| 6 | 电机温度 | uint8_t | 摄氏度 |
| 7 | 保留 | - | 常为 0 |
发送指令的代码不复杂,但要把轮速换算成两个字节并注意字节序:
void chassis_can_send(int16_t motor_speed[4]) { CAN_TxHeaderTypeDef tx_header; uint8_t tx_data[8]; tx_header.StdId = 0x200; tx_header.IDE = CAN_ID_STD; tx_header.RTR = CAN_RTR_DATA; tx_header.DLC = 8; for (int i = 0; i < 4; i++) { tx_data[i * 2] = (uint8_t)(motor_speed[i] >> 8); tx_data[i * 2 + 1] = (uint8_t)(motor_speed[i] & 0xFF); } HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, NULL); }参数说明:motor_speed 数组里四个元素依次对应 0x200 帧里的电机 1 到 4,这个顺序由 C620 上的拨码开关 ID 决定,不是按底盘前后左右排的。字节序是大端序,高字节在前低字节在后,符合 CAN 数据帧的通用约定。HAL_CAN_AddTxMessage 的最后一个参数是发送邮箱号,传入 NULL 表示让驱动自动选择空邮箱。
3.2 GM6020 云台电机与多电机控制帧 0x1FF/0x2FF
云台偏航和俯仰一般用 GM6020,它自带驱动,主控发电压指令。0x1FF 帧控制 ID 1 到 4 的四个 GM6020,0x2FF 帧控制 ID 5 到 7。一帧里同样是 8 字节、每电机两字节,但单位是 0.1 度电压值,范围 -1000 到 1000。反馈帧在 0x205 到 0x207,每个 ID 一帧,包含角度、速度和电流。读取反馈的关键在于 ID 映射:
typedef struct { int16_t angle_raw; // 角度编码值 int16_t speed_rpm; // 转速 int16_t current; // 电流 } gm6020_feedback_t; void gimbal_can_send(int16_t yaw_volt, int16_t pitch_volt) { CAN_TxHeaderTypeDef tx_header; uint8_t tx_data[8]; tx_header.StdId = 0x1FF; tx_header.DLC = 8; tx_header.IDE = CAN_ID_STD; tx_header.RTR = CAN_RTR_DATA; tx_data[0] = (uint8_t)(yaw_volt >> 8); tx_data[1] = (uint8_t)(yaw_volt & 0xFF); tx_data[2] = (uint8_t)(pitch_volt >> 8); tx_data[3] = (uint8_t)(pitch_volt & 0xFF); for (int i = 4; i < 8; i++) tx_data[i] = 0; // 其余电机不使能 HAL_CAN_AddTxMessage(&hcan2, &tx_header, tx_data, NULL); }这里三个坑值得注意。第一,0x1FF 控制的电机 ID 和反馈帧 0x205-0x207 的对应关系不是简单线性映射,代码里要手动建立 ID 到数组下标的查找表。第二,0x200 发的是转速目标,0x1FF 发的是电压值,两套协议含义不同,初学容易用同一组打包函数发两种帧,把云台电压当作电调速度发给底盘。第三,GM6020 的机械零位需要在装机后标定,代码里通常有 yaw_offset 或 angle_offset 变量,调车前先确认它的值和实际安装位置一致。
3.3 中断回调里的数据缓冲与互斥处理
CAN 数据异步到达,反馈帧靠 CAN 接收中断通知应用层。接收回调里只做两件事:取帧、更新共享缓冲区。PID 计算放在第 2 章说的 1kHz 控制中断里,CAN 中断只负责把数据暂存。裸机环境下用 volatile 全局数组即可:
volatile int16_t motor_rpm_feedback[4]; void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_data[8]; if (hcan->Instance == CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data); uint32_t id = rx_header.StdId; if (id >= 0x201 && id <= 0x204) { motor_rpm_feedback[id - 0x201] = (int16_t)((rx_data[2] << 8) | rx_data[3]); } } }参数说明:hcan->Instance 判断是 CAN1 还是 CAN2,底盘和云台可能挂在不同总线上。id - 0x201 把反馈帧号映射到数组下标,成立前提是 ID 连续。速度值由 rx_data[2] 和 rx_data[3] 组合,用 int16_t 接收保证负数符号位正确。volatile 告诉编译器该变量会被中断修改,防止优化时被缓存在寄存器里。如果工程跑 FreeRTOS,推荐中断里只做取帧和解析,然后再调用队列发送接口把数据交给控制任务处理,不要在中断里做运算或等待信号量。
注意:绝对不要在 CAN 接收中断里调用 HAL_CAN_AddTxMessage 或执行阻塞延时。高优先级中断内部的阻塞会让 1kHz 控制定时器中断无法按时触发,云台表现为周期性抽搐,而且排查起来极难发现。
4. 控制逻辑核心:串级 PID、麦轮解算与能量机关状态机
4.1 串级 PID 的 C 语言骨架:角度环与速度环怎么串
步兵云台控制的主要框架是串级 PID:外环角度,内环角速度。代码骨架如下:
typedef struct { float kp, ki, kd; float integral; float last_error; float i_limit; // 积分限幅 float out_limit; // 输出限幅 float out; } pid_t; float pid_run(pid_t *pid, float ref, float fdb) { float err = ref - fdb; pid->integral += err; if (pid->integral > pid->i_limit) pid->integral = pid->i_limit; if (pid->integral < -pid->i_limit) pid->integral = -pid->i_limit; float d_err = err - pid->last_error; pid->last_error = err; pid->out = pid->kp * err + pid->ki * pid->integral + pid->kd * d_err; if (pid->out > pid->out_limit) pid->out = pid->out_limit; if (pid->out < -pid->out_limit) pid->out = -pid->out_limit; return pid->out; } float angle_pid_out = pid_run(&yaw_angle_pid, yaw_ref, yaw_imu_angle); float speed_pid_out = pid_run(&yaw_speed_pid, angle_pid_out, yaw_imu_omega);参数说明:角度环的 ref 是期望云台朝向,fdb 是 IMU 解算的当前航向角;角度环输出作为速度环的 ref,速度环输出才是最终电压指令。角度误差在跨 0°/360° 时必须做角度差归一化,否则 359° 和 1° 的误差会算出 358°,云台会反向转一整圈。归一化代码通常在进 PID 前调用,如下:
float angle_err_wrap(float err) { while (err > 180.0f) err -= 360.0f; while (err < -180.0f) err += 360.0f; return err; }调参顺序固定为内环到外环。速度环 P 太小云台手感软,用手推一下回正很慢;P 太大云台高频抖动。角度环 P 调大后响应变快,但如果内环跟不上,外环输出的速度参考就会震荡。经验参数起步:速度环 P 从 1.5 起,I 从 0.02 起,D 先给 0;角度环 P 从 6 起。把参数定义为变量而不是宏,通过调试串口热调,能省掉大量反复烧录的时间。
4.2 麦轮底盘正向运动学:从 vx、vy、wz 到四个轮速
步兵全向底盘的“全向”来自四个麦克纳姆轮对 vx、vy、wz 三个自由度的组合。RoboMaster 电控代码里这个函数基本是标配,不同工程的差别只在符号约定和单位换算:
#define WHEEL_TRACK 0.34f // 轮距,单位米 #define WHEEL_BASE 0.32f // 轴距,单位米 void mecanum_solve(float vx, float vy, float wz, float *wheel_rpm) { float a = (WHEEL_TRACK + WHEEL_BASE) / 2.0f; wheel_rpm[0] = vx - vy - wz * a; // 左前 wheel_rpm[1] = vx + vy + wz * a; // 右前 wheel_rpm[2] = vx - vy + wz * a; // 左后 wheel_rpm[3] = vx + vy - wz * a; // 右后 }参数说明:vy 的正方向定义为“向左”,这与很多坐标系里 y 轴默认向右相反,根源是麦轮的安装方向。wz 正方向是逆时针。a 是轮距和轴距之和的一半,按整车机械图纸填写。解算出来的值单位是 m/s,还需要除以轮子半径换算成 rad/s,再乘 60/2π 得到 rpm,最后打包进 0x200 帧。有些工程把单位换算写在运动学函数内部,函数名带 mps 或 rpm 后缀,读代码时先确认单位再改参数。
调车现场验证方法:只推前进杆,四轮同时正转;只推横移杆,左右侧轮转向相反;只拨旋转杆,同侧同向、异侧反向。如果推横移时四个轮子“卡死”,多半是 vy 符号反了或者某个轮的安装方向画反了。麦轮打滑是物理问题,草地和磨损后的轮子打滑率升高,解算无法彻底消除,要靠 PID 把偏差吸收掉,不要靠增大 wz 系数去“补偿”。
4.3 发射状态机与 2021 能量机关的联动
步兵的射击链路由摩擦轮、拨盘和裁判系统热量限制三部分组成。摩擦轮把弹丸加速到出膛速度,拨盘把弹丸挤进两个摩擦轮之间,裁判系统实时上报热量和射速上限。三者的配合用状态机管理:
typedef enum { FIRE_IDLE = 0, FIRE_SPINUP, // 摩擦轮加速 FIRE_READY, // 摩擦轮就绪,允许拨弹 FIRE_SHOOTING, // 连发中 FIRE_COOLDOWN // 热量超限,强制停止 } fire_state_t; fire_state_t fire_state = FIRE_IDLE; void shoot_task(uint16_t heat_limit) { switch (fire_state) { case FIRE_IDLE: if (gimbal_ready() && heat_available(heat_limit)) fire_state = FIRE_SPINUP; break; case FIRE_SPINUP: if (friction_wheel_speed_ok()) fire_state = FIRE_READY; break; case FIRE_READY: trigger_wheel_run(cmd_fire_rate); fire_state = FIRE_SHOOTING; break; case FIRE_SHOOTING: if (heat_exceeded(heat_limit)) fire_state = FIRE_COOLDOWN; break; case FIRE_COOLDOWN: trigger_wheel_stop(); if (heat_available(heat_limit)) fire_state = FIRE_READY; break; } }状态机的核心逻辑:能不能开火的判断放在状态转换处,而不是用延时函数硬等。热量超限切到 COOLDOWN,清零后回 READY;摩擦轮未达到目标转速绝不拨弹,否则初速不足会卡弹。2021 赛季的能量机关对步兵提出了精确点射要求:目标在旋转,云台要跟随目标上的某个点并在对准瞬间射击。视觉模块通过串口把目标角度偏差发给电控,电控收到后更新云台参考角,并对齐后触发单发:
void gimbal_set_aiming(vision_cmd_t *cmd) { if (cmd->valid) { yaw_ref_angle += cmd->yaw_offset; pitch_ref_angle += cmd->pitch_offset; if (cmd->fire_cmd && aim_error() < 0.5f) trigger_single_shot(); } }能量机关跟得准不准,真正考验的是云台和底盘的联动质量。云台自身的角度环带宽必须够高,同时底盘要跟着云台当前朝向走,而不是云台被底盘拖着转。联动方式通常是把云台 yaw 角叠加到底盘旋转变量的参考系里,让云台指向哪里,底盘就往哪个方向平移。
5. 调车台三板斧:串口抽数、CAN 排错与发射安全自检
5.1 用调试串口把云台波形拉出来
调 PID 时眼睛看不到内部状态,只能把实时数据发到串口用波形软件看。推荐用 VOFA+ 或匿名上位机,定义一个固定帧:
uint8_t dbg_buf[15]; float yaw_ref, yaw_fdb, yaw_out; dbg_buf[0] = 0xA5; // 帧头 dbg_buf[1] = 0x5A; dbg_buf[2] = 0x0C; // 12 字节数据 memcpy(&dbg_buf[3], &yaw_ref, 4); memcpy(&dbg_buf[7], &yaw_fdb, 4); memcpy(&dbg_buf[11], &yaw_out, 4); HAL_UART_Transmit(&huart1, dbg_buf, 15, 10);按 250Hz 左右发送即可。看波形时盯三个点:目标阶跃时反馈有没有超调,输出值是否经常贴在限幅边界,收敛后误差是否稳定在 1° 以内。反馈没震荡但机械件在抖,问题出在云台结构共振;波形本身在震荡才是 PID 参数的事。
5.2 CAN 总线故障定位的顺序
排查 CAN 问题固定按“回环、物理层、ID 冲突”三步走。回环测试是把 CAN 初始化改成 Loopback 模式,发一帧能否自己收到,能收到说明外设和中断配置没问题。物理层用示波器量 CANH-CANL 差分电压,正常是 0V 到 2V 摆动的方波;如果总线静默不动,先查 120Ω 终端电阻是否在总线两端都存在。第三步是逐个隔离设备,把电机单独上电、单独发指令,确认只有目标电机响应。场地里最常见的“左后轮间歇不转”就是多个电调 ID 冲突导致的丢帧,逐个排查就能定位。
5.3 发射链路安全自检五步
发射机构是整车风险最高的部分,自检顺序必须固定。第一步断开发射电机动力线,手动转动摩擦轮确认轴承顺滑;第二步上电但不下发控制指令,确认电机不自转;第三步只给摩擦轮转速指令,等两个摩擦轮转速差小于 10 rpm;第四步才允许打开拨盘,并且炮口必须指向安全方向;第五步以无弹状态验证拨盘转动,再上实弹。热量阈值按裁判系统真实值设置,不要为了测试方便把 heat_limit 放大,否则正式比赛会因为超热被强制断电。初速不足时先检查摩擦轮直径磨损和轮距,不要急着改拨盘逻辑。把“转速-初速”映射表存在 Flash 里,比赛日按当天温度切换参数档。这五步顺序定下来后,再急的改动也不要跳过第四步直接上实弹验证。
本文还有配套的精品资源,点击获取