ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于AT89C2051与ST1101的电动自行车速度里程表设计

基于AT89C2051与ST1101的电动自行车速度里程表设计

简介:这份资源是一份面向电子、自动化及单片机课程设计学习者的电动自行车速度与里程表设计方案文档,适合作为课设毕设选题参考或嵌入式测速入门案例。方案以AT89C2051单片机与ST1101红外光电传感器为核心,通过铝盘过孔产生脉冲信号,经零偏置放大器与施密特触发器整形后送入单片机定时器计数,再由LED模块交替显示速度与累计里程,并借助AT24C02与I2C总线完成里程数据存储。资源包共1个PDF文件,约94KB,内容涵盖系统概述、硬件电路设计、信号预处理、E2PROM应用、显示模块及软件流程等完整章节,配有原理框图和电路图,便于读者理解测速与里程计算的整体实现思路。目前已有717人学习下载,适合需要快速搭建方案框架、梳理硬件选型与算法逻辑的读者参考借鉴。

1. 从机械指针到LED直读:这套速度里程表方案到底解决了什么

电动自行车骑了三年,仪表盘上那根机械指针抖得跟心电图似的,里程数全靠猜——这不是个例。市面上大量在跑的电动自行车,速度表和里程表还是机械结构,靠软轴带动指针,用齿轮组累加里程。看起来能用,实际上速度显示滞后、里程误差随使用时间越滚越大,夜间骑行基本看不清。这套以AT89C2051单片机和ST1101红外光电传感器为核心的速度与里程表设计方案,就是冲着这个痛点来的:用非接触式光电检测替代机械软轴,用单片机算频率替代齿轮计数,用6位LED数码管替代指针表盘,速度和里程直接数字显示,还能在超速时报警。

它适合谁?做课设毕设的学生,需要一套从传感器选型、信号调理、单片机编程到显示驱动的完整链路;做小批量电动自行车仪表改装的工程师,想评估光电方案替代机械方案的可行性;还有带单片机课程的一线教师,需要一个能讲清楚“脉冲频率→速度→里程”这条数据流的实战案例。方案本身不复杂,但每个环节都有值得抠的细节——铝盘开几个孔、施密特触发器怎么选阈值、E2PROM什么时候写、里程累计怎么防止掉电丢数据。下面按硬件链路、软件逻辑、避坑排查、进阶技巧四块拆开讲。

2. 硬件链路拆解:从铝盘过孔到LED数码管的信号旅程

2.1 脉冲发生源:11个过孔和ST1101的配合逻辑

整套系统的起点是后轮轴上的铝盘。铝盘与轮子旋转切面平行安装,边沿均匀挖出11个圆形过孔,留下10个等间距的实体部分。ST1101红外光电传感器固定在铝盘边沿处,检测部分对准过孔圆心位置。当铝盘随轮子旋转时,红外发光二极管和光电晶体管之间交替出现“有孔”和“无孔”状态——有孔时红外光穿过,光电晶体管导通输出低电平;无孔时红外光被铝盘挡住,输出高电平。这样每转过一个过孔就产生一个脉冲,轮子转一圈产生11个脉冲。

为什么是11个孔而不是更多或更少?孔数直接决定测量精度。孔太少,两个过孔之间的弧长太大,车轮在低速时可能刚好停在两个孔之间,单片机在采样窗口内检测不到脉冲,速度显示跳零。孔太多,铝盘加工难度上升,过孔间距太小容易导致红外光束同时被相邻两个孔之间的实体部分遮挡,脉冲波形畸变。11个孔在常见26寸、27.5寸轮径下,单孔对应弧长约5到6厘米,两秒采样窗口内即使低速骑行也能捕获足够脉冲数。这个数字不是随便定的,是精度和加工可行性之间的折中。

铝盘材质建议用1.5到2毫米厚铝合金板,太薄容易变形导致过孔位置偏移,太厚增加转动惯量。过孔直径比红外光束直径大1到2毫米即可,一般取3到4毫米。安装时铝盘必须与轮轴同心,偏心会导致脉冲间隔不均匀,速度显示出现周期性波动。

2.2 信号预处理:零偏置放大器和DM74LS14整形

传感器输出的脉冲信号不能直接进单片机。原因有三个:一是信号幅度可能不够,ST1101在长距离传输后输出高电平可能衰减到3V以下,达不到TTL电平的2.0V阈值;二是波形边沿可能变缓,铝盘转速低时脉冲上升沿和下降沿不够陡峭;三是传输线上可能耦合噪声,导致脉冲上出现毛刺。所以需要两级预处理。

