STM32F103智能小车三功能实战工程:红外遥控操作、超声波实时避障、黑白线精准循迹

发布时间:2026/6/7 15:43:15

STM32F103智能小车三功能实战工程:红外遥控操作、超声波实时避障、黑白线精准循迹 本文还有配套的精品资源点击获取简介基于STM32F103C8T6等主流型号提供三个开箱即用的Keil uVision工程遥控版支持VS1838B红外接收解析NEC协议实现前进/后退/左转/右转/启停避障版集成HC-SR04超声波模块配合L298N驱动芯片根据设定距离自动刹车或转向循迹版采用TCRT5000红外对管阵列通过阈值判断或简易PID算法稳定跟踪黑白边界线。所有工程均含标准固件库结构System/Core/FWLib/Object/Code_User引脚已预定义无需修改配置即可编译下载。配套例程说明.txt明确列出各模块硬件接线图、GPIO分配如PA0接超声波Trig、PB1接红外OUT、关键参数调节位置如循迹灵敏度阈值、避障触发距离及典型问题解决方法如电机不转排查方向信号电平、红外无响应检查供电与载波频率。代码全部使用标准C编写函数划分清晰关键逻辑处附中文注释适合从点灯入门到综合项目实践的嵌入式学习者也方便快速接入OLED显示、蓝牙串口透传或WiFi模组扩展远程控制能力。1. 项目概述为什么这套小车工程值得你花两小时认真读完我带过十几届嵌入式方向的毕业设计也帮不少电子爱好者调试过智能小车。说实话市面上标榜“三合一”“全功能”的STM32小车例程八成是把三个独立demo硬凑在一个工程里引脚冲突、时钟配置打架、中断优先级混乱新手烧录一次就卡在串口无输出最后只能删库重来。而眼前这套基于STM32F103特别是C8T6、RBT6等主流型号的实战工程是我近五年见过最“讲人话”的教学级项目——它不炫技不堆砌RTOS或FreeRTOS而是用最朴素的标准外设库StdPeriph Library裸机编程把红外遥控、超声波避障、黑白线循迹这三个高频刚需功能拆成三个完全解耦、互不干扰、各自可独立编译运行的Keil uVision工程。每个工程都像一个拧紧螺丝的机械模块你换一块板子改两行#define就能跑起来你加一个OLED屏只用在Code_User目录下新增.c/.h文件不用动核心驱动层。关键词里的红外遥控不是简单地“按一下前进”而是完整实现了NEC协议的起始码识别、地址码校验、数据码解析与反码验证连载波频率偏差±5kHz都能容忍超声波避障也不是“距离20cm就停”而是做了温度补偿下的声速修正虽然默认用340m/s但代码里留了SPEED_OF_SOUND_CM_PER_US宏供你填实测值还加入了连续三次有效测距才触发动作的防抖逻辑至于黑白线循迹它没一上来就上复杂PID而是先给你一个稳定可靠的阈值比较法四路TCRT5000支持左偏/右偏/居中/脱线四种状态判断再在注释里手把手教你如何把Kp、Ki、Kd参数从零填起调出不抖不冲的转向响应。配套的例程说明.txt更不是摆设——它明确告诉你PB1接VS1838B的OUT脚PA0是HC-SR04的TrigPB0是EchoPC0~PC3接TCRT5000的四路模拟输出连L298N的IN1~IN4该接哪个GPIO、ENA/ENB该用哪个PWM通道都列得清清楚楚。这不是教科书这是你焊好板子、接上线、点下Keil“Download”按钮后小车真能动起来的第一份可靠地图。2. 整体架构与设计思路为什么是三个独立工程而不是一个大工程2.1 模块解耦拒绝“牵一发而动全身”的灾难式耦合很多初学者拿到一个“多功能小车”工程第一反应是“哇功能好全”然后兴冲冲打开main.c发现里面混着红外解码、超声波定时器、循迹ADC采样、电机PWM输出……所有逻辑挤在同一个while(1)循环里变量全局声明中断服务函数ISR里还调用延时函数。这种结构在单功能验证时勉强能跑一旦你想改循迹算法结果红外接收突然失灵想优化避障响应速度循迹又开始抽风。根本原因在于资源竞争、时序冲突、耦合过深。这套工程的底层设计哲学非常务实每个功能对应一个最小可行系统MVP。遥控版只关心红外信号的捕获与解析其他外设全关避障版专注超声波测距与电机响应不碰ADC和红外IO循迹版则全力做好四路模拟量的快速采样、滤波与决策。它们共享同一套硬件抽象层HAL-like但基于StdPeriph比如delay_ms()、GPIO_Init()、USART_Printf()这些基础函数都封装在Core目录下但绝不共享业务逻辑层。你看Code_User目录下的三个工程remote_control.c只包含IR_Init()、IR_GetKeyValue()、Motor_Control_By_Key()三个核心函数ultrasonic_avoid.c只有US_Init()、US_GetDistance()、Avoidance_Action()line_follower.c精简到LF_Init()、LF_ReadSensors()、LF_PID_Calculate()。这种设计带来的直接好处是你今天只想搞懂红外遥控就只编译智能小车_遥控_V1.0.uvproj烧进去示波器钩住PB1看到脉冲就对了明天想研究超声波直接换工程连main.c都不用改一行因为每个工程的main()都是高度一致的模板int main(void) { SystemInit(); // 系统时钟初始化72MHz RCC_Configuration(); // 外设时钟使能 GPIO_Configuration(); // 所有相关GPIO初始化 NVIC_Configuration(); // 中断向量配置仅本功能所需 US_Init(); // 或 IR_Init(), LF_Init() while(1) { uint16_t dist US_GetDistance(); // 或 IR_GetKeyValue(), LF_ReadSensors() Avoidance_Action(dist); // 或 Motor_Control_By_Key(), LF_PID_Calculate() delay_ms(50); // 主循环节拍非阻塞式 } }提示这里的delay_ms(50)不是SysTick滴答延时而是基于SysTick_Config()配置的1ms中断在stm32f10x_it.c里用一个全局变量TimingDelay递减实现。它轻量、精准、不阻塞其他中断比for()循环延时靠谱得多。2.2 标准固件库结构为什么坚持用StdPeriph而不是HAL或LL你可能会问现在都2024年了为什么不用ST官方主推的HAL库答案很实在学习成本与可控性。HAL库封装太深一个HAL_GPIO_WritePin()背后可能调用七八层函数出问题时你根本不知道卡在哪一级时钟门控或寄存器锁。而StdPeriph库函数名就是寄存器名的直译GPIO_ResetBits(GPIOA, GPIO_Pin_0)→ 直接清零PA0的BSRR寄存器低16位。对初学者来说看一遍库函数源码stm32f10x_gpio.c再对照参考手册第9章《GPIO》的寄存器映射图半小时就能建立“写代码操作硬件”的肌肉记忆。这套工程的目录结构是StdPeriph的经典范式智能小车_遥控_V1.0/ ├── System/ # 启动文件startup_stm32f10x_md.s、system_stm32f10x.c时钟树配置 ├── Core/ # 主要用户代码入口、通用延时、串口printf、中断向量表重映射 ├── FWLib/ # 官方标准外设库源码stm32f10x_gpio.c, stm32f10x_exti.c等已裁剪仅保留用到的模块 ├── Object/ # Keil编译输出目录.axf, .hex, .map └── Code_User/ # 你的战场所有业务逻辑代码都在这里干净、隔离、可替换注意FWLib目录下没有stm32f10x_usart.c别慌因为三个工程都没用到串口通信除了调试用的USART_Printf()那只是发送不收。这就是“按需加载”的体现——不为炫技而引入冗余模块减少Flash占用C8T6只有64KB也避免时钟配置错误。2.3 引脚预定义与硬件抽象如何做到“换板即用”真正的工程友好体现在细节里。打开Code_User/stm32f10x_conf.h你会看到这样一组宏定义// 红外遥控引脚定义 #define IR_GPIO_PORT GPIOB #define IR_GPIO_PIN GPIO_Pin_1 #define IR_RCC_APB2PERIPH RCC_APB2Periph_GPIOB // 超声波模块引脚定义 #define US_TRIG_GPIO_PORT GPIOA #define US_TRIG_GPIO_PIN GPIO_Pin_0 #define US_ECHO_GPIO_PORT GPIOB #define US_ECHO_GPIO_PIN GPIO_Pin_0 #define US_RCC_APB2PERIPH (RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB) // 循迹传感器引脚定义 #define LF_SENSOR_GPIO_PORT GPIOC #define LF_SENSOR_GPIO_PIN (GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3) #define LF_RCC_APB2PERIPH RCC_APB2Periph_GPIOC这些不是随便写的。它对应着例程说明.txt里白纸黑字的接线图VS1838B的OUT脚必须接到开发板的PB1HC-SR04的Trig接PA0、Echo接PB0TCRT5000的四路OUT分别接PC0~PC3。为什么这么定因为PB1是EXTI线1VS1838B输出的是38kHz载波调制的方波需要用外部中断边沿触发捕获下降沿启动计时上升沿停止PB1天然支持EXTI1PA0和PB0组合可以共用一个TIM2定时器PA0输出PWM触发超声波PB0输入捕获Echo高电平时间TIM2_CH1和CH2正好配对PC0~PC3是ADC1的通道10~13四路模拟量同时采样无需切换通道效率最高。实操心得如果你用的是正点原子的战舰V3开发板它的PB1被蜂鸣器占用了。怎么办很简单把IR_GPIO_PIN改成GPIO_Pin_12PD12EXTI线12再把VS1838B接到PD12然后在IR_Init()里把EXTI_Line从EXTI_Line1改成EXTI_Line12NVIC_IRQChannel从EXTI1_IRQn改成EXTI15_10_IRQn。整个过程5分钟不碰其他代码。3. 核心功能模块深度解析从原理到代码每一行都经得起追问3.1 红外遥控NEC协议不是“收到脉冲就执行”而是严谨的状态机VS1838B这类一体化红外接收头输出的是经过放大、滤波、整形后的TTL电平信号。它不是直接给你原始红外光波而是把NEC协议的38kHz载波解调出来变成一串高低电平组合。很多人以为“检测到下降沿就记一次键值”结果发现按键失灵、重复触发。真相是NEC协议是一个严格的时间编码协议必须用定时器精确测量每个脉冲宽度再根据宽度分类为逻辑0、逻辑1、引导码、结束码。这套工程的红外解码放在Code_User/ir_decode.c里核心是一个基于TIM3的输入捕获状态机。TIM3配置为向上计数模式预分频器PSC72-1即1MHz计数频率所以每个计数值1μs。当PB1产生下降沿时触发EXTI1中断在中断里启动TIM3下一个上升沿到来时捕获CCR1寄存器值得到第一个脉冲宽度再下一个下降沿再捕获……如此循环构建一个9个元素的数组pulse_width[9]对应NEC帧的结构序号名称标准时长允许误差作用0引导码高电平9000μs±500μs告诉接收端一帧开始了1引导码低电平4500μs±500μs2地址码bit0560/1690μs—低电平560μs0高电平1690μs13地址码bit1同上—…………共8位地址码8位反码8结束码≥500μs—表示本帧结束关键代码片段ir_decode.c// EXTI1中断服务函数捕获每个边沿 void EXTI1_IRQHandler(void) { static uint8_t edge_cnt 0; static uint32_t last_time 0; uint32_t curr_time; if(EXTI_GetITStatus(EXTI_Line1) ! RESET) { curr_time TIM_GetCounter(TIM3); // 读取当前计数值单位μs if(edge_cnt 0) { // 第一个下降沿记录时间启动TIM3 last_time curr_time; TIM_Cmd(TIM3, ENABLE); } else { // 后续边沿计算脉冲宽度 pulse_width[edge_cnt-1] curr_time - last_time; last_time curr_time; } edge_cnt; if(edge_cnt 9) edge_cnt 0; // 防溢出 EXTI_ClearITPendingBit(EXTI_Line1); } } // 主循环中解析pulse_width数组 uint8_t IR_GetKeyValue(void) { if(pulse_width[0] 8500 pulse_width[0] 9500 pulse_width[1] 4000 pulse_width[1] 5000) { // 引导码正确开始解析地址码这里简化实际要校验反码 uint8_t addr 0; for(uint8_t i0; i8; i) { if(pulse_width[2i] 1200) addr | (1i); // 高电平长→逻辑1 } return addr; // 返回地址码对应遥控器按键 } return 0xFF; // 无效帧 }注意事项- VS1838B的供电必须稳定在5V3.3V供电会导致灵敏度暴跌10米外就收不到信号- PB1引脚必须接10kΩ上拉电阻到5V开发板若已集成可忽略否则空闲时电平浮动误触发中断-pulse_width[]数组必须用volatile修饰因为被中断修改主循环读取时不能被编译器优化掉。3.2 超声波避障HC-SR04不是“发个脉冲等回响”而是精密的时序协同HC-SR04的工作流程教科书上写得很简单“Trig脚给10μs高电平模块自动发出8个40kHz方波Echo脚输出高电平持续时间距离×58μs”。但真实世界里Echo高电平可能只有几微秒近距离也可能长达23ms4米远还夹杂着环境噪声。如果用普通GPIO查询方式CPU一直在while(!GPIO_ReadInputDataBit())里空转既耗电又不准。这套工程采用TIM2输入捕获DMA辅助的方案。TIM2配置为- CH1PA0为输出比较模式生成精确10μs高电平脉冲TIM_SetCompare1(TIM2, 10)因为CK_CNT1MHz- CH2PB0为输入捕获模式上升沿触发捕获记录Echo高电平起点下降沿再捕获一次得到高电平持续时间。核心代码ultrasonic_avoid.cvoid US_Init(void) { // PA0配置为TIM2_CH1输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PB0配置为TIM2_CH2输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); // TIM2初始化1MHz计数频率 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 72-1; // 72MHz / 72 1MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // CH1输出比较生成10μs脉冲 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_Toggle; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 10; // 10μs TIM_OC1Init(TIM2, TIM_OCInitStructure); // CH2输入捕获捕获Echo高电平 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel TIM_Channel_2; 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 0x0; TIM_IC2Init(TIM2, TIM_ICInitStructure); TIM_Cmd(TIM2, ENABLE); } uint16_t US_GetDistance(void) { static uint32_t rise_time 0, fall_time 0; uint32_t high_time 0; uint16_t distance 0; // 发送Trig脉冲 TIM_SetCompare1(TIM2, 10); delay_us(20); // 确保脉冲发出 // 等待Echo上升沿起点 while(TIM_GetFlagStatus(TIM2, TIM_FLAG_CC2) RESET); rise_time TIM_GetCapture2(TIM2); TIM_ClearFlag(TIM2, TIM_FLAG_CC2); // 切换为下降沿捕获终点 TIM_ICStructInit(TIM_ICInitStructure); TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Falling; TIM_IC2Init(TIM2, TIM_ICInitStructure); // 等待Echo下降沿 while(TIM_GetFlagStatus(TIM2, TIM_FLAG_CC2) RESET); fall_time TIM_GetCapture2(TIM2); TIM_ClearFlag(TIM2, TIM_FLAG_CC2); high_time fall_time - rise_time; distance (uint16_t)(high_time / 58.0f); // 单位cm58μs/cm是25℃声速换算值 // 防错距离超出400cm或小于2cm视为无效 if(distance 400 || distance 2) distance 0; return distance; }实操心得- HC-SR04的VCC必须接5VGND要和STM32共地否则Echo电平不匹配HC-SR04输出5V TTLSTM32输入耐压3.3V但多数开发板IO口有5V容限保险起见可在PB0前加1kΩ限流电阻- 测距时小车最好静止运动中轮子震动会导致Echo信号抖动建议在Avoidance_Action()里加入“连续3次测距值相差5cm才采纳”的滤波逻辑- 如果发现距离总是0先用万用表测PA0是否有10μs脉冲用示波器最佳再测PB0在触发时是否有高电平输出——这是最高效的排查路径。3.3 黑白线循迹TCRT5000不是“电压高就黑”而是动态阈值的艺术TCRT5000是红外反射式传感器由红外发射管和光敏三极管组成。当传感器悬空离地5mm反射光弱光敏管截止输出高电平约3.3V当靠近黑色胶带光被吸收输出仍为高电平当靠近白色地面光被强烈反射光敏管导通输出低电平0.5V。所以输出电压高低本身不能直接判断黑白必须设定一个阈值且这个阈值会随环境光、传感器老化、电池电压波动而漂移。这套工程提供两种循迹策略全部实现在line_follower.c中方案A四路阈值比较法推荐新手使用ADC1规则组同时采样PC0~PC3四路模拟量。关键不是绝对电压值而是相对差异。代码先做一次“白线校准”小车放在纯白区域按下某个按键如遥控器“OK”键程序记录四路ADC的平均值作为white_ref再放到纯黑区域记录black_ref最终阈值threshold (white_ref black_ref) / 2。#define ADC_CHANNEL_NUM 4 uint16_t adc_value[ADC_CHANNEL_NUM]; uint16_t white_ref 2500, black_ref 1200; // 默认值校准后更新 uint16_t threshold 1850; void LF_Init(void) { // ADC1初始化通道10~13PC0~PC3规则组连续转换 ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel ADC_CHANNEL_NUM; ADC_Init(ADC1, ADC_InitStructure); // 配置通道PC0CH10, PC1CH11, PC2CH12, PC3CH13 ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_55_5Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_55_5Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_12, 3, ADC_SampleTime_55_5Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_13, 4, ADC_SampleTime_55_5Cycles); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); } uint8_t LF_ReadSensors(void) { // 读取四路ADC值 for(uint8_t i0; iADC_CHANNEL_NUM; i) { adc_value[i] ADC_GetConversionValue(ADC1); } // 二值化高于阈值1白低于0黑 uint8_t sensor_state 0; for(uint8_t i0; iADC_CHANNEL_NUM; i) { if(adc_value[i] threshold) sensor_state | (1 i); } // 状态编码约定PC0左2PC1左1PC2右1PC3右2 // 0000全白脱线1111全黑0110居中1100左偏0011右偏... return sensor_state; }方案B简易位置式PID进阶推荐当你发现阈值法在弯道容易冲出就可以启用PID。它把四路ADC值看作一个“灰度图像”计算“质心”位置质心位置 (0*val0 1*val1 2*val2 3*val3) / (val0 val1 val2 val3) 理想质心 1.5四路均匀分布时 偏差error 1.5 - 质心位置然后用output Kp * error Ki * integral_error Kd * (error - last_error)计算左右轮速差。代码里Kp/Ki/Kd都定义为宏方便你在main.c顶部直接修改#define KP 20.0f #define KI 0.1f #define KD 5.0f float pid_output 0.0f; float integral_error 0.0f; float last_error 0.0f; float LF_PID_Calculate(void) { float val[4] {adc_value[0], adc_value[1], adc_value[2], adc_value[3]}; float sum val[0] val[1] val[2] val[3]; if(sum 0) return 0.0f; // 防除零 float centroid (0*val[0] 1*val[1] 2*val[2] 3*val[3]) / sum; float error 1.5f - centroid; integral_error error * 0.05f; // 采样周期50ms float derivative (error - last_error) / 0.05f; last_error error; pid_output KP * error KI * integral_error KD * derivative; return pid_output; }注意事项- TCRT5000的安装高度至关重要离地2~3mm最佳太高5mm环境光干扰大太低1mm易刮擦- 四路传感器间距建议3~5cm太密无法分辨弯道太疏在急弯会“跳线”- 白色打印纸反光太强建议用哑光白卡纸或PVC白板黑色胶带务必用电工胶布哑光黑透明胶带反光会导致误判。4. L298N电机驱动与运动控制不是“给高电平就转”而是H桥的逻辑艺术L298N不是简单的开关芯片而是一个双H桥驱动器。每个H桥由4个MOSFET组成通过控制IN1/IN2的电平组合决定电机的转向与制动。很多人接线后电机不转第一反应是“芯片坏了”其实90%是逻辑电平没配对。这套工程的电机控制逻辑封装在Code_User/motor_control.c中核心是Motor_SetSpeed()函数它接受两个参数left_speed-100~100和right_speed-100~100负数表示反转。// L298N引脚定义根据例程说明.txt #define MOTOR_LEFT_IN1 GPIO_Pin_6 // PA6 #define MOTOR_LEFT_IN2 GPIO_Pin_7 // PA7 #define MOTOR_RIGHT_IN1 GPIO_Pin_8 // PB8 #define MOTOR_RIGHT_IN2 GPIO_Pin_9 // PB9 #define MOTOR_LEFT_ENA GPIO_Pin_10 // PB10 (TIM3_CH3 PWM) #define MOTOR_RIGHT_ENB GPIO_Pin_11 // PB11 (TIM3_CH4 PWM) void Motor_Init(void) { // 初始化所有INx引脚为推挽输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin MOTOR_LEFT_IN1 | MOTOR_LEFT_IN2; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin MOTOR_RIGHT_IN1 | MOTOR_RIGHT_IN2; GPIO_Init(GPIOB, GPIO_InitStructure); // 初始化ENA/ENB为复用推挽PWM输出 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin MOTOR_LEFT_ENA | MOTOR_RIGHT_ENB; GPIO_Init(GPIOB, GPIO_InitStructure); // TIM3 PWM初始化CH3/CH472MHz主频PSC719ARR999 → 10kHz PWM TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 719; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM2; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; // 初始占空比0% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC3Init(TIM3, TIM_OCInitStructure); TIM_OC4Init(TIM3, TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); TIM_CtrlPWMOutputs(TIM3, ENABLE); } void Motor_SetSpeed(int8_t left_speed, int8_t right_speed) { // 左轮控制 if(left_speed 0) { GPIO_ResetBits(GPIOA, MOTOR_LEFT_IN1); GPIO_SetBits(GPIOA, MOTOR_LEFT_IN2); TIM_SetCompare3(TIM3, left_speed); // 占空比速度值 } else if(left_speed 0) { GPIO_SetBits(GPIOA, MOTOR_LEFT_IN1); GPIO_ResetBits(GPIOA, MOTOR_LEFT_IN2); TIM_SetCompare3(TIM3, -left_speed); } else { GPIO_ResetBits(GPIOA, MOTOR_LEFT_IN1 | MOTOR_LEFT_IN2); // 制动 TIM_SetCompare3(TIM3, 0); } // 右轮同理... if(right_speed 0) { GPIO_ResetBits(GPIOB, MOTOR_RIGHT_IN1); GPIO_SetBits(GPIOB, MOTOR_RIGHT_IN2); TIM_SetCompare4(TIM3, right_speed); } else if(right_speed 0) { GPIO_SetBits(GPIOB, MOTOR_RIGHT_IN1); GPIO_ResetBits(GPIOB, MOTOR_RIGHT_IN2); TIM_SetCompare4(TIM3, -right_speed); } else { GPIO_ResetBits(GPIOB, MOTOR_RIGHT_IN1 | MOTOR_RIGHT_IN2); TIM_SetCompare4(TIM3, 0); } }关键逻辑解释-正转IN10, IN21 → 电流从OUT2流向OUT1电机正转-反转IN11, IN20 → 电流从OUT1流向OUT2电机反转-制动IN10, IN20 → OUT1和OUT2都被拉低电机内部形成短路快速停转比单纯断电更稳-堵转保护代码里没写但你在main.c的主循环里应该加入电流检测L298N的SENSE引脚接运放当Motor_SetSpeed()后电流持续1A超过500ms自动降速或报警。5. 实操全流程与避坑指南从开箱到小车飞驰的每一步5.1 开发环境搭建Keil uVision5的“零配置”秘诀你不需要下载任何额外插件或补丁。这套工程基于Keil MDK-ARM v5.27兼容v5.14所有配置已固化在.uvoptx和.uvprojx文件中。只需三步安装Keil uVision5官网下载注册免费License即可安装STM32F1系列Device Family PackKeil菜单Pack Installer→ 搜索STM32F1→ Install打开工程双击智能小车_遥控_V1.0.uvprojx点击Project → Options for Target→Device选项卡确认芯片型号是STM32F103C8或STM32F103RB根据你的板子选Target选项卡里晶振频率填8000000外部8MHz晶振这是F103的标准配置。常见问题排查-编译报错undefined symbol SystemInit说明System目录下的system_stm32f10x.c没被添加到工程。右键Source Group 1→Add Existing Files to Group添加System/system_stm32f10x.c-下载失败提示“No Cortex-M device found”检查ST-Link驱动是否安装官网下载STSW-LINK009USB线是否插稳开发板上的BOOT0跳线是否为0运行模式-串口printf无输出确认Core/usart.c里USART1的TX引脚PA9已正确连接USB转TTL模块波特率在USART_Printf()里是115200终端软件如Xshell必须设为相同波特率。5.2 硬件接线实录一张表搞定所有迷思功能模块STM32引脚接线说明关键注意事项红外接收PB1VS1838B的OUT脚 → PB1VCC→5VGND→GNDPB1必须接10kΩ上拉电阻到5V超声波TrigPA0HC-SR04的Trig脚 → PA0PA0是TIM2_CH1勿与其他功能复用超声波EchoPB0HC-SR04的Echo脚 → PB0PB0是TIM2_CH2若开发板PB0接了其他外设需改引脚循迹传感器PC0~PC3TCRT5000的四路OUT → PC0, PC1, PC2, PC3传感器供电用5V输出接STM32的3.3V IO口有容限L298N左轮PA6,PA7,PB10IN1→PA6, IN2→PA7, ENA→PB10TIM3_CH3ENA必须接PWM引脚否则无法调速L298N右轮PB8,PB9,PB11IN1→PB8, IN2→PB9, ENB→PB11TIM3_CH4同上电源—L298N的VCC接7.4V锂电池或6~12V适配器GND与STM32共地5V_OUT给VS1838B/TCRT5000供电严禁用STM32的3.3V给电机或超声波供电实操心得第一次接线建议用杜邦线面包板不要焊死。先只接红外模块烧录遥控工程用示波器看PB1波形确认红外正常后再加超声波最后加循迹。每次只增一个模块问题定位快十倍。5.3 三大功能联调技巧如何让小车“听话”单功能验证通过后下一步是组合。但别急着写新工程用现有三个工程交叉调试遥控避障组合烧录遥控工程在Motor_Control_By_Key()函数里加入避障逻辑。例如当按键是“前进”时先调用US_GetDistance()如果dist 20则不执行前进改为右转2秒。代码只需加5行c if(key KEY_FORWARD) { uint16_t dist US_GetDistance(); if(dist 0 dist 20) { Motor_SetSpeed(50, -50); // 原地右转 delay_ms(2000); } else { Motor_SetSpeed(80, 80); // 正常前进 } }循迹避障组合烧录循迹工程在LF_PID_Calculate()之后插入避障判断。当dist 15时强制让小车停止等待障碍物移开后再继续循迹。这比“边走边避障”更稳定适合初学者。终极挑战三功能融合创建新工程复制三个Code_User目录下的.c/.h文件统一管理。这时要注意中断优先级红外EXTI1设为最高NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0超声波TIM2设为次高1循迹ADC设为最低2。否则红外按键会打断测距导致避障失效。5.4 经典问题速查表那些让你抓狂半小时的“小问题”现象最可能原因解决方案红外遥控无响应VS1838B供电不足4.5VPB1未接上拉遥控器电池没电载波频率不匹配部分国产遥控用36kHz用万用表测VS1838B VCC检查PB1上拉换遥控器修改ir_decode.c中脉宽容差范围超声波测距始终为0PA0无10μs脉冲示波器确认PB0未接EchoHC-SR04损坏环境温度过高声速变快示波器查PA0万用表查PB0电平变化换HC-SR04在US_GetDistance()里把/58.0f改为/60.0f高温补偿循迹小车总往一边跑四路TCRT5000灵敏度不一致白色校准不准确地面反光不均电机左右轮速不一致用万用表测四路ADC值手动调整threshold重新校准换哑光白板用Motor_SetSpeed(60,60)单独测试左右轮速电机转动无力或异响L298N散热片未装大电流下过热降频电池电压6.5VIN1/IN2电平接反PWM频率太低1kHz加装铝散热片换满电电池查原理图确认IN1/IN2在Motor_Init()里把TIM3的TIM_Period从999改为1995kHz小车跑着跑着突然停了电池电量告警STM32的VDDA电压监测未启用电机堵转触发保护代码未写无线干扰遥控器频段冲突在main.c里加入if(ADC_GetConversionValue(ADC1) 1800) Motor_SetSpeed(0,0);增加堵转检测换遥控器我踩过的最大坑有一批TCRT5000传感器出厂时发射管电流被厂商标定为20mA但我的板子只给了10mA导致输出信号幅度只有1.2VSTM32的2.0V高电平阈值。折腾三天才发现最后在传感器VCC和GND之间并联一个100μF电解电容瞬间解决。所以永远相信硬件手册但更要相信自己的万用表。6. 后续扩展与进阶方向你的小车不止于这三功能这套工程的价值不仅在于它能跑更在于它是一块“乐高底板”。所有扩展都遵循同一个原则只在Code_User目录下新增文件不动FWLib和Core。6.1 OLED状态显示让小车“会说话”买一块0.96寸SSD1306 OLEDI2C接口接在PB6SCL、PB7SDA。新建oled.c/h用标准I2C驱动stm32f10x_i2c.c已在FWLib中。在main.c里初始化OLED然后在主循环里实时刷新// main.c 主循环内 uint16_t dist US_GetDistance(); uint8_t state LF_ReadSensors(); char buf[32]; sprintf(buf, Dist:%dcm State:%04b, dist, state); OLED_ShowString(0, 0, buf, 16); // 显示在OLED第一行小技巧OLED的I2C地址通常是0x78写或0x7A读但有些模块是0x3C用I2C扫描工具如Arduino的I2CScanner先确认。6.2 蓝牙串口透传手机APP遥控升级加一个HC-05蓝牙模块UART接口TX→PA10USART1_RXRX→PA9USART1_TX。新建bt_control.c在USART1_IRQHandler()里解析手机发来的指令字符串如FWD:80表示前进速度80。这样你不用红外遥控器用手机串口助手就能控制。6.3 WiFi远程控制接入物联网的钥匙换成ESP8266-01S模块AT指令集同样接USART1。写一个wifi_control.c初始化WiFi连接家庭路由器开启TCP服务器。手机用网络调试助手连接IP:8080发送JSON指令{cmd:forward,speed:70}小车就能响应。这时你的小车已经是一个边缘节点可以接入Home Assistant或微信小程序。个人体会我最初做这个项目只是为了给大二学生上嵌入式实践课。没想到半年后有个学生把它改装成仓库巡检小车加了温湿度传感器和蜂鸣器每天自动巡逻三次发现异常温度就发短信报警。技术本身没有边界边界只在你的想象力里。而这套工程就是帮你把想象力稳稳落地的第一块基石。本文还有配套的精品资源点击获取简介基于STM32F103C8T6等主流型号提供三个开箱即用的Keil uVision工程遥控版支持VS1838B红外接收解析NEC协议实现前进/后退/左转/右转/启停避障版集成HC-SR04超声波模块配合L298N驱动芯片根据设定距离自动刹车或转向循迹版采用TCRT5000红外对管阵列通过阈值判断或简易PID算法稳定跟踪黑白边界线。所有工程均含标准固件库结构System/Core/FWLib/Object/Code_User引脚已预定义无需修改配置即可编译下载。配套例程说明.txt明确列出各模块硬件接线图、GPIO分配如PA0接超声波Trig、PB1接红外OUT、关键参数调节位置如循迹灵敏度阈值、避障触发距离及典型问题解决方法如电机不转排查方向信号电平、红外无响应检查供电与载波频率。代码全部使用标准C编写函数划分清晰关键逻辑处附中文注释适合从点灯入门到综合项目实践的嵌入式学习者也方便快速接入OLED显示、蓝牙串口透传或WiFi模组扩展远程控制能力。本文还有配套的精品资源点击获取

相关新闻