找回密码
 立即注册
查看: 113|回复: 9

数码管时钟,已经修改了昨天走时不精准的问题 | 走时很精准

[复制链接]
  • 打卡等级:偶尔看看I
  • 打卡总天数:11
  • 最近打卡:2026-04-10 01:15:45
已绑定手机

12

主题

8

回帖

98

积分

注册会员

积分
98
发表于 2026-4-8 08:28:01 | 显示全部楼层 |阅读模式
昨天帖子的数码管时钟,发现走时不准,误差很大,现在对定时器进行了修改,走时很精准,下载时选择12MHz频率。喜欢的朋友赶快复制拿走!!!


/*================================================================
  功能:STC32四位数码管时钟(带冒号2秒闪烁和简化按键设置)
  硬件连接:
    - MCU: STC32G12K128
    - 数码管: 4401AS(四位共阴时钟数码管)
    - 段码: P2.0-P2.7 -> 数码管a,b,c,d,e,f,g,DP
    - 位码: P3.0-P3.3 -> 数码管位选1-4
    - 按键: P0.0-P0.3 -> 功能/加/减/确认键
  注意:冒号通过第2位数码管的DP段以2秒周期闪烁
================================================================*/

#include "STC32G.h"        // STC32头文件
#include "intrins.h"

/*============ 引脚定义 ============*/
#define SEG_PORT P2        // 段码端口(P2.0-P2.7 -> a,b,c,d,e,f,g,DP)
#define BIT_PORT P3        // 位码端口(P3.0-P3.3 -> 位选1-4)

// 按键引脚定义
sbit KEY_MODE = P0^0;      // 功能/模式键
sbit KEY_ADD  = P0^1;      // 加键
sbit KEY_SUB  = P0^2;      // 减键
sbit KEY_OK   = P0^3;      // 确认键

/*============ 模式定义 ============*/
#define MODE_NORMAL      0     // 正常显示模式
#define MODE_SET_HOUR    1     // 设置小时模式
#define MODE_SET_MINUTE  2     // 设置分钟模式

/*============ 全局变量 ============*/
// 时间结构体
typedef struct {
    unsigned char hour;     // 小时 (0-23)
    unsigned char minute;   // 分钟 (0-59)
    unsigned char second;   // 秒钟 (0-59)
} TIME_TYPE;

TIME_TYPE time = {12, 0, 0};  // 初始时间 12:00:00

// 显示相关变量
unsigned char disp_buf[4] = {0};       // 显示缓冲区
bit colon_flag = 0;                    // 冒号显示标志
bit sec_led_on = 0;                    // 秒LED点亮标志
unsigned char display_mode = MODE_NORMAL;  // 当前显示模式
bit blink_flag = 0;                    // 设置闪烁标志
unsigned int t1_counter = 0;           // 定时器1计数器
unsigned int ms_counter = 0;           // 毫秒计数器

// 按键相关
unsigned char key_state = 0;           // 按键状态
unsigned int key_press_time = 0;       // 按键按下时间
bit key_pressed = 0;                   // 按键按下标志
bit key_enable = 1;                    // 按键使能标志
unsigned int key_repeat_delay = 0;     // 按键重复延时

/*============ 数码管编码表 ============*/
// 共阴数码管段码表 (0-9)
// 段码顺序:a b c d e f g DP (对应P2.0-P2.7)
const unsigned char code SEG_CODE[] = {
    // 0     1     2     3     4     5     6     7     8     9
    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F,
    // 不显示
    0x00
};

// 位选表
// 共阴数码管,位选低电平有效
// 假设P3.0-3.3分别控制第1-4位数码管
const unsigned char code BIT_CODE[] = {
    0xFE,  // 11111110 - 第1位数码管 (P3.0=0)
    0xFD,  // 11111101 - 第2位数码管 (P3.1=0)
    0xFB,  // 11111011 - 第3位数码管 (P3.2=0)
    0xF7   // 11110111 - 第4位数码管 (P3.3=0)
};

/*============ 函数声明 ============*/
void System_Init(void);
void GPIO_Init(void);
void Timer0_Init(void);
void Timer1_Init(void);
void Update_Display_Buffer(void);
void Key_Scan(void);
void Key_Process(unsigned char key_val);
void Delay_ms(unsigned int ms);

