1. 项目概述
这个基于STM32单片机的电话计费系统设计,是我在嵌入式开发领域的一次实战尝试。作为一名电子工程师,我经常需要设计各种实用的小型嵌入式系统,这次的项目目标是实现一个能够模拟传统电话计费功能的装置。
系统核心功能很简单:当用户"拿起"电话(按下自锁按键)时开始计时计费,"放下"时停止计费。但实际开发中需要考虑很多细节:不同时段的费率差异(白天/夜晚)、分段计费(前三分钟和后三分钟费率不同)、实时金额显示等。这些功能都需要通过硬件电路和软件逻辑的配合来实现。
选择STM32F103C8T6作为主控是因为它性价比高,48个引脚完全够用,而且STM32系列在嵌入式领域应用广泛,资料丰富。计时功能使用DS1302时钟芯片,显示部分则采用OLED屏,这些都是经过市场验证的成熟方案。
2. 系统硬件设计
2.1 主控芯片选型与电路设计
STM32F103C8T6这款单片机属于STM32的"中等容量"产品线,具有:
- 72MHz主频的Cortex-M3内核
- 64KB Flash + 20KB SRAM
- 2个SPI、2个I2C、3个USART接口
- 37个快速I/O口
最小系统电路设计要点:
- 复位电路:10kΩ上拉电阻+0.1μF电容构成典型复位电路
- 时钟电路:8MHz晶振+两个22pF负载电容
- 电源滤波:每个电源引脚就近放置0.1μF去耦电容
- 调试接口:预留SWD接口用于程序下载和调试
特别注意:PC13-PC15引脚驱动能力有限,不能直接驱动LED等负载。我在最初设计时就犯过这个错误,导致系统不稳定。
2.2 状态检测电路
电话状态(拿起/放下)检测使用自锁按键实现,电路设计要点:
- 按键一端接地,另一端通过10kΩ上拉电阻接3.3V
- 信号线接入单片机GPIO(配置为上拉输入模式)
- 并联0.1μF电容消除抖动
实际测试发现机械按键存在约5-10ms的抖动,因此在软件中需要添加防抖处理:
c复制#define DEBOUNCE_TIME 20 // 消抖时间20ms
uint8_t read_key_state(void) {
static uint32_t last_time = 0;
uint8_t state = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
if(state == 0) { // 按键按下
if(HAL_GetTick() - last_time > DEBOUNCE_TIME) {
last_time = HAL_GetTick();
return 1;
}
}
return 0;
}
2.3 显示模块设计
选用0.96寸OLED显示屏(SSD1306驱动),主要参数:
- 分辨率:128×64
- 接口:I2C(节省IO口)
- 工作电压:3.3V
接线方式:
- SCL → PB6
- SDA → PB7
- VCC → 3.3V
- GND → GND
显示内容布局设计:
code复制+---------------------+
| 时间:12:30:05 |
| 状态:白天模式 |
| |
| 当前通话:02:15 |
| 当前费用:1.25元 |
+---------------------+
2.4 实时时钟电路
DS1302时钟芯片关键特性:
- 实时时钟/日历功能
- 31字节静态RAM
- 2.0V-5.5V宽电压工作
- 三线接口
典型连接电路:
- VCC1 → 3.3V(主电源)
- VCC2 → 3V纽扣电池(备份电源)
- SCLK → PA0
- I/O → PA1
- CE → PA2
经验分享:DS1302对时序要求严格,在初始化时要先禁用写保护,设置完成后记得重新启用写保护,否则时间可能不准。
3. 系统软件设计
3.1 主程序流程设计
系统采用状态机设计模式,主要状态包括:
- 待机状态
- 通话状态
- 结算状态
主程序流程图:
c复制int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C_Init();
ds1302_init();
oled_init();
// 主循环
while (1) {
switch(system_state) {
case STANDBY:
standby_handler();
break;
case CALLING:
calling_handler();
break;
case BILLING:
billing_handler();
break;
}
update_display();
HAL_Delay(100);
}
}
3.2 计费算法实现
计费规则:
- 白天模式(8:00-22:00):
- 前三分钟:0.3元/分钟
- 之后:0.15元/分钟
- 夜晚模式(22:00-次日8:00):
- 前三分钟:0.2元/分钟
- 之后:0.1元/分钟
计费函数实现:
c复制float calculate_fee(uint32_t seconds, uint8_t is_daytime) {
float rate1, rate2;
uint32_t threshold = 180; // 3分钟=180秒
if(is_daytime) {
rate1 = 0.3f / 60; // 前三分钟费率
rate2 = 0.15f / 60; // 之后费率
} else {
rate1 = 0.2f / 60;
rate2 = 0.1f / 60;
}
if(seconds <= threshold) {
return seconds * rate1;
} else {
return threshold * rate1 + (seconds - threshold) * rate2;
}
}
3.3 时间处理模块
DS1302驱动关键函数:
c复制// 读取当前时间
void ds1302_read_time(TimeStruct *time) {
uint8_t buffer[7];
ds1302_read(0x81, buffer, 7); // 读取秒分时日月周年
time->seconds = bcd_to_dec(buffer[0] & 0x7F);
time->minutes = bcd_to_dec(buffer[1]);
time->hours = bcd_to_dec(buffer[2]);
// ...其他字段类似处理
}
// 判断当前是否为白天
uint8_t is_daytime(TimeStruct *time) {
return (time->hours >= 8 && time->hours < 22);
}
3.4 显示驱动实现
OLED显示刷新函数:
c复制void update_display(void) {
char buffer[20];
TimeStruct current_time;
ds1302_read_time(¤t_time);
// 清屏
oled_clear();
// 显示时间
sprintf(buffer, "时间:%02d:%02d:%02d",
current_time.hours,
current_time.minutes,
current_time.seconds);
oled_show_string(0, 0, buffer);
// 显示模式
oled_show_string(0, 2, is_daytime(¤t_time) ? "模式:白天" : "模式:夜晚");
// 显示通话时长和费用
if(system_state == CALLING || system_state == BILLING) {
sprintf(buffer, "通话:%02d:%02d", call_duration / 60, call_duration % 60);
oled_show_string(0, 4, buffer);
sprintf(buffer, "费用:%.2f元", current_fee);
oled_show_string(0, 6, buffer);
}
}
4. 系统调试与优化
4.1 硬件调试问题记录
-
电源问题:
- 现象:系统偶尔会复位
- 排查:用示波器观察3.3V电源,发现有大电流负载时电压跌落
- 解决:在电源输入端增加100μF电解电容,每个芯片VCC引脚增加0.1μF陶瓷电容
-
I2C通信失败:
- 现象:OLED屏不显示
- 排查:逻辑分析仪抓取I2C波形,发现SCL频率过高
- 解决:调整I2C时钟分频,将频率降至100kHz
-
DS1302时间不准:
- 现象:每天快约2分钟
- 排查:晶振负载电容不匹配
- 解决:将晶振的22pF负载电容更换为6pF
4.2 软件优化措施
- 低功耗优化:
c复制// 在待机状态下进入低功耗模式
void standby_handler(void) {
// 关闭不必要的外设时钟
__HAL_RCC_SPI1_CLK_DISABLE();
// 进入睡眠模式
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
// 唤醒后重新初始化外设
MX_SPI1_Init();
}
-
显示刷新优化:
- 原始方案:全屏刷新,耗时约50ms
- 优化方案:局部刷新,仅更新变化部分,耗时降至5-10ms
-
计费精度提升:
- 原始方案:每分钟计算一次费用
- 优化方案:每秒计算并累加费用,提高计费精度
4.3 系统测试数据
| 测试场景 | 通话时长 | 预期费用 | 实测费用 | 误差 |
|---|---|---|---|---|
| 白天-2分钟 | 120s | 0.60元 | 0.60元 | 0% |
| 白天-5分钟 | 300s | 0.90元 | 0.90元 | 0% |
| 夜晚-1分钟 | 60s | 0.20元 | 0.20元 | 0% |
| 夜晚-4分钟 | 240s | 0.50元 | 0.50元 | 0% |
| 跨时段通话 | 跨越22:00 | 分段计算 | 正确分段 | 0% |
5. 项目总结与扩展思考
经过两周的开发调试,这个电话计费系统已经能够稳定运行,各项功能指标都达到了设计要求。在这个过程中,有几个关键点值得总结:
-
硬件设计方面:
- 电源设计要预留足够余量
- 信号线要合理布局,避免交叉干扰
- 关键信号最好预留测试点
-
软件开发方面:
- 状态机模型非常适合这类控制系统
- 定时器中断要合理分配优先级
- 浮点运算在STM32F103上较慢,可以考虑使用定点数优化
-
扩展思考:
- 可以增加IC卡读卡器,实现预付费功能
- 添加蓝牙模块,实现手机APP查询通话记录
- 改用更大容量的STM32芯片,支持多路电话计费
这个项目虽然不大,但涵盖了嵌入式系统开发的完整流程:从需求分析、方案设计、硬件选型、电路设计、软件开发到调试优化。对于想学习STM32开发的朋友,这类实际项目是非常好的练手机会。