☰
基于STM32的霍尔传感器与语音播报出租车计价器设计
2026/11/3 1:08:28 网站建设 项目流程

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的默认引脚就是PA9(TX)和PA10(RX),不需要重映射。连接SU-03T时要注意交叉连接:STM32的TX接SU-03T的RX,STM32的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
里程累积过快每脉冲距离计算错误核对车轮周长重新计算distancePerPulse

4.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的双重计量,提高系统的可靠性。如果你也在做类似的项目,欢迎交流经验,共同进步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询