云计算百科
云计算领域专业知识百科平台

STM32F103 循迹小车全栈实战:从红外传感到 PID 闭环

目录

  • 1. 引言:小车为什么走蛇形?
  • 2. 系统总览与硬件架构
    • 2.1 主控与核心模块
    • 2.2 为什么选 TB6612 而不是 L298N
    • 2.3 系统框图
  • 3. 红外循迹原理
    • 3.1 物理原理
    • 3.2 传感器阵列布局
    • 3.3 红外反射原理图
  • 4. CubeMX 配置(HAL 库基础)
    • 4.1 时钟配置
    • 4.2 TIM2 → PWM 输出(电机调速)
    • 4.3 TIM3/TIM4 → 编码器模式
    • 4.4 GPIO 配置
    • 4.5 USART1
    • 4.6 配图建议
  • 5. 标准库代码实现
    • 5.1 红外传感器读取(tracking.c)
    • 5.2 电机驱动(motor_drive.c)
    • 5.3 编码器测速(encoder.c)
    • 5.4 PID 控制器(pid.c)
    • 5.5 主循环
  • 6. HAL 库代码实现
    • 6.1 红外读取(与标准库逻辑相同,使用 HAL API)
    • 6.2 电机控制(基于 CubeMX 生成的 htim4)
    • 6.3 编码器测速
    • 6.4 主循环(HAL)
  • 7. PID 参数整定方法
    • 7.1 三步整定法
    • 7.2 典型参数范围(智能小车循迹)
    • 7.3 工程经验
    • 7.4 PID 控制框图
  • 8. 调试与常见问题
    • 8.1 上电调试顺序
    • 8.2 常见问题排查
    • 8.3 调试流程图
  • 9. 进阶拓展
    • 9.1 多传感器融合
    • 9.2 速度闭环 + 位置闭环双环控制
    • 9.3 复杂路径识别
    • 9.4 双环 PID 控制框图
  • 10. 关键参数速查表
  • 11. 总结

1. 引言:小车为什么走蛇形?

很多同学第一次做循迹小车,都会遇到一个经典问题:小车明明能跟线,却总是左右摇摆,走出一条蛇形轨迹。还有人发现电机只发出"嗡嗡"的啸叫声,轮子却纹丝不动。这两个问题,一个指向控制算法,一个指向 PWM 频率配置——而它们恰恰是循迹小车从"能跑"到"跑得稳"的分水岭。

循迹技术,简单来说,就是让小车沿着地面上的引导线自主行驶。它看似只是"跟着线走",背后却串联起一条完整的嵌入式技术链路:先用红外传感器感知地面黑白差异,把物理位置信息变成电信号;再由主控芯片读取这些信号,判断小车相对黑线的偏移;接着通过电机驱动模块控制左右轮转速,让小车修正方向;而要让修正过程平滑、不抖动,还需要编码器测速和 PID 闭环控制来精确调节。这条"感知 → 决策 → 执行 → 反馈"的闭环,正是循迹小车的技术骨架,也是本文将要逐步展开的主线。

这篇文章面向 STM32F103 平台,从红外传感、电机驱动、编码器测速到 PID 闭环,覆盖循迹小车的完整技术链路。代码同时给出标准库与 HAL 库(含 CubeMX 配置)两套实现,方便不同工程习惯的读者对照使用。

2. 系统总览与硬件架构

2.1 主控与核心模块

模块常用型号接口
主控 STM32F103C8T6
循迹传感 TCRT5000 红外对管(3/5 路) GPIO 输入
电机驱动 TB6612FNG 或 L298N PWM + GPIO
动力 直流减速电机 + 编码器 PWM + TIM 编码器模式
电源 7.4V/11.1V 锂电 + 5V 稳压

2.2 为什么选 TB6612 而不是 L298N

实测数据对比如下:

  • 效率:TB6612 90-95%,L298N 仅 60-70%,前者发热量显著降低
  • PWM 响应:TB6612 上升时间 120ns,L298N 800ns
  • 持续电流:TB6612 1.2A/通道,L298N 2A/通道
  • 逻辑电平:TB6612 支持 3.3V,直接匹配 STM32;L298N 需 5V 逻辑

💡 小车项目首选 TB6612;若电机工作电压 >13.5V 或持续电流 >1.2A,再考虑 L298N。

2.3 系统框图

