目录
- 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 三步整定法
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 上电调试顺序
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 差速后轨迹会明显收敛。希望这篇文章能帮你把小车从"能跑"调教到"跑得稳"。
感谢您的阅读!本号长期深耕电子信息、通信与嵌入式领域的技术分享,如果这篇文章对您有所启发,欢迎点赞、收藏并关注,后续将持续输出更多硬核干货,与您一同成长!
网硕互联帮助中心




评论前必须登录!
注册