/*============ 主函数 ============*/
void main(void)
{
    // 系统初始化
    System_Init();
   
    // 主循环
    while(1)
    {
        Key_Scan();               // 按键扫描
        Delay_ms(10);             // 增加主循环延时
    }
}

/*============ 系统初始化 ============*/
void System_Init(void)
{
    GPIO_Init();      // GPIO初始化
    Timer0_Init();    // 定时器0初始化
    Timer1_Init();    // 定时器1初始化
   
    // 开全局中断
    EA = 1;
   
    // 初始化显示缓冲区
    Update_Display_Buffer();
}

/*============ GPIO初始化 ============*/
void GPIO_Init(void)
{
    // P2设置为准双向口(段码输出)
    P2M0 = 0x00;
    P2M1 = 0x00;
    P2 = 0x00;           // 初始输出低电平
   
    // P3设置为准双向口(位码输出)
    P3M0 = 0x00;
    P3M1 = 0x00;
    P3 = 0xFF;           // 初始输出高电平(关闭所有位)
   
    // P0设置为高阻输入(按键输入),启用内部上拉
    P0M0 = 0x00;
    P0M1 = 0x00;
    P0 = 0xFF;           // 初始化为高电平
}

/*============ 定时器0初始化 ============*/
// 用于数码管动态扫描,1ms定时
void Timer0_Init(void)
{
    // 设置定时器0为模式0(16位自动重装载)
    AUXR |= 0x80;        // 定时器0为1T模式
    TMOD &= 0xF0;        // 清除定时器0模式位
   
    // 计算1ms定时初值(假设系统时钟为12MHz)
    // 1T模式下,1个机器周期 = 1/12MHz ≈ 0.08333μs
    // 1ms = 1000μs,需要计数次数 = 1000 / 0.08333 ≈ 12000
    TH0 = (65536 - 12000) / 256;
    TL0 = (65536 - 12000) % 256;
   
    // 使能定时器0中断
    ET0 = 1;
    // 启动定时器0
    TR0 = 1;
}

/*============ 定时器1初始化 ============*/
// 用于时钟计时,1ms定时
void Timer1_Init(void)
{
    // 设置定时器1为模式0(16位自动重装载)
    AUXR |= 0x40;        // 定时器1为1T模式
    TMOD &= 0x0F;        // 清除定时器1模式位
   
    // 计算1ms定时初值(假设系统时钟为12MHz)
    // 1T模式下,1个机器周期 = 1/12MHz ≈ 0.08333μs
    // 1ms = 1000μs,需要计数次数 = 1000 / 0.08333 ≈ 12000
    TH1 = (65536 - 12000) / 256;
    TL1 = (65536 - 12000) % 256;
   
    // 使能定时器1中断
    ET1 = 1;
    // 启动定时器1
    TR1 = 1;
}

/*============ 定时器0中断服务函数 ============*/
// 处理数码管动态扫描
void Timer0_ISR(void) interrupt 1
{
    static unsigned char scan_index = 0;  // 扫描索引
    unsigned char seg_data;
   
    // 重装定时器初值
    TH0 = (65536 - 12000) / 256;
    TL0 = (65536 - 12000) % 256;
   
    // 关闭当前显示位(消隐)
    BIT_PORT = 0xFF;
   
    // 获取当前位的段码
    seg_data = SEG_CODE[disp_buf[scan_index]];
   
    // 处理第2位的冒号(DP段)
    if(scan_index == 1)  // 第2位数码管
    {
        if(colon_flag)  // 需要显示冒号
        {
            seg_data |= 0x80;  // 点亮DP段
        }
    }
   
    // 输出段码
    SEG_PORT = seg_data;
   
    // 输出位码
    BIT_PORT = BIT_CODE[scan_index];
   
    // 更新扫描索引
    scan_index++;
    if(scan_index >= 4)
    {
        scan_index = 0;
    }
}

