在嵌入式开发中,数码管显示是最基础也最常用的功能之一。许多初学者在驱动多位数码管时,往往会采用简单的阻塞延时方式来实现扫描显示,这种方式虽然容易理解,但在实际应用中却存在诸多问题。本文将深入探讨如何利用STM32的定时器中断来实现数码管的非阻塞驱动,让你的显示效果更加稳定可靠。
传统的数码管驱动方式通常采用轮询加延时的方法,代码结构大致如下:
c复制while(1) {
displayDigit(1, digit1); // 显示第一位
delay_ms(5); // 延时5ms
displayDigit(2, digit2); // 显示第二位
delay_ms(5); // 延时5ms
}
这种方法看似简单,但实际上存在几个严重问题:
相比之下,定时器中断驱动方式具有以下优势:
数码管本质上是由多个LED组成的显示器件。以常见的7段数码管为例,它包含7个LED段(a-g)和1个小数点(dp)。多位数码管则是将多个这样的单元集成在一起,共享段选线,通过位选线控制哪一位显示。
数码管有共阴和共阳两种类型:
| 类型 | 公共端连接 | 点亮条件 |
|---|---|---|
| 共阴 | 阴极 | 对应段输入高电平 |
| 共阳 | 阳极 | 对应段输入低电平 |
在设计STM32驱动电路时,需要考虑以下几点:
典型的连接方式如下:
code复制数码管段选 a -> PA0
数码管段选 b -> PA1
...
数码管位选 1 -> PB0
数码管位选 2 -> PB1
首先需要配置STM32的定时器,以产生定期中断。以下是一个基本的定时器配置示例:
c复制void TIM_Config(void)
{
TIM_HandleTypeDef htim;
htim.Instance = TIM2;
htim.Init.Prescaler = 72-1; // 72MHz/72 = 1MHz
htim.Init.CounterMode = TIM_COUNTERMODE_UP;
htim.Init.Period = 1000-1; // 1MHz/1000 = 1kHz
htim.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim);
HAL_TIM_Base_Start_IT(&htim);
}
这段代码配置TIM2定时器产生1kHz的中断频率(即每1ms中断一次)。
在中断服务函数中实现数码管的扫描逻辑:
c复制volatile uint8_t currentDigit = 0;
volatile uint8_t displayData[2] = {0, 0};
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2) {
// 关闭所有位选
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0|GPIO_PIN_1, GPIO_PIN_SET);
// 根据当前显示位设置段选数据
setSegments(displayData[currentDigit]);
// 打开当前位选
HAL_GPIO_WritePin(GPIOB, currentDigit ? GPIO_PIN_1 : GPIO_PIN_0, GPIO_PIN_RESET);
// 切换到下一位
currentDigit = !currentDigit;
}
}
为了方便管理,可以预先定义数字0-9的段选数据:
c复制const uint8_t digitPattern[10] = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F // 9
};
void setSegments(uint8_t digit)
{
uint8_t pattern = digitPattern[digit];
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, (pattern & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); // a
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, (pattern & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); // b
// ... 其他段类似
}
通过调整定时器中断频率或占空比,可以实现数码管亮度控制:
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint8_t phase = 0;
if(phase < brightness) {
// 显示阶段
setSegments(displayData[currentDigit]);
HAL_GPIO_WritePin(GPIOB, currentDigit ? GPIO_PIN_1 : GPIO_PIN_0, GPIO_PIN_RESET);
} else {
// 关闭阶段
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0|GPIO_PIN_1, GPIO_PIN_SET);
}
if(++phase >= 10) {
phase = 0;
currentDigit = !currentDigit;
}
}
对于更多位数的数码管,只需扩展位选控制和显示缓冲区:
c复制#define DIGIT_NUM 4
volatile uint8_t currentDigit = 0;
volatile uint8_t displayData[DIGIT_NUM] = {0, 0, 0, 0};
const uint16_t digitSelect[DIGIT_NUM] = {
GPIO_PIN_0, // 第1位
GPIO_PIN_1, // 第2位
GPIO_PIN_2, // 第3位
GPIO_PIN_3 // 第4位
};
为了避免在更新显示数据时出现闪烁或撕裂现象,可以采用双缓冲技术:
c复制uint8_t displayBuffer[2][DIGIT_NUM]; // 双缓冲
volatile uint8_t activeBuffer = 0;
void updateDisplay(uint8_t newData[DIGIT_NUM])
{
uint8_t inactiveBuffer = !activeBuffer;
memcpy(displayBuffer[inactiveBuffer], newData, DIGIT_NUM);
activeBuffer = inactiveBuffer;
}
利用定时器中断驱动数码管,可以轻松实现一个精确的计时器:
c复制volatile uint32_t milliseconds = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2) {
milliseconds++;
// 每1000ms更新一次显示
if(milliseconds % 1000 == 0) {
uint32_t seconds = milliseconds / 1000;
displayData[0] = seconds / 10;
displayData[1] = seconds % 10;
}
// 数码管扫描逻辑...
}
}
在主循环中处理按键输入,完全不影响数码管显示:
c复制while(1) {
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// 按键处理
displayData[0]++;
if(displayData[0] > 9) displayData[0] = 0;
HAL_Delay(50); // 简单的防抖
}
// 其他任务...
}
为了确保系统稳定性,需要尽量缩短中断服务函数的执行时间:
显示闪烁:
亮度不均:
显示错乱:
对于更高级的应用,可以结合DMA来进一步减轻CPU负担:
c复制// 配置DMA将显示数据自动传输到GPIO端口
void DMA_Config(void)
{
// DMA配置代码...
}
这种方案特别适合多位数字管或LED矩阵等需要高速刷新的场合。