
简介本资源是一套基于STM32的无人船嵌入式控制系统完整工程代码面向嵌入式开发初学者、智能船舶项目开发者及高校电子/自动化专业实践者聚焦无人船姿态控制、无线通信与高精度定位三大核心功能。代码集成SI4432无线数传模块驱动、GY-273电子罗盘数据融合、北斗GNSS定位解析及多环路PID航向与速度闭环控制算法覆盖从传感器采集、数据解算到执行机构如舵机、电机PWM输出的全链路逻辑。压缩包含322个文件主体为76个头文件.h与70个源码文件.c辅以编译中间文件.o/.d、Keil工程配置.uvprojx/.uvoptx及调试映射文件.map/.axf总大小4.33MB结构符合标准STM32 HAL库工程规范。已有552人学习下载可直接导入Keil MDK环境编译运行配套注释清晰便于理解PID参数整定、串口协议解析及多传感器时间同步等关键技术实现细节。 最近在整理之前做的无人船项目看到那个“无人船.zip”的压缩包一直躺在硬盘里里面是STM32的完整工程、PID控制代码、定位模块处理逻辑还有一些调试时候的记录。回想起来这个项目从零到下水跑通踩了不少坑也积累了不少可以直接用的经验。所以把核心内容整理出来聊聊无人船这种水上机器人怎么做控制、怎么做定位、怎么把算法真正跑到单片机上。这个项目适合两类人看一类是刚接触STM32、想找个完整实战练手的学生或者爱好者另一类是已经在做无人船/无人车/无人机想参考PID整定、GPS定位解析、串口通信这些模块的开发者。我会把从硬件选型到代码实现的思路都拆开讲重点说清楚为什么这么做以及哪些地方容易踩坑。1. 项目整体设计思路1.1 为什么用STM32做无人船主控无人船的控制不算特别复杂但对实时性和外设接口有明确要求。你需要同时处理电机PWM输出、GPS串口数据、遥控器信号、传感器采集可能还要跑PID计算和导航算法。当时我对比过Arduino、树莓派和STM32最后选了STM32F103系列核心原因有三点第一实时性够且稳定。STM32是Cortex-M3内核72MHz主频跑PID这种毫秒级控制循环绰绰余。更重要的是它有硬件定时器、硬件串口、硬件I2C/SPI不依赖软件模拟实时性稳定可控。Arduino虽然上手快但一旦外设多起来中断优先级和定时器冲突会很麻烦。第二外设接口丰富。无人船需要至少2路PWM驱动电机、2个串口一个接GPS、一个接遥控/数传、可能还有I2C接IMU、ADC采集电压。STM32F103C8T6这种小封装芯片就能全部搞定成本也低一个板子十几块钱。第三生态成熟。ST的HAL库和标准库资料非常多网上关于PWM、串口、DMA、ADC的例程一抓一大把尤其“江科大STM32”那份教程对我早期帮助很大。出了问题容易查到解决方案这一点在项目排期紧的时候特别重要。1.2 系统架构与模块划分这个无人船系统我按功能拆成了四个模块控制模块、定位模块、通信模块、动力与电源模块。各模块之间尽量解耦比如控制模块只负责读目标值和传感器值输出PWM定位模块只负责解析GPS数据和IMU数据输出当前经纬度和航向。这样每个模块可以独立调试出了问题也好定位。具体硬件清单大概是主控STM32F103C8T6最小系统板定位GPS模块NEO-6M串口输出NMEA协议 MPU6050六轴陀螺仪/加速度计I2C接口动力两个直流电机配电子调速器电调或用L298N电机驱动板直接驱动通信HC-12无线串口模块用于遥控指令和遥测数据回传电源3S锂电池给电调供电通过降压模块给主控板供电整体数据流是遥控器指令或预设航点作为目标值GPS和IMU作为反馈值STM32内部跑PID控制算法输出PWM控制电机转速实现直行、转向、保持航向等功能。通信模块负责把状态数据传回岸边的上位机方便调试。2. 核心算法增量式PID与航向控制2.1 PID控制基础与增量式/位置式怎么选PID本身不是新东西但在无人船上选对“算法形式”很重要。先复习一下两种主流形式。位置式PID输出的是执行机构的绝对控制量公式是u(k) Kp * e(k) Ki * Σe(i) Kd * [e(k) - e(k-1)]这个形式直观但有一个问题它需要累加历史误差也就是积分项Σe(i)。如果长时间存在稳态误差或者某个时刻误差突然变大积分项会累积到很大导致输出饱和。在无人船上如果电机输出饱和了舵可能一直打满船会一直转圈非常危险。增量式PID输出的是控制量的增量公式是Δu(k) Kp * [e(k) - e(k-1)] Ki * e(k) Kd * [e(k) - 2*e(k-1) e(k-2)]实际输出为u(k) u(k-1) Δu(k)增量式的优势在于不直接累加误差不会有积分饱和问题即使某些时刻计算异常输出也只是增量变化不会瞬间跳变到极限值。这对船这种惯性大、响应慢的执行对象特别友好。我最后在航向控制和电机调速里都用了增量式PID稳定性和平滑性都更好。那我是不是说位置式就没用也不是。如果你执行机构是那种需要绝对位置的设备比如舵机角度位置式PID计算后直接给角度反而简单。但我当时船是用双电机差速转向类似于坦克和扫地机器人每个电机需要单独调速增量式更合适。2.2 航向控制代码实现与参数整定航向控制的目标是让船保持或者改变到一个指定航向角度。实际代码里我用IMU的Z轴角速度积分得到当前航向角再用GPS航向角做融合修正。PID的输入是目标航向和当前航向的差值输出是左右两个电机的转速差。这是增量式PID的核心实现我做了个带输出限幅的版本typedef struct { float target; // 目标值 float actual; // 当前值 float err; // 本次误差 float err_last; // 上一次误差 float err_prev; // 上上次误差 float Kp, Ki, Kd; // PID参数 float output; // 输出增量 float output_lim; // 输出限幅 float out_last; // 上一次实际输出 } PID_TypeDef; void PID_Calc_Increase(PID_TypeDef *pid, float target, float actual) { pid-target target; pid-actual actual; pid-err target - actual; // 增量式PID核心 pid-output pid-Kp * (pid-err - pid-err_last) pid-Ki * pid-err pid-Kd * (pid-err - 2 * pid-err_last pid-err_prev); // 更新历史误差 pid-err_prev pid-err_last; pid-err_last pid-err; // 输出限幅防止增量过大 if (pid-output pid-output_lim) pid-output pid-output_lim; if (pid-output -pid-output_lim) pid-output -pid-output_lim; // 累积输出 pid-out_last pid-output; }这里有几个关键点。输出限幅很重要因为实际电机的PWM增量不能突变否则电调容易过流船体也会猛冲。误差历史值的更新顺序容易写错必须先保存旧误差再更新当前误差否则后面差分项全算错。参数整定我推荐用“先P后I再D”的顺序。第一次下水时把Ki和Kd设成0只调Kp。从一个很小的值比如0.5开始看船的反应如果船缓慢靠近目标航向但不稳定说明Kp太小如果船在目标值附近来回振荡说明Kp太大。找到临界振荡的Kp值后乘上0.6左右作为安全Kp。然后加一点Ki去消除稳态误差Ki一般取Kp的十分之一到五分之一就行。Kd用于抑制振荡船在水上的惯性很大Kd不能太大通常先给Kp的十分之一再微调。这里还要提醒一句PID参数和水域条件强相关。风浪大的水面船受到扰动大Kd可以适当加大一些静水环境参数放得很尖风浪一来就崩这是正常的。我当时在湖里调好的参数换到有波浪的水域就重新整定了一遍。3. STM32定位模块与数据融合3.1 GPS模块选型与NMEA数据解析定位是无人船实现自动巡航的基础。我用的NEO-6M是比较常用的GPS模块串口输出NMEA 0183协议。NMEA里最常见的语句是GPGGA和GPRMC分别提供定位信息和推荐的定位数据。解析GPS数据的重点不是去理解所有字段而是找对头、解析有效字段。比如GPRMC语句的格式$GPRMC,083559.00,A,3105.5238,N,12123.2765,E,1.20,77.52,140124,,,A*63字段含义依次是UTC时间、定位状态A有效、纬度、纬度方向、经度、经度方向、速度节、航向角、日期。我主要解析状态位、经纬度、速度和航向。常见的坑是NMEA经纬度是“度分”格式。比如3105.5238表示31度05.5238分必须先转成十进制度数才能做距离和方位角计算// 将NMEA的ddmm.mmmm格式转换为十进制度 float nmea_to_decimal(float nmea_coord) { int degree (int)(nmea_coord / 100); float minute nmea_coord - degree * 100; return degree minute / 60.0f; }解析我用的是状态机串口空闲中断每一帧以$开头以回车换行结尾。每次收到完整一行后调用解析函数提取需要的字段。这样比逐字符判断省事很多。3.2 定位数据滤波与航向修正GPS数据本身有噪声尤其在船低速运动时位置漂移明显且航向角很不稳定。如果直接拿GPS航向去控制船船会像喝醉了一样左右摇摆。所以需要做两层处理第一层对GPS坐标做滤波第二层把IMU航向与GPS航向融合。对于坐标滤波我实践下来滑动平均和中值滤波都够用。中值滤波对去除离群点比较有效但会引入几个周期的延迟滑动平均更平滑但也会有滞后。无人船本身惯性大控制周期不需要太高100ms左右的滤波窗口是可以接受的。代码实现一个简单的滑动平均滤波器#define FILTER_SIZE 5 typedef struct { float buffer[FILTER_SIZE]; uint8_t index; float sum; } MovingAvg; void MovingAvg_Init(MovingAvg *f) { memset(f-buffer, 0, sizeof(f-buffer)); f-index 0; f-sum 0; } float MovingAvg_Add(MovingAvg *f, float val) { f-sum - f-buffer[f-index]; f-buffer[f-index] val; f-sum val; f-index (f-index 1) % FILTER_SIZE; return f-sum / FILTER_SIZE; }航向修正我用的是互补滤波思路IMU的短时间航向变化非常快且平滑但会积分漂移GPS的航向长时间稳定但短时间内噪声很大。所以用高通滤波处理IMU角速度用低通滤波处理GPS航向然后合并heading alpha * (heading_last gyro_z * dt) (1 - alpha) * gps_headingalpha一般取0.7到0.9之间。这个公式很轻量适合STM32F103这种MCU。如果你想要更精确的姿态解算可以用Mahony或者Madgwick滤波但注意STM32F103跑起来会有一定的CPU占用需要优化一下采样频率。关于航向角的跨0度问题目标航向355度当前航向5度误差不是350度而是-10度。这个如果不处理PID会朝着相反方向全力打舵船会原地绕一个大圈。处理方法是把误差归一化到[-180, 180]代码写法float angle_norm(float err) { while (err 180.0f) err - 360.0f; while (err -180.0f) err 360.0f; return err; }这个函数在航向控制里属于“命根子”级别的细节千万别省。4. 实操过程与核心环节实现4.1 电机驱动与PWM控制无人船的动力系统我用的是双电机差速驱动。左右两个电机分别由PWM控制转速通过改变左右电机的转速差实现转向。STM32用定时器产生PWM波我常用的方案是TIM2的CH1和CH2输出两路PWM频率设置为50Hz或更高。电调对PWM信号有特定格式常见的是50Hz高电平脉宽1ms到2ms对应0%到100%油门。具体初始化HAL库代码void PWM_Init(void) { TIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC; __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; // 72MHz/721MHz htim2.Init.Period 2000 - 1; // 1MHz/2000500Hz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim2); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1000; // 初始脉宽1ms sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2); }在实际控制里我直接把PID输出的转速差值叠加到基础油门里。基础油门让船维持一个最低速度保证舵效否则船停下来的时候打舵也没用。PID输出则用于微调左右电机实现航向保持或转向。还有一个容易被忽略的点电机是强干扰源一定要在电机两端并联续流二极管如果是直流电机或者依靠电调的电路设计否则电机反电动势会影响STM32工作重启或者程序跑飞都是可能的。我的第一版没加保护一开电机主控就复位后来并联了二极管、加了共地才稳定下来。4.2 串口通信与无线遥控无人船需要和岸边的遥控器/上位机通信。我最初用的是蓝牙透传但距离太短后来换成HC-12无线串口模块空旷环境下几百米没问题而且完全透传STM32那边只需要当普通串口用。通信协议一定要自己定义不要直接发裸数据。我用的协议很简单帧头(0xAA 0x55) 数据长度(1字节) 数据类型(1字节) 数据(N字节) 校验(1字节累加和)遥控器发送的指令包括左电机油门、右电机油门、控制模式切换、目标航点设置等。STM32收到控制指令后判断模式如果是手动模式就直接按遥控PWM控制如果是自动模式就进入PID定位控制逻辑。串口空闲中断处理不定长数据是STM32开发里的高频需求。HAL库里面存在串口空闲中断的回调函数在CubeMX里打开串口的空闲中断然后重写HAL_UARTEx_RxEventCallback它是接收不定长数据的标准姿势#define RX_BUFFER_SIZE 128 uint8_t rx_buf[RX_BUFFER_SIZE]; void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { // 处理收到的 Size 字节数据 Process_Remote_Data(rx_buf, Size); // 重新开始接收 HAL_UARTEx_ReceiveToIdle_IT(huart1, rx_buf, RX_BUFFER_SIZE); } }CubeMX配置时需要在USART1的NVIC设置中打开全局中断。这个回调在处理完数据后必须重新调用一次接收函数不然就只收一次。经验不要把数据解析放在中断回调里做太多事尤其是GPS解析那种长处理。我在回调里只是拷贝数据到环形缓冲区然后设置一个标志位主循环再解析。否则中断服务程序占用太长时间会影响其他实时任务。4.3 ADC多通道采样与电源电压监控无人船在湖上跑电池电压低了如果不及时返航船可能直接停在湖中央捞起来很费劲。所以一定要做一个电源电压监测。STM32的ADC多通道扫描DMA采样是比较省CPU的方式。我用的是ADC1的通道0和通道1分别采集电池分压电压和电流检测放大后的电压。开启ADC连续扫描模式DMA自动把数据搬到内存数组主循环里直接读数组就行。CubeMX里配置ADC后代码ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; uint16_t adc_values[2]; void ADC_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); __HAL_RCC_ADC1_CLK_ENABLE(); hdma_adc1.Instance DMA2_Channel1; HAL_DMA_Init(hdma_adc1); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1); hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; HAL_ADC_Init(hadc1); // 配置两个通道这里省略具体配置代码 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_values, 2); }电压计算要去查ADC的参考电压和分压电阻比例。比如电池12V经过两个10k电阻分压后ADC输入约6V而STM32的ADC输入最高3.3V所以分压电阻要选大一些比如用20k和6.8k或者10k和3.3k确保最高电压时ADC输入不超过3.3V。这个分压比在代码里做一个常数保存好调试时用万用表校准一下。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因解决方案GPS无数据或长时间无定位模块供电不足、天线朝下、串口波特率不匹配给GPS独立稳压供电天线朝上确认波特率默认9600GPS解析出现乱码串口中断和主循环共用缓冲区没有加互斥用环形缓冲区或者关中断拷贝数据电机不转但有PWM输出电调需要先给低油门信号上电初始化上电后延时几百毫秒再输出PWM航向控制振荡严重PID的Kp过大或Kd过小降低Kp先不加Ki加入Kd抑制电源电压读数不准分压电阻误差大用万用表实测两点电压校准分压比STM32程序跑飞电机反电动势干扰或看门狗未喂电机两端并联续流二极管加独立电源必要时开看门狗串口接收丢字节中断处理时间过长或有更高优先级中断打断把耗时操作移出中断降低中断优先级HAL_UARTEx_ReceiveToIdle_IT回调只触发一次回调里没有重新调用接收函数处理完数据后再次调用接收函数5.2 调试与避坑经验先说说最让我头疼的“Serial Port乱码”问题。GPS模块和HC-12都可能在同一串口线上出现电平不匹配尤其是HC-12和GPS模块共用一个串口时要确认是不是需要TTL电平不要直接和RS232接。另外GPS模块上电后首次定位很慢在室内完全没有信号必须拿到室外空旷的地方把手或者板子尽量放高。有时候在水面上GPS模块会因为船体晃动导致信号不稳定建议加一个外部有源天线放在船顶。还有一个典型的“程序真的没法定位”的问题不是GPS信号而是STM32的Keil或者STM32CubeIDE报“无法打开源文件stdio.h”。这种一般是编译器头文件路径没设置好需要在工程配置里把标准库头文件路径加上或者检查是不是把MicroLIB开起来了。类似的坑我不止踩过一次尤其是从别人那里拷贝工程的时候编译环境不一致就会炸。调试PID参数时千万别直接下水试。我强烈建议先在“干跑”状态下调把船放在支架上让电机空转通过上位机发送目标航向看桨叶转速变化方向是否正确。然后再拿到水面做低速测试。每一步只改一个参数记录下当前参数和现象。比如Kp1.2时船摆动幅度大Kp0.5时响应太慢记录下来就能画出一条响应曲线比盲目试错高效得多。最后再分享一个小技巧给船上加一个简单的LED状态灯用不同的闪烁频率表示不同状态。比如GPS没定位时慢闪定位成功时常亮自动模式时快闪。在水面上调试时看不清串口打印的内容LED状态灯是最直接的反馈能省下大量来回搬船的时间。这个项目后续其实还有很多可以扩展的地方比如加入避障模块、声呐/超声波测距、OpenMV摄像头识别浮标、把控制算法移植到FreeRTOS多任务系统甚至加上远程图传做视觉巡航。如果你也在做类似的STM32无人船建议先从手动遥控和PID航向保持做起一步一个脚印别一上来就想全自动那样出了问题根本不知道是定位的问题、控制的问题还是通信的问题。稳扎稳打上岸那天不会太远。本文还有配套的精品资源点击获取