ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103ZET6数码相框BMP显示:FSMC驱动与解码实战

STM32F103ZET6数码相框BMP显示:FSMC驱动与解码实战 简介STM32F103ZET6开发板数码相框实验资料包含开发板PDF原理图与函数库版BMP图片显示源码是一套能同时学习硬件连接与软件驱动的入门实践资料面向单片机初学者、嵌入式竞赛学生以及正在做STM32相关课设或毕业设计的开发者。原理图详细标注了电源管理、时钟树、GPIO端口、LCD接口和常用通信外设的连接关系无论查看电源电路还是了解LCD背光控制都能快速定位对应节点尤其适合需要快速掌握板级外设分配的人群。源码基于ST标准函数库编写完整覆盖BMP文件解析、FatFs文件系统读写、LCD屏幕驱动、定时器配置等核心流程且代码注释清晰Keil工程文件可直接打开编译生成的hex固件可烧录到开发板运行验证其中LCD驱动流程完整便于学习屏幕初始化、画点和图片刷新等方法。压缩包共281个文件主要内容包括c/h源码、pdf原理图、uvprojx工程文件以及o/d/crf等编译中间文件、hex可执行文件和txt说明文档整体大小约8.38MB。已有149人学习下载既可以按例程逐行学习外设初始化与驱动编写也能直接在此数码相框项目基础上进行二次开发形成从硬件认知到软件调试的完整学习闭环。1. 数码相框实验的难点不在LCD画点而在BMP头文件和像素扫描很多拿到STM32F103ZET6开发板的同学看到数码相框-BMP图片显示实验时第一反应是去研究TFT-LCD驱动和刷屏速度结果屏幕能亮却显示不出一张完整的图。其实这个实验最容易被卡住的不是画点而是BMP文件本身它的像素数据通常自底向上存放像素顺序是BGR而不是RGB每一行结尾还会用0补齐到4字节边界。把这三件事处理对再用STM32F103ZET6的FSMC外设去驱动16位并口LCD整个相框就是读文件、解析头部、逐行搬运像素。它适合刚入门LCD的初学者也适合想搞清BMP文件头与显示设备如何衔接的开发者。拿到压缩包之后顺序应该是先看PDF原理图确认LCD接在FSMC的哪个Bank、哪条地址线上再写BMP解码函数用FATFS从SD卡读图并转换像素最后才是刷屏速度和显示效果优化。下面按这个路线把每一段讲透。2. 先读开发板PDF原理图把STM32F103ZET6的FSMC与LCD映射定下来开发板原理图是一张几十页的PDF不要从头到尾读重点是找出LCD接口、电源、晶振和复位。很多显示异常其实不是程序问题而是LCD_CS接在了NE2、LCD_RS接在了A6之类的位置上代码里却按NE1和A0算地址画面自然出不来。2.1 从原理图里先找LQFP144的电源、晶振和复位STM32F103ZET6的命名拆开看Z代表144引脚E代表512KB FlashT代表LQFP封装6代表工业级温度范围。这个“Z”决定了开发板的引出资源多FSMC、SDIO、DCMI这些外设都能同时用到。收到PDF后我一般先用PDF阅读器的搜索功能把下面几个信号在每一页标出来。要定位的信号原理图里找什么作用3.3V电源VDD、VDDA、VREF引脚附近检查滤波电容是否齐全VREF决定ADC量程8MHz主晶振OSC_IN/OSC_OUTSTM32F103的系统时钟必须从外部HSE倍频到72MHz32.768kHz晶振PC14/PC15RTC和待机唤醒时钟源数码相框支持关机唤醒时用复位NRST与复位芯片上电复位时序不稳会导致FSMC上的LCD白屏BOOT0/BOOT1引脚旁的跳线或拨码烧录后程序跑飞时用来强制进入ISP模式先定位电源和时钟的原因在于FSMC驱动的TFT-LCD对复位时序敏感。NRST上没有RC延时网络或者VDDA纹波偏大都会出现“下载成功但LCD不亮”的假象。此时查程序没有意义看原理图反而最快。2.2 顺着LCD插座反推CS和RS在FSMC上的地址LCD接口在原理图里一般画成40Pin或50Pin排母引脚名常见为LCD_CS、LCD_RS、LCD_WR、LCD_RD、LCD_RST、LCD_BL、D0到D15。这组信号要逐一回溯到STM32F103ZET6的FSMC引脚上关键是两个映射一个是片选。LCD_CS如果接到NE1那么这个LCD挂在FSMC Bank1的第1子区基地址为0x60000000如果接到NE2基地址变成0x64000000。另一个是寄存器选择。LCD_RS通常接A0、A6或A16它决定命令寄存器和数据寄存器之间差几个地址。typedef struct { volatile uint16_t LCD_REG; volatile uint16_t LCD_RAM; } LCD_TypeDef; #define LCD_BASE 0x60000000 #define LCD ((LCD_TypeDef *) LCD_BASE)代码里把LCD_REG和LCD_RAM做成一个结构体让两个成员连续排列在地址0x60000000和0x60000002上。这里的关键在于STM32的FSMC按16位访问时外部地址线A0对应内部字节地址的bit1所以数据地址相对命令地址偏移2个字节而不是1个字节。两个uint16_t成员之间没有填充LCD-LCD_RAM正好落在0x60000002上。如果原理图上LCD_RS接的是FSMC_A6需要把基地址偏移改成0x60000040再加结构体定义。看到实验源码里写死0x60000000时务必回查原理图不要认为所有开发板都一样。2.2.1 不同片选与RS接法对应的地址速查表LCD_CS接到基地址LCD_RS接A0时数据地址说明NE10x600000000x60000002最常见Bank1第1区NE20x640000000x64000002部分开发板预留NOR Flash也占用NE1NE30x680000000x68000002少见用于多设备扩展NE40x6C0000000x6C000002四片片选全用满时用到这个地址如果不匹配程序能编译能下载但写寄存器全写到了RAM区LCD不会响应任何命令。调试时第一件事就是在LCD_Init里先写一条读ID命令串口打印返回的控制器型号地址不对时返回值通常是0xFF或乱码。2.3 FSMC时序参数不是照抄要按LCD控制器写周期配标准外设库把FSMC初始化封装成FSMC_NORSRAMInitTypeDef和FSMC_NORSRAMTimingInitTypeDef比寄存器版省去大量移位计算。函数库版源码里通常会有一段类似下面的配置FSMC_NORSRAMInitTypeDef fsmc; FSMC_NORSRAMTimingInitTypeDef timing; timing.FSMC_AddressSetupTime 2; timing.FSMC_AddressHoldTime 0; timing.FSMC_DataSetupTime 5; timing.FSMC_BusTurnAroundDuration 0; timing.FSMC_AccessMode FSMC_AccessMode_A; fsmc.FSMC_Bank FSMC_Bank1_NORSRAM1; fsmc.FSMC_MemoryType FSMC_MemoryType_SRAM; fsmc.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; fsmc.FSMC_WriteOperation FSMC_WriteOperation_Enable; fsmc.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; fsmc.FSMC_WriteTimingStruct timing; FSMC_NORSRAMInit(fsmc); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);在72MHz主频下FSMC一个时钟周期约13.8ns。上面FSMC_AddressSetupTime 2给地址线留了约27ns建立时间FSMC_DataSetupTime 5给数据线留了约69ns保持窗口。这个量级能兼容大多数ILI9341、ILI9325、ST7789驱动的并口屏。参数调大不会烧坏屏幕只会让刷屏变慢调小到0会导致读到的像素颜色随机错位屏幕上出现细密的噪点。如果源码里给的是AddressSetupTime 16这种偏大的值先检查LCD控制器数据手册里的“Write Cycle Time”算一下硬件允许的最小时序再往下压。3. 函数库版源码里的BMP解码行对齐、图像方向和BGR转RGB565BMP解码是数码相框实验的核心代码量不大但每个细节都能单独制造一种花屏现象。先不要急着写刷图把BMP文件头结构定义正确后面的显示函数才有意义。3.1 用结构体定义BMP文件头先避开字节对齐陷阱Windows下的BMP文件由BITMAPFILEHEADER和BITMAPINFOHEADER两部分组成前者固定14字节后者固定40字节。C语言结构体默认按成员对齐直接定义会导致sizeof变成16或44读进来的头字段全错。#pragma pack(push, 1) typedef struct { uint16_t bfType; // 固定为0x4D42即字符BM uint32_t bfSize; // 整个BMP文件字节数 uint16_t bfReserved1; uint16_t bfReserved2; uint32_t bfOffBits; // 像素数据偏移24位无压缩图通常为54 } BMP_FILE_HEADER; typedef struct { uint32_t biSize; // 本结构体长度固定为40 int32_t biWidth; // 图像宽度像素 int32_t biHeight; // 正数自底向上负数自顶向下 uint16_t biPlanes; uint16_t biBitCount; // 24为常用32带Alpha通道 uint32_t biCompression; uint32_t biSizeImage; int32_t biXPelsPerMeter; int32_t biYPelsPerMeter; uint32_t biClrUsed; uint32_t biClrImportant; } BMP_INFO_HEADER; #pragma pack(pop)#pragma pack(push, 1)让结构体按1字节对齐bfType之后的bfSize不会在地址2和地址3之间空出两个字节。参数解释里最容易被忽略的是biHeight的符号大多数图像处理软件导出24位BMP时biHeight是正数像素数据从最后一行开始存储手机截屏程序则相反生成自顶向下的BMP。解码前先打印这个字段能省掉后面反复试方向的功夫。实验源码一般只处理24位和32位BMP。如果需要兼容8位索引色BMP还要处理调色板但数码相框场景下用24位图最省事转换代码也最短。3.2 BMP图片显示的逐行读取逻辑STM32F103ZET6只有64KB SRAM一张320x240的24位原始图像数据就要225KB不可能把整幅图读进内存再显示。正确做法是申请一块行缓冲区一次只读一行逐行写入LCD。#define BMP_MAX_WIDTH 800 static uint8_t lineBuf[BMP_MAX_WIDTH * 3 4]; uint8_t BMP_DisplayFile(const char *path, uint16_t x, uint16_t y) { FIL fp; BMP_FILE_HEADER fh; BMP_INFO_HEADER ih; UINT br; uint32_t lineBytes, fileRow, row, col; if (f_open(fp, path, FA_READ) ! FR_OK) return 0; f_read(fp, fh, sizeof(fh), br); f_read(fp, ih, sizeof(ih), br); if (fh.bfType ! 0x4D42 || ih.biBitCount ! 24) { f_close(fp); return 0; } lineBytes ((ih.biWidth * 24 31) / 32) * 4; if (lineBytes sizeof(lineBuf)) { f_close(fp); return 0; } LCD_SetWindow(x, y, x ih.biWidth - 1, y ih.biHeight - 1); for (row 0; row (uint32_t)ih.biHeight; row) { fileRow (ih.biHeight 0) ? (ih.biHeight - 1 - row) : row; f_lseek(fp, fh.bfOffBits fileRow * lineBytes); f_read(fp, lineBuf, lineBytes, br); for (col 0; col (uint32_t)ih.biWidth; col) { uint8_t b lineBuf[col * 3 0]; uint8_t g lineBuf[col * 3 1]; uint8_t r lineBuf[col * 3 2]; LCD_WriteRAM(((r 3) 11) | ((g 2) 5) | (b 3)); } } f_close(fp); return 1; }这个函数透出的三个关键点都写在lineBytes和fileRow两个变量里。lineBytes计算的是BMP文件中一行像素占用的字节数不是width * 3。因为BMP规定每行长度必须4字节对齐比如宽度为101像素的24位图实际行字节数是((101 * 3 3) / 4) * 4 304其中最后一个字节是填充。不按这个值寻址图像会从第2行开始出现锯齿状斜切。fileRow处理了原点方向。当biHeight 0时文件里第一行是图像的视觉最后一行所以第row行要取文件倒数第row行。因为LCD_SetWindow已经把写入顺序设置为从上到下这里映射关系一旦颠倒图像会上下翻转但颜色和行对齐都正常。3.2.1 24位BMP像素为什么是BGR顺序代码里lineBuf[col*30]取的是蓝色分量最后组合RGB565时把它放到低5位。很多第一次写转换的人在LCD_WriteRAM里直接传((r 3) 11) | ((g 2) 5) | (b 3)就是把r和b的位置写反结果是画面整体偏橙红。出现这种现象不要调LCD背光回BMP读取逻辑里交换红色和蓝色即可。转换公式的位运算是这样拆的24位BMP每个像素是B、G、R各8位LCD的RGB565格式里红色占bit15到bit11共5位绿色占bit10到bit5共6位蓝色占bit4到bit0共5位。因此r 3去掉低3位保留高5位g 2保留高6位b 3保留高5位。3.2.2 显示窗口与一次写一行像素LCD_SetWindow不是标准库函数是LCD驱动里封装好的接口作用是设置GRAM写入的矩形区域。设置一次窗口后连续往数据地址写像素控制器会自动横向递增坐标一行写完自动跳到下一行。整幅图只需要调用一次SetWindow逐行调用则因为频繁发命令刷新速度会明显变慢。如果图片宽度超过BMP_MAX_WIDTH函数直接返回。开发板的2.8寸屏一般是320x240这个800上限够用要支持更大尺寸把静态数组改大即可。3.3 数码相框的图片来源SD卡加FATFS与内部Flash数组函数库版源码里有两种图片存放方案。一种是先用PC工具把BMP转换成C语言数组编译进内部Flash这种方式实现简单但512KB Flash装不了几张图只能叫“单张图片显示”谈不上相框。数码相框实验的标准做法是SD卡加FATFS图片放在SD卡里文件名按顺序编号程序循环读取。FATFS fs; FRESULT fr; char path[16]; uint16_t imgNo; fr f_mount(fs, 0:, 1); if (fr ! FR_OK) return; for (imgNo 1; imgNo 100; imgNo) { sprintf(path, 0:/%d.bmp, imgNo); if (!BMP_DisplayFile(path, 0, 0)) break; Delay(5000); }f_mount的第二个参数是挂载点第三个参数为1表示立即挂载。“0:”对应SD卡所在的驱动器号。显示完一张图后延时5秒再显示下一张如果要手动切换把这个延时换成按键扫描即可。图片尺寸和LCD分辨率不一致时LCD_SetWindow会把宽度或高度算错可能出现只有一部分画面或者越界写GRAM实验里先保证BMP的宽高和屏幕一致缩放算法放到后面优化。4. 把函数库版数码相框源码在Keil工程里跑通拿到源码包后第一件事不是编译而是确认工程使用的库版本和芯片配置。函数库版对应的是ST标准外设库不是HAL库两者的初始化函数命名和中断处理方式完全不同混用会报一堆未定义标识符。4.1 函数库版与寄存器版的差别寄存器版程序里到处都是FSMC_Bank1-BTCR[0]这类直接操作代码短但对初学不友好。函数库版在此基础上包了一层FSMC_NORSRAMInit和GPIO_Init把寄存器的位域拆成了结构体成员。对数码相框实验来说函数库版的好处是修改FSMC时序参数只需改结构体字段不用查Reference Manual里的寄存器位。需要注意函数库版源码通常按C89风格编写变量声明都放在函数块开头。如果换成新版的AC6编译器#include stm32f10x.h之后会出现大量warning虽然多数不影响运行但还是建议在MDK里把编译器切换成AC5保持和源码风格一致。4.2 工程配置里最容易错的4个参数在Keil中新建或打开工程后四个设置直接决定能否正常下载和运行。配置项推荐值说明DeviceSTM32F103ZE型号选ZE选成C8T6则启动文件和内存配置全错C/C DefineSTM32F10X_HD, USE_STDPERIPH_DRIVERHD表示高密度芯片漏掉会初始化不到FSMC启动文件startup_stm32f10x_hd.s必须与HD宏对应Flash Download勾选Reset and Run不勾选则每次烧录后要手动按复位键STM32F10X_HD这个宏影响stm32f10x_flash.c里的Flash扇区划分也影响system_stm32f10x.c的时钟配置。ZET6属于高密度产品如果工程里误写成STM32F10X_MD默认只能识别到128KB Flash功能上暂时没影响但APB1外设时钟边界会配错。函数库版初始化系统时钟时默认读取外部8MHz晶振。部分开发板用的是12MHz晶振如果原理图上主晶振不是8MHz需要在system_stm32f10x.c里把PLL_M和PLL_N相关的宏改掉。检查方法很简单用示波器或万用表频率档量OSC_OUT引脚或者直接量MCO引脚输出的系统时钟。如果打算先用Proteus仿真而不是直接上板记得给仿真软件添加stm32f103zet6库但仿真里的LCD控制器模型和FSMC时序比真实芯片宽容很多在仿真上跑通的参数拿到开发板上不一定合适最可靠的路径还是先看PDF原理图再烧到板上调。4.3 串口打印BMP文件头字段先验证解析再验屏屏幕不显示的排查顺序应该从串口输出开始而不是反复改LCD初始化。把BMP_DisplayFile里读到的文件头信息通过重定向后的printf打印出来能立刻区分问题是出在FATFS、文件头解析还是LCD驱动。int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; } printf(BM0x%04X width%ld height%ld bitcount%u offset%lu\r\n, fh.bfType, (long)ih.biWidth, (long)ih.biHeight, ih.biBitCount, (unsigned long)fh.bfOffBits);这里fputc是给MDK的微库用的printf每输出一个字符都会进入这个函数往USART1的数据寄存器写入并等待发送完成。如果串口Modbus之类的程序里也用了USART1调试时要和它错开避免两边抢占发送。可以看到biHeight为正值时直接提醒自己后面代码里会走h - 1 - row分支biBitCount如果显示不是24说明SD卡里的图片格式不符合当前解码逻辑。5. 收尾技巧用Python把BMP预转成RGB565绕开运行时转换STM32F103ZET6主频72MHz没有硬件浮点和DSP指令BMP解码里的逐像素位运算虽然不重但转换24位BGR到RGB565仍然要占用几个机器周期。当图片固定不变时更好的做法是在PC端预处理成纯RGB565数据单片机只需要连续读像素并写入LCD连BMP文件头解析和行对齐计算都可以省掉。import struct def bmp_to_rgb565(filename): with open(filename, rb) as fp: data fp.read() off struct.unpack_from(I, data, 10)[0] w, h struct.unpack_from(ii, data, 18) bpp struct.unpack_from(H, data, 28)[0] if bpp ! 24: raise ValueError(只支持24位BMP) row_size ((w * 3 3) // 4) * 4 out bytearray() for y in range(h): src_row h - 1 - y row_start off src_row * row_size for x in range(w): b data[row_start x * 3 0] g data[row_start x * 3 1] r data[row_start x * 3 2] v ((r 3) 11) | ((g 2) 5) | (b 3) out struct.pack(H, v) with open(output.bin, wb) as fp: fp.write(out) if __name__ __main__: bmp_to_rgb565(1.bmp)这个脚本让BMP文件头解析在PC端完成struct.unpack_from按小端序读取文件头字段和STM32侧的结构体含义一致。src_row复用自底向上的转换逻辑output.bin里每个像素占2字节低字节在前、高字节在后顺序从上到下从左到右没有任何行填充。单片机读取这个BIN文件时分辨率不能被脚本写进数据里需要文件名约定或者放在固定内存中。显示函数变成两层循环宽度和高度由宏固定每读两个字节直接拼成一个RGB565颜色#define IMG_WIDTH 320 #define IMG_HEIGHT 240 for (row 0; row IMG_HEIGHT; row) { for (col 0; col IMG_WIDTH; col) { f_read(fp, pix, 2, br); LCD_WriteRAM(pix[0] | (pix[1] 8)); } }pix[0]是RGB565的低字节pix[1]是高字节拼回uint16_t时不需要命名转换直接移位即可。因为少了每行的0填充整个文件的体积正好是IMG_WIDTH * IMG_HEIGHT * 2代入320x240算出来是153600字节放在SD卡上没有任何压力。验证预处理结果时先检查这个文件大小再放到屏幕上显示只有文件大小和计算值一致时才说明PC端转换脚本没有踩到行对齐和BGR顺序的坑。颜色仍然不对就在脚本里交换r和b的取值位置图像上下颠倒则把src_row h - 1 - y改成src_row y。本文还有配套的精品资源点击获取
返回列表