#mermaid-svg-r6x0FMKBEhdMaqD5{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-r6x0FMKBEhdMaqD5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-r6x0FMKBEhdMaqD5 .error-icon{fill:#552222;}#mermaid-svg-r6x0FMKBEhdMaqD5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-r6x0FMKBEhdMaqD5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-r6x0FMKBEhdMaqD5 .marker.cross{stroke:#333333;}#mermaid-svg-r6x0FMKBEhdMaqD5 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-r6x0FMKBEhdMaqD5 p{margin:0;}#mermaid-svg-r6x0FMKBEhdMaqD5 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-r6x0FMKBEhdMaqD5 .cluster-label text{fill:#333;}#mermaid-svg-r6x0FMKBEhdMaqD5 .cluster-label span{color:#333;}#mermaid-svg-r6x0FMKBEhdMaqD5 .cluster-label span p{background-color:transparent;}#mermaid-svg-r6x0FMKBEhdMaqD5 .label text,#mermaid-svg-r6x0FMKBEhdMaqD5 span{fill:#333;color:#333;}#mermaid-svg-r6x0FMKBEhdMaqD5 .node rect,#mermaid-svg-r6x0FMKBEhdMaqD5 .node circle,#mermaid-svg-r6x0FMKBEhdMaqD5 .node ellipse,#mermaid-svg-r6x0FMKBEhdMaqD5 .node polygon,#mermaid-svg-r6x0FMKBEhdMaqD5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-r6x0FMKBEhdMaqD5 .rough-node .label text,#mermaid-svg-r6x0FMKBEhdMaqD5 .node .label text,#mermaid-svg-r6x0FMKBEhdMaqD5 .image-shape .label,#mermaid-svg-r6x0FMKBEhdMaqD5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-r6x0FMKBEhdMaqD5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-r6x0FMKBEhdMaqD5 .rough-node .label,#mermaid-svg-r6x0FMKBEhdMaqD5 .node .label,#mermaid-svg-r6x0FMKBEhdMaqD5 .image-shape .label,#mermaid-svg-r6x0FMKBEhdMaqD5 .icon-shape .label{text-align:center;}#mermaid-svg-r6x0FMKBEhdMaqD5 .node.clickable{cursor:pointer;}#mermaid-svg-r6x0FMKBEhdMaqD5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-r6x0FMKBEhdMaqD5 .arrowheadPath{fill:#333333;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-r6x0FMKBEhdMaqD5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r6x0FMKBEhdMaqD5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-r6x0FMKBEhdMaqD5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r6x0FMKBEhdMaqD5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-r6x0FMKBEhdMaqD5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-r6x0FMKBEhdMaqD5 .cluster text{fill:#333;}#mermaid-svg-r6x0FMKBEhdMaqD5 .cluster span{color:#333;}#mermaid-svg-r6x0FMKBEhdMaqD5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-r6x0FMKBEhdMaqD5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-r6x0FMKBEhdMaqD5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-r6x0FMKBEhdMaqD5 .icon-shape,#mermaid-svg-r6x0FMKBEhdMaqD5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r6x0FMKBEhdMaqD5 .icon-shape p,#mermaid-svg-r6x0FMKBEhdMaqD5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-r6x0FMKBEhdMaqD5 .icon-shape .label rect,#mermaid-svg-r6x0FMKBEhdMaqD5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r6x0FMKBEhdMaqD5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-r6x0FMKBEhdMaqD5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-r6x0FMKBEhdMaqD5 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

反馈

传感器阵列

主控 STM32F103

电机驱动 TB6612

直流电机

编码器

图1 系统框图:传感器阵列 → 主控 → 电机驱动 → 电机 → 编码器 → 反馈回主控,形成闭环。硬件实物图与接线图请参考文末附录。

3. 红外循迹原理

3.1 物理原理

红外发射管持续发射 850-940nm 红外光,地面反射后被接收管捕获:

  • 白底:反射率 70-80%,接收管导通 → 输出低电平
  • 黑线:反射率 10-20%,接收管截止 → 输出高电平

⚠️ 电平极性取决于具体模块设计,以实际模块调节/取反为准。TCRT5000 配合 LM393 比较器,通过电位器设定阈值,输出数字量。

3.2 传感器阵列布局

3-5 路传感器线性排布,常见权重赋值:

位置最左左中中间右中最右
权重 -3 -1 0 +1 +3

通过加权求和得到位置偏差 error,输入 PID 控制器。

3.3 红外反射原理图

