<think>我们已知:
1. 数码管型号:5461BS-1,这是一个四位共阳极数码管。
2. 要显示的数字:507(注意,四位数码管,所以应该是0507?或者我们要考虑显示为507,即第一位不显示或显示空格?但通常四位数码管,如果要显示507,我们一般会显示为“507 ”,即最后一位空,或者显示为“0507”?这里用户要求显示507,我们可以显示为“507 ”,即第一位显示5,第二位显示0,第三位显示7,第四位不显示。或者根据实际需求,也可以显示为“507”在右边,左边空,即第一位空,然后显示507。这里我们按常规,显示为“507 ”,即第四位空。但用户没有明确,我们按显示三位数字,第四位不显示(即消隐)来处理。
由于是共阳极数码管,所以公共端(位选)给高电平,段选给低电平,对应的段就会亮。
接线方案:
我们使用两个74HC595芯片级联来控制数码管。74HC595是串行输入并行输出的移位寄存器,可以节省IO口。
级联方式:第一个595的QH'(第9脚)连接到第二个595的SER(第14脚)。
STM32F103C8T6通过三个IO口控制两个74HC595:
- SCK(移位时钟):接两个595的SCK(第11脚)
- RCK(锁存时钟):接两个595的RCK(第12脚)
- DATA(数据线):接第一个595的SER(第14脚)
两个74HC595级联后,共有16位输出。我们使用8位数码管,但这里我们只有四位数码管,所以实际上我们只需要8位(段选)和4位(位选)共12位。但是我们的模块是8位数码管模块,所以它可能用了两个595来控制8位数码管的8个段选和8个位选?但注意,通常两个595级联,第一个595控制段选(8位),第二个595控制位选(8位),这样8位数码管就可以独立控制。
但是我们的数码管是四位的,所以我们可以这样分配:
- 第一个595的8位输出(Q0~Q7)分别接数码管的a,b,c,d,e,f,g,dp(即段选)
- 第二个595的8位输出中,我们只用低4位(Q0~Q3)分别接四位共阳极数码管的位选(COM1~COM4),高4位不用。
注意:共阳极数码管,位选为高电平时选中该位。
显示原理:
我们采用动态扫描,每次只点亮一位数码管,依次点亮四位,利用人眼视觉暂留效果。
步骤:
1. 初始化STM32的IO口(配置为推挽输出)。
2. 编写向74HC595发送一个字节(8位)数据的函数,由于级联,我们需要发送两个字节(先发送位选数据,再发送段选数据?注意:在移位时,先发送的数据会进入第二个595,后发送的数据在第一个595。所以如果我们先发送位选数据(第二个595),再发送段选数据(第一个595),那么在移位完成后,第一个595的数据对应段选,第二个595的数据对应位选。但是这样在移位时,我们发送的第一个字节(位选)会被移到第二个595,而第二个字节(段选)会留在第一个595。然后锁存,两个595同时输出。
因此,我们发送数据的顺序是:先发送段选数据(第二个字节),再发送位选数据(第一个字节)?不对,应该是先发送要进入第二个595的数据(即位选),再发送要进入第一个595的数据(段选)。这样在移位过程中,位选数据被移到了第二个595,段选数据在第一个595。所以发送顺序是:先发送位选数据(第二个595),再发送段选数据(第一个595)。但是这样我们发送两个字节时,先发送的位选数据在移位过程中会被推到第二个595,后发送的段选数据留在第一个595。然后锁存,输出。
但是,我们通常希望发送的数据结构是:
第一个字节(高字节):段选(第一个595的输出)
第二个字节(低字节):位选(第二个595的输出)
然而,在移位时,先移入的是位选(因为位选在第二个595,需要先移进去),然后移入段选。所以当我们发送时,应该先发送段选数据(因为段选要进入第一个595,而第一个595靠近输出,所以后发送的段选数据会占据第一个595),再发送位选数据(先发送的位选数据会通过第一个595移到第二个595)。但这样不符合我们的数据结构。
实际上,级联时,数据先进入第一个595,然后从第一个595的QH'串出到第二个595的SER。所以,如果我们先发送位选数据(即第二个595要接收的数据),再发送段选数据(第一个595要接收的数据),那么在移位过程中,先移入段选数据(最后会进入第一个595),再移入位选数据(会进入第二个595)。这样,当移位完成后,第一个595中存放的是段选数据,第二个595中存放的是位选数据。然后锁存输出。
因此,发送顺序应该是:先发送段选数据(8位),再发送位选数据(8位)。这样,段选数据先进入,然后被位选数据推到了第一个595,而位选数据在第二个595。但是这样不对,因为先发送的数据会先进入移位寄存器,然后被后发送的数据推出去。所以实际上,先发送的段选数据会被推到级联的第二个595,而后发送的位选数据留在第一个595。这不符合我们的要求。
所以,我们需要调整:我们想要第一个595输出段选,第二个595输出位选。那么我们应该先发送位选数据(因为位选数据要去第二个595),再发送段选数据(段选数据留在第一个595)。这样,在发送时,先发送位选数据(8位),再发送段选数据(8位)。在移位过程中,先移入位选数据,然后移入段选数据,位选数据被推入第二个595,段选数据留在第一个595。
因此,发送顺序是:先发送位选数据(第二个595的数据),再发送段选数据(第一个595的数据)。这样,当我们发送完16位后,第一个595中存储的是段选数据,第二个595中存储的是位选数据。
但是,我们在程序中定义两个字节:
- seg_data: 段选数据(8位)
- bit_data: 位选数据(8位,但我们只用低4位)
然后发送时,先发送bit_data,再发送seg_data。
注意:由于级联,我们发送16位数据,先发送bit_data(高位字节),再发送seg_data(低位字节)?不对,应该是先发送的bit_data会进入移位寄存器的最前端,然后被后面的数据推出去到第二个595。所以,我们发送的顺序应该是:先发送bit_data,再发送seg_data。
具体发送函数:
- 拉低SCK(移位时钟)
- 从高位到低位发送bit_data(8位)
- 从高位到低位发送seg_data(8位)
- 拉高SCK(移位时钟)?不对,应该是每发送一位,产生一个上升沿移位。具体步骤:
for (i=0; i<8; i++) {
// 先发送seg_data的最高位(因为seg_data是第二个字节,但我们要后发送它,所以先发送bit_data)
// 但实际上,我们这里先发送bit_data
}
所以,我们分别发送两个字节,先发送bit_data(从高位到低位),再发送seg_data(从高位到低位)。这样,bit_data的每一位先被移入,然后seg_data的每一位被移入。当全部移入后,bit_data被推到了第二个595,seg_data在第一个595。
3. 动态显示:分别显示每一位,每一位显示一定时间(比如1ms),然后切换到下一位。
段码表(共阳极,注意:共阳极是低电平点亮,所以段码表是低电平有效的):
0: 0xC0 (1100 0000) -> a,b,c,d,e,f灭?不对,共阳极,给低电平点亮。所以0的段码应该是点亮a,b,c,d,e,f,不点亮g和dp,即1100 0000(二进制)-> 0xC0
但是注意:我们模块的段选线连接顺序。通常数码管的段顺序是:a,b,c,d,e,f,g,dp,对应74HC595的Q0~Q7。所以:
0: a,b,c,d,e,f亮 -> 0b11000000 -> 0xC0
1: b,c亮 -> 0b11111001 -> 0xF9
2: a,b,d,e,g亮 -> 0b10100100 -> 0xA4
3: a,b,c,d,g亮 -> 0b10110000 -> 0xB0
4: b,c,f,g亮 -> 0b10011001 -> 0x99
5: a,c,d,f,g亮 -> 0b10010010 -> 0x92
6: a,c,d,e,f,g亮 -> 0b10000010 -> 0x82
7: a,b,c亮 -> 0b11111000 -> 0xF8
8: 全部亮 -> 0b10000000 -> 0x80
9: a,b,c,d,f,g亮 -> 0b10010000 -> 0x90
其他:比如空格(不显示)则段码全1(0xFF)
注意:这里我们使用的是共阳极,所以段选为0表示亮,1表示灭。
位选:我们使用低4位,分别对应数码管的第1位到第4位(从左到右)。例如,要显示第一位,则位选数据为0b00000001(即0x01),第二位0x02,第三位0x04,第四位0x08。注意:因为我们的位选是共阳极,所以给高电平选中。
但是,我们使用两个595,第二个595的输出Q0~Q7,我们只用Q0~Q3(低4位)作为位选,所以位选数据我们只使用低4位,高4位设为0(不选中其他位)。
要显示“507 ”(第四位空),那么:
第一位:5 -> 段码0x92(二进制10010010)
第二位:0 -> 段码0xC0
第三位:7 -> 段码0xF8
第四位:空 -> 段码0xFF
动态扫描时,我们依次显示每一位:
第一步:发送位选数据(0x01,即选中第一位),段选数据(0x92),然后锁存输出,延时1ms。
第二步:发送位选数据(0x02),段选数据(0xC0),延时1ms。
第三步:发送位选数据(0x04),段选数据(0xF8),延时1ms。
第四步:发送位选数据(0x08),段选数据(0xFF),延时1ms。
注意:由于人眼视觉暂留,刷新频率要大于50Hz,所以四位数码管,每位的显示时间1ms,则一周期4ms,刷新频率250Hz,满足要求。
程序框架:
1. 定义引脚:
#define DATA_PIN GPIO_Pin_0 // 假设使用GPIOA的0号引脚
#define SCK_PIN GPIO_Pin_1
#define RCK_PIN GPIO_Pin_2
2. 初始化GPIO:配置为推挽输出,初始化时全部置低。
3. 写一个函数:void HC595_Send_Data(uint8_t bit_data, uint8_t seg_data)
这个函数先发送bit_data(8位),再发送seg_data(8位),然后产生一个锁存信号。
4. 定义段码表:const uint8_t seg_code[] = {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90}; // 0~9的段码
5. 主循环中,不断扫描四位:
uint8_t display_digit[4] = {5,0,7,10}; // 10表示空,我们定义空用0xFF,即段码表中没有,我们单独处理
uint8_t bit_select[4] = {0x01, 0x02, 0x04, 0x08}; // 位选
for (int i=0; i<4; i++) {
uint8_t seg;
if (display_digit[i] == 10) // 假设10代表空
seg = 0xFF;
else
seg = seg_code[display_digit[i]];
HC595_Send_Data(bit_select[i], seg);
delay_ms(1); // 延时1ms
}
注意:在发送数据后,需要锁存,将移位寄存器的数据输出到锁存器。锁存信号:在发送完16位数据后,拉高RCK(锁存时钟)一个脉冲(先低再高,然后拉低)。
具体发送函数HC595_Send_Data的实现:
void HC595_Send_Data(uint8_t bit_data, uint8_t seg_data)
{
// 先发送bit_data(8位),从高位到低位
// 再发送seg_data(8位),从高位到低位
uint8_t i;
// 拉低RCK,在发送过程中保持低电平
GPIO_ResetBits(GPIOA, RCK_PIN);
// 先发送bit_data(第二个595的数据)
for (i=0; i<8; i++) {
GPIO_ResetBits(GPIOA, SCK_PIN); // 时钟拉低
// 判断最高位,然后设置数据线
if (bit_data & 0x80)
GPIO_SetBits(GPIOA, DATA_PIN);
else
GPIO_ResetBits(GPIOA, DATA_PIN);
bit_data <<= 1; // 左移一位
// 产生一个上升沿,移位
GPIO_SetBits(GPIOA, SCK_PIN);
// 这里可以加一个小的延时,或者不加,因为STM32速度很快
}
// 再发送seg_data(第一个595的数据)
for (i=0; i<8; i++) {
GPIO_ResetBits(GPIOA, SCK_PIN);
if (seg_data & 0x80)
GPIO_SetBits(GPIOA, DATA_PIN);
else
GPIO_ResetBits(GPIOA, DATA_PIN);
seg_data <<= 1;
GPIO_SetBits(GPIOA, SCK_PIN);
}
// 发送完毕,产生锁存信号:拉高RCK,再拉低
GPIO_SetBits(GPIOA, RCK_PIN);
// 延时一下
delay_us(10);
GPIO_ResetBits(GPIOA, RCK_PIN);
}
注意:由于我们发送的是两个字节,先发送bit_data(位选)到第二个595,再发送seg_data(段选)到第一个595,所以锁存后,两个595同时输出。
但是,在动态扫描时,由于我们每次只点亮一位,所以位选数据每次只选中一位。而段选数据则是对应该位的数字。
延时函数:可以使用SysTick或者简单的循环延时。
以上是程序的基本思路。
但是,我们还需要考虑硬件连接:
- 数码管的段选线(a,b,c,d,e,f,g,dp)是否按照顺序连接到了第一个595的Q0~Q7(注意:Q0对应a,Q1对应b,...,Q7对应dp)?这个需要根据实际硬件连接调整段码表。如果连接顺序不同,段码表需要调整。
另外,位选线:我们假设位选线是低4位(第二个595的Q0~Q3)分别接数码管的第1位到第4位。注意:共阳极,所以位选高电平选中。
如果实际连接不同,需要调整。
由于用户没有提供具体的硬件连接图,我们按照常见的连接方式编写程序。用户需要根据实际连接调整。
最后,我们给出完整的代码框架(基于标准外设库)。
注意:这里我们使用GPIOA的0、1、2引脚分别作为DATA、SCK、RCK。
初始化代码:
在main函数之前:
定义引脚和段码表,以及发送函数。
我们使用简单的延时函数(循环延时)。
完整代码:</think>以下是基于STM32F103C8T6驱动5461BS-1四位数码管显示"507"的完整程序代码及解析。5461BS-1是共阳极四位数码管,采用动态扫描方式驱动,通过两个级联的74HC595芯片实现控制(节省IO资源)[^1][^2]。
---
### 硬件连接
| STM32引脚 | 74HC595引脚 | 功能 |
|-----------|-------------|------------|
| PA0 | SER (14) | 串行数据 |
| PA1 | SRCLK (11) | 移位时钟 |
| PA2 | RCLK (12) | 锁存时钟 |
| VCC | VCC | 电源 |
| GND | GND | 地线 |
数码管段选(a-g,dp)连接第一个74HC595的Q0-Q7,位选(COM1-COM4)连接第二个74HC595的Q0-Q3。
---
### 程序代码(Keil MDK环境)
```c
#include "stm32f10x.h"
// 引脚定义
#define DATA_PIN GPIO_Pin_0 // PA0
#define SCK_PIN GPIO_Pin_1 // PA1
#define RCK_PIN GPIO_Pin_2 // PA2
#define GPIO_PORT GPIOA
// 共阳极数码管段码表 (0-9)
const uint8_t SEG_CODE[10] = {
0xC0, // 0 (11000000)
0xF9, // 1 (11111001)
0xA4, // 2 (10100100)
0xB0, // 3 (10110000)
0x99, // 4 (10011001)
0x92, // 5 (10010010) <- 需要显示的数字
0x82, // 6 (10000010)
0xF8, // 7 (11111000) <- 需要显示的数字
0x80, // 8 (10000000)
0x90 // 9 (10010000)
};
// 位选控制 (1<<0:COM1, 1<<1:COM2, ...)
const uint8_t DIGIT_SELECT[4] = {0x01, 0x02, 0x04, 0x08};
// 向74HC595发送16位数据 (高位在前)
void HC595_SendData(uint16_t data) {
GPIO_ResetBits(GPIO_PORT, RCK_PIN); // 锁存时钟拉低
for (uint8_t i = 0; i < 16; i++) {
GPIO_ResetBits(GPIO_PORT, SCK_PIN); // 移位时钟拉低
// 发送当前位 (高位先发)
if (data & 0x8000)
GPIO_SetBits(GPIO_PORT, DATA_PIN);
else
GPIO_ResetBits(GPIO_PORT, DATA_PIN);
data <<= 1; // 移位准备下一位
GPIO_SetBits(GPIO_PORT, SCK_PIN); // 移位时钟上升沿
}
GPIO_SetBits(GPIO_PORT, RCK_PIN); // 锁存时钟上升沿(输出数据)
}
// 初始化GPIO
void GPIO_Config(void) {
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStruct.GPIO_Pin = DATA_PIN | SCK_PIN | RCK_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIO_PORT, &GPIO_InitStruct);
// 初始状态置低
GPIO_ResetBits(GPIO_PORT, DATA_PIN | SCK_PIN | RCK_PIN);
}
// 动态显示数字
void DisplayNumber(uint16_t num) {
uint8_t digits[4];
// 分解数字:digits[0]=5, digits[1]=0, digits[2]=7, digits[3]=空
digits[0] = num / 100; // 百位: 5
digits[1] = (num / 10) % 10; // 十位: 0
digits[2] = num % 10; // 个位: 7
digits[3] = 10; // 第四位不显示(自定义值)
for (uint8_t i = 0; i < 4; i++) {
uint8_t seg = (digits[i] <= 9) ? SEG_CODE[digits[i]] : 0xFF; // 不显示时全关
uint16_t output = (DIGIT_SELECT[i] << 8) | seg; // 高8位:位选, 低8位:段选
HC595_SendData(output);
Delay_ms(3); // 每位数码管显示3ms(动态扫描)
}
}
// 简单延时函数
void Delay_ms(uint32_t ms) {
for (uint32_t i = 0; i < ms * 8000; i++);
}
int main(void) {
GPIO_Config(); // 初始化GPIO
while (1) {
DisplayNumber(507); // 持续显示507
}
}
```
---
### 关键代码解析
1. **动态扫描原理**
通过快速切换位选信号(每次使能1位数码管),依次显示"5"→"0"→"7"→空,利用视觉暂留效应形成连续显示[^2]。
2. **74HC595级联控制**
- 发送16位数据:高8位控制位选(第二个595),低8位控制段选(第一个595)
- 示例数据包:`0x01 0x92` 表示在COM1显示数字5(位选0x01 + 段选0x92)
3. **显示数据格式**
```math
\text{输出数据} = (\text{位选编码} \ll 8) \lor \text{段选编码}
```
例如显示十位数字"0":
- 位选:`0x02` (COM2)
- 段选:`0xC0` (数字0)
- 输出:`0x0200 | 0x00C0 = 0x02C0`
---
### 注意事项
1. **消隐处理**
第四位设置为`digits[3]=10`,段码返回`0xFF`(所有段关闭),实现该位数码管完全关闭。
2. **刷新频率**
每位数码管显示3ms,4位总周期12ms(≈83Hz),高于人眼闪烁临界值(50Hz)[^2]。
3. **硬件适配**
若实际段码顺序(a-g,dp)与定义不符,需调整`SEG_CODE`数组的二进制位顺序。
4. **电流限制**
共阳极数码管需在VCC串联限流电阻(每段100-220Ω),防止过流损坏。
---
### 优化建议
1. 使用定时器中断实现扫描,避免`Delay_ms`占用CPU
2. 添加小数点控制(修改段码最高位)
3. 实现滚动显示效果(通过移位缓冲区)