
简介面向船模设计比赛的STM32F103C8T6工程源码包基于NRF24L01无线模块建立遥控通信配合PWM信号控制动力、ADC采集反馈可快速搭建船模无线遥控与动力控制方案适合电子设计竞赛学生及嵌入式入门开发者参考。压缩包共238个文件容量约6.38MB包含54个.h头文件与53个.c源文件覆盖定时器、RCC时钟、ADC、I2C等标准外设驱动同时提供uvprojx工程文件、axf/hex烧录文件及Keil备份便于直接打开工程、编译烧录或按需裁剪。另有辅助脚本可清理编译中间产物保持目录整洁。目前已有1081人学习下载源码开放交流允许指正并在此基础上创新。整体借鉴船模搭建思路或单独提取无线通信、PWM调速、ADC采样模块都能为相关设计提供切实参考。 做船模的朋友应该都清楚比赛里最怕的不是电机不够猛而是遥控链路先出问题。我上一届比赛用STM32F103C8T6最小系统板加NRF24L01这套组合从零开始写控制代码最后拿到了小组赛第二。虽然没站上领奖台但整个项目把这类船模最常见的坑踩了个遍通信不稳定、下载连不上、电机一启动单片机就重启全遇到了。这篇文章把我当时的源码思路、硬件接法、协议设计和调试记录完整过一遍给准备参加船模比赛、智能小车比赛或者想快速上手NRF24L01无线通信的朋友一份可以直接照抄的作业。这个项目本质上是一个遥控船模工作流程很清晰遥控端采集摇杆数据通过NRF24L01无线发送控制指令船载端接收指令后用STM32F103C8T6的定时器产生PWM信号驱动电机控制船的速度和转向。核心就三块STM32基础外设配置、NRF24L01驱动与通信协议、运动控制策略。1. 项目需求与整体方案选择1.1 比赛任务怎么拆解船模比赛看起来简单就是把船开到终点但真做起来任务很杂。根据赛道规则首先要保证操控距离至少要覆盖一个标准泳池或小型湖泊的比赛区域其次要保证响应速度遥控指令到下个动作的延迟不能超过人能够感知的范围否则转弯和避障会非常难受最后是可靠性比赛现场可能有几十艘船同时用2.4G频段总不能一开机就和别人家的船“串台”。所以我当时把需求拆成了三个硬指标通信距离50米以上、指令周期控制在20毫秒以内、连续运行半小时不出死机。这三个指标直接决定后面的选型MCU用什么、无线模块用什么、电机驱动用什么。1.2 为什么是STM32F103C8T6加NRF24L01STM32F103C8T6这颗芯片在比赛圈里算是“国民级”MCU。它便宜、好买、资料多管脚够用主频72MHz处理一个简单的无线控制逻辑绰绰有余。更重要的是很多同学手里有现成的最小系统板就算芯片货期紧张还有GD32那类兼容替代型号管脚和寄存器基本一致代码改改编译选项就能跑不至于因为缺货影响整个项目进度。NRF24L01选择理由也很直接2.4GHz频段、SPI接口、最大32字节单包负载、支持增强型ShockBurst自动应答和自动重发延迟能做到很低。对比了几个方案方案优点缺点适不适合船模NRF24L01延迟低、地址可配、抗干扰不错供电要求严、寄存器配置繁琐非常适合HC-05蓝牙简单、手机可调试延迟高、连续通讯不稳定不适合ESP8266 WiFi距离远、可组网启动慢、协议开销大不太适合普通红外遥控简单、便宜必须直线、距离短完全不行最终确定方案遥控端和船载端都使用STM32F103C8T6最小系统板无线模块用NRF24L01PA版本带功放实测距离更有余量电机驱动用TB6612整船采用双电机差速转向不用舵机。1.3 系统架构的粗设计船载端整体流程STM32上电后初始化系统时钟、SPI、定时器、串口和NRF24L01然后进入主循环不断检查NRF24L01的接收数据标志位。收到指令后解析数据帧根据油门和转向计算左右电机的目标PWM值更新定时器比较寄存器。同时还要做超时保护如果连续一段时间收不到遥控数据自动把电机输出归零防止船失控乱窜。遥控端相对简单ADC采集摇杆两个轴的数据一个轴代表油门另一个轴代表转向然后把数据封装成数据包通过NRF24L01发送出去。发送不能太急要给NRF24L01留下自动重发和应答的时间一般设置每10到20毫秒发一包。2. 硬件搭建与接线细节2.1 最小系统板与管脚规划STM32F103C8T6最小系统板在国内比赛圈里几乎人手一块板载一颗8MHz晶振和蓝色LEDLED一般接在PC13。注意这颗LED和普通GPIO不同它由PC13控制有些板子LED是低电平点亮调试程序时很容易搞混。我一开始就习惯性以为LED高电平亮结果程序怎么调都不对查了半天发现是极性反了。管脚规划要提前做好。我当时的分配是这样的功能管脚说明NRF24L01 SCKPA5SPI1时钟NRF24L01 MISOPA6SPI1主机输入NRF24L01 MOSIPA7SPI1主机输出NRF24L01 CSNPA4SPI片选软件控制NRF24L01 CEPA3芯片使能软件控制NRF24L01 IRQPA2数据中断脚也可不用电机1 PWMPA8TIM1_CH1电机2 PWMPA9TIM1_CH2板载LEDPC13调试指示灯不建议把NRF24L01的IRQ脚漏掉因为它可以直接用外部中断通知主控“数据来了”比主循环轮询可靠得多。不过当时为了简化代码我改成了轮询方式实测在20ms周期内完全够用。如果你想做更复杂的协议或者低功耗IRQ中断是更好的选择。2.2 NRF24L01供电和SPI接线NRF24L01这个模块有两个地方特别容易翻车一是供电二是电平匹配。先说供电。NRF24L01工作电压是1.9V到3.6V模块上虽然很多带有稳压芯片但直接用STM32板上的3.3V去带一旦进入接收或发送模式电流会突然拉升板载线性稳压器如果余量不足电压就会跌落模块直接复位或者通讯乱码。我当时做的处理是在模块电源脚和地之间并了一个10uF陶瓷电容和一个100uF电解电容并且尽量用短粗的线供电。如果项目空间允许把NRF24L01的电源单独用一个低噪声LDO供电效果更好。再说SPI接线。STM32F103C8T6的SPI1最高能跑到18MHz但NRF24L01不一定能在这么高的SPI时钟下稳定工作。尤其是用杜邦线连接时线之间的电容和串扰会导致数据读回出错。我当时把SPI时钟降到9MHz甚至有些无人机飞控代码会降到1MHz求稳。船模对SPI速率要求不高稳比快重要。2.3 电机驱动与电源地处理电机驱动我选了TB6612FNG比L298N轻太多而且内部是MOSFET开关压降小不需要额外散热片。L298N用来带动大功率直流电机没问题但在船模这种讲究重量和电池续航的场景里L298N的功耗和体积都不划算。电源部分用了2S航模锂电池7.4V。电池直接给TB6612的VM供电VM脚接一个100uF电解电容吸收电机峰值电流。STM32最小系统板通过DC-DC降压模块单独供电最好是先把7.4V降到5V再由板载的3.3V稳压给MCU供电避免电机启动瞬间把主控电压拉垮。另外一定要“共地”。TB6612的GND、MCU的GND、电池的负极必须可靠接到一起。如果不共地PWM信号在驱动板输入端就是浮空状态电机可能不受控地抖动。这个错误我见过不少新手犯查了半天以为是代码问题结果就是一根共地线没接。3. NRF24L01无线链路与数据帧设计3.1 底层SPI驱动要点开发环境我用的标准外设库原因很简单比赛项目代码量不大寄存器直接操作可控性高出问题也容易定位。用HAL库当然也行但手工创建项目时如果对CubeMX生成的初始化代码不熟反而会把SPI参数配错。标准外设库写起来比较直观。SPI初始化核心参数主机模式、时钟极性CPOL0、时钟相位CPHA0数据帧8位先发高位。NRF24L01的时序要求就是模式0这个别改。void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }分频系数选872MHz除以8就是9MHz这是一个稳定性较好的折中值。3.2 NRF24L01寄存器初始化NRF24L01的寄存器配置是整个项目的核心。不能用默认值必须按照实际需求配置。我当时配置的是发送模式接收端再单独做成PRX模式。uint8_t nrf24l01_temp; nrf24l01_temp 0x0A; // 自动重发延时500us重发次数10 NRF_Write_Reg(WRITE_REG SETUP_RETR, nrf24l01_temp); nrf24l01_temp 0x03; // 5字节地址宽度 NRF_Write_Reg(WRITE_REG SETUP_AW, nrf24l01_temp); nrf24l01_temp 40; // 射频通道 NRF_Write_Reg(WRITE_REG RF_CH, nrf24l01_temp); nrf24l01_temp 0x0E; // 2Mbps发射功率0dBm NRF_Write_Reg(WRITE_REG RF_SETUP, nrf24l01_temp); NRF_Write_Reg(WRITE_REG CONFIG, 0x0E); // 上电、CRC校验、收发模式待定几个关键点使能自动应答EN_AA必须和发送地址、接收管道0地址一致否则ACK发送不成功。RF_CH在同一场比赛场地要尽量避免和其他队一样。如果现场干扰严重可以改信道比如用40附近的值但最好赛前实测定下来。CONFIG寄存器最后要配合PRX还是PTX状态再写一次因为bit0决定收发模式。3.3 自定义协议帧结构NRF24L01单包最多32字节如果直接发裸数据解析时很容易错位。我自定义了一个固定8字节的控制帧typedef struct { uint8_t head[2]; // 帧头 0xAA 0x55 uint8_t channel; // 船模编号防止误收别人的遥控信号 uint8_t cmd; // 命令类型 uint8_t throttle; // 油门值 0-100 uint8_t steer; // 转向值 0-100 uint8_t reserve; // 保留字段可做扩展 uint8_t crc8; // CRC8校验 } CtrlFrame;帧头是为了识别数据流的起始位置。比赛现场如果信道有干扰收到错包的概率不小加一个0xAA 0x55能快速过滤掉无效数据。channel字段用来区分多艘船普通比赛如果只有一队所有设备设同一个编号即可。数据包里不直接发PWM值而是发百分数因为每个电机的PWM范围可能不同到了船载端再做映射这样改电机电压或驱动板时遥控端不用重新编译。CRC8校验算法不复杂网上有现成查表实现。不能省因为NRF24L01虽然硬件有CRC但那是管链路层的从发送缓冲区到接收缓冲区如果出现整包错位应用层还是需要再做一次校验。3.4 多船配对与信道避让正式比赛时最怕多队用同一个信道和地址。NRF24L01的地址长度为5字节只要把发送地址和接收管道0地址设成队伍自己特定的5字节序列就能避开大部分串扰。我当时写了一个简单的配对逻辑遥控端开机的第一秒内按住特定按键程序发送一个配对广播包船载端上电后前两秒等待配对。配对包包含一个随机生成的5字节地址之后双方把这个地址写入寄存器。这样不用每次比赛前手动烧录不同固件。不过要注意NRF24L01的硬件地址只是“过滤器”不是加密。如果现场干扰太乱还是会出现丢包和重传。尽量选一个射频空洞大的信道并通过查看接收端的误码率来筛选。4. 船载端控制PWM、差速与状态机4.1 TIM1 PWM配置船载端要把接收到的百分比油门转成实际电机PWM输出。我使用的是TIM1的通道1和通道2对应PA8和PA9。TIM1是高级定时器输出PWM时比通用定时器多一步必须使能主输出MOE否则引脚没有波形。计算公式72MHz主频TIM1挂在APB2上时钟72MHz。预分频器设为71得到1MHz计数时钟ARR设为99则PWM频率为1MHz/10010kHz。这个频率对TB6612很合适既能避开人耳可听噪声又不会因为开关损耗过高导致驱动板发热。void TIM1_PWM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 99; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC2Init(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }如果使用带舵机的转向方案舵机要50Hz。那就可以把TIM1的ARR设为19999PWM输出周期20ms舵机中位对应1.5ms高电平PWM比较值设为1500。因为船模直流电机和舵机的工作频率差异很大如果同一块板子又控电机又控舵机建议用两个定时器分开配置。4.2 双电机差速控制我采用的是双电机差速不装舵机。这样好处是转向半径小甚至可以原地旋转。差速控制公式很简单uint8_t base throttle; // 基础油门 int8_t diff steer - 50; // 转向偏差范围-50到50 int8_t left base diff; int8_t right base - diff; left (left 100) ? 100 : (left 0) ? 0 : left; right (right 100) ? 100 : (right 0) ? 0 : right; TIM_SetCompare1(TIM1, left * 100 / 100); TIM_SetCompare2(TIM1, right * 100 / 100);注意把转向中点设为50而不是0。如果转向值100表示右满舵那steer-50的范围就是-50到50差速计算自然就出来了。有一个容易忽略的细节PWM比较寄存器直接赋百分比会让扭矩输出在低速时很不线性。直流电机在低占空比下可能根本转不动导致两侧电机启动不一致。我的处理方式是加一个“启动前馈”当目标PWM小于20%时实际输出20%当油门从零快速拉高时先给一个高频短脉冲让电机转起来再回落到目标值。这个措施明显改善了起步发抖问题。4.3 接收超时与失控保护比赛船最危险的就是失控。如果船开到湖中央遥控器没电或者链路被干扰船会全速直线冲出去轻则撞岸重则撞到其他参赛设备。所以必须做接收超时保护。我在主循环里用SysTick做时间基准每收到一帧合法数据就刷新一次“最后有效时间”。主循环每次判断当前时间和最后有效时间的差值如果超过150ms说明链路断了立刻把PWM输出清零并且点亮PC13的LED作为失联指示。这个150ms是根据20ms发送周期和NRF24L01自动重发次数算出来的。正常情况下即使偶尔丢两包也完全能在150ms内收到新数据。如果连续超过7个周期没收到数据基本可以确定链路断了不能拖延。实际测试中这个失控保护非常关键。有一次我在水池边调试时不小心绊到遥控器的摇杆船根本没注意到减速指令结果靠超时保护自动停了避免了一次撞墙维修。5. 遥控端软件与联调5.1 摇杆与电位器数据采集遥控端硬件更简单一个STM32F103C8T6最小系统板、一个双轴的PS2摇杆模块、一个NRF24L01、一块3.7V锂电池就够。摇杆模块输出的是两个模拟电压接在STM32的PA0和PA1上使用ADC1的通道0和通道1。ADC采样要处理几个问题机械电位器有死区手轻轻拨动摇杆时输出值会抖动。我在软件上做了10次采样取平均并且加了一个滞回比较只有当新的ADC值和上一次有效值相差超过一定阈值时才认为摇杆动了。这个阈值我调成20左右有效避免船模自己小幅漂移。5.2 发送端主循环遥控端主循环流程读取ADC转换结果映射成0到100的油门和转向值填入控制帧结构体计算CRC8调用NRF24L01发送函数等待发送完成或超时延时到20ms周期开始下一轮。发送时不能连续发送太快因为NRF24L01的自动ACK重发机制需要时间。如果发送间隔小于模块内部处理时间TX_FIFO满后新数据会直接丢弃。我试过5ms周期在近距离效果不明显但是一旦距离拉远重发次数增加就会出现发不出去的情况。最后稳定在15到20ms。发送函数里要检查MAX_RT标志位如果自动重发次数达到上限还没收到ACK就清掉TX_FIFO避免下一帧数据排队挤压。5.3 联调实测记录第一次联调是在学校操场距离约30米数据接收正常。但把船放到水面上距离同样30米马上出现卡顿。后来分析是水面反射和电机电磁干扰叠加的影响。解决办法不是加大发射功率而是把NRF24L01的天线尽量抬高并且远离电机连接线。另外一个问题是当时遥控端的NRF24L01和船载端NRF24L01用了相同的射频信道但发送地址不一致导致连续收到陌生数据包。这个在单队调试时不明显到了比赛现场多队一起开就能暴露出来。赛前一定要做一次多信道扫描最简单的方法是写一个程序扫描RF_CH的1到125统计每个信道的背景噪声选一个噪声最低的作为比赛信道。6. 调试现场与常见故障排查6.1 NRF24L01通信不稳定或距离短这是最频繁的问题。我整理了几个排查方向先确认SPI时序是否正确。用逻辑分析仪或者示波器看MISO上有没有正确回读寄存器值。最常见的是SPI引脚初始化成了开漏输出结果波形被拉不上去。检查NRF24L01的供电。看模块上的VCC是不是已经掉到3V以下。如果供电线的线压降太大直接把模块从5V或3.3V转换器上断开换成粗线或者独立LDO。检查天线区域有没有遮挡。NRF24L01上的天线部分本来是一段PCB走线如果被塑料外壳完全包住或者紧贴金属螺丝距离会大幅缩短。降低数据速率。RF_SETUP寄存器把2Mbps改成1Mbps或250kbps接收灵敏度会提高不少。船模控制数据量不大用1Mbps足够实测有效距离能提升接近20%。6.2 STM32F103C8T6下载失败或连接不上比赛前几天最容易遇到下载问题。我先说结论STM32F103C8T6用STLink下载连不上首先检查BOOT0跳线。正常下载时BOOT0要接低电平。如果BOOT0悬空或者被外部电路拉高芯片上电后进入系统存储器STLink的SWD口虽然能识别芯片但下载会失败或者卡在校验。另一个常见原因是驱动。在Windows下如果STLink的设备驱动不对Keil里能看到“No Target Connected”或者“SWD Communication Failure”。换成最新版驱动然后给STLink插上USB重新枚举一次很多时候就解决了。还有一种情况是芯片本身死锁。如果程序里把PA13和PA14当普通GPIO用或者误关了SWD引脚的重映射STLink就再也连不上了。解决办法是按住复位键在点击下载的瞬间松开复位让芯片在启动阶段被STLink接管。实在不行把BOOT0拉高用串口擦除Flash。6.3 电机一启动单片机就重启这个现象我见过太多次。基本原因就两个电源被拉垮、地线分离。电机启动瞬间电流很大如果MCU和电机共用同一路电源电压跌落会让STM32掉电复位。我当时用2S锂电池直供电机另一路DC-DC给MCU问题就解决了。但还是要在地线之间加一个0欧电阻或磁珠做单点接地防止电机电流通过数字地回流干扰MCU。还有一个细节TB6612的逻辑电源和电机电源要分开。VM接电池VCC接MCU的3.3V或5V。如果VCC不稳定PWM逻辑电平就可能被误判电机出现一顿一顿的现象。6.4 故障速查表现象可能原因检查方向NRF24L01读寄存器全0xFFSPI没通或接线错误检查MISO、MOSI、SCK、CSNNRF24L01读寄存器全0x00模块供电异常检查VCC、GND、电容能发送但接收不到地址或信道不一致比对两端TX_ADDR/RX_ADDR/RF_CH距离明显变短天线遮挡、电压跌落抬高天线、换粗线、降数据速率下载报错No TargetBOOT0脚被拉高强制接地后重试电机一转就复位电源共地不合理分电源、加电容、单点接地两个电机转速差异大电机驱动板死区电压不同软件加启动前馈、校准PWM映射7. 比赛之后的复盘与一点建议项目结束后最大的体会是赛题听上去是“船模设计”实际比拼的是系统集成能力和现场排错能力。STM32F103C8T6和NRF24L01都不是性能最强的组合但它们是稳定性最高的组合之一社区资料多、例程丰富、踩坑经验也相对透明。代码这块建议把NRF24L01驱动、串口打印、CRC校验、PWM控制拆成独立模块比赛现场修改参数时不用翻整个工程。如果后续想把这个项目扩展到无人船或者智能巡航方向可以在现有硬件框架上加一个GPS模块和电子罗盘把遥控控制改成半自主控制。NRF24L01已经能提供足够低延时的双向通信完全可以把船载端采集的电压、温度、姿态数据回传上来。到那一步前端加一个简单上位机整系统就又是一个新项目了。本文还有配套的精品资源点击获取