简介:一套基于STM32微控制器与MAX7219驱动芯片的八乘八LED点阵显示工程,面向嵌入式初学者,实现数字0至9的点阵显示。项目通过串行外设接口完成主控芯片与显示驱动的通信,覆盖时钟配置、引脚复用、显示数据生成与扫描刷新等环节,可帮助读者理解同步串行协议和点阵控制原理。压缩包共一百二十一个文件,大小约一点二兆,以C语言源文件与头文件为主,同时包含集成开发环境工程文件、编译生成的镜像文件及中间产物,既可直接编译烧录,也便于按需修改验证。目前已有四百二十人学习,适合用于课程设计、电子竞赛或入门实践。包内除核心显示逻辑外,还提供定时器、闪存、模数转换、集成电路总线、控制器局域网、通用异步收发器等标准外设库源码,可作为微控制器基础驱动代码参考。
1. 8x8点阵不是点亮就行:先说清这份资源的边界
STM32F103C8T6 驱动的 8x8LED点阵模块(MAX7219 方案)经常被当成入门外设,但很多人把示例代码烧进去之后,得到的是乱码、镜像、甚至是全暗屏幕——不是芯片坏了,而是字模方向、译码模式、供电接线三件事没对齐。这份压缩包就是围绕“显示 0-9”这个课程设计级目标整理出来的完整工程,里面包含驱动初始化、16 位串行写入函数、0-9 字模数组和主循环调用,拿过来改改引脚就能跑。适合两类人:一是想快速交差的学生,二是手里有 8x8 模块但始终搞不清“行扫描、列扫描、镜像”关系的人。下面我会按协议、字模、代码、坑位、进阶五条线拆开讲,每一步都能照着执行。
2. MAX7219驱动8x8点阵:从协议原理到引脚接线
2.1 MAX7219的串行协议:16位一帧,别和标准SPI寄存器概念混
MAX7219 不是 I2C,也不是严格意义上的标准 SPI 外设,它用一组由 DIN、CLK、CS/LOAD 组成的三线串行接口。每次传输固定为 16 位帧:前 8 位是要操作的寄存器地址,后 8 位是该寄存器的数据。所有位都按高位先出的顺序,在每个 CLK 上升沿被芯片采样,CS/LOAD 保持低电平,等到 16 位全部移完,再把 CS/LOAD 拉高,这次写入才真正锁存进去。你可以理解成“CS 低电平期间不允许中途跳变,否则芯片会认为这是一帧脏数据”。
资源包里默认用 GPIO 模拟这套时序,而不是直接开硬件 SPI,这个选择在实际课程设计里很常见。原因有三个:第一,单个 MAX7219 模块只需要 DIN、CLK、CS 三根信号线,GPIO 完全够用;第二,用 GPIO 模拟时,对时钟极性和相位的理解最直观,DS18B20、红外遥控这类外设以后也还会用到这个套路;第三,硬件 SPI 一次只能发 8 位,你要连发两个字节,中间还得手动控制 CS 线,处理不当反而容易在帧边界处多出一个时钟沿。所以压缩包里给出的是 GPIO 模拟版本,代码量不大,却能把协议吃透。
MAX7219 内部可操作寄存器主要有几个:0x01 到 0x08 是 8 个显示数据寄存器,对应 8 行或 8 列,具体对应关系看模块 PCB 走线;0x09 是译码模式寄存器,点阵应用必须写 0;0x0A 是亮度控制,取值 0x00 到 0x0F;0x0B 是扫描限制,控制扫描几行;0x0C 是掉电/正常工作开关;0x0F 是显示测试模式。后面初始化代码会逐个设置,但你要先记住一个结论:点阵显示 0-9 时,写的主要是 0x01 到 0x08 这 8 个数据寄存器,其他寄存器都只是辅助配置。
2.2 接线表:STM32F103C8T6最小系统板与MAX7219模块对号入座
拿到一块“8x8 点阵 + MAX7219 一体”模块时,模块上一般有 5 个引脚:VCC、GND、DIN、CS、CLK。下面这组接线是压缩包代码里默认的引脚方案,也恰好是 F103C8T6 最小系统板上 SPI1 的复用引脚位置,后续想切硬件 SPI 不用改板子:
| 模块引脚 | STM32F103C8T6 引脚 | 说明 |
|---|---|---|
| VCC | 5V 电源引脚 | 绝大多数模块需要 5V 供电,3.3V 下点阵亮度明显偏低 |
| GND | GND | 必须和单片机系统共地 |
| DIN | PA7 | 数据输入,对应 SPI1_MOSI |
| CLK | PA5 | 时钟输入,对应 SPI1_SCK |
| CS/LOAD | PA6 | 片选/锁存,用普通 GPIO 控制 |
你要注意,这个接线表里 VCC 接的是 5V,不是 3.3V。我见过不少人在最小系统板上找 3.3V 焊点,结果模块能显示但亮度极暗、字迹发虚,误以为是字模问题。原因是 MAX7219 内部是低压差恒流源结构,输入电压不足时,LED 段电流上不去。另外,DIN、CLK、CS 三根线尽可能短,面包板接线时不要跨过整个板子去绕,线太长会带来串扰和压降,后面做硬件 SPI 时会更容易出现花屏。
如果手里不是一体模块,而是那种矩阵屏和 MAX7219 分开卖的裸件,接线逻辑一样,但必须自己确认点阵的 16 个引脚哪个接 MAX7219 的 DIG 引脚、哪个接 SEG 段引脚。网购的很多 8x8 点阵屏本身没有标注行列,这需要你用手头万用表或者一颗测试电阻逐针点亮,不建议靠猜。用一体模块就省掉这一步,这也是为什么大多数课程设计选一体板。
2.3 模块选型:一体板与级联板差别很大
市面上的 MAX7219 点阵模块大致分两类。第一类是“单 8x8 点阵 + 单 MAX7219”,排针通常按 VCC、GND、DIN、CS、CLK 排列,偶尔也有 DOUT 引脚,即级联输出。第二类是 FC-16 级联模块,常见的是两个 8x8 拼成 8x16,或四个 8x8 拼成 16x16,板上有两颗或四颗 MAX7219,信号通过 DIN 进 DOUT 出串联。
压缩包里的驱动默认是针对单模块写的。如果插的是 FC-16 级联板,你用默认代码只会看到第一颗芯片有反应,后面的屏幕全暗或者出现错位数据。这不是程序没写对,而是级联时同一帧数据会从第一颗芯片的 DOUT 移向下一颗芯片。正确方法是把要显示给“离单片机最远那块屏”的数据先发出去,再发离单片机最近那块屏的数据,并在最后一个字节后统一拉高 CS。课程设计如果只是“显示 0-9”这个要求,直接用单模块最省事,不要主动给自己找级联的麻烦。
另外,模块的 5V 供电电流要比你想的大。全亮状态下,8x8 点阵满载电流往往到 250mA 以上,便宜的开发板板载 LDO 根本拖不动。最稳的做法是独立的 5V 稳压模块给 MAX7219 供电,F103C8T6 板子单独上电下载,两者只要共地即可。项目包里给的初始化代码不处理电源问题,但你在实际排线时必须先想清楚,否则后面做亮度测试时,数字 8 一亮,单片机板子上的指示灯都跟着闪。
3. 显示0-9的字模数据:编码方式与字体细节
3.1 先分清“段”和“位”:字模是行编码还是列编码
8x8 点阵里面有 8 行、8 列,共 64 个 LED。MAX7219 的数据寄存器地址 0x01 到 0x08 对应的是芯片扫描的 8 个“位”,每个数据字节的 8 个 bit 对应 8 个“段”。问题在于,模块厂家把点阵的行和列接到 MAX7219 的 DIG 与 SEG 上时,两种接法都有人用。
一种常见接法是:DIG0 到 DIG7 对应点阵的行,SEG A 到 SEG G、DP 对应点阵的列。这时候,你往 0x01 寄存器写一个字节,实际显示的是点阵第一行的 8 个点。另一种接法会反过来,DIG 对应列、SEG 对应行,那寄存器地址就代表列而不是行。字模表直接拷过来,经常出现数字整体旋转 90 度或者水平镜像。
怎么判断你的模块是哪一种?很简单,写一段测试代码,调用 MAX7219_Write(0x01, 0xFF),其余寄存器正常配置,看屏幕上是亮了一行还是一列;再写 MAX7219_Write(0x01, 0x01),看点亮的是不是这条线上最左边或最右边那一点。资源包里的字模如果上电后方向不对,就按这个测试方法去对位,而不是盲目改代码。
源码包的注释通常会写“行扫描”或“列扫描”,但新人最容易忽略的是,即使同为行扫描,字节内 bit0 对应左侧还是右侧 LED 也可能相反。这就导致字节层面的“镜像”,需要做位反转。下面一节给的字模数组是我按常用一体模块整理出来的基准表,实际使用前建议先烧一遍 0 到 9 循环,看看镜像问题是否出现。
3.2 0-9字模表:源码包里的font数组就是这路子
资源包里的字模数组在 font.h 文件中,结构大概是这样,每个数字占 8 个字节,每个字节表示一行或一列:
static const uint8_t font_8x8[10][8] = { {0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C}, /* 0 */ {0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x1C}, /* 1 */ {0x3E, 0x41, 0x01, 0x02, 0x0C, 0x10, 0x20, 0x7F}, /* 2 */ {0x3E, 0x41, 0x01, 0x0E, 0x01, 0x01, 0x41, 0x3E}, /* 3 */ {0x04, 0x0C, 0x14, 0x24, 0x44, 0x7F, 0x04, 0x04}, /* 4 */ {0x7F, 0x40, 0x40, 0x7E, 0x01, 0x01, 0x41, 0x3E}, /* 5 */ {0x1E, 0x20, 0x40, 0x7E, 0x42, 0x42, 0x42, 0x3C}, /* 6 */ {0x7F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10, 0x10}, /* 7 */ {0x3C, 0x42, 0x42, 0x3C, 0x42, 0x42, 0x42, 0x3C}, /* 8 */ {0x3C, 0x42, 0x42, 0x3E, 0x02, 0x02, 0x04, 0x38}, /* 9 */ };以上是一套按“行扫描、高比特对应左侧”的思路画的字模。可以看到,数字 0 是第 1 行两侧亮、中间不亮,第 2 到第 7 行两端亮、中间暗,最后一行两侧再收拢。你可以把 0x3C 拆成二进制,也就是 0b00111100,四颗灯在中间,两侧各留两个空位。逐字节核对,就知道每个数字的 8 行是怎么拼出来的。
这个表不要求你背,关键是学会验证。压缩包里的字模可能和我列出的不完全一致,因为不同模块的接法不同,作者在整理时可能事先做过位反转。你拿到代码后,第一件事不是改算法,而是把数字 0 单独显示出来,对照上面的二进制逻辑看左右是否反了。如果反了,就用一个字节反转函数预处理,后面会讲。
3.3 译码模式必须关掉:不然点阵画不出你要的形状
MAX7219 最让人困惑的一个寄存器是 0x09 译码模式。它本来是给 7 段数码管用的:芯片可以把数据字节直接解释成“0”到“9”对应的段码,省掉软件查表。但在 8x8 点阵上,打开译码模式会让点阵芯片把数据和内部 7 段译码表对应起来,你原本按“每行一个字节”组织的点阵图案会变得完全不可控,表现出来就是数字乱闪、无法还原字形。
所以初始化代码里必须写 MAX7219_Write(0x09, 0x00)。这个 0x00 的含义是:8 位显示数据全部不译码,D7 到 D0 每一位都直接驱动对应段。资源包里的主函数如果有“数字依然乱码”的反馈记录,十有八九是有人把 0x09 参数误写成 0xFF。写成 0xFF 在某些 7 段模块上会很好用,但在 8x8 点阵场景就是翻车现场。
另一个容易忽略的是 0x0C 关机寄存器。MAX7219 上电默认处于 shutdown 模式,需要写 0x01 才进入正常工作。如果代码里只配了译码、扫描、亮度,忘了最后把 0x0C 写成 0x01,那屏幕就是全暗的,而且你不会立刻怀疑到软件,因为硬件看着没毛病。初始化顺序我一般建议:先写 0x0C=0x00 进入关机,再配 0x09、0x0B、0x0A,最后写 0x0C=0x01 打开显示。这样能避免上电瞬间寄存器处于半配置状态,把花屏概率降到最低。
4. 完整工程解析:初始化、写入函数与调用顺序
4.1 初始化流程:先关显示,再配寄存器,最后打开
压缩包的主控制逻辑一般分两段,第一段是 MAX7219 初始化,第二段是主循环里循环显示 0-9。初始化代码如下面这样,模块里用到的寄存器顺序在这里体现得很完整:
void MAX7219_Init(void) { MAX7219_Write(0x0C, 0x00); // 进入shutdown模式,避免配置中途乱显示 MAX7219_Write(0x09, 0x00); // 关闭BCD译码,点阵模式必须设为0 MAX7219_Write(0x0B, 0x07); // 扫描限制设为7,即扫描全部8行 MAX7219_Write(0x0A, 0x06); // 亮度寄存器,初始值取中间档 MAX7219_Write(0x0C, 0x01); // 退出shutdown,恢复显示 }这段代码逻辑很清楚。第一句先写 0x0C 为 0,相当于把芯片先关掉,让后续寄存器配置过程在屏幕熄灭状态下完成。第二句关闭译码模式,点阵才能接收你自定义的字模。第三句 0x0B 写 0x07 不是“只扫描 7 行”,而是扫描限制值的含义是从 0 计数到 7,也就是 8 行。第四句 0x0A 写 0x06,亮度不是开最大,初始给个中等亮度,方便在调试时区分程序问题和硬件问题。最后一句再次写 0x0C 的 0x01,芯片才开始正常扫描显示。
这里要强调一个常见的错误:把这五句顺序倒过来,先打开显示再配置寄存器。上电瞬间 MAX7219 每个显示寄存器里可能是随机值,如果你先开 0x0C,屏幕会立刻把随机数据显示出来,然后你再一行一行改寄存器,就能看到“扫过去一行变一下”的怪像。虽然最终也能恢复正常,但每次复位都会闪一下,观感很差,而且会让新手误以为硬件不稳定。
4.2 逐字节写入函数:CS拉低、16个CLK边沿、CS拉高
GPIO 模拟的核心写入函数是这个工程里被调用最多次的函数。它的任务就是把“寄存器地址 + 数据”组成 16 位,按高位先发,最后锁存:
void MAX7219_Write(uint8_t reg, uint8_t data) { uint8_t i; CS_LOW(); // 拉低LOAD/CS,开始一帧传输 for (i = 0; i < 8; i++) { CLK_LOW(); // 时钟先拉低,为上升沿做准备 if ((reg >> (7 - i)) & 0x01) DIN_HIGH(); // 发送地址位,从最高位开始 else DIN_LOW(); CLK_HIGH(); // 上升沿,MAX7219采样当前DIN电平 } for (i = 0; i < 8; i++) { CLK_LOW(); if ((data >> (7 - i)) & 0x01) DIN_HIGH(); // 再发送8位数据 else DIN_LOW(); CLK_HIGH(); } CS_HIGH(); // 拉高CS,整帧16位数据被锁存进MAX7219 }逻辑拆开看:CS_LOW 之后,第一个 for 循环依次发送 reg 的 bit7 到 bit0,每次发送前 CLK 拉低,设置完 DIN 电平后 CLK 拉高,形成上升沿。MAX7219 就是在 CLK 上升沿把 DIN 读进去的。第二个 for 循环完全一样,只是发送对象换成 data。最后 CS_HIGH 这步不能省,如果不拉高,前面 16 个时钟沿全部白费。
参数上你只需要改两处:reg 是寄存器地址,比如 0x01 到 0x08 对应点阵第 1 行到第 8 行,data 是你要显示的那一行字节。代码里的(reg >> (7 - i)) & 0x01是一种标准写法,它的效果是依次取出 reg 的最高位、次高位……直到最低位。如果你更喜欢直观写法,也可以改成((reg << i) & 0x80),效果一样,只是可读性差一点。
4.3 压缩包内的文件结构:烧录前先改这三个地方
压缩包解开后通常包含这些文件,我按最常见的结构列一下:
| 文件 | 作用 |
|---|---|
| main.c | 时钟配置、引脚初始化、主循环调用显示 |
| MAX7219.c / MAX7219.h | 驱动主机代码 |
| font.h | 0-9 字模数组定义 |
| 工程文件 | 打开后直接编译烧录 |
烧录前建议你先改三个地方。第一,确认 main.c 里引脚的宏定义和你的接线一致,尤其是 DIN、CLK、CS 这三个宏,不要实际接 PA5/PA6/PA7,代码里却写 PB5/PB6/PB7。第二,确认 5V 供电确实到位,用万用表量模块 VCC 和 GND 之间电压,不要只看 USB 供电灯亮就默认模块也是 5V。第三,确认字体方向,烧进去之后先显示一个 0,判断是否需要做位反转或数组转置。
这个工程并不复杂,main.c 里主循环一般就是不断调用ShowDigit(0)、延时、ShowDigit(1)这样循环。看清楚调用关系后,你要改成“显示 0-9 循环一次后停留”或“按键切数字”都很容易。资源包覆盖的是完整流程,剩下的灵活性完全在你自己手里。
5. 常见问题排查:不显示、镜像、花屏和亮度坑
5.1 现象:上电全亮或按复位时闪过正常字形,平时全暗
很多同学第一次烧录后,点阵要么全亮没有变化,要么按一下单片机复位键,屏幕闪一下正确字形然后又暗掉。这个现象看起来像是程序没烧进去,其实多半是 CS 引脚浮空导致帧数据无法稳定锁存。
原因在于 STM32 上电复位瞬间,PA6 如果没被配置成推挽输出,输入模式下引脚电平不确定。MAX7219 在 CS 电平不稳定时,会把移位寄存器里的数据随机锁存,于是显示乱掉。复位一瞬间有一个低电平窗口,恰好让单片机把正确数据写入进去,看起来就“闪了一下正常字形”。
解决方法是:初始化引脚时,先把 CS 配置为推挽输出并拉高,再执行 MAX7219 初始化。如果你用的是某开发板,还要检查 CS 引脚是否和调试器接口或板上 LED 共用了同一 GPIO。F103C8T6 最小系统板的 PA6 一般没复用,但如果你把 PA6 插了杜邦线到模块,杜邦线悬空或接触不良也会产生同样现象。重插一遍并确保共地,能解决掉一半电源相关的玄学问题。
5.2 现象:数字能显示,但看起来像照镜子,左右完全相反
这是点阵模块最经典的镜像问题。你心里想的“1”在最右边靠边,屏幕上却出现在最左边,或者数字“6”的圆弧方向相反。第一个反应不要是换模块或重新下载程序,而是检查字模字节的位顺序。
原因是 MAX7219 的数据字节 D7 到 D0 对应 SEG A 到 SEG G、DP,但不同模块在布线时可能把 D7 接到点阵最左侧,也可能把 D0 接到最左侧。同一个字模数组,在两种模块上显示就互为镜像。解决的思路是写一个字节反转函数,在显示前对每个字模字节做一次镜像翻转:
uint8_t ReverseByte(uint8_t value) { uint8_t result = 0; uint8_t i; for (i = 0; i < 8; i++) { result <<= 1; result |= (value & 0x01); value >>= 1; } return result; }然后在 ShowDigit 函数里把font_8x8[num][row]替换成ReverseByte(font_8x8[num][row]),再调用 MAX7219_Write。注意反转的是字节内部位序,不是数组行号。如果你发现数字旋转了 90 度,那是行和列的概念对调了,需要改变循环方式,把第 row 行第 col 列点提出来重新组成新数组,这就不是一次 ReverseByte 能解决的。建议先只对数字 0 做测试,左右反了就用上面的函数,上下反了就是寄存器地址与字模行序对应反了,把 for 循环反过来即可。
5.3 现象:亮度调低没用,0x0A 寄存器写 0x00 还是刺眼
项目需求里可能会写“显示亮度可调”,于是你在主循环里把 0x0A 寄存器从 0x00 到 0x0F 都试了一遍,发现亮度变化不大,甚至写 0x00 还是亮得刺眼。这个问题的根因不在程序逻辑,而在硬件限流电阻。
MAX7219 的 ISET 引脚到 VCC 之间会接一个电阻,它决定了 LED 段电流的基准。很多一体模块出厂时为了达到足够亮度,把限流电阻选得较小,导致最小段电流也超过人眼舒适范围。0x0A 亮度寄存器控制的是扫描占空比,相当于调节平均亮度,但它的调节范围受硬件电流基准限制,并不能把亮度压到熄灭级别。正确做法是先检查模块硬件上的电阻参数,再通过代码把亮度寄存器设到 0x02 到 0x05 之间,不要把希望全压在软件上。
还有另一个容易被忽视的情况:初始化函数如果在主循环里被反复调用,每次都会把你设定的亮度重新覆盖掉。有人的代码在 while(1) 里先 ShowDigit 再 Init,结果初始化每次都把 0x0A 写回中间值,你在其他位置写多少都会被覆盖。我的习惯是初始化放到 main 函数上线开始处只执行一次,主循环只负责刷新显示数据。
5.4 现象:显示 0 和 1 正常,换到 2 或 3 出现毛刺、错位
如果数字 0 和 1 清晰,数字 2 和 3 偶尔出现多点亮或错行,先不要怀疑字模数组。你可以在调试器里单步执行,看写入 2 的字模数据是否正确,通常数据是对的,问题出在 GPIO 翻转速率与线缆质量。
GPIO 模拟 SPI 时,如果把 STM32 引脚输出速度配置成 50MHz,翻转沿会非常陡峭。当杜邦线较长、且 CLK 和 DIN 并行走线时,信号之间的串扰会让 MAX7219 在 CLK 上升沿采样到错误电平。这个现象不是每个数字都会出现,因为不同数字在同一时刻的 DIN 翻转次数不同,恰好翻转组合更容易触发串扰。解决方法是把 GPIO 速度降到 2MHz,或者在每个 bit 写入后加一个短暂延时,人为降低翻转速率。另一个有效手段是在 CLK 拉高后加一点 tCLK 保持时间,代码里表现为CLK_HIGH(); delay_us(1);,16 位走完后再 CS_HIGH 锁存。
如果毛刺只在快速循环切换数字时出现,还要检查刷新间隔。MAX7219 是动态扫描芯片,它在内部不断轮流点亮 8 行,如果在一次扫描周期中间连续写入 8 个寄存器,某些行难免读到新旧数据混杂。我一般会在数字切换之间加一个消隐步骤:先写 0x01 到 0x08 全 0,延时一两毫秒,再写入新的数字字模。这样一来,人眼感觉不到熄灭,但鬼影明显减少。
6. 进阶用法:切到硬件SPI,让数字轮流刷起来
6.1 硬件SPI1配置:把GPIO模拟换成外设发送
前面提到资源包默认用 GPIO 模拟,但你如果想更接近工程化,可以把发送部分换成 F103C8T6 的 SPI1 外设。接线不变,仍是 PA5、PA6、PA7。初始化配置如下,这里用的是常见库函数风格:
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }这段配置里最关键的是时钟极性和相位:CPOL 为低、CPHA 为 1Edge,即 SPI 模式 0。MAX7219 在 CLK 上升沿采样数据,模式 0 正好满足这个时隙要求。BaudRatePrescaler 选 16,也就是 SPI 时钟为 72MHz/16=4.5MHz,对 MAX7219 完全绰绰有余。PA6 要提前拉高,否则 CS 默认低电平会让芯片认为一直有数据在进入。
对应的写寄存器函数也要改,但整体逻辑不变:CS 拉低,发一个字节地址,发一个字节数据,等待发送完成,CS 拉高。所有操作都基于 SPI1 外设,12 个寄存器的写入速度会比 GPIO 模拟快得多。
6.2 轮播0-9:清屏、显示、延时三步走
硬件 SPI 写好后,主循环可以做成“0 到 9 自动轮播”。我用的是最简单的三步逻辑:
void ShowDigit(uint8_t num) { uint8_t row; if (num > 9) return; for (row = 0; row < 8; row++) { MAX7219_Write(row + 1, font_8x8[num][row]); } } void ClearScreen(void) { uint8_t row; for (row = 0; row < 8; row++) { MAX7219_Write(row + 1, 0x00); } } int main(void) { uint8_t num = 0; SystemInit(); GPIO_Init(); SPI1_Init(); MAX7219_Init(); while (1) { ClearScreen(); delay_ms(2); ShowDigit(num); delay_ms(400); num++; if (num >= 10) num = 0; } }这里为什么要先 ClearScreen?因为 MAX7219 的显示寄存器会被持续扫描,旧字模不会自动消失。数字 0 和 1 差距很大时可能没感觉,但 6 和 8 之间切换就会有亮度残留。先清屏两毫秒,再写入新数据,就能避免两个字模叠印。
如果你觉得 400ms 的轮播速度偏慢,可以改成 200ms;如果项目要求“按键切换数字”,把延时改成读按键状态即可。核心是保持刷新函数不阻塞太久,MAX7219 本身会动态扫描,你只需要在数据变化时重新写入,不需要像数码管软件扫描那样一直刷 CPU。
6.3 拿到新模块先做四步自检:全亮、清屏、定位、数字
分享一个我自己常用来验证新模块的流程,也适合拿到这份资源后第一次烧录时执行。第一步,让 MAX7219 进入测试模式,写 0x0F 寄存器为 0x01,屏幕会全部点亮,如果扫描数据异常就说明硬件本身有问题。第二步,退出测试模式并写 0x0F 为 0,做一次清屏,确认全灭。第三步,单独写 0x01 寄存器一个字节,确认第一行或第一列的位置,背下模块的位序关系。第四步,跑 0-9 轮播,如果出现镜像或旋转,回到第 5 章去改位序。
这个四步流程能筛掉九成问题。有一次我把一整套代码移植到对手头的模组,直接显示“0-9”全乱,后来发现是该模块的 CS 引脚上电逻辑与代码默认相反,必须先写低才能完成锁存,查了很久才发现。从那以后我每次拿到新矩阵模块都会强制走一遍全亮、清屏、定位、数字自检,再开始改功能,这个习惯帮我在课程设计答辩前少踩了很多坑。希望帮到你。
本文还有配套的精品资源,点击获取