用STM32的GPIO口驱动TM1668数码管,这份代码注释我帮你写好了
·
STM32驱动TM1668数码管:从零构建可维护的嵌入式代码
在嵌入式开发中,驱动数码管显示模块是基础但至关重要的技能。TM1668作为一款集成了键盘扫描和LED驱动功能的芯片,被广泛应用于各种嵌入式设备。本文将带你从零开始,用STM32的GPIO口构建一个结构清晰、注释完整、易于维护的TM1668驱动模块。
1. 硬件连接与初始化配置
1.1 GPIO引脚定义与硬件连接
首先我们需要明确硬件连接方式。TM1668通常需要三个控制信号线:
- DIO :数据输入/输出线
- CLK :时钟信号线
- STB :片选信号线
在STM32中,我们可以这样定义这些引脚:
// 端口与引脚定义
#define DIO_PORT GPIOC
#define DIO_PIN GPIO_Pin_9
#define CLK_PORT GPIOC
#define CLK_PIN GPIO_Pin_8
#define STB_PORT GPIOC
#define STB_PIN GPIO_Pin_7
// 电平操作宏
#define DIO_L GPIO_ResetBits(DIO_PORT, DIO_PIN)
#define DIO_H GPIO_SetBits(DIO_PORT, DIO_PIN)
#define CLK_L GPIO_ResetBits(CLK_PORT, CLK_PIN)
#define CLK_H GPIO_SetBits(CLK_PORT, CLK_PIN)
#define STB_L GPIO_ResetBits(STB_PORT, STB_PIN)
#define STB_H GPIO_SetBits(STB_PORT, STB_PIN)
注意:DIO线需要配置为开漏输出模式,因为它既用于输出也用于输入(读取按键值)。CLK和STB则使用推挽输出模式。
1.2 GPIO初始化函数
正确的GPIO初始化是驱动工作的基础:
void gpio_init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
// DIO配置为开漏输出,便于双向通信
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_InitStructure.GPIO_Pin = DIO_PIN;
GPIO_Init(DIO_PORT, &GPIO_InitStructure);
// CLK和STB配置为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT_PP;
GPIO_InitStructure.GPIO_Pin = CLK_PIN;
GPIO_Init(CLK_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = STB_PIN;
GPIO_Init(STB_PORT, &GPIO_InitStructure);
// 初始状态设置
STB_H; // 片选无效
CLK_H; // 时钟初始高电平
DIO_H; // 数据线初始高电平
}
2. TM1668通信协议实现
2.1 基本通信函数
TM1668使用类似I2C的通信协议,但有自己的时序要求。我们需要实现最基本的字节读写函数:
// 微秒级延时函数
void tm1668_delay(unsigned char us) {
while(us--) {
__NOP(); __NOP(); __NOP(); __NOP();
}
}
// 向TM1668写入一个字节
void write_byte(uint8_t data) {
uint8_t i;
for(i = 0; i < 8; i++) {
CLK_L;
tm1668_delay(2);
// 按位输出数据,LSB first
(data & 0x01) ? DIO_H : DIO_L;
tm1668_delay(2);
CLK_H;
tm1668_delay(2);
data >>= 1; // 准备下一位
}
}
// 从TM1668读取一个字节
uint8_t read_byte(void) {
uint8_t i, data = 0;
// 先将DIO设置为输入
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOating;
GPIO_InitStructure.GPIO_Pin = DIO_PIN;
GPIO_Init(DIO_PORT, &GPIO_InitStructure);
for(i = 0; i < 8; i++) {
CLK_L;
tm1668_delay(2);
CLK_H;
tm1668_delay(2);
data >>= 1; // 先接收的是最低位
if(GPIO_ReadInputDataBit(DIO_PORT, DIO_PIN)) {
data |= 0x80;
}
}
// 读取完成后将DIO恢复为输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT_OD;
GPIO_Init(DIO_PORT, &GPIO_InitStructure);
return data;
}
2.2 命令发送函数
TM1668的操作都是通过发送特定命令实现的:
// 发送命令函数
void send_command(uint8_t cmd) {
STB_L;
tm1668_delay(2);
write_byte(cmd);
STB_H;
tm1668_delay(2);
}
// 显示控制命令
#define DISP_CTRL_OFF 0x80
#define DISP_CTRL_ON 0x88
#define DISP_CTRL_PWM(x) (0x80 | ((x) & 0x07)) // 亮度控制
// 显示模式设置
#define DISP_MODE_4DIGIT_13SEG 0x00
#define DISP_MODE_5DIGIT_12SEG 0x01
#define DISP_MODE_6DIGIT_11SEG 0x02
#define DISP_MODE_7DIGIT_10SEG 0x03
// 数据命令设置
#define DATA_CMD_WRITE 0x40
#define DATA_CMD_READ_KEY 0x42
#define DATA_CMD_AUTO_ADDR_INC 0x40
#define DATA_CMD_FIXED_ADDR 0x44
3. 显示驱动实现
3.1 地址自增模式写入
地址自增模式适合连续写入多个显示数据:
void write_display_auto_inc(uint8_t *data, uint8_t len, uint8_t brightness) {
// 设置显示模式:7位数码管,10段
send_command(DISP_MODE_7DIGIT_10SEG);
// 设置数据命令:地址自动增加模式
send_command(DATA_CMD_AUTO_ADDR_INC);
// 开始写入数据
STB_L;
tm1668_delay(2);
// 写入起始地址
write_byte(0xC0); // 从地址0开始
// 连续写入数据
for(uint8_t i = 0; i < len; i++) {
write_byte(data[i]);
}
STB_H;
tm1668_delay(2);
// 设置亮度并开启显示
send_command(DISP_CTRL_PWM(brightness) | DISP_CTRL_ON);
}
3.2 固定地址模式写入
固定地址模式适合单独更新某个数码管:
void write_display_fixed_addr(uint8_t addr, uint8_t data, uint8_t brightness) {
// 设置显示模式
send_command(DISP_MODE_7DIGIT_10SEG);
// 设置数据命令:固定地址模式
send_command(DATA_CMD_FIXED_ADDR);
// 写入指定地址的数据
STB_L;
tm1668_delay(2);
// 地址格式:0xC0 + addr
write_byte(0xC0 | (addr & 0x0F));
write_byte(data);
STB_H;
tm1668_delay(2);
// 设置亮度并开启显示
send_command(DISP_CTRL_PWM(brightness) | DISP_CTRL_ON);
}
4. 按键扫描功能实现
TM1668还集成了键盘扫描功能,可以读取最多10个按键的状态:
#define KEY_DATA_LEN 2 // TM1668返回2字节按键数据
uint8_t read_keys(uint8_t *key_data) {
uint8_t valid = 0;
// 发送读按键命令
STB_L;
tm1668_delay(2);
write_byte(DATA_CMD_READ_KEY);
tm1668_delay(2);
// 读取按键数据
for(uint8_t i = 0; i < KEY_DATA_LEN; i++) {
key_data[i] = read_byte();
if(key_data[i] != 0xFF) {
valid = 1; // 检测到有按键按下
}
}
STB_H;
tm1668_delay(2);
return valid;
}
5. 高级应用与优化
5.1 数码管显示缓存管理
在实际应用中,我们通常会维护一个显示缓存:
uint8_t display_buffer[7]; // 7位数码管
void update_display(uint8_t brightness) {
write_display_auto_inc(display_buffer, 7, brightness);
}
void set_digit(uint8_t pos, uint8_t value, uint8_t dot) {
if(pos >= 7) return;
// 这里需要根据实际段码表转换
display_buffer[pos] = digit_to_segment(value) | (dot ? 0x80 : 0x00);
}
// 示例段码表(共阴极数码管)
static const uint8_t segment_map[] = {
// 0 1 2 3 4 5 6 7 8 9
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
uint8_t digit_to_segment(uint8_t digit) {
if(digit > 9) return 0;
return segment_map[digit];
}
5.2 按键消抖处理
读取按键时通常需要消抖处理:
#define DEBOUNCE_TIME 20 // 消抖时间(ms)
uint8_t get_key_press(void) {
static uint8_t last_key[KEY_DATA_LEN] = {0xFF, 0xFF};
static uint32_t last_time = 0;
uint8_t current_key[KEY_DATA_LEN];
uint8_t key_pressed = 0;
if(HAL_GetTick() - last_time < DEBOUNCE_TIME) {
return 0;
}
if(read_keys(current_key)) {
// 检查按键变化
for(uint8_t i = 0; i < KEY_DATA_LEN; i++) {
if(current_key[i] != last_key[i]) {
key_pressed = 1;
break;
}
}
if(key_pressed) {
memcpy(last_key, current_key, KEY_DATA_LEN);
last_time = HAL_GetTick();
return 1;
}
} else {
memset(last_key, 0xFF, KEY_DATA_LEN);
}
return 0;
}
5.3 低功耗优化
在电池供电应用中,我们可以优化功耗:
void tm1668_sleep(void) {
// 关闭显示
send_command(DISP_CTRL_OFF);
// 将GPIO设置为输入模式减少功耗
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_Pin = DIO_PIN | CLK_PIN | STB_PIN;
GPIO_Init(DIO_PORT, &GPIO_InitStructure);
}
void tm1668_wakeup(uint8_t brightness) {
// 重新初始化GPIO
gpio_init();
// 清空显示
uint8_t blank[7] = {0};
write_display_auto_inc(blank, 7, brightness);
}
6. 常见问题与调试技巧
6.1 显示不正常排查步骤
-
检查硬件连接
- 确认VCC和GND连接正确
- 检查DIO、CLK、STB线序
- 确保数码管共阴/共阳配置正确
-
检查GPIO配置
- DIO应为开漏输出
- CLK和STB应为推挽输出
- 确认GPIO时钟已使能
-
时序问题
- 增加延时观察是否改善
- 用逻辑分析仪抓取通信波形
6.2 按键读取不准确处理
- 确保在读取按键前已将DIO设置为输入模式
- 检查上拉电阻是否合适(通常4.7KΩ)
- 增加消抖处理算法
- 确认按键矩阵连接正确
6.3 性能优化建议
- 将频繁调用的函数声明为
inline - 使用DMA传输显示数据(如果支持)
- 将段码表存放在Flash而非RAM
- 使用位带操作加速GPIO控制
// 示例:使用位带操作优化GPIO控制
#define BITBAND(addr, bitnum) ((addr & 0xF0000000) + 0x2000000 + ((addr & 0xFFFFF) << 5) + (bitnum << 2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND((unsigned long)&addr, bitnum))
// 重定义GPIO操作为位带操作
#define DIO_L BIT_ADDR(DIO_PORT->ODR, DIO_PIN) = 0
#define DIO_H BIT_ADDR(DIO_PORT->ODR, DIO_PIN) = 1
更多推荐

所有评论(0)