简介:基于STM32F103C8T6的智能小车测速与行驶距离显示实验源码,采用Keil4工程组织,主要为STM32入门者、智能小车爱好者和相关课程设计提供可直接运行的参考实现。程序适配L293D电机驱动芯片、TT直流减速电机、测速模块和OLED液晶屏,通过外部中断和定时器测量单位时间脉冲数,计算实时车速并积分得到行驶里程,最终在OLED屏上刷新显示。整套资源压缩包约372KB,共42个文件,以C语言源文件、H头文件、Keil工程文件和hex/axf可烧录固件为主,同时附带map/lst链接列表、L293D驱动电路原理图和必看说明txt,工程文件组织规范,省去自行创建工程的步骤,打开即可编译烧录,方便对照理解结构与接线。代码在作者自制的STM32F103C8T6智能小车上亲测可用,已有1762人学习,模块划分清晰,注释与电路图配套完整,适合二次开发和毕业设计参考,也可按需调整测速参数以适配不同车体。
1. 为什么你手里的小车测速总差一半
里程和速度算不准,通常不是硬件坏,而是计数方式没想清楚。这套stm32f103c8t6智能小车测速实验,核心就是用光电编码器把TT直流减速电机的转动变成脉冲,再用STM32F103C8T6的外部中断配合定时器做M法测速,最后把速度值和累计行驶距离推到OLED上。它不依赖编码器专用接口,拿普通GPIO就能完成,非常适合手头只有一块最小系统板的场景。
适合谁:正在调智能小车循迹、壁障,发现轮子空转和实际位移对不上;或者刚把L293D驱动跑通,想加一个闭环速度反馈但不想换带硬件编码器接口的单片机。这套Keli4工程的价值在于把测速、距离累计、OLED显示三个模块拆得干净,能直接套进别的项目当速度反馈子模块。下面按硬件接线、M法测速原理、中断配置、显示修正四个层次把它剥开。
2. 硬件接线与传感器选型:L293D如何配合TT电机编码器
2.1 测速模块为什么选遮挡式红外,不选霍尔
这套程序配套的测速模块是常见的光电遮挡式,也就是码盘装在TT电机输出轴上,红外对管卡在码盘两侧。码盘旋转时,齿片交替挡住和放行红外光,接收管输出方波脉冲。
霍尔方案在STM32F103C8T6小车里偶尔也有人用,但TT减速电机自带的磁环磁极对数不固定,不同批次差异大,标定麻烦。遮挡式码盘一圈齿数是固定的,常见的为20或30格,单位换算时只需确认齿数,不需要额外测磁极对。对做行驶距离累计这个需求来说,遮挡式更便于工程计算。
硬件连接分两条链路:
- 电机驱动链路:STM32F103C8T6的PA1、PA2输出PWM到L293D的EN1、EN2控制使能,PB0、PB1接L293D的IN1、IN2控制方向。
- 测速模块链路:测速模块VCC接3.3V或5V(取决于模块有无上拉,通常5V供电,输出接单片机引脚时用3.3V板需留意电平兼容),GND共地,OUT接STM32F103C8T6的外部中断引脚PA0。
测速模块信号线 → PA0(EXTI0) L293D IN1 → PB0 L293D IN2 → PB1 L293D ENA → PA1(PWM输出)注意:如果测速模块没有内置开漏输出和三极管电平转换,直接接PA0前最好串联一个1kΩ电阻并加3.3V上拉,避免模块输出5V高电平灌进PA0。部分模块标注VCC支持5V,但输出引脚实际为5V电平,长时间直接接单片机引脚会有潜在风险。
2.2 码盘齿数与距离换算的关系
TT电机带减速器,输出轴转速不等于电机转子转速。测速模块放在输出轴端时,码盘每转一圈输出脉冲数等于码盘齿数。行驶距离则依赖轮径和减速比:
码盘齿数 = 20(常见,以实物为准) 减速比 = 1:48 轮径 = 65mm 编码器脉冲频率 f_count 轮转速 = f_count / 码盘齿数 圈/秒 车速 = 轮转速 × π × 轮径 毫米/秒 行驶距离 = 累计脉冲总数 / 码盘齿数 × π × 轮径这个资源包里的代码并不是把上面公式全部封装成浮点库,而是用简单的整型累加和定点缩放处理,具体实现下一章分析。先记住一个结论:如果你的码盘不是20齿,比如是30齿,就需要修改代码里的ZONGCHI宏,否则所有速度、距离显示会偏差50%甚至更多,这是多数人拿到工程直接烧录后第一个踩的坑。
3. Keil4工程结构与M法测速代码实现
3.1 三层文件:SYSTEM、OBJ、USER
解压后的工程目录分三层:
- SYSTEM:存放底层驱动,包含delay、sys、usart,红外测速模块的脉冲计数在定时器中断中完成。
- USER:主函数、中断服务函数和OLED显示逻辑所在位置。
- OBJ:编译输出中间文件。
代码开发环境是Keil4,打开USER下的工程文件即可。但Keil4对Win10/Win11的兼容性需要留意,最常见的是编译时报Error: C9552E: Cannot open file,原因是路径中含中文或空格。建议工程路径保持全英文,例如放在D:\car_speed_test\,不要放桌面(如果桌面路径含中文用户名)。
3.2 M法测速的实现逻辑
M法测速的核心是:固定一个时间窗口,统计该窗口内的脉冲数,再用脉冲数除以时间得频率,换算成速度。相比T法(测量相邻两个脉冲的间隔时间),M法适合转速较高、脉冲密度大的场景。TT电机经1:48减速后,输出轴转速约200-400转/分钟,码盘20齿,每秒脉冲数约66-133个,M法非常合适。
代码中用一个定时器产生1秒或100ms的中断窗口,同时用外部中断对脉冲计数。窗口中断到来时,读取累计脉冲数,计算速度,清空计数器,再把脉冲加入总里程累积。核心代码结构如下:
// 外部中断服务函数,每次脉冲触发一次 void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) != RESET) { pulse_count++; // 脉冲计数加1 total_pulse++; // 总脉冲累计,用于里程计算 EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志位 } }逻辑说明:pulse_count供速度测量使用,每个定时窗口清零;total_pulse从开机开始一直递增,不手动清零。两者分离的好处是速度计算不会影响到累计里程,即使你重置速度显示,历史里程仍然保留。
// 定时器2中断,用于固定时间窗口 void TIM2_IRQHandler(void) { static unsigned int time_window_counter = 0; if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); time_window_counter++; if(time_window_counter >= 100) // 100个10ms = 1秒 { time_window_counter = 0; speed = pulse_count; // 每秒脉冲数就是速度base pulse_count = 0; } } }定时器设置为10ms中断一次,每100次即为1秒。speed这时代表每秒脉冲数,不是实际米/秒,需要在主循环中再做一次单位换算:
// 码盘20齿,轮径65mm // 每秒脉冲数speed → 每秒转数 → 每秒毫米数 float wheel_circumference = 3.14159f * 65.0f; float speed_mm_per_sec = (float)speed / 20.0f * wheel_circumference; float distance_m = (float)total_pulse / 20.0f * wheel_circumference / 1000.0f;参数说明:20是码盘齿数,65是车轮直径(毫米),二者需要根据你的小车实物修改。其中total_pulse换成float计算前,注意溢出问题:unsigned int最大约42亿,按每秒133脉冲算,可持续运行超过370天,不会溢出,但主循环里浮点运算会占较多CPU时间,实际代码若用整数定点法替代更稳妥。
3.3 为什么不把测速放在剩下的定时器通道里
有些SmartCar项目用TIM编码器接口直接读正交编码器,但STM32F103C8T6只有两个定时器带编码器模式(TIM1、TIM2),其余定时器没有。这套资源的硬件用的是普通遮挡式红外模块,输出TTL方波,无法直接进正交解码接口。用GPIO外部中断是成本最低、兼容性最好的做法。代价是:脉冲频率高时外部中断会占用较多CPU资源,但当电机最高速约366转/分钟、码盘20齿时,每秒中断约122次,对72MHz主频来说消耗可忽略。
4. 外部中断与定时器配置的边界问题
4.1 GPIO复用和中断线映射的限制
stm32f103c8t6引脚功能和中断线映射有一定限制,需要在配置时格外注意。外部中断线0只能映射到PA0、PB0、PC0等引脚,不同引脚之间共用中断线。当PA0被设置为EXTI0后,PB0就不能再作为EXTI0输入。
本工程里,PA0用于脉冲输入,PB0、PB1用于L293D方向控制,没有冲突。但如果你同时想用另一路编码器测右轮速度,就要注意:右路脉冲不能再用PC0、PD0等映射到EXTI0,必须选EXTI1(PA1)或EXTI2(PA2)。只要引脚不同且中断线不冲突,代码只需要增加一个中断服务函数,并在NVIC配置中使能对应中断。
初学容易犯的错误是,把两个轮子的编码器一个接PA0、一个接PB0,然后代码里只写一个EXTI0_IRQHandler,结果两个信号都进同一个中断,脉冲数叠加成一个计数器值,左右轮无法分别测速。正确的接线方式是:左轮PA0(EXTI0),右轮PA1(EXTI1),分别写两个中断服务函数:
void EXTI1_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line1) != RESET) { pulse_count_right++; total_pulse_right++; EXTI_ClearITPendingBit(EXTI_Line1); } }4.2 定时器分频系数与测速窗口的搭配
定时器2的初始化代码中,预分频和自动重装直接决定了测速窗口周期。当测速窗口设置太长(比如1秒以上),速度刷新慢,OLED上有明显滞后感;设置太短(比如10ms),脉冲太稀疏(车轮低速时可能一个窗口只有1-2个脉冲),测出的速度跳动严重,甚至出现0、突跳、0交替。
对TT减速电机来说,1秒窗口是一个可接受的折中:低速下最低约每秒60个脉冲,窗口内脉冲数仍然足够统计。如果你希望显示器刷新更快,可以把窗口改到200ms,但主循环中速度换算需乘以5得到每秒脉冲数。
void TIM2_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1; // 72MHz / 7200 = 10kHz TIM_TimeBaseStructure.TIM_Period = 100 - 1; // 10kHz 100次 = 10ms TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }参数说明:TIM_Prescaler = 7200-1,等于把72MHz时钟分频成10kHz;TIM_Period = 100-1,重装值100,产生10ms中断,中断里再做100次累加形成1秒窗口。如果把TIM_Period改成500-1,则产生50ms中断,窗口累加次数相应调整为20次。修改时要保证两者乘积不变(100×10ms=50×20ms=1秒),否则速度基准就变了。
4.3 中断优先级配置的坑
外部中断与定时器中断之间需要设置优先级。推荐NVIC配置中把外部中断优先级设为高于定时器中断,因为外部中断是事件源,丢失脉冲会直接导致测量误差;定时器中断负责读数和清零,即使延迟几十微秒也不影响结果,只是计算时刻稍有波动。
低优先级的中断服务程序如果在响应时被高优先级打断20微秒,对一个10ms周期来说只占千分之二,几乎无感知。反过来,如果定时器中断优先打断外部中断,那么外部中断抢断期间到达的脉冲就会被漏掉,轮子转速越快,漏掉的越多,速度显示会成比例偏低。这是测速不准最隐蔽的原因,很多人查了计数逻辑半天,最后发现问题出在NVIC的NVIC_PriorityGroupConfig设置上。
常见配置:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStructure; // 外部中断线0优先级最高 NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 定时器中断优先级次之 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);用NVIC_PriorityGroup_2时,两个抢占优先级就足够不同。如果某块板子还有串口打印,串口中断建议设为抢占优先级2,避免调试信息干扰测速优先级。
4.4 OLED刷新逻辑与显示抖动
OLED模块如果是I2C接口,通常接PB10(SCL)和PB11(SDA),与测速用的PA0互不干扰。OLED刷新显示时,如果直接在每个测量周期内更新全部字符,屏幕上数字刷新会有些闪烁。常见做法是:只更新变化部分,即把速度、距离转换成字符串后,仅在当前值与上次值不一致时才调用OLED显示函数。另一个做法是用整型变量存储上次显示值,减少无谓的I2C写操作,同时降低对中断实时性的影响。
void Display_Speed_Distance(void) { char line1[16]; char line2[16]; unsigned int disp_speed = (unsigned int)(speed_mm_per_sec); unsigned int disp_distance = (unsigned int)(distance_m); // speed_changed和distance_changed为标志位,由主循环判断旧值是否变化 if(speed_changed) { OLED_ShowString(0, 0, "Vel(MM/S):"); OLED_ShowNum(70, 0, disp_speed, 4); speed_changed = 0; } if(distance_changed) { OLED_ShowString(0, 2, "Dist(M):"); OLED_ShowNum(70, 2, disp_distance, 6); distance_changed = 0; } }这段代码把屏幕分成两行,分别显示瞬时速度(单位mm/s)和累计距离(单位m)。OLED_ShowNum的数字参数是个数位控制参数,代码中显示4位速度、6位距离,如果实际距离超过6位数就需要改成8,否则超过显示位数的数字区域会被截断。
5. 距离校准:实测修正比改代码更有效
5.1 推一段实距,算真实脉冲/米
轮子在软地面和硬纸板上打滑距离差别很大,贴近实际的做法是:让小车在平整地面上走一段固定距离(比如5米),记录下OLED上累计距离,再算实际距离与显示距离的比值,修正代码中的wheel_circumference参数。
用遥控或手动推小车走5米,OLED显示4.15米,说明轮径或齿数参数偏小,修正量系数为5.0 / 4.15 = 1.2048。修改距离计算公式中的轮径:
// 修正前:wheel_circumference = 3.14159f * 65.0f; // 修正后:乘上实测修正系数 float wheel_circumference = 3.14159f * 65.0f * 1.2f;把1.2f换成你实测算出的修正系数1.2048f,再跑一遍同样距离,确认显示值趋近5米。这个系数同时影响速度显示和距离显示,因为它们共用同一个周长参数。如果你只对距离做修正、不动速度公式,会出现距离准了但速度仍然偏差1.2倍的问题。
5.2 启动和刹车阶段的处理
小车启动时,电机电流大,L293D的VCC如果电压跌落,测速模块的红外发射管电流变化可能使脉冲丢失;刹车时,车轮会因惯性多滑半圈,但编码器已经停止计数。两种误差方向相反,很多工程直接用累计脉冲算距离,结果总是偏小。
处理方案是在软件上做启动补偿:检测到方向信号变化后,启动阶段的前几个脉冲按双倍计数,变相补偿启动打滑。不过这属于经验修正,不适合所有路面。更可靠的方法是调整PWM启动曲线,让电机软启动,降低突然加速带来的打滑量。L293D驱动板的ENA引脚接PWM,可以靠PWM先输出30%占空比持续200ms,再逐步增加到目标值:
void Motor_SmoothStart(uint8_t target_pwm, uint16_t ramp_ms) { uint16_t i; uint8_t start_pwm = 30; // 起始占空比 uint8_t step = (target_pwm - start_pwm) / 20; for(i = 0; i < 20; i++) { TIM_SetCompare1(TIM1, start_pwm + step * i); delay_ms(ramp_ms / 20); // 分20步爬到目标占空比 } TIM_SetCompare1(TIM1, target_pwm); }5.3 OLED显示位置与小数位数
如果OLED显示距离时只能显示整数米,短距测试(比如1.2米)会一直显示0,肉眼会怀疑程序坏了。建议把距离显示切成厘米或毫米级:显示Dist(cm),数值(unsigned int)(distance_m * 100)。这样低速挪动和短距离测试都能看到数字立即变化,同时保留了小数精度。速度显示同理,单位建议用mm/s,因为OLED上4位数够用,你就不需要为凑小数点去写浮点格式化函数。
把这些修正做完,再把MCU主频、定时器初始化、中断优先级都复核一遍,这套源码就从一个演示实验变成能直接复用的速度反馈模块,换到其他轮式平台上时,只需改齿数、轮径和修正系数三处参数。
本文还有配套的精品资源,点击获取