C51驱动P10点阵屏:74HC595时序与扫描代码详解 简介面向51单片机开发者的P10单色LED点阵屏驱动工程基于Keil C51与STC12C5A32S2芯片包含电子时钟与动态广告显示两大功能模块。工程以C语言实现涵盖LED点阵扫描、延时控制、串口通信、定时器中断及EEPROM存取等关键逻辑支持文字滚动、动画切换与时间设定等显示模式适合学习点阵屏刷新原理、中断服务编程和嵌入式外设驱动设计。压缩包共42个文件以C源码、头文件、目标文件、十六进制烧录文件及Keil工程配置为主另有启动与内存映射文件可直接用Keil打开编译后烧录至51芯片。整体仅82KB结构紧凑已有201人学习下载。对于课程设计、电子竞赛或想入门点阵显示的开发者这是一份可运行的完整工程可帮助理解从底层驱动到上层应用的整体代码组织方式也能为实现广告招牌或时钟功能提供直接参考。1. P10 屏配 C51先搞懂 595 和扫描再动手从网上下到的P10_C51_2014-05-14.rar这类包里通常会有一个 p10.c、一个 main.c 和一份字模数组但很多人把程序烧进 STC89C52 之后屏上只亮一半、上下颠倒或者干脆花屏。问题大多不在 C51 代码逻辑而在于 P10 单元板的扫描方式是 1/4 动态扫描它和 51 的 IO 配合顺序非常敏感先送数还是先选行、OE 要不要参与消隐、A/B 接的是哪两根线任何一处颠倒都会在屏上表现为一个具体故障。这篇文章围绕 51 单片机Keil C51 工程直驱 P10 单色屏来写讲清楚 74HC595 加 74HC138 这条数据链上每一步该做什么再给出可以直接抄进 Keil 的扫描代码和取模参数。适合手里有吃灰的 P10 板、想用它做时钟或点阵广告牌的人。2. C51 驱动 P10 的代码骨架发数、锁存、消隐的时序2.1 按数据流向拆解 P10 的六根控制线P10 单红单元板的驱动链是典型的三段式51 的 IO 输出数据和控制电平74HC595 负责把串行数据变成 32 列并行输出74HC138 配合几个 MOS 管轮流导通 16 行中的某 4 行。接口上真正需要 51 控制的线通常只有六根R、CLK、LAT、OE、A、B部分板子会多两根 C、D那是 1/8 或 1/16 扫描的板子驱动逻辑按行数扩展即可。sbit DAT P1^0; // 串行数据输入接到 595 的 DS sbit CLK P1^1; // 移位时钟上升沿移入一位 sbit LAT P1^2; // 锁存信号接到 ST_CP上升沿把移位寄存器内容锁到输出 sbit OE P1^3; // 输出使能低电平有效 sbit LA P1^4; // 行选地址 A sbit LB P1^5; // 行选地址 B这六根线里DAT 和 CLK 负责把一行的点数据逐位移进 595LAT 负责在移完一整行后一次性输出A/B 决定当前点亮哪一组行OE 负责在切换行时把屏幕熄灭防止残影。1/4 扫描的本质是16 行分成 4 组每组 4 行某一时刻只有一组 4 行被选通并点亮另外 12 行熄灭靠人眼视觉暂留只要刷新率够高看到的就是一整屏内容。2.2 P10_SendByte高位先出的逐字节发送所有 P10 驱动代码的基础是一个发送单字节的函数它把 8 个 bit 按 MSB first 的顺序在 CLK 上升沿逐位移入 595 串行链。很多人习惯用dat 1从低位发结果整屏左右镜像原因就在这里595 的第一位会出现在最左侧的列上所以先发的必须是最高位。void P10_SendByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { if (dat 0x80) DAT 1; else DAT 0; CLK 0; CLK 1; // 上升沿595 移入一位 dat 1; } }发送一字节需要 8 个脉冲发送一行 32 个点需要 4 字节 32 个脉冲而一次完整的区扫描要送 4 行共 16 字节。CLK 0; CLK 1;这两句不能用CLK 1; CLK 0;替代595 只在上升沿采样顺序反了数据会在下降沿被误锁。51 的 IO 翻转速度在 12T 模式下约 1MHz一字节约需 8us对 P10 的刷新要求完全够用。2.3 用 1/4 扫描循环点亮整屏发送完数据只是第一步锁存和消隐的顺序才是 P10 驱动的关键。正确顺序是先拉高 OE 消隐再送一区 4 行的数据锁存选通行最后拉低 OE 使能输出。如果把消隐放在锁存之后切换行的瞬间上一组行的残影会短暂亮一下表现为文字边缘有拖影或整体有鬼影。#define ZONE_DATA_BYTES 16 // 一区 4 行每行 4 字节 void P10_ScanZone(unsigned char zone) { unsigned char r, b; OE 1; // 1. 消隐先关输出 for (r 0; r 4; r) { // 2. 送本区 4 行数据 for (b 0; b 4; b) { P10_SendByte(disp_buf[zone * 4 r][b]); } } LAT 0; LAT 1; // 3. 锁存数据到输出寄存器 LA zone 0x01; // 4. 选行A/B 决定 4 组中的哪组 LB (zone 1) 0x01; OE 0; // 5. 使能这组 4 行点亮 }zone * 4 r算出的是整屏的行号例如 zone1、r2 时对应第 6 行。disp_buf 是二维数组第一维是行号 0 到 15第二维是每行的 4 个字节这种排布方式在第 3 章会详细讲。整个函数执行时间约 120us16 字节移位 128us其中前 110us 花在送数上点亮时间是留给主循环或定时器控制的。2.4 定时器做扫描调度把刷新放中断里最简单的做法是在主循环里反复调用P10_ScanZone(0)到P10_ScanZone(3)但主循环一旦插入耗时的字模运算或按键扫描刷新率就会抖动屏会出现滚动的亮暗条纹。常见做法是把扫描调度放进定时器中断每次中断只扫一个区4 次中断凑齐一帧。volatile unsigned char zone_index; void Timer0_ISR(void) interrupt 1 { TH0 0xFC; TL0 0x66; // 11.0592MHz 12T 模式1ms 中断 P10_ScanZone(zone_index); zone_index (zone_index 1) 0x03; }1ms 扫一个区完整一帧耗时 4ms刷新率 250Hz肉眼看不到闪烁。zone_index按 0、1、2、3 循环配合 0x03做模 4 运算比取余更快。主循环里只需要在需要时修改disp_buf的内容显示更新是异步的这就是无撕裂滚动字幕的基础。定时器初值0xFC66是按 65536 减 922 得到的对应 1ms如果换 12MHz 晶振初值要重算。3. P10 显示汉字取模参数和缓冲区怎么配合 C513.1 取模软件里的参数怎么选P10 单元板的行宽是 32 点列高是 16 行适合做 16x16 汉字两个并排刚好占满一行或 8x16 ASCII。取模软件里常见的参数是逐行式、阴码、自左向右、高位在前这四个选项直接和P10_SendByte的移位方向对应任何一个不对都会导致字反或者花。取模参数推荐设置说明取模方式逐行式横向一行一行输出每行 2 字节点阵格式阴码点亮的点输出 1与 IO 赋值逻辑一致字节顺序自左向右先输出左半字节再输出右半字节位顺序高位在前MSB first与P10_SendByte的移位逻辑匹配如果取模软件没有阴码选项生成的是阳码代码侧可以把if (dat 0x80)改成if (!(dat 0x80))或者在赋值后取反DAT ~(dat 0x80) 7但最干净的做法还是在取模时选对格式。逐行式取模生成的 16x16 汉字是 32 字节前 16 字节是上半屏 8 行的数据后 16 字节是下半屏 8 行的数据每行 2 字节代表左 8 点和右 8 点。3.2 16x16 汉字的 32 字节排布一个 16x16 汉字字模数组长这样每 2 字节组成一行共 16 行。第二字节不一定是下一行要按取模软件的说明确认。下面是一个你字的前 4 行示意实际工程里用取模软件生成完整 32 字节后直接粘贴进 C51 的 code 区。unsigned char code HanZi_Ni[32] { 0x00, 0x00, // 第 0 行 0x7F, 0xFC, // 第 1 行 0x08, 0x10, // 第 2 行 0x08, 0x10, // 第 3 行 // ...共 32 字节 };code关键字把字模放到程序存储区STC89 系列片内 Flash 足够放下几千个汉字不占宝贵的片内 RAM。disp_buf 是运行时缓冲区必须放 RAM但 16 行乘 4 字节只有 64 字节51 的 256 字节 RAM 容量完全够用。字模数组和 disp_buf 是两套数据字模是静态的底片disp_buf 是当前屏上显示的内容。3.3 把字模写进显示缓冲区取模得到的是字体数据要显示到 P10 上需要按列位置写入 disp_buf。一屏 32 列正好放下两个 16x16 汉字第一个字占第 0 到 15 列列字节 0 和 1第二个字占第 16 到 31 列列字节 2 和 3。void PutHZ(unsigned char zone, unsigned char col_byte, unsigned char *font) { unsigned char i; for (i 0; i 16; i) { disp_buf[zone * 4 i][col_byte] font[i * 2]; disp_buf[zone * 4 i][col_byte 1] font[i * 2 1]; } }调用PutHZ(0, 0, HanZi_Ni)会把你字写到第 0 到 3 行zone 0的第 0、1 字节PutHZ(2, 2, HanZi_Hao)会写到第 8 到 11 行的第 2、3 字节。zone * 4 i让一个字跨越了 zone 边界也正确前提是 16 行的字模数据能连续写入。注意col_byte只能传 0 或 2因为汉字是 16 点宽占 2 字节插在中间会错位。3.4 一屏混排汉字和 ASCII 的字节计算P10 屏上最常见的版面是2 个汉字 4 个 ASCII这样也是 32 列。8x16 ASCII 字模每字符 16 字节每行 1 字节取模方式同样是逐行式、阴码、MSB first。写入缓冲区时只占 1 个列字节所以函数要单独写。void PutASCII(unsigned char zone, unsigned char col_byte, unsigned char *font) { unsigned char i; for (i 0; i 16; i) { disp_buf[zone * 4 i][col_byte] font[i]; } }混合排版时的列字节分配是个易错点汉字占 2 字节ASCII 占 1 字节。例如气温 25C从左到右汉字气占 0、1 字节汉字温占 2、3 字节ASCII 2占 0 字节但注意它的区号zone不能超过 2否则一屏根本排不下。实际操作中最好先算好每个字符的起始列再决定 col_byte 和 zone 的对应关系而不是边写边猜。4. 多块 P10 级联和刷新率从一块板到一面屏4.1 级联连接R 进 Q7 出单块 32x16 的板子只能显示两个汉字做时钟或横幅至少要 2 到 4 块。级联不需要额外 IO只要有 595 的串行输出脚 Q7第 9 脚上一块板的数据走完自己的 4 个 595 后从 Q7 送给下一块板的 DAT。接口上通常标着R in和R out或者直接标注上下行的数据箭头。51 DAT - 板1 R_IN 板1 R_OUT - 板2 R_IN 板2 R_OUT - 板3 R_IN 板1..板N 的 CLK/LAT/OE/A/B 全部并连到 51CLK、LAT、OE、A、B 是并连的所有板同时移位、同时锁存、同时选行只有数据是串行穿过的。这意味着级联后一帧的数据量从 16 字节变成 16 乘板数P10_ScanZone里的内层循环要改成按板数扩展。4.2 发送顺序先远端后近端级联时最常犯的方向性错误数据入口在第 1 块板数据会按顺序穿过所有板先发送的字节最终停留在最远端的板。所以要显示从左到右排列的ABCD并且数据从最左边板进入发送顺序应该是 D、C、B、A。级联板数总宽度每区数据字节数发送循环次数1 块32 点164 行 x 4 字节2 块64 点324 行 x 8 字节4 块128 点644 行 x 16 字节void P10_ScanAllZone(unsigned char zone) { signed char p, b; unsigned char r; OE 1; for (r 0; r 4; r) { for (p BOARD_COUNT - 1; p 0; p--) { // 先远端 for (b 0; b 4; b) { P10_SendByte(disp_buf[zone * 4 r][p * 4 b]); } } } LAT 0; LAT 1; // 选行和使能同单板 }如果发现级联后文字左右次序反了把BOARD_COUNT - 1的循环方向反过来即可不需要改硬件。另一种反馈是文字整体被拆成几段乱序那往往不是发送顺序问题而是缓冲区的列字节编址错了检查p * 4 b这个索引p 是板号b 是板内列字节。4.3 刷新率和定时器初值的计算刷新率由两个因素决定定时器中断周期和扫描区数。1/4 扫描的屏4 次中断才显示完整一帧所以实际帧率等于中断频率除以 4。按 1ms 中断算就是 250Hz刚好高于人眼闪烁阈值但动态滚动内容在强光下能看出轻微闪动可以缩短中断周期。中断周期帧率4 区适用场景2ms125Hz静态文字节约 CPU1ms250Hz常规时钟显示500us500Hz滚动字幕、多级联定时器初值按公式65536 - 晶振频率 / 12 / 1000000 * 中断周期(us)计算。11.0592MHz、12T 模式、1ms 中断65536 - 921 64615十六进制 0xFC66。用 12MHz 晶振时是 65536 - 1000 645360xFC18差别不大但要注意。4.4 STC12 的 1T 内核要改哪里新版开发板常配 STC12C5A60S2 这类 1T 单片机直接烧 89C52 的程序会出现两个明显问题一是定时器中断频率快了 12 倍扫描节奏全乱二是发送字节的时间缩短595 的 CLK 上升沿可能因为信号反射产生误触发。解决办法是配置定时器工作模式时把TMOD的定时器时钟改为 12T 分频或重新计算初值。// STC12 系列 1T 模式1ms 中断初值约 65536 - 11059 54477即 0xD4CD // 推荐的做法在 STC-ISP 工具里选择定时器时钟 12T 分频初值沿用 0xFC66发送字节部分一般不用改51 的 IO 翻转速度提高到 12MHz 后每个 CLK 高低电平仍满足 595 的建立时间要求但线材超过 20cm 时建议在 DAT 和 CLK 上各串联一个 33 欧姆电阻。级联超过 4 块板后CLK 的扇出能力会下降可以在板间加 74HC244 缓冲或者在软件里把两条 CLK 语句之间插一个_nop_()拉宽低电平时间。5. 排 P10 屏时用逻辑分析仪抓时序的几个技巧5.1 先抓 CLK 和 DAT确认移位的方向拿到一块不确定接线的板子时先用逻辑分析仪同时量 DAT 和 CLK在空跑P10_SendByte(0xAA)的固件下看波形。正确的波形应该是CLK 产生 8 个脉冲每个上升沿对应 DAT 电平交替变化。如果 DAT 只在低电平附近抖动检查sbit定义和板子原理图是否把线定义反了。逻辑分析仪采样率不用高2MHz 足够重点看脉冲个数和相位关系。// 测试固件只发 0xAA观察 8 个脉冲 while (1) { P10_SendByte(0xAA); delay_ms(500); }0xAA 的二进制是 10101010波形上应该看到 DAT 在每个 CLK 上升沿前稳定变化。如果看到的是连续先低后高再低说明发送函数里移位方向错了把dat 0x80改成dat 0x01或者把dat 1改成dat 1。确认发送方向后再接屏能避免把后续所有排错努力浪费在错误假设上。5.2 单点亮灯法验证行列极性花屏时最有效的定位手段是只点亮一个点。写一个测试函数清空缓冲区后只置位指定坐标逐个观察亮点落在屏幕哪个位置就能反推出取模方向和行选逻辑。void P10_LightOne(unsigned char x, unsigned char y) { unsigned char i, b; for (i 0; i 16; i) for (b 0; b 4; b) disp_buf[i][b] 0x00; disp_buf[y][x / 8] 0x80 (x % 8); }x / 8算出列字节0x80 (x % 8)把对应位放到正确位置。点亮 (0,0) 后如果亮点出现在右上角说明行扫描是从下往上的把 LA 和 LB 的定义互换即可如果亮点在左上角但左右镜像说明取模的 MSB/LSB 选反了。5.3 黑屏、花屏、重影对应哪条线屏上现象优先检查常见原因整屏不亮OE 电平、5V 电源OE 被拉高或悬空595 输出始终禁用全屏亮但内容乱DAT/CLK 波形CLK 有毛刺或 DAT 时序不满足建立时间文字有残影OE 消隐位置消隐放在锁存之后切换行时有残留上下颠倒A/B 电平接法138 的行选地址反了左右镜像取模方向发送时低位先出或取模选了 LSB first重影问题在第 2 章的扫描函数里已经用先 OE1 消隐解决如果你把这段代码改成别的写法比如先把 OE0 使能再选行就要注意残影是否回来。用逻辑分析仪量 OE 波形正常应该看到每扫一区 OE 先拉高约 110us 再拉低如果 OE 高电平时间过短残影也会出现。最后提醒一个排查顺序惯例先供电、再 OE、再 CLK、再数据不要一上来就怀疑汉字取模错误大部分 P10 驱动问题出在时序顺序而不是字形数据上。本文还有配套的精品资源点击获取