
16×16点阵汉字显示这个题目在电子技术课程设计和嵌入式爱好者圈子里算是常青树了。每年都能看到有人在问怎么把汉字点阵数据弄出来怎么接线才能不烧芯片为什么显示出来全是乱码或者重影。最近更是有不少人把它和STM32、贪吃蛇游戏联系到一起一个16×16的LED点阵屏既能显示汉字又能跑游戏听起来挺酷做起来其实也有一堆细节坑。这篇东西我打算从设计思路、选型、字模处理、硬件搭建到软件刷新的完整流程都过一遍最后再加一段我做贪吃蛇扩展时的实测记录和踩坑复盘。不管你是刚拿到课程设计题目的大三学生还是想给实验室做个点阵屏时钟的爱好者按这套思路走下来基本能把“能亮”做到“好用”把“跑起来”做到“不闪不糊”。1. 设计思路与整体方案选型1.1 16×16点阵为什么是“教科书级”的显示题目先聊聊为什么这个题目经久不衰。16×16点阵意味着256个独立控制的LED灯珠这个规模刚好卡在一个很微妙的点上直接用一个单片机引脚驱动一个灯256个灯就需要256个IO普通单片机根本扛不住所以必须引入扫描驱动、锁存器、译码器这类外围电路但它的规模又没有大到需要复杂的灰度算法或者专用的LED驱动芯片人手焊板子、写程序都还在可承受范围内。汉字的本质是图形文字一个16×16的汉字刚好用256个二进制位表示每个点用“1”或“0”表达亮与不亮。这个数据规模是32字节不管是用查表法还是字库索引都能轻松放在单片机的Flash里。你把这个项目做通了相当于把数码管显示、按键扫描、定时器中断、串行通信、动态扫描这些单片机核心知识点全过了一遍这也就是为什么很多课程设计都喜欢拿它当题目。从应用场景看16×16点阵能做的事情非常多。最简单的就是汉字展示牌比如“欢迎光临”这种标语升级一点可以做滚动字幕、时钟显示加个按键和简单的游戏逻辑就变成了你看到的贪吃蛇版本。基础架构完全一样变的只是上层软件这是这个设计最值钱的地方。1.2 方案选型驱动架构怎么定点阵屏驱动方案大体有三条路我分别说一下适用场景和关键取舍。第一种是直接用单片机GPIO逐行逐列扫描。比如用STM32F103C8T6这种引脚数量48个起的芯片把16行接一组IO、16列接另一组IO然后循环拉高行、送列数据。这个方案接线最直观逻辑也最好理解适合验证电路和学习原理。但问题很明显占用的IO太多而且单片机的IO驱动能力有限单个引脚灌电流一般也就20mA左右同时点亮一整行16个灯就是16倍电流直接怼单片机上很容易把引脚拉坏。第二种是行用译码器、列用串行移位寄存器。这是最经典的方案行端用74HC1544-16译码器或者两片74HC13816行只需要4根地址线列端用两片74HC595级联16列只需要3根线数据、时钟、锁存。整个点阵屏接到主控上的线不超过10根是效率和易用性最好的平衡点。第三种是全用专用驱动芯片比如MAX7219、HT16K33这种。这类芯片内置了扫描逻辑和电流控制软件上只需要往寄存器里写数据就能完成显示3168或者8×8模块拼接成16×16时特别方便。缺点是芯片成本高一些而且对于想搞清楚底层原理的课程设计来说把显示逻辑全包给芯片了答辩的时候反而说不清楚。我的建议是如果目标是做课程设计或者进阶练手走第二种方案也就是“138译码器选行 595级联送列”的组合。这个方案既能展示你对数字电路的理解实际写代码调试的工作量又可控后面扩展到贪吃蛇游戏时性能也完全够用。注意如果用的是51单片机时钟频率只有12MHz软件模拟595时序时要注意时序裕量换成STM32的话基本不用担心速度问题就算用GPIO模拟72MHz主频下跑16行×16列的扫描也是富余的。1.3 刷新原理人眼视觉暂留和扫描时序点阵显示的底层原理其实特别朴素你从来没真正“同时点亮”过所有LED你只是把每一行依次点亮速度快到人眼分辨不出来而已。具体来说16行点阵按行扫描每一时刻只有一行是点亮的这一行上对应列为高电平的LED亮起来持续一小段时间然后切换到下一行。16行全部扫一遍就是一个完整帧。人眼的视觉暂留效应大约在20Hz以上就感觉不到闪烁所以只要帧率保持在50Hz以上看起来就是一幅稳定的画面。这里有个重要的数学关系。假设帧率是100Hz那么每帧的时间是10ms分配给16行每行的点亮时间只有0.625ms。这个时间非常短是后续写延时函数时所有bug的来源。如果每行停留时间太短LED平均电流不够亮度会很低如果时间太长帧率掉下去就会看到闪烁或者滚动条纹。刷新时序设计上有个容易忽略的点行切换的瞬间如果列数据还没准备好会出现“串行”的现象——也就是上一行的数据残留在这一行上表现出来就是所谓的重影或者拖尾。解决方法是先把列数据全部锁存好再切换行或者利用595的OE引脚在行切换瞬间把输出禁用一下等稳定了再打开。这个细节后面在软件部分细说。2. 核心部件点阵屏、字模与数据组织2.1 从8×8模块到16×16面板的拼装市面上最常见的单色点阵模块是8×8的一个模块上有64个LED背后引出16个引脚8行8列。要做16×16最划算的方式是买4块8×8模块拼成一个大方阵。拼装的时候核心问题是搞清楚引脚定义。8×8模块内部有两种接法行共阳、列共阴或者行共阴、列共阳。买之前一定问清楚卖家是哪一种因为这决定了驱动电路的极性设计。拿行共阳、列共阴的模块来说某个LED点亮需要同时满足“该行接高电平”和“该列接低电平”两个条件扫描时行端给出高电平选通信号列端给出低电平来点亮对应的点。4块模块拼装时行线和列线要分别并联到对应的分组上。一般做法是第一行第一行的8个LED在模块A上第九行到第十六行的8个LED在模块B上所以行信号需要跨模块连接列信号则在每4个模块之间形成4组8列这4组列线要分别引出来做扩展。焊接的时候我习惯先把4块模块用排针固定在一块洞洞板上确定好对齐和间距再从背面做飞线连接。这一步看着简单但实际很考验耐心焊错一根线排查起来比焊一整块板子还费时间。有条件的话直接买焊好的16×16点阵屏模块也行省下的时间足够把软件多调几个层次。2.2 字模提取取模软件怎么用背后原理是什么汉字点阵数据的获取有两条路径。一条是自己手工对着16×16的网格画点然后按字节整理成数组另一条是用取模软件自动生成。手工画点适合理解原理但做项目效率太低一般都用软件。以PCtoLCD2002这款软件为例操作流程是这样的先设置字模选项点阵格式选“阴码”1表示亮、0表示灭取模走向选“逐行式”每行显示字节数填2自定义格式选C51格式生成的是unsigned char数组。然后在文字输入框里输入汉字点“生成字模”软件就会输出一行一行的十六进制数据。比如“电”这个字16×16点阵会被拆成16行每行16个点、2个字节总共32个字节。但其实取模方式并不唯一。逐行式是最直观的显示缓存第n个字节对应第n行的前8列还有逐列式数据排列方向和扫描方向是垂直的。写驱动代码之前必须确认取模软件的设定和你的硬件扫描方式一致否则显示出来就是整个画面翻转或者错位。如果需要显示任意汉字可以走编码计算路线。汉字内码在GB2312编码下一个汉字对应两个字节通过区位码公式可以算出这个汉字在16×16点阵字库中的偏移量再从这个偏移量开始读取32字节的点阵数据。这种方式适合做完整字库显示比如把整个UCDOS的HZK16字库文件放进SD卡或者外部Flash里那就能显示任意汉字了而不仅仅是烧死在程序里的一小段。2.3 数据结构与显示缓存设计显示数据在程序里怎么组织直接决定了代码好不好写。我比较推荐的做法是设置一个“显示缓冲区”逻辑上就是16个uint16_t变量或者32个uint8_t变量每个bit对应一个LED。为什么要设缓冲区因为扫描是一个高频操作而内容更新是一个低频操作。如果没有缓冲区每次要更新显示内容时直接改硬件端口很容易出现扫描到一半数据被改掉的情况导致画面撕裂。有了缓冲区之后扫描定时器只负责把缓冲区数据按顺序搬出来显示主程序想改内容就只管改缓冲区两边互不干扰这是一个非常经典的“生产者-消费者”解耦思路。缓冲区和工作流程配合起来的伪代码大致是uint8_t display_buffer[32]; // 16行 × 2字节 void scan_row(uint8_t row) { // 从buffer中取出第row行的2字节数据送入595 send_data_to_595(display_buffer[row * 2]); send_data_to_595(display_buffer[row * 2 1]); // 锁存输出 latch_output(); // 选通第row行 select_row(row); }如果要做滚动显示缓冲区里放的就是一帧画面每次滚动时整体平移一个点再重新刷新。如果要做贪吃蛇缓冲区就变成了游戏画面的渲染结果每帧游戏逻辑跑完后把蛇和食物的位置画到缓冲区里然后扫描硬件照常刷新游戏逻辑和显示驱动就彻底分开了。3. 硬件电路搭建与驱动实现3.1 74HC595级联驱动的列端设计74HC595是一个8位串行输入、并行输出的移位寄存器它最重要的特性是可以级联一片595的串行输出脚Q7接到下一片的串行输入脚DS这样两片串联起来就能同时控制16位数据而主控只需要提供3根信号线。引脚功能要记清楚DS是串行数据输入SHCP是移位寄存器时钟上升沿把数据移进一位STCP是存储寄存器时钟上升沿把移位寄存器的数据锁存到输出OE是输出使能低电平有效MR是复位低电平清零。正常工作时的时序是SHCP给16个上升沿把16位数据一位一位移进去然后STCP给一个上升沿16位数据同时出现在并行输出端。这里有个细节容易被忽略595的输出端Q0到Q7对应数据的哪一位如果数据从DS进入第一个进入的bit会在16个时钟之后被推到第二片595的Q0位置。这也就是说如果你从第一片开始送“列0”的数据最终“列0”会出现在第二片595的输出脚上。写代码时要么把数据排列顺序反过来要么把两片595的物理位置反过来接总之先想清楚再动手不然显示内容左右颠倒是最常见的问题。3.2 行端驱动与消隐问题行端用138译码器的话要明确138是3-8译码器一片只能选通8行16行需要两片。两片138的A、B、C三根地址线并联用第四根地址线D区分选哪一片D0时选通第一片D1时选通第二片这个“片选”信号接的是138的E1或E2使能端。4根地址线就能选出16个不同的行号这个逻辑在数字电路课上都学过但真接的时候很容易把使能极性和时序搞混。选通之后还有个关键问题138的输出能提供的电流非常小几个毫安级别直接点亮一整行16个LED肯定不够。所以行端输出一般要经过驱动电路常见做法是加PNP三极管或者达林顿管ULN2803。用ULN2803的话它是集电极开路的达林顿管阵列输入高电平、输出低电平适合做列端灌电流驱动行端用PNP三极管做高边驱动3.3V或者5V的控制信号通过三极管去控制12V或者5V的电源轨。消隐问题就在这个环节出现。行切换的瞬间如果595的输出还维持着上一行的数据这一行来不及关闭两行数据叠加在一起看起来就像画面变“糊”了。最简单的消隐方案是先把595的OE拉高禁用输出然后切换行地址再送新数据、锁存、把OE拉低使能输出。这一套流程只要写进扫描函数里重影问题基本就不会出现。提示如果你用的是STM323.3V逻辑驱动595完全没问题但138和三极管的基极驱动电平要注意有些74系列芯片在3.3V下工作正常有些老型号的HC系列需要5V才能保证高电平阈值这时候要么选供电3.3V也能用的型号要么加个电平转换。3.3 供电与电平匹配注意16×16点阵的功耗比很多人想象中大。假设一个LED的额定电流是10mA同时点亮最多256个灯那就是2.56A的峰值电流。实际扫描时同一时间只有一行是点亮的也就是最多16个LED同时亮电流大约160mA但如果电源余量不足电压跌落会导致亮度不稳定。我建议电源至少留50%的余量选5V/2A以上的适配器比较稳。如果让我推荐一个更稳妥的设计在595输出到LED列线之间串联限流电阻阻值根据LED的额定电流算电阻值等于电源电压 - LED压降除以电流。红色LED的压降大约1.8V到2.0V5V供电下用220Ω到330Ω比较合适。阻值太小会超过LED额定电流导致亮度衰减快阻值太大则整体亮度不够。还有一个经常被忽略的点如果主控和点阵屏不是同一个电源供电地线一定要共地。不然主控发出的3.3V高电平信号和点阵屏的地参考点不一致逻辑电平判断就会出问题表现出来就是数据乱跳、显示毫无规律排查半天发现是共地没做好。4. 底层驱动软件与扫描刷新实现4.1 串行发送与锁存时序写595的驱动代码其实就三件事送数据、打锁存、切行。STM32的HAL库环境下IO操作的效率不算高直接用寄存器操作或者HAL的GPIO读写函数都能完成关键是时序要对。一次完整的行刷新代码如下void row_refresh(uint8_t row) { // 第一步关闭输出防止切换行时出现重影 HAL_GPIO_WritePin(RCK_GPIO_Port, RCK_Pin, GPIO_PIN_SET); // 锁存器输出保持 HAL_GPIO_WritePin(OE_GPIO_Port, OE_Pin, GPIO_PIN_SET); // OE拉高禁用输出 // 第二步串行移入16位数据先送第二片的8位再送第一片的8位 uint16_t row_data display_buffer[row]; for (int8_t bit 15; bit 0; bit--) { HAL_GPIO_WritePin(DS_GPIO_Port, DS_Pin, (row_data bit) 0x01); HAL_GPIO_WritePin(SHCP_GPIO_Port, SHCP_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(SHCP_GPIO_Port, SHCP_Pin, GPIO_PIN_SET); // 上升沿移入 } // 第三步锁存数据到并行输出 HAL_GPIO_WritePin(RCK_GPIO_Port, RCK_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(RCK_GPIO_Port, RCK_Pin, GPIO_PIN_SET); // 上升沿锁存 // 第四步选通当前行 select_row(row); // 第五步打开输出 HAL_GPIO_WritePin(OE_GPIO_Port, OE_Pin, GPIO_PIN_RESET); // OE拉低使能输出 }这段代码有几个关键点值得展开。第一步里锁存器先保持输出、OE拉高是防止后面的数据移位过程中输出端出现不确定状态。第三步先拉低RCK再拉高产生一个上升沿这个上升沿把移位寄存器里的16位数据一次性送到输出锁存器注意这一步完成之前输出端的数据还是旧的完成了才更新。第四步选通新行之前OE是禁用的所以即使行地址切换产生了瞬时的干扰信号也不会反映到LED上。实操心得GPIO翻转操作之间有ns级到us级的间隔74HC595的时序要求比这个宽松得多所以不用刻意加延时。但是如果你的主控频率太高、GPIO翻转太快比如用寄存器操作在72MHz下翻转偶尔会碰到数据采样不完全的问题这时候在时钟引脚操作之间加几个__NOP()空指令或者改用SPI硬件外设来送数据能从根本上解决。4.2 行扫描主循环与定时器设计扫描循环可以放在主循环里死循环跑也可以放在定时器中断里跑。两种方式各有优劣我推荐用定时器中断来做因为主程序可以专心处理游戏逻辑、按键检测这些非实时性任务而扫描的时序完全由硬件定时器保证不会因为主程序里某个操作耗时过长而出现闪烁。拿STM32的定时器举例配置一个1ms的定时器中断每次中断刷新一行16次中断刚好刷新完16行那么一帧的时间就是16ms帧率约62.5Hz人眼基本看不出来闪烁。如果想让画面更稳可以把时间缩小到0.5ms一行帧率变成125Hz肉眼完全感觉不到闪动代价是MCU的中断占用率变高但STM32在72MHz下跑这点负载毫无压力。定时器中断的任务很简单volatile uint8_t current_row 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { row_refresh(current_row); current_row (current_row 1) % 16; TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }这里有个很重要的模块化思想定时器中断里只做“把缓冲区里已有的数据刷出去”这件事不改缓冲区的内容。缓冲区内容的修改全部放在主程序里这样就不会出现“一边扫描一边改数据”导致的显示撕裂。如果用的是51单片机思路完全一样只是定时器配置方式不同。51的定时器用模式116位自动重装配合中断也能实现同样的效果。系统时钟是12MHz的话机器周期1us定时器溢出周期10ms以内都没问题。4.3 刷新率计算与残影控制刷新率到底需要多高这个可以算一算。人眼对低频闪烁比较敏感尤其是直视点阵屏这种亮度变化大的场景。低于50Hz会明显看到闪烁60Hz以上基本稳定100Hz以上大多数人完全无感。对于16行扫描的情况单行刷新时间t等于1除以帧率乘16。如果目标是100Hz那么t 1 / (100 × 16) 625us也就是每行停留0.625ms。这个数字对软件延时来说是很紧张的。如果用简单的delay函数在循环里做IO翻转和位操作16行全刷一遍的代码执行时间必须严格控制在10ms以内。在主循环里做扫描时一个不小心加了断点或者插了比较耗时的函数画面就会抽搐。所以我还是坚持用定时器中断做扫描把时间基准交给硬件这是残影控制的核心。残影还有一个隐形来源是LED的响应速度。普通LED的开关时间在纳秒级本身不是问题但行选通的三极管或达林顿管导通和截止都需要时间ULN2803的关断时间大约几百纳秒到微秒级如果行切换太快上一行还没完全关断下一行就打开了会出现一个很淡的残影带。这个用软件消隐OE先禁用切行后再使能是最有效的办法比单纯靠器件特性可靠得多。残影排查时有个快速验证方法将扫描全部停止让某一列常亮然后用示波器或者逻辑分析仪看这一列的波形。如果方波边缘有长时间的拖尾或者毛刺基本可以锁定是行驱动电路的关断问题如果波形是干净的方波但画面仍然发糊那大概率是数据时序或者锁存时序的锅。5. 从显示到交互贪吃蛇等扩展玩法5.1 游戏逻辑与显示刷新的关系把16×16点阵从“显示汉字”升级成“玩贪吃蛇”是一个非常自然的扩展。16×16的网格天然是256个坐标点蛇身在网格里移动每次吃掉一个食物长度加1撞到边界或者自己的身体就结束游戏。游戏逻辑和显示刷新之间的关系我建议严格分层。显示刷新由定时器中断驱动每1ms或者0.5ms刷一行这个频率是固定的不受游戏逻辑影响游戏逻辑由另一个定时器或者主循环里的状态机驱动比如每200ms走一步这个“走一步”的节奏就是游戏速度。当游戏逻辑决定蛇的新位置时先更新蛇身数组然后把蛇身和食物渲染到display_buffer里显示刷新那边自然就能看到新画面。这里有个很关键的体验问题点阵屏的更新是瞬时的游戏画面没有传统LCD那种动画过渡。要让蛇的移动看起来平滑一方面游戏步进频率不能太高200ms一步比较合适太快了人眼跟不上而且没有“一格一格前进”的感觉另一方面显示缓冲区必须在游戏逻辑更新后立即重绘不然上一帧的蛇尾残留会变成“拖尾”效果看起来像蛇变长了。5.2 输入处理与帧率控制贪吃蛇的方向控制用四个按键就行接在主控的普通GPIO上做软件消抖。按键检测放哪里很讲究如果放在主循环里死循环检测一旦扫描或者游戏逻辑卡住按键响应就会延迟如果放在定时器中断里又可能漏掉短按。比较稳的做法是主循环里周期性读按键比如每10ms读一次检测到状态变化就更新方向变量。方向逻辑有个细节蛇不能180度掉头。比如当前方向是向右玩家按了左直接掉头就会撞到自己身体逻辑上应该忽略这个输入。处理方式是保存一个“上次有效方向”每次按键先判断是否和当前方向相反是就忽略不是才更新转向标志。这个转向标志在下次游戏步进时生效。帧率控制的实现方式很简单用一个变量记录距离上次游戏步进经过的毫秒数uint32_t last_step_time 0; const uint32_t STEP_INTERVAL_MS 200; void game_loop(void) { uint32_t now HAL_GetTick(); if (now - last_step_time STEP_INTERVAL_MS) { update_snake(); // 更新蛇的位置检查碰撞 check_food(); // 检查是否吃到食物生成新食物 render_to_buffer(); // 把蛇和食物画到显示缓冲区 last_step_time now; } }这只是一个朴素的非阻塞延迟写法但足够用。如果你想做难度递增可以把这个STEP_INTERVAL_MS从300逐步减小到100游戏速度越来越快可玩性提升不少。5.3 静态显示、滚动字幕和动画的切换同一个硬件平台不仅能玩游戏还能做动态显示。静态显示最简单缓冲区里放好一帧数据就不动了滚动字幕则是每次刷新时把整帧画面左移一位最左边一列移出去最右边一列从字模数据里补充进来给人一种文字平滑滚动的视觉效果动画则是利用帧切换把几帧不同画面按时间顺序轮换显示。代码实现上滚动和动画都是对缓冲区的脱机操作。滚动显示里最重要的是从哪个地方取新数据如果数据是按16×16点阵排布的连续字模数组那么沿水平方向滚动时一次移动是整个画面移动一列后画面的左边界要补入下一列的字模数据这需要维护一个“当前滚动位置”指针。动画则简单得多设定一个定时器每500ms把缓冲区内容替换为下一帧数据。这些功能本质上是“显示状态机”系统有一个状态变量指示当前是静态显示、滚动还是游戏模式主循环根据状态执行不同的更新逻辑而底层的显示刷新函数完全不用改。这种分层设计让代码扩展性非常好后面想加更多功能比如日历显示、数字时钟只需要往里添加状态和对应的渲染函数就行。6. 常见问题与调试经验实录6.1 故障排查速查表我把实际操作中最高频的几个问题整理成一个表方便你对照排查。现象可能原因排查方法整屏完全不亮电源没接好、共阴共阳接反、OE极性反了先测模块供电再看OE引脚是不是被一直禁用只有一行亮行选通信号固定在某一行用万用表量138的输出确认地址线是否在变化某一行或某一列常亮对应行/列的驱动管击穿或虚焊断电后测对应引脚到地/电源的电阻画面闪烁帧率不够、扫描循环被阻塞确认扫描是否在定时器中断里做帧率是否高于60Hz显示内容重影/拖尾行切换时没有消隐、595输出和行选通时序不对检查代码里OE拉高到行切换之间是否有足够的时序间隔文字左右颠倒了595级联顺序或数据移位方向反了交换两片595的级联顺序或者反转数据位顺序文字上下颠倒了取模方向是逐列但扫描是逐行或行地址映射反了确认取模软件和扫描函数里行序的定义是否一致亮度不均匀列的限流电阻阻值不匹配、LED驱动电流不足逐列测量电流确认所有列的电流接近一致这些现象里重影和颠倒占了我遇到的80%。颠倒问题基本是取模方式和扫描方向没对齐重影问题基本是消隐时序不对排查起来都有比较明确的套路。6.2 我踩过的坑第一个大坑是电源。刚开始做的时候我用了一根USB转TTL线给整个系统供电结果只要同时点亮超过30个灯电压就掉到4.2V以下屏幕明显变暗而且单片机偶尔复位。换成一个5V/2A的独立电源之后所有怪异现象都消失了。所以电源这一块建议一步到位别省。第二个大坑是595的OE引脚忘记接。有的595模块上OE默认拉低使能但如果是自己焊板子这个引脚悬空的话输出状态是不确定的。第一次上电时屏幕所有灯随机亮灭查了半天发现是OE接错了。这个引脚要么接到GPIO方便软件控制消隐要么硬件上固定接GND并配合软件先锁存再切行二者选其一不能悬空。第三个大坑是在STM32上用HAL库的GPIO翻转函数做扫描发现速度太慢。HAL_GPIO_WritePin每次调用都有大量判断和错误检查代码频繁调用时效率远不如寄存器操作。如果把扫描函数放在定时器中断里且中断里需要调用几十次HAL函数中断执行时间会超过200us在100Hz帧率下几乎占用了全部剩余CPU时间。优化方式是扫描函数里直接用GPIO的BSRR寄存器操作一行代码完成一次翻转。第四个大坑是贪吃蛇的方向按键消抖。刚开始我只在检测到电平变化时立刻更新方向结果按键按下瞬间的抖动让蛇在极短时间内连续转向经常自己撞死。后来改成按键状态稳定10ms后才生效问题立刻解决。软件消抖在点阵屏这种应用里是绝对必须的因为GPIO的电平抖动是真实存在的不是玄学。最后一个很有价值的调试经验是做一个“测试模式”固件。在正式写复杂游戏逻辑之前我先写了一个简单的测试程序让屏幕上依次显示全亮、隔行亮、棋盘格、单点移动等几种固定图案用来验证硬件接线是否正确。如果这些基础图案能正确显示说明硬件和底层扫描没有问题之后再调字模、再调游戏就事半功倍。这个习惯建议每个做硬件显示项目的朋友都养成能从源头上省掉大量的无效调试时间。我个人的体会是16×16点阵这个项目真正花费时间的往往不是原理而是硬件连线上的低级错误和数据组织的细节。只要把缓冲区分层、扫描顺序、消隐时序这三件事想清楚整个系统的底子就稳了。至于贪吃蛇、滚动字幕、时钟显示都只是在这个稳固地基上盖的不同房子而已。