wengqidaifeng

个人主页:

wengqidaifeng

永远在路上,永远向前走

个人专栏:

数据结构
C语言
嵌入式小白启动!
重要OJ算法题详解
蓝桥杯备战
C++从菜鸟到强手
python启航

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

项目背景:以 2025 年电赛 E 题“小车 + 自行瞄准装置”为例,使用 MSPM0G3507、Keil、SysConfig 完成底盘循迹,并结合 STM32F103C8T6 完成云台瞄准与双主控联调。

这篇是整个系列的第一篇,主要讲“开工前要准备什么”。我不会一上来就塞满 PID、串口协议和状态机细节,而是先把赛题拆开:它到底要求我们做什么,为什么这个项目最后会被拆成“小车底盘”和“云台瞄准”两部分,以及用 Keil + SysConfig 开发 MSPM0G3507 时要提前踩平哪些坑。

为了让小白也能顺着看下去,这篇会先把项目里最常见的名词讲明白。你可以把它当成这套工程的“入门词典”:

  • MSPM0G3507:底盘主控芯片,负责让小车跑起来。
  • STM32F103C8T6:云台主控芯片,负责让激光对准目标。
  • Keil:写代码、编译、下载程序的开发环境。
  • SysConfig:TI 的外设配置工具,相当于帮你生成底层初始化代码。
  • 灰度传感器:用来识别黑线的“眼睛”。
  • TB6612:电机驱动芯片,负责把 MCU 的控制信号变成电机能用的电流输出。
  • 编码器:测电机转了多少、转得多快的反馈器件。
  • PID:让系统自动修正误差的一种控制方法。
  • UART:串口通信,两块主控之间靠它交换状态。
  • I2C:一种常用的外设总线,这里用来驱动 OLED。
  • CAN:更适合工业控制的总线,云台用它控制步进驱动器。

如果你之前没接触过这些词,没关系,后面都会在对应篇幅里继续解释。

后面三篇会分别展开:

  1. 上篇:MSPM0G3507 小车篇
    重点讲 8 路灰度循迹、TB6612 四驱、电机编码器、速度环、循迹环、直角转弯、缺口穿越、计圈和 OLED 调试。

  2. 中篇:STM32 云台篇
    重点讲 STM32F103C8T6 控制二维云台、视觉模块串口解析、CAN 步进电机、激光控制、按键和云台状态机。

  3. 下篇:综合联调篇
    重点讲 MSPM0 与 STM32 的 UART 通信、底盘圈进度同步、云台画圆/画矩形、安全策略和最终调试经验。
    该篇所涉及代码均在GitHub仓库
    在这里插入图片描述


一、先看赛题:它不是单纯的循迹小车

2025 年电赛 E 题的核心可以概括成一句话:

小车沿着规定黑线跑,瞄准装置要在静止或运动过程中用激光对目标靶完成瞄准、画圆或画矩形等动作。

如果只看“小车”两个字,很容易把它当成传统循迹车来做:灰度传感器采线、PID 修正方向、跑完指定圈数停车。但这道题的难点不只在底盘,而在于它把两个系统强行绑到了一起:

  • 底盘系统:负责沿赛道稳定行驶,处理直角转弯、长缺口、圈数统计、速度控制。
  • 瞄准系统:负责识别目标靶,控制激光落点,完成靶心瞄准、画圆、画矩形。
  • 综合系统:小车运动时,瞄准装置还要知道底盘状态,例如当前是否在跑、跑到一圈的哪个进度、是否通信正常。

所以这道题不能只按“写一个主循环”去想,它更像一个小型嵌入式系统工程:传感器、执行器、通信、控制算法、安全逻辑都要同时考虑。


二、赛道与任务拆解

根据项目整理文档,赛道是一个约 100cm x 100cm 的正方形平台,四条边构成小车行驶路线。小车需要逆时针沿 A -> B -> C -> D -> A 行驶。

