☰
STM32F103驱动P4表贴屏:从点灯到音频频谱显示实战
2026/10/27 17:10:05 网站建设 项目流程

简介:这份资源是面向嵌入式开发初学者与LED显示爱好者的STM32F103驱动P4表贴LED屏程序资料,屏幕分辨率为32x64像素,点间距4mm,适合中近距离观看。项目不仅支持汉字与静态文字显示,还实现了彩色音乐频谱实时可视化,可随音频变化动态刷新,兼具装饰与互动观赏价值。压缩包共480个文件,约15.17MB,包含75个c源文件、60个头文件、2个uvproj工程文件、2个hex固件及axf、map等编译产物,另有pdf文档、png图片与ini配置,覆盖驱动、音频处理与显示算法等模块。目前已有446人学习下载。通过该资料,读者可掌握STM32的GPIO、定时器与中断编程,理解RGB LED屏的串行或并行控制方式,学习音频频谱提取与FFT图形显示算法,并熟悉固件烧录与工程编译流程,是实践嵌入式显示与音频可视化的完整素材。

1. P4表贴屏 32x64 用 STM32F103 驱动:从点灯到频谱显示的那条最短路径

手里有一块 32x64 的 P4 表贴全彩单元板,又刚好有一片 STM32F103 最小系统板,很多人第一反应是「接上线就能亮」。真上手才发现,屏不亮、颜色错乱、扫一半黑屏、频谱跳得像抽风,全是坑。P4 表贴屏本质是 1/16 扫的 HUB75 接口屏,32x64 指的是 32 行、64 列像素,每行由 R1/G1/B1/R2/G2/B2 六路数据加 A/B/C/D 四根行选地址线驱动,靠 OE 做亮度调制、LAT 做数据锁存、CLK 做移位时钟。STM32F103 主频 72MHz,用定时器加 DMA 喂数据,跑 1/16 扫的刷新完全够用,再叠加 FFT 做频谱柱状显示也不吃力。这篇笔记面向的是手里已经有屏、有最小系统板、想把它跑起来并做成音频频谱的嵌入式玩家,从接线、工程搭建、扫描时序一直讲到频谱落地和踩坑排查,能照着复现。

2. 先搞懂 P4 屏的扫描时序,再谈 STM32F103 怎么喂数据

2.1 HUB75 接口的 16 根线到底在干什么

P4 表贴屏背面通常是 HUB75 标准的 16P 排针,引脚定义各家略有差异,但核心信号一致。数据线 6 根:R1、G1、B1 管上半屏,R2、G2、B2 管下半屏,这是 1/16 扫屏的典型结构——一次行选同时点亮上下两个半区的对应行。控制线 4 根:CLK 上升沿把数据移入屏上的移位寄存器,LAT 上升沿把移位寄存器内容锁存到输出锁存器,OE 低电平点亮、高电平熄灭,用来做灰度调制,A/B/C/D 是 4 位行地址,选 16 行中的一行。

理解这套时序的关键是:屏本身没有显存,它靠你不停地「送一行、锁存、点亮一小段时间、换下一行」循环刷新。人眼靠视觉暂留看到完整画面。所以 STM32F103 要做的不是「写一次显存」,而是以足够高的频率把整屏数据循环推出去。刷新率低于 400Hz 左右,手机拍照会有横纹,肉眼在快速移动时也会觉得闪。

2.2 为什么选定时器 + DMA 而不是纯 GPIO 翻转

新手最容易踩的坑是用HAL_GPIO_WritePin在 while 循环里一位一位翻转 CLK。这样做的后果是刷新率被函数调用开销拖到几百 Hz 以下,而且 CPU 被完全占死,根本没法同时跑 FFT。常见做法是用一个定时器产生 CLK,用 DMA 把一整行的数据从内存搬到 GPIO 的 BSRR 寄存器,硬件自动翻转,CPU 只在每行搬完后进一次中断换行地址。

STM32F103 的 TIM3 通道可以触发 DMA 请求,把 GPIOB 或 GPIOA 的 ODR/BSRR 作为 DMA 目标地址。每次 DMA 传输写一次 BSRR,就同时设置了 CLK、数据位和 LAT 的电平。这样一行 64 个像素 × 6 位颜色,加上控制位,一次 DMA 搬几百个字,硬件跑完再中断。CPU 占用率能压到 10% 以内,剩下的算力全留给频谱。

提示:STM32F103 的 GPIO 翻转速度在 72MHz 下约 18MHz 方波,P4 屏的 CLK 一般能吃到 10~20MHz,所以定时器分频后 CLK 设在 8~12MHz 比较稳,太快屏上的移位寄存器会丢数据。

2.3 行选与 OE 的配合:灰度是怎么调出来的

