首页 / 资讯中心 / 文章详情

基于STM32的霍尔传感器与语音播报出租车计价器设计

基于STM32的霍尔传感器与语音播报出租车计价器设计 ★ FEATURED ARTICLE
1. 项目缘起与整体设计思路1.1 为什么选择出租车计价器作为练手项目出租车计价器这个题目在单片机圈子里算是经典中的经典。它不像流水灯那样一眼看到底也不像四轴飞控那样门槛高得吓人而是恰好卡在一个“跳一跳够得着”的位置上。你要处理按键输入、要驱动显示、要算钱、要存数据还得让机器发出声音告诉乘客“本次行程XX元”。这一套下来基本上把单片机开发中最常用的几类外设都摸了一遍。我当初选这个题目最直接的原因是它能把霍尔传感器、语音播报、掉电存储这三个看起来不相关的技术点串成一条线。霍尔传感器负责感知车轮转了多少圈语音模块负责把冷冰冰的数字变成人话掉电存储则保证突然熄火时里程和金额不会丢。这三样东西单独拿出来都不难但要让它们在一个系统里协调工作就需要认真考虑中断优先级、数据同步、状态机设计这些实际问题。从应用场景来说出租车计价器本质上是一个实时计量与计费系统。它的核心需求可以拆成四条第一准确计量行驶距离第二根据预设费率实时计算金额第三把关键信息清晰地展示给司机和乘客第四在异常情况下比如断电保护数据不丢失。这四条需求对应到技术实现上就是脉冲计数、定时计算、LCD显示和Flash读写。你看一个看似简单的计价器其实把嵌入式开发中最常见的几类任务都覆盖了。这个项目适合谁呢我认为有三类人可以从中获益。第一类是正在学习STM32的在校学生尤其是需要做毕业设计或者课程设计的同学这个题目工作量适中、技术点明确、演示效果好答辩时容易讲清楚。第二类是想从51单片机过渡到STM32的开发者计价器项目涉及的GPIO、定时器、外部中断、串口通信、Flash读写等外设在STM32上都有对应的实现方式做完一遍就能建立起基本的使用框架。第三类是对计量计费系统感兴趣的产品开发者虽然出租车计价器是个老产品但它背后的设计思路——比如如何防止作弊、如何处理异常、如何保证计量精度——在很多物联网计费场景中仍然适用。1.2 系统整体架构与模块划分整个系统我把它分成五个模块来设计每个模块各司其职模块之间通过明确的接口通信。这种划分方式的好处是调试的时候可以逐个击破不会出现“一锅粥”的情况。主控模块选用STM32F103C8T6也就是大家常说的“蓝板”或“最小系统板”。这颗芯片是Cortex-M3内核主频72MHz有64KB Flash和20KB SRAM对于计价器这种规模的应用绰绰有余。选它的理由很实在资料多、价格低、社区活跃遇到问题容易找到答案。而且它的定时器资源丰富有4个通用定时器加2个高级定时器可以轻松分配出用于脉冲计数、系统滴答、语音触发延时等不同任务的定时器。传感器模块采用霍尔传感器具体型号是A3144。这里要特别说明一下A3144是双极锁存型霍尔开关不是线性霍尔传感器。它的输出是数字信号当检测到南极磁场时输出低电平检测到北极磁场时输出高电平磁场消失后保持上次状态不变。这个特性非常适合用来做车轮转速检测——在车轮上固定一块小磁铁霍尔传感器固定在车架靠近车轮的位置车轮每转一圈磁铁扫过传感器一次传感器就输出一个脉冲。STM32通过外部中断捕获这个脉冲的上升沿或下降沿就能精确计数。注意A3144和3144是两个不同的东西。A3144是双极锁存型输出数字量而线性霍尔传感器比如SS49E输出的是模拟电压需要接ADC才能读取。买元件的时候一定要看清楚型号我见过不止一个同学买错了型号结果调了半天发现输出根本不是数字信号。显示模块用LCD1602液晶屏这是最经典也最便宜的选择。1602可以显示两行每行16个字符足够显示“里程”和“金额”两组数据。有同学可能会问为什么不选OLED或者TFT彩屏我的考虑是第一1602的驱动简单并行接口时序清晰适合学习第二出租车计价器在实际使用中并不需要花哨的界面清晰易读才是第一位的第三1602的价格便宜坏了换一个也不心疼。当然如果你想让界面更好看换成OLED也不是不行但那是另一个话题了。语音模块选用SU-03T这是一个离线语音合成模块支持UART串口控制内置了多种音色和播报风格。选它的原因是它不需要联网响应速度快而且支持自定义播报内容。你只需要通过串口发送特定的指令码它就能播报对应的语音。比如发送“0x01”让它说“欢迎乘坐”发送“0x02”让它说“本次行程XX元”。SU-03T的另一个好处是它自带功放接口可以直接接一个小喇叭不需要额外的音频放大电路。存储模块使用STM32内部的Flash来保存关键数据。计价器需要保存的数据主要有三项总里程、总金额、以及当前费率参数。这些数据不需要频繁写入只在行程结束或者费率调整时才更新一次所以用内部Flash完全够用。STM32F103的Flash擦写寿命大约是10000次按照每天写入10次计算可以用将近三年对于学习项目来说完全足够。1.3 关键设计决策与取舍分析在设计过程中有几个关键决策点需要仔细权衡我把当时的思考过程记录下来供大家参考。第一个决策脉冲计数用外部中断还是定时器捕获STM32的定时器有输入捕获功能可以自动记录脉冲的上升沿和下降沿并且能通过硬件自动计数不占用CPU资源。外部中断则需要CPU在每次脉冲到来时进入中断服务函数如果脉冲频率太高CPU可能会被频繁打断。我最终选择了外部中断方案原因是出租车车轮的转速并不高。假设车轮直径0.5米周长约1.57米车速60公里/小时相当于16.67米/秒车轮每秒转约10.6圈。如果每圈产生一个脉冲脉冲频率只有10Hz左右外部中断完全能轻松应对。而且外部中断的编程更直观调试时容易观察计数变化。当然如果你要做更高精度的计量比如每圈多个脉冲或者车速非常高那还是用定时器捕获更稳妥。第二个决策语音播报用离线模块还是在线合成在线语音合成需要联网延迟高而且在出租车这种移动场景下网络信号不稳定。离线语音模块虽然音色不如在线的自然但响应速度快、可靠性高、不依赖网络。SU-03T的播报延迟大约在200毫秒以内对于计价器这种应用来说完全够用。而且SU-03T支持自定义词条你可以把常用的播报内容提前烧录进去比如“起步价XX元”、“每公里XX元”、“本次行程XX元”等。第三个决策数据存储用内部Flash还是外部EEPROM外部EEPROM比如AT24C02的擦写寿命更长100万次以上而且读写更方便不需要擦除整个扇区。但它的缺点是占用额外的IO口和PCB空间而且需要额外的驱动代码。内部Flash虽然擦写寿命短一些但对于学习项目来说完全够用而且省去了外部元件简化了硬件设计。我最终选择了内部Flash但在代码中做了磨损均衡的简单处理——把数据分成两个区域轮流写入避免总是擦写同一个扇区。第四个决策费率参数是硬编码还是可配置硬编码就是把起步价、每公里单价等参数直接写在代码里修改时需要重新编译烧录。可配置则是把这些参数存在Flash中通过按键或者串口命令来修改。我选择了可配置方案虽然代码复杂一些但更接近真实产品的使用场景。实际使用中出租车公司可能会调整费率如果每次都要重新烧录固件那就太麻烦了。可配置方案允许通过特定的按键组合进入设置模式修改费率参数后保存到Flash中。2. 核心硬件选型与电路设计要点2.1 STM32最小系统板的引脚分配与注意事项STM32F103C8T6最小系统板是很多人的入门首选但它的引脚分配有一些坑需要注意。我先把本项目中用到的引脚列出来然后逐一说明选择理由和注意事项。功能模块引脚模式说明霍尔传感器PA0外部中断上升沿触发接A3144输出LCD1602数据PB0-PB7推挽输出8位并行数据线LCD1602控制PB8-PB10推挽输出RS、RW、E控制线语音模块TXPA9复用推挽USART1_TX接SU-03T的RX语音模块RXPA10浮空输入USART1_RX接SU-03T的TX按键1开始/结束PA1上拉输入行程开始和结束控制按键2设置PA2上拉输入进入费率设置模式按键3加PA3上拉输入设置模式下增加数值按键4减PA4上拉输入设置模式下减少数值LED指示灯PC13推挽输出系统运行指示这里有几个关键点需要展开说明。PA0作为外部中断输入PA0是STM32的外部中断线0可以配置为上升沿、下降沿或双边沿触发。A3144的输出在检测到磁铁时会产生电平跳变我选择上升沿触发因为A3144在磁场消失后输出高电平磁铁靠近时输出低电平所以磁铁离开的瞬间会产生上升沿。当然具体用上升沿还是下降沿取决于你的磁铁安装方式和传感器型号调试时可以用LED指示灯观察中断是否正常触发。注意PA0同时也是WKUP引脚如果之前用过待机唤醒功能可能会影响外部中断的正常使用。如果发现PA0中断不触发检查一下是不是被其他功能占用了。PB0-PB7作为LCD1602数据线STM32的GPIOB端口0-7正好是连续的8个引脚用来做8位并行数据线非常方便。但要注意PB3和PB4默认是JTAG调试引脚如果直接用作普通IO需要在代码中关闭JTAG功能。具体做法是调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这样PB3和PB4就可以作为普通IO使用了。我当初在这个问题上卡了半天LCD1602死活不显示后来才发现是PB3和PB4被JTAG占用了。PA9和PA10作为串口USART1的默认引脚就是PA9TX和PA10RX不需要重映射。连接SU-03T时要注意交叉连接STM32的TX接SU-03T的RXSTM32的RX接SU-03T的TX。SU-03T的工作电压是3.3V和STM32的IO电平兼容不需要电平转换。PC13作为LED指示灯PC13是STM32最小系统板上自带的LED引脚低电平点亮。用它来指示系统运行状态比如正常运行时每秒闪烁一次进入设置模式时常亮出现错误时快速闪烁。2.2 霍尔传感器A3144的安装与信号调理A3144是一款三脚霍尔开关引脚定义是面对印字面从左到右依次是VCC、GND、OUT。供电范围是4.5V到24V输出是集电极开路OC结构需要接一个上拉电阻才能输出高电平。我一般用10KΩ的上拉电阻接到3.3V这样输出信号可以直接接入STM32的IO口。安装方式对计量精度影响很大。我的做法是在车轮辐条上粘一块小钕铁硼磁铁直径5毫米、厚度2毫米就够了。霍尔传感器固定在车架靠近车轮的位置距离磁铁大约5到10毫米。这个距离需要实际调试太远了检测不到太近了可能被车轮碰到。调试时可以用LED指示灯接在A3144的输出端转动车轮观察LED是否闪烁闪烁频率是否与车轮转速一致。提示磁铁的极性会影响A3144的输出状态。A3144是双极锁存型南极靠近时输出低电平北极靠近时输出高电平。如果你发现车轮转一圈产生了两个脉冲可能是磁铁安装位置让传感器同时检测到了南极和北极调整磁铁方向或者换用单极触发的方式可以解决。信号调理方面A3144的输出是干净的方波不需要额外的滤波电路。但如果你的应用环境电磁干扰比较强可以在输出端并联一个0.1μF的电容到地滤除高频噪声。另外连接线最好用屏蔽线并且尽量远离电机、继电器等干扰源。2.3 SU-03T语音模块的配置与通信协议SU-03T的配置需要通过专用的配置工具来完成这个工具可以在模块的官方资料中找到。配置过程主要做三件事设置串口波特率、定义播报词条、设置触发方式。波特率我设置为9600这是SU-03T的默认值也是STM32串口的常用波特率。播报词条我定义了以下几组指令码播报内容使用场景0x01欢迎乘坐行程开始时0x02本次行程XX元行程结束时0x03起步价XX元进入设置模式时0x04每公里XX元进入设置模式时0x05设置已保存退出设置模式时0x06电量不足电压检测异常时SU-03T支持变量播报也就是说你可以在播报内容中插入变量。比如“本次行程XX元”中的XX可以替换成实际的金额数字。具体做法是在配置工具中使用特定的占位符然后在串口发送指令时把实际数值一起发过去。SU-03T会自动把数字转换成语音播报出来。通信协议很简单STM32通过串口发送一个字节的指令码SU-03T收到后就播报对应的内容。如果需要播报变量就发送指令码加变量值比如发送“0x02 0x00 0x64”表示播报“本次行程100元”0x0064是100的十六进制表示。注意SU-03T在播报过程中会占用一定的处理时间如果连续发送多条播报指令后面的指令可能会被忽略。我的做法是在发送播报指令后延时500毫秒再发送下一条确保每条语音都能完整播报。2.4 电源管理与抗干扰设计出租车计价器在实际使用中会接入车载电源而车载电源的电压波动很大从9V到16V都有可能而且存在大量的电磁干扰。虽然学习项目可以用USB供电但了解一些电源管理的知识还是很有必要的。我的方案是车载电源经过一个LM2596降压模块降到5V再经过一个AMS1117-3.3降到3.3V给STM32和SU-03T供电。LM2596是开关型降压芯片效率高、发热小输入电压范围宽4.5V到40V非常适合车载环境。AMS1117是线性稳压芯片输出纹波小适合给对电源噪声敏感的MCU和语音模块供电。抗干扰方面我在电源输入端并联了一个100μF的电解电容和一个0.1μF的陶瓷电容分别滤除低频和高频干扰。在霍尔传感器的信号线上串联了一个100Ω的电阻并在STM32的IO口处并联了一个0.1μF的电容到地组成一个简单的RC低通滤波器截止频率约为16kHz可以有效滤除高频噪声而不影响正常的脉冲信号。3. 软件架构与核心代码实现3.1 系统状态机设计与任务调度计价器的软件核心是一个状态机我把它分成五个状态空闲状态、行程进行状态、行程结束状态、设置状态、错误状态。状态之间的转换由按键事件和传感器事件驱动。typedef enum { STATE_IDLE, // 空闲状态等待开始 STATE_RUNNING, // 行程进行中计量计费 STATE_FINISHED, // 行程结束显示总金额 STATE_SETTING, // 设置模式调整费率 STATE_ERROR // 错误状态如传感器故障 } SystemState_t; SystemState_t currentState STATE_IDLE;状态转换的逻辑是这样的在空闲状态下按下开始键进入行程进行状态同时清零里程和金额启动定时器开始计时。在行程进行状态下霍尔传感器每产生一个脉冲里程增加一个单位比如0.1公里金额根据费率重新计算。按下结束键进入行程结束状态语音播报总金额数据保存到Flash。在行程结束状态下再次按下开始键回到空闲状态准备下一次行程。在任何状态下长按设置键3秒进入设置状态在设置状态下可以调整起步价和每公里单价按下结束键保存并退出。任务调度方面我没有使用RTOS而是用一个简单的时间片轮询结构。系统滴答定时器每1毫秒产生一次中断在中断中设置不同的标志位主循环根据标志位执行对应的任务。volatile uint8_t flag_10ms 0; volatile uint8_t flag_100ms 0; volatile uint8_t flag_1s 0; void SysTick_Handler(void) { static uint16_t count_10ms 0; static uint16_t count_100ms 0; static uint16_t count_1s 0; count_10ms; if (count_10ms 10) { count_10ms 0; flag_10ms 1; } // 类似地处理100ms和1s标志 }这种结构的优点是简单直观不需要复杂的任务调度器对于计价器这种实时性要求不高的应用完全够用。缺点是如果某个任务执行时间过长会影响其他任务的响应。所以在编写任务函数时要尽量避免长时间的阻塞操作比如延时等待。3.2 霍尔脉冲计数与里程计算霍尔脉冲的计数通过外部中断实现。PA0配置为上升沿触发每次中断发生时脉冲计数器加一。volatile uint32_t pulseCount 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { pulseCount; EXTI_ClearITPendingBit(EXTI_Line0); } }里程的计算需要知道每个脉冲对应多少距离。假设车轮直径是0.5米周长就是π×0.5≈1.57米。如果每圈产生一个脉冲那么每个脉冲对应1.57米。但实际使用中为了显示方便我们通常以公里为单位保留一位小数。所以每产生一个脉冲里程增加0.00157公里。float mileage 0.0f; const float distancePerPulse 0.00157f; // 每个脉冲对应的公里数 void updateMileage(void) { mileage pulseCount * distancePerPulse; }这里有一个精度问题需要注意float类型在STM32上默认是32位的有效数字大约7位。如果里程累积到1000公里以上float的精度可能不够。解决办法是使用double类型STM32支持64位double或者用整数来存储里程的厘米数显示时再转换成公里。我选择了用整数存储厘米数这样精度最高而且计算速度也快。uint32_t mileage_cm 0; // 里程单位厘米 const uint32_t cmPerPulse 157; // 每个脉冲对应157厘米 void updateMileage(void) { mileage_cm pulseCount * cmPerPulse; }显示的时候再除以100000得到公里数保留一位小数。3.3 计费算法与费率参数存储计费算法是计价器的核心逻辑。出租车计费通常采用“起步价超出部分”的模式。比如起步价10元包含3公里超出3公里后每公里2.5元。这个逻辑用代码实现如下float calculateFare(uint32_t mileage_cm) { float km mileage_cm / 100000.0f; float fare 0.0f; if (km 3.0f) { fare 10.0f; // 起步价 } else { fare 10.0f (km - 3.0f) * 2.5f; } return fare; }费率参数起步价、包含公里数、每公里单价存储在Flash中系统上电时读取修改后写回。STM32F103的Flash页大小是1KB我使用第63页地址0x0800FC00来存储费率参数。#define FARE_PARAM_ADDR 0x0800FC00 typedef struct { float basePrice; // 起步价 float baseDistance; // 起步包含公里数 float pricePerKm; // 每公里单价 uint32_t magic; // 魔术字用于判断数据是否有效 } FareParam_t; FareParam_t fareParam; void loadFareParam(void) { FareParam_t *p (FareParam_t *)FARE_PARAM_ADDR; if (p-magic 0x5A5A5A5A) { fareParam *p; } else { // 默认参数 fareParam.basePrice 10.0f; fareParam.baseDistance 3.0f; fareParam.pricePerKm 2.5f; fareParam.magic 0x5A5A5A5A; } } void saveFareParam(void) { FLASH_Unlock(); FLASH_ErasePage(FARE_PARAM_ADDR); FLASH_ProgramWord(FARE_PARAM_ADDR, *(uint32_t *)fareParam.basePrice); // 继续写入其他字段... FLASH_Lock(); }注意Flash写入之前必须先擦除整个页而且擦除操作会消耗较多时间大约20毫秒。在擦除期间CPU会暂停执行指令如果此时有中断发生可能会导致不可预期的行为。我的做法是在擦除前关闭全局中断擦除完成后再打开。3.4 LCD1602显示驱动与界面布局LCD1602的驱动是嵌入式开发中的经典内容网上有很多现成的代码可以参考。我这里重点说一下界面布局的设计。第一行显示里程格式为“DIST: 123.4km”。第二行显示金额格式为“FARE: 45.60元”。在设置模式下第一行显示“SET BASE: 10.0”第二行显示“SET PRICE: 2.50”通过按键切换要修改的参数。void displayNormal(void) { char buf[17]; // 第一行显示里程 sprintf(buf, DIST:%6.1fkm, mileage_cm / 100000.0f); LCD_SetCursor(0, 0); LCD_WriteString(buf); // 第二行显示金额 sprintf(buf, FARE:%6.2f, currentFare); LCD_SetCursor(1, 0); LCD_WriteString(buf); }LCD1602的显示刷新不需要太频繁我设置为每100毫秒刷新一次。如果刷新太快屏幕会闪烁如果太慢数值变化会有明显的延迟感。100毫秒是一个比较平衡的值。3.5 语音播报的触发时机与内容组织语音播报的触发时机很重要播报太频繁会打扰乘客播报太少又起不到提示作用。我设计了以下几个播报场景行程开始时播报“欢迎乘坐”让乘客知道计价器已经开始工作。行程结束时播报“本次行程XX元”这是最重要的播报内容。进入设置模式时播报当前费率参数方便操作者确认。设置保存后播报“设置已保存”给操作者一个反馈。void voicePlay(uint8_t cmd) { USART_SendData(USART1, cmd); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); delay_ms(500); // 等待播报完成 } void voicePlayFare(float fare) { uint16_t fareInt (uint16_t)(fare * 100); // 转换为分 USART_SendData(USART1, 0x02); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, (fareInt 8) 0xFF); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, fareInt 0xFF); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); delay_ms(1000); }提示SU-03T播报“本次行程XX元”时XX的数值范围是0到9999。如果你的金额超过这个范围需要特殊处理。不过对于出租车来说单次行程金额超过9999元的情况几乎不可能出现。4. 实操调试与常见问题排查4.1 硬件调试步骤与工具准备硬件调试我建议按照以下顺序进行每一步确认无误后再进行下一步这样可以避免多个问题同时出现导致排查困难。第一步电源检查。用万用表测量各个模块的供电电压确保STM32的3.3V、SU-03T的3.3V、LCD1602的5V都正常。如果电压偏差超过5%检查稳压芯片和滤波电容。第二步最小系统测试。烧录一个简单的LED闪烁程序确认STM32能正常运行。如果LED不亮检查晶振是否起振、复位电路是否正常、BOOT引脚电平是否正确。第三步LCD1602测试。烧录一个显示固定字符的程序确认LCD能正常显示。如果显示乱码或者不显示检查数据线和控制线的连接、对比度调节电位器是否调好。第四步霍尔传感器测试。烧录一个中断计数程序在LCD上显示脉冲数。用手拿磁铁靠近和远离传感器观察计数是否变化。如果计数不变检查传感器供电、上拉电阻、信号线连接。第五步语音模块测试。烧录一个串口发送程序每隔一秒发送一次播报指令听喇叭是否有声音。如果没有声音检查串口连接、波特率设置、喇叭连接。第六步整体联调。把所有模块组合在一起运行完整的计价器程序模拟实际使用场景进行测试。调试工具方面我强烈建议准备一个USB逻辑分析仪价格不贵但非常实用。它可以同时抓取多路数字信号帮你分析时序问题。比如LCD1602不显示时用逻辑分析仪抓一下数据线和控制线的波形就能看出是时序不对还是数据错误。另外一个带电流显示的USB电压表也很有用可以实时观察系统的功耗变化。4.2 常见问题速查表问题现象可能原因排查方法解决方案LCD1602不显示对比度不对调节电位器调到字符清晰为止LCD1602显示乱码数据线接触不良用万用表逐根测量重新焊接或更换杜邦线霍尔计数不准确磁铁极性不对观察LED指示灯翻转磁铁方向霍尔计数跳变信号干扰示波器观察波形增加RC滤波电路语音模块不发声串口连接错误检查TX/RX是否交叉交换TX和RX接线语音播报不完整延时不够测量播报时间增加延时到500ms以上Flash数据丢失擦写次数过多检查擦写频率增加磨损均衡逻辑系统频繁复位电源不稳定测量3.3V电压增加滤波电容或更换电源按键不灵敏上拉电阻过大测量按键引脚电压减小上拉电阻到4.7K里程累积过快每脉冲距离计算错误核对车轮周长重新计算distancePerPulse4.3 调试中踩过的坑与经验总结第一个坑PB3和PB4被JTAG占用。这个问题我在前面提过但值得再强调一次。STM32F103的PB3、PB4、PA15默认是JTAG调试引脚如果直接用作普通IO需要先关闭JTAG功能。我当初用PB3和PB4做LCD1602的数据线结果屏幕死活不显示用万用表测量引脚电压发现一直是高电平后来查资料才知道是JTAG的问题。解决办法是在初始化代码的最开始加上GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);。第二个坑外部中断抖动。霍尔传感器在磁场切换的瞬间可能会产生抖动导致一次磁场变化产生多个中断。我最初没有做去抖处理结果里程计数比实际值多了将近一倍。解决办法是在中断服务函数中增加一个简单的软件去抖记录上次中断的时间戳如果两次中断间隔小于10毫秒就忽略后一次。volatile uint32_t lastPulseTime 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { uint32_t now getTickCount(); if (now - lastPulseTime 10) { // 10ms去抖 pulseCount; lastPulseTime now; } EXTI_ClearITPendingBit(EXTI_Line0); } }第三个坑Flash擦除时中断导致死机。前面提到过Flash擦除期间CPU会暂停如果此时有中断发生可能会导致硬件错误。我最初没有关闭中断结果在保存费率参数时系统偶尔会死机。解决办法是在Flash操作前后加上中断开关。void saveFareParam(void) { __disable_irq(); // 关闭全局中断 FLASH_Unlock(); FLASH_ErasePage(FARE_PARAM_ADDR); // 写入数据... FLASH_Lock(); __enable_irq(); // 打开全局中断 }第四个坑语音播报与显示刷新冲突。语音播报需要发送多个字节的串口数据如果此时LCD正在刷新可能会导致串口数据发送不完整。我的解决办法是在语音播报期间暂停LCD刷新播报完成后再恢复。具体做法是设置一个voiceBusy标志位LCD刷新任务检测到这个标志位为1时就跳过本次刷新。第五个坑里程单位换算错误。我最初把每个脉冲对应的距离算成了车轮周长但实际上磁铁每转一圈只产生一个脉冲所以每个脉冲对应的距离就是车轮周长。但我在代码中把厘米和公里搞混了导致显示的里程比实际值大了1000倍。这个问题的教训是在涉及单位换算的地方一定要写清楚注释并且用实际数据验证。4.4 提高计量精度的几个实用技巧计量精度是计价器的核心指标这里分享几个我在实践中总结的技巧。技巧一增加脉冲分辨率。如果条件允许可以在车轮上均匀安装多个磁铁比如4个磁铁这样每转一圈产生4个脉冲分辨率提高4倍。但要注意磁铁之间的间距要一致否则会导致计数不均匀。技巧二使用定时器捕获代替外部中断。定时器捕获可以记录每个脉冲的精确时间戳通过计算相邻脉冲的时间差可以更准确地判断车轮转速。在低速时这种方法比简单的脉冲计数更精确。技巧三定期校准。在实际使用中车轮的磨损、胎压的变化都会影响周长从而影响计量精度。可以设计一个校准模式让司机在已知距离的道路上行驶一段系统根据实际距离自动调整distancePerPulse参数。技巧四温度补偿。霍尔传感器的输出会随温度变化虽然A3144是数字输出影响不大但如果使用线性霍尔传感器就需要考虑温度补偿。对于学习项目来说这一条可以暂时忽略。5. 项目扩展与功能升级方向5.1 增加GPS模块实现自动计费当前的计价器依赖霍尔传感器计量里程如果传感器故障或者磁铁脱落计量就会出错。增加一个GPS模块比如ATGM336H可以实时获取车辆位置和速度通过积分计算行驶距离与霍尔传感器的数据进行交叉验证。如果两者差异超过一定阈值就发出报警提示。GPS模块通过串口与STM32通信输出NMEA格式的数据。解析GPRMC语句可以获取经纬度和速度信息。两个相邻定位点之间的距离可以用半正矢公式计算float haversine(float lat1, float lon1, float lat2, float lon2) { float dLat (lat2 - lat1) * PI / 180.0f; float dLon (lon2 - lon1) * PI / 180.0f; float a sinf(dLat/2) * sinf(dLat/2) cosf(lat1 * PI / 180.0f) * cosf(lat2 * PI / 180.0f) * sinf(dLon/2) * sinf(dLon/2); float c 2 * atan2f(sqrtf(a), sqrtf(1-a)); return 6371000 * c; // 返回米 }注意GPS在隧道、地下车库等信号遮挡的地方会丢失定位此时需要切换回霍尔传感器计量。所以GPS和霍尔传感器是互补关系不是替代关系。5.2 添加无线通信实现远程监控通过增加一个4G模块或者LoRa模块可以把计价器的数据实时上传到服务器实现远程监控和调度。比如调度中心可以实时看到每辆车的位置、载客状态、当前金额等信息。这对于出租车公司来说非常有价值。无线通信模块的选择取决于使用场景。如果车辆在城市范围内运行4G模块比如Air724UG是最佳选择覆盖广、速率快。如果是在偏远地区或者对功耗有严格要求LoRa模块更合适传输距离远、功耗低但需要自建网关。数据上传的格式可以用JSON方便服务器解析{ deviceId: TAXI001, timestamp: 1700000000, latitude: 39.9042, longitude: 116.4074, mileage: 12.5, fare: 35.60, status: occupied }5.3 打印发票与移动支付集成真实出租车计价器通常配备微型打印机行程结束后打印发票。增加一个热敏打印机模块比如CSN-A2可以实现这个功能。打印机通过串口接收数据支持中文和数字打印。移动支付集成则需要增加一个二维码显示功能。可以用一个小的TFT屏幕显示收款二维码乘客扫码支付。STM32可以通过串口控制TFT屏幕显示指定的图片。收款确认可以通过语音播报或者屏幕提示来实现。这两个功能的增加会显著提升项目的实用性和演示效果但也会增加硬件成本和代码复杂度。建议在完成基础版本后再逐步添加。5.4 数据记录与行程回放当前的系统只保存总里程和总金额不记录每次行程的详细信息。增加一个SD卡模块可以把每次行程的起始时间、结束时间、里程、金额、费率等信息保存到CSV文件中。这样不仅方便对账还可以用于数据分析。SD卡通过SPI接口与STM32通信使用FatFS文件系统来管理文件。每次行程结束时打开CSV文件追加一行记录然后关闭文件。CSV格式简单通用可以直接用Excel打开查看。void saveTripRecord(uint32_t startTime, uint32_t endTime, uint32_t mileage_cm, float fare) { FIL file; FRESULT res f_open(file, trips.csv, FA_OPEN_ALWAYS | FA_WRITE); if (res FR_OK) { f_lseek(file, f_size(file)); // 移动到文件末尾 char buf[128]; sprintf(buf, %lu,%lu,%.1f,%.2f\n, startTime, endTime, mileage_cm/100000.0f, fare); UINT written; f_write(file, buf, strlen(buf), written); f_close(file); } }这个功能对于学习文件系统操作非常有帮助而且在实际产品中也是标配。我建议在基础版本稳定后优先添加这个功能。5.5 低功耗设计与电池备份如果计价器需要在车辆熄火后继续工作比如保存数据、维持时钟就需要考虑低功耗设计。STM32F103支持多种低功耗模式其中待机模式功耗最低大约2μA。在待机模式下只有备份寄存器和RTC继续工作其他外设全部关闭。实现方案是车辆熄火后系统检测到电源电压下降立即把关键数据保存到Flash然后进入待机模式。下次上电时系统从待机模式唤醒读取Flash中的数据恢复到熄火前的状态。为了在熄火期间维持RTC计时需要给STM32的VBAT引脚接一个纽扣电池CR2032。这样即使主电源断开RTC也能继续走时保证行程时间的连续性。提示进入待机模式前一定要确保所有数据都已经保存到Flash因为待机模式会丢失SRAM中的所有数据。另外唤醒引脚需要配置正确否则可能无法正常唤醒。6. 项目总结与个人实操体会这个基于STM32的语音出租车计价器项目从最初的想法到最终稳定运行前后花了大约三周时间。其中硬件搭建用了三天软件编写用了十天剩下的时间都在调试和优化。回过头来看最大的收获不是学会了某个具体的外设驱动而是理解了如何把一个实际需求拆解成可执行的技术方案以及如何在资源受限的嵌入式环境中做出合理的取舍。如果让我给正在做类似项目的朋友提几条建议我会说第一不要一上来就写代码先花时间把硬件调通确认每个模块都能独立工作这样后面联调时会轻松很多。第二中断服务函数一定要短小精悍只做标志位设置和数据记录复杂的计算放到主循环中处理。第三Flash操作一定要谨慎擦除前关中断写入后校验避免数据损坏。第四语音播报的内容要提前规划好避免在代码中频繁修改因为每次修改都需要重新配置SU-03T模块。这个项目后续还可以往很多方向扩展比如增加GPS定位、无线通信、发票打印、移动支付等功能。每增加一个功能都会带来新的技术挑战和学习机会。我个人的计划是下一步把GPS模块加上去实现霍尔传感器和GPS的双重计量提高系统的可靠性。如果你也在做类似的项目欢迎交流经验共同进步。
阅读完成 · 觉得有帮助?
咨询建站