这里先把“赛道”两个字说清楚。比赛里说的赛道,不是普通操场上的一条黑线,而是一个带有明确边界、转角、缺口和标记的任务场地。你要做的不是沿着线“随便走走”,而是要让车知道:

  • 当前是不是还在黑线上;
  • 什么时候走到拐角;
  • 什么时候遇到没有黑线的缺口;
  • 一圈究竟有没有走完;
  • 走完以后是否停车。

这就是为什么底盘程序里一定要有状态机,而不是只写一个“看到黑线就偏转”的死循环。

其中有两类边:

  • ABCD:全程有黑线,适合正常循迹。
  • BCDA:两端各有一段黑线,中间存在约 60cm 的无黑线缺口。

这意味着底盘不能只会“看到黑线就跟”,还必须能处理“短时间看不到线但仍然要继续直行”的情况。也就是说,程序里要区分:

  • 正常循迹;
  • 进入直角转弯;
  • 丢线穿越缺口;
  • 重新找线;
  • 完成一圈或多圈后停车;
  • 发生异常时停车。

项目中底盘状态机集中在:

car/empty/app/app_chassis.c

入口很简单:

int main(void)
{
    SYSCFG_DL_init();
    app_chassis_init();

    while (1) {
        app_chassis_loop();
    }
}

真正的工作不是写在 while(1) 里硬等,而是通过 1ms 定时中断调度:

void TIMER_1MS_INST_IRQHandler(void)
{
    switch (DL_TimerG_getPendingInterrupt(TIMER_1MS_INST)) {
        case DL_TIMER_IIDX_ZERO:
            app_chassis_on_1ms_tick();
            break;
        default:
            break;
    }
}

这就是后面小车篇要重点讲的结构:定时中断负责周期任务,主循环负责 OLED 刷新和通信发送这类后台任务


三、为什么我把项目拆成双主控

这个项目最终采用:

MSPM0G3507 底盘主控 + STM32F103C8T6 云台主控

原因很直接:底盘和云台的实时任务不一样,硬件生态也不一样。

MSPM0G3507 负责底盘

MSPM0G3507 这块板适合做小车底盘控制,主要任务包括:

  • 8 路灰度传感器采样;
  • TB6612 电机驱动;
  • 编码器测速;
  • 速度 PID;
  • 循迹 PID;
  • 直角转弯和缺口处理;
  • K1/K2 按键;
  • OLED 显示;
  • 向 STM32 上报底盘状态。

当前工程路径:

car/empty/

关键文件:

car/empty/empty.c
car/empty/app/app_chassis.c
car/empty/app/chassis_state.h
car/empty/bsp/bsp_track8.c
car/empty/bsp/bsp_motor_tb6612.c
car/empty/bsp/bsp_encoder.c
car/empty/protocol/protocol_stm32.c

STM32F103C8T6 负责云台

STM32F103C8T6 负责瞄准系统,主要任务包括:

  • 接收 MaxicamPro 视觉模块输出的 dx/dy 偏差;
  • 控制 X/Y 两轴步进电机;
  • 控制激光开关;
  • OLED 显示云台状态;
  • KEY8 切换模式;
  • 与 MSPM0 交换状态;
  • 在底盘失联时关闭激光。

当前工程路径:

gimbal_stm32f103c8t6/

关键文件:

gimbal_stm32f103c8t6/Core/Inc/gimbal_config.h
gimbal_stm32f103c8t6/Core/Src/main.c
gimbal_stm32f103c8t6/App/Src/app_gimbal.c
gimbal_stm32f103c8t6/App/Src/bsp_uart_vision.c
gimbal_stm32f103c8t6/App/Src/bsp_uart_mspm0.c
gimbal_stm32f103c8t6/App/Src/bsp_step_motor.c

两块板如何协作

两块板通过串口通信:

MSPM0 PB2 / UART3_TX  ->  STM32 PA3 / USART2_RX
MSPM0 PB3 / UART3_RX  <-  STM32 PA2 / USART2_TX
GND                   ->  GND