#mermaid-svg-eDRigXnpLnZ9c1mT{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eDRigXnpLnZ9c1mT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eDRigXnpLnZ9c1mT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eDRigXnpLnZ9c1mT .error-icon{fill:#552222;}#mermaid-svg-eDRigXnpLnZ9c1mT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eDRigXnpLnZ9c1mT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eDRigXnpLnZ9c1mT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eDRigXnpLnZ9c1mT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eDRigXnpLnZ9c1mT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eDRigXnpLnZ9c1mT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eDRigXnpLnZ9c1mT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eDRigXnpLnZ9c1mT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eDRigXnpLnZ9c1mT .marker.cross{stroke:#333333;}#mermaid-svg-eDRigXnpLnZ9c1mT svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eDRigXnpLnZ9c1mT p{margin:0;}#mermaid-svg-eDRigXnpLnZ9c1mT .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-eDRigXnpLnZ9c1mT .cluster-label text{fill:#333;}#mermaid-svg-eDRigXnpLnZ9c1mT .cluster-label span{color:#333;}#mermaid-svg-eDRigXnpLnZ9c1mT .cluster-label span p{background-color:transparent;}#mermaid-svg-eDRigXnpLnZ9c1mT .label text,#mermaid-svg-eDRigXnpLnZ9c1mT span{fill:#333;color:#333;}#mermaid-svg-eDRigXnpLnZ9c1mT .node rect,#mermaid-svg-eDRigXnpLnZ9c1mT .node circle,#mermaid-svg-eDRigXnpLnZ9c1mT .node ellipse,#mermaid-svg-eDRigXnpLnZ9c1mT .node polygon,#mermaid-svg-eDRigXnpLnZ9c1mT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eDRigXnpLnZ9c1mT .rough-node .label text,#mermaid-svg-eDRigXnpLnZ9c1mT .node .label text,#mermaid-svg-eDRigXnpLnZ9c1mT .image-shape .label,#mermaid-svg-eDRigXnpLnZ9c1mT .icon-shape .label{text-anchor:middle;}#mermaid-svg-eDRigXnpLnZ9c1mT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eDRigXnpLnZ9c1mT .rough-node .label,#mermaid-svg-eDRigXnpLnZ9c1mT .node .label,#mermaid-svg-eDRigXnpLnZ9c1mT .image-shape .label,#mermaid-svg-eDRigXnpLnZ9c1mT .icon-shape .label{text-align:center;}#mermaid-svg-eDRigXnpLnZ9c1mT .node.clickable{cursor:pointer;}#mermaid-svg-eDRigXnpLnZ9c1mT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eDRigXnpLnZ9c1mT .arrowheadPath{fill:#333333;}#mermaid-svg-eDRigXnpLnZ9c1mT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eDRigXnpLnZ9c1mT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eDRigXnpLnZ9c1mT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eDRigXnpLnZ9c1mT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eDRigXnpLnZ9c1mT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eDRigXnpLnZ9c1mT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eDRigXnpLnZ9c1mT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eDRigXnpLnZ9c1mT .cluster text{fill:#333;}#mermaid-svg-eDRigXnpLnZ9c1mT .cluster span{color:#333;}#mermaid-svg-eDRigXnpLnZ9c1mT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eDRigXnpLnZ9c1mT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eDRigXnpLnZ9c1mT rect.text{fill:none;stroke-width:0;}#mermaid-svg-eDRigXnpLnZ9c1mT .icon-shape,#mermaid-svg-eDRigXnpLnZ9c1mT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eDRigXnpLnZ9c1mT .icon-shape p,#mermaid-svg-eDRigXnpLnZ9c1mT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eDRigXnpLnZ9c1mT .icon-shape .label rect,#mermaid-svg-eDRigXnpLnZ9c1mT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eDRigXnpLnZ9c1mT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eDRigXnpLnZ9c1mT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eDRigXnpLnZ9c1mT :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

发射

白底反射率 70-80%

黑线反射率 10-20%

红外发射管850-940nm

地面

接收管导通输出低电平

接收管截止输出高电平

图4 红外反射原理图:发射管→地面→反射→接收管的光路示意。五路传感器间距建议 1.5-2cm,匹配黑线宽度 1.8cm。

4. CubeMX 配置(HAL 库基础)

4.1 时钟配置

  • HSE 8MHz → PLL → HCLK 72MHz
  • APB1 定时器时钟 36MHz / APB2 定时器时钟 72MHz

4.2 TIM2 → PWM 输出(电机调速)

  • Channel1/2 配置为 PWM Generation CH1/CH2
  • 对应引脚 PA0/PB8(用户原有工程使用 PB8/PB9)
  • Prescaler = 71,Period = 99 → 10kHz PWM(电机运行最平稳的频率)
  • 用户原代码 TIM_Prescaler=7199 是错误值,必须改为 71

4.3 TIM3/TIM4 → 编码器模式

以 TIM3 为例:PA6/PA7 接编码器 A/B 相。

  • Combined Channels 选择 Encoder Mode
  • 参数设置:Encoder Mode = TI1 and TI2(4 倍频),Counter Period = 65535,ICFilter = 4(中等滤波)
  • 引脚 GPIO Settings 设为 Pull-up