/*============ 定时器1中断服务函数 ============*/
// 处理时钟计时、冒号闪烁和设置闪烁
void Timer1_ISR(void) interrupt 3
{
    static unsigned int colon_cnt = 0;    // 冒号计数器
    static unsigned int blink_cnt = 0;    // 闪烁计数器
   
    // 重装定时器初值
    TH1 = (65536 - 12000) / 256;
    TL1 = (65536 - 12000) % 256;
   
    // 定时器1计数器 - 用于1秒计时
    t1_counter++;
   
    // 毫秒计数器 - 用于按键延时
    ms_counter++;
   
    // 1秒定时(1000 * 1ms = 1秒) - 修正为1000
    if(t1_counter >= 1000)
    {
        t1_counter = 0;
        
        // 仅在正常显示模式下更新时间
        if(display_mode == MODE_NORMAL)
        {
            time.second++;
            if(time.second >= 60)
            {
                time.second = 0;
                time.minute++;
                if(time.minute >= 60)
                {
                    time.minute = 0;
                    time.hour++;
                    if(time.hour >= 24)
                    {
                        time.hour = 0;
                    }
                }
            }
        }
        
        // 更新显示缓冲区
        Update_Display_Buffer();
    }
   
    // 冒号闪烁控制(2秒周期,亮1秒灭1秒)
    // 1000ms = 1秒,所以计数到1000切换一次
    colon_cnt++;
    if(colon_cnt >= 1000)  // 1000 * 1ms = 1秒
    {
        colon_cnt = 0;
        sec_led_on = ~sec_led_on;  // 切换冒号状态
        
        // 在正常显示模式下更新冒号
        if(display_mode == MODE_NORMAL)
        {
            colon_flag = sec_led_on;
        }
    }
   
    // 设置时的数字闪烁控制(2Hz,亮0.25秒灭0.25秒)
    // 250ms = 0.25秒
    blink_cnt++;
    if(blink_cnt >= 250)  // 250 * 1ms = 250ms
    {
        blink_cnt = 0;
        blink_flag = ~blink_flag;  // 切换闪烁标志
        
        // 更新显示缓冲区(当闪烁状态改变时)
        Update_Display_Buffer();
    }
   
    // 按键延时控制
    if(!key_enable)
    {
        key_repeat_delay++;
        if(key_repeat_delay >= 300)  // 300ms按键延时
        {
            key_repeat_delay = 0;
            key_enable = 1;  // 重新使能按键
        }
    }
}

/*============ 更新显示缓冲区 ============*/
void Update_Display_Buffer(void)
{
    // 正常情况下显示小时和分钟
    disp_buf[0] = time.hour / 10;    // 小时十位
    disp_buf[1] = time.hour % 10;    // 小时个位
    disp_buf[2] = time.minute / 10;  // 分钟十位
    disp_buf[3] = time.minute % 10;  // 分钟个位
   
    // 初始化冒号标志
    colon_flag = 0;
   
    // 根据当前模式处理显示
    switch(display_mode)
    {
        case MODE_NORMAL:  // 正常显示模式
            // 正常显示冒号
            colon_flag = sec_led_on;
            
            // 处理小时十位为0的情况
            if(disp_buf[0] == 0)
            {
                disp_buf[0] = 10;  // 不显示
            }
            break;
            
        case MODE_SET_HOUR:  // 设置小时模式
            // 设置模式下冒号常亮
            colon_flag = 1;
            
            if(blink_flag)  // 闪烁时清除显示
            {
                // 整个小时一起闪烁
                disp_buf[0] = 10;  // 小时十位不显示
                disp_buf[1] = 10;  // 小时个位不显示
            }
            break;
            
        case MODE_SET_MINUTE:  // 设置分钟模式
            // 设置模式下冒号常亮
            colon_flag = 1;
            
            if(blink_flag)  // 闪烁时清除显示
            {
                // 整个分钟一起闪烁
                disp_buf[2] = 10;  // 分钟十位不显示
                disp_buf[3] = 10;  // 分钟个位不显示
            }
            break;
    }
}

