STM32F103ZET6数码相框BMP显示:原理图解读与FSMC驱动实践

STM32F103ZET6数码相框BMP显示:原理图解读与FSMC驱动实践 简介这是基于STM32F103ZET6开发板的数码相框实验资源包包含PDF原理图与函数库版源码面向嵌入式初学者及正在做STM32课程设计的开发者用于掌握硬件连接、LCD显示与BMP图片解码的完整流程。压缩包共281个文件大小约8.38MB以c/h源文件、uvprojx工程文件、hex烧录文件为主并提供PDF原理图和编译过程生成的o/d/crf中间文件便于直接查看电路连接、阅读源码或二次编译。已有149人学习浏览。原理图详细描绘了电源管理、时钟系统、GPIO端口、串口/USB/CAN等接口的分配关系源码基于函数库编写涵盖FATFS文件系统操作、BMP图像数据读取与解析、LCD控制器驱动以及系统时钟初始化模块划分清晰关键步骤带错误与异常处理。通过分析原理图并结合例程源码可以理解从Flash读取图片到LCD刷屏的完整链路也能借鉴其代码结构快速移植到自己的项目中适合作为毕业设计或竞赛作品的基础框架。1. STM32F103ZET6开发板原理图与数码相框BMP显示一份资料的三种用法拿到“STM32F103ZET6单片机开发板PDF原理图数码相框-BMP图片显示实验源码函数库版.zip”这类压缩包多数人只盯着源码把PDF原理图当成可有可无的附件。实际顺序应当反过来原理图定义硬件边界源码里的main函数只是在这个边界内的一种实现。数码相框实验表面上要做图片显示真正的难点反而是TFTLCD的FSMC读写时序、BMP头文件里的偏移量计算以及STM32F103ZET6只有64KB SRAM时如何用小缓冲区逐行解码。把这三件事串起来才算真正吃透这个实验。这条链路适合三类读者准备用F103做课程设计或毕业设计的学生想把旧板显示代码移植到新板子的工程师以及刚点亮LED想往图形界面跳一步的入门者。下面按“原理图 → BMP格式 → 函数库版代码 → 调优验证”的顺序展开文中引脚编号以常见开发板为模板具体网络名以手上PDF为准。2. 解读STM32F103ZET6开发板PDF原理图最小系统与LCD接口定位2.1 供电、晶振与复位先在PDF里圈出最小系统五处拿到PDF原理图不要先找主控芯片U1按电源入口、DC-DC输出、晶振、复位、BOOT跳线五个顺序找。STM32F103ZET6工作电压是2.0V到3.6V板上通常会有一个AMS1117-3.3把5V降到3.3V也有设计用MP2359先降到5V再转3.3V。无论哪种重点看VDDA、VSSA、VREF、VREF-这组模拟电源网络它们通常经过磁珠或小电阻再接入主电源域。如果这组网络没接好ADC和内部温度传感器会出明显偏差但数码相框实验恰恰不涉及ADC很多人因此忽略。电路块常见网络名关键器件固件库中的对应项主电源VCC3V3 / GNDAMS1117-3.3RCC_APB2PeriphClockCmd模拟电源VDDA / VREF磁珠 104电容不影响本实验主晶振OSC_IN / OSC_OUT8MHz 2颗22pFRCC_HSEConfigRTC晶振RTC_OSC1 / RTC_OSC232.768kHzRCC_LSEConfig复位NRST10K上拉 0.1uFNVIC_SystemReset主晶振的封装旁会标频率值绝大多数F103板子用8MHz倍频到72MHz。看原理图的目的是确认这个频率因为库函数里的时钟初始化跟它强绑定。RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) RESET); RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);逻辑说明8MHz晶振进PLLHSE不分频9倍频得72MHz系统时钟。若原理图上晶振是12MHz同样的RCC_PLLMul_9会得到108MHz超过F103上限必须把倍频改成6。这就是为什么不能把所有开发板例程直接复制粘贴。2.2 FSMC与TFTLCD接口片选、RS和读写信号如何决定寄存器地址数码相框实验的LCD几乎都挂在FSMC的Bank1上原理图里会看到LCD_CS、LCD_RS、LCD_WR、LCD_RD、LCD_RST、LCD_BL以及DB0到DB15共16根数据线。FSMC的好处是CPU把LCD当作SRAM来写写时序由硬件生成不用GPIO模拟8080时序所以刷屏快很多。Bank1被分成4个region片选信号决定起始地址片选信号Base地址典型用途FSMC_NE10x60000000板载LCDFSMC_NE20x64000000外部设备FSMC_NE30x68000000外部设备FSMC_NE40x6C000000外部设备LCD_RS这根线决定访问的是命令还是数据。当原理图上把LCD_RS接到A6地址线时写命令地址是0x60000000写数据地址是0x60000000 | (1 6)。NE1配合地址线A6把LCD分割成两个地址窗口这是FSMC访问LCD的核心。很多移植失败的项目都挂在RS地址判断上原因是没有看原理图里RS到底接在A几。FSMC_NORSRAMInitTypeDef fsmc; FSMC_NORSRAMTimingInitTypeDef timing; timing.FSMC_AddressSetupTime 1; timing.FSMC_AddressHoldTime 0; timing.FSMC_DataSetupTime 3; 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 ENABLE; fsmc.FSMC_ExtendedMode DISABLE; fsmc.FSMC_ReadWriteTimingStruct timing; FSMC_NORSRAMInit(fsmc); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);逻辑说明AddressSetupTime是片选有效到地址信号建立的时间DataSetupTime是数据建立保持时间。F103的HCLK为72MHz时每个时间单位约13.9nsDataSetupTime3约合41.7ns对大多数LCD控制器够用。如果初始化后读LCD型号返回0xFFFF先把DataSetupTime加大到5或8再试。2.3 把原理图网络名翻译成库函数里的GPIO配置表FSMC的多组数据线、地址线分布在多个GPIO端口上。从PDF反推GPIO配置最好的办法不是背表而是在原理图上搜索“LCD_D0”顺着网络标号找到对应主控引脚再把引脚序号记录到表格中。/* 以某块把LCD数据线全部接到GPIOE的板子为例引脚号以手上的PDF为准 */ GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE); gpio.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOE, gpio);逻辑说明FSMC控制线NWE、NOE、NE1必须配置为AF_PP数据线和地址线同样配置为AF_PP。若把某根数据线误配成GPIO_Mode_Out_PPFSMC外设无法接管引脚LCD显示会整屏乱码。配置前先查原理图信号线上若有上拉或串联电阻不影响GPIO模式但会限制电平翻转速率此时GPIO_Speed仍用50MHz即可。提示有些F103开发板把LCD数据线分两组接在不同GPIO端口上。遇到这种情况把代码里的GPIOE替换成实际端口并分别打开对应端口的RCC_APB2Periph_GPIOx时钟少打开一个时钟的表现通常是某几根数据线无输出现象是图像左侧出现竖向色带。3. 数码相框BMP图片显示前置BMP头文件解析与24位转RGB565算法3.1 BMP文件结构文件头和信息头里必须读的7个字段数码相框要从SD卡读BMP第一步不是解码像素而是读好BMP头。BMP文件由14字节文件头、40字节位图信息头、调色板可选和像素数据组成。C语言结构体定义需要加#pragma pack(1)避免字节对齐把结构体撑大导致f_read读入的字段错位。字段偏移字节数说明bfType02固定为0x4D42即ASCII“BM”bfSize24整个BMP文件字节数bfOffBits104像素数据起始偏移必须读取biWidth184像素宽度biHeight224正数自下而上负数自上而下biBitCount282每像素位数常见24、32、8biCompression3040表示未压缩#pragma pack(1) typedef struct { uint16_t bfType; uint32_t bfSize; uint32_t bfReserved; uint32_t bfOffBits; } BMP_FILEHEAD; typedef struct { uint32_t biSize; int32_t biWidth; int32_t biHeight; uint16_t biPlanes; uint16_t biBitCount; uint32_t biCompression; uint32_t biSizeImage; int32_t biXPelsPerMeter; int32_t biYPelsPerMeter; uint32_t biClrUsed; uint32_t biClrImportant; } BMP_INFOHEAD; #pragma pack()逻辑说明很多BMP解析教程把bfOffBits当作固定54但带压缩标志或带alpha通道的图片会在这个字段前塞额外数据必须读取并使用。biPlanes固定为1若读到其他值说明文件损坏应当直接终止显示。3.2 24位真彩色BMP与LCD的RGB565转换字节序决定颜色24位BMP的每个像素占3字节存储顺序依次是蓝、绿、红。这意味着从缓冲区里读到buf[0]是Bbuf[1]是Gbuf[2]是R与常规RGB顺序相反。直接把buf[0]当R传给LCD红色和蓝色会完全互换整体画面变成补色。代码里用无符号右移屏蔽低位的写法uint16_t RGB24_TO_RGB565(uint8_t b, uint8_t g, uint8_t r) { return ((r 0xF8) 8) | ((g 0xFC) 3) | (b 3); }参数说明LCD以RGB565格式显示红色占5位绿色占6位蓝色占5位。r 0xF8取红分量的高5位再左移8位落到bit11到bit15g 0xFC取绿分量高6位左移3位落到bit5到bit10b右移3位落到bit0到bit4。之所以不用乘除法是因为这个函数会在主循环里被调用数万次位运算能明显减少刷屏耗时。24位BMP还有逐行对齐问题。每行字节数不能简单按宽度乘3计算而是按如下对齐规则uint32_t lineBytes ((ih.biWidth * 24 31) / 32) * 4;当宽度是320时320×3960字节恰好能被4整除对齐值对显示没有影响但当宽度是127这种奇数像素时每行末尾会多出几个字节的填充。若忽略填充位下一行数据会整体错位图像呈现阶梯状斜纹。lineBytes公式等价于(w*3 3) ~3计算效率更高。3.3 8位索引色BMP的分支处理调色板读法网络下载的素材里经常混着8位索引色BMP尤其是从老软件导出的图标。8位图的信息头之后紧跟一个256色的调色板每项4字节颜色顺序同样是BGR。uint8_t palette[256][4]; uint32_t colorIndex; UINT br; f_lseek(file, 14 40); /* 跳到信息头之后的调色板 */ f_read(file, palette, 256 * 4, br); f_lseek(file, fh.bfOffBits); f_read(file, colorIndex, 1, br); LCD_PutPixel(x, y, RGB24_TO_RGB565(palette[colorIndex][2], palette[colorIndex][1], palette[colorIndex][0]));参数说明palette[colorIndex][2]是红分量palette[colorIndex][1]是绿分量palette[colorIndex][0]是蓝分量这个下标顺序很容易写反。数码相框实验通常只要支持24位图就能覆盖绝大多数素材但保留8位分支能提高容错性。注意biClrUsed字段为0时表示调色板数量是2的biBitCount次方也就是256项不能图省事直接读0个字节。4. 函数库版源码拆解数码相框BMP显示主流程与文件加载4.1 main函数骨架时钟、LCD、SD卡与FATFS挂载顺序函数库版代码一般按“系统时钟初始化 → 延时函数初始化 → LCD初始化 → FSMC初始化 → SD卡初始化 → FATFS挂载 → 显示图片”的固定顺序执行。初始化顺序直接影响排错范围LCD初始化放在SD卡之前画面显示错误消息就有LCD可看不然出问题只能干瞪眼。int main(void) { SystemInit(); delay_init(72); LCD_Init(); FSMC_Init(); if (SD_Init() ! SD_OK) { LCD_ShowString(0, 0, SD Card Error); while (1); } if (f_mount(fs, 0:, 1) ! FR_OK) { LCD_ShowString(0, 20, Mount Failed); while (1); } bmp_show_picture(0:/photo.bmp, 0, 0); while (1); }逻辑说明f_mount的第三个参数传1表示立即挂载传0只会注册一个卷直到第一次访问文件才真正检查文件系统。SD卡物理初始化时若还没挂载文件系统f_mount返回FR_NOT_ENABLED比返回FR_NO_FILESYSTEM更容易被误解为其他错误。f_mount常见的返回码可以辅助定位问题返回码数值含义与排查方向FR_OK0挂载成功FR_NOT_ENABLED12卷未注册检查f_mount调用和卷标参数FR_NO_FILESYSTEM13SD卡不是FAT16/FAT32格式重新格式化FR_INVALID_OBJECT9文件句柄未初始化或已关闭4.2 BMP逐行解码缓冲读取、4字节对齐与LCD坐标定位F103ZET6的SRAM只有64KB一张320×240的24位BMP换算后约230KB不可能整张读入内存。标准做法是逐行读取每行数据放在512字节的小缓冲区里。LCD写入一行后再读下一行。uint8_t bmp_show_picture(const char *path, uint16_t x, uint16_t y) { FIL file; BMP_FILEHEAD fh; BMP_INFOHEAD ih; uint8_t buf[512]; uint32_t lineBytes, readLen; UINT br; if (f_open(file, path, FA_READ) ! FR_OK) return 1; f_read(file, fh, 14, br); f_read(file, ih, 40, br); if (fh.bfType ! 0x4D42) { f_close(file); return 1; } lineBytes ((ih.biWidth * ih.biBitCount 31) / 32) * 4; f_lseek(file, fh.bfOffBits); for (uint32_t row 0; row (uint32_t)ih.biHeight; row) { readLen 0; while (readLen lineBytes) { UINT want lineBytes - readLen; if (want sizeof(buf)) want sizeof(buf); f_read(file, buf, want, br); for (uint32_t col 0; col br; col 3) { LCD_PutPixel(x col / 3, y row, RGB24_TO_RGB565(buf[col], buf[col 1], buf[col 2])); } readLen br; } } f_close(file); return 0; }逻辑说明外层循环走行内层while循环处理一行超过512字节的情况。col/3计算当前读取字节对应的像素横坐标因为每个像素3字节。这里假设LCD坐标原点是左上角且BMP按自上而下的顺序存储。参数说明biHeight为正数时BMP像素自下而上存文件的第0行实际是图片最底行直接按row递增画会上下颠倒。处理方式是把目标y写为y (ih.biHeight - 1 - row)。biHeight为负数时第0行就是图片首行可以直接画。判断条件就一行但漏掉的后果是图像上下翻转。4.3 多图切换与按键逻辑f_findfirst遍历根目录数码相框不可能只显示一张图常见做法是把BMP文件放在SD卡根目录程序启动后用FATFS的目录读取接口扫描出所有BMP文件名存成二维字符数组再通过按键或定时器轮播。DIR dir; FILINFO fno; static char names[64][64]; uint16_t count 0; f_opendir(dir, 0:); while (count 64 f_readdir(dir, fno) FR_OK fno.fname[0]) { const char *ext strrchr(fno.fname, .); if (ext (strcmp(ext, .bmp) 0 || strcmp(ext, .BMP) 0)) { strncpy(names[count], fno.fname, 63); names[count][63] \0; count; } } f_closedir(dir);参数说明f_readdir每次调用返回一个目录项fno.fname是8.3短文件名格式。扩展名比较必须同时处理大写和小写否则数码相机导出的BMP后缀为.BMP时会被过滤掉。names数组声明为static避免在main函数栈上占用过多内存F103的栈默认只有几百字节放不下64×64的数组。切换图片时每次显示前f_open显示完立即f_close不要让文件句柄一直占用。FATFS的FIL结构体每个约550字节同时打开多个文件会挤占内存。翻页逻辑最简单的实现是GPIO轮询检测到按键下降沿后把索引加一再调用bmp_show_picture显示下一张。5. 验证与调优用自造BMP和FSMC时序参数解决花屏偏色5.1 生成一张自检测BMP快速定位颜色与对齐问题与其从网上下载一张内容不确定的图片做测试不如用脚本生成一张四色分区的BMP主动暴露两类问题颜色被换序以及行对齐错误。下面这段Python脚本会在当前目录生成128×128的测试图左上红、右上蓝、左下绿、右下白。import struct w h 128 pixels bytearray() for y in range(h): for x in range(w): if x 64 and y 64: b, g, r 0, 0, 255 elif x 64 and y 64: b, g, r 255, 0, 0 elif x 64 and y 64: b, g, r 0, 255, 0 else: b, g, r 255, 255, 255 pixels.extend((b, g, r)) pixels.extend(b\x00 * ((4 - (w * 3) % 4) % 4)) hdr struct.pack(2sIHHI, bBM, 54 len(pixels), 0, 0, 54) info struct.pack(IiiHHIIiiII, 40, w, h, 1, 24, 0, len(pixels), 0, 0, 0, 0) open(test.bmp, wb).write(hdr info bytes(pixels))把这个test.bmp放进SD卡通过数码相框代码显示。如果红色区域和蓝色区域互换检查RGB24_TO_RGB565的参数顺序。如果出现斜向条纹或颜色溢出检查lineBytes对不对齐。如果整个画面往上或往下偏移一行检查biHeight正负号处理。5.2 FSMC时序参数与LCD_ID验证法花屏不全是BMP解析问题FSMC时序太紧也会导致数据线上的电平还没稳定就被LCD采样。最直接的验证方法是在LCD初始化完成后读一次控制器型号寄存器读回的值不是0xFFFF也不是0基本就能确认FSMC时序可工作。FSMC参数初始值异常时的调整方向AddressSetupTime1LCD命令和数据混淆时加到2或3DataSetupTime3花屏、出现规律性横条纹时加到5BusTurnAroundDuration0频繁读写切换但读ID不稳定时设1读写时序参数的调整原则是尽量小到能通过而不是大到最稳。把DataSetupTime设成15虽然能显示但每个像素多浪费上百纳秒整屏刷完的耗时明显增加。调参时从大到小逐档缩小每次修改后重新读LCD_ID直到刚好能读到合法厂商型号值。ID稳定后再回到BMP显示函数观察刷屏速度若刷新时能明显看到从上往下的扫描线说明时序已经足够。验证完这些再回头看数码相框实验的源码排错的范围就会小很多。LCD初始化失败先查FSMC时序参数BMP显示颜色错位先查字节序图像倾斜先查lineBytes对齐。这三个检查点能覆盖这个实验九成以上的异常现象。最后留一个习惯在工程配置里把f_open、f_read的返回值打印到LCD上文件系统层的错误就不会被误判成显示问题。本文还有配套的精品资源点击获取