资讯动态

C51单片机驱动CH452实现6位数码管精确计时显示

发布时间:2026/9/7 7:49:09 来源:尧图企业网站定制
简介一套基于C51单片机与CH452芯片的数码管显示及定时器控制工程源码面向单片机初学者与电子实训课程围绕00~99循环显示这一典型任务演示动态扫描、段码映射、定时器中断与位选/段选控制等核心环节。压缩包共收纳18个文件整体大小仅37KB其中包含C源程序、A51启动文件、HEX烧录文件以及Keil工程配置文件另有编译辅助的lst/obj/m51等文件可直接在Keil中打开构建并烧录到开发板。目前已有418人学习浏览项目规模虽小但流程完整适合课程设计、竞赛准备或毕业设计参考。借助该工程可深入理解CH452芯片的I/O驱动方式、定时器工作模式及中断服务程序逻辑并掌握动态扫描消隐等实用技巧。工程内已定义好0~9段码数组通过定时器溢出中断自动更新并回零整体代码结构清晰便于快速验证硬件时序与软件联动。1. 项目背景与整体设计思路1.1 为什么是C51 CH452这个组合做单片机开发的人应该都有感触数码管显示这件事看起来简单真正做好其实有不少门道。如果是2位、3位数码管直接用单片机GPIO口动态扫描完全没问题但一旦位数到了6位、8位或者你还需要同时检测按键输入那么用三极管做位选、电阻做限流的传统方案就会变得非常占资源——IO口不够用不说动态扫描的刷新频率还要时刻惦记着稍不留神显示就会闪烁或者有重影。我这次做的是一个计时显示项目需求很明确用C51单片机作为主控驱动6位共阴数码管实时显示定时器计数值并且要求时间精度稳定、显示刷新平滑不占用主循环太多CPU时间。选型的时候最先定下来的是C51——这个不用多说无论是学习还是小批量产品开发STC89C52或者AT89S52这类芯片的资料最全、上手最快而且对于数码管显示这种低速外设场景完全够用。接下来要解决的就是显示驱动方案。最初我也想直接用P0口加三极管扫描6位数码管后来算了一下账6位数码管需要6个位选IO口加8个段选IO口即使段选用P0口复用位选也要占掉6个IO口而且主程序要不停地在每一位之间切换还要考虑消影、延时等细节。更麻烦的是定时器的计时中断一旦进来如果和动态扫描的时序撞上了显示就会出现肉眼可见的跳动。于是我决定把显示这块彻底交给专用的数码管驱动芯片——CH452。CH452是南京沁恒出品的一款数码管显示驱动芯片内部集成了数码管段驱动和位驱动电路支持动态扫描最大可以驱动8位数字还自带键盘扫描功能。它和单片机之间只需要两根线通信类似I2C的两线接口这就把原本需要十几根线的显示电路简化成了两根线而且芯片内部自动完成扫描刷新单片机只需要在需要更新显示内容的时候把数据发过去就行刷新过程完全不占用CPU。这个特性对于带定时器的项目来说太重要了——定时器中断可以最高优先级地跑显示刷新根本不和它抢资源。1.2 核心需求拆解定时精度与显示刷新如何兼得这个项目表面上是“数码管显示”和“定时器”两个功能的叠加实际设计的时候会遇到一个核心矛盾定时器需要精确计数而动态扫描显示需要周期性占用CPU去刷新。如果用常规的动态扫描方案这两个需求是互相干扰的——扫描间隔稍微不稳定显示就会闪烁而定时中断如果被扫描程序阻塞计时精度就会受影响。CH452的方案直接把这个问题消解掉了。显示刷新由芯片内部的扫描电路自动完成单片机与它之间是“只在显示内容变化时通信一次”的关系平时完全不用管它。这样一来定时器中断可以做得非常干净我在定时中断服务程序里只做一件事对一个毫秒计数变量加1。整个中断服务程序的执行时间只有几个微秒对主循环几乎没有影响。在实际测试中这套方案的显示稳定性和计时精度都很理想。6位数码管没有闪烁感也没有重影用示波器测量秒脉冲输出24小时累计误差在1秒以内。下面把整个设计思路和实作细节从头到尾拆给大家。2. CH452芯片的核心原理与通信方式2.1 CH452到底是怎么工作的CH452的内部结构可以简单理解成三块扫描控制逻辑、移位寄存器和驱动输出电路。扫描控制逻辑负责自动产生位选信号的循环扫描时序频率由芯片内部振荡器决定不需要外部提供时钟。它按照固定的顺序依次选中每一位数码管同时在对应的段选线上输出当前这位需要显示的数据。很多人第一次用CH452的时候会有一个疑问既然它内部自己扫描那单片机怎么知道它扫到哪一位了答案是——不需要知道。单片机要做的事情只有一个告诉CH452第几位要显示什么内容。至于CH452什么时候把那一位点亮那是它自己的事。这就好比你在公司里安排了一个得力助手去盯监控屏幕你只需要把需要显示的内容写在便签上递给他至于他什么时候切换画面、怎么看完全不用你操心。和CH452通信走的是一个两线接口类似I2C协议但又不完全一样。它只有两个引脚SCL时钟线和SDA数据线数据传输方向是单向的即单片机向CH452写入数据。每次通信先发送一个字节的设备地址CH452的地址是0x40紧接着发送两个字节的命令或者数据一共是24位。2.2 命令格式与几种常用指令CH452的命令可以分为两大类一类是操作命令用于设置系统参数、显示方式等另一类是显示命令用于写入哪一位要显示什么段码。操作命令的格式是12位最高位为1时表示这是操作命令剩下的位用来编码具体的操作内容。显示命令的格式同样是12位最高位为0紧跟着4位是位选地址最后8位是段码数据。实际项目中最常用的几条命令如下命令功能命令字二进制说明系统命令1000 0000 0000第7位为1时开显示为0时关显示显示方式命令1000 0010 0000设置扫描位数、段码方式等限流命令1000 0100 0000设置段驱动电流控制显示亮度显示数据命令0xxx xxxx xxxx写入某一位的段码数据把上面这个表格再展开说几句。系统命令里除了开显示还包含一个很重要的位——看门狗。CH452内部有一个看门狗定时器上电默认是开启的如果单片机在2秒左右没有给它发送任何命令芯片就会自动复位。这个设计本意是防止系统死机但在实际开发中如果不注意就会遇到“为什么数码管显示一会儿自己就灭了”的怪问题。解决办法很简单在系统初始化时发送一条命令关闭看门狗只要包含看门狗控制位的系统命令即可。显示方式命令用来设置扫描的位数2位、4位、6位或者8位以及段码的编码方式是共阴还是共阳。这里的“共阴共阳”指的是数码管内部的LED连接方式CH452的驱动输出是可以配置适配两种数码管的。我用的是共阴数码管所以在初始化时把这个位配置为共阴模式。限流命令用来设定段驱动电流的大小有16级可调。电流越小数码管亮度越低反过来电流越大越亮。如果出现显示过亮甚至数码管发热的情况优先从这里调。2.3 段码映射表数码管显示字母数字的关键说到段码就绕不开数码管显示字母对应表这个问题。数码管内部有a到g共7个段再加上小数点dp一共8个LED。要显示一个数字或者字母就是把对应的那几段点亮。比如要显示数字“1”点亮b和c两个段就够了要显示字母“A”点亮a、b、c、e、f、g六段。CH452输出的段码和普通共阴数码管不太一样这一点非常容易踩坑。传统共阴数码管段码是“段亮为1”但CH452的段码定义中某一位为0时对应的段点亮。换句话说你直接拿网上的共阴段码表给CH452用显示出来的内容会正好反相——该亮的不亮不该亮的全亮。所以在这个项目里我重新做了一个针对CH452的段码映射表数字0到9的段码如下注意位序从高到低是dp g f e d c b a数字00xC0数字10xF9数字20xA4数字30xB0数字40x99数字50x92数字60x82数字70xF8数字80x80数字90x90这个表和传统的共阳数码管段码表正好一致。第一次用CH452的朋友我建议先把“0”这个段码0xC0发上去试一下如果显示出来是数字8那就说明你的段码极性配反了检查一下共阴共阳模式的配置。3. C51定时器的配置与精确计时实现3.1 定时器工作方式选择与初值计算C51单片机内部通常有两个定时器STC89C52有3个定时器0和定时器1。这个项目里我选用了定时器0工作方式1——也就是16位定时器模式。为什么选方式1而不选方式0或者方式2需要解释一下。方式0是13位计数兼容老的8048单片机用的在现代项目中基本没有优势。方式2是8位自动重装初值的好处是定时中断发生后初值自动恢复不需要在中断里重新赋值省了几条指令但缺点是只有8位精度最大计数值只有256在12MHz晶振下最长的定时时间只有256微秒想做1毫秒的定时周期就得在中断里额外数4次比较麻烦。方式1虽然需要在中断服务程序里手动重新装载初值但它有完整的16位计数范围在12MHz晶振下最大定时时间约65.5毫秒做1毫秒的定时周期很从容初值计算也直观。由于晶振频率是12MHz机器周期等于12个时钟周期也就是1微秒所以要让定时器每1毫秒产生一次中断需要计数1000个机器周期。16位定时器的初值计算公式是初值 65536 - 计数值。代入1000得到64536换算成十六进制就是0xFC18。也就是说在中断服务程序里要把TH0赋值为0xFCTL0赋值为0x18。3.2 定时中断服务程序的极简设计定时器中断服务程序我写得非常克制这是很多初学者容易忽略的一点。中断服务程序里做的事情越少整个系统的实时性就越好。我这个项目的中断服务程序只有两句话void timer0_isr() interrupt 1 { TH0 0xFC; TL0 0x18; ms_count; }第一行和第二行是重新装载初值第三行是对毫秒计数变量加1。整个中断服务程序的执行时间大约在4到5个微秒对于一个1毫秒的定时周期来说占空比还不到0.5%对主循环的影响几乎可以忽略。毫秒计数变量ms_count用的是无符号整型unsigned int最大可以数到65535毫秒也就是65.535秒。如果需要更长的计时可以再加一个更高位的变量来扩展。我这个项目需要显示到秒的十分位所以用了一个秒计数器和一个十分位计数器来逐级进位。每次定时中断把ms_count加1后判断是否满100满了就清0并在十分位变量上加1十分位变量满10了秒变量就加1。这个逻辑放在主循环里处理不放在中断服务程序里目的还是为了缩短中断执行时间。3.3 显示缓冲区与定时器计数值的联动显示数据从哪里来我在内存里开了一个长度为6的数组当作显示缓冲区对应6位数码管的每一位。unsigned char disp_buffer[6] {0, 0, 0, 0, 0, 0};主循环里做两件事第一件把当前的计时值秒、十分位、百分位等拆分成单个数字填入显示缓冲区第二件调用CH452的写显示函数把缓冲区里的每一位数据加上对应的段码转换发送给CH452。这里要注意一个细节当定时器计数在实时变化时主循环拆数据的时刻和显示的时刻之间会有一个极小的延迟但这个延迟最多不会超过一个循环周期对于数码管显示来说根本看不出来。另外数码管显示一个数值时如果某些高位的数字是0通常会选择熄灭那一位而不显示0这样读数更直观。我在主循环里做了这个判断如果某一位是最高位的有效数字之前的位而且该位的数字为0就发送熄灭命令而不是发送数字0的段码。4. 硬件连接与完整代码实现4.1 引脚连接与最小系统搭建硬件部分很简单CH452和单片机之间只需要两根线。我用的是STC89C52RCSDA接到P2.0口SCL接到P2.1口数码管是6位共阴的直接把位选和段选引脚接到CH452对应的DIG0到DIG5和SEG0到SEG7上即可。CH452能提供的段驱动电流相当大最大可以让每个段到80mA所以数码管整个亮起来之后亮度非常高如果是用在夜间环境记得把限流命令配置成小电流否则有些数码管会因为电流过大而出现显示颜色偏白甚至段体发热的情况。CH452的电源用5V供电和C51共用一组电源即可。芯片旁边要加一个0.1uF的退耦电容靠近电源引脚放置。这是开关电路的基本常识——数字芯片在状态切换的瞬间会产生电流尖峰如果没有退耦电容把这种高频噪声吸收掉轻则显示出现乱码重则整个系统复位。4.2 完整代码解析先把核心代码整体贴出来然后逐段解释。这是从我的项目里精简出来的测试版本可以直接拿去用。#include reg52.h #include intrins.h sbit CH452_SCL P2^1; sbit CH452_SDA P2^0; unsigned char code ch452_seg_table[] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 }; unsigned char disp_buffer[6] {0, 0, 0, 0, 0, 0}; unsigned int ms_count 0; unsigned char sec 0; unsigned char sec_10 0; void delay_short(void) { _nop_(); _nop_(); _nop_(); _nop_(); } void ch452_start(void) { CH452_SDA 1; CH452_SCL 1; delay_short(); CH452_SDA 0; delay_short(); CH452_SCL 0; } void ch452_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { if (dat 0x80) CH452_SDA 1; else CH452_SDA 0; CH452_SCL 1; delay_short(); CH452_SCL 0; dat 1; } } void ch452_write_command(unsigned int cmd) { ch452_start(); ch452_write_byte(0x40); ch452_write_byte((unsigned char)(cmd 4)); ch452_write_byte((unsigned char)(cmd 4)); CH452_SDA 1; CH452_SCL 1; delay_short(); CH452_SCL 0; } void ch452_set_digit(unsigned char pos, unsigned char seg_data) { unsigned int cmd; cmd 0x8000; cmd | ((unsigned int)(pos 0x07)) 8; cmd | seg_data; ch452_write_command(cmd); } void timer0_init(void) { TMOD 0x01; TH0 0xFC; TL0 0x18; ET0 1; EA 1; TR0 1; } void main(void) { unsigned char i; timer0_init(); // CH452初始化序列 ch452_write_command(0x0800); // 关闭看门狗 ch452_write_command(0x0200); // 显示方式8位扫描共阴模式 ch452_write_command(0x0400); // 亮度设置为中等 ch452_write_command(0x0100); // 开显示 while (1) { if (ms_count 100) { ms_count 0; sec_10; if (sec_10 10) { sec_10 0; sec; if (sec 60) sec 0; } } disp_buffer[0] sec_10; disp_buffer[1] sec % 10; disp_buffer[2] (sec / 10) % 10; disp_buffer[3] sec / 100; disp_buffer[4] 0; disp_buffer[5] 0; for (i 0; i 6; i) { if ((disp_buffer[i] 0) (i 2)) { ch452_set_digit(i, 0xFF); } else { ch452_set_digit(i, ch452_seg_table[disp_buffer[i]]); } } } }4.3 几个关键函数的实现细节ch452_write_command是整个通信的核心它的时序逻辑是这样的先发送起始信号然后把SDA拉低SCL保持高电平随后开始逐位发送8位数据。CH452在时钟的上升沿锁存数据所以必须在SCL为低电平的时候改变SDA的电平然后拉高SCL让芯片采样采样完成后再把SCL拉低准备下一位。有一个细节很多人第一次搞会栽跟头CH452的有效数据是12位但单片机每次要发8位所以一次命令实际上要发两个字节。第一个字节发高8位第二个字节发低8位其中低8位只有高4位有效低4位是无关位。发送的时候我把命令字整体左移和右移来拆分逻辑上比较直接。这个细节如果不注意你会发现发送的段码和实际显示的不对应而且往往是隔一位错一位。ch452_set_digit里的命令编码也值得讲一下。显示数据命令的12位结构是最高位固定为1第11位到第8位是位选地址000表示第0位001表示第1位以此类推第7位到第0位是段码数据。所以在代码里我写的是cmd 0x8000; cmd | ((unsigned int)(pos 0x07)) 8; cmd | seg_data;0x8000对应最高位为1位选地址左移8位后和它与运算再加上段码数据。这12位命令通过前面的写命令函数分解成两个字节发出去。4.4 初始化序列的坑与正确顺序CH452的初始化看似简单实际上顺序是有讲究的。我见过很多人一上来的初始化代码就是开显示结果数码管显示乱码或者亮度异常。正确的第一步应该是先发送系统命令关闭看门狗然后设置显示方式再设置限流最后开显示。为什么要把关看门狗放在最前面因为CH452上电后看门狗是默认开启的如果在2秒内单片机没有和它通信芯片就会复位。如果你的程序下载进去后发现数码管闪了一下然后就再也不亮了而且检查程序逻辑也没有问题那十有八九就是看门狗没有关闭芯片不断复位显示命令根本来不及执行。显示方式命令我设置的是8位扫描模式虽然我只用了6位数码管但这样设置的好处是扫描时序均匀后续如果要扩展到8位也不需要改配置。共阴模式的选择要和你的数码管硬件一致这个前面已经强调过了。5. 调试实录常见问题与排查技巧5.1 显示乱码、亮度异常的排查方向我自己调试的时候遇到过最典型的一个问题是显示内容完全不对发段码0xC0按道理应该显示数字0结果数码管显示的是一个小横杠。后来反复查才发现是共阴共阳模式配置错了——我把共阴数码管配成了共阳模式导致段码逻辑正好反相。所以如果你遇到显示内容和自己预期的不一致首先不要怀疑段码表先检查一下显示方式命令中的极性位配置和你的硬件是否匹配。另一个常见问题是显示亮度不均匀有些位数码管亮有些暗。这个通常有两个原因一是限流设置过低导致段驱动电流不足以让所有位都达到一样的亮度二是使用的数码管位数和扫描模式不匹配。比如你只用了4位数码管但配置成了8位扫描那么每一位实际点亮的时间就短了一半亮度自然会降低。我通常根据实际使用位数来匹配扫描位数这样亮度最大化。还有一个看起来不起眼但很误导人的问题CH452和单片机之间的连接线如果太长或者用了杜邦线而不是直接焊接通信波形会因为线路阻抗和分布电容的影响而失真导致显示数据偶发性错误。项目在万用板上用飞线调试的时候一切正常一旦转移到正式PCB上反而出了问题——后来查了很久才发现是PCB布线时SDA和SCL走线太长而且靠得太近产生了串扰。在布线时这两根线应该尽量短并且不要和电源线或者大电流走线平行。5.2 定时器计时不准的排查思路计时精度的问题也值得一提。我以为用12MHz晶振不会出问题结果测试发现每过一分钟会慢大约0.2秒。排查之后发现原因有两个。第一个原因是中断服务程序里重新装载初值需要几条指令的时间这部分时间没有计入定时周期内导致每个1毫秒的定时周期实际多出了大约4微秒。第二个原因是中断响应本身有延迟——当定时器溢出中断发生时CPU要保存现场、跳转到中断向量这个过程也需要时间。解决办法是“提前装载”技巧。在中断服务程序里先给TH0和TL0赋值然后再执行其他逻辑。因为16位定时器的计数是连续进行的如果先做其他事情再装载初值那么从溢出到装载完成这段时间内定时器已经继续数了好几个数这些多出来的计数就浪费了。反过来先装载再干别的事定时器从新初值开始计数而中断服务程序的剩余部分执行时间相对固定整体精度反而更可控。用这个办法调整后误差从每分钟0.2秒降到了几乎测量不出来的水平。如果是要求更高的项目我一般会再补充一个补偿方法用一个独立的毫秒计数变量来检验实际定时周期然后在主循环里根据累计误差动态修正初值。这个方案适合精度要求到ppm级别的场景不过对数码管显示这个项目来说有点杀鸡用牛刀了。5.3 主循环顺序对显示实时性的影响最后一个问题比较隐蔽是关于主循环里刷新显示的时机。我的主循环里有“更新显示缓冲区”和“发送给CH452”两个步骤。如果在上电最开始的时候定时器已经跑了一会儿但显示缓冲区还没有被赋值那么数码管上就会显示内存里的随机值。虽然这个时间很短但会在上电瞬间出现一次花屏。解决的办法是在初始化阶段就先给显示缓冲区全部清零然后再开定时器。代码里的顺序是先调用timer0_init再初始化CH452最后进入主循环。但在实际项目中我会把这个顺序改成先初始化CH452并清空显示缓冲区再开定时器。因为CH452的初始化过程很快大约几毫秒而已而定时器一旦开启就开始走数了所以优先把显示准备就绪再开启计时功能避免任何一帧的显示错误。5.4 常见问题速查表问题现象可能原因解决办法数码管不亮看门狗未关闭芯片不断复位初始化时先发送关闭看门狗命令显示内容反相共阴共阳模式配置错误检查显示方式命令匹配硬件显示乱码SDA/SCL线序接反或接触不良检查接线缩短信号线长度亮度不均匀扫描位数和实际位数不匹配设置扫描位数等于实际使用位数数字显示正确但闪主循环里反复发送显示命令造成干扰只在数据变化时发送或降低发送频率计时偏慢中断服务程序执行时间未计入补偿使用提前装载初值的技巧6. 项目扩展与个人体会这个项目做完之后我最大的感触是好的方案不是把每个细节做到极致而是让每个模块之间的配合足够合理。CH452把显示刷新的负担从CPU上完全卸下来了这才是它能和定时器共存的关键。如果当初坚持用GPIO动态扫描想达到同样的显示稳定性和计时精度代码复杂度至少要翻一倍。这套方案后续做扩展也方便。CH452本身自带键盘扫描功能如果需要加几个按键不需要改显示电路只要在初始化时把按键扫描功能打开然后定时读取按键状态即可。我在实际项目中就顺手加了两个调整按钮用来设置定时时长整个硬件没有增加一根飞线程序只多写了一个读键值的函数而已。把显示驱动交给CH452还有一个好处C51的主循环被解放出来了可以腾出精力做更复杂的事比如把当前计时状态通过串口上报给上位机或者根据显示内容切换不同的工作模式。单片机的资源就那么多能把一件重复性强的事情交给专用芯片去做省下的不仅仅是几个IO口更是一大段调试时间。最后再分享一个小技巧如果项目里用到了CH452建议在PCB上预留一个测试点把SDA信号引出来。调试的时候用逻辑分析仪或者示波器挂上去直接看波形里有没有毛刺、时序对不对比对着代码猜问题效率高得多。我在这类小项目上吃过不少亏后来养成了习惯——凡是涉及自定义时序的外设芯片第一件事就是把通信引脚的波形抓出来看一遍再写业务逻辑。这个习惯省下来的时间远比多花的那点功夫多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价