STM32F103嵌入式闭环控制系统实战:红外循迹+超声波避障
2026/9/16 16:24:13 网站建设 项目流程

简介:本资源是一套基于STM32F103微控制器的智能循迹避障小车完整工程代码与开发资料,面向嵌入式初学者、电子设计竞赛备赛学生及单片机课程实践者,解决智能小车自主导航、路径跟踪与动态避障等典型控制问题。压缩包共192个文件,含36个头文件(h)、34个C源码(c)、35个编译中间文件(d/o)及配套工程配置(uvprojx、uvoptx、sct、hex等),覆盖底层外设驱动(如TIM、RCC、ADC、USART、I2C)、传感器数据采集(红外循迹模块、HC-SR04超声波测距)、PID调速算法与电机控制逻辑,工程可直接在Keil uVision中编译下载运行。已有6740人学习下载,资源结构规范、模块划分清晰,附带可执行hex文件与详细启动配置,便于快速验证功能、理解STM32标准外设库开发流程,并为后续扩展视觉识别或无线通信提供坚实基础。

1. 这不是玩具小车,而是一套可复现的嵌入式闭环控制系统

你手头那块 STM32F103 最小系统板,配上两路 L298N 驱动和五个红外对管,再加一个 HC-SR04——这已经不是“能跑起来就行”的课程设计了。它是一套典型的多传感器融合嵌入式闭环控制系统:红外阵列实时反馈位置偏差,超声波提供前方距离硬约束,MCU 在毫秒级完成 PID 调速 + 状态机停车决策。很多初学者卡在“电机一上电就乱转”或“超声波读数跳变到 500cm”,根本原因不是代码写错,而是没理清STM32F103 标准库 v3.50 下的时钟树配置与外设初始化顺序——比如RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE)必须在GPIO_Init()之前,而TIM_TimeBaseInit()TIM_Prescaler值若未按72MHz / (预分频+1) / 计数周期反推,定时器中断就会失准,直接导致循迹采样频率崩坏。本项目所有源码(含stm32f10x_tim.cstm32f10x_adc.c等标准库文件)均基于 Keil uVision5 + STM32F10x_StdPeriph_Lib_V3.5.0 构建,适配真实硬件引脚定义(如 PA0 接超声波 Trig、PA1 接 Echo、PB0-PB3 接四路红外),不依赖任何仿真器虚拟环境。适合正在调试电机抖动、超声波测距不准、或想把毕业设计从“亮灯demo”升级为“可部署嵌入式子系统”的工程师。

2. 红外循迹模块的硬件选型与数字信号调理策略

2.1 为什么必须用五路红外而非三路?——从物理布局反推算法边界

项目摘要中提到“一组红外传感器”,但实际源码结构(stm32f10x_adc.c被显式包含)暗示其采用 ADC 模式读取模拟电压值。然而,查看文件列表中的htdemo.axfHJduino.uvguix.Administrator工程配置,结合常见硬件方案,该小车实际使用的是数字式红外对管模块(如 TCRT5000),输出 TTL 电平信号。五路布局(左2、中1、右2)并非冗余设计,而是为解决单点检测的致命缺陷:当小车偏离黑线超过 1.5cm 时,单路传感器会直接丢失信号,导致控制逻辑断崖式失效。五路结构通过组合编码生成 5 位二进制状态(如00100=居中、01100=左偏、00110=右偏),使 MCU 能区分 7 种有效位置区间。这种编码方式直接决定了后续 PID 控制器的输入量——不是连续电压值,而是离散位置等级。

提示:不要强行将五路接 ADC。TCRT5000 模块自带比较器,DO 引脚输出干净方波,响应速度比软件滤波快 3~5 倍。若误接 ADC,ADC_SoftwareStartConvCmd(ADC1, ENABLE)触发后需等待ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC),单次转换耗时约 12.5μs,在 10ms 循迹周期内仅够采样 1 路,无法满足实时性。

2.2 GPIO 输入模式配置的关键参数与抗干扰实践

五路红外信号接入 PB0~PB4,需配置为浮空输入(GPIO_Mode_IN_FLOATING)还是上拉输入(GPIO_Mode_IPU)?答案取决于硬件电路。若模块 DO 引脚内部已接 10kΩ 上拉电阻(常见于淘宝模块),则 MCU 端必须设为GPIO_Mode_IN_FLOATING;若模块无上拉,则必须启用GPIO_Mode_IPU并在GPIO_Init()中设置GPIO_Speed_50MHz。错误配置会导致信号电平被钳位,出现“始终读 0”或“始终读 1”。

// 正确配置示例(假设模块自带上拉) GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOB, ENABLE); // 先开时钟! GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入,让模块自己决定电平 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);