P4 屏的灰度不是靠数据线电压高低,而是靠 OE 在一个行周期内点亮的时间比例。比如一行周期 100us,OE 低电平占 30us,那这一行就是 30% 亮度。要实现 8 位灰度,就得把一行周期切成 256 份,按像素值决定 OE 什么时候拉高。纯软件做这个很吃 CPU,常见做法是 BCM(二进制编码调制):把一行时间分成 8 段,每段长度按 1:2:4:8… 递增,每段开始时送对应位的数据,段内 OE 常亮。这样 8 位灰度只需要 8 次数据搬运,DMA 完全扛得住。

STM32F103 的定时器可以输出 PWM 到 OE 脚,但更省事的做法是把 OE 也编进 DMA 数据流里,用 BSRR 同时控制。代价是 DMA 缓冲区变大,但对 32x64 这种小屏,SRAM 完全够。

3. 用 STM32CubeMX 搭一个能跑的 32x64 扫描工程

3.1 时钟、GPIO 和 TIM3 的最小配置

打开 CubeMX,选 STM32F103C8T6,先配时钟树:HSE 8MHz 晶振,PLL ×9 得到 72MHz 系统时钟,APB1 分频到 36MHz,APB2 保持 72MHz。GPIO 分配上,数据线和控制线尽量集中在同一个 GPIO 端口,方便 DMA 一次写 BSRR。我一般这样分:

信号引脚端口说明
R1PA0GPIOA上半屏红
G1PA1GPIOA上半屏绿
B1PA2GPIOA上半屏蓝
R2PA3GPIOA下半屏红
G2PA4GPIOA下半屏绿
B2PA5GPIOA下半屏蓝
CLKPA6GPIOA移位时钟
LATPA7GPIOA锁存
OEPA8GPIOA使能
APB0GPIOB行地址 bit0
BPB1GPIOB行地址 bit1
CPB2GPIOB行地址 bit2
DPB3GPIOB行地址 bit3

全部 GPIO 设为推挽输出、50MHz 速度。TIM3 配置为向上计数,预分频设为 0,自动重装值设为 5,这样更新频率 72MHz/(5+1)=12MHz,作为 CLK 的节拍。开启 TIM3 的更新 DMA 请求。

3.2 DMA 搬运 BSRR 的数据组织方式

DMA 的目标地址是&GPIOA->BSRR,源地址是我们自己拼的一行数据缓冲区。BSRR 低 16 位写 1 置位,高 16 位写 1 复位。所以每个像素的 6 位颜色要拆成两次写:先写需要置 1 的位,再写需要复位的位。为了省事,通常把 CLK 的上升沿也编进去:先写数据位 + CLK 低,再写数据位 + CLK 高,这样每个像素占 2 个 BSRR 字。

// 一行 64 像素,每个像素 2 个 BSRR 字,共 128 字 // 再加 LAT 和 OE 的控制字 #define ROW_BUF_WORDS (64 * 2 + 4) uint32_t row_buf[ROW_BUF_WORDS]; void build_row(uint8_t *pixel_rgb, uint16_t row_idx) { uint32_t *p = row_buf; for (int i = 0; i < 64; i++) { uint32_t set = 0, clr = 0; // 上半屏数据 if (pixel_rgb[i*3+0] & 0x80) set |= (1<<0); else clr |= (1<<0); if (pixel_rgb[i*3+1] & 0x80) set |= (1<<1); else clr |= (1<<1); if (pixel_rgb[i*3+2] & 0x80) set |= (1<<2); else clr |= (1<<2); // 下半屏数据 if (pixel_rgb[i*3+0] & 0x40) set |= (1<<3); else clr |= (1<<3); if (pixel_rgb[i*3+1] & 0x40) set |= (1<<4); else clr |= (1<<4); if (pixel_rgb[i*3+2] & 0x40) set |= (1<<5); else clr |= (1<<5); // CLK 低 + 数据 *p++ = set | (clr << 16); // CLK 高 + 数据 *p++ = set | (clr << 16) | (1<<6); } // LAT 脉冲 *p++ = (1<<7); *p++ = 0; // OE 使能 *p++ = 0; *p++ = (1<<8); }

这段代码的逻辑是:每个像素先输出数据位并保持 CLK 低,再输出同样的数据位并把 CLK 拉高,屏上的移位寄存器在 CLK 上升沿采样。64 个像素走完后,拉高 LAT 锁存,再拉低,然后拉低 OE 点亮这一行。参数上,pixel_rgb是当前行的 RGB 数据,row_idx用来设置 PB0~PB3 的行地址,实际工程里会在 DMA 完成中断里改行地址并重新 build 下一行。

注意:BSRR 的高 16 位是复位,低 16 位是置位,写 0 的位不变。所以set | (clr << 16)这种写法是安全的,不会误改其他引脚。

3.3 中断里换行与双缓冲