/*============ 按键扫描函数 ============*/
void Key_Scan(void)
{
    unsigned char key_current = 0;
   
    // 读取按键状态
    if(KEY_MODE == 0) key_current |= 0x01;
    if(KEY_ADD == 0)  key_current |= 0x02;
    if(KEY_SUB == 0)  key_current |= 0x04;
    if(KEY_OK == 0)   key_current |= 0x08;
   
    // 无按键按下
    if(key_current == 0)
    {
        key_state = 0;
        key_press_time = 0;
        key_pressed = 0;
        return;
    }
   
    // 有按键按下
    switch(key_state)
    {
        case 0:  // 第一次检测到按键
            key_state = 1;
            key_press_time = 0;
            break;
            
        case 1:  // 按键消抖
            key_press_time++;
            if(key_press_time > 5)  // 50ms消抖
            {
                key_state = 2;
                key_press_time = 0;
            }
            break;
            
        case 2:  // 按键按下确认
            if(key_enable)  // 按键使能
            {
                Key_Process(key_current);
                key_enable = 0;  // 禁用按键
                key_state = 3;   // 等待释放
            }
            break;
            
        case 3:  // 等待释放
            // 等待按键释放
            break;
    }
}

/*============ 按键处理函数 ============*/
void Key_Process(unsigned char key_val)
{
    switch(key_val)
    {
        case 0x01:  // 功能/模式键
            if(display_mode == MODE_NORMAL)
            {
                display_mode = MODE_SET_HOUR;  // 进入设置小时模式
            }
            else if(display_mode == MODE_SET_HOUR)
            {
                display_mode = MODE_SET_MINUTE;  // 进入设置分钟模式
            }
            else if(display_mode == MODE_SET_MINUTE)
            {
                display_mode = MODE_NORMAL;  // 返回正常显示模式
            }
            Update_Display_Buffer();  // 更新显示
            break;
            
        case 0x02:  // 加键
            switch(display_mode)
            {
                case MODE_SET_HOUR:  // 设置小时
                    time.hour++;
                    if(time.hour >= 24)
                        time.hour = 0;
                    break;
                    
                case MODE_SET_MINUTE:  // 设置分钟
                    time.minute++;
                    if(time.minute >= 60)
                        time.minute = 0;
                    break;
                    
                default:
                    break;
            }
            Update_Display_Buffer();  // 更新显示
            break;
            
        case 0x04:  // 减键
            switch(display_mode)
            {
                case MODE_SET_HOUR:  // 设置小时
                    if(time.hour > 0)
                        time.hour--;
                    else
                        time.hour = 23;
                    break;
                    
                case MODE_SET_MINUTE:  // 设置分钟
                    if(time.minute > 0)
                        time.minute--;
                    else
                        time.minute = 59;
                    break;
                    
                default:
                    break;
            }
            Update_Display_Buffer();  // 更新显示
            break;
            
        case 0x08:  // 确认键
            // 确认键功能简化
            if(display_mode == MODE_SET_HOUR)
            {
                display_mode = MODE_SET_MINUTE;  // 从设置小时跳到设置分钟
            }
            else if(display_mode == MODE_SET_MINUTE)
            {
                display_mode = MODE_NORMAL;  // 返回正常显示
            }
            Update_Display_Buffer();  // 更新显示
            break;
    }
}

/*============ 延时函数 ============*/
void Delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 114; j++);
}


2.jpg
1.jpg
回复

使用道具 举报 送花

  • 打卡等级:以坛为家I
  • 打卡总天数:243
  • 最近打卡:2026-05-09 00:06:43
已绑定手机

22

主题

422

回帖

1906

积分

金牌会员

DIY玩家

积分
1906
发表于 2026-4-8 08:55:14 | 显示全部楼层
定时相关配置
需要考虑工作频率
真正的学徒往往怀着大师的心
回复

使用道具 举报 送花

  • 打卡等级:常住居民III
  • 打卡总天数:110
  • 最近打卡:2026-05-08 14:40:33
已绑定手机

4

主题

277

回帖

6199

积分

论坛元老

积分
6199
发表于 2026-4-8 09:30:04 | 显示全部楼层
定时器模式0是16位自动重装,就没必要在中断里手动重装了。
回复

使用道具 举报 送花

  • 打卡等级:以坛为家I
  • 打卡总天数:243
  • 最近打卡:2026-05-09 00:06:43
已绑定手机

22

主题

422

回帖

1906

积分

金牌会员

DIY玩家