串口参数:

115200 8N1
3.3V TTL

通信帧格式:

A5 5A VER MSG_ID SEQ LEN PAYLOAD CRC8

方向和含义:

方向 MSG_ID 周期 作用
MSPM0 -> STM32 0x10 20ms / 50Hz 上报底盘模式、圈数、赛段、圈进度、速度、循迹误差
STM32 -> MSPM0 0x20 50ms / 20Hz 上报云台状态、视觉状态、激光状态、错误信息

这个设计的好处是:底盘和云台可以先独立调通,最后再联调。比赛项目最怕“一上来全接一起,然后不知道哪里坏了”,双主控拆分后排查会清楚很多。


四、开发环境准备

这套项目的重点是 MSPM0G3507 + Keil + SysConfig。STM32 端也用 Keil,但 MSPM0 的环境更容易卡在 SDK、SysConfig、Keil 工程和生成文件之间,所以这里先把准备项列清楚。

1. 必备软件

建议准备:

  • Keil MDK-ARM;
  • TI MSPM0 SDK;
  • TI SysConfig;
  • J-Link 驱动;
  • 串口助手或 VOFA+;
  • STM32CubeMX,云台端如需重新改外设配置时使用;
  • Git 或至少能管理工程版本的备份习惯。

先别急着装一堆工具,先理解它们各自的分工:

  • Keil:写代码、编译、下载程序。
  • SDK:芯片厂家提供的底层驱动库。
  • SysConfig:图形化配置外设并生成初始化代码。
  • J-Link:把程序烧进板子并支持调试。
  • 串口助手 / VOFA+:用来观察运行中的数据。
  • CubeMX:如果你还要动 STM32 的外设,它就是云台侧的配置工具。

本项目中 MSPM0 的 Keil 工程是:

car/empty/keil/empty_LP_MSPM0G3507_nortos_keil.uvprojx

SysConfig 配置文件是:

car/empty/empty.syscfg

Keil 构建前会通过脚本重新生成 MSPM0 的外设初始化文件:

car/empty/keil/generate_syscfg.bat

生成结果主要是:

car/empty/ti_msp_dl_config.c
car/empty/ti_msp_dl_config.h

这里有一个很重要的概念:不要手改 ti_msp_dl_config.c/.h 里的外设初始化逻辑。这些文件是 SysConfig 生成的,真正要改引脚、PWM、UART、I2C、定时器,应该改 .syscfg,再让 Keil 或脚本重新生成。

2. MSPM0 下载与调试

MSPM0G3507 使用 SWD 下载调试。项目中 SWD 引脚不要被占用:

SWDIO = PA19
SWCLK = PA20

调车时最常用的不是断点,而是:

  • OLED 看状态;
  • 串口看数据;
  • 低速跑车看现象;
  • 每改一两个参数就烧录验证。

这个项目的底盘 OLED 显示了很多关键信息,例如模式、目标圈数、赛段、速度、循迹误差、传感器 bitmask、PWM 和通信状态。后面小车篇会专门讲怎么看这些信息。

3. STM32 云台环境

STM32 端 Keil 工程:

gimbal_stm32f103c8t6/MDK-ARM/gimbal_stm32f103c8t6.uvprojx

统一配置文件:

gimbal_stm32f103c8t6/Core/Inc/gimbal_config.h

云台端比较关键的配置包括:

#define VISION_UART_BAUD             115200
#define MSPM0_UART_BAUD              115200
#define GIMBAL_CAN_BAUD              500000UL
#define GIMBAL_HEARTBEAT_PERIOD      50u
#define CHASSIS_LOST_MS              300u

这里面 CHASSIS_LOST_MS 很关键:如果 STM32 超过一定时间收不到 MSPM0 的底盘状态,就会关闭激光。这不是为了炫技,而是比赛中非常实用的安全逻辑。


五、需要提前掌握的知识

