当前位置: 首页 > news >正文

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,支持左偏/右偏/居中/脱线四种状态判断),再在注释里手把手教你如何把KpKiKd参数从零填起,调出不抖不冲的转向响应。配套的例程说明.txt更不是摆设——它明确告诉你:PB1接VS1838B的OUT脚,PA0是HC-SR04的Trig,PB0是Echo,PC0~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脚必须接到开发板的PB1,HC-SR04的Trig接PA0、Echo接PB0,TCRT5000的四路OUT分别接PC0~PC3。为什么这么定?因为:

  • PB1是EXTI线1,VS1838B输出的是38kHz载波调制的方波,需要用外部中断边沿触发捕获(下降沿启动计时,上升沿停止),PB1天然支持EXTI1;
  • PA0和PB0组合,可以共用一个TIM2定时器:PA0输出PWM触发超声波,PB0输入捕获Echo高电平时间,TIM2_CH1和CH2正好配对;
  • PC0~PC3是ADC1的通道10~13,四路模拟量同时采样,无需切换通道,效率最高。

实操心得:如果你用的是正点原子的战舰V3开发板,它的PB1被蜂鸣器占用了。怎么办?很简单,把IR_GPIO_PIN改成GPIO_Pin_12(PD12,EXTI线12),再把VS1838B接到PD12,然后在IR_Init()里把EXTI_LineEXTI_Line1改成EXTI_Line12NVIC_IRQChannelEXTI1_IRQn改成EXTI15_10_IRQn。整个过程5分钟,不碰其他代码。

3. 核心功能模块深度解析:从原理到代码,每一行都经得起追问

3.1 红外遥控:NEC协议不是“收到脉冲就执行”,而是严谨的状态机

VS1838B这类一体化红外接收头,输出的是经过放大、滤波、整形后的TTL电平信号。它不是直接给你原始红外光波,而是把NEC协议的38kHz载波解调出来,变成一串高低电平组合。很多人以为“检测到下降沿就记一次键值”,结果发现按键失灵、重复触发。真相是:NEC协议是一个严格的时间编码协议,必须用定时器精确测量每个脉冲宽度,再根据宽度分类为逻辑0、逻辑1、引导码、结束码

这套工程的红外解码放在Code_User/ir_decode.c里,核心是一个基于TIM3的输入捕获状态机。TIM3配置为向上计数模式,预分频器PSC=72-1(即1MHz计数频率),所以每个计数值=1μs。当PB1产生下降沿时,触发EXTI1中断,在中断里启动TIM3;下一个上升沿到来时,捕获CCR1寄存器值,得到第一个脉冲宽度;再下一个下降沿,再捕获……如此循环,构建一个9个元素的数组pulse_width[9],对应NEC帧的结构:

序号名称标准时长允许误差作用
0引导码高电平9000μs±500μs告诉接收端:一帧开始了
1引导码低电平4500μs±500μs
2地址码bit0560/1690μs低电平560μs=0,高电平1690μs=1
3地址码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 i=0; i<8; i++) { if(pulse_width[2+i] > 1200) addr |= (1<<i); // 高电平长→逻辑1 } return addr; // 返回地址码,对应遥控器按键 } return 0xFF; // 无效帧 }

注意事项:
- VS1838B的供电必须稳定在5V,3.3V供电会导致灵敏度暴跌,10米外就收不到信号;
- PB1引脚必须接10kΩ上拉电阻到5V(开发板若已集成可忽略),否则空闲时电平浮动,误触发中断;
-pulse_width[]数组必须用volatile修饰,因为被中断修改,主循环读取时不能被编译器优化掉。

3.2 超声波避障:HC-SR04不是“发个脉冲等回响”,而是精密的时序协同

HC-SR04的工作流程教科书上写得很简单:“Trig脚给10μs高电平,模块自动发出8个40kHz方波,Echo脚输出高电平,持续时间=距离×58μs”。但真实世界里,Echo高电平可能只有几微秒(近距离),也可能长达23ms(4米远),还夹杂着环境噪声。如果用普通GPIO查询方式,CPU一直在while(!GPIO_ReadInputDataBit())里空转,既耗电又不准。

这套工程采用TIM2输入捕获+DMA辅助的方案。TIM2配置为:
- CH1(PA0)为输出比较模式,生成精确10μs高电平脉冲(TIM_SetCompare1(TIM2, 10),因为CK_CNT=1MHz);
- CH2(PB0)为输入捕获模式,上升沿触发捕获,记录Echo高电平起点;下降沿再捕获一次,得到高电平持续时间。

核心代码(ultrasonic_avoid.c):

void 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); // 单位:cm,58μs/cm是25℃声速换算值 // 防错:距离超出400cm或小于2cm视为无效 if(distance > 400 || distance < 2) distance = 0; return distance; }