第一级是零偏置放大器,用开关三极管构成。当输入信号为零或负电压时三极管截止,输出高电平;输入正电压时三极管导通,输出电压随输入上升而下降。这个电路的好处是既能放大方波也能放大正弦波,输入信号幅度只要大于0.5V就能被有效处理。开关三极管选高频管,保证放大器有足够带宽,不会把快速变化的脉冲边沿吃掉。

第二级是DM74LS14施密特触发器反相器。施密特触发器的核心价值在于它的滞回特性:输入电压超过正向阈值VT+时输出翻转,低于负向阈值VT-时输出才翻回去。只要输入信号幅度大于VT+,输出端就能得到边沿陡峭、频率相同的矩形脉冲。传输线电容导致的上升沿变缓、阻抗不匹配导致的振荡、电源耦合的噪声,经过施密特触发器后都能被整形掉。DM74LS14是六反相器,这里只用其中一路,输出接到单片机P3.4引脚也就是T0定时器外部计数输入口。

注意:DM74LS14的VT+和VT-由芯片内部电路决定,典型值VT+约1.7V、VT-约0.9V。如果传感器输出信号经过长线传输后幅度低于1.7V,施密特触发器无法翻转,单片机就收不到脉冲。这种情况下要么缩短传感器到电路板的距离,要么在第一级放大器提高增益。

2.3 单片机资源分配:T0计数、T1定时、P3口读写的分工

AT89C2051是这个方案的核心。它只有20个引脚,但资源分配得当完全够用。关键引脚分配如下:

引脚功能连接对象
P3.4T0外部计数输入DM74LS14输出
P3.5I2C时钟SCLAT24C02 SCL
P3.7I2C数据SDAAT24C02 SDA
P1.0-P1.7显示数据/位选MC14511/74LS138
P3.0/P3.1串口备用未使用

T0工作在计数模式,对P3.4引脚上的脉冲下降沿计数。T1工作在定时模式,产生固定采样窗口。具体做法是:T1设定为50ms中断一次,中断服务程序里对一个软件计数器累加,累加到40次就是2秒。2秒到达后读取T0计数值,这就是两秒内的脉冲数N。然后清零T0重新计数,开始下一个采样周期。

为什么选2秒作为采样窗口?这是测速精度和系统反应时间的折中。窗口太短,低速时脉冲数太少,计算出的速度分辨率差;窗口太长,速度变化时显示跟不上,骑行者感觉仪表“迟钝”。2秒窗口下,假设轮径26寸、铝盘11孔,车速20km/h时轮子转速约2.7转/秒,两秒内约5.4转,对应约59个脉冲。这个脉冲数足够算出精确的速度值。车速降到5km/h时两秒内约15个脉冲,仍然可测。再低的速度比如3km/h以下,脉冲数少于10个,速度显示开始跳变,但实际骑行中低于3km/h基本是推行状态,不影响使用。

2.4 显示与存储:MC14511驱动LED和AT24C02存里程

显示部分用6位共阴极LED数码管,分两组:一组显示速度,一组显示里程。驱动链路是74LS138位选加MC14511段驱动。74LS138的Y0到Y5分别选通6个数码管的公共端,Y6和Y7控制CD4076寄存器来驱动小数点。MC14511把BCD码转换成7段码,内部集成锁存器、译码器和驱动器,一片顶三片,简化了电路。

AT24C02是256字节的I2C E2PROM,用来存累计里程。为什么不用单片机内部RAM存?因为掉电会丢。电动自行车的使用场景是频繁启停,每次关钥匙再打开,如果里程存在RAM里就归零了。AT24C02的写入寿命约100万次,按每天写100次算能用27年,足够。地址线A0、A1、A2全部接地,器件地址设为000。单片机通过P3.5和P3.7模拟I2C时序,写里程时先发写命令,收到应答后从内部数据单元取数据写入指定地址;读里程时先发读命令,收到应答后从AT24C02读出数据供显示。

提示:AT24C02的写周期典型值5ms,最大10ms。连续写入时必须等待上一个写周期完成,否则数据会丢。常见做法是写完后延时10ms再发下一个字节,或者用应答查询方式判断器件是否就绪。

3. 软件逻辑与算法实现:脉冲频率怎么变成速度和里程

3.1 频率测量模块:T0计数加T1定时的组合逻辑