4.4 GPIO 配置

  • 五路红外输入:PA4、PB5、PB1(用户指定)等,配置为 Input 模式
  • 电机方向控制:PB12-PB15,配置为 GPIO_Output
  • STBY 引脚(TB6612):Output,初始 High

4.5 USART1

Asynchronous,115200bps,用于调试输出。

4.6 配图建议

  • 图7 CubeMX 时钟树配置截图
  • 图8 CubeMX TIM2 PWM 参数配置截图(突出 PSC=71, ARR=99)
  • 图9 CubeMX TIM3 编码器模式配置截图(突出 TI1 and TI2)
  • 图10 CubeMX 引脚分配总览图

5. 标准库代码实现

5.1 红外传感器读取(tracking.c)

// tracking.c – 五路红外循迹
#define LEFT_PIN GPIO_Pin_4 // PA4
#define CENTER_PIN GPIO_Pin_5 // PB5
#define RIGHT_PIN GPIO_Pin_1 // PB1

void Tracking_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Pin = LEFT_PIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin = CENTER_PIN;
GPIO_Init(GPIOB, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin = RIGHT_PIN;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}

// 返回 -1/0/+1 表示位置偏差方向
int8_t Tracking_GetError(void)
{
uint8_t L = GPIO_ReadInputDataBit(GPIOA, LEFT_PIN) == Bit_RESET;
uint8_t C = GPIO_ReadInputDataBit(GPIOB, CENTER_PIN) == Bit_RESET;
uint8_t R = GPIO_ReadInputDataBit(GPIOB, RIGHT_PIN) == Bit_RESET;

if (C && !L && !R) return 0; // 中间在线上
if (L && !R) return 1; // 偏左
if (R && !L) return 1; // 偏右
return 2; // 丢线
}

5.2 电机驱动(motor_drive.c)

// 基于用户原有 TIM4 PWM 代码修正版
void Motor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);

GPIO_InitTypeDef GPIO_InitStructure = {0};
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure = {0};
TIM_OCInitTypeDef TIM_OCInitStructure = {0};

// 方向引脚 PB12-PB15
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 |
GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

// PWM 引脚 PB8/PB9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);

// 10kHz PWM:72MHz / 72 / 100 = 10kHz
TIM_TimeBaseInitStructure.TIM_Prescaler = 71; // 修正:原为 7199
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 99;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStructure);

TIM_ARRPreloadConfig(TIM4, ENABLE);
TIM_Cmd(TIM4, ENABLE);

TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_Pulse = 0;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC3Init(TIM4, &TIM_OCInitStructure);
TIM_OC4Init(TIM4, &TIM_OCInitStructure);
TIM_OC3PreloadConfig(TIM4, TIM_OCPreload_Enable);
TIM_OC4PreloadConfig(TIM4, TIM_OCPreload_Enable);
}

// 设置电机速度(-99~99,正前进负后退)
void Motor_SetSpeed(int8_t left, int8_t right)
{
// 左电机
if (left >= 0) {
GPIO_SetBits(GPIOB, GPIO_Pin_12); // AIN1=1
GPIO_ResetBits(GPIOB, GPIO_Pin_13); // AIN2=0
} else {
GPIO_ResetBits(GPIOB, GPIO_Pin_12);
GPIO_SetBits(GPIOB, GPIO_Pin_13);
}
TIM4->CCR3 = abs(left);

// 右电机
if (right >= 0) {
GPIO_SetBits(GPIOB, GPIO_Pin_14);
GPIO_ResetBits(GPIOB, GPIO_Pin_15);
} else {
GPIO_ResetBits(GPIOB, GPIO_Pin_14);
GPIO_SetBits(GPIOB, GPIO_Pin_15);
}
TIM4->CCR4 = abs(right);
}

5.3 编码器测速(encoder.c)

// TIM3 编码器模式
void Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure = {0};
TIM_ICInitTypeDef TIM_ICInitStructure = {0};

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM3, ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);

TIM_TimeBaseInitStructure.TIM_Prescaler = 0;
TIM_TimeBaseInitStructure.TIM_Period = 0xFFFF;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);

TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 4;
TIM_ICInit(TIM3, &TIM_ICInitStructure);

TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInit(TIM3, &TIM_ICInitStructure);

// 编码器模式:TI1 和 TI2 均计数(4 倍频)
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_Cmd(TIM3, ENABLE);
}

// 读取编码器计数(定期调用并清零)
int16_t Encoder_ReadAndClear(void)
{
int16_t cnt = (int16_t)TIM3->CNT;
TIM3->CNT = 0;
return cnt;
}

5.4 PID 控制器(pid.c)

typedef struct {
float target;
float actual;
float err;
float err_last;
float integral;
float Kp, Ki, Kd;
float out_min, out_max;
} PID_TypeDef;

