STM32F103循迹小车控制程序源码技术分析

在高校电子设计竞赛和创客项目中,你可能经常看到这样一幕:一辆小巧的四轮小车沿着地面上的黑色轨迹快速前进,转弯时毫不迟疑,仿佛“看”得清清楚楚。这背后其实并没有复杂的视觉系统,而是一套基于STM32F103微控制器与红外传感器的精巧闭环控制系统。

这类循迹小车看似简单,实则集成了嵌入式开发、模拟信号处理、电机驱动和自动控制算法等关键技术。尤其是以STM32F103C8T6为代表的“蓝丸”开发板,凭借其高性能、低成本和丰富的外设资源,已成为此类项目的首选主控。本文将深入剖析其控制程序的核心逻辑,从硬件选型到代码实现,带你真正理解这套系统的运行机制,并为后续功能拓展打下坚实基础。


微控制器的选择:为什么是STM32F103?

很多人初学单片机时用的是51或AVR,但一旦涉及实时性要求较高的控制任务——比如需要同时处理多路传感器输入、生成精确PWM波形并执行PID运算——这些8位MCU就显得力不从心了。而STM32F103的出现,彻底改变了这一局面。

它基于ARM Cortex-M3内核,主频高达72MHz,支持单周期乘法和硬件除法,这对控制算法中的频繁数学运算至关重要。更重要的是,它的外设配置非常贴合电机控制场景:

  • 多个通用定时器(TIM2~TIM5)可灵活配置为PWM输出模式,用于驱动L298N模块;
  • 12位ADC最多支持16个通道,采样速率可达1μs级,完全能满足5路甚至8路红外传感器的模拟量采集需求;
  • 丰富且可复用的GPIO引脚,方便连接各类外围设备;
  • 支持SWD调试接口,配合STM32CubeIDE或Keil MDK可实现在线烧录与实时变量监控。

举个例子,在实际项目中我们通常使用TIM3的两个通道(CH1和CH2)分别输出左右电机的PWM信号,频率设为1kHz以上,避免人耳可闻的“滋滋”声,同时也减少电机因低频脉冲导致的抖动问题。

// 初始化TIM3为PWM模式(HAL库示例)
__HAL_RCC_TIM3_CLK_ENABLE();

TIM_HandleTypeDef htim3;
htim3.Instance = TIM3;
htim3.Init.Prescaler = 72 - 1;           // 72MHz / 72 = 1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 100 - 1;             // 1MHz / 100 = 10kHz PWM频率
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 左轮PWM
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // 右轮PWM

这段初始化代码设置了一个10kHz的PWM信号,周期为100个计数单位。通过修改比较寄存器值即可动态调整占空比,从而控制电机转速。


红外传感器阵列:如何“看见”黑线?

循迹的本质是位置检测。最经济高效的方案就是利用红外反射原理。TCRT5000是最常见的红外对管模块,由一个红外发射二极管和一个光电三极管组成。当光线照射到白色区域时反射强,光电三极管导通程度高,输出电压低;反之照到黑线时反射弱,输出电压升高。

市面上的模块通常提供两种输出方式:
- 数字输出(DO) :内置LM393比较器,设定阈值后直接输出高低电平,适合快速判断;
- 模拟输出(AO) :输出连续电压值(0~VCC),可用于灰度识别,精度更高但需占用ADC资源。

对于入门级项目,多数采用5路数字传感器阵列,布局如下:

[ S0 ][ S1 ][ S2 ][ S3 ][ S4 ]

理想情况下,S2位于轨迹正上方时代表居中状态。若S4触发,则说明小车严重右偏,应向左修正。这种离散化判断虽然简单,但在直道和缓弯中表现良好。

不过要注意几个工程细节:
- 安装高度建议控制在0.5~1.5cm之间。太高会降低灵敏度,太低则容易受地面起伏影响;
- 模块间间距应略小于黑线宽度(一般2~3cm),确保至少有一个传感器能稳定捕捉到轨迹;
- 光照环境变化会影响反射强度,因此最好保留电位器调节阈值的功能,或在软件中加入自适应校准机制。

如果你追求更高的控制平滑性,可以改用模拟量读取。例如通过ADC采集每路传感器的电压值,再进行加权平均计算质心位置:

uint16_t adc_values[5];
int weighted_pos = 0, total_weight = 0;

for (int i = 0; i < 5; i++) {
    if (adc_values[i] > threshold) {
        weighted_pos += adc_values[i] * (i - 2);  // 中心为0,左右分别为±1, ±2
        total_weight += adc_values[i];
    }
}
int error = total_weight ? weighted_pos / total_weight : 0;

这种方式能实现亚像素级别的定位精度,尤其适用于高速或曲率较大的路径。


电机驱动与差速控制:让小车听话转弯

动力系统决定了小车能否“动起来”,而驱动电路则是连接数字世界与机械运动的关键桥梁。L298N模块因其价格低廉、接线直观、驱动能力强,成为大多数初学者的首选。

它本质上是一个双H桥芯片,能够独立控制两路直流电机的正反转和调速。每个H桥由四个MOSFET构成,通过对角导通形成电流回路。例如,IN1=1、IN2=0时,左侧电机正转;反之则反转;两者同为0或1时进入刹车或悬空状态。

使能端ENA/ENB接收来自MCU的PWM信号,控制电机转速。值得注意的是,STM32的GPIO输出为3.3V,而L298N逻辑电平兼容5V TTL,因此可以直接驱动,无需电平转换。

下面是典型的电机控制封装:

// motor_control.h
#define LEFT_MOTOR_FORWARD()   do { \
    HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_SET);   \
    HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_RESET); } while(0)

#define RIGHT_MOTOR_FORWARD()  do { \
    HAL_GPIO_WritePin(IN3_GPIO_Port, IN3_Pin, GPIO_PIN_SET);   \
    HAL_GPIO_WritePin(IN4_GPIO_Port, IN4_Pin, GPIO_PIN_RESET); } while(0)

void SetMotorSpeed(uint8_t left_speed, uint8_t right_speed) {
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, left_speed);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, right_speed);
}

这里的 SetMotorSpeed 函数通过改变PWM占空比来调节速度。例如传入 (80, 60) 表示左轮快、右轮慢,小车将向右缓慢转向。这就是所谓的“差速转向”——不需要舵机,仅靠轮速差异就能完成转弯动作。

但实际使用中常遇到一个问题:电机响应非线性。低速时可能根本无法启动,或者左右电机特性略有差异导致直线跑偏。解决办法包括:
- 设置最小有效速度阈值(如不低于30);
- 在固件中加入左右轮速度补偿参数;
- 使用编码器反馈构建速度闭环(进阶做法)。

此外,电源管理也不容忽视。电机启动瞬间电流可达1A以上,若与MCU共用电池,可能导致电压骤降引发复位。推荐做法是使用独立供电,或在电源入口加装肖特基二极管隔离。


控制算法:从查表法到P控制的演进

有了感知和执行能力,接下来就是“大脑”的工作——根据当前状态做出决策。最简单的策略是查表法,即预定义每种传感器组合对应的电机动作。例如:

S4 S3 S2 S1 S0 动作
0 0 1 0 0 直行
0 1 1 0 0 微左转
0 0 0 0 1 急右转

这种方法实现简单,但扩展性差,且难以应对中间状态或噪声干扰。

更优的选择是引入比例控制(P控制)。核心思想是将位置偏差量化为一个误差值 error ,然后乘以比例系数 kp 得到修正量,施加于左右轮速度上。

int8_t GetPositionError(void) {
    uint8_t sensor[5];
    sensor[0] = HAL_GPIO_ReadPin(S0_GPIO_Port, S0_Pin);
    sensor[1] = HAL_GPIO_ReadPin(S1_GPIO_Port, S1_Pin);
    sensor[2] = HAL_GPIO_ReadPin(S2_GPIO_Port, S2_Pin);
    sensor[3] = HAL_GPIO_ReadPin(S3_GPIO_Port, S3_Pin);
    sensor[4] = HAL_GPIO_ReadPin(S4_GPIO_Port, S4_Pin);

    int8_t error = 0;
    if (sensor[0]) error = -2;
    else if (sensor[1]) error = -1;
    else if (sensor[2]) error = 0;
    else if (sensor[3]) error = +1;
    else if (sensor[4]) error = +2;
    else error = 0;  // 所有都未检测到,默认居中(可根据需要优化)

    return error;
}

