
16×16点阵汉字显示从方案选型到逐行扫描一次讲透16×16点阵汉字显示算得上电子技术课程设计里的经典款。说白了就是拿256个LED排成16行16列的矩阵把汉字的笔画拆成一个个点的亮灭状态再通过快速扫描让整片区域看起来像同时点亮最终显示出汉字。这个项目听着简单真正做起来驱动方案怎么选、字模怎么取、扫描时序怎么调、限流电阻怎么算每个环节都有坑。尤其当你试过好几版代码屏幕却要么只有半屏亮要么字是镜像的就会明白细节有多要命。这篇文章把我做这个项目的思路、计算和调试过程完整捋一遍给正在做课程设计、或者想搞懂点阵显示原理的朋友做个参考。1. 整体设计思路与驱动方案推演1.1 需求拆解256个LED怎么管得过来先想清楚一件事如果让单片机直接控制每个LED16×16就是256个引脚任何单片机都扛不住。所以必须把LED接成矩阵形式16根行线和16根列线就够了总共32个引脚。但32个引脚对MCU来说依然太多而且每个LED还牵扯到驱动电流问题直接挂IO口也不现实。这时候就要引入动态扫描。所谓动态扫描就是同一时刻只点亮一行LED按顺序快速轮流点亮16行利用人眼的视觉暂留效应让整屏看起来是同时亮的。打个比方老式荧光灯其实是以50Hz的频率在闪烁但人眼看不出闪因为视觉暂留把亮灭过程“抹平”了。点阵扫描也是这个原理只要每行刷新频率够高整屏就是稳定的画面。动态扫描的好处是省IO、省驱动电路。但代价也有就是每个LED真正发光的时间只有1/16亮度会打折设计时要用电流和刷新率补回来。这部分我在后面参数计算里详细说。理解了动态扫描整个项目的框架就清晰了MCU只需要做两件事一是告诉电路“现在轮到第几行”二是把这一行16个LED的亮灭数据送出去然后不断重复。1.2 驱动方案对比为什么选138加595行选择和列数据送数主流方案有这么几种。第一种是IO直接驱动直接排除引脚根本不够。第二种是MAX7219这类专用LED驱动芯片一片驱动8×8做16×16需要4片级联三线接口很方便但成本高而且它把扫描、译码、电流控制全封装好了做完你只学会了接线没学到点阵工作的底层原理。课程设计如果选这个答辩的时候很容易被问住。第三种就是最常见的74HC138加74HC595组合也是我推荐的做法。74HC138是3线-8线译码器3个地址输入可以译出8路选通输出两片级联就能选通16行。74HC595是串入并出的移位寄存器带锁存输出两片级联能输出16位列数据。MCU只需要3根线控制595送数据4根线控制138选行加起来7个IO就能驱动整个16×16点阵还能继续扩展。方案对比其实也简单方案IO占用成本能学到的东西适合场景IO直驱很高低少小尺寸点阵MAX72193高少全封装快速出成品1385957低多底层原理全流程课程设计、入门学习这里还有个容易忽略的点138只能做选通不能输出任意数据组合。显示汉字需要每一行的16个点任意亮灭所以列数据必须用595这类带寄存器的并行输出芯片数据可以在移位过程中不影响当前输出等数据全部就位后再一次锁存出去避免切换过程中画面乱闪。这就是“138负责选行、595负责送数据”的分工逻辑。1.3 几个关键参数的计算逻辑参数计算是很多人跳过但后来被坑的地方。先明确一点我这里以共阳点阵为例也就是LED阳极接行线阴极接列线列线由595输出低电平点亮。工程上很多8×8点阵模块也是这个接法通用性比较好。先说限流电阻。每个LED需要串联限流电阻但不可能256个LED每个串一个实际做法是每列共用一个电阻一共16个。因为动态扫描时同一列同时只有一个LED导通所以没问题。电阻值按峰值电流算公式是R (VCC - V_LED - V_饱和压降) / I_peakVCC取5V红色LED压降约2V595灌电流饱和压降约0.2V行驱动三极管饱和压降约0.2V峰值电流取15mA的话电阻就是(5 - 2 - 0.2 - 0.2) / 0.015约173Ω实际取150Ω或180Ω都行。取150Ω时峰值电流约17mA动态扫描下平均电流只有1mA多一点室内看亮度合适。如果用的是蓝色或白色LED导通压降有3V以上电阻要重新算经常有人换LED颜色后整屏变暗就是这个原因。然后看行电流。一行最多16个LED同时亮峰值电流就是16×15mA约240mA。这个电流不能直接让138输出74HC138每路输出能力只有几毫安所以必须加三极管驱动。用8550这类PNP三极管发射极接VCC集电极接行线基极经1k电阻接138输出138输出低电平时三极管导通行线被拉高。基极电阻根据驱动能力算一般取值680Ω到1kΩ之间。刷新率也要提前算好。16行轮流扫描一遍是一帧如果每行点亮时间设为625微秒一帧就是16×625us约10ms换算下来刷新率100Hz人眼完全感觉不到闪烁。这个刷新率是底线低于50Hz就会明显闪所以后面写代码时每行延时时间别拍脑袋乱定最好按这个目标反推。2. 汉字字模原理与取模实操2.1 一个汉字如何变成32个字节动态扫描解决了怎么点亮的问题下一个问题是怎么让LED组成汉字。汉字的本质是在16×16的网格里笔画覆盖到的格子点亮没覆盖到的熄灭。把每一行的16个点拆成两个字节左边8点一个字节右边8点一个字节从上到下16行就是32个字节。举个例子假设我们要在16×16网格上画一个方框第一行16个点全亮对应的两个字节都是0xFF第二行只有最左边和最右边的点亮中间全灭那左边字节是0x80右边字节是0x01。16行拼下来就是完整的方框。汉字也是一样的逻辑只不过笔画更复杂手画太费劲所以一般用取模软件自动生成。你只需要理解一点每个汉字的字形最终都会以32字节的数组形式存在单片机里扫描程序按行取出这些字节就能还原汉字。如果要在程序里动态显示任意汉字还可以引入HZK16这类16×16点阵字库文件根据汉字的GB2312内码偏移去读取对应的32字节。不过课程设计一般只需要显示固定的几个汉字直接取模存数组更省事字库方式适合做更复杂的应用。2.2 取模软件的关键设置与方向匹配取模软件我用得最多的是PCtoLCD2002免费小巧设置直观。用的时候有几个关键选项必须注意。点阵格式要选阴码也就是二进制1代表点亮、0代表熄灭这样字模数据直接和595输出对应。取模走向选逐行式先从左到右取第一行16个点拆成两个字节再取第二行依次往下。每行显示数设成16生成格式选C51风格方便直接粘进代码。这几个设置只要有一个不对后面显示就可能是花的。这里有一个必须记住的匹配关系取模方向必须和扫描方式一致。如果取模是逐行式扫描程序就要按行取数据也就是第N行的两个字节存在font[N×2]和font[N×21]如果取模选了逐列式扫描程序就得改成按列处理。我在调试时经常遇到“字是花的”八成就是这里不匹配。可以在取模软件里生成一个全亮行和单列点亮的测试图案先验证扫描方向对不对再上汉字。还有一个细节取模软件里的“字节位序”选项控制的是一个字节内高位在左还是低位在左。如果显示出来的汉字是左右镜像除了检查接线也可以在这里调整位序。建议刚开始做的时候把“高位在前”固定下来所有代码和硬件都按这个约定来能省很多事。2.3 字模数组、显示缓冲区与代码组织多个汉字的字模按顺序放进一个二维数组里就行const uint8_t F16[][32] { { /* 汉字0的字模数据 */ }, { /* 汉字1的字模数据 */ }, { /* 汉字2的字模数据 */ }, };需要显示第几个字就把对应下标的数组指针传给显示函数。这个方式简单直接显示固定词组完全够用。但如果想做得更灵活比如做滚动显示、动态画面甚至做点阵游戏我强烈建议引入一个“显示缓冲区”的概念。缓冲区就是一个32字节的数组扫描程序只管读取这个数组并显示至于数组内容是从字模表拷过来的还是游戏程序动态生成的扫描程序完全不管。这样职责分离显示引擎和业务逻辑解耦后面扩展贪吃蛇之类的小游戏只需要往缓冲区里写数据就行扫描代码一行不用改。这个设计是后面所有扩展玩法的基础。3. 硬件连接与搭建细节3.1 元器件清单与作用做这个项目需要准备的材料如下器件数量作用16×16点阵屏1块或4块8×8拼显示载体74HC1382片3-8译码器级联选通16行74HC5952片串入并出提供16位列数据8550三极管16只行驱动每行一个电阻1kΩ16只三极管基极限流电阻150Ω16只LED列限流STM32F103最小系统板1块主控5V电源1路点阵供电如果不愿意焊16个三极管可以用ULN2803达林顿管驱动阵列替代一片8路两片搞定接线更整齐但要注意驱动极性和8550方案不同。另外如果手头有74HC154四线-十六线译码器一片就能完成16行选通逻辑更简单只是市面上不如138常见。STM32和51单片机都能做这个项目。51单片机IO是5V电平和595、138直连很省心STM32是3.3V电平后面会提到要注意电平匹配问题。从性能看STM32主频高定时器资源多做贪吃蛇这类扩展游戏更从容所以我后面代码以STM32为例。3.2 接线方案与常见连接错误先说595这边的接线。两片595级联第一片的串行数据输出脚QH接到第二片的串行数据输入脚SERSCK和RCLK并联MCU用3根线控制DATA接PB2SCK接PB1RCLK接PB0。595的并行输出Q0到Q7经限流电阻接点阵列线第二片对应第8到第15列。138这边两片的地址输入A、B、C都接MCU的PA0、PA1、PA2。片选部分建议用两个IO分别控制两片138的使能比如PA3控制第一片PA4控制第二片这样不需要反相器也能实现16选1逻辑最不容易错。具体是行号0到7时PA3输出低电平使能第一片138PA4输出高电平关闭第二片行号8到15时反过来。138的输出端接8550的基极基极串1k电阻8550发射极接VCC集电极接点阵行线。所有芯片的GND必须和单片机共地这一步忘了整个电路就是各种乱闪。有两个常见的连接错误要特别小心。第一是电平匹配74HC595如果工作在5V电源下输入高电平阈值大约是0.7倍VCC也就是3.5V左右STM32的3.3V高电平信号理论上不够可靠。实际很多人直接连也能跑但按规范设计应该选74HCT595这类TTL电平兼容的芯片或者加电平转换。第二是8×8点阵模块的引脚顺序不同厂家模块的引脚排列不一定按丝印来上电前必须用万用表确认行列定义否则接上去就是乱码。3.3 上电前的检查步骤每次做硬件我都有个固定流程这能省下大量排查时间。先用目检确认电源正负极没有接反板子上没有明显短路。然后拿万用表测点阵模块确认哪几个引脚是行、哪几个是列以及模块是共阳还是共阴。记住模块的共阳共阴和电路设计是强相关的如果买回来的模块是共阴那行驱动和列数据的方向都得反过来。接着做单点点亮测试。用一根导线串联一个电阻把5V电源和某个LED点一下确认极性无误。点亮正常之后再写一段最简单的程序先只点亮一行验证138行选和595列数据是否正常。一步到位跑完整扫描程序是最忌讳的因为到时候出了问题你根本不知道是行的问题还是列的问题是硬件的问题还是字模的问题。4. 核心代码实现与扫描逻辑4.1 底层驱动代码595数据发送与行选595的驱动时序不复杂关键是理解SCK和RCLK的分工。SCK是移位时钟每个上升沿把DATA上的电平移入一位RCLK是锁存时钟把移位寄存器里的数据一次性输出到并行口。所以一次完整发送是拉低RCLK循环16次送数据、拉高SCK、拉低SCK最后拉高RCLK。模拟时序的核心函数如下void HC595_Send16(uint16_t dat) { uint8_t i; GPIO_ResetBits(GPIOB, GPIO_Pin_0); // RCLK拉低 for (i 0; i 16; i) { if (dat 0x8000) GPIO_SetBits(GPIOB, GPIO_Pin_2); // DATA 1 else GPIO_ResetBits(GPIOB, GPIO_Pin_2); // DATA 0 GPIO_SetBits(GPIOB, GPIO_Pin_1); // SCK上升沿移位 GPIO_ResetBits(GPIOB, GPIO_Pin_1); dat 1; } GPIO_SetBits(GPIOB, GPIO_Pin_0); // RCLK拉高锁存输出 }这里“先发高位还是先发低位”是个容易搞乱的点。我代码里从最高位开始发但最终数据到595的哪个输出脚取决于硬件接线。不同接法下要么改代码里的移位方向要么调整数据字节的排列。我建议你先画一张数据位序对应图再用“点亮单列”的测试程序验证一遍确认第0列数据到底对应哪个bit。这个验证最多花十分钟但能避免之后所有显示都是乱的。行选就简单多了PA0到PA2输出行号的低三位PA3、PA4做片选void SetRow(uint8_t row) { GPIOA-ODR 0xFFE0; GPIOA-ODR | (row 0x07); if (row 8) { GPIO_ResetBits(GPIOA, GPIO_Pin_3); GPIO_SetBits(GPIOA, GPIO_Pin_4); } else { GPIO_SetBits(GPIOA, GPIO_Pin_3); GPIO_ResetBits(GPIOA, GPIO_Pin_4); } }4.2 扫描显示函数与消隐背后的道理有了底层驱动扫描函数就顺理成章了。核心逻辑是循环16行每一行先送该行的列数据再选通行延时一段时间然后清屏。void Display_Char(const uint8_t *font) { uint8_t row; for (row 0; row 16; row) { uint16_t col (font[row * 2] 8) | font[row * 2 1]; HC595_Send16(col); // 先送列数据 SetRow(row); // 再选通行 Delay_us(600); // 当前行保持点亮 HC595_Send16(0x0000); // 清屏消隐 } }这个顺序是调试中踩坑换来的教训。必须先送数据、再选通行。如果反过来先切行、后送数据那么在数据移位的那几十微秒里新选通的这一行会按上一行的旧数据点亮画面上会拖出残影。而每次延时结束后清屏也是一样的道理不清屏直接切下一行上一行数据会瞬时残留到当前行亮度不均还是小事严重时会有明显的横线拖影。每行延时600微秒16行一帧9.6ms刷新率约104Hz稳定无闪烁。如果觉得屏暗不要靠无限拉长延时来解决因为行延时加长到1ms以上刷新率就掉到60Hz以下闪烁感会很明显。亮度不够应该从限流电阻和行驱动能力上想办法这个方向要做对比。同理如果觉得太亮就减小延时或者降低峰值电流而不是反向操作。4.3 用定时器中断做刷新更稳的显示架构主循环里跑扫描函数对纯显示足够了但一旦同时要处理按键、游戏逻辑、串口通信主循环被卡住几毫秒扫描就不均匀画面上会看到局部闪烁或撕裂。这时候就该上定时器中断。思路很简单定时器每625微秒触发一次中断中断里切换一行。16次中断扫完一屏刷新率稳定在100Hz不受主循环任何影响。主循环只负责更新要显示的字模指针或者修改显示缓冲区里的内容。中断服务函数可以这么写volatile uint8_t current_row 0; const uint8_t *current_font; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (current_font ! 0) { uint16_t col 0; HC595_Send16(0x0000); // 先清上一行数据 col (current_font[current_row * 2] 8) | current_font[current_row * 2 1]; HC595_Send16(col); SetRow(current_row); current_row; if (current_row 16) current_row 0; } } }注意中断里先清屏再送新数据防止上一行的内容在切换瞬间残留。595发送16位数据在模拟时序下也就几十微秒放在中断里完全没问题。这种架构看着比主循环扫描复杂一点但为后面扩展贪吃蛇这类需要持续刷新和逻辑更新的项目打了很好的基础。5. 常见问题速查与实战排查5.1 现象对照表先定位再动手调试了这么多块点阵屏我把常见问题整理成了一张速查表遇到异常先对着定位比盲目改代码高效得多。现象可能原因处理思路整屏不亮电源没接、共地漏了、595极性接反先测电源再测595输出脚电平只有半屏亮138片选逻辑错、第二片595没供上数据检查PA3/PA4对应关系检查级联SER引脚某一行特别暗对应8550基极电阻偏大、三极管坏换电阻或换三极管某一行常亮不受控三极管接错、对应138输出异常断电测三极管引脚逐段排查显示花屏取模方向与扫描方向不匹配统一取模“逐行式”扫描按行送数左右镜像595数据位序反了交换发送位序或调整取模字节位序上下颠倒行扫描顺序反了SetRow里用15-row文字拖影没消隐或刷新率太低行切换前清屏缩短每行延时屏幕闪烁每行延时过长、刷新率不足每行延时控制在625us左右显示内容有亮带清屏顺序不对先清屏再送数据再选行5.2 一个真实排查案例半屏亮不起来说一个我实际遇到的案例。两片138级联上半屏能正常显示汉字下半屏全是黑的。第一反应是第二片138没工作。我用万用表量第二片138的使能脚发现片选信号一直处于无效状态原来是PA4没有初始化成输出模式默认状态不稳定导致第二片138从来没被使能过。把PA4配置成推挽输出后下半屏亮了。但紧接着又出现新问题下半屏显示的内容整体错位像是对不上号。我检查发现两片138的A、B、C地址线虽然都接了PA0到PA2但第二片我接的时候顺序接反了导致第8行到第15行里地址译码结果对不上。重新排列之后整个屏幕显示正常。这个案例的教训就是出问题别急着改代码先用现象缩小范围。“只有半屏亮”基本锁定在片选和级联跟字模没关系。检查顺序上先看使能再看地址线再看数据线一层层剥问题跑不掉。另一个花屏案例让我印象很深。取模软件默认生成的字模是逐列式的但我扫描代码是按逐行式写的结果屏幕上全是密密麻麻的杂乱亮点完全看不出汉字。重新用逐行式取模后马上正常。这类“软件逻辑没问题但效果很怪”的情况优先怀疑取模设置和扫描方式不匹配。6. 扩展玩法从汉字屏到点阵小游戏6.1 显示缓冲区复用与贪吃蛇整体设计做完了静态汉字显示很多朋友会想着加点料16×16点阵做贪吃蛇就是个经典扩展。其实原理特别简单显示引擎完全复用扫描程序还是盯着那32字节的显示缓冲区只是缓冲区里的内容不再是字模而是由游戏逻辑动态生成的地图画面。贪吃蛇的设计思路可以拆成几个模块。地图用二维数组map[16][16]表示蛇身用一组坐标记录食物用一对坐标标记。游戏定时器每200毫秒触发一次蛇向当前方向前进一步检查是否吃到食物、是否撞墙、是否咬到自己。每次地图变化后把map渲染到显示缓冲区uint8_t display_buf[32]; void RenderToBuffer(void) { uint8_t row, col; for (row 0; row 16; row) { uint16_t line 0; for (col 0; col 16; col) { if (map[row][col]) line | (0x8000 col); } display_buf[row * 2] line 8; display_buf[row * 2 1] line 0xFF; } }扫描中断里读的是display_buf游戏主循环只管更新map和调用RenderToBuffer两边互不干扰。16×16正好是贪吃蛇的经典地图尺寸玩起来视觉效果也合适。很多人一听到“点阵贪吃蛇”觉得很高端拆开看就是“显示引擎不变把字模换成游戏帧”没有想象的那么复杂。6.2 仿真验证怎么做才靠谱以及我的几点建议如果你打算先在仿真软件里跑通逻辑再做实物Proteus是很常见的选择。仿真对验证驱动逻辑、字模数据、扫描顺序是有帮助的而且出问题不用反复拆线焊板子。但要注意仿真里看不到动态扫描的真实电气表现亮度、拖影这些现象仿真不出来仿真速度慢的时候还会给你一种“好像在闪烁”的错觉其实是软件本身的刷新限制。仿真验证的重点应该放在字模数据与扫描方向的匹配上。在Proteus里搭好电路后可以先用简单的测试图案点亮单行、单列确认行选和列数据逻辑没问题再加载汉字的字模数组。如果仿真里都花屏那大概率是取模方向或位序问题不要怀疑仿真软件。逻辑验证通过后还是要上实物调一次因为实物才能检验限流电阻是否合适、行驱动电流够不够、消隐是否彻底。最后分享一个我做这个项目最大的心得点阵显示真正磨人的不是代码量而是“顺序”和“方向”这些细节。先固定一套约定比如取模方向用逐行式、字节位序用高位在前、扫描顺序按行从上到下然后所有代码和硬件接线都围绕这套约定来做。这样一来哪怕后面换屏幕、换单片机、增加功能出问题的概率都会小很多。我自己做完这个项目后再去看各种点阵屏的应用都会先下意识确认那套“方向约定”很多奇奇怪怪的显示问题究其原因都是这个基础没打牢。