☰
基于STM32F103ZET6的舵机转向循迹小车:从硬件选型到PID控制实战
2026/10/19 9:17:16 网站建设 项目流程

简介:面向智能车竞赛与嵌入式初学者的STM32循迹小车完整工程资料包。方案以72MHz主频的STM32F103ZET6为核心控制器,采用一字型灰度传感器阵列识别赛道黑线,配合舵机转向与双编码电机差速驱动,并以位置式PID作为主要控制算法,可应对直道、环岛、S弯和连续直角弯等典型赛道元素。压缩包共207个文件,大小约5.48MB,内含Keil工程源码(.c/.h)、编译生成的目标文件(.o/.d)、工程配置文件(.uvprojx/.uvoptx)以及可直接烧录的hex文件,并保留了map、lst等链接与列表输出,便于核对编译细节和程序结构,代码按模块组织,方便阅读与二次开发。该资源已吸引8435人浏览学习,受欢迎程度较高。对想要快速搭建循迹小车、理解PID参数整定和差速控制逻辑的开发者来说,这套资料既能提供完整代码参考,也能帮助排查工程配置问题,是值得收藏的实战项目,无论用于备赛还是课程设计,都能直接参考其完整闭环。

1. 循迹小车为什么值得做成“舵机+电机”的竞速底子

做循迹小车,很多同学第一个想到的是“四驱差速转向”,两个后轮转速不一样就能拐弯,结构简单、代码也好写。但如果你要的是“循迹竞速”,也就是在白色赛道上跑出尽量快的圈速,差速方案很快就会撞到天花板:转向响应慢、循迹精度靠轮子打滑来凑,速度一上来就容易冲出赛道。这时候用“舵机控制前轮转向 + 电机驱动后轮”的模型车架构,反而更适合高速循迹,这也是不少竞赛车队实际采用的方案。本文围绕基于STM32F103ZET6的循迹小车展开,从硬件选型、传感器布局、PID控制到赛道策略,给出一个可以直接照着搭的完整设计。适合正在做课程设计或毕业设计的学生,也适合想从差速小车升级到高速循迹方案的开发者参考。

2. 总体方案设计:STM32F103ZET6的资源盘活与车体架构选择

2.1 为什么选STM32F103ZET6做主控

首先需要明确一件事:做循迹小车,主控不需要多高性能,但外设资源要够用。STM32F103ZET6属于大容量型号,144引脚封装,Flash有512KB、RAM有64KB,主频72MHz。循迹小车的传感器读取、舵机PWM输出、电机PWM输出、编码器采集、蓝牙调试这些任务,它的外设完全扛得住,而且库函数资料多,遇到问题容易搜到参考。

从资源分配角度,我做方案时会这样安排:

  • 灰度传感器模块:用GPIO读取8路数字信号,或者用ADC读取模拟量,取决于传感器模块本身是数字输出还是模拟输出。
  • 舵机:需要一个定时器产生50Hz的PWM,占空比在2.5%到12.5%之间对应0度到180度。通常用TIM1的通道1。
  • 驱动电机:需要一个频率高一点的PWM来控制速度,比如20kHz,减少电机啸叫,用TIM2或TIM3。还要配合两个GPIO控制方向。
  • 编码器测速:如果要做速度闭环,需要两个定时器工作在编码器模式,TIM4和TIM5比较常用。
  • 调试接口:USART1接蓝牙模块或者无线串口,用来在跑车的时候看PID输出和传感器状态。

这套分配下,F103ZET6的定时器和GPIO都还留有大量余量,后续想加超声波避障、OLED显示状态、按键切换模式,都不用换主控。

2.2 车体架构:舵机转向和电机驱动的职责划分

