本项目整合了 UART异步串口通信、定时器中断计时、数码管动态扫描显示、BCD码转换、中断与查询混合编程 等核心嵌入式开发技术,构建了一个可通过串口设置时间、自动走时并动态显示的数字时钟系统。
一、UART串口通信基础与寄存器详解
1. UART核心概念
异步通信:无共享时钟,依赖预设波特率同步。
串行传输:数据逐位在单线上传输(对比并行P0/P1口)。
全双工:TXD(发送)、RXD(接收)可同时工作。
标准数据帧(本代码配置):
起始位:1位(低电平)
数据位:8位
校验位:无
停止位:1位(高电平)
2. 关键SFR寄存器配置
SCON 串口控制寄存器(地址 98H,可位寻址)
方式1说明:8位UART,波特率由定时器1决定 —— 本项目采用模式。
SBUF 串口数据缓冲寄存器(地址 99H)
物理上是两个独立寄存器(发送缓冲器 + 接收缓冲器),共用同一地址。
写操作 → 写入发送缓冲器 → UART自动发送:
SBUF = dat; // 将数据写入发送缓冲区,硬件自动串行发送
读操作 → 从接收缓冲器读取刚接收完成的数据:
uart_buf[uart_num++] = SBUF; // 读取接收缓冲器中的数据
PCON 电源控制寄存器(SMOD波特率倍增位)
PCON &= ~(1 << 7); // SMOD = 0,波特率不加倍
波特率公式(方式1 + 定时器1方式2 + SMOD=0):
波特率 = (1/32) × (fosc / 12 / (256 - TH1))
TMOD & TCON 定时器控制
mod &= ~(0xf << 4); // 清除定时器1模式位
mod |= (0x2 << 4); // 设置定时器1为模式2(8位自动重装)
TMOD = mod;
TR1 = 1; // 启动定时器1,开始产生波特率
为何用模式2? 自动重装避免软件干预误差,确保波特率精确稳定。
二、波特率计算与晶振选择
1. 波特率计算(11.0592MHz晶振)
TH1 = 0xfd; // 十进制253
TL1 = 0xfd;
代入公式:
波特率 = (1/32) × (11059200 / 12 / (256 - 253))
= (1/32) × (11059200 / 36)
= (1/32) × 307200
= 9600 bps
结论:配置波特率为标准 9600 bps。
为何用11.0592MHz?
该频率可被整除,产生精确标准波特率(9600/19200/38400等)。12MHz会有误差,长期通信易出错。
三、程序模块详解(含注释与运行结果)
1. 头文件与全局变量定义
#include <reg51.h>
// 数码管段码表(共阴极,0-9 和 "-" 符号)
// 索引0~9对应数字0~9,索引10对应"-"
unsigned char digit_data[11] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x40};
// 全局变量:用于计时(时、分、秒)
unsigned char sec = 0; // 秒
unsigned char min = 0; // 分
unsigned char hour = 0; // 时
// 全局变量:UART接收缓冲区
unsigned char uart_buf[20] = {0}; // 接收数据缓冲数组
unsigned char uart_num = 0; // 接收字节数计数器
2. 延时函数(软件延时)
// 延时函数:通过循环实现延时
// 参数 i:延时基数(数值越大延时越长)
void delay(unsigned int i)
{
unsigned int num = i; // 保存延时参数
int j = 0;
for(j = 0; j < 10; j++) // 外层循环10次,扩大延时范围
{
num = i; // 每次内层循环前重置计数器
while(num--); // 内层空循环,消耗机器周期
}
}
// 理想结果:调用 delay(1000) 约产生数毫秒级延时,用于数码管消隐与扫描间隔
3. 数码管动态扫描显示函数
// 数码管显示函数:动态扫描显示8位数字
// 参数 num:指向8位数字数组的指针(数组内容为digit_data索引值)
void digit_show(unsigned char * num)
{
char i = 0;
for(i = 0; i < 8; i++) // 依次扫描8个数码管
{
P2 = i << 2; // 位选:P2[7:2] 控制哪一位亮(假设使用3-8译码器)
P0 = digit_data[num[8 - i - 1]]; // 段码:P0输出对应数字的段码(倒序显示)
delay(1); // 短暂延时维持显示
P0 = 0; // 消隐:关闭段码,避免残影
}
}
// 理想结果:循环快速扫描,在人眼视觉暂留效应下稳定显示 "HH-MM-SS" 格式时间
4. 定时器0初始化(50ms定时)
// 定时器0初始化函数:配置为50ms定时
void timer0_init(void)
{
unsigned char mod = 0;
TH0 = (65536 - 50000) / 256 ; // 设置定时器0初值高8位(50ms @12MHz)
TL0 = (65536 - 50000) % 256 ; // 设置定时器0初值低8位
mod = TMOD; // 读取当前TMOD值
mod &= ~(0xf << 0); // 清除定时器0的模式位(低4位)
mod |= (0x1 << 0); // 设置定时器0为模式1(16位定时器)
TMOD = mod; // 写回TMOD寄存器
ET0 = 1; // 允许定时器0中断
EA = 1; // 开启总中断
TR0 = 1; // 启动定时器0
}
// 理想结果:每50ms触发一次中断,用于累计实现1秒定时
5. UART初始化(9600bps,方式1)
// UART初始化函数:配置为9600波特率,8位数据,无校验
void uart_init(void)
{
unsigned char mod = 0;
mod = TMOD; // 读取当前TMOD值
SM0 = 0; // 设置UART为模式1(8位UART,可变波特率)
SM1 = 1;
SM2 = 0; // 禁止多机通信
REN = 1; // 允许串行接收
PCON &= ~(1 << 7); // SMOD=0,波特率不加倍
mod &= ~(0xf << 4); // 清除定时器1的模式位(高4位)
mod |= (0x2 << 4); // 设置定时器1为模式2(8位自动重装)
TMOD = mod;
TH1 = 0xfd; // 设置定时器1初值,波特率9600(11.0592MHz晶振)
TL1 = 0xfd;
ET1 = 0; // 禁止定时器1中断(仅用作波特率发生器)
ES = 1; // 允许串口中断
EA = 1; // 开启总中断
TR1 = 1; // 启动定时器1(开始产生波特率时钟)
}
// 理想结果:UART初始化完成,可接收外部设备发送的时间数据(格式:时、分、秒各1字节十六进制)
6. 十六进制转BCD码函数
// 十六进制转BCD函数:将1字节十六进制数转换为2位BCD码
// 参数 hex:输入的十六进制数(如0x15)
// 返回值:转换后的BCD码(如15)
unsigned char hextobcd(unsigned char hex)
{
unsigned char num = 0;
num = (hex >> 4) * 10 + (hex & 0x0f); // 高4位×10 + 低4位
return num;
}
// 理想结果:输入0x12 → 返回12;输入0x59 → 返回59(用于将串口接收的十六进制时间转为十进制显示值)
7. 主函数(系统主循环)
// 主函数:系统入口
void main(void)
{
unsigned char array[8] = {0,0,10,0,0,10,0,0}; // 显示缓冲区(10表示"-",初始显示"00-00-00")
P2 = 0x55; // 初始化P2口(测试或默认状态)
timer0_init(); // 初始化定时器0(50ms中断,用于计时)
uart_init(); // 初始化UART(9600bps,接收中断)
uart_num = 10; // 【注意】人为设为10,仅为测试发送功能,实际应由中断递增
while(1) // 主循环
{
if(uart_num >= 3) // 接收到至少3字节数据(时、分、秒)
{
hour = hextobcd(uart_buf[0]); // 第1字节转BCD → 小时
min = hextobcd(uart_buf[1]); // 第2字节转BCD → 分钟
sec = hextobcd(uart_buf[2]); // 第3字节转BCD → 秒
uart_num = 0; // 清空接收计数器,等待下一次设置
}
// 分解当前时间到显示缓冲区 array[8]
array[0] = hour / 10; // 小时十位
array[1] = hour % 10; // 小时个位
array[3] = min / 10; // 分钟十位
array[4] = min % 10; // 分钟个位
array[6] = sec / 10; // 秒十位
array[7] = sec % 10; // 秒个位
// array[2]和array[5]固定为10(显示"-"),已在初始化时设定
digit_show(array); // 动态扫描显示当前时间
}
}
// 理想结果:
// 1. 上电后显示 "00-00-00"
// 2. 通过串口发送3字节如 {0x12, 0x34, 0x56} → 显示 "12-34-56"
// 3. 定时器自动每秒递增,时间持续走动
// 4. 到59秒后分钟+1,到59分后小时+1,到23小时后归零
8. 定时器0中断服务函数(1秒累计)
// 定时器0中断服务函数:50ms定时,用于计时
void timer0_handler(void) interrupt 1
{
static unsigned char num_ms = 0; // 静态变量,记录50ms次数
TH0 = (65536 - 50000) / 256 ; // 重装定时器0高8位初值
TL0 = (65536 - 50000) % 256 ; // 重装定时器0低8位初值
TR0 = 1; // 确保定时器0继续运行(部分编译器需重置)
num_ms++; // 50ms计数+1
if(num_ms == 20) // 20 × 50ms = 1000ms = 1秒
{
num_ms = 0; // 清零毫秒计数
sec++; // 秒+1
if(sec == 60) // 秒满60
{
min++; // 分+1
sec = 0; // 秒归零
if(min == 60) // 分满60
{
hour++; // 时+1
min = 0; // 分归零
if(hour == 24) // 时满24
{
hour = 0; // 时归零(00:00:00)
}
}
}
}
}
// 理想结果:每1秒自动更新 sec/min/hour,实现精准走时,支持24小时制循环
9. UART中断服务函数(接收数据)
// UART中断服务函数:接收数据
void uart_handler(void) interrupt 4
{
if(RI) // 接收中断标志置位 → 有数据接收完成
{
uart_buf[uart_num++] = SBUF; // 读取SBUF接收缓冲器数据,存入缓冲区
RI = 0; // 必须软件清零RI标志,否则重复进入中断
}
else if(TI) // 发送中断标志置位(本程序发送使用查询方式,此分支仅做安全清除)
{
TI = 0; // 清除发送中断标志(防止意外中断)
}
}
// 理想结果:
// 1. 每接收到1字节数据,存入 uart_buf,uart_num 自增
// 2. 接收满3字节后,主循环中触发时间更新
// 3. 支持连续接收,缓冲区可扩展(当前最大20字节)
四、关键技术点总结
1. 查询 vs 中断 混合策略
发送:采用查询方式(while(!TI);),逻辑简单,适合教学示例。
接收:采用中断方式,高效不阻塞,适合实时接收。
实际项目推荐发送也改用中断,实现全非阻塞通信。
2. 时间数据格式设计
接收格式:3字节十六进制 → {Hour, Minute, Second}
内部存储:转换为BCD码便于显示分解。
显示格式:8位数码管 → "H H - M M - S S"
3. 定时器双重角色
定时器1:仅作波特率发生器(模式2自动重装)。
定时器0:作系统时钟源(50ms中断 → 累计成1秒)。
4. 动态扫描显示优化
使用 delay(1) + P0=0 消隐,避免鬼影。
位选 P2 = i << 2 假设使用3-8译码器驱动共阴极数码管。
五、系统运行理想效果
上电初始化:
显示 00-00-00
UART准备就绪,等待接收时间设置
串口设置时间:
电脑发送 {0x08, 0x30, 0x00} → 显示 08-30-00
发送 {0x17, 0x45, 0x30} → 显示 17-45-30
自动走时:
每秒秒数+1,自动进位分钟/小时
23-59-59 → 下一秒变为 00-00-00
稳定显示:
无闪烁、无重影,数字清晰
知识点:
UART异步通信协议、SCON/SBUF/PCON/TMOD寄存器操作、波特率计算、定时器中断应用、数码管动态扫描、BCD码转换、中断与查询混合编程、嵌入式系统主循环设计。
————————————————
版权声明:本文为CSDN博主「cellurw」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/oareen/article/details/151835097
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?注册
×
|