简介:STM32F103C8T6前轮舵机转向智能小车黑线循迹避障运动程序源代码,面向智能小车爱好者、嵌入式初学者及单片机课程设计人群,旨在解决前轮转向小车在循迹与避障场景下的运动控制问题。整个工程基于Keil4编写,针对STM32F103C8T6芯片,覆盖L293D电机驱动、TT直流减速电机、1602液晶显示、舵机转向、六路循迹模块与红外避障模块的协同控制,逻辑完整,作者已在实车上亲测可用。资源包共189个文件、2.18MB,包含C语言源程序、头文件、启动汇编、Keil工程配置以及编译生成的hex烧录文件等,还涉及定时器、RCC、ADC等标准外设驱动文件,便于直接编译烧录,也可在此基础上二次开发。目前已有677人学习下载,可作前轮转向小车方案的参考样板,帮助理解前轮舵机转向控制、黑线循迹算法、红外避障判断及PWM调速等关键环节,适合竞赛准备与课程设计参考。
1. 前轮转向小车循迹避障,和两轮差速小车差在哪
同样做黑线循迹和避障,把两轮差速底盘换成前轮转向结构,很多人第一版程序跑起来就发现车不走直线、弯道冲线、遇到障碍想原地掉头也做不到。这颗 STM32F103C8T6 最小系统板驱动前轮转向小车,核心难点不在代码量,而在运动模型变了:后轮驱动、前轮舵机指向,决定了小车只能靠转向角和车速配合实现曲线运动,转弯半径有下限,循迹和避障本质上是两个不同优先级的控制状态。这篇文章就围绕 STM32F103C8T6 来写,把前轮转向运动学、灰度传感器循迹、超声波避障和状态仲裁拆开讲,附上能直接改的 C 语言代码、参数表和调试方法,适合正在做课设、竞赛车模或想理解轮式机器人运动控制的工程师对照落地。
2. 前轮转向运动学:阿克曼几何与舵机转向控制
2.1 阿克曼转向几何:左右前轮为什么不能转同样角度
两轮差速小车的转向靠左右轮速度差实现,车体可以绕自身中心旋转,运动学上是典型的微分驱动模型。前轮转向小车则相反,后轮是驱动轮,前轮是转向轮,转向时前轮并不是简单偏转同一个角度,而是遵循阿克曼转向几何:内侧轮的转角要大于外侧轮,使四个车轮的瞬时转动中心交于同一点,轮胎在弯道中才不会产生侧滑。实际车模的前轮转向结构大多是四连杆或五连杆,做不出教科书级别的完整阿克曼,但计算和调参时仍用阿克曼模型做近似。
常用参数定义如下:
- 轴距
L:前轮轴到后轮轴的水平距离,小车底盘尺寸决定,典型值 15~20cm。 - 轮距
T:左右转向轮中心的间距,典型值 12~18cm。 - 等效转角
δ:舵机带给主销的转向角,也就是代码里最终输出的角度。
转弯半径近似为R = L / tan(δ)。这个式子决定了控制器设计的物理上限:轴距越大、同样转角下转弯半径越大,转向越迟钝;轮距只影响内外轮转角分配,不改变主转角下的半径。循迹时如果误差持续大于一个车身宽度,靠加大转向角救不回来,只能降速。所以在代码里,转向角一般限制在 ±35° 而不是舵机物理极限 ±45°,机械限位处的堵转电流和舵机抖动都会放大,留出余量既是保护舵机,也是给循迹的 PD 控制留出调节空间。
2.2 舵机 PWM 中位校准与转向角映射
转向舵机三根线:电源、地、PWM 信号。PWM 频率固定 50Hz,周期 20ms,脉宽 1.0ms 对应一侧极限,1.5ms 理论中位,2.0ms 对应另一侧极限。STM32F103C8T6 通常用定时器 TIM2 或 TIM3 的 PWM 输出通道驱动,信号线接任意带复用功能的引脚,这里以 TIM2_CH3 在 PA2 输出为例:
// 舵机PWM初始化:TIM2_CH3,PA2输出,50Hz void Servo_PWM_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_2; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); tb.TIM_Prescaler = 72 - 1; // 72MHz分频到1MHz,计数单位1us tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_Period = 20000 - 1; // 20000us = 20ms周期 TIM_TimeBaseInit(TIM2, &tb); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 1500; // 1.5ms中位脉宽 TIM_OC3Init(TIM2, &oc); TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); }这段代码把系统主频 72MHz 分频到 1MHz,计数器每 1us 加一,重装值 20000 得到 20ms 周期,TIM_Pulse设成 1500 对应 1.5ms 脉宽。后面转向时直接调用TIM_SetCompare3即可,脉宽值和微秒数一比一,不用换算。
中位是不能信理论值的。同样标 1.5ms 的国产和进口舵机,中位偏差可能到 ±20us,表现就是直线循迹时前轮本身偏了一点,车辆走 S 形。校准方法是通电后用代码从 1400 到 1600 每隔 10us 停留一秒扫一遍,找到车轮刚好处在正中的脉宽值,写进常量。
| 舵机信号 | 脉宽 | 对应状态 |
|---|---|---|
| 左极限 | 1.0ms | 转向角 -45° |
| 中位 | 1.5ms(需实测校准) | 转向角 0° |
| 右极限 | 2.0ms | 转向角 +45° |
转向角到脉宽的映射用线性关系:
#define SERVO_CENTER 1480 // 实测校准后的中位脉宽 #define SERVO_MIN 1120 // 左极限脉宽 #define SERVO_MAX 1880 // 右极限脉宽 void Servo_SetAngle(int8_t angle) { uint16_t pulse; if (angle > 35) angle = 35; if (angle < -35) angle = -35; if (angle >= 0) { pulse = SERVO_CENTER + (SERVO_MAX - SERVO_CENTER) * angle / 35; } else { pulse = SERVO_CENTER - (SERVO_CENTER - SERVO_MIN) * (-angle) / 35; } TIM_SetCompare3(TIM2, pulse); }正负号约定要和机械装配保持一致。舵机装在车架左右两侧时,转向方向容易取反,所以Servo_SetAngle里建议加串口打印脉宽,配合肉眼确认方向。
2.3 转向角限制、车速前馈与电机驱动
前轮转向小车过弯时速度必须降下来,原因在R = L / tan(δ)本身:转向角确定后稳态转弯半径就定了,入弯过程有延迟,车速越快越容易冲出线外。代码里不要把转向和速度做成两个完全独立的控制器,更常见的是由转向角实时反推速度上限。
uint8_t speed = 45 - (uint8_t)(abs(angle) * 0.8f); if (speed < 20) speed = 20; Motor_SetSpeed(speed);三个参数分别对应电机 PWM 上限 45、降速斜率 0.8 和最低速度 20,数值按你的电机驱动原理图和电池电压调整。降压斜率越大,弯道减速越明显,直线恢复越快;设置太高会导致频繁加减速,电流冲击反而让循迹不稳定。电机驱动部分和舵机供电建议完全分开,TB6612 或 DRV8833 的逻辑电源单独从 5V 取,电机电源直接从电池取,避免舵机堵转时把传感器 ADC 的参考电压拉偏。
3. 黑线循迹:传感器阈值、误差计算与 PD 转向
3.1 循迹传感器布局与动态阈值校准
循迹通常用 4~5 路红外反射传感器或灰度传感器。我一般推荐 5 路,奇数路存在明确的中心传感器,中心对准黑线中心时误差为 0,而且误差正负对称。传感器做成弧形排列,横向跨距 8~12cm,间距 1.5~2cm,间隔参照黑线宽度——通常比赛用的黑线宽 1.8~2cm,传感器间距比线宽略小才能保证压线时至少一路有响应。
读取方式上,ST MCU 多路 GPIO 直接读数字量虽然简单,但地线干扰和元器件差异会带来误判。STM32F103C8T6 的 PA0~PA7 都带 ADC,把 5 路传感器接 5 个 ADC 引脚,用 12 位采样值做动态阈值,抗环境光能力比固定比较器的方案好。上电时让小车平稳放在无黑线的地面上采样 50 次作为地面基准,再推到全黑区域采样作为黑线基准。
static uint16_t groundBase[5]; static uint16_t lineBase[5]; void Sensor_Calibrate(void) { // 先测地面再测黑线,每路各自累计50次取平均 for (uint8_t i = 0; i < 5; i++) { uint32_t sum = 0; for (uint8_t k = 0; k < 50; k++) { sum += ADC_Read(i); // 12位ADC,范围0~4095 Delay_ms(2); } groundBase[i] = (uint16_t)(sum / 50); } // lineBase[] 的采集同理,小车推至黑线上方执行 } uint8_t Sensor_GetState(uint8_t index) { uint16_t value = ADC_Read(index); if (value < (groundBase[index] + lineBase[index]) / 2) return 1; // 检测到黑线 return 0; // 地面 }5 个传感器各自用独立基准,而不是全系统共用一个固定阈值,可以有效解决元器件离散性造成的某一路提前翻转、另一路延迟翻转问题。注意采集基准时传感器离地高度要保持一致,手持晃动会让基准整体偏移。
3.2 传感器权重、误差计算与 PD 转向控制
5 路状态折算成连续误差值,用加权法。每路传感器分配权重,中心为 0,向左依次为 -2、-1,向右依次为 +1、+2,检测到黑线就累加对应权重:
int16_t GetLineError(void) { int16_t error = 0; error += Sensor_GetState(0) ? -2 : 0; error += Sensor_GetState(1) ? -1 : 0; error += Sensor_GetState(2) ? 0 : 0; error += Sensor_GetState(3) ? 1 : 0; error += Sensor_GetState(4) ? 2 : 0; return error; }| 传感器位置 | 权重 | 含义 |
|---|---|---|
| 最左 | -2 | 黑线大幅偏左 |
| 偏左 | -1 | 黑线小幅偏左 |
| 中心 | 0 | 黑线对准中心 |
| 偏右 | +1 | 黑线小幅偏右 |
| 最右 | +2 | 黑线大幅偏右 |
误差不需要精确等于物理偏移量,只要沿偏移方向单调、极限可区分即可。然后 PD 控制转向角:
static int16_t lastError = 0; void LineFollow_Control(void) { int16_t error = GetLineError(); float P = 1.2f; float D = 1.5f; int16_t angle; angle = (int16_t)(P * error + D * (error - lastError)); if (angle > 35) angle = 35; if (angle < -35) angle = -35; Servo_SetAngle((int8_t)angle); lastError = error; uint8_t speed = 45 - (uint8_t)(abs(angle) * 0.8f); if (speed < 20) speed = 20; Motor_SetSpeed(speed); }D 项在循迹里起阻尼作用。前轮转向舵机有响应延迟,只有 P 项时小车会围绕黑线来回摆动:误差越大转角越大,转角大了又冲过线,形成低频振荡。D 项对误差变化率做反向修正,误差快速增大时抑制转角,误差快速减小时提前回正,这是直线稳定性和弯道跟线效果的关键。
提示:如果车在直线段持续摆头超过 1 秒,先检查 D 项是否被主循环的阻塞破坏了采样周期,再检查舵机中位。多数“PD 调不稳”的问题不在算法本身,而在舵机中位不准和主循环周期抖动。
P 和 D 的整定顺序建议:先设 D=0,P 从 0.5 逐步加,找到能稳定过中等弯道的临界值;然后逐步加 D 直到直线上不再摆头。对前轮转向小车,速度为 30~50 油门时,P 正常落在 1.0~2.0,D 落在 0.8~2.5,继续放大概率就是振荡。
3.3 防冲线:速度低通滤波与转向解耦
基础 PD 在长直线末端接急弯时容易冲线,因为误差信号来得晚,P 项瞬间打到限幅。一个实用的技巧是把速度闭环和转向闭环解耦:转向直接用当前误差,速度用上一周期的误差或转向角平滑值。速度指令加一级低通滤波:
static uint8_t smoothSpeed = 0; void Motor_SetSpeedFiltered(uint8_t target) { smoothSpeed = (uint8_t)(smoothSpeed * 0.7f + target * 0.3f); Motor_SetSpeed(smoothSpeed); }0.7 和 0.3 是速度低通系数,让速度变化跟随转向角的整体趋势,而不是被某一次误差跳变瞬间打满。效果是入弯前车先因为转向角增大而降速,而不是冲到弯心才刹车,过弯成功率明显提升。低通系数越大越平滑,但响应也越慢;如果直道末端接的是连续 S 弯,0.6/0.4 的分配会更激进一些。
4. 避障设计:超声波滤波、状态机与循迹仲裁
4.1 超声波测距与异常数据过滤
避障传感器通常用 HC-SR04,测距范围 2cm~4m,精度 ±3mm,低速小车完全够用。模块就两个控制脚:Trig 拉高 10us 以上触发测距,Echo 输出高电平宽度和距离成正比。测量 Echo 高电平时间再乘声速的一半得到距离。主循环在跑循迹 PD 时,如果用延时轮询等 Echo,容易丢脉冲,建议用定时器输入捕获或外部中断记录边沿:
// 假设Echo接PB4,外部中断响应上升沿和下降沿 void EXTI4_IRQHandler(void) { static uint8_t edge = 0; if (EXTI_GetITStatus(EXTI_Line4) != RESET) { EXTI_ClearITPendingBit(EXTI_Line4); if (edge == 0) { TIM_SetCounter(TIM4, 0); // 上升沿清零定时器 edge = 1; } else { echoTicks = TIM_GetCounter(TIM4); // 下降沿读出时间 edge = 0; } } }TIM4 配置为 1MHz 自由计数,echoTicks单位就是 us。距离换算用distance = echoTicks / 58,单位厘米数。58 来自声速换算:往返时间除以 2 再乘 343m/s,简化后就是除以 58。温度变化 10 度约产生 2% 误差,避障触发距离取 20~30cm,完全在容忍范围内。
异常数据过滤比测距本身更关键。HC-SR04 第一次测距经常返回 0,物体表面不平整时还有跳变值。我在状态机上加三层过滤:距离为 0 或超过 200cm 直接丢弃;连续 5 次取中值不用平均值;与上一次有效值差超过 30cm 时数据标记可疑,不参与决策。取中值是为了滤掉瞬时反射干扰,平均值反而会被一个异常大值整体拉偏,避障对最近障碍距离更敏感,中值更合理。
4.2 前轮转向避障状态机设计
前轮转向小车避障不能照抄差速车的“原地转身”逻辑。差速车可以 90 度转体绕开障碍再 90 度归位,前轮转向的最小转弯半径可能已经接近障碍尺寸,路径摆动幅度很大。最稳妥的是六状态机:正常循迹、停车、左打满绕行、保持方向直行、右打回轨、重新找线。
typedef enum { FSM_FOLLOW, // 正常循迹 FSM_BRAKE, // 检测到障碍停车 FSM_AVOID_LEFT, // 左打满绕行 FSM_AVOID_RIGHT, // 右打回轨方向 FSM_SEARCH_LINE // 重新找线 } CarFSM_t; CarFSM_t fsm = FSM_FOLLOW; uint16_t fsmTimer = 0; void Obstacle_Avoid(void) { switch (fsm) { case FSM_FOLLOW: if (ultraDist > 0.1f && ultraDist < 25.0f) { fsm = FSM_BRAKE; fsmTimer = 0; } break; case FSM_BRAKE: Motor_SetSpeed(0); Servo_SetAngle(0); if (++fsmTimer > 80) // 80 * 10ms = 0.8s { fsm = FSM_AVOID_LEFT; fsmTimer = 0; } break; case FSM_AVOID_LEFT: Servo_SetAngle(-30); Motor_SetSpeed(30); if (++fsmTimer > 150) // 1.5s横向绕行 { fsm = FSM_AVOID_RIGHT; fsmTimer = 0; } break; case FSM_AVOID_RIGHT: Servo_SetAngle(35); Motor_SetSpeed(30); if (lineDetected()) // 任一传感器压到黑线 { fsm = FSM_SEARCH_LINE; fsmTimer = 0; } break; case FSM_SEARCH_LINE: if (GetLineError() == 0 && lineDetected()) { fsm = FSM_FOLLOW; Servo_SetAngle(0); } else if (++fsmTimer > 300) // 3s找不到线就继续摆头扫描 { Servo_SetAngle(25); } break; } }每个状态必须有明确的进入条件、执行动作和退出条件,外加超时强制转移。FSM_BRAKE里的 0.8 秒强制转移是防止超声波偶发误触发把状态机卡死;FSM_SEARCH_LINE的 300 个周期超时则是防止丢失黑线后无限摆动。超时参数按主循环节拍计算,10ms 一个 tick,150 就是 1.5s。
| 状态 | 退出条件 | 超时保护 |
|---|---|---|
| FSM_FOLLOW | 距离 < 25cm | 不设超时 |
| FSM_BRAKE | 0.8s 定时结束 | 1s 强制转移 |
| FSM_AVOID_LEFT | 1.5s 定时结束 | 2s |
| FSM_AVOID_RIGHT | 任一传感器压线 | 3s |
| FSM_SEARCH_LINE | 中心传感器压线 | 5s 后停车 |
4.3 避障与循迹的仲裁:谁优先、何时切回
避障和循迹同时开启时,避障永远优先,循迹只在避障结束并且重新检测到黑线时恢复。这个优先级不能靠一个简单的 if 嵌套实现,因为两者都要持续控制转向和速度,如果写成“先循迹再判断障碍”,循迹控制器会在避障状态下继续覆盖舵机角度,出现转向角叠加的乱输出。
正确做法是主循环入口直接判断距离,满足避障条件就走避障逻辑,不满足才走循迹:
while (1) { ultraDist = Ultrasonic_GetFilteredDistance(); if (ultraDist > 0.1f && ultraDist < 25.0f) { Obstacle_Avoid(); // 进入避障状态机 } else { LineFollow_Control(); // 正常循迹PD } Delay_ms(10); }避障状态执行期间,循迹函数不会被调用,转向和速度完全由避障状态机接管。还有一个隐藏问题:超声波朝前检测,循迹过弯时检测到的可能是弯道外侧挡板而不是路径上的障碍。解决方法是给避障判定加一个约束条件:只有abs(GetLineError()) <= 1,也就是车头基本对准黑线时,超声波距离才参与避障判断;误差偏大说明车辆正在入弯,这个周期跳过避障判定。
5. 代码框架、固定节拍与串口在线调参
5.1 主循环节拍与实际初始化顺序
裸机程序按固定节拍跑,让 PD 采样和超声波测距都在同一时间基准上。用定时器产生 10ms 中断置标志,主循环每次处理一个 tick,所有控制逻辑按节拍执行。初始化顺序建议:系统主频、GPIO、定时器 PWM、串口、ADC 采样、传感器基准校准、舵机回中、电机归零,最后打开全局中断进入主循环。传感器校准必须放在舵机回中之前,因为校准过程需要小车静止,舵机上电瞬间可能会有抖动,先回中再校准可以避免车头偏移影响传感器对地面的采样。
5.2 串口在线调参与对照表
串口是调小车最重要的工具,把 error、angle、speed、distance、状态机编号按固定周期上报。调试 PD 时不要每次改参数都重新烧写,把 P 和 D 设成全局变量,用串口中断在线修改:
void USART1_IRQHandler(void) { uint8_t ch = USART_ReceiveData(USART1); if (ch == 'q') Pgain += 0.1f; else if (ch == 'a') Pgain -= 0.1f; else if (ch == 'w') Dgain += 0.1f; else if (ch == 's') Dgain -= 0.1f; }串口通信波特率固定 115200,调试效率比改一次烧一次快三四倍。实测时先把车架起、驱动轮悬空,手动拨动前轮检查舵机方向,再放地上跑直线看纠偏范围。
| 调参参数 | 典型范围 | 设置建议 |
|---|---|---|
| 舵机中位脉宽 | 实测 ±20us | 换舵机必须重新校准 |
| 转向角限幅 | ±30° ~ ±40° | 留 10° 给机械限位 |
| 循迹速度 | 30% ~ 50% 油门 | 直线快、弯道由转角反推 |
| 避障触发距离 | 20cm ~ 30cm | 大于车速 × 舵机延迟的 2 倍 |
| PD 采样周期 | 10ms 固定 | 必须固定,否则 D 失效 |
| 速度低通系数 | 0.6 ~ 0.8 | 越大越平滑,响应越慢 |
最后收在 D 项的真实含义上:D 项完全依赖相邻两次误差的差值,主循环如果被超声波等待或串口打印阻塞超过 5ms,D 项就失去了导数意义,系统等价于在低采样率下跑 PD,循迹效果会明显劣化。测距改成中断方式后,串口打印只在整定阶段开启,正式比赛或验收时关掉打印和调试标志。只有固定节拍下的 PD,才是前轮转向小车真正能稳定复现的参数。
本文还有配套的精品资源,点击获取