float PID_Calc(PID_TypeDef *pid, float actual)
{
pid->actual = actual;
pid->err = pid->target actual;
pid->integral += pid->err;

// 积分限幅,防饱和
if (pid->integral > 1000) pid->integral = 1000;
if (pid->integral < 1000) pid->integral = 1000;

float out = pid->Kp * pid->err +
pid->Ki * pid->integral +
pid->Kd * (pid->err pid->err_last);
pid->err_last = pid->err;

// 输出限幅
if (out > pid->out_max) out = pid->out_max;
if (out < pid->out_min) out = pid->out_min;
return out;
}

5.5 主循环

int main(void)
{
Motor_Init();
Tracking_Init();
Encoder_Init();
uart_AInit();

PID_TypeDef pid = {
.target = 0, // 目标偏差 0(居中)
.Kp = 15.0f, .Ki = 0.2f, .Kd = 3.0f,
.out_min = 99, .out_max = 99
};
const int8_t BASE_SPEED = 50;

while (1)
{
// 1. 读取位置偏差
int8_t err = Tracking_GetError();

// 2. PID 计算转向修正量
float steer = PID_Calc(&pid, (float)err);

// 3. 差速控制
int8_t left = BASE_SPEED (int8_t)steer;
int8_t right = BASE_SPEED + (int8_t)steer;

// 4. 限幅并输出
if (left > 99) left = 99; if (left < 99) left = 99;
if (right > 99) right = 99; if (right < 99) right = 99;

Motor_SetSpeed(left, right);
Delay(10); // 10ms 控制周期
}
}

6. HAL 库代码实现

6.1 红外读取(与标准库逻辑相同,使用 HAL API)

// tracking_hal.c
uint8_t Tracking_Read(void)
{
uint8_t status = 0;
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == GPIO_PIN_RESET)
status |= 0x04; // 左
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_5) == GPIO_PIN_RESET)
status |= 0x02; // 中
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET)
status |= 0x01; // 右
return status;
}

6.2 电机控制(基于 CubeMX 生成的 htim4)

// motor_hal.c
void Motor_SetSpeed(int8_t left, int8_t right)
{
if (left >= 0) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET);
} else {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET);
}
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, abs(left));

if (right >= 0) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET);
} else {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET);
}
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, abs(right));
}

6.3 编码器测速

// encoder_hal.c
void Encoder_Init(void)
{
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}

int16_t Encoder_ReadAndClear(void)
{
int16_t cnt = (int16_t)__HAL_TIM_GET_COUNTER(&htim3);
__HAL_TIM_SET_COUNTER(&htim3, 0);
return cnt;
}

6.4 主循环(HAL)

int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
MX_TIM3_Init();
MX_TIM4_Init();
MX_USART1_UART_Init();

HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_4);
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);

PID_TypeDef pid = {0};
pid.target = 0;
pid.Kp = 15.0f; pid.Ki = 0.2f; pid.Kd = 3.0f;
pid.out_min = 99; pid.out_max = 99;
const int8_t BASE_SPEED = 50;
uint32_t last_tick = 0;

while (1)
{
if (HAL_GetTick() last_tick >= 10) // 10ms 控制周期
{
last_tick = HAL_GetTick();
int8_t err = Tracking_GetError();
float steer = PID_Calc(&pid, (float)err);
int8_t left = BASE_SPEED (int8_t)steer;
int8_t right = BASE_SPEED + (int8_t)steer;
Motor_SetSpeed(left, right);
}
}
}

7. PID 参数整定方法