实操心得:
- HC-SR04的VCC必须接5V,GND要和STM32共地,否则Echo电平不匹配(HC-SR04输出5V TTL,STM32输入耐压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~13(PC0~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); // 配置通道:PC0=CH10, PC1=CH11, PC2=CH12, PC3=CH13 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 i=0; i<ADC_CHANNEL_NUM; i++) { adc_value[i] = ADC_GetConversionValue(ADC1); } // 二值化:高于阈值=1(白),低于=0(黑) uint8_t sensor_state = 0; for(uint8_t i=0; i<ADC_CHANNEL_NUM; i++) { if(adc_value[i] > threshold) sensor_state |= (1 << i); } // 状态编码(约定:PC0=左2,PC1=左1,PC2=右1,PC3=右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/CH4,72MHz主频,PSC=719,ARR=999 → 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); } }

关键逻辑解释:
-正转:IN1=0, IN2=1 → 电流从OUT2流向OUT1,电机正转;
-反转:IN1=1, IN2=0 → 电流从OUT1流向OUT2,电机反转;
-制动:IN1=0, IN2=0 → OUT1和OUT2都被拉低,电机内部形成短路,快速停转(比单纯断电更稳);
-堵转保护:代码里没写,但你在main.c的主循环里应该加入电流检测(L298N的SENSE引脚接运放),当Motor_SetSpeed()后电流持续>1A超过500ms,自动降速或报警。

5. 实操全流程与避坑指南:从开箱到小车飞驰的每一步

5.1 开发环境搭建:Keil uVision5的“零配置”秘诀

你不需要下载任何额外插件或补丁。这套工程基于Keil MDK-ARM v5.27(兼容v5.14+),所有配置已固化在.uvoptx.uvprojx文件中。只需三步:

  1. 安装Keil uVision5(官网下载,注册免费License即可);
  2. 安装STM32F1系列Device Family Pack(Keil菜单Pack Installer→ 搜索STM32F1→ Install);
  3. 打开工程:双击智能小车_遥控_V1.0.uvprojx,点击Project → Options for TargetDevice选项卡,确认芯片型号是STM32F103C8STM32F103RB(根据你的板子选),Target选项卡里晶振频率填8000000(外部8MHz晶振,这是F103的标准配置)。

常见问题排查:
-编译报错undefined symbol SystemInit:说明System目录下的system_stm32f10x.c没被添加到工程。右键Source Group 1Add Existing Files to Group,添加System/system_stm32f10x.c
-下载失败,提示“No Cortex-M device found”:检查ST-Link驱动是否安装(官网下载STSW-LINK009),USB线是否插稳,开发板上的BOOT0跳线是否为0(运行模式);
-串口printf无输出:确认Core/usart.cUSART1的TX引脚(PA9)已正确连接USB转TTL模块,波特率在USART_Printf()里是115200,终端软件(如Xshell)必须设为相同波特率。

5.2 硬件接线实录:一张表搞定所有迷思

功能模块STM32引脚接线说明关键注意事项
红外接收PB1VS1838B的OUT脚 → PB1,VCC→5V,GND→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→PB10(TIM3_CH3)ENA必须接PWM引脚,否则无法调速
L298N右轮PB8,PB9,PB11IN1→PB8, IN2→PB9, ENB→PB11(TIM3_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.5V);PB1未接上拉;遥控器电池没电;载波频率不匹配(部分国产遥控用36kHz)用万用表测VS1838B VCC;检查PB1上拉;换遥控器;修改ir_decode.c中脉宽容差范围
超声波测距始终为0PA0无10μs脉冲(示波器确认);PB0未接Echo;HC-SR04损坏;环境温度过高(声速变快)示波器查PA0;万用表查PB0电平变化;换HC-SR04;在US_GetDistance()里把/58.0f改为/60.0f(高温补偿)
循迹小车总往一边跑四路TCRT5000灵敏度不一致;白色校准不准确;地面反光不均;电机左右轮速不一致用万用表测四路ADC值,手动调整threshold;重新校准;换哑光白板;用Motor_SetSpeed(60,60)单独测试左右轮速
电机转动无力或异响L298N散热片未装(大电流下过热降频);电池电压<6.5V;IN1/IN2电平接反;PWM频率太低(<1kHz)加装铝散热片;换满电电池;查原理图确认IN1/IN2;在Motor_Init()里把TIM3的TIM_Period从999改为199(5kHz)
小车跑着跑着突然停了电池电量告警(STM32的VDDA电压监测未启用);电机堵转触发保护(代码未写);无线干扰(遥控器频段冲突)main.c里加入if(ADC_GetConversionValue(ADC1) < 1800) Motor_SetSpeed(0,0);;增加堵转检测;换遥控器

我踩过的最大坑:有一批TCRT5000传感器,出厂时发射管电流被厂商标定为20mA,但我的板子只给了10mA,导致输出信号幅度只有1.2V(<STM32的2.0V高电平阈值)。折腾三天才发现,最后在传感器VCC和GND之间并联一个100μF电解电容,瞬间解决。所以,永远相信硬件手册,但更要相信自己的万用表。

6. 后续扩展与进阶方向:你的小车,不止于这三功能

这套工程的价值,不仅在于它能跑,更在于它是一块“乐高底板”。所有扩展,都遵循同一个原则:只在Code_User目录下新增文件,不动FWLibCore

6.1 OLED状态显示:让小车“会说话”

买一块0.96寸SSD1306 OLED(I2C接口),接在PB6(SCL)、PB7(SDA)。新建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→PA10(USART1_RX),RX→PA9(USART1_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模组扩展远程控制能力。


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

http://www.jsqmd.com/news/968908/

相关文章:

  • 2026年AI豆包GEO推广深度测评:深圳昊客网络一风AI脱颖而出 - 猫头鹰AI推广
  • 常州市天宁区黄金回收指南:金价高企如何安全变现? - 专业黄金回收
  • 如何构建iOS游戏修改神器:H5GG JavaScript引擎深度解析与实战指南
  • 047、Quad Bayer 四合一像素:Binning 模式下的分辨率与信噪比权衡
  • 宁波2026年6月名表回收实测:从开盖到到账的全程记录 - 薛定谔的梨花猫
  • openLCA 2.6.2 开源生命周期评估软件:免费可持续性分析终极指南
  • 2026 石家庄手表回收怎么挑商户 添价收实体门店口碑出众 - 薛定谔的梨花猫
  • LabVIEW在线模式遥控乐高NXT机器人:蓝牙通信与实时避障实践
  • FPGA调试实战:SignalTap II嵌入式逻辑分析仪原理与EBI接口时序验证
  • 3步彻底解决Realtek 8852AE无线网卡在Linux上的技术调优与性能优化终极方案
  • 美度品牌官方售后网点全面核查报告(含搬迁及新增站点)|实地走访与多重交叉验证|2026年6月最新发布 - 亨得利官方服务中心
  • 2026新疆旅行封神攻略|私藏8位本地金牌导游,让你无脑玩转新疆 - 必辉旅行
  • **主标题**:新能源汽车维修工程师哪里找?[城市名] [企业名] **备选标题**:热门新能源汽车维修工程师[城市名] [企业名]有吗? - 资讯纵览
  • yuzu模拟器完整指南:在电脑上完美运行Switch游戏的终极教程
  • 苏州军事夏令营选哪家?避坑+种草整理,家长直接抄作业 - 资讯纵览
  • Windows端口转发终极指南:如何用PortProxyGUI快速配置网络代理
  • 深入解析LED效率下降:从芯片物理到系统热管理的全链路优化
  • 济南历下区金价实探:6家回收机构全维度实测 - 专业黄金回收
  • 微信聊天记录永久保存:3步导出完整历史,让珍贵对话永不丢失
  • Windows 11性能优化实战:从臃肿到流畅的终极系统定制指南
  • K210开发板MicroPython环境搭建实战:从驱动安装到AI模型部署
  • FPGA软核处理器PicoBlaze:轻量级嵌入式控制与协处理器设计实战
  • 2026 孝感漏水维修全攻略|苏易修缮:厨卫 / 阳台 / 外墙 / 屋顶 / 地下室|靠谱防水门店 - 苏易修缮
  • 20250607OIFHA总结
  • 硬件研发如何从源头避免缺货:器件归一化与供应链协同设计
  • 主标题:新能源汽车热门培训,助力维修技能提升[地域]企业 备选标题:热门新能源汽车维修培训,[地域]企业开启技能新篇 - 资讯纵览
  • MASA模组全家桶汉化包:为中文玩家打造的终极本地化解决方案
  • Visual C++运行库全版本修复工具:5分钟解决Windows软件兼容性问题
  • 2026 西安屋面漏水渗水维修机构 TOP4:专业修缮机构优选盘点 专业防水公司排名推荐(2026年5月防水补漏最新TOP权威排名) - 冠盾建筑修缮
  • WrenAI架构深度解析:如何为AI代理构建企业级数据上下文层