如果你准备复现或改这个项目,建议先补下面这些知识。不需要一开始全部精通,但至少要知道每个模块在系统里起什么作用。

1. C 语言与嵌入式基本功

至少要熟悉:

  • struct 结构体;
  • enum 状态枚举;
  • volatile 的意义;
  • 中断函数;
  • 位运算;
  • 定时任务调度;
  • 头文件和源文件分层。

比如底盘状态不是散落在一堆全局变量里,而是集中在 chassis_state_t 这类结构体中。这样 OLED、通信、控制算法都能围绕统一状态工作。

2. GPIO、PWM、UART、I2C、定时器

本项目涉及的外设很多:

外设 在项目中的作用
GPIO 按键、LED、蜂鸣器、电机方向、激光开关
PWM TB6612 电机调速
UART MSPM0 和 STM32 通信、视觉模块通信
I2C OLED 显示
Timer 1ms 系统节拍、周期调度
CAN STM32 控制 CAN 步进电机

学习顺序建议:

GPIO -> PWM -> Timer -> UART -> I2C -> CAN

如果还没接触过 MSPM0,不建议一上来就写完整小车。可以先做:

  1. 点灯;
  2. 按键控制 LED;
  3. PWM 控制一个电机;
  4. OLED 显示一行字;
  5. 读取 8 路灰度;
  6. 串口发送一帧状态。

这些小实验跑通后,再进入完整工程会舒服很多。

3. PID 与状态机

这道题里 PID 不是只有一个。

底盘至少有:

  • 左轮速度 PID;
  • 右轮速度 PID;
  • 循迹 PID。

云台也有:

  • X 轴瞄准 PID;
  • Y 轴瞄准 PID。

但比 PID 更重要的是状态机。比如底盘不可能永远处于“正常循迹”,它至少有:

IDLE      待机
RUN       正常行驶
CORNER    转弯
DONE      完成
FAULT     故障

云台也有:

IDLE
AIM
SCAN
CIRCLE
RECT
RESET

如果没有状态机,代码很容易变成几十个 if 互相打架,调试时会非常痛苦。

4. 调试意识

比赛项目能不能成,很多时候不取决于你会不会写出“看起来很高级”的算法,而取决于你能不能快速定位问题。

建议每个模块都留下可观察信息:

  • OLED 显示当前状态;
  • 串口能输出关键变量;
  • 按键能手动启停;
  • 电机可以低速测试;
  • 激光可以单独开关;
  • 通信有在线/离线标志。

本项目中 MSPM0 OLED 上的 G+ / G-,STM32 OLED 上的 C:1 / C:0 就是为了联调时快速判断两块板有没有正常通信。


六、硬件准备清单

以下是本项目实际涉及的硬件类型,具体型号可以按自己手头器件替换,但接口和电气特性要对上。

底盘部分

  • MSPM0G3507 开发板;
  • 小车底盘;
  • 4 个直流减速电机;
  • 4 路或 2 路编码器;
  • 2 块 TB6612 电机驱动;
  • 8 路灰度传感器;
  • OLED 显示屏;
  • K1/K2 按键;
  • 电池、电源模块、开关、杜邦线。

这些器件分别做什么:

  • 底盘主控:管车跑不跑、怎么跑。
  • 电机:真正带动车轮转动的执行器。
  • TB6612:电机和主控之间的“放大器”,因为 MCU 不能直接带电机。
  • 灰度传感器:识别黑线和白底的传感器。
  • 编码器:告诉主控电机到底转了多少。
  • OLED:把内部状态显示出来,方便调试。
  • 按键:让你不用改代码就能启动、停止、切换目标圈数。

云台部分

  • STM32F103C8T6;
  • 二维云台结构;
  • 两个步进电机;
  • CAN 步进电机驱动器;
  • CAN 收发器;
  • MaxicamPro 或类似视觉模块;
  • 激光模块;
  • OLED 显示屏;
  • KEY8 按键。

