ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32图像显示系统:SPI/FSMC选型与DMA刷屏实战

STM32图像显示系统:SPI/FSMC选型与DMA刷屏实战 简介这份资源是一份面向电子信息、自动化及嵌入式方向学生与初学者的课程设计报告主题为基于STM32的图像显示系统适合正在学习单片机、TFT-LCD驱动与嵌入式文件系统的读者作为参考方案。压缩包共1个文件为1份PDF文档整体约1.84MB内容以完整课设报告形式呈现涵盖摘要、目录、引言、总体设计、详细设计、实验结果与分析、结论及参考文献等章节。报告从图片显示基本原理切入说明LCD作为现代信息发布媒体的应用背景继而展开硬件设计与软件设计两大模块硬件部分涉及ALIENTEK MiniSTM32开发板、TFT-LCD液晶显示屏与存储卡的选型与功能软件部分拆解主函数、硬件驱动程序、图片识别以及FAT文件系统等实现环节并给出程序流程图与系统结构框图。读者可据此了解图片显示、更换、自定义与按键切换、定时跳转等功能的实现思路掌握图片存入内存卡后更新显示的操作逻辑。目前该资源已有197人学习适合作为课设写作模板与嵌入式显示入门的实践参考。1. 一块 2.4 寸屏接上 STM32 之后真正卡住你的往往不是驱动很多人拿到「基于 STM32 的图像显示系统」这个题目第一反应是买一块最便宜的 SPI TFT插上排线找一份现成驱动然后发现屏幕要么白屏要么花屏要么能显示但刷一屏要一两百毫秒动画像幻灯片。问题通常不在初始化序列抄错了而在选型阶段没有算过带宽240×320 的 RGB565 一帧就是 153600 字节STM32F103 的 SPI1 跑满 18MHz光搬数据就要约 68ms也就是 14fps 封顶。这篇内容按「算带宽选接口 → 搭工程 → 跑通最小刷屏 → 接上真实图像数据 → 提帧率与排错」的顺序展开代码和参数都能直接落到你的工程里。适合正在做 stm32项目 的学生也适合要把图像界面塞进小体积设备的工程师。2. STM32 图像显示系统的接口选型与硬件链路选屏这件事参数表上的分辨率和尺寸是最不重要的两项先要确定的是数据怎么过去。图像显示系统里屏幕是唯一的耗带宽外设接口定错后面所有优化都是在给一个错误的前提打补丁。2.1 SPI、8080 并口与 FSMC先把一帧的账算清楚一帧字节数的算法很固定宽 × 高 × 每像素字节数。RGB565 每像素 2 字节240×320 就是 153600 字节。把这个数除以接口的有效吞吐得到的就是理论满屏时间。接口方式典型引脚数时钟或写周期240×320 RGB565 一帧耗时等效帧率SPI 单线 18MHz4~618 MHz约 68 ms约 14 fpsSPI 单线 40MHz4~640 MHz约 31 ms约 32 fpsFSMC 8080 并口模式 A约 20写周期 100 ns约 15 ms约 60 fpsGPIO 模拟 8080 并口约 20受循环开销限制50 ms 以上20 fps 以下STM32F1 的 SPI1 挂 APB272MHz 分频后可用 18MHzF4/F7/H7 的分频更细SPI 能上到 40MHz 以上。FSMC 并口模式 A 的写周期由 ADDSET 和 DATAST 两个时序参数决定F1 的 FSMC 挂在 HCLK 上周期 13.9ns 量级配出 100ns 左右的写周期是常规做法。提示屏的规格书写的是「SPI 最高 50MHz」那是驱动 IC 的接收能力不代表你的 MCU 能发那么快也不代表排线能跑那么快。长排线加高时钟先出问题的是信号完整性表现为偶发花屏而不是完全不亮。选型结论很直接静态图片、参数配置页、字符界面SPI 足够要播放动画、波形、摄像头预览直接上 FSMC 并口屏或者用带 LTDC 的型号配 RGB 屏。介于两者之间的做法是 SPI 屏幕只做局部刷新不动全屏。2.2 ILI9341、ST7789、ST7735 三种常见驱动 IC 的接线差异模块的驱动 IC 决定了初始化序列、坐标方向和颜色格式先认芯片再写代码。驱动 IC常见分辨率常用接口像素格式备注ILI9341240×320SPI / 8080 并口RGB565资料最全两种接口版本都买得到ST7789V240×240、240×320SPIRGB565小尺寸圆角屏主流偏移量需按模块设ST7735S128×160SPIRGB565引脚少适合窄体设备SSD1963480×272 及以上8080 并口RGB565大屏基本不用 SPISPI 版本的最小接线是 SCK、MOSI、CS、DC、RST 五根背光 BL 单独一路MISO 只在读显存或读 ID 时需要。DC 决定这次传的是命令还是数据是整个驱动里时序最容易出错的一根线。并口版本多出 D0~D15、WR、RD、RS接 FSMC 时 RS 接地址线 A0 或 A16。引脚分配上有两个坑要提前避开。第一PA13/PA14/PA15 默认被 JTAG 占用如果屏幕用到这几个脚需要在初始化里禁用 JTAG、只保留 SWD否则引脚被调试口握着屏幕怎么都不亮。第二SPI1 的默认引脚被外部 Flash 或无线模块占用时要么改 SPI2 要么重映射改之前先把其它外设的占用关系列一遍。背光部分别直接挂在 GPIO 上拉电流。普通 2.4 寸模块的背光 LED 正向压降约 3.0V工作电流 20mA 上下用 GPIO 直驱会烧口。常见做法是三极管或 MOS 管做开关PWM 调亮度三极管基极串 1k 电阻。2.3 Keil5 工程搭建、芯片包安装与烧录链路开发环境这一步标准库和 HAL 库都能用区别在于你想不想让 CubeMX 帮你生成初始化代码。做图像显示系统初始化代码量大、外设多CubeMX 生成再手工改显示驱动是更省时的路径。工程建立的关键节点安装对应系列的芯片包Keil5 里如果新建工程看不到 STM32 型号基本是芯片包没装或者包装错了系列。用 CubeMX 配好时钟树确认 SPI 或 FSMC 的时钟源和分频把算出来的实际频率记在注释里。加入显示驱动文件把引脚宏定义统一放在一个头文件里方便换成别的模块时只改一处。编译后用 ST-Link 或 J-Link 烧录。烧录失败先看是不是读写保护没解Flash Loader Demonstrator 这类工具可以在烧录器不认芯片时作为备用通道。/* lcd_port.h —— 引脚与接口宏换模块只改这里 */ #define LCD_SPI SPI1 #define LCD_CS_PORT GPIOA #define LCD_CS_PIN GPIO_Pin_4 #define LCD_DC_PORT GPIOA #define LCD_DC_PIN GPIO_Pin_3 #define LCD_RST_PORT GPIOA #define LCD_RST_PIN GPIO_Pin_2 #define LCD_CS_L() GPIO_ResetBits(LCD_CS_PORT, LCD_CS_PIN) #define LCD_CS_H() GPIO_SetBits(LCD_CS_PORT, LCD_CS_PIN) #define LCD_DC_CMD() GPIO_ResetBits(LCD_DC_PORT, LCD_DC_PIN) /* DC0 传命令 */ #define LCD_DC_DATA() GPIO_SetBits(LCD_DC_PORT, LCD_DC_PIN) /* DC1 传数据 */宏的意义在于把「哪个引脚」和「怎么用」分开。上层驱动只调用 LCD_CS_L 这类语义化名称接线换到另一组 GPIO 时改动量集中在一个文件里。DC 的两个宏是整条链路的核心几乎所有显示异常第一件事都是拿逻辑分析仪看 DC 在传输期间有没有跳变。3. 最小可跑的刷屏链路初始化序列、窗口命令与 RGB565把屏幕点亮这件事可以拆成四步复位、发初始化命令序列、设定显示窗口、往显存写像素。任何一步错了现象都是黑屏或白屏但原因完全不同。3.1 复位时序与初始化命令序列复位有两种硬复位靠 RST 引脚拉低再拉高软复位靠命令 0x01。硬复位之后必须留出足够的延时驱动 IC 内部要完成上电自检这段时间太短会导致后续命令被忽略。/* ili9341_init关键命令与必要延时省略部分厂商参数 */ static void ili9341_init(void) { LCD_RST_L(); delay_ms(20); /* 拉低至少 10ms */ LCD_RST_H(); delay_ms(120); /* 上电后等 120ms 再发命令 */ lcd_write_cmd(0x01); delay_ms(120); /* 软复位 */ lcd_write_cmd(0x11); delay_ms(120); /* 退出睡眠必须等 120ms */ lcd_write_cmd(0x3A); /* 像素格式 */ lcd_write_data8(0x55); /* 0x55 RGB56516bit/像素 */ lcd_write_cmd(0x36); /* 显存访问控制 MADCTL */ lcd_write_data8(0x48); /* MY0 MX1 MV0 BGR1横屏 */ lcd_write_cmd(0x29); delay_ms(20); /* 开显示 */ }延时长度不是随便写的。0x11 退出睡眠后必须等 120ms这是 ILI9341 手册里的硬指标很多「屏幕亮一半然后白屏」的案例就是这里只等了 10ms。0x3A 写 0x55 表示按 16 位传输如果这里写成 0x6618 位后面所有像素数据的字节数都错现象是颜色整体偏移。3.2 0x2A、0x2B、0x2C三条命令决定往哪写屏幕点亮之后它并不会自动知道你要写哪块区域。每写一块内容之前都要先声明一个矩形窗口再连续灌数据。窗口由三条命令配合完成。/* 设置显示窗口x0x1, y0y1坐标从 0 开始 */ static void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { lcd_write_cmd(0x2A); /* 列地址设置 */ lcd_write_data8(x0 8); lcd_write_data8(x0 0xFF); lcd_write_data8(x1 8); lcd_write_data8(x1 0xFF); lcd_write_cmd(0x2B); /* 行地址设置 */ lcd_write_data8(y0 8); lcd_write_data8(y0 0xFF); lcd_write_data8(y1 8); lcd_write_data8(y1 0xFF); lcd_write_cmd(0x2C); /* 写显存之后的数据自动按行填充 */ }窗口设完之后0x2C 之后的每个 16 位数据都会按从左到右、从上到下的顺序自动填进矩形区域写满一个角会自动折到下一行。这意味着全屏填充只需要设置一次窗口然后连续发 153600 字节。很多人逐点上屏慢得离谱原因就是每个像素都重新设一次窗口、重新发一次 0x2C命令开销远远超过数据本身。3.3 MADCTL 与坐标方向横屏竖屏怎么切MADCTL 的位定义决定了显存地址到玻璃坐标的映射关系改它比在代码里到处改坐标换算划算得多。位名称作用D7MY行地址方向置 1 时上下翻转D6MX列地址方向置 1 时左右翻转D5MV行列交换置 1 时横竖屏互换D3BGR置 1 时按 BGR 顺序解释像素竖屏用 0x48 或 0x08横屏靠 MV 位翻。ST7789 的模块还多一层偏移屏是 240×240 但显存控制器是 240×320窗口坐标要加一个偏移量才能对上偏移值按模块的实际走线确定通常是 (0,80) 或 (0,0)。搞错偏移的典型现象是图像整体错位一条边或者下半部分跑到上半部分去。RGB/BGR 位则决定颜色有没有红蓝互换。写一个纯红色块上去如果显示成蓝色先把 BGR 位翻一下不要急着去改像素数据的字节序。3.4 两级验证先刷纯色再刷图标点亮流程要分两级验证能快速定位问题在哪一层。第一级刷纯色验证的是复位、初始化、窗口和颜色格式这四件事第二级刷一张小图标验证的是数据来源和坐标映射。/* 全屏填充单色color 为 RGB565 */ void lcd_fill(uint16_t color) { uint32_t i; lcd_set_window(0, 0, LCD_W - 1, LCD_H - 1); LCD_CS_L(); LCD_DC_DATA(); for (i 0; i (uint32_t)LCD_W * LCD_H; i) { lcd_spi_write16(color); /* 先发高字节再发低字节 */ } LCD_CS_H(); }先依次刷红、绿、蓝三色。三色都正确说明初始化和颜色格式没问题三色互换查 MADCTL 的 BGR 位只有前几行正确后面花屏查 SPI 时钟是否过高或者 DMA 配置。这一步做完再画图标出问题时就能确定是数据内容的问题而不是链路的问题。调完之后可以留着这个函数当测试入口接串口调试时用一条命令触发屏幕自检。4. 图像数据从哪来取模、SD 卡 BMP 与 DMA 双缓冲刷新屏幕能刷纯色之后接下来的问题变成图像数据从哪来。数据来源大致三类编译进 Flash 的常量数组、SD 卡或外部 Flash 里的图片文件、运行时生成的图形。三类方案的取舍取决于图像数量、是否要在线更换、以及内存预算。4.1 取模软件的四个必设参数小图标、开机 Logo 这类固定图片最省事的做法是取模成 C 数组编进固件。取模软件选项很多但真正影响能不能显示的只有四个。参数正确取值设错的后果颜色格式16 位 RGB565颜色整体偏色或花屏字节序高字节在前相邻像素颜色互换扫描方式逐行从上到下图像上下颠倒数据排列每个像素一个 16 位数相邻像素错位生成之后数组直接声明成 const 让它落在 Flash 而不是 RAM。/* 16x16 图标取模软件生成RGB565 高字节在前 */ const uint16_t icon_wifi[16 * 16] { 0xFFFF, 0xFFFF, /* ... 实际数据由取模软件填充 */ }; /* 在指定位置显示这幅图标 */ void lcd_draw_icon(uint16_t x, uint16_t y, const uint16_t *data, uint16_t w, uint16_t h) { uint32_t i, total (uint32_t)w * h; lcd_set_window(x, y, x w - 1, y h - 1); LCD_CS_L(); LCD_DC_DATA(); for (i 0; i total; i) { lcd_spi_write16(data[i]); } LCD_CS_H(); }Flash 存储的代价是图片数量固定、占用代码空间好处是零延迟、不需要文件系统。图片多到几十张以上就该考虑外部存储了。4.2 FATFS 读 BMP 直接上屏的完整流程BMP 是结构最简单的常见位图格式头部信息固定不需要压缩解码适合在 STM32 上直接读。但要处理两个格式差异BMP 每行按 4 字节对齐并且默认是从下到上存储。/* 从 SD 卡读 24 位 BMP 并在屏幕上显示简化版未处理非 24 位情况 */ void lcd_show_bmp(const char *path, uint16_t x, uint16_t y) { FIL fp; UINT br; uint8_t hdr[54]; uint32_t width, height, row_bytes, i, j; static uint8_t line[3 * 320]; /* 一行最多 320 像素 */ f_open(fp, path, FA_READ); f_read(fp, hdr, 54, br); /* BMP 头固定 54 字节 */ width *(uint32_t *)(hdr 18); height *(uint32_t *)(hdr 22); row_bytes ((width * 3 3) / 4) * 4; /* 每行 4 字节对齐 */ lcd_set_window(x, y, x width - 1, y height - 1); LCD_CS_L(); LCD_DC_DATA(); for (i 0; i height; i) { /* BMP 自下而上所以倒着读行 */ f_lseek(fp, 54 (height - 1 - i) * row_bytes); f_read(fp, line, row_bytes, br); for (j 0; j width; j) { uint8_t b line[j * 3 0]; uint8_t g line[j * 3 1]; uint8_t r line[j * 3 2]; uint16_t px ((r 0xF8) 8) | ((g 0xFC) 3) | (b 3); lcd_spi_write16(px); } } LCD_CS_H(); f_close(fp); }RGB 转 RGB565 的三行移位是标准算法红取高 5 位、绿取高 6 位、蓝取高 5 位因为人眼对绿色更敏感。行内读缓冲的容量按最大宽度预留320 像素宽时是 960 字节放在栈上很容易溢出加 static 或者改成全局缓冲。这套流程逐行边读边写速度瓶颈在 SD 卡读和 SPI 写串行执行。如果图像来自上位机而不是本地存储也可以通过串口或以太网通道把压缩后的数据传进来MCU 侧只负责解码和刷屏前提是先算清楚通道带宽够不够别让显示等着网络。4.3 SPI 加 DMA 双缓冲把刷新率抬上去前面所有写法都是 CPU 逐字节搬数据SPI 发一个字节的时间远大于 CPU 准备一个字节的时间算力全浪费在等标志位上。用 DMA 接管数据传输CPU 就空出来做坐标计算、下一块数据的准备。/* 双缓冲刷新一块矩形区域buf 为两块行缓冲每块 W*2 字节 */ #define CHUNK_LINES 8 static uint8_t buf[2][LCD_W * 2 * CHUNK_LINES]; static volatile uint8_t dma_busy 0; void lcd_flush_block(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, const uint16_t *src) { uint32_t total (uint32_t)(x1 - x0 1) * (y1 - y0 1) * 2; lcd_set_window(x0, y0, x1, y1); LCD_CS_L(); LCD_DC_DATA(); /* 配置 DMA 源地址、目标地址、长度启动一次传输 */ DMA_SetCurrDataCounter(DMA1_Channel3, total); DMA_SetMemoryAddress(DMA1_Channel3, (uint32_t)src); DMA_Cmd(DMA1_Channel3, ENABLE); dma_busy 1; /* 等传输完成实际工程中应该用中断而不是死等 */ while (dma_busy); LCD_CS_H(); }关键参数有三个。第一DMA 通道要和 SPI 的 TX 请求线对应STM32F103 上 SPI1_TX 是 DMA1_Channel3配错通道的表现是传输完全不启动。第二DMA 传输期间 CS 和 DC 必须保持有效不能在中断里随手拉高否则命令和数据会串。第三缓冲区大小要平衡太小则 DMA 频繁中断太大则占 RAM240 宽下 8 行一块是 3840 字节F103 的 20KB RAM 放两块够用。真正把 DMA 用出效果是让「DMA 在发第 N 块」和「CPU 准备第 N1 块」并行。做法是配置传输完成中断在中断里切换缓冲区并立刻启动下一次 DMA主循环只管往非活动缓冲区里算像素。这样刷新率的上限就由 SPI 时钟决定而不是由 CPU 速度决定。提示DMA 配置完成后如果屏幕只亮一部分先看 DMA 传输长度是不是写成了字节数以外的单位。DMA 的 DataCounter 单位是「传输次数」SPI 按 8 位传输时等于字节数按 16 位传输时等于半字数。4.4 用定时器给帧率定节拍别用 delay 死等动画类显示需要一个固定节拍常见做法是开一个定时器在中断里置一个标志主循环检测到标志就刷新一帧。这样帧率由定时器决定不受单帧耗时波动影响。/* TIM3 配置 30Hz 帧节拍72MHz / (35991) / (6651) 30Hz */ void tim3_frame_tick_init(void) { TIM_TimeBaseInitTypeDef t; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); t.TIM_Period 665; /* 自动重装载值 */ t.TIM_Prescaler 3599; /* 预分频 */ t.TIM_ClockDivision 0; t.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, t); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); TIM_Cmd(TIM3, ENABLE); } void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { frame_flag 1; TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } }中断里只置标志不做任何刷屏操作。刷屏放在主循环里做因为一次全屏刷新耗时几十毫秒放在中断里会拖垮整个系统的时间基准。延时函数本身也是个坑。很多工程用 SysTick 做 delay_ms如果在中断里调用它或者 DMA 等待期间用忙等会出现程序卡在延时里出不来的现象表现出来就是「延时函数卡死」。检查方向是 SysTick 的中断优先级是否低于当前中断、以及 delay 的实现是否依赖了被关掉的中断。刷屏等待 DMA 时用标志位加超时计数比死等一个 while 循环可靠得多。5. 帧率测量、撕裂定位与显示异常的排查技巧前面的流程跑通之后剩下的问题往往变成「哪里不对」而不是「怎么跑通」。这一章给几个可以直接用的测量和定位手段。5.1 用 GPIO 翻转加逻辑分析仪测真实帧率代码里算出来的帧率是理论值真实值要用引脚输出测。在每帧开始和结束时翻转同一个 GPIO把逻辑分析仪接上去量出来的高电平宽度就是单帧耗时。/* 帧首尾翻转 PB0用于示波器/逻辑分析仪测帧率 */ #define FRAME_MARK_H() GPIO_SetBits(GPIOB, GPIO_Pin_0) #define FRAME_MARK_L() GPIO_ResetBits(GPIOB, GPIO_Pin_0) while (1) { if (frame_flag) { frame_flag 0; FRAME_MARK_H(); render_one_frame(); /* 整帧渲染与刷屏 */ FRAME_MARK_L(); } }高电平宽度就是渲染耗时相邻两个上升沿间隔就是实际帧周期。如果渲染耗时接近甚至超过节拍周期说明节拍定得太快要么降帧率要么减内容。这个方法比在屏幕上显示数字更直接因为它测的是引脚电平不占用任何显示资源。另一个细节是测「有效像素速率」。在 SPI 的 DC 引脚上量数据段持续时间除以像素数能反推实际 SPI 时钟。如果实测比配置值低不少检查是不是每次传输都在反复拉 CS、或者中间插了不必要的延时。5.2 花屏、偏色、错位的现象对照表现象常见原因检查点全白或全黑背光亮初始化序列未执行或被跳过0x11 后延时是否够 120ms纯色测试正常图片花屏取模字节序或扫描方式错高低字节顺序、从上到下红蓝互换MADCTL 的 BGR 位反了写 0x48 还是 0x08图像整体偏移一条边模块偏移量未补偿ST7789 加 (0,80) 偏移图像上下颠倒MADCTL 的 MY 位或 BMP 行序翻转 MY 或反向读行屏幕闪烁、随机噪点SPI 时钟过高或排线过长降频到 9MHz 复测只显示前几行DMA 传输长度算错DataCounter 单位画面纵向撕裂无同步边写边显示改局部刷新或整体刷新同步排查顺序建议固定先纯色再单行渐变再小图标最后整图。每换一级只动一个变量避免同时改三处导致无法归因。5.3 局部刷新与脏矩形全屏刷新的代价固定界面里真正变化的区域往往只占一小块。做法是维护一个脏矩形列表每帧只重绘变化区域。/* 脏矩形记录本次需要重绘的最小包围盒 */ static uint16_t dirty_x0 LCD_W, dirty_y0 LCD_H, dirty_x1 0, dirty_y1 0; void mark_dirty(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { if (x0 dirty_x0) dirty_x0 x0; if (y0 dirty_y0) dirty_y0 y0; if (x1 dirty_x1) dirty_x1 x1; if (y1 dirty_y1) dirty_y1 y1; } void flush_dirty(void) { if (dirty_x1 dirty_x0) return; /* 本帧无变化 */ lcd_set_window(dirty_x0, dirty_y0, dirty_x1, dirty_y1); /* 只搬这块区域的像素数据 */ dirty_x0 LCD_W; dirty_y0 LCD_H; dirty_x1 0; dirty_y1 0; }一个时钟界面上秒针走过的区域可能只有 30×30 像素局部刷新的数据量不到全屏的 1.5%。脏矩形的实现难点在于包围盒合并多个小组件同时变化时合并成一个大矩形可能反而不划算这时按组件分别刷新更合适。真正决定效果的是先测出「全屏刷新一帧多少毫秒」再拿这个数去对比每个区域的刷新耗时选择权就有了依据。本文还有配套的精品资源点击获取
返回列表