简介:这份资源面向嵌入式开发初学者与LED显示爱好者,提供基于STM32F103的32×64像素P4表贴全彩LED屏完整程序资料,可解决汉字显示、彩色音乐频谱实时可视化与装饰灯效等开发需求。压缩包共480个文件、约15.17MB,包含75个c与60个h源文件、2个uvproj工程、2个hex与2个axf固件、2个pdf文档及若干ini配置、png图片与bat脚本,覆盖驱动、音频处理与烧录各环节。目前已有446人学习下载。资料涉及GPIO、定时器、中断、FFT频谱提取、RGB屏串并行控制与图形显示算法,并含DS1302、DHT11、农历、菜单等模块,便于读者直接编译烧录、对照源码理解频谱与汉字显示实现,快速搭建可运行的LED屏项目。
1. 从一块 32x64 的 P4 屏说起:这套 STM32F103 资料到底能跑出什么
手里有一块 32x64 点阵的 P4 表贴 LED 屏,想用 STM32F103 把它点起来,顺便做点频谱显示的效果,这是很多做灯光、做音频可视化、做小型信息屏的工程师都会碰到的事。P4 指的是像素间距 4mm,32x64 就是横向 64 颗灯、纵向 32 颗灯,单色或双色屏居多,扫描方式常见 1/16 扫。这套资料的核心价值在于:它把 STM32F103 驱动 P4 屏的完整工程、扫描时序、字库取模、以及频谱显示相关的处理都打包好了,拿到手不用从零推 GPIO 翻转时序。适合谁?适合已经会点 STM32 基础外设、但第一次接 LED 点阵屏、被 HUB75 接口和行选译码绕晕的人。也适合想拿它当音频频谱显示练手的人,因为 32x64 这个分辨率做柱状频谱刚好够看。下面我按“先搞懂屏怎么被扫、再动手把工程跑起来、最后把频谱接进去”的顺序拆。
2. P4 屏的扫描原理与 STM32F103 的引脚分配:先搞懂再接线
2.1 1/16 扫描到底在扫什么
P4 32x64 屏内部不是 2048 颗灯同时亮,而是分成 16 行一组轮流点亮。屏的接口通常是 HUB75 标准:R1/G1/B1、R2/G2/B2 两组数据线,A/B/C/D 四根行选地址线,加上 CLK、LAT、OE。1/16 扫描意味着 A/B/C/D 组合出 16 个行地址,每次只驱动一行,靠人眼余晖把 16 行拼成一幅完整画面。这里有个反直觉的点:你写代码时感觉是在“画一整屏”,实际底层是在以极高频率轮询 16 行,每行只亮 1/16 的时间。所以亮度不是靠电流拉满,而是靠刷新率和 OE 占空比调出来的。STM32F103 主频 72MHz,用定时器中断或者 DMA 刷屏都够,但如果你用软件延时去翻转 GPIO,刷新率会掉到几百赫兹,屏会肉眼可见地抖。常见做法是用 TIM 定时触发,把一行数据的移位和锁存放进中断里,保证每行时间片均匀。
2.2 引脚怎么分、为什么这么分
这套资料里 STM32F103 的引脚分配一般会避开 JTAG 占用的 PA13/PA14/PA15 和 PB3/PB4,因为那几个脚上电默认是调试口,直接当普通 GPIO 用会翻车。典型分配是:数据线走 PB0/PB1/PB2 或者 PA0~PA5,行选 A/B/C/D 走 PC0~PC3,CLK 和 LAT 走 PC4/PC5,OE 走 PC6。为什么把行选放一整组?因为行地址变化最频繁,同一端口写起来一条 BSRR 指令就能改,不用跨端口拼位。下面这段是行选和移位输出的核心逻辑,我按常见写法给你捋一遍:
// 行选地址:A/B/C/D 对应 PC0~PC3,直接操作 BSRR 避免读改写 static void select_row(uint8_t row) { GPIOC->BSRR = (0x0F << 16); // 先清 PC0~PC3 GPIOC->BSRR = (row & 0x0F); // 再写行号,row 范围 0~15 } // 移位输出一行 64 位数据,R1 单色为例 static void shift_row(const uint8_t *rowbuf) { for (int i = 0; i < 8; i++) { // 64 位 = 8 字节 uint8_t byte = rowbuf[i]; for (int b = 0; b < 8; b++) { if (byte & 0x80) GPIOB->BSRR = (1 << 0); // 数据位拉高 else GPIOB->BSRR = (1 << 16); // 数据位拉低 GPIOB->BSRR = (1 << 16) << 4; // CLK 拉低 GPIOB->BSRR = (1 << 4); // CLK 拉高,上升沿移入 byte <<= 1; } } GPIOB->BSRR = (1 << 16) << 5; // LAT 拉低 GPIOB->BSRR = (1 << 5); // LAT 拉高,锁存整行 }逻辑说明:select_row用 BSRR 的高 16 位清位、低 16 位写位,一次操作完成,不会影响 PC 口其他脚。shift_row里 CLK 是先低后高产生上升沿,LED 驱动芯片在上升沿采样数据线。参数上,rowbuf是当前行 64 个像素按位打包的 8 字节,高位在前还是低位在前取决于屏的驱动芯片型号,这个必须对着屏的规格书确认,搞反了整行镜像。OE 的占空比控制亮度,一般放在定时器中断里,一行扫完拉高 OE 一小段时间再拉低,具体时间靠示波器看波形调。
2.3 刷新率和定时器的关系
刷新率 = 行数 × 每行时间片的倒数。1/16 扫、要跑到 1000Hz 整屏刷新,每行只有 62.5 微秒。STM32F103 的 TIM3 挂在 APB1 上,72MHz 下预分频到 1MHz,每 62 微秒进一次中断刚好。资料里如果用的是 TIM3,注意它的中断优先级要设高一点,别被串口打印打断,否则屏上会出现一条横向亮线,这就是典型的“刷新被拖慢”现象。我一般会把刷屏中断设成抢占优先级 0,串口设 1,保证时序不被抢。
3. 把工程跑起来:Keil MDK 搭建、ST-Link 调试与串口打印
3.1 工程导入和编译前的三个检查点
拿到 zip 解压后,里面通常是 Keil MDK 工程(.uvprojx)。先别急着编译,按顺序检查三件事。第一,确认器件包。STM32F103 需要装 Keil.STM32F1xx_DFP,也就是常说的 pack 包,没装的话打开工程会提示 device not found。第二,确认启动文件和库版本,标准库工程和 HAL 工程的启动文件不一样,混用会报一堆重复定义。第三,确认 ST-Link 驱动装好,设备管理器里能看到 STLink 才算通。这三步任何一步漏了,编译或下载都会卡住,而且报错信息往往不指向真正原因,属于典型的黑匣子式翻车。
3.2 ST-Link 下载配置和常见连接问题
Keil 里 Options for Target → Debug 选 ST-Link Debugger,Settings 里 Port 选 SW,能读到 IDCODE 说明连接正常。如果读不到,先查三根线:SWDIO、SWCLK、GND,VCC 可以不接但 GND 必须共地。还有一种情况是程序里把 PA13/PA14 配成了普通 GPIO 或者关了 SWD,导致下次连不上,这时候要按住复位键点下载、松开复位,抢一个时间窗。这个血泪经验几乎每个用 F103 的人都踩过。下载成功后如果屏不亮,先量 OE 脚有没有波形,再量 CLK,逐级往后推,别一上来就怀疑程序。
3.3 串口打印辅助调试
调屏的时候串口打印是后悔药。把 USART1 的 TX 接 PA9、RX 接 PA10,波特率 115200,重定向 printf 到串口,就能在刷屏的同时打印行号、帧计数。注意刷屏中断里不要直接 printf,会拖慢时序,正确做法是在中断里置个标志,主循环里打印。下面这段是重定向和主循环打印的骨架:
// printf 重定向到 USART1 int fputc(int ch, FILE *f) { while (!(USART1->SR & (1 << 7))); // 等 TXE 置位 USART1->DR = (ch & 0xFF); return ch; } volatile uint32_t frame_cnt = 0; // 中断里自增的帧计数 int main(void) { uart_init(115200); led_init(); timer3_init(); // 启动刷屏定时器 while (1) { printf("frame=%lu\r\n", frame_cnt); // 主循环打印,不占中断 delay_ms(500); } }逻辑说明:fputc是标准库 printf 的底层输出函数,重定向后所有 printf 都走串口。frame_cnt用 volatile 修饰,因为它在中断里改、主循环里读,不加编译器可能优化掉。参数上,波特率 115200 是调试常用值,再高在长导线上容易出错。打印周期 500ms 是为了不淹没串口,实际调时序时可以临时改成 100ms 看帧率变化。
3.4 字库取模和显示一帧静态画面
点屏第一步不是频谱,是先显示一个静态字或者一条横线,验证扫描正确。字库用常见的取模软件生成,模式选“纵向取模、字节倒序”还是“横向取模”取决于你的扫描代码怎么读。32x64 屏显示一个 16x16 汉字,占 2 行 × 16 列,把取模数据按行填进显存数组,刷屏中断从显存读数据移位输出。如果显示出来是乱码或者上下颠倒,八成是取模方向和扫描方向没对齐,这个只能对着屏一点点试,没有捷径。
4. 频谱显示怎么接进来:从 ADC 采样到 32 列柱状图
4.1 音频采样和 FFT 的基本链路
频谱显示的本质是把音频信号做 FFT,取频域幅度映射到 32 列高度。STM32F103 没有 FPU,跑浮点 FFT 会吃力,常见做法是用定点 FFT,比如 64 点或 128 点,采样率 8kHz 左右,覆盖 0~4kHz 足够看音乐频谱。ADC 用 PA0 采音频,TIM 触发 ADC 以固定采样率转换,DMA 搬进缓冲区,攒够一帧做 FFT。这里的关键参数是采样率和点数:采样率决定频率上限,点数决定频率分辨率。64 点、8kHz 采样,分辨率 125Hz,32 列柱状图每列大约覆盖 2 个频点,够用。
4.2 定点 FFT 和幅度映射到 32 列
定点 FFT 常用的是 Q15 格式,输入范围 -32768~32767。做完 FFT 得到复数频谱,取模平方得到功率,再开方或者直接取对数映射到 0~32 的高度。为什么要取对数?因为人耳对响度的感知是对数的,线性映射会导致低频柱很高、高频几乎看不见。下面这段是幅度映射的核心:
// fft_out 是 FFT 后的复数数组,N=64,只取前 N/2 个正频率点 // 映射到 32 列高度,每列高度 0~32 void spectrum_to_columns(int16_t *fft_out, uint8_t *cols) { for (int c = 0; c < 32; c++) { // 每列取 1 个频点,64 点 FFT 正频率有 32 个 int16_t re = fft_out[2 * c]; int16_t im = fft_out[2 * c + 1]; uint32_t mag = (uint32_t)(re * re + im * im); // 功率 // 取对数近似:用前导零个数估算 log2,再缩放 uint8_t h = 0; while (mag > 1 && h < 32) { mag >>= 1; h++; } cols[c] = h; // 高度 0~32 } }逻辑说明:fft_out是交错存放的实部虚部,第 c 列对应第 c 个频点。mag是功率,用移位估算 log2 是为了避开浮点开方,F103 上跑得快。h就是柱高,直接写进显存对应列。参数上,如果发现柱子整体偏低,把h乘个增益系数再截断到 32;如果低频太挤,可以改成每列覆盖多个频点取最大值,视觉上更均匀。
4.3 把柱状图刷到屏上
32 列柱状图对应屏的 64 列,可以每列占 2 个像素宽,或者只占中间 32 列。柱子的高度从底部往上填,显存里对应位置置 1。刷新时和静态画面一样走扫描中断,只是显存内容每帧更新。注意 FFT 计算和刷屏不要在同一个中断里做,FFT 放主循环,算完更新显存,刷屏中断只管从显存读,这样时序才稳。
5. 避坑与排查:那些让屏不亮、闪烁、花屏的真实原因
5.1 屏完全不亮,OE 和行选都没波形
现象:上电后屏全黑,量 CLK 有波形但 OE 一直高。原因:OE 是高电平关闭,程序里初始化后没把它拉低,或者定时器中断根本没进。解决:先在 main 里手动把 OE 脚拉低,看屏是否常亮一行,确认硬件通路;再查 TIM3 中断是否使能、NVIC 是否配置。这一步能快速区分是硬件问题还是软件没跑起来。
5.2 显示内容上下颠倒或左右镜像
现象:字是反的,或者行顺序倒过来。原因:取模方向和扫描方向不一致,或者行选地址 A/B/C/D 的位序接反。解决:先改取模软件的“纵向/横向”“高位在前/低位在前”两个选项,四个组合试一遍;如果行顺序反了,把select_row里的 row 做15 - row反转。这类问题没有理论可推,就是对着试。
5.3 屏闪烁、有横向亮线
现象:画面整体抖,或者某一行特别亮。原因:刷屏中断被更高优先级或长耗时任务打断,导致某行停留时间过长。解决:把刷屏定时器中断优先级设到最高,中断里只做移位和行选,不做任何打印、浮点、FFT。串口打印、FFT 全部挪到主循环。这是最常见的翻车点,很多人把 FFT 写进中断,屏必闪。
5.4 亮度不均、半边暗
现象:屏左边亮右边暗,或者上下亮度不一样。原因:OE 占空比在整行时间内不均匀,或者数据移位太慢导致行末数据建立时间不够。解决:缩短每行移位时间,提高 CLK 频率;OE 的开启时间放在整行数据锁存之后、下一行切换之前,保证每行占空比一致。如果电源压降导致远端暗,那是供电问题,跟程序无关,要加粗电源线或就近加电容。
5.5 频谱柱子跳得厉害或者不动
现象:柱子乱跳,或者一直贴底。原因:ADC 采样没对齐、FFT 输入溢出、或者幅度映射阈值不对。解决:先把 ADC 采到的原始值通过串口打出来,确认有正常波形;再检查 FFT 输入是否做了 Q15 缩放,溢出会让结果全乱;最后调映射的增益和底噪门限,让静止时柱子贴底、有声音时能起来。
6. 进阶:把刷新率、频谱和资源占用压到极限的几个手法
调通之后,真正拉开差距的是细节。第一,刷屏用 DMA 加定时器,把移位输出交给 SPI 或者 GPIO 的 DMA 请求,CPU 只负责行选和 OE,这样主循环有更多时间跑 FFT。STM32F103 的 SPI1 在 72MHz 下能到 18MHz 时钟,移位 64 位只要几微秒,比软件循环快一个量级。第二,FFT 点数不要盲目加大,64 点够看频谱,128 点会让主循环占用明显上升,F103 的 RAM 也只有 20KB,显存加 FFT 缓冲要算着用。第三,亮度用 PWM 调 OE,而不是改限流电阻,PWM 频率要远高于刷新率,否则会看到闪烁。第四,验证刷新率最直接的办法是在刷屏中断里翻转一个空闲 IO,用示波器量频率,除以 16 就是整屏刷新率,我一般要求不低于 800Hz。第五,频谱的平滑处理,可以对每列高度做一阶滤波,新值 = 旧值 × 0.7 + 当前值 × 0.3,柱子会稳很多,这个系数根据你想要的下落速度调。下面这个表是我调这套屏时常用的参数起点,可以直接抄:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 整屏刷新率 | 800~1200Hz | 低于 600Hz 肉眼可见抖 |
| 每行时间片 | 50~80us | 1/16 扫,对应上面刷新率 |
| FFT 点数 | 64 | 兼顾分辨率和 RAM |
| 采样率 | 8kHz | 覆盖 0~4kHz |
| OE PWM 频率 | 10kHz 以上 | 避免可见闪烁 |
| 频谱平滑系数 | 0.3 | 越大下落越快 |
从那以后我每次接新屏,都强制先跑一个只显示一条横线的测试工程,确认扫描、行选、OE 全对,再往上叠字库和频谱。这个习惯帮我省了无数次“以为是程序问题、其实是接线反了”的来回折腾。希望帮到你。
本文还有配套的精品资源,点击获取