这些器件分别做什么:

  • STM32:云台的大脑。
  • 步进电机:让云台按角度细致移动。
  • CAN 步进驱动器:接收速度命令并驱动步进电机。
  • 视觉模块:告诉云台靶心偏哪儿了。
  • 激光模块:最终指向目标的光点。
  • OLED:显示模式、误差和联动状态。
  • KEY8:让云台在调试时更容易操作。

联调注意事项

最容易出问题的不是代码,而是接线和供电:

  • 两块主控必须共地;
  • 电机电源和逻辑电源不要混接;
  • TB6612 的 VM 接电机电压,VCC 接逻辑电压;
  • 激光不要直接由 GPIO 供电,建议用 MOS 或三极管控制;
  • STM32 的 CAN_TX/CAN_RX 不能直接接 CANH/CANL,中间必须有 CAN 收发器;
  • 串口 TX/RX 要交叉连接;
  • 调试器不要当作电机供电电源。

七、从代码看系统总体结构

为了让后面三篇更好接上,这里先浅浅看一下项目目录。

MSPM0 底盘目录

car/empty/
├── empty.c                    # main 入口
├── empty.syscfg               # SysConfig 外设配置
├── ti_msp_dl_config.c/.h      # SysConfig 生成文件
├── app/
│   ├── app_chassis.c          # 底盘主状态机和周期调度
│   └── chassis_state.h        # 底盘状态和配置结构体
├── bsp/
│   ├── bsp_track8.c           # 8 路灰度
│   ├── bsp_motor_tb6612.c     # TB6612 电机驱动
│   ├── bsp_encoder.c          # 编码器测速
│   ├── bsp_oled.c             # OLED 显示
│   └── bsp_key.c              # 按键扫描
├── control/
│   └── pid.c                  # 通用 PID
└── protocol/
    ├── crc8.c
    ├── protocol_frame.c
    └── protocol_stm32.c       # MSPM0 与 STM32 通信

可以看到,工程不是把所有东西堆在 main.c,而是分成:

  • app:应用逻辑;
  • bsp:板级硬件驱动;
  • control:控制算法;
  • protocol:通信协议。

这种结构非常适合写博客讲解,也适合比赛后复盘。

STM32 云台目录

gimbal_stm32f103c8t6/
├── Core/
│   ├── Inc/
│   │   ├── main.h
│   │   └── gimbal_config.h
│   └── Src/
│       └── main.c
├── App/
│   ├── Inc/
│   └── Src/
│       ├── app_gimbal.c
│       ├── bsp_uart_vision.c
│       ├── bsp_uart_mspm0.c
│       ├── bsp_step_motor.c
│       └── pid.c
└── MDK-ARM/
    └── gimbal_stm32f103c8t6.uvprojx

云台的配置更集中,很多题目相关参数都在:

gimbal_stm32f103c8t6/Core/Inc/gimbal_config.h

比如:

#define GIMBAL_MODE_AIM              1u
#define GIMBAL_MODE_DRAW_CIRCLE      3u
#define GIMBAL_MODE_DRAW_RECT        4u

#define GIMBAL_CIRCLE_RADIUS_ERR     20.0f
#define GIMBAL_RECT_HALF_X_ERR       42.0f
#define GIMBAL_RECT_HALF_Y_ERR       30.0f

这些值不是“写死就一定对”,而是要根据摄像头安装角度、靶纸距离、激光偏移进行现场标定。


八、我的整体开发路线

这个项目比较推荐按下面顺序做,不要一开始就全系统联调。

第一步:只调 MSPM0 底盘

目标:

  • OLED 正常显示;
  • K1/K2 正常;
  • 灰度传感器 bitmask 正常;
  • 电机方向正确;
  • 编码器方向正确;
  • 小车能低速循迹;
  • 能处理直角转弯;
  • 能通过缺口;
  • 能按目标圈数停车。

这一阶段不需要云台参与。

第二步:只调 STM32 云台

