编程技术分享

分享编程知识,探讨技术创新

0%

简介:基于STC89C52RC单片机设计的简易数码管电子时钟

好的,作为一名高级嵌入式软件开发工程师,很高兴能和你一起探讨基于STC89C52RC单片机的简易数码管电子时钟项目。这个项目虽然看似简单,但却是学习和实践嵌入式系统开发流程的绝佳案例。我们可以通过这个项目,深入理解从需求分析、架构设计、代码实现、测试验证到维护升级的完整过程,并构建一个可靠、高效、可扩展的系统平台。
关注微信公众号,提前获取相关推文

项目概述

本项目旨在设计一个基于STC89C52RC单片机的简易数码管电子时钟。该时钟能够准确显示时、分、秒,并具备基本的校时功能。通过本项目,我们将实践嵌入式系统开发的各个环节,并采用经过验证的技术和方法,确保系统的稳定性和可靠性。

需求分析

在项目启动之初,我们需要进行详细的需求分析,明确系统的功能和性能指标。对于简易数码管电子时钟,其主要需求如下:

  1. 基本功能

    • 时间显示:能够以24小时制或12小时制(可配置)在数码管上清晰显示当前时间,包括小时、分钟和秒。
    • 时间精度:基于单片机内部定时器,保证时间的准确性。
    • 时间可调:提供按键或旋钮等输入方式,允许用户手动调整时间,包括小时和分钟。
    • 断电保持:即使系统断电重启,时间信息也能得到一定程度的保持(虽然STC89C52RC本身不带RTC,但我们可以通过软件方式模拟或外接RTC模块)。
  2. 性能指标

    • 功耗:在保证功能的前提下,尽量降低系统功耗,延长使用寿命。
    • 响应速度:按键操作响应及时,时间更新显示流畅。
    • 稳定性:系统能够长时间稳定运行,不易出现死机或时间错乱等问题。
    • 可靠性:硬件和软件设计可靠,能够抵抗一定的干扰。
  3. 扩展性(虽然是简易项目,但架构设计需考虑)

    • 功能扩展:预留扩展接口,方便后续添加闹钟、秒表等功能。
    • 硬件扩展:架构设计应易于移植到其他单片机平台或扩展外围模块。

系统架构设计

为了构建一个可靠、高效、可扩展的系统平台,我推荐采用分层架构的设计模式。分层架构将系统划分为多个独立的层次,每个层次负责特定的功能,层次之间通过清晰定义的接口进行交互。这种架构模式具有以下优点:

  • 模块化:系统被分解为多个模块,每个模块专注于特定功能,易于开发、测试和维护。
  • 可重用性:底层模块可以被上层模块复用,提高代码的复用率。
  • 可扩展性:新增功能只需在相应的层次进行扩展,不会影响其他层次。
  • 可移植性:通过抽象硬件接口,可以方便地将系统移植到不同的硬件平台。

基于分层架构,我们可以将简易数码管电子时钟系统划分为以下几个层次:

  1. **硬件抽象层 (HAL, Hardware Abstraction Layer)**:

    • 功能:封装底层硬件的驱动细节,向上层提供统一的硬件访问接口。
    • 模块
      • GPIO 驱动:控制数码管段选和位选引脚,以及按键输入引脚。
      • 定时器驱动:配置和管理单片机定时器,提供精确的定时中断。
      • 中断管理:处理外部中断(例如按键中断)和定时器中断。
    • 优势:使上层代码与具体的硬件细节解耦,提高代码的可移植性。
  2. **驱动层 (Driver Layer)**:

    • 功能:基于 HAL 层提供的接口,实现特定外围设备的驱动功能。
    • 模块
      • 数码管驱动:控制数码管的显示,包括数字显示、字符显示、动态扫描等。
      • 按键驱动:检测按键按下和释放,并进行按键去抖处理。
    • 优势:将硬件操作封装成易于使用的函数接口,方便应用层调用。
  3. **应用层 (Application Layer)**:

    • 功能:实现系统的核心业务逻辑,例如时间管理、显示更新、按键响应等。
    • 模块
      • 时间管理模块:负责时间的计数、更新和校准。
      • 显示管理模块:将时间数据转换为数码管显示格式,并控制数码管显示。
      • 输入管理模块:处理按键输入,实现时间调整功能。
    • 优势:专注于系统功能实现,无需关心底层硬件细节。
  4. **配置层 (Configuration Layer)**:

    • 功能:存储系统的配置参数,例如显示模式(12/24小时制)、时间格式等。
    • 模块
      • 配置参数定义:定义系统所需的配置参数。
      • 配置加载/保存:提供配置参数的加载和保存接口(对于简易时钟,配置可能直接在代码中定义,不需要复杂的加载保存机制)。
    • 优势:方便系统配置的修改和管理。