频率测量是整个软件的核心。AT89C2051的T0和T1配合方式如下:T0配置为计数器模式,C/T位设为1,对P3.4引脚的外部脉冲下降沿计数,计数寄存器是TL0和TH0。T1配置为定时器模式,C/T位设为0,对内部机器周期计数,产生固定时间基准。

初始化代码大致如下:

// AT89C2051 频率测量初始化 // 假设晶振12MHz,机器周期1us void timer_init(void) { TMOD = 0x15; // T1定时模式16位,T0计数模式16位 TH0 = 0; // T0计数初值清零 TL0 = 0; TH1 = 0x3C; // T1初值,50ms定时 TL1 = 0xB0; // 12MHz下50ms = 50000us,初值65536-50000=15536=0x3CB0 ET0 = 0; // 关闭T0中断,只读计数 ET1 = 1; // 开启T1中断 EA = 1; // 开总中断 TR0 = 1; // 启动T0计数 TR1 = 1; // 启动T1定时 } // T1中断服务程序,每50ms执行一次 void timer1_isr(void) interrupt 3 { static unsigned char tick = 0; TH1 = 0x3C; // 重装初值 TL1 = 0xB0; tick++; if (tick >= 40) // 40 * 50ms = 2s { tick = 0; pulse_count = (TH0 << 8) | TL0; // 读取2秒内脉冲数 TH0 = 0; // 清零T0,开始下一轮 TL0 = 0; measure_flag = 1; // 通知主循环数据就绪 } }

这段代码的逻辑是:T1每50ms中断一次,中断里对tick变量累加,累加到40次就是2秒。2秒到达时读取T0的计数值存入pulse_count,清零T0,置位measure_flag通知主循环。主循环检测到measure_flag后执行速度和里程计算,然后清零标志等待下一轮。

参数说明:晶振频率决定T1初值。12MHz晶振下机器周期1us,50ms需要50000个机器周期,16位定时器最大值65536,初值就是65536减50000等于15536,十六进制0x3CB0。如果晶振换成11.0592MHz,机器周期约1.085us,50ms对应约46080个周期,初值要重新算。T0计数模式对外部脉冲计数,最高计数频率是晶振频率的1/24,12MHz下约500kHz,远高于实际脉冲频率,不会丢计数。

3.2 速度换算:从脉冲数到km/h的完整推导

拿到2秒内的脉冲数N之后,速度计算分四步:

第一步,算铝盘转过的圈数。铝盘每转一圈产生11个脉冲,所以圈数等于N除以11。

第二步,算铝盘圆周走过的距离。铝盘上两个相邻过孔之间的弧长M是已知的,11个孔对应11段弧,铝盘周长等于11乘以M。铝盘转过的距离等于圈数乘以铝盘周长。

第三步,算自行车轮子走过的距离。铝盘和轮子同轴,角速度相同。轮子周长与铝盘周长的比值等于轮子半径与铝盘半径的比值。设轮子周长C_wheel,铝盘周长C_disc,则轮子走过距离等于铝盘走过距离乘以C_wheel除以C_disc。

第四步,除以时间2秒,换算成km/h。

// 速度计算函数 // 输入:pulse_count为2秒内脉冲数 // 输出:speed_kmh为速度值,单位km/h,保留两位小数 void calc_speed(unsigned int pulse_count) { float disc_circum; // 铝盘周长,单位mm float wheel_circum; // 轮子周长,单位mm float disc_distance; // 铝盘走过距离,单位mm float wheel_distance; // 轮子走过距离,单位mm float speed_mmps; // 速度,单位mm/s // 铝盘周长 = 11 * 单段弧长M,假设M=50mm disc_circum = 11.0 * 50.0; // 550mm // 轮子周长,26寸轮径约2075mm wheel_circum = 2075.0; // 铝盘走过距离 = (脉冲数/11) * 铝盘周长 disc_distance = ((float)pulse_count / 11.0) * disc_circum; // 轮子走过距离 = 铝盘距离 * (轮子周长/铝盘周长) wheel_distance = disc_distance * (wheel_circum / disc_circum); // 速度 = 距离/时间,2秒=2000ms speed_mmps = wheel_distance / 2000.0; // 换算成km/h:mm/s * 3600 / 1000000 speed_kmh = speed_mmps * 3.6 / 1000.0; }

