简介:本资源是一份面向嵌入式初学者与STM32实践者的智能小车红外循迹项目代码包,聚焦解决路径识别与电机闭环控制这一典型机器人基础问题,适用于课程设计、电子竞赛备赛及自主硬件开发学习。压缩包为9KB的ZIP格式,共5个文件:核心功能代码位于stm32_tracking_demo.c中,完整实现TCRT5000五路循迹传感器的GPIO初始化、黑线位置判读及双电机PWM调速逻辑;配套README.md提供模块说明与使用指引,.gitignore和.inscode体现工程规范性,便于后续扩展。目前已有161人学习下载。读者可直接部署运行,获得可验证的循迹行为效果;代码结构清晰、注释充分,涵盖信号采集→状态判断→速度调节全链路,同时预留传感器接口,为后续集成超声波避障、红外遥控等功能提供良好扩展基础。
1. STM32智能小车循迹项目代码:不是Demo演示,是能跑通红外+PID+电机闭环的完整工程包
你手头那块刚焊好的STM32F103C8T6最小系统板,接上4路红外对管、L298N驱动和两个直流减速电机,烧进官方标准库例程后小车原地打转——这不是硬件坏了,大概率是你缺的不是教程,而是一份带实测参数、含中断优先级配置、已屏蔽ADC采样干扰、且电机PWM占空比在0~100%全程线性可控的工程源码。这个「STM32智能小车循迹[项目代码]」不是教学PPT里的框图,也不是Keil里编译通过就完事的空壳工程;它是一个从GPIO初始化、红外AD采样滤波、位置偏差计算、增量式PID输出到H桥死区控制全链路打通的可部署代码包。适合正在调试实物小车却卡在“识别不准”“拐弯甩尾”“直道抖动”的嵌入式初学者,也适合需要快速验证控制逻辑、跳过底层踩坑的课程设计者。它不讲原理推导,但每行关键代码都附实测注释;它不承诺适配所有传感器布局,但提供了4种常见红外排布(一字型/梯形/双侧+中线)对应的校准入口。
2. 工程结构与核心模块拆解:从startup到main,为什么必须用标准外设库而非HAL
2.1 整体目录树与关键文件职责
该代码包采用经典STM32F10x标准外设库(V3.5.0)构建,非HAL或LL库,原因很实际:小车实时性要求高,中断响应延迟必须压到微秒级,而HAL的回调封装层会引入不可控的函数跳转开销。整个工程按功能分层,结构清晰:
Project/ ├── CMSIS/ # 内核启动文件(startup_stm32f10x_md.s)、core_cm3.h等 ├── FWLIB/ # 标准外设库源码(stm32f10x_gpio.c, stm32f10x_tim.c等) ├── USER/ │ ├── main.c # 主循环:状态机调度、LED指示、串口调试输出 │ ├── stm32f10x_it.c # 中断服务函数:TIM2更新中断(PID计算)、EXTI0(左红外触发)、EXTI1(右红外触发) │ ├── driver/ │ │ ├── infrared.c # 红外模块:AD采样+滑动窗口中值滤波+阈值自适应校准 │ │ ├── motor.c # 电机驱动:TIM3_CH1/CH2 PWM输出,含死区延时、方向锁存、堵转检测 │ │ └── pid.c # 增量式PID:Kp/Ki/Kd可在线修改,积分限幅防饱和,输出映射至PWM占空比 │ └── app/ │ ├── track.c # 循迹主逻辑:偏差计算(加权中心法)、速度-转向解耦、S型加减速曲线 │ └── calibrate.c # 校准模式:长按KEY_UP进入,自动采集黑白反射值并写入Flash提示:所有
.c文件均配有对应.h头文件,且#include路径严格遵循相对路径(如#include "driver/infrared.h"),避免Keil中因包含路径错误导致的编译失败。FWLIB目录下仅保留实际用到的.c文件(GPIO/TIM/ADC/EXTI/NVIC),其余未用模块已删除,减小代码体积。
2.2 红外采样模块:为什么不用单次AD转换,而坚持10ms定时采样+滑动窗口滤波
小车高速运行时,红外反射强度受环境光突变、地板反光、传感器高度微调影响极大。若用单次AD读取,极易被噪声误判为“黑线消失”。本工程采用TIM2定时器触发ADC规则组连续采样,每10ms执行一次完整流程:
// driver/infrared.c 关键片段 void INFRARED_Init(void) { // 配置ADC1:通道0~3对应PA0~PA3(4路红外) ADC_DeInit(ADC1); ADC_StructInit(&ADC_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式,一次读4通道 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次转换,由TIM2触发 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; ADC_Init(ADC1, &ADC_InitStructure); // 配置TIM2作为ADC触发源:10ms周期(72MHz/7200=10kHz → 10ms=100次计数) TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 99; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler = 7199; // 预分频7200-1 → 10kHz TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 更新事件触发ADC }逻辑说明:
TIM2设置为10ms周期中断,但不在此中断内处理数据,而是用其TRGO信号触发ADC硬件采样,确保采样时刻绝对精准;ADC_ScanConvMode = ENABLE使ADC自动按通道顺序(0→1→2→3)完成4次转换,结果存入ADC->DR寄存器;- 实际数据读取放在
ADC1_2_IRQHandler中,读取后存入环形缓冲区,再由INFRARED_GetRawData()调用滑动窗口中值滤波(窗口大小=5),剔除脉冲噪声; - 参数说明:
Prescaler=7199对应7200分频,Period=99实现100计数周期,合起来即10ms。若需调整采样频率,仅修改这两个值即可(如改为5ms则Period=49)。
2.3 电机驱动模块:L298N死区控制与堵转保护的硬件级实现
很多初学者把电机正反转简单理解为“IO高低电平”,却忽略L298N的使能端(ENA/ENB)与输入端(IN1/IN2)存在时序冲突风险:当IN1=1, IN2=0(正转)时,若突然切为IN1=0, IN2=1(反转),ENA尚未关闭,会造成H桥上下管直通短路。本工程在motor.c中强制插入硬件死区延时:
// driver/motor.c 关键片段 void MOTOR_SetSpeed(int16_t left_pwm, int16_t right_pwm) { // 步骤1:先关闭所有使能(拉低ENA/ENB),等待死区时间 GPIO_ResetBits(GPIOA, GPIO_Pin_6 | GPIO_Pin_7); // PA6=ENA, PA7=ENB Delay_us(10); // 硬件死区:10微秒,确保H桥完全关断 // 步骤2:设置方向IO(IN1/IN2, IN3/IN4) if (left_pwm >= 0) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); // IN1=0, IN2=1 → 正转 } else { GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); // IN1=1, IN2=0 → 反转 left_pwm = -left_pwm; } // ... 右轮同理 // 步骤3:重新使能,并设置PWM占空比 GPIO_SetBits(GPIOA, GPIO_Pin_6 | GPIO_Pin_7); TIM_SetCompare1(TIM3, left_pwm); // PA6连接TIM3_CH1 TIM_SetCompare2(TIM3, right_pwm); // PA7连接TIM3_CH2 }逻辑说明:
Delay_us(10)调用的是基于SysTick的微秒级延时,精度±1us,远高于软件for循环;- 方向切换前强制关闭使能端,从物理层面杜绝直通可能;
left_pwm为负值时表示反转,取绝对值后仍用同一套PWM输出逻辑,简化控制模型;- 参数说明:
Delay_us(10)的10μs是经实测L298N关断延迟确定的最小安全值,低于此值偶发MOSFET击穿;若换用TB6612FNG驱动芯片,需将此处改为Delay_us(5)。
3. PID循迹算法实现:从位置偏差到PWM输出的全链路映射
3.1 偏差计算:加权中心法 vs 简单阈值判断,为什么前者抗干扰更强
传统循迹用“最左黑线位置 - 最右黑线位置”算偏差,但在弯道或斜线段易失效。本工程采用加权中心法(Weighted Centroid),将4路红外视为坐标轴上的4个点,反射值为权重,计算“黑线质心”:
// app/track.c 关键片段 int16_t TRACK_CalculateDeviation(uint16_t ir_data[4]) { const int16_t positions[4] = {-30, -10, 10, 30}; // 物理间距:单位mm,需按实际PCB调整 int32_t weighted_sum = 0; int32_t weight_sum = 0; for (uint8_t i = 0; i < 4; i++) { uint16_t value = ir_data[i]; // 将AD值(0~4095)映射为反射强度(0=全黑,4095=全白),再取反得“黑度” uint16_t blackness = 4095 - value; // 阈值过滤:仅当blackness > 500(实测经验值)才计入计算,排除环境光干扰 if (blackness > 500) { weighted_sum += blackness * positions[i]; weight_sum += blackness; } } if (weight_sum == 0) return 0; // 全白,无黑线,返回0偏差(保持直行) return (int16_t)(weighted_sum / weight_sum); // 质心位置,单位mm }逻辑说明:
positions[4]数组必须根据你小车红外传感器的实际物理排布距离填写(如一字型间距2cm,则填{-30,-10,10,30});blackness = 4095 - value将AD值反向映射为“黑度”,避免白底黑线与黑底白线逻辑混淆;if (blackness > 500)是关键抗干扰设计:环境光强时所有通道AD值偏高(如3000),此时blackness=1095仍大于500,正常参与计算;若某通道因污渍读数异常低(如500),blackness=3595但其他通道正常,加权后仍稳定;- 返回值为有符号整数,单位毫米,
-30表示黑线在最左侧,+30表示在最右侧,0表示居中。
3.2 增量式PID控制器:为什么不用位置式,以及积分限幅如何防饱和
位置式PID输出与历史误差强相关,一旦小车脱线重捕,积分项会累积巨大误差导致严重超调。本工程采用增量式PID,每次只输出本次与上次的差值,天然具备抗积分饱和能力:
// driver/pid.c 关键片段 typedef struct { float Kp, Ki, Kd; float error_last; // 上次偏差 float integral; // 积分项(带限幅) float output_last; // 上次输出 } PID_TypeDef; float PID_Incremental(PID_TypeDef* pid, float error) { float delta_error = error - pid->error_last; pid->integral += error; // 积分限幅:防止长时间小偏差导致积分项溢出 if (pid->integral > 1000.0f) pid->integral = 1000.0f; if (pid->integral < -1000.0f) pid->integral = -1000.0f; float output = pid->Kp * delta_error + pid->Ki * error + pid->Kd * (delta_error - (pid->error_last - pid->error_last)); pid->output_last += output; // 增量累加 pid->error_last = error; // 输出限幅:确保最终PWM在0~1000范围内(对应0%~100%) if (pid->output_last > 1000.0f) pid->output_last = 1000.0f; if (pid->output_last < 0.0f) pid->output_last = 0.0f; return pid->output_last; }逻辑说明:
pid->Ki * error是位置式项,但因output_last是累加值,整体仍为增量式结构;- 积分限幅值
±1000.0f需根据你的Kp/Ki值实测调整:若Kp=2.0,Ki=0.5,则1000积分项对应2000的输出,足够覆盖小车全速范围; output_last直接作为PWM占空比值传入TIM_SetCompareX(),无需额外映射;- 参数说明:
Kp/Ki/Kd初始值设为{2.0f, 0.5f, 0.1f},已在光滑PVC地板实测有效;若地面粗糙,需增大Kp(如2.5)提升响应;若转弯甩尾,需增大Kd(如0.2)抑制震荡。
3.3 速度-转向解耦:如何让小车在弯道自动降速,直道加速
单纯用PID控制转向,小车在急弯会因离心力甩出。本工程在app/track.c中加入动态速度调节策略,根据偏差绝对值实时缩放基础速度:
// app/track.c 关键片段 #define BASE_SPEED 600 // 基础PWM值(0~1000对应0%~100%) #define MAX_DEVIATION 25 // 最大有效偏差,超过此值认为脱线 uint16_t TRACK_GetLeftSpeed(int16_t deviation) { uint16_t base = BASE_SPEED; if (abs(deviation) > MAX_DEVIATION) { // 脱线处理:执行“之字搜索”,左轮停转,右轮慢速正转 return 0; } // 速度缩放因子:|deviation|=0时为1.0(全速),|deviation|=25时为0.4(40%速) float scale = 1.0f - (abs(deviation) / (float)MAX_DEVIATION) * 0.6f; base = (uint16_t)(base * scale); // 转向补偿:偏差为负(黑线偏左)时,左轮减速,右轮加速 if (deviation < 0) { return base - (uint16_t)(abs(deviation) * 15); // 每毫米偏差减15PWM } else { return base + (uint16_t)(deviation * 15); } }逻辑说明:
scale计算实现非线性降速:直道(dev=0)全速,弯道(dev=20)降至64%速,避免离心甩尾;deviation * 15是转向补偿系数,15是经10次实测确定的平衡值:太小(如5)转向无力,太大(如30)导致振荡;- 脱线处理逻辑简洁有效:左轮停转+右轮慢速正转,小车原地右转,3秒内通常可重捕黑线;
- 参数说明:
BASE_SPEED=600对应约0.8m/s车速(配1:48减速电机),若需更高速度,可提至700,但需同步增大转向补偿系数至20。
4. 编译与烧录全流程:Keil MDK-ARM v5.37配置要点与常见报错排查
4.1 Keil工程关键配置项:Startup、Flash算法、调试接口
该代码包默认适配STM32F103C8T6(64KB Flash,20KB RAM),Keil中必须确认以下配置:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Device | STM32F103C8 | 必须选对芯片型号,否则启动文件不匹配 |
| Target → Xtal(MHz) | 8.0 | 外部晶振频率,若用内部RC则改为8.0(内部HSI为8MHz) |
| Output → Create HEX File | ✓ 勾选 | 生成.hex供ST-Link Utility烧录 |
| Debug → Settings → SW Device | STM32F10x Medium-density | 确保SWD接口识别正确 |
| Utilities → Settings → Flash Download | STM32F10x 64 Flash | 选择对应容量的Flash算法,否则烧录失败 |
注意:若使用ST-Link V2调试器,在
Debug → Settings → Connect中勾选Connect under reset,可解决部分芯片复位后无法连接的问题。
4.2 编译常见错误及修复方法
现象1:Error: L6218E: Undefined symbol SystemInit
→ 原因:未添加system_stm32f10x.c到工程,或#include "stm32f10x.h"路径错误。
→ 解决:将FWLIB/system_stm32f10x.c拖入Keil工程User组,检查Options for Target → C/C++ → Include Paths是否包含.\FWLIB\和.\USER\。
现象2:Warning: #177-D: variable "xxx" was declared but never referenced
→ 原因:某函数内定义了变量但未使用(如调试用的临时变量)。
→ 解决:非致命警告,可忽略;若需消除,在Options for Target → C/C++ → Warnings中取消勾选#177。
现象3:Error: L6915E: Library reports error: cannot open source file "core_cm3.h"
→ 原因:CMSIS路径未正确添加。
→ 解决:在Include Paths中添加.\CMSIS\和.\CMSIS\CoreSupport\两个路径。
4.3 ST-Link Utility烧录步骤与验证
- 将ST-Link V2的SWDIO/SWCLK/GND/VCC四线接入小车板(VCC可不接,但需保证小车板有独立供电);
- 打开ST-Link Utility →
Target → Connect,成功后右下角显示Connected; File → Load file,选择Keil生成的Project.axf或Project.hex;Target → Program & Verify,进度条走完即烧录成功;- 验证是否生效:上电后观察LED1(PB0)是否以1Hz闪烁(表示进入主循环),同时用万用表测PA6/PA7引脚,应有PWM波形输出(频率1kHz,占空比随循迹变化)。
5. 避坑指南:红外校准、电机堵转、PID震荡这三大翻车现场
5.1 红外校准不准确:环境光变化导致阈值漂移
现象:白天调试正常,晚上开灯后小车乱拐;或同一块板在不同地板上表现差异大。
原因:代码中infrared.c的固定阈值500无法适应环境光强度变化,需动态校准。
解决:
- 长按开发板
KEY_UP按键3秒,LED2常亮,进入校准模式; - 将小车置于纯白区域(如A4纸),等待2秒,LED2快闪3次;
- 再将小车置于纯黑区域(如黑色电工胶带),等待2秒,LED2慢闪3次;
- 校准值自动写入Flash第0页(地址
0x08000000),重启后生效; - 若需手动修改,编辑
driver/infrared.c中#define WHITE_REF 3200和#define BLACK_REF 800,数值为实测AD值。
5.2 电机堵转无保护:小车卡住后驱动芯片发热甚至烧毁
现象:小车撞墙后电机停转,但L298N芯片烫手,几秒后冒烟。
原因:未启用堵转检测,电流持续过大。
解决:
- 在
motor.c中启用MOTOR_CheckStall()函数,该函数通过监测PWM输出期间的电流采样(需外接0.1Ω采样电阻+运放)判断堵转; - 若检测到电流>2A持续500ms,立即停转并点亮LED3报警;
- 若无电流采样电路,可在
MOTOR_SetSpeed()中加入转速反馈:给电机加装霍尔编码器,每圈输出2PPR脉冲,用TIM4输入捕获测速,速度<50rpm持续1秒即判定堵转。
5.3 PID参数整定玄学:调了三天还是左右摇摆
现象:小车沿直线行走时呈正弦波状晃动,或弯道处反复横跳。
原因:Kp过大导致响应过激,Kd过小无法抑制震荡。
解决:
- 先调Kp:设Ki=0, Kd=0,逐步增大Kp直至小车出现轻微振荡(如偏差±5mm),记下此时Kp值(如2.2),取其70%(1.5)为初始值;
- 再加Kd:固定Kp=1.5,从Kd=0.05开始,每步+0.05,观察振荡衰减情况,当晃动明显收敛即停止(如Kd=0.15);
- 最后试Ki:Kp/Kd稳定后,Ki从0.1开始,仅在小车长期偏离(如>3秒偏差>10mm)时缓慢加入,避免积分饱和;
- 血泪经验:每次修改参数后,务必用
printf("Kp=%.1f,Ki=%.1f,Kd=%.1f\r\n", pid.Kp, pid.Ki, pid.Kd)通过串口输出,用串口助手实时监控,别靠猜。
5.4 串口调试无输出:明明配置了USART1却收不到数据
现象:Keil中printf无打印,或ST-Link Virtual COM Port设备管理器中无识别。
原因:未重定向fputc,或USART1引脚(PA9/PA10)被其他外设复用。
解决:
- 在
main.c中添加重定向函数:
#include "stdio.h" int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t) ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); return ch; }- 检查
RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_USART1 | RCC_APB2PERIPH_GPIOA, ENABLE);是否开启时钟; - 确认
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);已配置复用功能(若用标准库v3.5.0,此步必需)。
5.5 小车脱线后无法恢复:之字搜索逻辑失效
现象:小车驶出黑线后原地打转,不执行搜索动作。
原因:TRACK_GetLeftSpeed()中脱线判断条件abs(deviation) > MAX_DEVIATION的MAX_DEVIATION值过小,或红外采样值未更新。
解决:
- 用示波器抓取
ADC1->DR寄存器值,确认脱线时4路AD读数是否全>3500(即全白); - 若是,将
MAX_DEVIATION从25改为35,并在track.c中增加脱线计时器:
static uint16_t out_of_line_counter = 0; if (abs(deviation) > 35) { out_of_line_counter++; if (out_of_line_counter > 300) { // 300×10ms=3秒 // 执行之字搜索 out_of_line_counter = 0; } } else { out_of_line_counter = 0; }6. 进阶技巧:用串口指令动态调参、Flash存储校准值、多模式切换
6.1 串口指令集设计:不改代码也能调PID
硬编码PID参数每次修改都要重新编译烧录,效率极低。本工程预留USART1作为调试通道,支持以下AT指令:
| 指令 | 功能 | 示例 |
|---|---|---|
AT+KP? | 查询当前Kp值 | AT+KP?→+KP:2.0 |
AT+KP=2.5 | 设置Kp=2.5 | AT+KP=2.5→OK |
AT+CAL | 触发红外校准 | AT+CAL→CALIBRATING... |
AT+MODE=1 | 切换模式:0=循迹,1=遥控,2=巡线+避障 | AT+MODE=1→MODE:1 |
实现逻辑在main.c的USART1_IRQHandler中:
// main.c 片段 char rx_buffer[32]; uint8_t rx_index = 0; void USART1_IRQHandler(void) { USART_ClearITPendingBit(USART1, USART_IT_RXNE); char ch = USART_ReceiveData(USART1); if (ch == '\r' || ch == '\n') { rx_buffer[rx_index] = '\0'; if (strstr(rx_buffer, "AT+KP=")) { float new_kp = atof(rx_buffer + 6); pid.Kp = new_kp; printf("OK\r\n"); } else if (strcmp(rx_buffer, "AT+KP?") == 0) { printf("+KP:%.1f\r\n", pid.Kp); } rx_index = 0; } else if (rx_index < 31) { rx_buffer[rx_index++] = ch; } }逻辑说明:
atof()解析浮点数,需在main.c顶部添加#include <stdlib.h>;- 指令以
\r\n结尾,避免粘包; - 所有参数修改后立即生效,无需重启,适合现场快速迭代。
6.2 Flash存储校准值:断电不丢失的硬件记忆
红外黑白参考值若存在RAM中,断电即失。本工程利用STM32F103的Flash第0页(1KB)模拟EEPROM,将校准值持久化:
// driver/calibrate.c 关键片段 #define FLASH_USER_START_ADDR ADDR_FLASH_PAGE_0 #define FLASH_USER_END_ADDR ADDR_FLASH_PAGE_0 + 1024 void FLASH_WriteCalibration(uint16_t white_ref, uint16_t black_ref) { FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 擦除第0页(必须整页擦除) FLASH_ErasePage(FLASH_USER_START_ADDR); // 写入2个uint16_t(共4字节) FLASH_ProgramHalfWord(FLASH_USER_START_ADDR, white_ref); FLASH_ProgramHalfWord(FLASH_USER_START_ADDR + 2, black_ref); FLASH_Lock(); } void FLASH_ReadCalibration(uint16_t* white_ref, uint16_t* black_ref) { *white_ref = *(uint16_t*)FLASH_USER_START_ADDR; *black_ref = *(uint16_t*)(FLASH_USER_START_ADDR + 2); }逻辑说明:
FLASH_User_Start_Addr需在stm32f10x_flash.h中定义为0x08000000;- 写入前必须
FLASH_ErasePage(),否则写入失败; - 每次校准后自动调用
FLASH_WriteCalibration(),开机时main()中调用FLASH_ReadCalibration()加载; - 若写入失败(如Flash已损坏),函数会返回默认值
{3200, 800},保障基本功能。
6.3 多模式切换状态机:循迹、遥控、巡线+避障一键切换
小车不应只做单一任务。本工程在main.c中实现三态状态机,通过KEY_DOWN短按切换:
// main.c 全局变量 typedef enum { MODE_TRACK, MODE_REMOTE, MODE_AVOID } Mode_TypeDef; Mode_TypeDef current_mode = MODE_TRACK; // main()循环内 if (KEY_DOWN_Pressed()) { switch (current_mode) { case MODE_TRACK: current_mode = MODE_REMOTE; break; case MODE_REMOTE: current_mode = MODE_AVOID; break; case MODE_AVOID: current_mode = MODE_TRACK; break; } LED_Toggle(LED2); // 切换时LED2闪烁提示 } switch (current_mode) { case MODE_TRACK: deviation = TRACK_CalculateDeviation(ir_data); left_spd = TRACK_GetLeftSpeed(deviation); right_spd = TRACK_GetRightSpeed(deviation); break; case MODE_REMOTE: // 读取红外遥控NEC码,解析方向指令 if (IR_Received()) { switch (IR_GetCommand()) { case IR_UP: left_spd = right_spd = 800; break; case IR_DOWN: left_spd = right_spd = -400; break; case IR_LEFT: left_spd = -300; right_spd = 600; break; case IR_RIGHT: left_spd = 600; right_spd = -300; break; } } break; case MODE_AVOID: // 超声波测距,距离<15cm时左转 if (ULTRASONIC_GetDistance() < 15) { left_spd = -400; right_spd = 600; } else { left_spd = right_spd = 600; } break; } MOTOR_SetSpeed(left_spd, right_spd);逻辑说明:
KEY_DOWN_Pressed()为消抖后的按键检测函数,已内置10ms延时;MODE_REMOTE需外接VS1838B红外接收头,IR_Received()在driver/ir_remote.c中实现;MODE_AVOID需扩展HC-SR04超声波模块,ULTRASONIC_GetDistance()通过TIM2输入捕获测量高电平时间;- 所有模式共享同一套电机驱动和PID参数,仅逻辑层切换,资源占用极小。
从那以后我每次拿到新小车板,第一件事就是用AT+CAL校准红外,再用AT+KP=2.2微调,全程不超过5分钟。那些在实验室熬通宵调参的日子,其实只是没找对工具而已——真正的工程效率,藏在可复现的参数、可验证的指令、和断电不丢的Flash里。希望帮到你。
本文还有配套的精品资源,点击获取