void TraceLine(void) {
    int8_t error = GetPositionError();
    int base_speed = 60;
    int kp = 15;
    int left_speed = base_speed - kp * error;  // 注意符号方向
    int right_speed = base_speed + kp * error;

    // 限幅处理
    left_speed = (left_speed > 100) ? 100 : (left_speed < 0 ? 0 : left_speed);
    right_speed = (right_speed > 100) ? 100 : (right_speed < 0 ? 0 : right_speed);

    SetMotorSpeed((uint8_t)left_speed, (uint8_t)right_speed);
}

这里的关键在于 kp 的选取。太小会导致响应迟钝,错过弯道;太大则会引起震荡,表现为“蛇形前进”。最佳方法是在实际赛道上逐步调试,观察不同速度下的稳定性。

进一步优化可考虑加入积分项(I)消除静态误差,或微分项(D)抑制超调。完整的PID公式如下:

output = Kp * error + Ki * ∑error*dt + Kd * (error - prev_error)/dt

虽然代码复杂度上升,但对于高速运行或复杂轨迹(如S弯、十字路口)有显著提升。不过对初学者而言,先掌握P控制已足够应付大多数场景。


系统集成与常见问题排查

一个完整的小车系统包含多个子模块协同工作,任何一环出错都会导致整体失效。以下是典型架构与调试建议:

                    +------------------+
                    |  STM32F103       |
                    |  (Main Controller)|
                    +--------+---------+
                             |
        +--------------------+--------------------+
        |                                         |
+-------v------+                        +---------v--------+
| 5-channel IR |                        | L298N Motor Driver |
| Sensor Array |                        | (Left & Right DC Motors) |
+--------------+                        +--------------------+

常见问题及解决方案:

  • 小车原地打转?
    检查电机接线是否反接,确认左右轮转向定义是否正确。可通过单独测试各轮正反转验证。

  • 行驶中剧烈抖动?
    首先检查KP值是否过大;其次可在 GetPositionError() 中加入滑动平均滤波:
    c static int history[5] = {0}; memmove(history, history+1, sizeof(history)-sizeof(int)); history[4] = raw_error; return (history[0]+history[1]+history[2]+history[3]+history[4]) / 5;

  • 无法识别急弯或T型路口?
    数字传感器存在盲区。可增加侧向感应范围,或在检测到全白时启动搜索逻辑(如原地旋转扫描)。

  • 串口打印卡顿?
    避免在主循环中频繁调用 printf 。建议仅在调试阶段开启,正式运行前关闭日志输出,以免影响控制周期。

设计建议:

  • 预留调试接口 :UART连接PC,实时输出传感器状态和误差值,极大提升调试效率;
  • 合理布局PCB :传感器间距不宜过大,防止在细弯处丢失轨迹;
  • 固件可升级性 :利用STM32内置Bootloader或SWD接口,支持后期功能迭代;
  • 电源去耦 :在L298N电源端并联100μF电解电容+0.1μF陶瓷电容,抑制电压波动。

写在最后:不只是一个小车

别看这只是一辆循迹小车,它实际上是一个微型机器人系统的缩影。从传感器感知、数据处理、算法决策到执行机构响应,完整实现了“感知-决策-执行”的闭环流程。

更重要的是,这个平台具备极强的可扩展性。在此基础上,你可以轻松添加以下功能:
- 接入OLED显示屏,实时显示速度、误差、电池电压;
- 加入HC-05蓝牙模块,实现手机遥控或参数调节;
- 安装编码器,结合定时中断实现里程估算与速度闭环;
- 替换为OpenMV摄像头,迈向真正的机器视觉导航。

当你第一次看着自己写的代码让小车稳稳地跑完一圈又一圈,那种成就感远超理论学习。而这,正是嵌入式开发的魅力所在——把抽象的代码变成看得见、摸得着的智能行为。

掌握STM32F103循迹小车的设计与编程,不仅是学会了一个项目,更是迈入智能控制系统大门的第一步。未来的机器人、自动驾驶、工业自动化,其底层逻辑都不过是这一原理的延伸与深化。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