资讯详情

资讯详情

行业资讯

STM32F103驱动P4全彩屏:从扫描原理到音频频谱显示实战

简介这份资源是面向嵌入式开发初学者与LED显示爱好者的STM32F103驱动P4表贴LED屏程序资料屏幕分辨率为32x64像素点间距4mm适合中近距离观看。项目不仅支持汉字等静态文字显示还实现了彩色音乐频谱实时可视化与呼吸灯等装饰效果可用于桌面摆件、氛围灯或嵌入式练手项目。压缩包共480个文件约15.17MB包含75个c与60个h源码文件、2个uvproj工程文件、2个hex与2个axf固件、若干pdf文档及png图片等覆盖驱动代码、编译产物与说明资料。目前已有446人学习下载。通过该资料可掌握STM32的GPIO、定时器与中断配置理解RGB LED屏的串行驱动方式并学习音频采样、FFT频谱提取与图形显示算法是练习硬件驱动与实时系统应用的完整素材。1. P4表贴屏 32x64 用 STM32F103 驱动从接线到频谱显示的完整路径手里有一块 32x64 的 P4 表贴全彩 LED 屏想用 STM32F103 把它点起来再做一个音频频谱显示——这个需求在实验室、创客空间和小型广告屏项目里非常常见。P4 指的是像素间距 4mm32x64 表示屏体分辨率是 32 行、64 列整屏 2048 个 RGB 像素点。它通常采用 1/16 扫描方式需要行选信号、列数据信号和锁存/使能信号协同工作接口时序比常见的 08 接口单色屏要复杂不少。STM32F103 作为驱动主控优势在于主频 72MHz、外设丰富、资料多、成本低但它的 IO 翻转速度和 RAM 容量决定了你不能用「暴力刷屏」的思路去写代码。这篇文章面向已经拿到屏和核心板、准备动手的工程师把扫描原理、行选译码、数据搬运、频谱采集和常见翻车点一次讲透让你少走弯路。2. P4 屏的扫描原理与 STM32F103 的选型账2.1 1/16 扫描到底在扫什么P4 全彩屏的 32 行并不是同时点亮的。驱动板把 32 行分成 16 组每组两行上下半屏各一行同时被选中依次快速轮询。人眼有视觉暂留只要整屏刷新率高于 60Hz看起来就是一幅稳定画面。这意味着每一行的点亮时间只有整个刷新周期的 1/16要保证亮度就得在行选有效期间把该行的 RGB 数据全部串行移入列驱动芯片然后锁存输出。列数据接口通常是 RGB 各一组串行数据线配合时钟、锁存和使能。以常见的 32x64 P4 屏为例它内部用了多片 74HC595 或专用恒流驱动芯片做列驱动行选则通过 74HC138 这类 3-8 译码器扩展。STM32F103 要做的就是产生行选地址、在行选稳定后把该行 64 列 x 3 色的数据按位移出、给锁存脉冲、再切下一行。整个过程靠定时器中断驱动中断频率 刷新率 x 16。2.2 为什么选 STM32F103 而不是 51 或 ESP3251 单片机主频低、RAM 小驱动 32x64 全彩屏时刷屏率很难做高频谱运算更跑不动。ESP32 性能强但如果你手头只有 STM32F103 最小系统板且项目对成本敏感、不需要联网F103 是更务实的选择。它的 72MHz 主频配合 DMA 和定时器足以把 32x64 屏刷到 200Hz 以上同时留出 CPU 时间做 ADC 采样和 FFT。选型时注意三点第一RAM 要够。32x64 全彩屏如果做双缓冲每个缓冲需要 32 x 64 x 3 字节 6144 字节两个就是 12KB。STM32F103C8T6 只有 20KB RAM做双缓冲后剩给 FFT 和栈的空间很紧张建议用 C8T6 时只做单缓冲加局部刷新或者换 RCT6。第二IO 数量要够。行选 4 根、RGB 数据 3 根、时钟 1 根、锁存 1 根、使能 1 根至少 10 根再加上音频采样和串口打印C8T6 的 37 个 IO 够用但需要规划。第三供电要独立。P4 屏全亮时电流可能到 2A 以上绝不能从 STM32 的 3.3V 取电屏体要用 5V 独立电源且 STM32 的 IO 与屏的 5V 逻辑之间要做电平匹配。2.3 行选译码与 IO 分配表行选信号一般由 74HC138 译码器产生STM32 只需要输出 4 位地址A、B、C、D和使能。下面是一份我常用的 IO 分配基于 STM32F103C8T6 最小系统板信号STM32 引脚方向说明R1PA0输出上半屏红色数据G1PA1输出上半屏绿色数据B1PA2输出上半屏蓝色数据R2PA3输出下半屏红色数据G2PA4输出下半屏绿色数据B2PA5输出下半屏蓝色数据CLKPA6输出列数据时钟LATPA7输出锁存脉冲OEPA8输出使能低有效APB0输出行选地址 bit0BPB1输出行选地址 bit1CPB2输出行选地址 bit2DPB3输出行选地址 bit3ADCPA9模拟输入音频采样这个分配把数据口集中在 PA 口方便用 BSRR 寄存器一次性写多位减少 IO 翻转时间。行选地址用 PB0-PB3也是连续的四位可以直接用位带操作或 BSRR 更新。3. 用定时器中断加 DMA 把屏刷起来3.1 定时器配置刷新率与中断频率的换算假设你要 100Hz 刷新率1/16 扫描意味着每行切换频率是 100 x 16 1600Hz定时器中断周期就是 625 微秒。STM32F103 的 TIM3 挂在 APB1 上时钟 72MHz预分频设为 71计数时钟就是 1MHz自动重装载值设为 624中断周期正好 625 微秒。// TIM3 初始化中断频率 1600Hz void TIM3_Init(void) { RCC-APB1ENR | RCC_APB1ENR_TIM3EN; // 使能 TIM3 时钟 TIM3-PSC 71; // 72MHz / (711) 1MHz TIM3-ARR 624; // 1MHz / (6241) 1600Hz TIM3-DIER | TIM_DIER_UIE; // 使能更新中断 TIM3-CR1 | TIM_CR1_CEN; // 启动定时器 NVIC_EnableIRQ(TIM3_IRQn); // 使能 NVIC NVIC_SetPriority(TIM3_IRQn, 1); // 优先级设高保证刷新稳定 }参数说明PSC 和 ARR 共同决定中断频率公式是 72MHz / ((PSC1) x (ARR1))。如果你把刷新率改成 200HzARR 要改成 312。注意 ARR 不能小于某个下限否则中断太频繁CPU 来不及处理反而导致画面撕裂。3.2 中断服务函数行选、移数据、锁存中断里要做的事很固定关使能、切行选、移出该行数据、锁存、开使能。顺序不能乱否则会出现鬼影。volatile uint8_t row 0; extern uint8_t framebuffer[32][64][3]; // 显存RGB 各一字节 void TIM3_IRQHandler(void) { if (TIM3-SR TIM_SR_UIF) { TIM3-SR ~TIM_SR_UIF; // 清中断标志 GPIOA-BSRR (1 8); // OE 拉高关显示防止切换时花屏 GPIOB-BRR 0x000F; // 清行选地址低四位 GPIOB-BSRR (row 0x0F); // 写新行选地址 // 移出该行 64 列 x 2上下半屏x 3 色数据 for (int col 0; col 64; col) { uint8_t r1 framebuffer[row][col][0]; uint8_t g1 framebuffer[row][col][1]; uint8_t b1 framebuffer[row][col][2]; uint8_t r2 framebuffer[row 16][col][0]; uint8_t g2 framebuffer[row 16][col][1]; uint8_t b2 framebuffer[row 16][col][2]; // 按位输出先准备数据再给时钟上升沿 GPIOA-BRR (1 0) | (1 1) | (1 2) | (1 3) | (1 4) | (1 5); GPIOA-BSRR (r1 0x80 ? (1 0) : 0) | (g1 0x80 ? (1 1) : 0) | (b1 0x80 ? (1 2) : 0) | (r2 0x80 ? (1 3) : 0) | (g2 0x80 ? (1 4) : 0) | (b2 0x80 ? (1 5) : 0); GPIOA-BSRR (1 6); // CLK 上升沿 GPIOA-BRR (1 6); // CLK 下降沿 r1 1; g1 1; b1 1; r2 1; g2 1; b2 1; } GPIOA-BSRR (1 7); // LAT 锁存 GPIOA-BRR (1 7); GPIOA-BRR (1 8); // OE 拉低开显示 row (row 1) 0x0F; // 0-15 循环 } }这段代码的逻辑是先关显示避免切换行时出现残影然后更新行选地址接着把该行对应的上下半屏数据逐位移出。注意 framebuffer 的索引row 取 0-15下半屏对应 row16。移完 64 列后给锁存脉冲再开显示。整个中断执行时间要控制在 625 微秒以内否则会丢帧。如果发现刷屏闪烁优先检查中断里是不是做了浮点运算或调用了耗时函数。3.3 显存布局与局部刷新上面的 framebuffer 是 [32][64][3] 的三维数组按行优先存储。如果你要做频谱显示只需要更新下半屏或特定区域可以只改对应行不必整屏重写。局部刷新的关键是中断里始终从 framebuffer 读数据主循环只改 framebuffer 内容两者通过「行边界」自然同步不需要加锁。提示framebuffer 定义时加 volatile 不是必须的但如果你在中断和主循环里都访问它建议用双缓冲或至少保证主循环改数据时不会改到正在被中断读取的那一行。4. 音频采样与频谱计算的落地细节4.1 ADC 采样定时器触发加 DMA 搬运频谱显示的第一步是拿到音频波形。用 STM32F103 的 ADC1 通道 0PA0采样但 PA0 已经被 R1 占了所以实际项目里要换一个空闲引脚比如 PA9 对应 ADC1 通道 2。采样率建议 8kHz 到 16kHz做 64 点或 128 点 FFT 足够显示频谱。// ADC1 DMA1 通道1 初始化采样率 8kHz #define FFT_SIZE 128 uint16_t adc_buf[FFT_SIZE]; void ADC_DMA_Init(void) { RCC-APB2ENR | RCC_APB2ENR_ADC1EN | RCC_APB2ENR_IOPAEN; RCC-AHBENR | RCC_AHBENR_DMA1EN; // PA9 模拟输入 GPIOA-CRH ~(0xF 4); GPIOA-CRH | (0x0 4); // 模拟输入模式 DMA1_Channel1-CPAR (uint32_t)ADC1-DR; DMA1_Channel1-CMAR (uint32_t)adc_buf; DMA1_Channel1-CNDTR FFT_SIZE; DMA1_Channel1-CCR DMA_CCR_MINC | DMA_CCR_TCIE | DMA_CCR_EN; ADC1-CR2 | ADC_CR2_ADON | ADC_CR2_CONT | ADC_CR2_DMA; ADC1-SQR3 2; // 通道 2 ADC1-SMPR2 7 0; // 采样时间 239.5 周期 ADC1-CR2 | ADC_CR2_ADON; // 再次置位启动 }参数说明FFT_SIZE 决定频率分辨率8kHz 采样率下 128 点 FFT 的分辨率是 62.5Hz。如果你要显示低频鼓点可以降到 4kHz 采样率但高频会丢失。DMA 循环模式可以连续采样但做 FFT 时通常用正常模式采完一帧处理一帧。4.2 用 ARM 的 CMSIS-DSP 库做 FFTSTM32F103 没有硬件浮点但 CMSIS-DSP 提供了定点 Q15 的 FFT 函数速度足够。你需要把 ADC 的 12 位无符号数转成 Q15 格式然后调用 arm_cfft_q15。#include arm_math.h q15_t fft_in[FFT_SIZE * 2]; // 实部虚部交错 q15_t fft_out[FFT_SIZE]; uint8_t spectrum[32]; // 最终显示的 32 列高度 void Process_FFT(void) { // ADC 值转 Q15并加汉宁窗减少频谱泄漏 for (int i 0; i FFT_SIZE; i) { q15_t sample (q15_t)((adc_buf[i] - 2048) 4); fft_in[2 * i] sample; fft_in[2 * i 1] 0; } arm_cfft_instance_q15 cfft; arm_cfft_init_q15(cfft, FFT_SIZE); arm_cfft_q15(cfft, fft_in, 0, 1); // 计算幅值 for (int i 0; i FFT_SIZE / 2; i) { q15_t re fft_in[2 * i]; q15_t im fft_in[2 * i 1]; fft_out[i] (q15_t)sqrtf((float)(re * re im * im)); } // 把 64 个频点映射到 32 列显示 for (int col 0; col 32; col) { uint32_t sum 0; for (int k 0; k 2; k) { sum fft_out[col * 2 k]; } uint32_t level sum / 2; // 对数压缩否则低频柱太高 level (uint32_t)(logf((float)level 1) * 8); if (level 31) level 31; spectrum[col] (uint8_t)level; } }逻辑说明arm_cfft_q15 的输入是实部虚部交错数组长度是 2 x FFT_SIZE。变换后同样格式输出取前一半频点计算幅值。因为人耳对声压的感知是对数的所以要对幅值做对数压缩否则低频能量大会把高频压得看不见。最后把 64 个频点合并成 32 列每列取两个频点的平均。4.3 把频谱画到屏上拿到 spectrum[32] 后只需要更新 framebuffer 的下半屏或整屏。画柱状图时每一列从底部往上点亮对应高度的像素。void Draw_Spectrum(void) { for (int col 0; col 32; col) { uint8_t h spectrum[col]; for (int row 0; row 32; row) { uint8_t on (row (32 - h)) ? 1 : 0; // 上半屏和下半屏都画形成对称效果 framebuffer[row][col * 2][0] on ? 0xFF : 0x00; framebuffer[row][col * 2][1] on ? 0x00 : 0x00; framebuffer[row][col * 2][2] on ? 0x00 : 0x00; framebuffer[row][col * 2 1][0] on ? 0xFF : 0x00; framebuffer[row][col * 2 1][1] on ? 0x00 : 0x00; framebuffer[row][col * 2 1][2] on ? 0x00 : 0x00; } } }这里每列占两个像素宽度32 列正好铺满 64 列屏。颜色只用了红色你可以改成按高度渐变低频红、高频蓝视觉效果更好。5. 调试 P4 屏时最容易翻车的五个地方5.1 现象上电后整屏微亮显示内容全无原因OE 使能极性搞反了。很多 P4 屏的 OE 是低有效但有些驱动板加了反相器变成高有效。如果你按低有效写代码实际输出一直是关显示或一直开显示。解决先用万用表量 OE 引脚在复位后的默认电平再对照屏体手册确认极性。如果不确定写一个最简单的测试只让 OE 翻转看屏有没有反应。5.2 现象画面有鬼影切换行时上一行残留原因行选切换和锁存顺序不对。如果在 OE 还开着的时候切行选旧数据会短暂地打到新行上。解决严格按「关 OE - 切行选 - 移数据 - 锁存 - 开 OE」的顺序。另外检查 74HC138 的使能脚是不是一直有效如果使能脚有毛刺也会导致行选错乱。5.3 现象刷新率上不去画面闪烁原因中断服务函数里做了浮点运算或调用了 arm_cfft 这类耗时函数。FFT 必须放在主循环里中断只做刷屏。解决把 FFT 和频谱计算全部移到 main 的 while(1) 里中断只负责从 framebuffer 搬数据。如果还慢检查 framebuffer 是不是放在外部 SRAM 里F103 没有外部总线的话只能放内部 RAM。5.4 现象颜色不对红色显示成绿色原因RGB 数据线的顺序和屏体不一致。不同厂家的 P4 屏R1/G1/B1 的排列可能不同有的甚至是 BGR 顺序。解决写一个纯色测试依次只点亮 R、G、B观察实际颜色。如果不对在移数据时交换对应的位。这个坑没有捷径只能试。5.5 现象频谱柱状图跳动剧烈无法稳定显示原因ADC 采样没有加窗或者 FFT 点数太少导致频率分辨率不够。另外如果音频输入没有隔直电容直流分量会淹没交流信号。解决在 ADC 输入前加一个 10uF 隔直电容和一个 10k 偏置电阻把信号偏置到 1.65V。FFT 前加汉宁窗代码里用 arm_hanning_q15 或者手动乘窗函数。如果还跳把 FFT_SIZE 从 128 加到 256但要注意 RAM 够不够。6. 把频谱显示做成产品级效果的三个进阶技巧6.1 用峰值保持和衰减做视觉暂留直接画实时频谱柱子会跳得很快看起来像噪声。产品级的频谱显示通常加一个峰值保持每个频点维护一个峰值变量新值大于峰值时立即更新否则峰值按固定速率衰减。uint8_t peak[32] {0}; void Update_Peak(void) { for (int i 0; i 32; i) { if (spectrum[i] peak[i]) { peak[i] spectrum[i]; } else if (peak[i] 0) { peak[i]--; // 每帧衰减 1 像素 } } }然后在 Draw_Spectrum 里把 peak[i] 对应的那一行画成白色或另一种颜色就能看到一条慢慢下落的峰值线。衰减速度取决于你的主循环帧率8kHz 采样 128 点 FFT 的话一帧大约 16ms衰减 1 像素就是每秒 60 像素视觉上比较舒服。6.2 用 DMA 刷屏解放 CPU上面的中断刷屏方案CPU 在中断里要执行 64 次循环每次循环有多次 IO 操作占用不少时间。如果 CPU 还要做 FFT 和串口打印可能会吃力。进阶做法是用定时器触发 DMA把 framebuffer 的数据直接搬到 GPIO 的 BSRR 寄存器。但 STM32F103 的 DMA 不能直接产生带时钟的串行时序所以更实际的做法是用 SPI 加 DMA。把 RGB 数据当成 SPI 的 MOSICLK 用 SPI 的 SCK这样一次 DMA 传输就能移出整行数据CPU 只需要在传输完成中断里切行选和锁存。具体配置SPI1 设置为主机、8 位、MSB 先出波特率预分频到 18MHz 左右。DMA1 通道 3 负责 SPI1 发送。每行数据在内存里按 R/G/B 交错排列DMA 一次发 64 x 3 字节。传输完成中断里给 LAT 脉冲、切行选、重新装填 DMA。这个方案能把 CPU 占用降到 10% 以下但代码复杂度高不少建议先把中断方案跑通再升级。6.3 用串口打印调试频谱数据调频谱时光看屏上的柱子很难判断 FFT 算得对不对。我习惯用串口把前 16 个频点的幅值打印出来在电脑上看数值变化。// 在 main 循环里每 500ms 打印一次 if (HAL_GetTick() - last_print 500) { last_print HAL_GetTick(); printf(FFT:); for (int i 0; i 16; i) { printf( %d, fft_out[i]); } printf(\r\n); }注意 printf 要重定向到 USART1并且不要在中断里调用。如果打印出来的数值全是 0 或全是最大值检查 ADC 的参考电压和输入偏置。如果数值随声音变化但频谱柱不动检查频点映射是不是把能量都算到了同一个柱子里。6.4 一个我踩过的坑电源地线环路最后说一个血泪经验。P4 屏的 5V 电源和 STM32 的 3.3V 电源如果分别用两个适配器而两个适配器的地没有共在一起会出现随机花屏和复位。原因是地电位差导致逻辑电平误判。解决方法是屏的 5V 电源和 STM32 的电源共地最好用同一个电源加降压模块。如果必须分开供电把两个电源的地线用粗线连在一起并且 STM32 的 IO 和屏之间加 74HC245 做电平隔离。这个坑我调了两天才定位到希望帮到你。本文还有配套的精品资源点击获取

准备好开启您的数字化征程了吗?

专业团队为您量身定制,让您的企业网站脱颖而出

立即咨询