7.1 三步整定法

  • 先调 P:Ki=0, Kd=0,逐步增大 Kp 直到系统出现小幅等幅振荡
  • 再加 D:取振荡周期一半作为基准,加入 Kd 抑制振荡
  • 最后加 I:消除静态误差,Ki 从小值开始(典型 0.01~0.5)
  • 7.2 典型参数范围(智能小车循迹)

    参数作用典型值
    Kp 响应速度 10~30
    Ki 消除稳态误差 0.1~0.5
    Kd 抑制振荡 2~8

    7.3 工程经验

    • 采样周期 10-20ms,太短引入噪声,太长响应迟钝
    • 必须做积分限幅防止积分饱和
    • PWM 频率 10kHz 时电机运行最平稳
    • 编码器 100ms 读取一次计数并清零,避免溢出

    7.4 PID 控制框图

    #mermaid-svg-cuLnOrhLBLIH6iS1{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cuLnOrhLBLIH6iS1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cuLnOrhLBLIH6iS1 .error-icon{fill:#552222;}#mermaid-svg-cuLnOrhLBLIH6iS1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cuLnOrhLBLIH6iS1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cuLnOrhLBLIH6iS1 .marker.cross{stroke:#333333;}#mermaid-svg-cuLnOrhLBLIH6iS1 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cuLnOrhLBLIH6iS1 p{margin:0;}#mermaid-svg-cuLnOrhLBLIH6iS1 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-cuLnOrhLBLIH6iS1 .cluster-label text{fill:#333;}#mermaid-svg-cuLnOrhLBLIH6iS1 .cluster-label span{color:#333;}#mermaid-svg-cuLnOrhLBLIH6iS1 .cluster-label span p{background-color:transparent;}#mermaid-svg-cuLnOrhLBLIH6iS1 .label text,#mermaid-svg-cuLnOrhLBLIH6iS1 span{fill:#333;color:#333;}#mermaid-svg-cuLnOrhLBLIH6iS1 .node rect,#mermaid-svg-cuLnOrhLBLIH6iS1 .node circle,#mermaid-svg-cuLnOrhLBLIH6iS1 .node ellipse,#mermaid-svg-cuLnOrhLBLIH6iS1 .node polygon,#mermaid-svg-cuLnOrhLBLIH6iS1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cuLnOrhLBLIH6iS1 .rough-node .label text,#mermaid-svg-cuLnOrhLBLIH6iS1 .node .label text,#mermaid-svg-cuLnOrhLBLIH6iS1 .image-shape .label,#mermaid-svg-cuLnOrhLBLIH6iS1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cuLnOrhLBLIH6iS1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cuLnOrhLBLIH6iS1 .rough-node .label,#mermaid-svg-cuLnOrhLBLIH6iS1 .node .label,#mermaid-svg-cuLnOrhLBLIH6iS1 .image-shape .label,#mermaid-svg-cuLnOrhLBLIH6iS1 .icon-shape .label{text-align:center;}#mermaid-svg-cuLnOrhLBLIH6iS1 .node.clickable{cursor:pointer;}#mermaid-svg-cuLnOrhLBLIH6iS1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cuLnOrhLBLIH6iS1 .arrowheadPath{fill:#333333;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cuLnOrhLBLIH6iS1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cuLnOrhLBLIH6iS1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cuLnOrhLBLIH6iS1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cuLnOrhLBLIH6iS1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cuLnOrhLBLIH6iS1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cuLnOrhLBLIH6iS1 .cluster text{fill:#333;}#mermaid-svg-cuLnOrhLBLIH6iS1 .cluster span{color:#333;}#mermaid-svg-cuLnOrhLBLIH6iS1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cuLnOrhLBLIH6iS1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cuLnOrhLBLIH6iS1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cuLnOrhLBLIH6iS1 .icon-shape,#mermaid-svg-cuLnOrhLBLIH6iS1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cuLnOrhLBLIH6iS1 .icon-shape p,#mermaid-svg-cuLnOrhLBLIH6iS1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cuLnOrhLBLIH6iS1 .icon-shape .label rect,#mermaid-svg-cuLnOrhLBLIH6iS1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cuLnOrhLBLIH6iS1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cuLnOrhLBLIH6iS1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cuLnOrhLBLIH6iS1 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    反馈

    目标速度/位置

    误差

    PID 控制器

    PWM

    电机

    编码器

    图11 PID 控制框图。阶跃响应曲线:P 控制震荡、PD 控制收敛、PID 控制无静差三种对比,可用串口绘图工具观察 Kp 从小到大的响应变化。

    8. 调试与常见问题

    8.1 上电调试顺序

  • 电机测试:单独测试左右电机正反转,确认接线无误
  • PWM 输出测试:示波器观察 PB8/PB9 波形,确认频率 10kHz
  • 编码器测试:串口打印计数变化,正转递增、反转递减
  • 红外传感器校准:逐个遮挡,确认电平变化符合预期
  • 基础循迹:先跑状态机逻辑(非 PID),确认能基本跟线
  • PID 优化:开启 PID,逐步微调参数
  • 8.2 常见问题排查

    现象原因解决
    电机只啸叫不转 PWM 频率过低(用户原 100Hz) PSC 改为 71,频率升至 10kHz
    小车蛇形走位 纯逻辑控制,无平滑过渡 引入 PID 差速
    直线跑偏 左右电机参数不一致 编码器闭环或单独标定
    传感器误判 环境红外干扰/阈值漂移 调节电位器或改用 38kHz 调制
    电源跌落导致复位 电机与 MCU 共电源 电机独立供电,VC+GND 并联 100μF 电容
    高速时计数异常 编码器溢出 开启溢出中断维护 32 位扩展计数

    8.3 调试流程图

    #mermaid-svg-3vBeu2RGRoaYmRde{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3vBeu2RGRoaYmRde .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3vBeu2RGRoaYmRde .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3vBeu2RGRoaYmRde .error-icon{fill:#552222;}#mermaid-svg-3vBeu2RGRoaYmRde .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3vBeu2RGRoaYmRde .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3vBeu2RGRoaYmRde .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3vBeu2RGRoaYmRde .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3vBeu2RGRoaYmRde .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3vBeu2RGRoaYmRde .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3vBeu2RGRoaYmRde .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3vBeu2RGRoaYmRde .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3vBeu2RGRoaYmRde .marker.cross{stroke:#333333;}#mermaid-svg-3vBeu2RGRoaYmRde svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3vBeu2RGRoaYmRde p{margin:0;}#mermaid-svg-3vBeu2RGRoaYmRde .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-3vBeu2RGRoaYmRde .cluster-label text{fill:#333;}#mermaid-svg-3vBeu2RGRoaYmRde .cluster-label span{color:#333;}#mermaid-svg-3vBeu2RGRoaYmRde .cluster-label span p{background-color:transparent;}#mermaid-svg-3vBeu2RGRoaYmRde .label text,#mermaid-svg-3vBeu2RGRoaYmRde span{fill:#333;color:#333;}#mermaid-svg-3vBeu2RGRoaYmRde .node rect,#mermaid-svg-3vBeu2RGRoaYmRde .node circle,#mermaid-svg-3vBeu2RGRoaYmRde .node ellipse,#mermaid-svg-3vBeu2RGRoaYmRde .node polygon,#mermaid-svg-3vBeu2RGRoaYmRde .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3vBeu2RGRoaYmRde .rough-node .label text,#mermaid-svg-3vBeu2RGRoaYmRde .node .label text,#mermaid-svg-3vBeu2RGRoaYmRde .image-shape .label,#mermaid-svg-3vBeu2RGRoaYmRde .icon-shape .label{text-anchor:middle;}#mermaid-svg-3vBeu2RGRoaYmRde .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3vBeu2RGRoaYmRde .rough-node .label,#mermaid-svg-3vBeu2RGRoaYmRde .node .label,#mermaid-svg-3vBeu2RGRoaYmRde .image-shape .label,#mermaid-svg-3vBeu2RGRoaYmRde .icon-shape .label{text-align:center;}#mermaid-svg-3vBeu2RGRoaYmRde .node.clickable{cursor:pointer;}#mermaid-svg-3vBeu2RGRoaYmRde .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3vBeu2RGRoaYmRde .arrowheadPath{fill:#333333;}#mermaid-svg-3vBeu2RGRoaYmRde .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3vBeu2RGRoaYmRde .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3vBeu2RGRoaYmRde .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3vBeu2RGRoaYmRde .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3vBeu2RGRoaYmRde .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3vBeu2RGRoaYmRde .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3vBeu2RGRoaYmRde .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3vBeu2RGRoaYmRde .cluster text{fill:#333;}#mermaid-svg-3vBeu2RGRoaYmRde .cluster span{color:#333;}#mermaid-svg-3vBeu2RGRoaYmRde div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3vBeu2RGRoaYmRde .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3vBeu2RGRoaYmRde rect.text{fill:none;stroke-width:0;}#mermaid-svg-3vBeu2RGRoaYmRde .icon-shape,#mermaid-svg-3vBeu2RGRoaYmRde .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3vBeu2RGRoaYmRde .icon-shape p,#mermaid-svg-3vBeu2RGRoaYmRde .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3vBeu2RGRoaYmRde .icon-shape .label rect,#mermaid-svg-3vBeu2RGRoaYmRde .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3vBeu2RGRoaYmRde .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3vBeu2RGRoaYmRde .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3vBeu2RGRoaYmRde :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    电机测试

    PWM 输出测试

    编码器测试

    红外传感器校准

    基础循迹(状态机)

    PID 优化

    图14 调试流程图:从电机测试到 PID 整定的 6 步顺序。示波器截图(10kHz vs 100Hz 波形对比)与电源去耦电路图(100μF 电解 + 0.1μF 陶瓷电容并联)建议用真实截图补充。

    9. 进阶拓展

    9.1 多传感器融合

    • 超声波 + 红外:循迹同时避障
    • 摄像头 + 红外:摄像头做主循迹,红外做兜底

    9.2 速度闭环 + 位置闭环双环控制

    • 外环:位置 PID(红外偏差 → 目标速度差)
    • 内环:速度 PID(编码器反馈 → PWM 占空比)

    9.3 复杂路径识别

    • 十字路口、T 型路口、圆环的处理逻辑
    • 基于状态机的路径决策

    9.4 双环 PID 控制框图

    #mermaid-svg-FBubl78SvJok171a{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FBubl78SvJok171a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FBubl78SvJok171a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FBubl78SvJok171a .error-icon{fill:#552222;}#mermaid-svg-FBubl78SvJok171a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FBubl78SvJok171a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FBubl78SvJok171a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FBubl78SvJok171a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FBubl78SvJok171a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FBubl78SvJok171a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FBubl78SvJok171a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FBubl78SvJok171a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FBubl78SvJok171a .marker.cross{stroke:#333333;}#mermaid-svg-FBubl78SvJok171a svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FBubl78SvJok171a p{margin:0;}#mermaid-svg-FBubl78SvJok171a .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-FBubl78SvJok171a .cluster-label text{fill:#333;}#mermaid-svg-FBubl78SvJok171a .cluster-label span{color:#333;}#mermaid-svg-FBubl78SvJok171a .cluster-label span p{background-color:transparent;}#mermaid-svg-FBubl78SvJok171a .label text,#mermaid-svg-FBubl78SvJok171a span{fill:#333;color:#333;}#mermaid-svg-FBubl78SvJok171a .node rect,#mermaid-svg-FBubl78SvJok171a .node circle,#mermaid-svg-FBubl78SvJok171a .node ellipse,#mermaid-svg-FBubl78SvJok171a .node polygon,#mermaid-svg-FBubl78SvJok171a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FBubl78SvJok171a .rough-node .label text,#mermaid-svg-FBubl78SvJok171a .node .label text,#mermaid-svg-FBubl78SvJok171a .image-shape .label,#mermaid-svg-FBubl78SvJok171a .icon-shape .label{text-anchor:middle;}#mermaid-svg-FBubl78SvJok171a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FBubl78SvJok171a .rough-node .label,#mermaid-svg-FBubl78SvJok171a .node .label,#mermaid-svg-FBubl78SvJok171a .image-shape .label,#mermaid-svg-FBubl78SvJok171a .icon-shape .label{text-align:center;}#mermaid-svg-FBubl78SvJok171a .node.clickable{cursor:pointer;}#mermaid-svg-FBubl78SvJok171a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FBubl78SvJok171a .arrowheadPath{fill:#333333;}#mermaid-svg-FBubl78SvJok171a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FBubl78SvJok171a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FBubl78SvJok171a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FBubl78SvJok171a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FBubl78SvJok171a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FBubl78SvJok171a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FBubl78SvJok171a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FBubl78SvJok171a .cluster text{fill:#333;}#mermaid-svg-FBubl78SvJok171a .cluster span{color:#333;}#mermaid-svg-FBubl78SvJok171a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FBubl78SvJok171a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FBubl78SvJok171a rect.text{fill:none;stroke-width:0;}#mermaid-svg-FBubl78SvJok171a .icon-shape,#mermaid-svg-FBubl78SvJok171a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FBubl78SvJok171a .icon-shape p,#mermaid-svg-FBubl78SvJok171a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FBubl78SvJok171a .icon-shape .label rect,#mermaid-svg-FBubl78SvJok171a .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FBubl78SvJok171a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FBubl78SvJok171a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FBubl78SvJok171a :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    实际速度

    实际位置

    目标位置

    位置误差

    位置 PID外环

    目标速度差

    速度误差

    速度 PID内环

    PWM

    电机

    编码器

    图17 双环 PID 控制框图。复杂路径状态机:直行/左转/右转/十字/圆环的状态迁移,可参考此框图设计。

    10. 关键参数速查表

    项目取值
    PWM 频率 10kHz(PSC=71, ARR=99)
    编码器模式 TI1 and TI2(4 倍频)
    编码器 ARR 65535
    控制周期 10-20ms
    基础速度 50/99(约 50% 占空比)
    传感器数量 3-5 路
    传感器间距 1.5-2cm
    TB6612 工作电压 2.7-13.5V
    电源滤波 100μF + 0.1μF 并联

    11. 总结

    循迹小车看似简单,难点在于机械对称性和电源完整性。电机、轮胎、底盘的微小差异都会导致跑偏,单纯靠算法难以完全弥补。调试时先确保硬件对称,再调软件参数。

    回顾开篇的两个问题:电机只响不转,多半是 PWM 频率太低,把 PSC 从 7199 改成 71 就能解决;小车走蛇形,则是因为纯逻辑控制缺少平滑过渡,引入 PID 差速后轨迹会明显收敛。希望这篇文章能帮你把小车从"能跑"调教到"跑得稳"。



    感谢您的阅读!本号长期深耕电子信息、通信与嵌入式领域的技术分享,如果这篇文章对您有所启发,欢迎点赞、收藏并关注,后续将持续输出更多硬核干货,与您一同成长!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » STM32F103 循迹小车全栈实战:从红外传感到 PID 闭环
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!