参数说明:单段弧长M由铝盘加工决定,需要实测。轮子周长C_wheel根据轮胎规格查表或实测,26寸山地车约2075mm,27.5寸约2155mm,700C约2130mm。这两个参数在代码里定义为常量,换车型时只需改这两个值。速度换算中假设2秒内匀速,实际骑行中速度波动会导致瞬时误差,但2秒窗口的平均速度对骑行者来说已经足够。

3.3 里程累计与E2PROM读写:掉电不丢数据的实现

里程累计的逻辑是:每个2秒周期算出轮子走过的距离S1,把S1加到当前总里程上,然后写入AT24C02。但每个周期都写E2PROM会快速消耗写入寿命,而且写周期10ms会阻塞主循环。实际做法是:RAM里维护一个里程累加变量,每算出S1就加到RAM变量上,只有当累计增量超过0.1km时才写入E2PROM一次。这样按每天骑行30km算,每天写300次,一年约10万次,远低于100万次寿命。

// 里程累计与存储 // mileage_ram为RAM中的当前里程,单位米 // mileage_last_stored为上次存入E2PROM的里程值 void update_mileage(float wheel_distance_mm) { float distance_m; distance_m = wheel_distance_mm / 1000.0; // 换算成米 mileage_ram += distance_m; // 累计增量超过100米才写E2PROM if ((mileage_ram - mileage_last_stored) >= 100.0) { e2prom_write_mileage(mileage_ram); mileage_last_stored = mileage_ram; } } // I2C写AT24C02,写入里程值 // 里程以米为单位,用4字节存储 void e2prom_write_mileage(float mileage) { unsigned long m; unsigned char buf[4]; m = (unsigned long)mileage; buf[0] = (m >> 24) & 0xFF; buf[1] = (m >> 16) & 0xFF; buf[2] = (m >> 8) & 0xFF; buf[3] = m & 0xFF; i2c_start(); i2c_send_byte(0xA0); // 器件地址+写 i2c_wait_ack(); i2c_send_byte(0x00); // 内部地址0 i2c_wait_ack(); for (unsigned char i = 0; i < 4; i++) { i2c_send_byte(buf[i]); i2c_wait_ack(); } i2c_stop(); delay_ms(10); // 等待写周期完成 }

参数说明:AT24C02器件地址高7位是1010000,加上读写位后写地址是0xA0,读地址是0xA1。内部地址0x00到0xFF共256字节,里程用4字节存储,从地址0x00开始。写入后必须延时10ms等待内部写周期完成,否则下一次操作会失败。读取时类似,先发写地址和内部地址,再发重复起始条件和读地址,然后读取4字节。

3.4 BCD转换与LED动态扫描:6位显示的刷新策略

速度和里程都是二进制数,LED数码管需要BCD码。转换方法是用除法和取余逐位提取。比如速度值1234表示12.34km/h,需要拆成1、2、3、4四个数字。整数部分除以1000得千位,余数除以100得百位,以此类推。小数部分乘以100后取整再拆。

6位数码管采用动态扫描方式,74LS138依次选通每一位,MC14511输出对应段码。扫描频率要大于50Hz,否则人眼会感觉闪烁。每位显示时间约1ms,6位一轮6ms,刷新率约166Hz,足够。扫描在T1中断里做还是主循环里做?建议放在T1中断里,每50ms中断一次,中断里除了计时还执行一轮6位扫描,这样显示刷新和计时互不干扰。

注意:动态扫描时每位点亮时间短,峰值电流大。共阴极LED每段电流约5到10mA,6位同时只有一位点亮,平均电流小,但峰值电流可能达到60mA。AT89C2051的I/O口灌电流能力有限,建议用74LS138做位选驱动,MC14511做段驱动,不要直接用单片机I/O口驱动LED。

4. 避坑与排查:这套方案最容易翻车的五个地方

4.1 速度显示跳零或乱跳

现象:骑行中速度显示偶尔跳到0,或者数值剧烈波动。

原因:最常见的是铝盘过孔位置偏移或传感器安装松动。铝盘偏心导致某些过孔离传感器太远,红外光无法有效穿过,脉冲丢失。另一个原因是传感器输出信号经过长线传输后幅度衰减到施密特触发器阈值以下,部分脉冲无法触发翻转。

解决:重新校准铝盘同心度,用百分表打表确认偏心小于0.5mm。传感器固定支架要加弹垫防松。如果信号衰减,缩短传感器到电路板的距离,或者在第一级放大器后加一级射极跟随器提高驱动能力。用示波器看DM74LS14输入端的波形,确认每个脉冲幅度都超过1.7V。

