简介:这是一套基于STM32F103C8T6最小系统板与nRF24L01无线模块的船模设计比赛项目源码,适合电子竞赛、嵌入式入门及船模爱好者学习参考。工程以标准外设库编写,涉及PWM电机调速、ADC数据采集、无线遥控通信等关键环节,并涵盖定时器、RCC、I2C等底层外设初始化代码,代码结构清晰,可直接用Keil打开编译,便于在此基础上修改和创新。压缩包共238个文件,约6.38MB,其中包含54个H头文件、53个C源文件,以及Keil工程配置、hex/axf固件和编译过程文件,便于对照源码学习或直接烧录验证。目前已有1081人学习下载,作者为e_xiaoan。同时,资源描述中附带配套博客说明与更新链接,可帮助读者更快理解整体设计思路和硬件接线;但代码仅供交流参考,实际使用时需结合自身船模结构、供电与机械部分调试。 三年前参加大学生船模设计比赛那次,我的船下水不到半分钟就失联了,遥控器按键按到冒汗,船在水面上一动不动,最后靠着捞杆把它勾回来,成绩自然也没好看。后来我把整套遥控方案推倒重做,换成 STM32F103C8T6 最小系统板加 NRF24L01 无线模块,才真正把遥控稳定性和响应速度做住了。这篇文章就把这套 stm32f103c8t6+nrf24l01船模设计比赛项目源码 的核心思路和关键代码拆开讲清楚,包括硬件怎么选、管脚怎么分配、通信帧怎么设计、收发两端的程序怎么写,以及我在实船调试中踩过的几个大坑。不管你是要备战船模比赛、自组遥控船,还是单纯想在 STM32 上做一套无线遥控项目,都值得花十分钟把这套方案过一遍。
1. 硬件底座选择:为什么这套组合能抗住比赛
1.1 比赛场景下的需求拆解
船模比赛最怕的不是做不快,而是遥控失灵。比赛场景要求整条船在几十米范围内操控可靠,转向指令延迟不能明显,油门响应要跟手,而且整套系统成本要压得住。拆开来看,核心需求就三块:一块能稳定采集中位信号的控制板,一个能实时收发控制指令的无线通道,以及一套能驱动电机和舵机动作的执行机构。当时我对比过 Arduino、51 单板机,最后选 STM32F103C8T6,理由是它的资源在同类板子里足够“溢出”:72MHz 主频、64KB Flash、20KB SRAM,ADC、定时器、SPI、USART 全都有,还有大量网络资料和现成例程兜底。最重要的,蓝色药丸形态的最小系统板只要几块钱,即便比赛中烧坏一块也不会心疼。现在市面上还有不少 GD32、APM32 这类国产兼容替代型号,引脚基本一致,采购也不慌。
1.2 为什么要选 NRF24L01 而不是蓝牙或 WiFi
无线方案的选型我纠结了很久。蓝牙方案手机配对麻烦,比赛中快速掉线重连是致命伤;WiFi 延迟和功耗都不占优,而且组网过程徒增复杂度;433MHz 遥控模块天线大,比赛现场干扰源多。NRF24L01 的定位恰好卡在“低延迟、低成本、点对点”这个点上:工作在 2.4GHz 频段,有 125 个可选信道,能现场避开拥挤的 WiFi 频段;Enhanced ShockBurst 硬件协议在模块内部自动完成帧头、CRC 校验等打包工作,MCU 只负责从 SPI 口搬运几个字节,学习成本很低。实测在 1Mbps 速率下,一发一收延迟基本在微秒到毫秒级,比手机蓝牙方案跟手得多。整套无线链路也就十几块成本,完全符合学生项目预算。
提示:这个项目用裸机加状态机就足够了,不需要上 FreeRTOS,也更不需要 LVGL 这类图形框架。遥控端最多加一块 OLED 显示参数,就是很舒服的状态。
2. 系统架构建法与管脚分配
2.1 双端硬件清单
整套系统拆成发射端(遥控器)和接收端(船载),两边各用一块 STM32F103C8T6 最小系统板和一块 NRF24L01 模块。发射端还用了两个 10K 电位器模拟摇杆,分别控制油门和转向;接收端则接 L9110S 电机驱动板、一个直流电机带螺旋桨,以及一个 SG90 舵机负责船舵转向。电源方面,发射端用锂电池通过 AMS1117-3.3 降压给板子和无线模块供电;船载部分我特意把电机供电和逻辑供电分开,电机直接吃电池电压,STM32 和 NRF24L01 走独立的 3.3V 稳压,避免电机大电流把数字电路拖垮。这个供电隔离在船上很重要,后面调试章节会详细讲。
2.2 管脚分配表
发射端和接收端我把 NRF24L01 的接线保持完全一致,这样调试时两块板子可以互换,排查问题非常方便。具体的管脚分配如下:
| 功能 | 发射端管脚 | 接收端管脚 |
|---|---|---|
| NRF24L01 SCK | PA5(SPI1_SCK) | PA5(SPI1_SCK) |
| NRF24L01 MISO | PA6(SPI1_MISO) | PA6(SPI1_MISO) |
| NRF24L01 MOSI | PA7(SPI1_MOSI) | PA7(SPI1_MOSI) |
| NRF24L01 CE | PB0 | PB0 |
| NRF24L01 CSN | PB1 | PB1 |
| NRF24L01 IRQ | PB10 | PB10 |
| 油门摇杆 | PA0(ADC1_CH0) | - |
| 转向摇杆 | PA1(ADC1_CH1) | - |
| 校准按键 | PA2 | - |
| 电机 PWM | - | PA8(TIM1_CH1) |
| 电机方向 | - | PA1 |
| 舵机 PWM | - | PA0(TIM2_CH1) |
| 调试串口 | PA9/PA10 | PA9/PA10 |
注意 PA8 是 TIM1 的通道 1,PA0 是 TIM2 的通道 1。用这两个定时器输出 PWM,正好覆盖了电机高频率调速和舵机 50Hz 标准周期控制两种需求。如果你手里只有其他型号的板子,只要保证 NRF24L01 挂在同一个 SPI 总线上,CE/CSN 任选普通 GPIO 就行。
2.3 供电和布线的几个细节
NRF24L01 对供电电压挺敏感,模块上电瞬间电流会拉到十几毫安以上,发射瞬间更高,如果供电线太细或者稳压器余量不足,通信距离会肉眼可见地缩水。我给无线模块的 3.3V 引脚旁边并联了 10uF 电解电容和 0.1uF 陶瓷电容,位置尽量靠近供电脚,实测对通信稳定性帮助很大。另外,船上的天线别贴着水面的金属件,NRF24L01 模块用排针架高一点,天线朝上,减少水面对 2.4GHz 信号的吸收。这些都是后来实测摸索出来的细节。
3. NRF24L01 通信链路:数据帧、初始化与收发流程
3.1 通信参数的确定
NRF24L01 属于 SPI 从设备,所有寄存器操作都走 SPI 总线。它内部有发送 FIFO 和接收 FIFO,数据包最大 32 字节,Enhanced ShockBurst 模式会自动补上帧头、地址和 CRC。在这个船模项目里,我手动把自动应答(auto-ACK)关掉了,因为每一帧遥控指令本就是一发一收,不需要应答重传;ACK 功能反而会增加空中的往返次数,拉高延迟。通信速率选 1Mbps 而不是 2Mbps,距离优先,实测 2Mbps 下丢包率明显升高,比赛现场环境复杂,稳定压倒一切。信道我固定在第 10 信道,因为常见 WiFi 路由集中在 1、6、11 信道,第 10 信道相对干净,到了比赛现场还可以通过按键切信道避开瞬时干扰。
3.2 数据帧结构
NRF24L01 本身是一个数据包承载工具,我在它的 payload 之上又加了一层极简协议,用来区分指令类型和校验完整性。实际发送 5 个字节,结构如下:
| 字节 | 内容 | 说明 |
|---|---|---|
| byte[0] | 0xAA | 帧头1 |
| byte[1] | 0x55 | 帧头2 |
| byte[2] | 油门值 | 0~255 |
| byte[3] | 舵量 | 0~180 |
| byte[4] | 校验值 | throttle ^ steer |
帧头加两字节不是为了防 NRF 数据包自身错位,而是为了过滤旧包。当遥控器连续发送时,如果某一包丢了,接收端下一次收到的可能是 FIFO 里排队的旧指令;帧头加校验可以快速识别并丢弃不完整的指令。校验方式没有用累加和,因为指令就两个数据字节,异或足够,MCU 解析起来也快。
3.3 核心初始化与收发函数
初始化阶段需要配置 SPI 时钟、设置 CE/CSN 为输出、把 IRQ 引脚设为输入,然后按顺序写入寄存器。这里给出精简版初始化代码,能跑通收发链路:
void NRF24L01_Init(void) { NRF_CE_LOW(); NRF_CSN_HIGH(); // 先关自动应答,使能数据通道0 NRF_WriteReg(0x01, 0x00); // EN_AA = 0 NRF_WriteReg(0x02, 0x01); // EN_RXADDR = 启动管道0 // 设置发送和接收地址一致,5字节 const uint8_t addr[5] = {0x34, 0x43, 0x10, 0x10, 0x01}; NRF_WriteReg(0x10, addr, 5); // TX_ADDR NRF_WriteReg(0x0A, addr, 5); // RX_ADDR_P0 NRF_WriteReg(0x11, 5); // RX_PW_P0 = 5字节payload NRF_WriteReg(0x05, 10); // RF_CH = 10 NRF_WriteReg(0x06, 0x06); // RF_SETUP:1Mbps,发射功率0dBm NRF_WriteReg(0x00, 0x0E); // CONFIG:使能CRC,2字节CRC }发送函数逻辑是:拉低 CE 进入待机,清空发送 FIFO,写入 payload,拉高 CE 至少 130us 进入发送模式,然后查询状态寄存器确认发送完成。发送失败一定要清掉 TX FIFO,否则模块会反复重发旧包,后面的新指令根本排不进去。
void NRF24L01_TxPacket(uint8_t *buf, uint8_t len) { NRF_CE_LOW(); NRF_CSN_LOW(); SPI_WriteByte(0xE0); // FLUSH_TX NRF_CSN_HIGH(); NRF_CSN_LOW(); SPI_WriteByte(0xA0); // W_TX_PAYLOAD for (uint8_t i = 0; i < len; i++) { SPI_WriteByte(buf[i]); } NRF_CSN_HIGH(); NRF_CE_HIGH(); delay_us(150); NRF_CE_LOW(); }接收函数一般通过 IRQ 引脚或者轮询状态寄存器判断是否有新数据。我建议项目早期用轮询方式,逻辑简单,后面再切到外部中断。接收时要留意读 payload 后清零 RX_DR 标志,否则中断会反复触发。
4. 遥控端代码:摇杆采样、映射与发送状态机
4.1 ADC 采样与摇杆映射
电位器摇杆本身输出的是模拟电压,经过 STM32 的 ADC1 采样后变成 0~4095 的数值。ADC 采样要加滤波,否则手指轻微抖动就会变成舵量和油门的毛刺。我用最朴素的办法:单次读取连续采样 10 次,排序取中值。这个滤波代码写起来简单,但对电位器接触噪声的抑制效果很好。原始值映射到 0~255 和 0~180 时留一点死区,避免摇杆回中后因电阻误差导致舵机轻微摆动。更讲究一点的做法是上电校准时按住按键,把当前摇杆物理位置记录为基准中位,之后所有映射都相对这个基准展开,大大减少电位器个体差异的影响。
4.2 发送状态机
遥控端主循环就是“采样—组帧—发送—刷新指示灯”的循环,周期大概 20ms,也就是 50Hz 发送频率,指令跟手程度和信道占用之间平衡得比较好。代码如下:
while (1) { uint16_t adc_throttle = ADC_ReadFilter(ADC_CH0); uint16_t adc_steer = ADC_ReadFilter(ADC_CH1); uint8_t throttle = map(adc_throttle, 0, 4095, 0, 255); uint8_t steer = map(adc_steer, 0, 4095, 0, 180); uint8_t tx_buf[5]; tx_buf[0] = 0xAA; tx_buf[1] = 0x55; tx_buf[2] = throttle; tx_buf[3] = steer; tx_buf[4] = throttle ^ steer; NRF24L01_TxPacket(tx_buf, 5); delay_ms(20); }如果 NRF24L01 连续出现发送失败,遥控端不能傻等着,要在主循环里及时发现并重置发送 FIFO。具体做法是读 STATUS 寄存器的 MAX_RT 位,如果置位就写 1 清掉该标志并执行 FLUSH_TX,防止无线链路卡死。比赛中遇到干扰造成的瞬时失败很正常,只要做到“失败后迅速恢复”就不会影响操控。
4.3 遥控端的实测提醒
发射端的电池电压下降之后,AMS1117 输出如果不够稳,NRF24L01 的发射功率也会跟着掉,遥控距离明显变短。所以遥控器低电量报警不是一个可选功能,我后来把 ADC 的采样通道分一路给电池电压,液晶屏上直接显示电压值,低于 6V 就闪烁提醒。另一个提醒是发射端和接收端的信道必须一致,否则两边都在“空忙”。我给遥控端加了一个按键循环切信道,OLED 上显示当前信道号,下水前先确认两端信道一致再封仓。
5. 船体端代码:指令解析、PWM 输出与失控保护
5.1 接收与解析
船体端上电后,NRF24L01 切到接收模式,通过外部中断或者主循环轮询 IRQ 引脚来获取新包。我建议收到数据后先丢进一个 buffer,解析放在主循环里做,不要在中断服务函数里执行 PWM 更新这种重操作,否则容易造成定时器计数毛刺。解析逻辑就是对帧头和校验值做判断:
uint8_t Parse_Command(uint8_t *buf) { if (buf[0] == 0xAA && buf[1] == 0x55) { if ((buf[2] ^ buf[3]) == buf[4]) { return 1; } } return 0; }只有校验通过的指令才认为有效,否则直接丢弃,不更新 PWM 输出。因为 NRF24L01 本身有硬件 CRC,出现帧内单字节错误的概率很低,这里的异或校验只是双保险,防止极端情况下接收到了错误数据。
5.2 电机和舵机的 PWM 输出
船体端有两路 PWM:一路电机调速,一路舵机转向。电机 PWM 挂在 TIM1_CH1 也就是 PA8 上,输出频率 20kHz,这样电机运转噪声小、调速线性度好。注意 TIM1 是高级定时器,初始化时除了配置 ARR 和 CCR,还必须使能主输出,否则 PWM 引脚永远是低电平:
TIM1->BDTR |= TIM_BDTR_MOE;这一行不加,PA8 不会输出任何波形,很多第一次用 STM32 的人都会在这个坑里卡半天。舵机 PWM 挂在 TIM2_CH1 也就是 PA0 上,周期 50Hz,对应 20ms 周期,脉宽 0.5ms 到 2.5ms 映射到 0 到 180 度。如果定时器配置为 1MHz 计数频率,那 CCR 寄存器的值直接写脉宽微秒数就行:0 度时写 500,90 度时写 1500,180 度时写 2500,非常直观。
船模如果不要求倒车,L9110S 的 PWM 方向控制直接接高电平,只通过 PWM 占空比控制转速即可。如果比赛规则允许倒车,那 PA1 作为方向脚控制 H 桥反向,占空比在 0~255 之间双向映射。
5.3 失控保护是船模的命根子
遥控船在水面上突然失控,最怕的是电机保持全速往前冲,船直接跑出视线范围。所以我专门加了一套“看门狗”逻辑:每收到一包合法指令,就记录当前时间;主循环里用 SysTick 检查,如果超过 500ms 没有新指令,就强制把油门设为 0,舵机打回中位。这个保护逻辑在岸上测试时感觉不到,但下水之后它就是最后一道安全网。同时船载板上的 LED 可以指示状态:正常接收时 LED 闪烁,失联超过 500ms 后 LED 常亮,方便岸上人员在远处判断通信状态。
6. 实船调试:三次翻车后的排查链路与经验沉淀
6.1 通信距离只剩三米
第一次下水测试我就碰到通信距离暴跌:船在架子上测试时能走二十米,一放进水池就三米失联。我先怀疑天线位置,把模块架空之后有所好转,但没根治。后来拿万用表去量 NRF24L01 的供电电压,发现舵机一动作电压就掉到 2.8V 左右。问题根源是船载电源方案太粗糙,电机、舵机、无线模块共用一个稳压,大电流冲击直接拉低逻辑电压。解决方法是把电机驱动供电独立出来,走电池直供,STM32 和 NRF 单独用 AMS1117 供电,再给无线模块附近补电容。同时我把 RF 速率从 2Mbps 降到 1Mbps,SPI 时钟降到一个保守值,通信距离一下子就上去了。
6.2 TIM1 高级定时器没有 PWM 输出
调试舵机时遇到过一个很隐蔽的问题:代码从网上抄来,舵机就是纹丝不动。用示波器量 PA8 引脚,波形始终是平的。排查了很久,最后翻手册才发现 TIM1 作为高级定时器,有刹车和主输出使能机制,默认状态下主输出是关断的。普通定时器不存在这个问题,所以网上很多例程不会特意写启用 MOE 的代码。加上那一行TIM1->BDTR |= TIM_BDTR_MOE;之后,PWM 立刻出现。这个坑特别典型,如果你用 TIM1、TIM8 这类高级定时器输出 PWM,一定要先确认主输出使能寄存器。
6.3 电机一开就丢包
下水测试的另一个经典现象是:船静止时遥控一切正常,电机一转就丢包。我用串口打印接收端的丢包计数,电机转动时丢包率直接飙到接近 100%。用示波器看 3.3V 和 GND 之间,电机启动瞬间有大量毛刺,这是典型的传导干扰。解决办法有几个方向:电机两端并联 104 陶瓷电容和 100uF 电解电容,信号线绞合后远离电机电源线,NRF24L01 模块用排针抬起来不贴主板。最有用的一步是让电机电源地线和逻辑地线单点相连,两个地不要在板上大面积共铜。做完这些,电机全速转动时丢包率立刻降到零。船下水后还要注意天线尽量垂直指向上方,不要让船壳遮蔽天线,因为水面本身对 2.4GHz 信号吸收很严重。
6.4 下载器连接不上
新拉回来的 STM32F103C8T6 核心板,ST-Link 经常出现连接不上。我踩过一次之后养成了习惯:遇到连不上就先检查 BOOT0 跳线,按下复位键重试,再不行就把 SWDIO 和 SWCLK 两根线重新插一遍。国内很多蓝色药丸核心板自带 CH340 串口转 USB 电路,也可以用串口 ISP 方式下载,把 BOOT0 拉高再上电,配合串口工具就能烧录。两种方式配合使用,几乎不会出现板子变砖的情况。如果你发现下载器怎么都识别不到芯片,先别怀疑芯片坏了,多半是驱动或者线序问题。
6.5 下水前的自检清单
比赛前我在房间里贴了一张自检清单,每次装船前按顺序过一遍,省掉了很多现场问题的排查时间:
- 发射端和接收端信道一致,OLED 显示的信道号相同
- 电池电压都在安全范围之内
- 遥控端摇杆回中后,舵机初始力臂处于中立位置
- 电机空转时油门从低到高逐步推,确认 PWM 输出平滑无跳变
- 不装螺旋桨的状态下,把船放在装满水的塑料桶里测试 30 分钟,确认不会突然失联
- 用透明胶带把 NRF24L01 模块固定好,避免船体震动导致排针接触不良
- 比赛场地如果信道拥挤,先用电台扫描功能找到干扰小的信道再下水
船模遥控项目做到这里,其实已经不是单纯写代码的问题,而是无线链路、电源、机械、水面环境等多个环节的统筹。每个人下水前都觉得自己没问题,但真正跑起来,问题往往出在你最没预料到的地方。如果非要说一个最重要的经验,那就是一定把“可观测性”做足——遥控器上显示信道、丢包率、电池电压,船载端用 LED 指示通信状态。你能在岸上看到的东西越多,需要在水里捞船的次数就越少。准备比赛的朋友不妨从这套方案开始,先跑通一对 NRF24L01 的收发,再往板子上接电机和舵机,一步一步来,稳定性自然就有了。
本文还有配套的精品资源,点击获取