代码设计与实现 (C 语言)

接下来,我们将详细介绍每个层次的代码实现,并提供具体的 C 代码示例。为了代码的清晰性和可读性,我们将采用模块化编程,并将代码分为多个源文件和头文件。

1. 硬件抽象层 (HAL)

  • hal.h (头文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef HAL_H
#define HAL_H

#include <reg52.h> // STC89C52RC 头文件

// ** GPIO 定义 **
#define SEG_PORT P0 // 数码管段选端口
#define DIG_PORT P2 // 数码管位选端口

#define KEY_SET P1_0 // 设置按键
#define KEY_ADJUST_H P1_1 // 小时调整按键
#define KEY_ADJUST_M P1_2 // 分钟调整按键

// ** GPIO 初始化函数 **
void HAL_GPIO_Init(void);
void HAL_GPIO_SetPinDirection(unsigned char port, unsigned char pin, unsigned char direction); // 设置引脚方向 (INPUT/OUTPUT)
void HAL_GPIO_WritePin(unsigned char port, unsigned char pin, unsigned char value); // 写引脚 (HIGH/LOW)
unsigned char HAL_GPIO_ReadPin(unsigned char port, unsigned char pin); // 读引脚

// ** 定时器 初始化函数 **
void HAL_Timer0_Init(unsigned int timer_value); // 初始化 Timer0,设置定时器初值
void HAL_Timer0_Start(void); // 启动 Timer0
void HAL_Timer0_Stop(void); // 停止 Timer0

// ** 中断使能函数 **
void HAL_EnableInterrupts(void); // 全局中断使能
void HAL_DisableInterrupts(void); // 全局中断禁止
void HAL_EnableTimer0Interrupt(void); // 使能 Timer0 中断
void HAL_DisableTimer0Interrupt(void); // 禁止 Timer0 中断

#endif
  • hal.c (源文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include "hal.h"

// ** GPIO 初始化函数 **
void HAL_GPIO_Init(void) {
// 初始化数码管端口为输出
P0 = 0xFF; // 初始化为高电平,根据数码管类型可能需要调整
P2 = 0xFF;

// 初始化按键端口为输入,并使能上拉电阻 (STC89C52RC 默认使能上拉)
P1 = 0xFF;
}

void HAL_GPIO_SetPinDirection(unsigned char port, unsigned char pin, unsigned char direction) {
// STC89C52RC GPIO 端口方向控制较为简单,这里可以简化处理,实际应用中可能需要更精细的控制
// 这里仅作为示例,实际应用中需要根据具体 MCU 的 GPIO 寄存器进行配置
if (port == 0) {
if (direction == 0) { // INPUT
P0 &= ~(1 << pin); // 设置为输入 (实际 STC89C52RC 端口默认就是输入,这里仅作示例)
} else { // OUTPUT
P0 |= (1 << pin); // 设置为输出 (实际 STC89C52RC 端口默认就是输出,这里仅作示例)
}
} else if (port == 1) {
if (direction == 0) {
P1 &= ~(1 << pin);
} else {
P1 |= (1 << pin);
}
} else if (port == 2) {
if (direction == 0) {
P2 &= ~(1 << pin);
} else {
P2 |= (1 << pin);
}
}
// ... 可以根据需要添加其他端口的处理
}

void HAL_GPIO_WritePin(unsigned char port, unsigned char pin, unsigned char value) {
if (value) { // HIGH
if (port == 0) P0 |= (1 << pin);
else if (port == 1) P1 |= (1 << pin);
else if (port == 2) P2 |= (1 << pin);
// ...
} else { // LOW
if (port == 0) P0 &= ~(1 << pin);
else if (port == 1) P1 &= ~(1 << pin);
else if (port == 2) P2 &= ~(1 << pin);
// ...
}
}

unsigned char HAL_GPIO_ReadPin(unsigned char port, unsigned char pin) {
if (port == 0) return (P0 & (1 << pin)) ? 1 : 0;
else if (port == 1) return (P1 & (1 << pin)) ? 1 : 0;
else if (port == 2) return (P2 & (1 << pin)) ? 1 : 0;
// ...
return 0; // 默认返回 0
}

// ** 定时器 初始化函数 **
void HAL_Timer0_Init(unsigned int timer_value) {
TMOD &= 0xF0; // 设置 Timer0 工作模式为模式 1 (16 位定时器)
TMOD |= 0x01;
TH0 = (unsigned char)((65536 - timer_value) / 256); // 设置定时器初值
TL0 = (unsigned char)((65536 - timer_value) % 256);
TF0 = 0; // 清除 Timer0 溢出标志
}

void HAL_Timer0_Start(void) {
TR0 = 1; // 启动 Timer0
}

void HAL_Timer0_Stop(void) {
TR0 = 0; // 停止 Timer0
}

// ** 中断使能函数 **
void HAL_EnableInterrupts(void) {
EA = 1; // 全局中断使能
}

void HAL_DisableInterrupts(void) {
EA = 0; // 全局中断禁止
}

void HAL_EnableTimer0Interrupt(void) {
ET0 = 1; // 使能 Timer0 中断
}

void HAL_DisableTimer0Interrupt(void) {
ET0 = 0; // 禁止 Timer0 中断
}

2. 驱动层 (Driver)

  • display_driver.h (头文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#ifndef DISPLAY_DRIVER_H
#define DISPLAY_DRIVER_H

#include "hal.h"

// ** 数码管类型定义 ** (根据实际硬件选择共阴或共阳)
#define COMMON_CATHODE // 共阴数码管

// ** 数码管段码表 ** (共阴数码管)
#ifdef COMMON_CATHODE
const unsigned char SEG_CODE[] = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F, // 9
0x77, // A
0x7C, // B
0x39, // C
0x5E, // D
0x79, // E
0x71, // F
0x00 // 空白
};
#else // 共阳数码管 (需要根据实际硬件修改段码)
const unsigned char SEG_CODE[] = {
0xC0, // 0
0xF9, // 1
0xA4, // 2
0xB0, // 3
0x99, // 4
0x92, // 5
0x82, // 6
0xF8, // 7
0x80, // 8
0x90, // 9
0x88, // A
0x83, // B
0xC6, // C
0xA1, // D
0x86, // E
0x8E, // F
0xFF // 空白
};
#endif

// ** 数码管位选表 ** (根据实际硬件连接修改位选)
const unsigned char DIG_SELECT[] = {
0xFE, // 第 1 位
0xFD, // 第 2 位
0xFB, // 第 3 位
0xF7, // 第 4 位
0xEF, // 第 5 位
0xDF // 第 6 位
};

#define DISPLAY_DIGITS 6 // 数码管位数

// ** 显示驱动函数 **
void Display_Init(void);
void Display_ShowDigit(unsigned char digit, unsigned char pos); // 在指定位置显示数字
void Display_ShowChar(unsigned char chr, unsigned char pos); // 在指定位置显示字符 (可以扩展支持 A-F 等)
void Display_Clear(void); // 清空所有数码管显示
void Display_DynamicScan(unsigned char *digits); // 动态扫描显示多个数字

#endif
  • display_driver.c (源文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include "display_driver.h"
#include "delay.h" // 假设有 delay 延时函数

// ** 显示初始化函数 **
void Display_Init(void) {
HAL_GPIO_Init(); // 初始化 GPIO 端口
Display_Clear(); // 清空显示
}

// ** 显示单个数字 **
void Display_ShowDigit(unsigned char digit, unsigned char pos) {
if (digit > 9 || pos >= DISPLAY_DIGITS) return; // 参数检查

HAL_GPIO_WritePin(DIG_PORT, 0, 1); // 关闭所有位选,防止残影 (假设 DIG_PORT 的位选是按位控制的,实际可能需要循环设置)
HAL_GPIO_WritePin(DIG_PORT, 1, 1);
HAL_GPIO_WritePin(DIG_PORT, 2, 1);
HAL_GPIO_WritePin(DIG_PORT, 3, 1);
HAL_GPIO_WritePin(DIG_PORT, 4, 1);
HAL_GPIO_WritePin(DIG_PORT, 5, 1);


HAL_GPIO_WritePin(SEG_PORT, 0, (SEG_CODE[digit] & 0x01) ? 1 : 0); // 将段码数据写入段选端口 (假设 SEG_PORT 的位按位控制段)
HAL_GPIO_WritePin(SEG_PORT, 1, (SEG_CODE[digit] & 0x02) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 2, (SEG_CODE[digit] & 0x04) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 3, (SEG_CODE[digit] & 0x08) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 4, (SEG_CODE[digit] & 0x10) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 5, (SEG_CODE[digit] & 0x20) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 6, (SEG_CODE[digit] & 0x40) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 7, (SEG_CODE[digit] & 0x80) ? 1 : 0);


// 位选使能
if (pos < DISPLAY_DIGITS) {
// 假设 DIG_PORT 端口是整体控制位选的,需要根据实际硬件修改
DIG_PORT = DIG_SELECT[pos]; // 选中指定位
}

}

// ** 显示字符 (这里简单实现,可以扩展支持更多字符) **
void Display_ShowChar(unsigned char chr, unsigned char pos) {
if (pos >= DISPLAY_DIGITS) return;

unsigned char segCode = 0x00; // 默认显示空白
if (chr >= '0' && chr <= '9') {
segCode = SEG_CODE[chr - '0'];
} else if (chr >= 'A' && chr <= 'F') {
segCode = SEG_CODE[chr - 'A' + 10]; // A-F 的段码
} else if (chr == '-') {
segCode = 0x40; // 显示 '-' 符号 (共阴)
} else if (chr == ' ') {
segCode = SEG_CODE[16]; // 空白
}

HAL_GPIO_WritePin(DIG_PORT, 0, 1); // 关闭所有位选
HAL_GPIO_WritePin(DIG_PORT, 1, 1);
HAL_GPIO_WritePin(DIG_PORT, 2, 1);
HAL_GPIO_WritePin(DIG_PORT, 3, 1);
HAL_GPIO_WritePin(DIG_PORT, 4, 1);
HAL_GPIO_WritePin(DIG_PORT, 5, 1);

HAL_GPIO_WritePin(SEG_PORT, 0, (segCode & 0x01) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 1, (segCode & 0x02) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 2, (segCode & 0x04) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 3, (segCode & 0x08) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 4, (segCode & 0x10) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 5, (segCode & 0x20) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 6, (segCode & 0x40) ? 1 : 0);
HAL_GPIO_WritePin(SEG_PORT, 7, (segCode & 0x80) ? 1 : 0);


// 位选使能
if (pos < DISPLAY_DIGITS) {
DIG_PORT = DIG_SELECT[pos]; // 选中指定位
}
}

// ** 清空所有数码管显示 **
void Display_Clear(void) {
for (int i = 0; i < DISPLAY_DIGITS; i++) {
Display_ShowChar(' ', i); // 显示空白字符
}
}

// ** 动态扫描显示多个数字 **
void Display_DynamicScan(unsigned char *digits) {
for (int i = 0; i < DISPLAY_DIGITS; i++) {
Display_ShowDigit(digits[i], i);
Delay_ms(1); // 适当延时,控制扫描频率,避免闪烁 (需要实现 Delay_ms 函数)
}
}
  • key_driver.h (头文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef KEY_DRIVER_H
#define KEY_DRIVER_H

#include "hal.h"

// ** 按键类型定义 **
typedef enum {
KEY_NONE,
KEY_SET_PRESSED,
KEY_ADJUST_H_PRESSED,
KEY_ADJUST_M_PRESSED
} KeyState_t;

// ** 按键驱动函数 **
void Key_Init(void);
KeyState_t Key_Scan(void); // 按键扫描,返回按键状态

#endif
  • key_driver.c (源文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "key_driver.h"
#include "delay.h" // 假设有 delay 延时函数

#define KEY_DEBOUNCE_DELAY_MS 20 // 按键去抖延时 (毫秒)

// ** 按键初始化函数 **
void Key_Init(void) {
HAL_GPIO_Init(); // 初始化 GPIO 端口 (按键端口已经在 HAL_GPIO_Init 中初始化为输入)
}

// ** 按键扫描函数 (带去抖) **
KeyState_t Key_Scan(void) {
static KeyState_t lastKeyState = KEY_NONE; // 上一次按键状态
KeyState_t currentKeyState = KEY_NONE;

// 检测设置按键
if (!HAL_GPIO_ReadPin(1, 0)) { // 按键按下 (低电平有效,根据实际硬件修改)
Delay_ms(KEY_DEBOUNCE_DELAY_MS); // 去抖延时
if (!HAL_GPIO_ReadPin(1, 0)) { // 再次确认按键按下
currentKeyState = KEY_SET_PRESSED;
}
}
// 检测小时调整按键
else if (!HAL_GPIO_ReadPin(1, 1)) {
Delay_ms(KEY_DEBOUNCE_DELAY_MS);
if (!HAL_GPIO_ReadPin(1, 1)) {
currentKeyState = KEY_ADJUST_H_PRESSED;
}
}
// 检测分钟调整按键
else if (!HAL_GPIO_ReadPin(1, 2)) {
Delay_ms(KEY_DEBOUNCE_DELAY_MS);
if (!HAL_GPIO_ReadPin(1, 2)) {
currentKeyState = KEY_ADJUST_M_PRESSED;
}
}

// 如果当前按键状态与上次不同,则更新上次状态
if (currentKeyState != lastKeyState) {
lastKeyState = currentKeyState;
return currentKeyState;
} else {
return KEY_NONE; // 没有按键按下或状态未改变
}
}

3. 应用层 (Application)

  • time_manager.h (头文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef TIME_MANAGER_H
#define TIME_MANAGER_H

// ** 时间结构体 **
typedef struct {
unsigned char hours;
unsigned char minutes;
unsigned char seconds;
} Time_t;

// ** 时间管理函数 **
void Time_Init(Time_t *time); // 初始化时间
void Time_IncrementSecond(Time_t *time); // 秒计数加 1
void Time_SetTime(Time_t *time, unsigned char hours, unsigned char minutes, unsigned char seconds); // 设置时间
void Time_GetTime(Time_t *time); // 获取当前时间 (这里实际上是直接访问 time 结构体,可以根据需要调整)

#endif
  • time_manager.c (源文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "time_manager.h"

// ** 初始化时间 **
void Time_Init(Time_t *time) {
time->hours = 0;
time->minutes = 0;
time->seconds = 0;
}

// ** 秒计数加 1 **
void Time_IncrementSecond(Time_t *time) {
time->seconds++;
if (time->seconds >= 60) {
time->seconds = 0;
time->minutes++;
if (time->minutes >= 60) {
time->minutes = 0;
time->hours++;
if (time->hours >= 24) {
time->hours = 0; // 24 小时制
}
}
}
}

// ** 设置时间 **
void Time_SetTime(Time_t *time, unsigned char hours, unsigned char minutes, unsigned char seconds) {
if (hours < 24 && minutes < 60 && seconds < 60) {
time->hours = hours;
time->minutes = minutes;
time->seconds = seconds;
}
// 可以添加参数错误处理,例如返回错误码
}

// ** 获取当前时间 ** (直接访问结构体,函数可以省略,这里仅作为示例)
void Time_GetTime(Time_t *time) {
// 实际应用中,如果时间是从其他地方获取的,可以在这里进行获取操作
// 例如从 RTC 模块读取时间
}
  • display_manager.h (头文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H

#include "time_manager.h"
#include "display_driver.h"

// ** 显示管理函数 **
void DisplayManager_Init(void);
void DisplayManager_UpdateTimeDisplay(const Time_t *time); // 更新时间显示
void DisplayManager_ShowSettingMode(void); // 显示设置模式提示 (例如闪烁)
void DisplayManager_ShowNormalMode(void); // 显示正常模式

#endif
  • display_manager.c (源文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include "display_manager.h"
#include "delay.h" // 假设有 delay 延时函数

#define DISPLAY_UPDATE_INTERVAL_MS 1000 // 显示更新间隔 (毫秒)
#define SETTING_MODE_BLINK_INTERVAL_MS 500 // 设置模式闪烁间隔 (毫秒)

// ** 显示初始化 **
void DisplayManager_Init(void) {
Display_Init(); // 初始化显示驱动
}

// ** 更新时间显示 **
void DisplayManager_UpdateTimeDisplay(const Time_t *time) {
unsigned char digits[DISPLAY_DIGITS];

// 小时
digits[0] = time->hours / 10;
digits[1] = time->hours % 10;
// 分钟
digits[2] = time->minutes / 10;
digits[3] = time->minutes % 10;
// 秒
digits[4] = time->seconds / 10;
digits[5] = time->seconds % 10;

Display_DynamicScan(digits); // 动态扫描显示
}

// ** 显示设置模式提示 (闪烁小时和分钟) **
void DisplayManager_ShowSettingMode(void) {
static unsigned char blinkState = 0; // 闪烁状态

if (blinkState == 0) {
Display_ShowChar(' ', 0); // 小时位闪烁
Display_ShowChar(' ', 1);
Display_ShowChar(' ', 2); // 分钟位闪烁
Display_ShowChar(' ', 3);
} else {
// 恢复正常显示 (假设在主循环中会更新时间,这里可以先显示空白,实际应用中可能需要更复杂的处理)
// 这里简化处理,假设主循环会重新调用 UpdateTimeDisplay
//DisplayManager_UpdateTimeDisplay(currentTime); // 恢复显示
Display_ShowChar(' ', 0);
Display_ShowChar(' ', 1);
Display_ShowChar(' ', 2);
Display_ShowChar(' ', 3);
Display_ShowChar(' ', 4);
Display_ShowChar(' ', 5);
}

blinkState ^= 1; // 切换闪烁状态
Delay_ms(SETTING_MODE_BLINK_INTERVAL_MS);
}

// ** 显示正常模式 ** (这里可以根据需要添加正常模式下的显示效果,例如常亮显示)
void DisplayManager_ShowNormalMode(void) {
// 在正常模式下,显示更新应该在主循环中定期调用 UpdateTimeDisplay
}
  • input_manager.h (头文件)
1
2
3
4
5
6
7
8
9
10
11
#ifndef INPUT_MANAGER_H
#define INPUT_MANAGER_H

#include "key_driver.h"
#include "time_manager.h"

// ** 输入管理函数 **
void InputManager_Init(void);
void InputManager_ProcessInput(Time_t *time, unsigned char *settingMode); // 处理按键输入

#endif
  • input_manager.c (源文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "input_manager.h"
#include "delay.h" // 假设有 delay 延时函数

#define SETTING_TIMEOUT_MS 10000 // 设置模式超时时间 (毫秒)

// ** 输入初始化 **
void InputManager_Init(void) {
Key_Init(); // 初始化按键驱动
}

// ** 处理按键输入 **
void InputManager_ProcessInput(Time_t *time, unsigned char *settingMode) {
static unsigned long settingStartTime = 0; // 设置模式开始时间
KeyState_t keyState = Key_Scan();

if (keyState == KEY_SET_PRESSED) {
*settingMode = !(*settingMode); // 切换设置模式
if (*settingMode) {
settingStartTime = GetTickCount(); // 记录设置模式开始时间 (需要实现 GetTickCount 函数或使用定时器)
}
}

if (*settingMode) { // 设置模式下
if (keyState == KEY_ADJUST_H_PRESSED) {
time->hours++;
if (time->hours >= 24) time->hours = 0;
} else if (keyState == KEY_ADJUST_M_PRESSED) {
time->minutes++;
if (time->minutes >= 60) time->minutes = 0;
}

// 设置模式超时检测
if ((GetTickCount() - settingStartTime) > SETTING_TIMEOUT_MS) {
*settingMode = 0; // 超时退出设置模式
}
}
}

// ** 获取系统运行时间 (简单示例,实际应用中需要更精确的定时器实现) **
unsigned long GetTickCount(void) {
// 这里使用简单的软件计数延时作为示例,实际应用中应使用定时器中断来精确计时
static unsigned long tickCount = 0;
tickCount++;
return tickCount;
}
  • config.h (头文件)
1
2
3
4
5
6
7
8
#ifndef CONFIG_H
#define CONFIG_H

// ** 系统配置参数 **
#define SYSTEM_CLOCK_FREQUENCY 11059200UL // 系统时钟频率 (11.0592MHz)
#define TIMER0_RELOAD_VALUE 50000 // Timer0 定时器重载值,根据需要调整,计算方法见下文

#endif
  • delay.h (头文件)
1
2
3
4
5
6
#ifndef DELAY_H
#define DELAY_H

void Delay_ms(unsigned int ms); // 毫秒级延时函数

#endif
  • delay.c (源文件)
1
2
3
4
5
6
7
8
9
10
#include "delay.h"
#include "config.h"

// ** 毫秒级延时函数 **
void Delay_ms(unsigned int ms) {
unsigned int i, j;
for (i = 0; i < ms; i++) {
for (j = 0; j < 110; j++); // 粗略延时,需要根据实际时钟频率和指令周期调整
}
}

4. 主程序 (main.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <reg52.h>
#include "hal.h"
#include "display_driver.h"
#include "key_driver.h"
#include "time_manager.h"
#include "display_manager.h"
#include "input_manager.h"
#include "config.h"
#include "delay.h"

// ** 全局变量 **
Time_t currentTime; // 当前时间
unsigned char settingMode = 0; // 设置模式标志 (0: 正常模式, 1: 设置模式)

// ** 定时器 0 中断服务函数 **
void Timer0_ISR() interrupt 1 {
TH0 = (unsigned char)((65536 - TIMER0_RELOAD_VALUE) / 256); // 重新加载定时器初值
TL0 = (unsigned char)((65536 - TIMER0_RELOAD_VALUE) % 256);

Time_IncrementSecond(&currentTime); // 秒计数加 1
}

// ** 主函数 **
void main() {
// ** 初始化 **
HAL_DisableInterrupts(); // 禁用全局中断,初始化期间避免中断干扰

HAL_GPIO_Init(); // 初始化 GPIO
DisplayManager_Init(); // 初始化显示
InputManager_Init(); // 初始化输入
Time_Init(&currentTime); // 初始化时间

// 配置 Timer0 定时器,产生 1 秒定时中断
// 计算 Timer0 重载值,假设系统时钟频率为 11.0592MHz,需要定时 1 秒 (1000ms)
// Timer0 工作在模式 1 (16 位定时器),计数频率为系统时钟频率的 1/12
// 定时器计数次数 = 定时时间 (秒) * 系统时钟频率 / 12
// 定时器重载值 = 65536 - 定时器计数次数
// 例如:定时 1ms: 65536 - (0.001 * 11059200 / 12) = 64611.2,取整 64611,换算成十六进制为 0xFC63
// 定时 1秒: 65536 - (1 * 11059200 / 12) = 5636.0,取整 5636,换算成十六进制为 0x1604

HAL_Timer0_Init(TIMER0_RELOAD_VALUE); // 初始化 Timer0,重载值在 config.h 中定义
HAL_EnableTimer0Interrupt(); // 使能 Timer0 中断
HAL_EnableInterrupts(); // 使能全局中断
HAL_Timer0_Start(); // 启动 Timer0

DisplayManager_Clear(); // 清空显示
DisplayManager_UpdateTimeDisplay(&currentTime); // 初始显示时间

// ** 主循环 **
while (1) {
InputManager_ProcessInput(&currentTime, &settingMode); // 处理按键输入

if (settingMode) {
DisplayManager_ShowSettingMode(); // 设置模式显示
} else {
DisplayManager_ShowNormalMode(); // 正常模式显示
DisplayManager_UpdateTimeDisplay(&currentTime); // 更新时间显示
}

Delay_ms(50); // 适当延时,降低 CPU 占用率
}
}

5. 计算 Timer0 重载值 (config.h 中 TIMER0_RELOAD_VALUE)

根据 STC89C52RC 的定时器特性,Timer0 在模式 1 下,计数器每 12 个时钟周期计数一次。如果系统时钟频率为 11.0592MHz,则定时器计数频率为 11059200 / 12 = 921600 Hz。

要实现 1 秒定时中断,需要定时器计数 921600 次。但是 Timer0 是 16 位定时器,最大计数值为 65536。我们需要根据实际情况选择合适的定时中断周期,例如 10ms 或 50ms,然后累加计数达到 1 秒。

为了简化,我们假设直接使用 Timer0 定时 1 秒,计算重载值:

定时器计数次数 = 1 秒 * 921600 Hz = 921600
重载值 = 65536 - 921600 (这个值显然是负数,说明 1 秒定时对于 11.0592MHz 时钟频率的 Timer0 模式 1 来说,计数次数超过了 16 位定时器的范围)

我们需要调整定时时间,例如使用 50ms 定时中断,然后累加 20 次达到 1 秒:

定时器计数次数 (50ms) = 0.05 秒 * 921600 Hz = 46080
重载值 (50ms) = 65536 - 46080 = 19456 (十进制) = 0x4C00 (十六进制)

因此,TIMER0_RELOAD_VALUE 可以设置为 19456 (十进制) 或 0x4C00 (十六进制)。

main.cTimer0_ISR 中,我们需要添加一个计数器,累加中断次数,当计数达到 20 时,才执行 Time_IncrementSecond 函数。

修改 Timer0_ISR 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ** 定时器 0 中断服务函数 **
void Timer0_ISR() interrupt 1 {
static unsigned char timerCount = 0; // 定时器计数器

TH0 = (unsigned char)((65536 - TIMER0_RELOAD_VALUE) / 256); // 重新加载定时器初值
TL0 = (unsigned char)((65536 - TIMER0_RELOAD_VALUE) % 256);

timerCount++;
if (timerCount >= 20) { // 累加 20 次 50ms 中断,达到 1 秒
timerCount = 0;
Time_IncrementSecond(&currentTime); // 秒计数加 1
}
}

6. 编译和下载

将以上代码保存为相应的 .h.c 文件,使用 Keil C51 或其他 C51 编译器编译,生成 .hex 文件,然后使用 STC-ISP 等工具将 .hex 文件下载到 STC89C52RC 单片机中。

测试验证

完成代码编写和下载后,需要进行全面的测试验证,确保系统的功能和性能符合需求。

  1. 功能测试

    • 时间显示测试:观察数码管是否能够正确显示时间,时、分、秒是否按预期递增。
    • 时间精度测试:将电子时钟与标准时间源(例如手机或网络时间)对比,观察长时间运行后时间的偏差是否在允许范围内。
    • 时间调整测试:测试设置、小时调整、分钟调整按键是否能够正常工作,时间调整是否准确。
    • 断电保持测试:断开电源,再重新上电,观察时间是否能够保持(由于没有 RTC,这里只能测试软件模拟的保持,实际效果可能不佳)。
  2. 性能测试

    • 功耗测试:使用电流表测量系统运行时的功耗,评估是否符合低功耗设计目标。
    • 响应速度测试:测试按键操作的响应速度,观察显示更新是否流畅。
    • 稳定性测试:让系统长时间运行(例如 24 小时以上),观察是否出现死机、时间错乱等异常情况。
    • 可靠性测试:模拟常见的干扰环境(例如电磁干扰、电压波动),测试系统是否能够稳定运行。

维护升级

虽然这是一个简易项目,但我们仍然需要考虑维护和升级方面。

  1. 代码维护

    • 注释完善:在代码中添加清晰的注释,方便后续维护和理解。
    • 代码规范:遵循良好的代码编写规范,提高代码的可读性和可维护性。
    • 版本控制:使用 Git 等版本控制工具管理代码,方便代码的版本管理和协作开发。
  2. 功能升级

    • 添加闹钟功能:可以扩展代码,添加闹钟设置和响铃功能。
    • 添加秒表功能:可以添加秒表功能,方便计时。
    • 使用 RTC 模块:为了提高时间精度和断电保持能力,可以外接 RTC 模块,例如 DS3231,并修改代码驱动 RTC 模块。
    • 显示更多信息:可以扩展数码管显示内容,例如显示日期、星期等信息。
    • 增加无线通信功能:可以添加蓝牙或 Wi-Fi 模块,实现远程时间同步或控制功能。

总结

通过以上详细的分析和代码实现,我们完成了一个基于 STC89C52RC 单片机的简易数码管电子时钟项目。这个项目采用了分层架构设计,将系统划分为硬件抽象层、驱动层、应用层和配置层,提高了代码的模块化、可重用性、可扩展性和可移植性。代码实现方面,我们提供了详细的 C 代码示例,包括 HAL 层、显示驱动、按键驱动、时间管理、显示管理和输入管理等模块。最后,我们还介绍了测试验证和维护升级方面的内容,确保项目的完整性和实用性。

这个项目虽然简单,但涵盖了嵌入式系统开发的基本流程和关键技术,希望能够帮助你深入理解嵌入式系统开发,并为你后续更复杂的项目打下坚实的基础。请记住,实践是检验真理的唯一标准,只有不断地动手实践,才能真正掌握嵌入式系统开发的精髓。

欢迎关注我的其它发布渠道