STM32F103C8T6超声波避障硬核实现:TIM2输入捕获+L293D驱动协同
简介本资源是一套基于STM32F103C8T6主控的智能小车超声波避障实验完整源码工程面向嵌入式初学者与课程实践者解决智能小车环境感知与自主避障功能开发的学习痛点。压缩包共44个文件包含8个核心头文件h、3个C源文件c、1个Keil工程文件uvproj、1个可执行固件hex及OLED显示、HC-SR04测距、L293D电机驱动等关键模块的配置与实现代码辅以电路图jpg、程序说明文档txt和编译中间文件结构完整、即装即用。目前已有2919人学习下载所有代码在真实STM32F103C8T6小车上实测通过涵盖超声波实时测距、红外辅助避障、双轮差速转向控制及OLED状态反馈等典型嵌入式功能模块适合用于课程设计、毕业设计或竞赛原型开发。1. STM32F103C8T6智能小车超声波避障不是“接上线就跑”而是时序、驱动与状态协同的硬核闭环你手里的那块蓝色STM32F103C8T6最小系统板引脚图上标着PA0、PB12、PC13这些编号但真正让小车在桌角前刹住的从来不是某个IO口输出高电平那么简单。超声波模块如HC-SR04发出的8个40kHz脉冲需要精确计时回波高电平持续时间L293D驱动芯片的使能端EN若未按逻辑电平序列激活电机只会嗡嗡抖动不转而HAL库中HAL_TIM_Base_Start_IT()和HAL_GPIO_ReadPin()的调用顺序稍有错位测距值就会跳变50cm以上。这不是Arduino式“写个loop()就能动”的玩具级实现——它要求你同时理解定时器输入捕获的边沿触发机制、L293D双H桥的真值表约束、以及超声波物理传播中温度补偿的工程取舍。适合正在用STM32F103C8T6最小系统板做课程设计、工创赛智能物流小车原型或嵌入式入门实战的开发者尤其当你已烧录过LED闪烁例程却卡在“电机不动”或“避障距离忽大忽小”时这篇就是为你拆解真实硬件链路上每一处可测量、可调试、可复现的节点。2. 从原理到引脚为什么必须用TIM2_CH2做输入捕获而不是随便一个GPIO超声波避障的核心是精确测距而HC-SR04的回波信号本质是一个宽度与距离成正比的方波脉冲典型范围150μs~25ms。STM32F103C8T6的通用定时器TIM2/TIM3支持输入捕获模式能硬件级记录上升沿/下降沿时刻精度达1μs系统时钟72MHz分频后远超软件延时循环的误差。若强行用GPIO中断HAL_GetTick()测宽因中断响应延迟通常10μs和系统滴答时钟分辨率1ms10cm以内距离将完全无法分辨。2.1 TIM2_CH2与GPIO映射的不可替代性HC-SR04的Echo引脚必须接到具备输入捕获功能的定时器通道。查阅STM32F103C8T6数据手册第10章“Alternate function mapping”TIM2_CH2对应PB3非重映射或PA1部分重映射。但PA1在最小系统板上常被用作SWD调试接口SWCLK强行占用会导致无法烧录。因此PB3是唯一安全选择——它不参与调试且TIM2在APB1总线上最高36MHz满足72MHz系统时钟下的分频需求。// stm32f103c8t6_hal_init.c 关键初始化片段 void MX_TIM2_Init(void) { TIM_IC_InitTypeDef sConfigIC {0}; htim2.Instance TIM2; htim2.Init.Prescaler 71; // 72MHz / (711) 1MHz即1μs计数单位 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFF; // 自动重装载值覆盖最大25ms回波25000μs htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; if (HAL_TIM_Base_Init(htim2) ! HAL_OK) { Error_Handler(); } if (HAL_TIM_IC_Init(htim2) ! HAL_OK) { Error_Handler(); } // 配置PB3为TIM2_CH2输入捕获上拉防止浮空 sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_BOTHEDGE; // 捕获上升沿和下降沿 sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0x0F; // 采样4次滤波抑制噪声 HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_2); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_2); // 启用中断避免轮询阻塞 }提示ICPolarity TIM_INPUTCHANNELPOLARITY_BOTHEDGE是关键。HC-SR04回波信号起始为高电平触发时刻结束为低电平回波结束捕获两个边沿才能计算宽度。若只设RISING将丢失下降沿时间戳导致距离恒为0。2.2 L293D驱动芯片的真值表约束与电源隔离设计L293D不是“通电即转”的黑盒。其内部为双H桥每个桥臂需同时满足① 使能端EN为高② 输入A/B逻辑组合符合真值表③ VCC1逻辑电源与VCC2电机电源严格分离。常见错误是共用5V给两者导致电机启停时逻辑电压跌落MCU复位。ENIN1IN2OUT1状态说明0XX高阻态电机断开无功耗1000刹车两端接地110正转OUT1VCC2, OUT2GND101反转OUT1GND, OUT2VCC2STM32F103C8T6的PA0-PA3控制左轮IN1/IN2/EN1PB0-PB1控制右轮IN3/IN4/EN2。注意EN引脚必须接PWM输出而非普通GPIO——否则无法调速。我们使用TIM3_CH1PA6和TIM3_CH2PA7生成两路互补PWM占空比决定速度。// PWM初始化TIM3 void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 71; // 1MHz计数频率 htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 999; // 1kHz PWM频率1MHz/1000 htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; if (HAL_TIM_PWM_Init(htim3) ! HAL_OK) { Error_Handler(); } sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 500; // 初始占空比50%500/1000 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); // PA6 - EN1 HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_2); // PA7 - EN2 HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); }注意L293D的VCC2电机电源建议用7.4V锂电池两节18650经LM2596降压至6V供电机VCC1逻辑电源必须独立接STM32的3.3V。二者地线在单点如L293D GND引脚汇合避免电机电流干扰MCU地平面。3. 超声波测距与电机控制的实时协同中断服务程序中的状态机设计单纯测出距离值毫无意义必须将其转化为“前进/左转/右转/停止”的决策并在毫秒级完成执行。这里不能依赖delay()阻塞而要用状态机标志位驱动。核心是三个同步信号超声波中断更新距离、主循环检查距离阈值、PWM更新电机动作。3.1 TIM2输入捕获中断中的距离计算逻辑TIM2_CH2中断服务程序ISR需在微秒级完成边沿时间戳记录与宽度计算。关键点两次捕获值相减可能溢出计数器从0xFFFF回绕必须用无符号减法自动处理。// stm32f103c8t6_main.c 中的中断回调 uint32_t IC1Value 0, IC2Value 0; uint8_t CaptureNumber 0; uint16_t Distance_cm 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2 htim-Channel HAL_TIM_ACTIVE_CHANNEL_2) { if (CaptureNumber 0) { IC1Value HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_2); CaptureNumber 1; __HAL_TIM_SET_CAPTUREPOLARITY(htim2, TIM_CHANNEL_2, TIM_INPUTCHANNELPOLARITY_FALLING); } else if (CaptureNumber 1) { IC2Value HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_2); CaptureNumber 0; __HAL_TIM_SET_CAPTUREPOLARITY(htim2, TIM_CHANNEL_2, TIM_INPUTCHANNELPOLARITY_RISING); // 计算高电平时间单位μs处理计数器溢出 uint32_t us (IC2Value IC1Value) ? (IC2Value - IC1Value) : (0x10000 IC2Value - IC1Value); // 距离公式距离(cm) 时间(μs) × 声速(340m/s) ÷ 2 ÷ 10000 // 简化us × 340 / 20000 us / 58.8 ≈ us / 59 Distance_cm (uint16_t)(us / 59); // 添加粗略滤波剔除异常值2cm或400cm if (Distance_cm 2 || Distance_cm 400) { Distance_cm 0; // 无效值标记 } } } }参数说明us / 59是经验公式基于20℃空气声速340m/s。若需更高精度可加入温度传感器如DS18B20动态修正声速但对桌面小车场景±2cm误差可接受。3.2 主循环中的避障状态机与PWM占空比调节主循环以20ms为周期50Hz运行读取Distance_cm并决策。状态机避免频繁启停电机造成机械冲击当前距离动作左轮PWM右轮PWM说明30cm直行600600全速前进15~30cm减速缓行300300提前降速准备转向5~15cm原地右转0600左轮停右轮正转5cm紧急后退400400反向全速退2秒后恢复// 主循环片段while(1)内 static uint8_t state STATE_FORWARD; static uint32_t last_action_ms 0; while (1) { uint32_t now_ms HAL_GetTick(); // 每20ms执行一次避障决策 if (now_ms - last_action_ms 20) { last_action_ms now_ms; if (Distance_cm 0) { // 超声波无响应保持当前状态或默认直行 continue; } if (Distance_cm 30) { state STATE_FORWARD; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 600); // PA6: EN1占空比60% __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 600); // PA7: EN2占空比60% HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // IN11 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // IN20 → 左轮正转 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // IN31 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); // IN40 → 右轮正转 } else if (Distance_cm 15) { state STATE_SLOW; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 300); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 300); } else if (Distance_cm 5) { state STATE_TURN_RIGHT; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 0); // 左轮EN0 → 刹车 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 600); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // IN10 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // IN20 → 左轮刹车 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // IN31 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); // IN40 → 右轮正转 } else { // 5cm state STATE_BACKWARD; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 400); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 400); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // IN10 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // IN21 → 左轮反转 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // IN30 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); // IN41 → 右轮反转 } } }提示HAL_GPIO_WritePin()直接操作寄存器比HAL_GPIO_TogglePin()更可靠。状态切换时先改PWM占空比再改方向IO避免电机驱动芯片出现非法真值表组合如EN1且IN1IN21。4. 调试与验证用逻辑分析仪抓取TIM2_CH2波形与L293D使能信号当小车不避障或距离跳变时仅看代码无法定位问题。必须用硬件工具验证信号链路是否真实符合预期。逻辑分析仪如Saleae Logic 8是最低成本方案采样率≥10MHz即可。4.1 测量TIM2_CH2PB3回波信号的关键参数将探头接PB3触发条件设为“上升沿”捕获完整测距周期约60ms。正常波形应包含Trigger脉冲HC-SR04的Trig引脚由MCU PA8输出发出的10μs高电平Echo脉冲PB3上紧随其后的高电平脉冲宽度与距离对应如20cm→1160μs间隔Trig与Echo之间有约500μs静默期确保模块准备就绪。若Echo脉冲缺失或宽度恒为0检查HC-SR04供电是否稳定实测VCC需≥4.5VTrig引脚是否真有10μs脉冲用另一通道测PA8PB3是否被其他外设复用如SPI1_MISO默认不启用但需确认HAL_MspInit()中未开启。4.2 验证L293D使能信号与电机实际响应将探头接PA6EN1和PA7EN2观察PWM波形占空比应严格匹配__HAL_TIM_SET_COMPARE()设置值如600/100060%频率应为1kHz周期1msEN信号与IN1/IN2电平必须同步变化——若EN为高而IN1/IN2为00则电机刹车若EN为低OUT1/OUT2必须为高阻态万用表测对地电阻1MΩ。常见故障表现象可能原因验证方法电机完全不转L293D VCC2未供电或GND未共接万用表测VCC2对GND电压确认5V小车原地打转左右轮PWM占空比差异过大逻辑分析仪对比PA6/PA7波形占空比避障距离忽大忽小HC-SR04 Echo引脚接触不良或受电机干扰示波器看PB3波形是否毛刺多加100nF电容滤波烧毁L293DVCC1与VCC2电源反接或电机堵转电流超限断电后测L293D各引脚对地电阻正常时IN/EN对GND应为kΩ级注意调试时务必断开电机先用万用表蜂鸣档测L293D输出端OUT1/OUT2对GND通断确认无短路后再上电。STM32F103C8T6最小系统板的3.3V电源能力有限切勿直接驱动L293D逻辑端——需经74HC245等缓冲器隔离。5. 进阶技巧用串口输出实时距离与状态构建可追溯的调试日志仅靠LED指示灯无法诊断复杂时序问题。将Distance_cm、state、HAL_GetTick()时间戳通过USART1PA9/PA10输出到串口助手是快速定位问题的黄金标准。关键在于避免printf阻塞采用DMA非阻塞发送。5.1 配置USART1DMA实现零等待日志输出// 初始化USART1与DMA void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huart1) ! HAL_OK) { Error_Handler(); } // 启用DMA发送缓冲区大小256字节 tx_buffer[0] \0; HAL_UART_Transmit_DMA(huart1, (uint8_t*)tx_buffer, sizeof(tx_buffer)); } // 定义环形缓冲区与发送函数 #define LOG_BUF_SIZE 256 static char log_buffer[LOG_BUF_SIZE]; static volatile uint16_t log_head 0, log_tail 0; void log_printf(const char* format, ...) { va_list args; va_start(args, format); int len vsnprintf(log_buffer log_head, LOG_BUF_SIZE - log_head, format, args); va_end(args); if (len 0) { log_head (log_head len) % LOG_BUF_SIZE; // 触发DMA发送若空闲 if (HAL_UART_GetState(huart1) HAL_UART_STATE_READY) { HAL_UART_Transmit_DMA(huart1, (uint8_t*)(log_buffer log_tail), (log_head log_tail) ? (log_head - log_tail) : (LOG_BUF_SIZE - log_tail log_head)); log_tail log_head; } } }5.2 在主循环中注入结构化日志每200ms输出一行包含时间戳、距离、状态、PWM值便于用Excel绘图分析// 主循环中添加 static uint32_t last_log_ms 0; if (now_ms - last_log_ms 200) { last_log_ms now_ms; log_printf(T:%dms D:%dcm S:%d L:%d R:%d\r\n, now_ms, Distance_cm, state, __HAL_TIM_GET_COMPARE(htim3, TIM_CHANNEL_1), __HAL_TIM_GET_COMPARE(htim3, TIM_CHANNEL_2)); }典型输出T:1240ms D:28cm S:1 L:600 R:600 T:1440ms D:18cm S:2 L:300 R:300 T:1640ms D:8cm S:3 L:0 R:600提示串口日志会占用CPU资源正式部署时可注释掉log_printf调用或通过宏开关控制如#ifdef DEBUG_LOG。真正的工创赛作品评委最看重的是你能展示出“问题在哪、怎么发现、如何解决”的完整证据链——而这串ASCII字符就是最硬核的凭证。本文还有配套的精品资源点击获取