4.2 里程数每次开机归零

现象:关钥匙再打开,累计里程回到零。

原因:E2PROM写入失败或者读取时序错误。AT24C02的写周期最大10ms,如果写完立即断电,数据可能没写进去。读取时如果器件地址或内部地址发错,读出来的是随机值。

解决:写入后延时10ms再允许断电,或者在电源端加一个大电容保证断电后单片机还能维持10ms供电完成写操作。读取时用示波器看SDA和SCL波形,确认起始条件、地址字节、应答位都正确。AT24C02的A0、A1、A2必须接地,不能悬空。

4.3 低速时速度显示为0

现象:车速低于5km/h时速度显示0,推行时完全没反应。

原因:2秒采样窗口内脉冲数太少,除以11后圈数小于1,计算出的距离为0。这是算法本身的限制,不是故障。

解决:如果需要在极低速下显示,可以缩短采样窗口到1秒,或者改用测量脉冲周期而非频率的方法。但低速显示在实际骑行中意义不大,推行时看里程就够了。可以在代码里加一个判断:脉冲数小于3时速度显示“0.0”,不参与里程累计,避免原地抖动导致里程虚增。

4.4 LED显示重影或亮度不均

现象:数码管某些位看起来有重影,或者不同位亮度不一致。

原因:动态扫描时位选和段码切换存在竞争。74LS138的位选信号变化时,MC14511的段码还没稳定,导致上一位的段码短暂出现在下一位上。另外,不同数码管的正向压降有差异,同样的段电流下亮度不同。

解决:在位选切换和段码输出之间加一个小延时,或者用锁存器在切换前先关闭所有位选。亮度不均可以给每位串联不同的限流电阻来补偿,或者选用同一批次的正向压降一致的数码管。

4.5 超速报警误触发

现象:正常骑行时报警偶尔响,或者超速时不报警。

原因:报警阈值判断用的是瞬时速度,而瞬时速度受脉冲计数误差影响有波动。如果阈值设得太接近常用巡航速度,波动就会导致误触发。

解决:报警判断用连续3个采样周期的平均速度,而不是单次速度值。阈值加一个回差,比如超过25km/h触发报警,低于23km/h才解除报警,避免在阈值附近反复触发。报警输出可以用单片机某个空闲I/O口驱动蜂鸣器,加三极管扩流。

5. 进阶技巧:让这套方案更接近量产仪表

5.1 用脉冲周期法替代频率法提升低速精度

频率法在低速时脉冲数少、分辨率差。脉冲周期法是测量两个相邻脉冲之间的时间间隔,用T1定时器以微秒级精度计时。速度越低,脉冲间隔越长,测量精度反而越高。具体做法是:T0配置为边沿触发中断,每个脉冲下降沿触发中断,中断里读取T1计数值并清零,这个值就是脉冲周期。速度等于单孔弧长除以脉冲周期。

两种方法可以结合:高速时用频率法,低速时自动切换到周期法。切换阈值设在脉冲数小于10时。这样全速度段都能保持较好精度。

5.2 轮径参数在线校准

不同车型轮径不同,硬编码轮子周长需要改代码重新烧录。可以在AT24C02里留一个区域存轮径参数,开机时读取。校准方法是:在已知距离的直路上推行,比如100米,看里程显示值,算出比例系数,通过串口或按键写入E2PROM。这样一套硬件可以适配多种车型。

5.3 掉电保护与数据完整性校验

E2PROM存储里程时加CRC校验。每次写入4字节里程数据后,再写入1字节校验和。读取时先算校验和,与存储值比对,不一致说明数据损坏,用上一次的有效值。另外可以在E2PROM里存两份里程,地址0x00和0x10各一份,读取时取较大的那个,防止写入过程中断电导致数据回退。

5.4 显示刷新与计算任务的时序隔离

主循环里做速度计算和里程累计,T1中断里做显示扫描和计时。两者共享的变量比如pulse_count、speed_kmh要加volatile修饰,防止编译器优化导致读取到旧值。计算任务执行时间要短,避免影响中断响应。如果计算量大,可以把计算拆成多个小步骤,每个主循环周期执行一步。

从那以后我每次做带E2PROM存储的项目,都强制走一遍“写入-断电-上电-读取”的完整验证流程,不看到数据正确恢复不往下走。这套速度里程表的方案不复杂,但存储和信号调理这两个环节最容易埋雷,提前验证比事后排查省事得多。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进