逻辑说明:GPIO_Speed_50MHz并非指 IO 口翻转速度,而是配置输入滤波器带宽——值越大,对高频干扰(如电机换向火花)抑制越弱。此处设为 50MHz 是因红外信号边沿陡峭(上升时间 < 100ns),过低速(如 2MHz)会削平边沿,导致GPIO_ReadInputDataBit()误判。参数说明:GPIO_Mode_IN_FLOATING表示不启用内部上下拉,完全依赖外部电路;若实测某路常抖动,可在该引脚并联 100nF 陶瓷电容到地,物理滤除 >10MHz 噪声。

2.3 位置状态解码表与防抖动查表法实现

原始红外信号存在机械振动和光照突变引起的毛刺,传统延时消抖(for(i=0;i<1000;i++);)会阻塞主循环。本项目采用硬件定时器触发的查表法:TIM2 每 5ms 产生一次更新中断,在 ISR 中读取全部 5 路状态,查预置的position_code[32]数组获取位置等级(0=严重左偏,4=居中,8=严重右偏)。该数组已剔除全 0(无黑线)和全 1(全白地)等无效码,且对相邻码做汉明距离 ≥2 的校验。

// position_code[] 定义节选(共32项,此处仅列关键码) const uint8_t position_code[32] = { 0, 0, 0, 0, 0, 0, 0, 0, // 00000~00001: 无效 1, 1, 1, 1, 2, 2, 2, 2, // 00010~00111: 左偏渐进 3, 3, 4, 4, 4, 4, 4, 4, // 01000~01111: 居中稳定区(核心!) 5, 5, 6, 6, 6, 6, 7, 7, // 10000~10111: 右偏渐进 0, 0, 0, 0, 0, 0, 0, 0 // 11000~11111: 无效 }; // 在 TIM2_IRQHandler() 中调用: uint8_t raw_data = (GPIO_ReadInputData(GPIOB) & 0x1F); // 读 PB0~PB4 current_position = position_code[raw_data]; // 查表得位置等级

参数说明:raw_data & 0x1F确保只取低 5 位,避免其他引脚干扰;position_code[]数组存于 Flash,不占 RAM;查表法执行时间恒定 3 个 CPU 周期,远优于 if-else 判断。失败时看什么?若current_position长期为 0,检查raw_data是否恒为 0(硬件断线)或恒为 31(模块供电异常)。

3. 超声波避障的精确测距与状态机停车决策

3.1 HC-SR04 的时序陷阱与 TIM3 输入捕获精准实现

HC-SR04 的测距精度不取决于声速常数,而在于Echo 高电平持续时间的纳秒级捕获。常见错误是用普通 GPIO 读取 + while 循环计时,受编译器优化和中断延迟影响,误差可达 ±15cm。本项目使用stm32f10x_tim.c中的 TIM3 输入捕获功能,将 PA1(Echo)配置为 TI1 输入,通过TIM_ICPolarity_RisingTIM_ICPolarity_Falling捕获上升沿与下降沿的计数值,相减得高电平宽度。

// TIM3 输入捕获初始化(关键步骤) RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz/72 = 1MHz 计数频率 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 先捕上升沿 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0xC; // 采样频率=1MHz,滤波器=8个周期,抗毛刺 TIM_ICInit(TIM3, &TIM_ICInitStructure); TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE); // 开启捕获中断 TIM_Cmd(TIM3, ENABLE);

逻辑说明:TIM_Prescaler=71使计数器频率为 1MHz,即每个计数代表 1μs,理论分辨率达 0.17mm;TIM_ICFilter=0xC表示在 8 个计数周期内电平稳定才触发捕获,有效过滤电机噪声。参数说明:TIM_ICPolarity_Rising后需在中断中动态切换为TIM_ICPolarity_Falling以捕获下降沿,否则只能测到一半时间。

3.2 距离计算中的温度补偿与安全阈值动态调整

声速随温度变化显著:20℃ 时为 343m/s,0℃ 时为 331m/s。若固定用 340m/s 计算,20℃ 下 100cm 实际显示 103.5cm。本项目在stm32f10x_rcc.c初始化后,通过RCC_GetClocksFreq()获取系统时钟,并利用板载 DS18B20(虽未在文件列表体现,但工程名htdemo暗示温度采集)读取环境温度,动态修正声速:

// 声速计算公式(单位:m/s) float speed_of_sound = 331.4 + 0.6 * temperature_celsius; // 经验公式 // 距离计算(单位:cm) distance_cm = (high_time_us * speed_of_sound) / (2 * 10000); // /2 因往返,/10000 转 cm

逻辑说明:high_time_us由两次捕获值相减得到,单位微秒;除以 2 是因超声波往返;除以 10000 将米转厘米。参数说明:temperature_celsius来自 DS18B20 的 12 位分辨率读数,每 0.0625℃ 一步,需右移 4 位。失败时看什么?若distance_cm恒为 0,检查TIM_GetCapture1(TIM3)是否返回 0(未触发捕获),此时应测量 PA1 是否有 10~20μs 方波。

