使用LCD1602显示STM32 多功能电子万年历

📅 2026/7/19 20:35:14
使用LCD1602显示STM32 多功能电子万年历
一、项目概述本项目基于 STM32 标准库72MHz 系统时钟实现LCD1602 显示实时日历时钟支持 24/12 小时制切换、时间调节、整点蜂鸣、按键消抖、闪烁光标调节等功能采用 1ms SysTick 中断作为全局时基分层模块化开发包含时间日历、LCD1602 驱动、矩阵独立按键、蜂鸣器驱动四大模块非阻塞架构无延时阻塞主循环。核心功能清单实时走时年 / 月 / 日 / 星期 / 时 / 分 / 秒自动进位闰年、每月天数自动计算修正时间调节模式切换调节小时 / 分钟被调节位闪烁提示12/24 小时制显示12 小时区分 AM/PM 标识整点蜂鸣提醒可开关非阻塞蜂鸣计时不占用主循环4 路独立按键模式切换、数值加、数值减、确认退出调节硬件消抖1ms 中断计时消抖消抖阈值 15ms稳定无连击LCD1602 50ms 周期刷新界面分两行显示日期 时间基姆拉尔森公式自动计算星期日期变更自动刷新星期数据二、硬件资源分配1. 系统时钟HCLK72MHzSysTick 配置为 8 分频1ms 中断时基SysTick 中断优先级5低优先级避免干扰 LCD 时序2. GPIO 外设分配表格外设引脚功能说明独立按键PA0~PA3PA0MODE、PA1ADD、PA2SUB、PA3OK上拉输入蜂鸣器PB7推挽输出高电平鸣响整点自动触发 200msLCD1602 控制脚PB0(RS)、PB1(RW)、PB2(E)推挽输出LCD1602 数据脚PB8~PB158 位并行数据口支持 4/8 位模式切换3. 硬件器件主控STM32F103C8T672MHz显示LCD1602 2 行 16 字符液晶支持 4/8 位数据接口输入4 路独立轻触按键发声有源蜂鸣器高电平触发三、软件架构分层设计整体分为 4 个独立驱动模块 主应用逻辑模块间低耦合头文件统一接口调用time_calendar.c/h日历时钟核心计时、闰年、星期、蜂鸣、时间进位lcd1602.c/hLCD1602 底层驱动 上层封装接口anjian.c/h独立按键底层扫描 中断消抖计时SysTick.c/h系统 1ms 时基、毫秒延时、中断调度main.c应用主逻辑按键事件处理、界面刷新、模式管理运行架构中断层SysTick 1ms全局毫秒计数、按键消抖计时、蜂鸣时长计时、1 秒计时触发走时、闪烁标志位翻转主循环层轮询按键扫描、50ms 周期刷新 LCD 界面无阻塞延时响应实时性高四、各模块详细技术说明模块 1SysTick 系统时基模块核心功能72MHz 时钟 8 分频配置SysTick-LOAD8999精准产生 1ms 中断全局sys_tick_ms毫秒计数器系统统一时间戳中断回调SysTick_Handler统一调度所有底层定时任务提供Sys_Delay_ms毫秒阻塞延时仅初始化使用中断任务调度1ms 执行一次全局毫秒计数自增按键消抖计时数组累加蜂鸣器时长计时到达 200ms 自动关闭蜂鸣秒计数器sec_cnt累加满 1000 触发 1 秒走时逻辑500ms 翻转闪烁标志sec_blink_flag用于调节位闪烁显示模块 2日历时钟模块 time_calendar1. 全局时间结构体c运行TimeTypeDef time_data {年,月,日,星期,时,分,秒};全局变量存储当前时间中断内修改、主循环读取。2. 核心算法函数闰年判定 Is_LeapYear判定规则能被 4 整除且非整百年 / 整百年能被 400 整除返回 1 闰年0 平年。当月最大天数 Get_Month_MaxDay根据年份 月份自动返回当月天数2 月自动区分 28/29 天用于日期进位边界修正。星期计算 Calc_Week基姆拉尔森公式输入年月日自动换算星期返回 1 周一7 周日1/2 月自动转换为上一年 13/14 月计算仅日期进位时调用减少运算开销。时间进位 Time_Add1Sec每秒执行秒→分→时→日→月→年逐级进位自动钳位边界 每日 0 时自动刷新星期 0 分 0 秒触发整点蜂鸣标志非阻塞蜂鸣。3. 整点蜂鸣非阻塞机制静态变量buzz_run_flag鸣响标志、buzz_ms_cnt计时变量整点置位标志PB7 输出高电平开启蜂鸣1ms 中断内累加计时满 200ms 自动关闭蜂鸣、清除标志全程无延时阻塞不影响按键与显示刷新4. 全局配置参数en_12h1开启 12 小时制024 小时制en_hour_buzz1开启整点蜂鸣0 关闭蜂鸣提醒模块 3独立按键模块 anjian设计思路中断计时消抖非阻塞消抖传统延时消抖会阻塞主循环本方案采用 1ms 中断统一计时无阻塞。静态数组key_tick[4]4 路按键独立消抖计时器Key_Tick_1msSysTick 中断每 1ms 调用所有按键计时 1上限 30mskey_sta[4]按键状态记录1 松开0 按下扫描逻辑 KEY_Scan读取 PA0~PA3 输入电平低电平为按键按下按下检测当前松开状态 消抖计时≥15ms判定有效按键返回键值 1~4松手检测电平恢复高电平重置按键状态与计时单次按键仅触发一次返回无长按连击消抖阈值 15ms键值定义1MODE 切换调节模式0 正常 / 1 调小时 / 2 调分钟 2ADD 数值加 3SUB 数值减 4OK 退出调节模式模块 4LCD1602 驱动模块 lcd1602硬件接口支持通过宏LCD1602_4OR8_DATA_INTERFACE切换08 位并行模式PB8~PB15 完整 8 位数据14 位半字节模式仅高 4 位数据传输底层基础函数lcd1602_gpio_init控制脚 数据脚推挽输出初始化lcd_set_dataport8 位数据映射至 PB8~PB15 输出lcd1602_write_cmd写指令清屏、地址、显示模式lcd1602_write_data写显示字符数据上层封装统一接口应用层调用表格接口函数功能LCD1602_InitLCD 初始化显示开、光标关闭、自动右移LCD1602_Clear全屏清屏LCD1602_SetCursor设置光标行列地址LCD1602_ShowString指定行列显示字符串LCD1602_ShowNum指定位置固定长度数字不足补空格LCD1602_Cursor_On开启 / 关闭光标界面刷新机制主循环 50ms 周期刷新 LCD两行固定布局第一行YYYY-MM-DD Wx年 - 月 - 日 星期第二行时分秒 AM/PM 标识调节模式下对应位空白闪烁模块 5主应用逻辑 main.c全局变量set_mode运行模式 0 正常走时 /1 调小时 /2 调分钟g_sys_run.sec_blink_flag500ms 翻转闪烁标志用于调节位黑屏闪烁按键事件处理逻辑MODE 键循环切换调节模式刷新显示计时戳ADD 键调节小时 10~23 循环/ 调节分钟 10~59 循环修改小时自动重新计算星期SUB 键调节小时 - 123~0 循环/ 调节分钟 - 159~0 循环OK 键退出调节模式恢复正常时钟显示12/24 小时制显示转换逻辑24 小时制直接输出time_data.hour12 小时制转换规则0 点 → 12 AM1~11 点 → 原小时 AM12 点 → 12 PM13~23 点 → 小时 - 12 PM闪烁显示逻辑正常模式完整时分秒显示调小时模式闪烁标志为 1 时隐藏小时位空白占位调分钟模式闪烁标志为 1 时隐藏分钟位空白占位五、关键技术亮点全非阻塞定时架构所有延时、消抖、蜂鸣、秒计时全部依托 1ms SysTick 中断实现主循环无Delay_ms阻塞按键响应、显示刷新实时流畅。模块化分层解耦底层驱动、业务逻辑、应用界面完全分离可单独替换 LCD、修改按键引脚、扩展闹钟功能。高效日历算法基姆拉尔森星期仅日期变更时计算闰年、月份天数自动适配无需手动修正日期边界。硬件资源复用单一 SysTick 中断统一管理全部定时任务减少多定时器占用资源开销低。兼容双模式 LCD1602支持 4 位 / 8 位并行接口切换适配不同硬件 PCB 布线。安全数值边界保护时间增减自动循环钳位小时 0~23、分钟 0~59、日期按月自动修正不存在非法时间数据。六、运行流程说明上电初始化蜂鸣器、SysTick 时基、LCD1602、按键 GPIO 初始化进入无限主循环扫描按键检测有效按键并执行对应调节逻辑每 50ms 刷新一次 LCD 界面根据当前模式拼接显示字符串SysTick 1ms 中断并行执行后台任务按键消抖计时、蜂鸣时长计时每 1000ms 触发一次时间 1 秒自动进位日期月份年份每 500ms 翻转闪烁标志实现调节位闪烁效果整点 0 分 0 秒自动开启蜂鸣 200ms无阻塞提醒七、可扩展功能预留增加多组闹钟存储Flash 掉电保存切换 12/24 小时制按键开关整点蜂鸣开关按键控制秒数调节功能扩展温度传感器 DS18B20 对接增加温度显示背光开关控制八、编译与移植说明平台STM32F10x 标准库 V3.5Keil MDK 编译引脚修改修改头文件中KEY_RCC/KEY_PIN、LCD 控制脚、蜂鸣器引脚宏即可适配硬件LCD 接口切换修改LCD1602_4OR8_DATA_INTERFACE宏08 位14 位消抖时长调整修改KEY_Scan内 15ms 阈值、Key_Tick_1ms最大计时 30ms蜂鸣时长修改宏BUZZ_PLAY_MS调整整点鸣响时间系统主频SysTick_Init 入参修改适配不同主频单片机九、核心代码//main.c #include time_calendar.h #include lcd1602.h #include sys.h #include anjian.h #include string.h #include stdio.h #include SysTick.h Sys_ConfigTypeDef g_sys_cfg; Sys_RunTypeDef g_sys_run; u8 set_mode 0; // 0正常 1调小时 2调分钟 void SysTick_Handler(void) { (void)SysTick-CTRL; Time_SysTick_Process(); Key_Tick_1ms(); // 1ms按键计时 if(sys_tick_ms%500 0) { g_sys_run.sec_blink_flag ^ 1; } } int main(void) { u8 key_val 0; uint32_t disp_tick 0; uint32_t now_tick; // 外设初始化 Buzzer_Init(); SysTick_Init(72); SysTick_Time_Init(); lcd1602_init(); KEY_Init(); // 按键初始化 while(1) { key_val KEY_Scan(); if(key_val ! 0) { switch(key_val) { case 1: // MODE切换 set_mode; if(set_mode 2) set_mode 0; disp_tick 0; break; case 2: // ADD加 if(set_mode 1) { time_data.hour; if(time_data.hour 24) time_data.hour 0; time_data.week Calc_Week(time_data.year,time_data.month,time_data.day); } else if(set_mode 2) { time_data.min; if(time_data.min 60) time_data.min 0; } disp_tick 0; break; case 3: // SUB减 if(set_mode 1) { if(time_data.hour 0) time_data.hour 23; else time_data.hour--; time_data.week Calc_Week(time_data.year,time_data.month,time_data.day); } else if(set_mode 2) { if(time_data.min 0) time_data.min 59; else time_data.min--; } disp_tick 0; break; case 4: // OK退出调节 set_mode 0; disp_tick 0; break; } key_val 0; // 清空键值防止重复触发 } // 50ms刷新LCD now_tick sys_tick_ms; if(now_tick - disp_tick 50) { char buf[32]; uint8_t disp_h; char apm[3] ; disp_tick now_tick; if(en_12h) { if(time_data.hour 0){disp_h12;strcpy(apm,AM);} else if(time_data.hour12){disp_htime_data.hour;strcpy(apm,AM);} else if(time_data.hour12){disp_h12;strcpy(apm,PM);} else {disp_htime_data.hour-12;strcpy(apm,PM);} } else { disp_h time_data.hour; strcpy(apm, ); } // 第一行日期 sprintf(buf,%04d-%02d-%02d W%d,time_data.year,time_data.month,time_data.day,time_data.week); lcd1602_show_string(0, 0, (uint8_t *)buf); // 第二行时间闪烁 if(set_mode 0) { sprintf(buf,%02d:%02d:%02d %s,disp_h,time_data.min,time_data.sec,apm); } else if(set_mode 1) { if(g_sys_run.sec_blink_flag) sprintf(buf, :%02d:%02d %s,time_data.min,time_data.sec,apm); else sprintf(buf,%02d:%02d:%02d %s,disp_h,time_data.min,time_data.sec,apm); } else if(set_mode 2) { if(g_sys_run.sec_blink_flag) sprintf(buf,%02d: :%02d %s,disp_h,time_data.sec,apm); else sprintf(buf,%02d:%02d:%02d %s,disp_h,time_data.min,time_data.sec,apm); } lcd1602_show_string(0, 1, (uint8_t *)buf); } } }//SysTick.c #include SysTick.h #include stdint.h static uint8_t fac_us 0; // us延时倍乘数 static uint16_t fac_ms 0; // ms延时倍乘数 uint32_t sys_tick_ms 0; // 全局毫秒计数器SysTick中断自增 // 初始化SysTick延时 // SYSCLK系统时钟(MHz) void SysTick_Init(uint8_t SYSCLK) { SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); fac_us SYSCLK / 8; fac_ms (uint16_t)fac_us * 1000; } // 微秒级延时 void Sys_Delay_us(uint32_t nus) { uint32_t temp; SysTick-LOAD nus * fac_us; SysTick-VAL 0x00; SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; do { temp SysTick-CTRL; } while ((temp 0x01) !(temp (1 16))); SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; SysTick-VAL 0x00; } // 毫秒级延时 void Sys_Delay_ms(uint16_t nms) { uint32_t temp; SysTick-LOAD (uint32_t)nms * fac_ms; SysTick-VAL 0x00; SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; do { temp SysTick-CTRL; } while ((temp 0x01) !(temp (1 16))); SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; SysTick-VAL 0x00; } // 供SysTick_Handler中断调用每1ms自增全局计时 void SysTick_Tick(void) { sys_tick_ms; } //SysTick.h #ifndef __SYSTICK_H #define __SYSTICK_H #include stm32f10x.h #include stdint.h extern uint32_t sys_tick_ms; void SysTick_Init(uint8_t SYSCLK); void Sys_Delay_us(uint32_t nus); void Sys_Delay_ms(uint16_t nms); void SysTick_Tick(void); void SysTick_Handler(void); #endif//time_calendar.c #include time_calendar.h // 全局时间变量 TimeTypeDef time_data {2026,7,19,6,6,23,59}; uint8_t en_12h 1; uint8_t en_hour_buzz 1; uint8_t sec_cursor 0; // 中断修改、主循环读取必须加volatile防止编译器优化 static uint16_t sec_cnt 0; // 蜂鸣非阻塞状态变量仅本文件可见 static uint8_t buzz_run_flag 0; static uint16_t buzz_ms_cnt 0; #define BUZZ_PLAY_MS 200 // 整点蜂鸣200ms // 月份平年天数表 const uint8_t month_day[12] {31,28,31,30,31,30,31,31,30,31,30,31}; // 1.闰年判定能被4整除非整百 / 整百能被400整除 uint8_t Is_LeapYear(uint16_t year) { if((year%40 year%100!0) || (year%4000)) return 1; return 0; } // 2.获取当月最大天数自动钳位修正日期 uint8_t Get_Month_MaxDay(uint16_t year, uint8_t month) { if(month1 || month12) return 31; if(month 2) { if(Is_LeapYear(year)) return 29; else return 28; } return month_day[month-1]; } // 3.基姆拉尔森公式返回1周一 ~7周日 uint8_t Calc_Week(uint16_t y, uint8_t m, uint8_t d) { int year y; int mon m; int day d; int w 0; if(mon 1 || mon 2) { mon 12; year --; } w(day 2*mon 3*(mon1)/5 year year/4 - year/100 year/400) %7; if(w 0) return 7; else return w; } // 4.时间1秒逐级进位日期非法修正 void Time_Add1Sec(void) { uint8_t max_d 0; time_data.sec ; if(time_data.sec 60) { time_data.sec 0; time_data.min ; // 整点判断00分00秒触发蜂鸣标志非阻塞不延时 if(time_data.min 0 en_hour_buzz buzz_run_flag 0) { buzz_run_flag 1; buzz_ms_cnt 0; GPIOB-ODR | (17); } if(time_data.min 60) { time_data.min 0; time_data.hour ; if(time_data.hour 24) { time_data.hour 0; // 日期1 time_data.day ; max_d Get_Month_MaxDay(time_data.year, time_data.month); if(time_data.day max_d) { time_data.day 1; time_data.month ; if(time_data.month 12) { time_data.month 1; time_data.year ; } // 仅日期变更时刷新星期删除每秒重复计算 time_data.week Calc_Week(time_data.year, time_data.month, time_data.day); } } } } } // PB7蜂鸣器初始化 void Buzzer_Init(void) { RCC-APB2ENR | RCC_APB2ENR_IOPBEN; GPIOB-CRL ~((uint16_t)0x0F (7*4)); GPIOB-CRL | ((uint16_t)0x03 (7*4)); GPIOB-ODR ~(17); // 默认关闭 } // SysTick 1ms中断初始化 72MHz HCLK/8 void SysTick_Time_Init(void) { SysTick-LOAD 8999; SysTick-VAL 0; // CLKSOURCE0 8分频、开中断、开启定时器 SysTick-CTRL ~SysTick_CTRL_CLKSOURCE_Msk; SysTick-CTRL | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; // 优先级5低优先级不抢占LCD时序 NVIC_SetPriority(SysTick_IRQn,5); } // 1ms时基处理函数中断内调用 void Time_SysTick_Process(void) { sys_tick_ms ; sec_cnt ; // 非阻塞蜂鸣计时处理在中断内分时处理不阻塞主线程LCD if(buzz_run_flag 1) { buzz_ms_cnt ; if(buzz_ms_cnt BUZZ_PLAY_MS) { buzz_run_flag 0; buzz_ms_cnt 0; GPIOB-ODR ~(17); } } // 1Hz秒光标翻转 if(sec_cnt 1000) { sec_cnt 0; sec_cursor !sec_cursor; Time_Add1Sec(); // 每秒1秒 } } //time_calendar.h #ifndef __TIME_CALENDAR_H #define __TIME_CALENDAR_H #include sys.h typedef struct { uint16_t year; uint8_t month; uint8_t day; uint8_t week; // 1周一 ~7周日 uint8_t hour; uint8_t min; uint8_t sec; }TimeTypeDef; extern TimeTypeDef time_data; extern uint8_t en_12h; extern uint8_t en_hour_buzz; extern uint8_t sec_cursor; uint8_t Is_LeapYear(uint16_t year); uint8_t Get_Month_MaxDay(uint16_t year, uint8_t month); uint8_t Calc_Week(uint16_t y, uint8_t m, uint8_t d); void Time_Add1Sec(void); void Buzzer_Init(void); void SysTick_Time_Init(void); void Time_SysTick_Process(void); #endif//anjian.c #include anjian.h #include SysTick.h static uint16_t key_tick[KEY_NUM] {0}; static uint8_t key_sta[KEY_NUM] {1,1,1,1}; //1松开 0按下 // 新增SysTick中断每1ms调用一次统一累加消抖计时 void Key_Tick_1ms(void) { uint8_t i; for(i0;iKEY_NUM;i) { if(key_tick[i] 30) // 最大30ms消抖 key_tick[i]; } } void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(KEY_RCC,ENABLE); GPIO_InitStruct.GPIO_Pin KEY_PIN_ALL; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(KEY_PORT,GPIO_InitStruct); } u8 KEY_Scan(void) { uint16_t read_val GPIO_ReadInputData(KEY_PORT); // KEY0 MODE PA0 if((read_val KEY_MODE_PIN) 0) { if(key_sta[1]) // 当前松开 { if(key_tick[0] 15) // 消抖15ms { key_sta[0] 0; key_tick[0] 0; return 1; } } } else { if(!key_sta[0]) // 之前按下现在松手 { key_sta[0] 1; key_tick[0] 0; } } // KEY1 ADD PA1 if((read_val KEY_ADD_PIN) 0) { if(key_sta[1]) { if(key_tick[1] 15) { key_sta[1] 0; key_tick[1] 0; return 2; } } } else { if(!key_sta[1]) { key_sta[1] 1; key_tick[1] 0; } } // KEY2 SUB PA2 if((read_val KEY_SUB_PIN) 0) { if(key_sta[2]) { if(key_tick[2] 15) { key_sta[2] 0; key_tick[2] 0; return 3; } } } else { if(!key_sta[2]) { key_sta[2] 1; key_tick[2] 0; } } // KEY3 OK PA3 if((read_val KEY_OK_PIN) 0) { if(key_sta[3]) { if(key_tick[3] 15) { key_sta[3] 0; key_tick[3] 0; return 4; } } } else { if(!key_sta[3]) { key_sta[3] 1; key_tick[3] 0; } } return 0; } //anjian.h #ifndef __ANJIAN_H #define __ANJIAN_H #include sys.h #include stdint.h // 按键硬件定义 PA0~PA3 #define KEY_PORT GPIOA #define KEY_RCC RCC_APB2Periph_GPIOA #define KEY_PIN_ALL GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3 #define KEY_MODE_PIN GPIO_Pin_0 #define KEY_ADD_PIN GPIO_Pin_1 #define KEY_SUB_PIN GPIO_Pin_2 #define KEY_OK_PIN GPIO_Pin_3 #define KEY_NUM 4 #define LONG_PRESS_MS 2000 // 对外函数声明 void KEY_Init(void); u8 KEY_Scan(void); void Key_Tick_1ms(void); #endif