循迹小车的车体结构直接决定了控制策略怎么写。差速小车靠左右轮速差转向,转向半径由轮速差决定,而舵机转向车型的前轮有一个最大转角,舵机角度和转向半径呈近似线性关系。这个区别在控制上非常关键:

  • 差速方案:转向相当于一个积分量,轮速差持续存在才会持续转弯,控制周期要求更高,否则会左右摆动手感很差。
  • 舵机方案:舵机角度可以直接映射为目标转向角,控制环只需要把当前角度调整到目标值,天然适合PD控制,响应快且超调小。

在竞赛场景下,舵机转向车型的另一个优势是支撑结构稳定。舵机装在底盘前方,通过连杆带动前轮转向,后轮由电机通过齿轮或直驱驱动,整车重心分布可以做得更靠后,加速出弯时前轮不容易打滑。顺带说一个血泪经验:如果用差速小车跑高速,后轮打滑是整个控制环路里最难解决的“玄学问题”,而舵机车型可以把打滑问题主要局限在后轮驱动轮上,通过限幅和加速度控制来缓解。

2.3 传感器布局:8路灰度探头的安装策略

循迹小车最常用的传感器是灰度传感器,核心是红外对管——一组红外发射管加接收管,利用黑线和白底对红外光的反射率差异来输出不同的电平或电压。典型的灰度传感器模块上有电位器可以调节灵敏度阈值,也有比较器芯片做数字输出。

8路灰度探头的安装位置我一般建议这样:

  • 探头排成一条直线,横向间距约1.5厘米,覆盖总宽度约12厘米,刚好能覆盖常见的白色赛道配黑色引导线,引导线宽度通常在2厘米到3厘米之间。
  • 中间两路探头负责直线循迹,左右各三路负责提前探测弯道方向。
  • 探头离地高度控制在1厘米到2厘米之间,太近容易刮到地面,太远反射率下降,红外光会扩散。

安装时要注意一个细节:探头不能完全垂直于地面,稍微向前倾斜一个角度,让红外光打到白底后反射回接收管的角度更稳定。这个角度在1.5厘米离地高度下通常选20度到30度。

为什么是8路而不是4路或16路?4路的覆盖宽度太窄,小车稍微偏一点就丢失全部信号;16路的信息确实更丰富,但读取和状态判断的复杂度上升,对于51单片机可能有问题,对于F103ZET6来说其实也能处理,但布线难度更高。8路是竞速小车上最常见的选择,因为状态组合可以比较完整地映射到转向角度,后续做补线策略也方便。

3. 路径检测模块实现:灰度数据的读取、阈值处理与中线计算

3.1 灰度数据的采集与去抖

无论传感器模块是数字输出还是模拟输出,第一步都是把原始信号稳定地读进主控。数字输出的模块内部有比较器,直接接GPIO输入即可;模拟输出则需要接ADC通道。前者代码简单,后者可以灵活调节灵敏度,适合在不同光照环境下使用。

8路数字信号可以用GPIOA的低8位一次读入,然后做一个软件去抖。常见的做法是连续读取三次,取中间值或者两次相同的结果作为有效值。代码示例:

uint8_t Gray_Read(void) { uint8_t raw = GPIOA->IDR & 0xFF; // 一次读取PA0-PA7共8路 uint8_t stable; static uint8_t last_raw = 0; uint8_t cnt = 0; // 连续读取3次,统计每一位的稳定结果 for (uint8_t i = 0; i < 3; i++) { raw = GPIOA->IDR & 0xFF; cnt = (cnt << 8) | (raw == last_raw ? 1 : 0); last_raw = raw; } stable = raw; return stable; }

这个代码的原理是:连续三次读取,如果两次读取结果一致就认为信号稳定,否则用最后一次结果。实际测试中,灰度传感器在黑白边界附近会有几十毫秒的抖动区间,软件去抖能避免舵机在边界处高频摆动。

另一个更实用的办法是识别“哪几路压线”,用一个8位变量记录状态,每一位代表一路探头,1表示压到黑线,0表示压到白底。在控制循环里直接对这个状态做映射即可。

3.2 中线计算的两种方式:加权平均与状态映射

拿到8路压线状态后,需要把它转换成一个数值,用来表示“黑线相对于车体的横向位置”。这个数值就是中线偏差,是PID控制器的输入。

方式一:加权平均法。把每一路探头的位置索引作为权重,压线的探头索引加权求和再除以压线数量。比如第3路和第4路压线,权重分别是3和4,计算结果为3.5,表示黑线在中线偏右半个探头间距。这个方法的优点是连续性好,能输出小数级别的偏差,适合PID精细调节。

方式二:状态映射法。直接把8路状态的每一种组合对应到一个预定义的角度增量。比如只第4路压线(正中)偏差为0,第3路压线偏差为-1,第2路压线偏差为-2,依此类推。这个方法的优点是响应快、确定性强,缺点是组合数多,8路全状态有256种,处理起来麻烦,通常只映射单路压线和双路压线的情况。

我在竞速方案里推荐的做法是混合式:正常循迹时用加权平均法计算连续偏差,在弯道中如果出现多路压线,则切换到状态映射法,直接给定一个较大的转向角预设值。这样直线段控制细腻,弯道段响应迅速。

3.3 阈值自适应:一个经常被忽略的细节

灰度传感器的输出很大程度上依赖赛道表面的反射率。白色哑光纸和白色高光纸的反射率差异很大,室内灯光和自然光的红外干扰也不同。如果阈值固定不变,换一个场地就可能出现整片误判或整片丢失。

通常的做法是在小车启动前做一个初始化校准:让小车在赛道上扫一遍,采集8路传感器在白底和黑线上的电压范围,取两者的中间值作为判断阈值。如果传感器模块是模拟输出,这个校准过程还能顺便记录每路传感器的基线偏差——不同探头之间因为元件离散性会导致输出电压不完全一致,需要做归一化处理。

typedef struct { uint16_t white_val[8]; uint16_t black_val[8]; uint16_t threshold[8]; } Gray_Calib; void Gray_Calibrate(Gray_Calib *calib) { // 每个通道采集10次白底最大值和黑线最小值 // 实际代码里会先让用户按键触发,扫过黑白区域后取统计值 for (uint8_t i = 0; i < 8; i++) { calib->threshold[i] = (calib->white_val[i] + calib->black_val[i]) / 2; } }

这个初始化过程花不了两秒钟,但能避免跑了十分钟发现传感器全飘了、数据从“正常”变成“玄学”的尴尬。有些团队会直接把阈值做成可调参数,通过蓝牙下发修正在线调,这在赛场上属于后悔药级别的功能,建议预留。

4. 控制核心实现:舵机PD转向环与电机速度环的协同调参

4.1 舵机的PWM初始化与角度映射

舵机控制看起来简单,实际上第一步就容易翻车:舵机的PWM频率必须是50Hz左右,也就是周期20ms,占空比变化范围约2.5%到12.5%。很多同学把PWM频率配成几百赫兹甚至几千赫兹,舵机要么疯狂抖动,要么直接不转。

使用定时器生成50Hz PWM的配置要点:

  • 定时器时钟72MHz,预分频系数设为71,得到1MHz计数频率。
  • 自动重装载值设为19999,得到50Hz的PWM频率,同时计数分辨率是1微秒。
  • 舵机中位对应的占空比是7.5%,即1500微秒;左右极限分别是1000微秒和2000微秒。

代码片段如下:

void Steering_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能TIM1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 72MHz / 71 / 20000 = 50Hz TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_Period = 19999; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // PWM模式1,输出比较预装载使能 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 1500; // 初始中位 TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_Cmd(TIM1, ENABLE); }

舵机角度与PWM占空比不是严格的线性映射,不同品牌的舵机线性度也有差异。实际使用中会让小车放地上,从小到大给舵机加角度,观察前轮是否在中位两端对称,如果不对称,用软件做一次角度补偿。

4.2 PD控制器的C语言实现与参数意义

循迹小车转向控制最常用的控制器是PD,也就是比例加微分,一般不加积分。原因很简单:循迹的误差是动态变化的,没有稳态残差问题,加积分反而会在弯道处造成超调和振荡。

PD控制器核心代码:

typedef struct { float kp; float kd; float last_error; float output; float out_max; } PID_TypeDef; void Steering_PID(PID_TypeDef *pid, float error) { float d_term = (error - pid->last_error); pid->output = pid->kp * error + pid->kd * d_term; pid->last_error = error; // 输出限幅,防止舵机指令超出机械极限 if (pid->output > pid->out_max) pid->output = pid->out_max; if (pid->output < -pid->out_max) pid->output = -pid->out_max; }

这段代码有几个需要说明的点:

第一,误差error的来源是前面计算的中线偏差,加权平均法输出的是“当前位置与目标位置的偏差”,单位是探头间距的倍数,范围通常在-3.5到+3.5之间。

第二,微分项用的是误差变化量而不是误差对时间的导数,原因是控制周期固定,误差变化量本身就是微分的离散形式。这里不需要除以控制周期,因为它等效于把kd放大了一个系数,调参时直接通过kd来消化。

第三,输出限幅out_max要跟舵机机械限位对应。舵机中位1500微秒,假设左右各允许转动30度对应约200微秒的变化范围,那么输出限幅就设为200,防止PID在高速过弯时给出一个根本执行不了的舵机角度。

调参的顺序也是一门学问。我的习惯是先调kp,从小到大,让小车能在直线上稳定循迹,出现轻微摆动即可;然后加kd,逐渐增大直到弯道出弯时不再有明显的左右甩动。如果发现小车在进弯时反应迟钝,多半是kp不够;如果发现小车在直线段高频抖动,多半是kd过大,放大了一丁点噪声。

4.3 电机速度环:为什么竞速小车要加编码器闭环

驱动电机的PWM输出和舵机类似,但频率要高一些,一般用20kHz。电机的速度控制有两种思路:开环给固定PWM占空比,或者用编码器做闭环PID。开环方案在电池电压充足的时候很稳定,但跑着跑着电池电压下降,同样占空比下电机转速会下跌,速度环就失效了。

加编码器闭环的好处有两层:

一是稳速。用TIM4编码器模式读取电机转速,STM32的定时器自带正交解码功能,只需要把编码器的A相和B相接在定时器的两个通道上,硬件会自动计数,不需要外部中断。

二是能实现“弯道降速”策略。竞速小车不追求全程跑最高速,而是直线快速、弯道减速。通过编码器把当前车速反馈回来,在检测到大偏差时给定一个目标速度曲线,速度环自动把车速压下来,出弯后再升上去。

void Speed_Control(float target_rps) { float current_rps = Encoder_GetSpeed(); // 编码器换算为实际转速 float error = target_rps - current_rps; static float integral = 0; integral += error; integral = CLAMP(integral, -50.0f, 50.0f); // 抗积分饱和 // 增量式PID输出,叠加到当前PWM上 float delta = speed_pid.kp * (error - speed_pid.last_error) + speed_pid.ki * error; speed_pid.output += delta; speed_pid.output = CLAMP(speed_pid.output, -1000, 1000); speed_pid.last_error = error; Motor_SetPWM((int)speed_pid.output); }

这段代码里用的实际上是增量式PID,输出的是PWM的增量而不是绝对值。这样做的优点是即使pid参数不合适,也不会一下把PWM冲到满值导致小车猛冲出去。调参时速度环的kp比转向环小一个数量级,因为被控对象是电机机械系统,响应慢,kp太大会出现电机啸叫。

4.4 转向环与速度环的优先级:先稳住舵机再谈速度

在实际跑车中,两个环同时工作,如果速度环没有收敛,车速时快时慢,转向环的误差变化率会被放大,导致舵机输出的微分项波动剧烈。反过来,如果转向环不收敛,小车蛇形走位,速度环也会因为负载波动而振荡。

所以我的调车顺序非常固定:先把小车架起来,让后轮悬空,速度环开到目标速度闭环稳定;然后把小车放地上,速度环关闭,只调舵机PD让车能循迹;最后两个环同时开启,从小目标速度开始,逐步加速。整个过程每调整一次速度目标值,都要同时看一下转向环的微分项是否在合理范围。

5. 常见问题与避坑指南:从传感器失灵到PID震荡的排查思路

5.1 现象:传感器阈值固定导致全盘误判

小车在实验室白纸上跑得好好的,拿到比赛场地之后,上电一看8路传感器全部输出“压线”,或者全部输出“白底”,小车原地打转或直接冲出赛道。

原因:灰度传感器模块的电位器调节的是比较器参考电压,不同场地的白底反射率差异很大。如果你用的是数字输出模块,出场默认电位器位置可能只适配某一种光照环境;如果用的是模拟输出模块,ADC读到的白底电压范围可能整体偏移。

解决:在程序里加入初始化校准流程,启动后小车保持在赛道上,程序自动采集白底和黑线的电压范围并计算阈值。如果传感器模块是数字输出不能读模拟量,就把电位器调到一个中性位置,在比赛场地用螺丝刀微调,直到地面白底时指示灯全灭、黑线时对应指示灯亮起——这个方法比较原始,但确实管用。

5.2 现象:舵机PWM配置正确但抖动严重

舵机上电后持续发出“嗡嗡”声,用手摸舵机外壳能感觉到高频振动,转动角度在中位附近来回抖动。

原因:最常见的是PWM频率不对,舵机标准频率是50Hz,如果你把定时器配成100Hz甚至更高,舵机内部的电路会不断尝试重新定位,表现就是抖动;还有一种情况是PWM信号有毛刺,可能是时钟配置里PWM输出引脚和另一个定时器共用了通道。

解决:用示波器看PWM波形,确认周期确实是20ms。没有示波器的话,写一个最简单的测试程序,固定输出1500微秒脉宽,如果舵机还在抖,大概率是PWM配置问题。还有一个隐蔽坑:电机的PWM地线没有和舵机地线共地,舵机信号参考电位浮空也会造成抖动,检查电源地和信号地是否连在一起。

5.3 现象:PID参数调好之后高速过弯还是冲出赛道

小车在低速时循迹很顺,速度一提高,直角弯直接冲出去,或者S弯连续摆动失控。

原因:这通常不是PID参数本身的问题,而是“转向响应速度跟不上车速”。舵机的物理响应时间在50毫秒到100毫秒级别,车速2米/秒时,100毫秒车已经前进了20厘米,而探头排列覆盖宽度只有12厘米,传感器已经检测不到线了。也就是说,问题出在信息滞后,而不是控制算法。

解决:一是降低进弯速度,在检测到连续多路压线时提前减速;二是增加前瞻距离,把传感器支架往前延伸,让探头比前轮更早探测到弯道;三是使用补线策略——当最外侧探头压线时,直接把偏差量设为一个预设的较大值,而不是依赖PID逐步逼近。这个做法相当于在算法里做了“弯道预判”,实车效果提升非常明显。

5.4 现象:电池电压下降后电机明显变慢

小车刚开始速度正常,跑了两三分钟后越跑越慢,即使PWM输出没变。

原因:锂电池放电过程中电压会从12.6V降到10V左右,电机转速和供电电压近似成正比。开环PWM控制补偿不了这个电压跌落。

解决:如果已经加了编码器速度环,这个现象会自动消失,速度环会通过提高占空比来补偿电压下降;如果用的是开环,可以加一个电压监测ADC,根据电池电压修正PWM比例系数。这算不上真正的闭环,但能缓解最明显的掉速问题。注意编码器速度环的PID输出上限要留够余量,否则电压低时PID饱和,还是会出现掉速。

5.5 现象:蓝牙调试数据正常,但小车实际表现异常

代码里打印的传感器状态和解算出来的偏差数值看起来都对,但小车跑起来和预期完全不符。

原因:大部分情况是“调试状态”和“运行状态”的程序路径不一致。比如调试时传感器输出通过串口打印,运行循环里又读了一遍GPIO,两处读取的GPIO引脚或去抖逻辑不同,导致实际控制用的数据和你在串口上看到的不一样。

解决:把数据源统一成一个函数,无论调试还是运行都调用同一个读取接口。然后做一个关键验证——用串口把“PID输入偏差”和“PID输出指令”同时打出来,让小车固定在一个偏移位置,检查输出的舵机角度是否符合直觉。这个操作能一次性验证传感器、PID计算、PWM输出三个环节的通路。

6. 验收与答辩:用可量化的测试赛道和调参记录证明方案有效

到了临近交付或者答辩的时候,很多同学会陷入一个误区:反复调车试图跑出一个完美成绩,却忘了准备“如何证明这套设计是可行的”的呈现材料。课程设计和毕业设计的评审关注点是:需求理解是否准确、方案是否合理、实现是否完整、结果是否可靠。按照下面这套验收流程走下来,基本能把所有可能被问到的问题前置堵住。

第一步,搭建标准测试赛道。用黑色电工胶带在白色KT板上贴出包含直线段、90度直角弯、S弯、十字交叉、大回环五种元素的赛道,直线段长度不少于1米,弯道曲率覆盖高中低三档。赛道宽度取30厘米到40厘米,引导线宽度2.5厘米。这项工作的意义是让测试可复现——换一个场地、换一个时间跑同一个赛道,成绩差异能说明系统的稳定性。

第二步,定义量化指标并逐项测试。建议至少记录四个指标:完赛率,连续跑10圈的成功圈数占比;平均圈速,完整一圈的时间;最大舵机响应延迟,通过串口把传感器变化时间戳和舵机指令时间戳打出来计算;掉线次数,8路传感器全部无压线的累计计数。每调整一组PID参数,就完整测一遍这四项指标,记录在一个表格里。最终的验收表可以这样展示:

测试项验收标准实测结果
直线循迹稳定性直线段不发生S形摆动,偏差保持在±1路探头内通过
直角弯通过率连续10次过弯不冲出赛道9次通过,1次因入弯速度过高冲出
S弯平均偏差全程加权平均偏差小于1.5路探头平均值0.8路
十字交叉处理直行通过不断线通过
平均圈速至少达到0.8米/秒0.9米/秒

第三步,梳理演示主线。答辩时不要从“什么是灰度传感器”讲起,直接从问题切入:为什么选择舵机转向方案、如何把8路传感器信号变成PID输入、PID参数是怎么调的、最终测出来的数据说明什么。每一个结论都能对应到调试过程中的一张截图或者一段代码。我会建议把串口打印的偏差曲线用串口绘图工具录制一段视频,答辩现场放10秒就够,比讲一百句话都有说服力。

还有一个容易被问到的核心问题:“你的PID参数为什么是这几个值?”如果回答“调出来的”显得没有深度,正确的打开方式是展示调参过程:初始kp=0.5时小车振荡,增大到1.0后振荡消失但过弯响应慢,加kd=8后出弯稳定,kp和kd的比例大约在1比5到1比10之间。这套数据链能体现你对控制算法的理解是完整的,而不只是会调用库函数。

最后说一个习惯:从那以后我每次调完车,都会把最终的PID参数、传感器布局、机械连杆位置拍照存档,并且把每轮测试数据做成一个表格放在工程目录里。答辩前翻一遍这些记录,几乎能回答评审提出的所有细节问题。做项目最怕的不是没解决问题,而是解决了问题却没有记录下是怎么解决的。希望这篇基于STM32F103ZET6的循迹小车设计方案,能帮你把硬件、算法和验收逻辑串起来,少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询