DMA 搬完一行会触发传输完成中断。在中断里做三件事:把行地址 A/B/C/D 更新到下一行,切换 DMA 源地址到另一块缓冲区(双缓冲),重新启动 DMA。双缓冲的意义是让 CPU 在 DMA 搬当前行的时候,去准备下一行的数据,避免撕裂。

void DMA1_Channel3_IRQHandler(void) { if (DMA1->ISR & DMA_ISR_TCIF3) { DMA1->IFCR = DMA_IFCR_CTCIF3; static uint8_t row = 0; row = (row + 1) & 0x0F; // 更新行地址 PB0~PB3 GPIOB->BSRR = (0x0F << 16) | row; // 切换缓冲区 DMA1_Channel3->CMAR = (uint32_t)(row & 1 ? buf_b : buf_a); DMA1_Channel3->CNDTR = ROW_BUF_WORDS; DMA1_Channel3->CCR |= DMA_CCR_EN; } }

这里row从 0 到 15 循环,对应 16 行。每次中断换一行,整屏刷新一次需要 16 次中断。如果一行 DMA 耗时 10us,整屏刷新约 160us,刷新率 6kHz 以上,远超肉眼需求。参数上CNDTR必须重装,否则 DMA 只搬一次就停。

4. 从点灯到频谱:FFT 在 STM32F103 上的落地细节

4.1 采样、ADC 与 1024 点 FFT 的资源账

频谱显示的第一步是把音频信号采进来。用 STM32F103 的 ADC1 通道 0(PA0 被占的话换 PA1),配合 TIM2 触发,采样率设 8kHz 左右,采 1024 点做 FFT,频率分辨率约 7.8Hz,覆盖 0~4kHz,正好是人声和音乐的主要能量区。1024 点复数 FFT 用 CMSIS-DSP 的arm_cfft_f32,需要 1024×2 个 float 的输入缓冲和同样大小的输出,共 8KB,STM32F103C8T6 有 20KB SRAM,扣掉屏幕 DMA 缓冲和栈,勉强够。

如果 SRAM 紧张,可以降到 512 点,分辨率 15.6Hz,频谱柱少一半但也能看。常见做法是采样用 DMA 循环模式,ADC 采满 1024 点触发中断,在中断里置标志,主循环做 FFT 和柱状图映射。

#define FFT_SIZE 1024 float fft_in[FFT_SIZE * 2]; float fft_mag[FFT_SIZE / 2]; void do_fft(void) { // 加汉宁窗,减少频谱泄漏 for (int i = 0; i < FFT_SIZE; i++) { float w = 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); fft_in[i*2] = adc_buf[i] * w; fft_in[i*2+1] = 0.0f; } arm_cfft_f32(&arm_cfft_sR_f32_len1024, fft_in, 0, 1); arm_cmplx_mag_f32(fft_in, fft_mag, FFT_SIZE / 2); }

arm_cfft_f32的第三个参数 0 表示不逆变换,第四个参数 1 表示按位反转。arm_cmplx_mag_f32算复数模值,得到 512 个频点的幅度。加汉宁窗是为了压旁瓣,不加的话低频强信号会把邻近频点抬起来,频谱柱看起来糊成一片。

4.2 把 512 个频点映射成 32 列柱状图

32x64 的屏,竖着放可以显示 32 列、每列 64 像素高。把 512 个频点按对数刻度合并成 32 组,每组取最大值或平均值,再映射到 0~63 的高度。对数刻度更符合人耳听感,低频分得细、高频分得粗。

void map_spectrum(float *mag, uint8_t *bars) { for (int b = 0; b < 32; b++) { // 对数分 bin:第 b 组覆盖 [f_lo, f_hi) int lo = (int)(powf(2.0f, b / 4.0f)); int hi = (int)(powf(2.0f, (b + 1) / 4.0f)); if (hi <= lo) hi = lo + 1; float peak = 0; for (int i = lo; i < hi && i < FFT_SIZE/2; i++) if (mag[i] > peak) peak = mag[i]; // 转 dB 并归一化到 0~63 float db = 20.0f * log10f(peak + 1e-6f); int h = (int)((db + 60.0f) * 63.0f / 60.0f); bars[b] = h < 0 ? 0 : (h > 63 ? 63 : h); } }

powf(2.0f, b/4.0f)让每 4 个柱频率翻倍,32 个柱覆盖约 8 个倍频程。db + 60是把 -60dB 到 0dB 映射到 0~63 像素高。实际调试时这个 60 要按你的麦克风增益改,增益小就改成 40,否则柱子永远趴着。

4.3 柱状图渲染与屏幕刷新率的平衡

频谱柱画到显存后,屏幕扫描线程只管从显存读数据。显存用uint8_t fb[32][64][3]太大,32x64x3=6KB,可以接受。渲染时每列从底部往上填 bars[b] 个像素,颜色按高度做渐变,低绿高红。注意渲染和扫描是两个上下文,渲染在主循环、扫描在 DMA 中断,操作显存时要么关中断要么用双缓冲,否则会看到柱子撕裂。

提示:如果发现频谱柱跳动不跟手,先查 ADC 采样是不是被屏幕 DMA 中断打断导致丢点。常见做法是把 ADC 的 DMA 优先级设得比屏幕 DMA 高,或者屏幕 DMA 用定时器触发而不是软件触发。

5. 调 P4 屏最容易翻车的几个地方

5.1 上电全亮或全暗,一行都不扫

现象:接好线通电,屏要么全亮白屏,要么全黑,行选完全没反应。原因通常是 OE 极性搞反了。有些 P4 单元板的 OE 是低有效,有些驱动芯片(比如某些带 FB/CS 脚的恒流驱动)是高有效,板子上如果带了反相器就更乱。解决:先用万用表量 OE 脚静态电平,再在代码里把 OE 的置位/复位反过来试。另一个原因是行地址 A/B/C/D 顺序接错,16 行只扫了 4 行,看起来就是半屏黑。

5.2 颜色错乱,红绿蓝对不上

现象:显示纯红却出来绿色,或者上下半屏颜色互换。原因是 R1/G1/B1 和 R2/G2/B2 的引脚映射和屏的排针定义不一致。P4 表贴屏的 HUB75 排针没有强制标准,不同厂家 R1 和 G1 可能对调。解决:写一个测试模式,依次只点亮 R1、G1、B1、R2、G2、B2,看屏上哪个颜色亮,反推映射关系,改代码里的位定义。血泪经验是别信卖家给的接线图,一定自己测一遍。

5.3 刷新率上不去,拍照有横纹

现象:肉眼看着还行,手机一拍全是横条纹,或者快速移动的画面有拖影。原因是整屏刷新率低于 1kHz。解决:检查 DMA 一行搬运的字数是不是太多,BSRR 双字写法可以省一半;检查中断里有没有浮点运算或printf,这些会拖慢换行;把 TIM3 的 ARR 调小提高 CLK,但别超过屏上移位芯片的上限。一般 32x64 屏刷新率做到 2kHz 以上,拍照就干净了。

5.4 频谱柱不动或乱跳

现象:放音乐柱子不动,或者无音乐时柱子乱跳。原因一是 ADC 参考电压不稳,STM32F103 的 VDDA 如果和数字电源共用且没加滤波,采样噪声很大。解决:VDDA 加 100nF + 10uF 退耦,麦克风模块单独供电。原因二是 FFT 输入没加窗或没去直流,直流分量会把第一个频点顶满。解决:采样后先减去均值再加窗。原因三是映射的 dB 范围不对,改db + 60里的 60。

5.5 DMA 和 FFT 抢 SRAM 导致死机

现象:跑一会儿就 HardFault,或者屏幕突然花屏。原因是 FFT 的 8KB 缓冲和屏幕 DMA 缓冲、栈叠在一起,SRAM 溢出踩了关键变量。解决:用 CubeMX 看内存分布,把大数组用__attribute__((section(".ccmram")))放 CCM(如果芯片有),或者把 FFT 降到 512 点。STM32F103C8T6 没有 CCM,只能精简缓冲,比如 FFT 输入用float但输出幅度用uint16_t存。

6. 让频谱更好看的两个进阶技巧:峰值保持与对数坐标

柱子能动了之后,下一步是让它像专业频谱仪。第一个技巧是峰值保持:每个柱维护一个peak_hold[b],当前高度超过它就更新,否则每帧衰减 1,这样瞬间的鼓点会留一道细线,视觉上更有节奏感。实现上就是在map_spectrum之后加一段:

static uint8_t peak[32]; for (int b = 0; b < 32; b++) { if (bars[b] > peak[b]) peak[b] = bars[b]; else if (peak[b] > 0) peak[b]--; // 渲染时在 peak[b] 高度画一个白点 }

第二个技巧是纵轴对数化。现在的高度是线性映射 dB,低频小信号和高频大信号高度差很大,看起来低频柱总是很矮。可以把h再开一次方,或者直接对 dB 做分段映射,让 0~20dB 占一半高度。我一般用h = (int)(sqrtf((db + 60) / 60.0f) * 63),低频柱会明显抬起来,整体更饱满。

这两个改动都不吃多少 CPU,但观感提升很大。我自己调这块屏的时候,前前后后烧了十几版固件,最深的教训是:别一上来就追求 1024 点 FFT 加 8 位灰度,先把 16 行扫稳、颜色对正、刷新率够,再往上叠频谱。屏幕时序是地基,地基歪了,上面算法再漂亮也是花屏。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询