1. 项目概述
这个基于STM32的计时秒表项目,是我在嵌入式系统开发课程中的一次实践作业。作为一个经常需要精确计时的电子爱好者,市面上的秒表要么功能单一,要么价格昂贵,于是决定自己动手做一个。这个设计不仅实现了基本的计时功能,还加入了分段计时、数据存储等实用特性。
STM32系列单片机因其丰富的外设资源和适中的价格,成为嵌入式开发的理想选择。在这个项目中,我使用了STM32F103C8T6这款Cortex-M3内核的芯片,搭配简单的按键和LCD显示屏,就实现了一个功能完整的计时秒表系统。
2. 硬件设计
2.1 核心器件选型
主控芯片选择了STM32F103C8T6,主要基于以下几点考虑:
- 72MHz主频完全满足计时精度要求
- 内置定时器资源丰富,特别是高级定时器TIM1非常适合精确计时
- 64KB Flash和20KB SRAM的存储空间足够程序运行和数据存储
- 价格亲民,开发板容易获取
显示部分使用了1602字符型LCD,虽然分辨率不高,但显示数字和简单字符足够,而且驱动简单。相比图形LCD,它不需要复杂的驱动电路和大量的RAM缓存。
2.2 电路设计要点
电源部分采用AMS1117-3.3稳压芯片,将5V输入转换为3.3V供STM32使用。虽然STM32F103的某些IO可以容忍5V,但为了系统稳定性,所有IO都工作在3.3V电平。
按键电路设计需要注意防抖处理。我在硬件上使用了0.1uF电容进行简单滤波,同时在软件中也实现了消抖算法。四个按键分别用于:启动/停止、复位、分段计时和模式切换。
重要提示:STM32的NRST复位引脚必须接10kΩ上拉电阻和0.1uF电容到地,这是保证系统可靠复位的关键。
3. 软件实现
3.1 定时器配置
精确计时是这个项目的核心。我使用了STM32的高级定时器TIM1,配置如下:
c复制// TIM1初始化
void TIM1_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
// 定时器时钟72MHz,预分频7200-1,计数频率10kHz
TIM_TimeBaseStructure.TIM_Period = 9999; // 1秒周期
TIM_TimeBaseStructure.TIM_Prescaler = 7200-1;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM1, ENABLE);
}
这样配置后,定时器每1ms产生一次更新中断,在中断服务程序中累积毫秒数,实现精确计时。
3.2 按键处理
按键处理采用状态机方式,可以很好地处理长按、短按等不同操作:
c复制typedef enum {
KEY_IDLE,
KEY_DEBOUNCE,
KEY_PRESSED,
KEY_RELEASE
} KeyState;
void Key_Process(void)
{
static KeyState state = KEY_IDLE;
static uint32_t pressTime;
switch(state) {
case KEY_IDLE:
if(按键按下) {
state = KEY_DEBOUNCE;
pressTime = GetTickCount();
}
break;
case KEY_DEBOUNCE:
if(GetTickCount() - pressTime > 20) { // 20ms消抖
if(按键仍按下) {
state = KEY_PRESSED;
// 处理按键按下事件
} else {
state = KEY_IDLE;
}
}
break;
// 其他状态处理...
}
}
3.3 显示刷新
显示刷新采用定时刷新方式,每100ms刷新一次显示内容,避免频繁刷新影响系统响应:
c复制void Display_Update(void)
{
static uint32_t lastUpdate = 0;
uint32_t now = GetTickCount();
if(now - lastUpdate >= 100) {
lastUpdate = now;
// 格式化时间显示
char buf[17];
sprintf(buf, "Time:%02d:%02d.%02d",
minutes, seconds, milliseconds/10);
LCD_SetCursor(0, 0);
LCD_WriteString(buf);
}
}
4. 功能实现细节
4.1 基本计时功能
基本计时功能实现思路:
- 在TIM1中断中累积毫秒数
- 每1000毫秒进位到秒
- 每60秒进位到分钟
- 最大计时99分59秒999毫秒
计时数据结构设计:
c复制typedef struct {
uint8_t minutes;
uint8_t seconds;
uint16_t milliseconds;
bool isRunning;
} Stopwatch;
4.2 分段计时功能
分段计时是专业秒表的重要功能。实现方法是:
- 按下分段按键时,将当前时间值存入分段记录数组
- 最多支持10次分段记录
- 通过模式切换键可以浏览全部分段记录
分段记录数据结构:
c复制#define MAX_LAPS 10
typedef struct {
Stopwatch laps[MAX_LAPS];
uint8_t lapCount;
uint8_t currentLap;
} LapRecords;
4.3 数据存储
为了实现断电不丢失数据,使用了STM32内部的Flash来存储最佳记录:
- 定义Flash页地址(避开程序存储区)
- 擦除页后再写入
- 读取时校验数据有效性
Flash操作关键代码:
c复制#define FLASH_PAGE_ADDR 0x0801F000
void Save_Record(Stopwatch* record)
{
FLASH_Unlock();
FLASH_ErasePage(FLASH_PAGE_ADDR);
uint32_t* pData = (uint32_t*)record;
for(int i=0; i<sizeof(Stopwatch)/4; i++) {
FLASH_ProgramWord(FLASH_PAGE_ADDR + i*4, pData[i]);
}
FLASH_Lock();
}
5. 系统优化与调试
5.1 计时精度优化
初始测试发现计时有约0.5%的误差,通过以下方法优化:
- 使用更高精度的外部晶振(8MHz±20ppm)
- 在SystemInit()中优化时钟树配置
- 定期校准(与标准时间源对比调整)
校准算法实现:
c复制void Calibrate_Timer(float errorPPM)
{
// 根据误差调整预分频值
uint16_t newPrescaler = (uint16_t)(7200 * (1 + errorPPM/1e6));
TIM1->PSC = newPrescaler - 1;
TIM1->EGR = TIM_PSCReloadMode_Immediate;
}
5.2 低功耗设计
虽然秒表通常不需要考虑功耗,但作为学习实践,我加入了低功耗模式:
- 无操作3分钟后进入STOP模式
- 任一按键唤醒
- RTC保持运行(用于记录休眠时间)
进入低功耗代码:
c复制void Enter_LowPowerMode(void)
{
// 配置唤醒源
PWR_WakeUpPinCmd(ENABLE);
// 进入STOP模式
PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
// 唤醒后恢复系统时钟
SystemInit();
}
5.3 调试技巧
在开发过程中,有几个调试技巧特别有用:
- 使用SWD接口和ST-Link调试器
- 利用STM32的串口打印调试信息
- 在关键代码处设置断点
- 使用逻辑分析仪抓取定时器波形
串口调试初始化:
c复制void USART_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置TX(PA9)和RX(PA10)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}
6. 常见问题与解决方案
6.1 计时不准确
可能原因及解决方法:
- 晶振负载电容不匹配 - 调整负载电容值
- 定时器配置错误 - 检查预分频和自动重载值
- 中断响应延迟 - 优化中断优先级,减少中断服务程序执行时间
6.2 按键响应异常
常见问题:
- 按键抖动 - 增加硬件滤波电容和软件消抖时间
- 长按识别错误 - 调整长按判定时间阈值
- 多键同时按下冲突 - 实现按键优先级处理
6.3 LCD显示异常
排查步骤:
- 检查对比度调节电压
- 确认初始化序列正确
- 检查数据传输时序
- 确保电源稳定
6.4 Flash写入失败
可能原因:
- 未先擦除就写入
- 写入地址不对齐
- 写保护未解除
- 电压不稳定
解决方法:
c复制FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
FLASH_ErasePage(FLASH_PAGE_ADDR);
// 确保写入地址是4的倍数
FLASH_ProgramWord(FLASH_PAGE_ADDR, data);
FLASH_Lock();
7. 项目扩展思路
这个基础秒表项目还有很大的扩展空间:
7.1 增加蓝牙连接
通过HC-05蓝牙模块,可以将计时数据发送到手机APP,实现:
- 远程控制秒表
- 数据记录和分析
- 多设备同步计时
7.2 添加运动传感器
集成MPU6050等运动传感器,可以实现:
- 运动触发计时
- 动作分析
- 运动次数统计
7.3 改用OLED显示
升级到128x64 OLED显示屏可以:
- 显示更多信息
- 支持图形化界面
- 实现动画效果
7.4 增加数据统计分析
在现有分段计时基础上,可以增加:
- 平均速度计算
- 最快/最慢分段比较
- 历史记录趋势图
这个STM32秒表项目虽然不大,但涵盖了嵌入式开发的多个关键技术点。从硬件选型到软件实现,从功能开发到性能优化,整个过程让我对STM32的开发有了更深入的理解。特别是在计时精度优化和低功耗设计方面,通过实际测试和调整,获得了教科书上难以学到的实践经验。