目标:

  • OLED 正常显示;
  • KEY8 能切换模式;
  • 视觉模块能输出目标偏差;
  • 云台能根据 dx/dy 调整;
  • 激光能受控开关;
  • 画圆和画矩形轨迹能独立跑。

这一阶段不需要小车参与。

第三步:只调双主控通信

目标:

  • MSPM0 周期发送底盘状态;
  • STM32 能解析 0x10 底盘帧;
  • STM32 周期发送心跳;
  • MSPM0 能解析 0x20 云台帧;
  • OLED 能显示在线状态。

这一阶段可以先不让电机转,只验证通信和状态。

第四步:低速综合测试

目标:

  • 小车低速跑一圈;
  • 云台保持目标瞄准;
  • 底盘失联时云台关闭激光;
  • 小车停止时云台状态可控;
  • 圈进度能用于同步画圆或画矩形。

综合测试时一定要低速开始,尤其是激光和电机都接上的情况下。


九、Keil + SysConfig 开发 MSPM0 的几个坑

1. 生成文件和用户代码要分清

MSPM0 工程里有一类文件是 SysConfig 生成的:

ti_msp_dl_config.c
ti_msp_dl_config.h

这类文件不要当成普通业务代码改。否则下一次生成时,你的改动可能会被覆盖。

建议把自己的代码写在:

app/
bsp/
control/
protocol/

SysConfig 只负责外设底层初始化。

2. 改了 SysConfig 后要确认 Keil 工程也包含相关文件

添加新模块后,Keil 工程里也要包含对应 .c 文件。比如协议相关文件:

protocol/crc8.c
protocol/protocol_frame.c
protocol/protocol_stm32.c

如果只把文件放到目录里,却没有加入 Keil 工程,编译时就会出现链接错误。

3. 引脚复用要提前规划

MSPM0G3507 的引脚资源并不是无限的。PWM、UART、I2C、编码器、OLED、按键、灰度传感器都要占引脚。

本项目中底盘一侧比较关键的引脚有:

功能 引脚
TB6612 PWM PB04、PB01、PB06、PB07
8 路灰度 PB25、PB24、PB20、PA14、PB18、PB19、PB10、PA07
OLED I2C PA31、PA28
K1/K2 PB26、PA25
STM32 UART PB02、PB03
SWD PA19、PA20

尤其要注意 SWD 引脚不要被误占用,否则下载调试会非常麻烦。

4. 先让系统“可观察”,再追求效果

调车不是玄学,前提是你能看到内部状态。

这个项目里我比较依赖 OLED 显示:

RUN N2 L0 S0 U+ G+
VL  30 VR  29
E  -120 M3F P0000
F0000 PWM 850

含义大致是:

  • RUN:当前模式;
  • N2:目标圈数;
  • VL/VR:左右轮速度;
  • E:循迹误差;
  • M:灰度传感器 mask;
  • P:圈进度;
  • G+:云台在线;
  • F0000:故障码。

后面小车篇会详细讲这些数据怎么用来调 PID、判断灰度极性、定位转弯失败。


十、第一篇总结

这篇主要是把项目的“地图”画出来:

  • 这不是单纯的循迹车,而是底盘、云台、视觉、通信一起工作的系统;
  • MSPM0G3507 更适合负责底盘实时控制;
  • STM32F103C8T6 更适合负责云台、视觉、CAN 步进和激光;
  • Keil + SysConfig 开发 MSPM0 时,要分清生成文件和用户代码;
  • 开工前要准备好 GPIO、PWM、UART、I2C、Timer、PID、状态机等基础知识;
  • 调试时要先保证系统可观察,再谈控制效果。

从下一篇开始,我们正式进入 MSPM0G3507 小车篇。我会从 empty.c 的入口开始,逐步讲到 app_chassis.c 里的 1ms 调度、8 路灰度循迹、速度 PID、循迹 PID、TB6612 四驱、直角转弯和缺口穿越。也就是说,下一篇会真正开始“让车跑起来”。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