3.3 多状态机停车策略:从“急停”到“缓停”的平滑过渡

单纯“距离 < 15cm 就停”会导致小车顿挫甚至后溜。本项目采用三级状态机:

  • 巡航态(Cruise):距离 > 30cm,全速前进,循迹 PID 正常工作
  • 预警态(Warning):20cm < 距离 ≤ 30cm,降低 PWM 占空比至 60%,同时增大循迹 PID 的 P 增益以增强纠偏
  • 停车态(Stop):距离 ≤ 20cm,启动刹车程序:先令电机反向通电 100ms(能耗制动),再切断驱动
// 状态机核心逻辑(在主循环中调用) if (distance_cm <= 20.0f) { current_state = STOP; brake_motor(); // 执行能耗制动 } else if (distance_cm <= 30.0f) { current_state = WARNING; set_pwm_duty(60); // 占空比降至60% pid.Kp *= 1.5f; // 增强纠偏力度 } else { current_state = CRUISE; set_pwm_duty(100); pid.Kp /= 1.5f; // 恢复默认Kp }

参数说明:brake_motor()函数将 L298N 的 IN1/IN2 置为 1/0 或 0/1,使电机产生反向扭矩;set_pwm_duty()通过TIM_SetCompare1(TIM4, duty_value)修改 PWM 占空比。该策略避免了急停冲击,且在预警态下提升循迹鲁棒性——实测表明,当小车以 0.8m/s 速度接近障碍物时,此策略可将停车距离稳定控制在 18±2cm。

4. STM32F103 标准库工程构建与 Keil 调试排错实战

4.1 解决 “Error: No STM32 target found!” 的硬件链路诊断流程

Keil 下载时报错No STM32 target found!是最常见拦路虎,但根源往往不在 ST-Link。按以下顺序排查:

  1. 供电验证:用万用表测 VDDA/VSSA 是否为 3.3V,若低于 3.2V,检查 AMS1117-3.3 输入电容是否虚焊
  2. SWD 线序确认:ST-Link 的 SWDIO(PA13)、SWCLK(PA14)、GND、3.3V 四线必须与目标板一一对应,禁止接错 SWO(PA15)
  3. BOOT 引脚锁定:确保 BOOT0=0、BOOT1=0(出厂默认),若曾刷入 Bootloader,需短接 BOOT0 并复位进入系统存储器
  4. Keil 配置修正:Project → Options → Debug → Settings → Port 选 SW,然后点击 "Scan" —— 若仍失败,勾选 "Connect under reset"

注意:HJduino.uvguix.Administrator工程名暗示使用管理员权限运行 Keil,若 Win10 UAC 关闭,需右键 Keil 图标 → “以管理员身份运行”,否则 ST-Link 驱动无法访问 USB 设备。

4.2stm32f10x_flash.c中的 Flash 写保护解除与参数存储

项目需保存停车阈值等参数到 Flash,但FLASH_Unlock()后若未调用FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout),可能因 Flash 忙碌导致写入失败。标准库 v3.50 的stm32f10x_flash.c包含完整流程:

FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 擦除第 31 页(0x0800FC00,1KB) FLASH_ErasePage(0x0800FC00); // 写入 2 个 uint16_t 参数 FLASH_ProgramHalfWord(0x0800FC00, warning_threshold); // 预警距离 FLASH_ProgramHalfWord(0x0800FC02, stop_threshold); // 停车距离 FLASH_Lock(); // 写完立即上锁

逻辑说明:FLASH_ErasePage()必须整页擦除(1KB),不能只擦几个字节;FLASH_ProgramHalfWord()每次写 16 位,地址必须偶对齐;FLASH_Lock()防止意外改写。参数说明:warning_threshold存储为 10 倍整数(如 25 表示 25.0cm),避免浮点运算开销。失败时看什么?若FLASH_WaitForLastOperation()返回FLASH_TIMEOUT,检查FLASH->CR寄存器的PG(编程使能)和PER(页擦除使能)位是否被正确置位。

4.3 使用stm32f10x_usart.c实现串口调试协议解析

为验证传感器数据,需通过 USART1(PA9/PA10)输出调试信息。但直接printf()会因fputc()重定向问题卡死。本项目在stm32f10x_usart.c基础上实现轻量协议:

// 定义帧格式:0xAA + LEN + CMD + DATA[LEN] + CRC8 void uart_send_frame(uint8_t cmd, uint8_t* data, uint8_t len) { uint8_t frame[32]; frame[0] = 0xAA; frame[1] = len + 2; // LEN 字段含 CMD 和 DATA frame[2] = cmd; for(uint8_t i=0; i<len; i++) frame[3+i] = data[i]; frame[3+len] = crc8_calc(frame, 3+len); // CRC8 校验 // 通过 DMA 发送 DMA_Cmd(DMA1_Channel4, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, len+4); DMA_Cmd(DMA1_Channel4, ENABLE); }

逻辑说明:DMA1_Channel4绑定 USART1_TX,避免 CPU 占用;crc8_calc()使用查表法,比循环计算快 5 倍。参数说明:cmd=0x01表示发送红外状态,cmd=0x02表示超声波距离,上位机(如 XCOM)可据此绘图分析。若串口无输出,检查RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_USART1, ENABLE)是否漏写,以及USART_Init()USART_InitStruct->USART_BaudRate是否设为 115200。

5. 电机驱动与 PID 参数整定:从振荡到稳定的实操技巧

5.1 L298N 驱动电路的续流二极管选型与 PWM 频率设定

L298N 内置续流二极管压降大(1.2V),在 1A 电流下功耗达 1.2W,易发热失效。本项目在 PCB 上外置SB560 肖特基二极管(正向压降 0.55V),并确保阴极接 VCC,阳极接电机端。PWM 频率选择至关重要:过低(<1kHz)人耳可闻啸叫,且电机转矩脉动大;过高(>20kHz)则 L298N 开关损耗剧增。经实测,16kHz 是最佳平衡点——既超声波不可闻,又使 L298N 温升低于 45℃。

// TIM4 PWM 初始化(16kHz,72MHz 主频) TIM_TimeBaseStructure.TIM_Period = 4499; // (72MHz / 16000) - 1 = 4499 TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频 TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 2250; // 初始占空比50% TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM4, &TIM_OCInitStructure);

参数说明:TIM_Period=4499确保 PWM 周期严格为 62.5μs(1/16kHz);TIM_Pulse=2250对应 50% 占空比(2250/4500);TIM_OCPolarity_High表示高电平有效。若电机启动无力,检查TIM_OCInitStructure.TIM_OutputState是否为TIM_OutputState_Enable(非TIM_OutputState_Disable)。

5.2 循迹 PID 的 Ziegler-Nichols 临界比例度法整定

面对五路红外的位置等级(0~8),传统位置式 PID 易积分饱和。本项目采用增量式 PID,仅输出 PWM 增量 ΔDuty,避免累积误差:

// 增量式 PID 计算(伪代码) error = setpoint - current_position; // setpoint=4(居中) d_error = error - last_error; output_delta = Kp*(error - last_error) + Ki*error + Kd*(d_error - last_d_error); pwm_duty += output_delta; // 直接叠加到当前占空比 pwm_duty = constrain(pwm_duty, 30, 100); // 限幅防失稳 last_error = error; last_d_error = d_error;

逻辑说明:constrain()将占空比限制在 30%~100%,保证最低速度不丢线;Kp/Ki/Kd需现场整定。推荐 Ziegler-Nichols 法:先置 Ki=Kd=0,逐步增大 Kp 直至小车左右振荡(临界振荡),记下此时 Kp_cr=2.8,振荡周期 Tu=0.8s,则:

  • Kp = 0.6 × Kp_cr = 1.68
  • Ki = 1.2 × Kp_cr / Tu = 4.2
  • Kd = 0.075 × Kp_cr × Tu = 1.68

提示:整定时务必断开超声波模块,避免停车干扰;若振荡剧烈,立即降 Kp。实测发现,当 Kp > 2.0 时,小车在直道上会出现高频抖动,此时需增加 Ki 抑制静差,而非继续加 Kp。

5.3 超声波与循迹的优先级仲裁机制

当红外判定“严重右偏”需左打舵,而超声波同时报警“前方 18cm”,谁该优先?本项目在主循环中采用时间戳仲裁

  • 红外状态每 5ms 更新一次,打上时间戳ir_ts
  • 超声波距离每 100ms 更新一次,打上时间戳us_ts
  • us_ts > ir_ts(即超声波数据更新更近),则停车态覆盖循迹态;否则循迹态生效
// 仲裁逻辑 if ((millis() - us_ts) < 50) { // 超声波数据新鲜度 <50ms if (distance_cm <= 20.0f) force_stop = 1; } else { force_stop = 0; } if (force_stop) { set_motor_stop(); } else { apply_pid_control(); // 执行循迹PID }

参数说明:millis()stm32f10x_tim.c的 TIM2 每 1ms 中断累加;force_stop=1时直接关闭 PWM 输出,无视 PID 结果。该机制确保避障永远高于循迹,符合安全第一原则。实测中,当小车高速冲向墙壁时,即使红外还显示“居中”,超声波的 100ms 新鲜数据仍能触发及时停车。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询