简介成都信息工程大学风信子战队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 接口而不是直接操作寄存器。这么做的好处是换主控板只改 BSPPID 和状态机代码可以原样带走。如果你解压的代码里还有 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_t0~8191 原始读数2-3转速int16_t单位 rpm4-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 的四个 GM60200x2FF 帧控制 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 里比赛日按当天温度切换参数档。这五步顺序定下来后再急的改动也不要跳过第四步直接上实弹验证。本文还有配套的精品资源点击获取