积分
1906
发表于 2026-4-8 09:53:56 | 显示全部楼层
lcws*** 发表于 2026-4-8 09:30
定时器模式0是16位自动重装,就没必要在中断里手动重装了。

猜想自动重装更快:
自动重装靠底层电路硬件
手动重装靠CPU执行软件程序

点评

最大的区别还是定时精度,手动重装依赖中断服务程序,会占用时钟周期,而且有被其他更高优先级中断截胡的风险,造成不确定性的误差增大。  详情 回复 发表于 2026-4-8 16:36
真正的学徒往往怀着大师的心
回复

使用道具 举报 送花

  • 打卡等级:以坛为家III
  • 打卡总天数:738
  • 最近打卡:2026-05-09 00:44:57
已绑定手机

49

主题

2668

回帖

2778

积分

荣誉版主

积分
2778
发表于 2026-4-8 16:36:52 | 显示全部楼层
狂热*** 发表于 2026-4-8 09:53
猜想自动重装更快:
自动重装靠底层电路硬件
手动重装靠CPU执行软件程序 ...

最大的区别还是定时精度,手动重装依赖中断服务程序,会占用时钟周期,而且有被其他更高优先级中断截胡的风险,造成不确定性的误差增大。
~~~
回复

使用道具 举报 送花

  • 打卡等级:常住居民I
  • 打卡总天数:67
  • 最近打卡:2026-05-08 06:02:18
已绑定手机

3

主题

115

回帖

561

积分

高级会员

积分
561
发表于 2026-4-8 20:01:31 | 显示全部楼层
用MCU定时器做时钟的时基要选高精度晶振,定时器中断最高优先级,周期尽可能短些,不要超过100us。选择自动重装模式,还要加入修正系数x。 if(t1_counter >= (10000+x))。采取这些措施后才能将日误差控制在1秒内。另外秒点闪烁周期1秒才比较合适。
回复

使用道具 举报 送花

  • 打卡等级:偶尔看看I
  • 打卡总天数:11
  • 最近打卡:2026-04-10 01:15:45
已绑定手机

12

主题

8

回帖

98

积分

注册会员

积分
98
发表于 2026-4-9 21:48:14 | 显示全部楼层
wul*** 发表于 2026-4-8 20:01
用MCU定时器做时钟的时基要选高精度晶振,定时器中断最高优先级,周期尽可能短些,不要超过100us。选择自动 ...

谢谢指导
回复

使用道具 举报 送花

  • 打卡等级:偶尔看看I
  • 打卡总天数:11
  • 最近打卡:2026-04-10 01:15:45
已绑定手机

12

主题

8

回帖

98

积分

注册会员

积分
98
发表于 2026-4-9 21:48:33 | 显示全部楼层
晓*** 发表于 2026-4-8 16:36
最大的区别还是定时精度,手动重装依赖中断服务程序,会占用时钟周期,而且有被其他更高优先级中断截胡的 ...

谢谢
回复

使用道具 举报 送花

  • 打卡等级:偶尔看看I
  • 打卡总天数:11
  • 最近打卡:2026-04-10 01:15:45
已绑定手机

12

主题

8

回帖

98

积分

注册会员

积分
98
发表于 2026-4-9 22:01:16 | 显示全部楼层
狂热*** 发表于 2026-4-8 09:53
猜想自动重装更快:
自动重装靠底层电路硬件
手动重装靠CPU执行软件程序 ...

谢谢指导,刚学习单片机
回复

使用道具 举报 送花

  • 打卡等级:偶尔看看I
  • 打卡总天数:11
  • 最近打卡:2026-04-10 01:15:45
已绑定手机

12

主题

8

回帖

98

积分

注册会员

积分
98
发表于 2026-4-9 22:03:05 | 显示全部楼层
wul*** 发表于 2026-4-8 20:01
用MCU定时器做时钟的时基要选高精度晶振,定时器中断最高优先级,周期尽可能短些,不要超过100us。选择自动 ...

试过了,1秒闪烁太快了。没有2秒好。
回复

使用道具 举报 送花

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|深圳国芯人工智能有限公司 ( 粤ICP备2022108929号-2 )

GMT+8, 2026-5-9 05:41 , Processed in 0.142846 second(s), 105 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表