十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

STM32驱动TFT屏做数码相框:从ILI9325初始化到触摸校准

STM32驱动TFT屏做数码相框:从ILI9325初始化到触摸校准 简介这是一份基于51单片机STC12C5A60S2的TFT触摸屏刷屏测试工程源码包适合嵌入式入门开发者、单片机爱好者以及需要快速上手ILI9325驱动方案的工程师。资源围绕TFT显示与触摸交互展开涵盖屏幕初始化、9325tp.h驱动库调用、像素刷新时序、触摸事件读取等关键环节并配有完整工程文件与烧录Hex文件。包内共103个文件以C源码、头文件、列表文件为主附带UV2工程、OBJ中间文件、BMP图片及SD卡相关模块压缩包约548KB目录结构可用于对照学习。目前已有125人学习浏览。通过研读代码可快速掌握STC12C5A60S2控制TFT屏幕的初始化流程、颜色填充与触摸坐标解析方法了解数码相框等图形交互项目的基本实现路径。1. 一块旧屏、一个头文件数码相框的组装逻辑把库存里翻出来的 TFT 触摸屏接到单片机上做数码相框最卡人的不是焊线而是那个写在模块标签上的驱动型号ILI9325配套头文件通常就是 9325tp.h。很多开发者的第一反应是上网找完整例程但真正有用的恰恰是把这个头文件拆开搞清楚初始化序列、并口时序和触摸校准后面那几条数学公式。这个项目能解决的不是“点亮一块屏”而是从零开始把一块显示模块变成可交互的相框能刷满屏照片、能按触摸区域切图、能记住校准参数。适合手里有闲置 TFT 屏、准备做毕业设计或小型消费类样机的嵌入式开发者。接下来按“驱动初始化 → 图像数据流转 → 触摸映射 → 调试”的顺序展开每一步都拆到可以直接落码。2. 看懂 9325tp.h 之前ILI9325 初始化与总线选择2.1 先确认控制器TFT 屏和 9325tp.h 的对应关系绘图型 TFT 屏在单片机项目里有两类驱动方案一类是带控制器不带显存的裸屏必须由单片机不断刷新另一类是带显存、能直接写命令的控制器比如 ILI9325、ST7789、SSD1963。9325tp.h 这个命名方式在传统 51/STM32 项目里很典型它封装的是 ILI9325 控制器的寄存器操作外面用一组宏定义把片选、复位、RS数据/命令选择、WR、RD 和 16 条数据线映射到 MCU 的 GPIO 或 FSMC。拿到一个陌生的 TFT 模块时第一步不是接屏而是看背面的丝印编号确认里面是 ILI9325 还是兼容型号否则拿别人的初始化表刷上去会出现白屏或颜色错乱。这块屏幕的数据接口常见为 8 位或 16 位并口。16 位并口一次传一个像素写全屏 320×240 大约需要 7.68 万个像素点对总线压力很大。做数码相框这类以静态图片为主的应用多数人会选 16 位模式少一半写时序开销。9325tp.h 里是否启用 16 位模式通常由文件开头的一个宏控制类似#define LCD_USE_16BIT。建议你在新工程里保留这个宏开关方便在调试逻辑先把数据线降到 8 位验证接线再切回 16 位跑性能测试。以下是对比不同数据接口在 320×240 分辨率和 25MHz 写时序下的粗略差异接口模式数据位数写一像素周期全屏写显存时间适用场景8 位并口82 次写周期约 50ms 以上引脚紧张、对功耗不敏感16 位并口161 次写周期约 25ms 左右数码相框、小型 UI4 线 SPI1多周期串行更慢低分辨率屏、引脚复用注意SPI 屏虽然能大幅减少引脚但 320×240 全屏刷新时的带宽压力会让 MCU 负载明显升高。数码相框如果还要同时解 JPEG优先选并口屏。2.2 初始化顺序背后的逻辑先关显示、再配电源时序ILI9325 初始化表看似是一堆十六进制值实际存在明确的顺序先关显示再设置电源、扫描方向、显存地址窗口最后重新开显示。之所以必须先关显示是因为同时改变时序发生器和显存写指针时屏幕会扫描到未定义的行出现花屏或短暂闪烁。初始化表里通常会看到0x0001、0x0002这类控制寄存器它们负责触发内部电源启动。如果这些寄存器在重置后没有被正确写入即使能点亮背光屏幕也会一直灰白无画面。更需要注意的地方在于 9325tp.h 中的lcd_wr_reg和lcd_wr_data这两个函数。前者把 RS 置低表示传命令后者把 RS 置高表示传数据。很多初始化失败不是因为寄存器值错而是 9325tp.h 在适配不同 MCU 时默认了某些时序比如对 STM32F407 用 FSMC 时不需要手动拉高 RS直接用 FSMC 的地址线 A6 来区分命令和数据。如果你是从旧的 AVR 工程移植这个头文件必须把所有 GPIO 翻转函数替换成 FSMC 总线写否则速度会慢到肉眼可见的刷新帧率而且读 ID 时会一直返回 0xFFFF。2.3 适配 STM32F407 的最小初始化骨架在写具体寄存器之前先做好总线初始化。下面这个示例是 FSMC 映射到地址 Bank1 第三个区域的通用写法代码里的Bank1_LCD_C是命令地址Bank1_LCD_D是数据地址#define Bank1_LCD_C ((uint32_t)0x6C000000) // A16 选择命令 #define Bank1_LCD_D ((uint32_t)0x6C020000) // A16 选择数据 static void LCD_FSMC_Init(void) { GPIO_InitTypeDef gpio {0}; FSMC_NORSRAMInitTypeDef fsmc {0}; FSMC_NORSRAMTimingInitTypeDef fsmc_timing {0}; // 使能对应 GPIO 和 FSMC 时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_FSMC_CLK_ENABLE(); // D0-D15、控制脚配置为复用推挽输出 gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FAST; gpio.Alternate GPIO_AF12_FSMC; // 省略每个 pin 的具体配置... // 按你板子上的 FSMC 引脚表逐组使能 HAL_GPIO_Init(GPIOD, gpio); fsmc_timing.AddressSetupTime 0x01; fsmc_timing.DataSetupTime 0x03; fsmc_timing.AccessMode FSMC_ACCESS_MODE_A; fsmc.FSMC_Bank FSMC_Bank1_NORSRAM3; fsmc.FSMC_MemoryType FSMC_MEMORY_TYPE_SRAM; fsmc.FSMC_MemoryDataWidth FSMC_NORSRAM_MEM_BUS_WIDTH_16; fsmc.FSMC_WriteTiming fsmc_timing; fsmc.FSMC_ReadTiming fsmc_timing; FSMC_NORSRAMInit(fsmc); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM3, ENABLE); }这段代码把 FSMC 配置成 16 位异步 SRAM 模式。AddressSetupTime和DataSetupTime分别表示地址建立和输出建立时钟数这两个值决定了 MCU 写寄存器时拉高 WR 的持续时间。对于 STM32F407 跑 168MHz0x01和电压上升沿偏慢的屏可以适当调整。参数太小会出现偶发数据错位表现为初始化时偶尔花屏参数太大会让全屏刷新慢 20%。初始化完成后可以直接用宏定义读写寄存器不需要再手动翻转 RS、WR、CS。初始化序列本身较长通常包含写入触发片内电源升压的寄存器、设置面板驱动数量、设置扫描方向和 RGB 顺序。下面抽出最常见的骨架实际寄存器值需要按你的屏幕制造商提供的参考表填充不能用其他品牌屏幕的表硬套void LCD_Init_ILI9325(void) { LCD_RESET_ACTIVE(); // 拉低 RST delay_ms(50); LCD_RESET_DEACTIVE(); // 拉高 RST delay_ms(120); LCD_WriteReg(0x00E5, 0x78F0); // 内部时钟设置 LCD_WriteReg(0x0001, 0x0100); // 输出控制扫描方向 LCD_WriteReg(0x0002, 0x0700); // 显示基准波形 LCD_WriteReg(0x0003, 0x1030); // 面板输出控制 LCD_WriteReg(0x0007, 0x0000); // 先关闭显示 LCD_WriteReg(0x0008, 0x0303); // 帧率设置 LCD_WriteReg(0x000F, 0x0000); // 关闭测试模式 LCD_WriteReg(0x0010, 0x0000); // 电源控制启动 LCD_WriteReg(0x0020, 0x0000); // 水平地址起点 LCD_WriteReg(0x0021, 0x0000); // 垂直地址起点 LCD_WriteReg(0x0050, 0x0000); // 窗口水平起点 LCD_WriteReg(0x0051, 0x00EF); // 窗口水平终点 239 LCD_WriteReg(0x0052, 0x0000); // 窗口垂直起点 LCD_WriteReg(0x0053, 0x013F); // 窗口垂直终点 319 LCD_WriteReg(0x0007, 0x0133); // 重新开启显示 }LCD_WriteReg的常见实现是先写寄存器地址到命令地址再写数据到数据地址。以宏定义的形式放在 9325tp.h 中可以避免每次调用都开函数栈。上面代码里的0x0003寄存器在不同数据手册里的叫法不同但作用都是配置 RGB/BGR 顺序如果你的屏幕显示的红蓝位置互换调整这个寄存器的第 4、5 位。序列完成后跑一次读 ID 指令来验证总线是否正确读回 ID 通常是 0x9325 或者 0x9320如果读回是 0xFFFF基本可以判断 FSMC 时序配置有误或者数据线没有连接到位。3. 把照片变成 TFT 能吃的数据取模、窗口写显存与局部刷新3.1 RGB565 与图片取模的坑电脑上的图片大多是 24 位 RGB888而 ILI9325 的显存格式是 RGB565每个像素用 2 字节表达红色占高 5 位、绿色中间 6 位、蓝色低 5 位。直接把 BMP 数据copy到显存会出现颜色完全错位比如纯红色显示成洋红。因此在把图片灌进相框前必须先转换颜色格式。常见的转换公式是rgb565 ((r 3) 11) | ((g 2) 5) | (b 3)在批量转换时可以用查表法加速但更常用的方案是用 Python 脚本预处理把图片转成单片机平台可以直接用的.c 数组或写到 SD 卡的 BMP 文件里。数码相框有两种资源加载路线一是把图片全部做成 C 数组烧进 Flash优点是稳定、不需要文件系统缺点是图片数量受限二是用 SD 卡存 BMP/JPEG 文件通过 FATFS 边读边刷。做项目时我一般选第二种因为相框的核心卖点就是换图方便把新图拷进 SD 卡比重新烧录固件友好得多。取模方向是另一个大坑。上面初始化的0x0003寄存器设置了扫描方向如果屏幕 A 面朝上和 B 面朝上的取模顺序不同图片会上下颠倒。9325tp.h 里通常有LCD_set_oritation()函数切换扫描方向后必须同步调整图片数据的起始坐标。下表列出常见场景下的取值屏幕旋转水平显存起点垂直显存起点实际效果0 度00左上角开始扫描90 度0239从左下角开始180 度239239从右下角开始270 度2390从右上角开始3.2 用窗口函数刷整幅图避免全局显存等待全屏刷图最忌讳一块一块慢慢写像素。ILI9325 支持设置一个显示窗口窗口内的所有坐标自动递增写指针这就是 9325tp.h 里的LCD_set_window(x0, y0, x1, y1)函数。设置窗口后批量写显存只需要在窗口起点的物理地址开始按顺序写像素。来看一段直接从 SD 卡读 BMP 并刷屏的代码void ShowBMPFile(const char *path) { FIL file; UINT bytes_read; uint8_t bmp_header[54]; uint8_t line_buf[480]; // 240 像素 * 2 字节 uint16_t x0, y0; uint32_t pixel_offset; f_open(file, path, FA_READ); f_read(file, bmp_header, 54, bytes_read); // 解析 BMP 文件头BMP 数据偏移量在 0x0A 处 pixel_offset *(uint32_t *)bmp_header[10]; int32_t width *(int32_t *)bmp_header[18]; int32_t height *(int32_t *)bmp_header[22]; uint16_t bpp *(uint16_t *)bmp_header[28]; if (bpp ! 24 bpp ! 16) { f_close(file); return; } // BMP 的行是自下而上存储这里从最后一行开始读 LCD_set_window(0, 0, width - 1, height - 1); for (int i 0; i height; i) { // 计算实际文件行位置 uint32_t seek_pos pixel_offset (height - 1 - i) * width * (bpp / 8); f_lseek(file, seek_pos); f_read(file, line_buf, width * 2, bytes_read); for (int x 0; x width; x) { uint8_t b line_buf[x * 2 0]; uint8_t g line_buf[x * 2 1]; uint8_t r line_buf[x * 2 2]; uint16_t color; if (bpp 24) { color (r 8) 0xF800; color | (g 3) 0x07E0; color | (b 3) 0x001F; } else { // 16 位直接使用 color (line_buf[x * 2] | (line_buf[x * 2 1] 8)); } // 每次写一个像素代码演示保留可读性 // 实际项目可以连续写一段内存提高性能 LCD_WriteData(color); } } f_close(file); }这段代码展示了 BMP 读取到像素输出的完整路径但注意它每次只写一个像素频繁切换文件句柄和刷新总线会让速度很慢。在 24 位 BMP 里每一行末尾还可能有 0-3 字节的补齐内容读取时需要按(width * 3 3) ~3计算行字节数否则下一行的起点会错位。上面代码里为了简化假设行字节数是width*2只适合 16 位 BMP。更好的做法是把一行写入逻辑拆成两个部分先推进文件指针再用一个LCD_WriteDataBuffer(buffer, length)函数批量刷显存。批量刷可以利用 FSMC 连续写地址的 burst 特性也可以挂 DMA 到数据总线把 CPU 释放出来做 JPEG 解码。很多 9325tp.h 里的LCD_WriteData在循环里被反复调用这是效率瓶颈。我一般会在工程里加一个内联函数将数据指针强转成uint16_t*连续输出指定长度例如LCD_WriteData_DMA(buf, 240)。3.3 JPEG 图片的流式解码策略相框项目如果只支持 BMP每张图片轻易超过 300KB擦写 Flash 一次要等几秒。换 JPEG 可以压缩到 30-50KB但解码是难点。单片机平台常用方案是 TjpgDec它是一个面向嵌入式系统的 JPEG 解码库解码器通过回调函数读取输入流并通过jd_decomp逐块输出。关键点在于解码器的输出不一定是整张图而是按 MCU 块输出每解码完一个 MCU需要把输出数据写到屏幕上对应位置不能再假设图片是从头部开始连续排列的。使用 TjpgDec 时要自定义一个输出回调函数传入坐标和 RGB 数组然后在回调内调用LCD_set_window设置那个 MCU 块的区域再填入数据。数码相框切图时用户滑动触摸屏后立即切到下一张这时如果解码一帧需要 800ms会明显卡顿。常见的优化方案是降采样先把 JPEG 缩放到 LCD 分辨率再送到屏幕。TjpgDec 的JD_TINY模式和wd,ht参数可以控制缩放因素设置采样率为 1/2 或 1/4解码速度能提升约一倍成本是轻微锯齿。对照片展示来说1/4 缩放在 320×240 屏幕上观感完全可接受。4. 触摸屏滑动手感来自哪四点校准法与坐标映射4.1 电阻触摸屏的原始读数与不稳定因素数码相框的触摸屏多数是四线电阻式四个引出线分别接 X、X-、Y、Y-。读取时需要对某对电极施加基准电压在另一对电极上通过 ADC 测电压分压。9325tp.h 里的TP_XP,TP_XM,TP_YP,TP_YM就是这四个通道的控制引脚。触摸屏的原始 ADC 值并不是像素坐标它和屏面的机械装配角度、玻璃折射、前框偏移都有关系。同一套触摸面板的 ADC 值在屏幕不同位置会有明显的非线性区域尤其是边缘处。很多人直接用比例缩放x adc_x * lcd_width / 4095结果点屏幕右上角光标却出现在左上角。原因很简单ADC 值方向和像素地址方向不一定一致需要先做物理上的反向映射。触摸屏的标准读取流程是先给 X 轴的两个引脚一端接电源一端接 GND在 Y 轴的两个引脚上采集电压得到 X 坐标相关值。这个值并不是真正线性关系面板电阻会随温度漂移时间久了还会产生氧化层因此不能用固定公式一次性解决。工程上必须先校准校准的本质是找到从触摸 ADC 空间到屏幕像素空间的线性变换。四线电阻屏一般用仿射变换模型屏幕坐标和触摸坐标之间是一个二维线性关系加平移量。用三个校准点可以解出六个参数但实际中让用户点四个角并参与线性最小二乘可以抵消单点触摸时的随机抖动。4.2 四点校准法的参数求解代码校准流程四个目标屏幕坐标完全由代码定义比如显示四个十字光标让用户依次点击。每次点击后记录 ADC 读取值如 12 位 ADC 读出 0-4095。然后把四组对应点放进线性方程组目标是找到参数 A、B、C使得pixel_x A * adc_x B * adc_y C最小二乘逼近四个点。同样的方法再对pixel_y求 D、E、F。因为四个点的约束方程数量大于未知数最小二乘正好可以用正规方程求解。下面是求解参数的 C 语言片段typedef struct { double A, B, C; // x 映射参数: px A*ax B*ay C double D, E, F; // y 映射参数: py D*ax E*ay F } Calibration; Calibration calib; int Calibrate_4Point(Point lcd_pts[4], Point adc_pts[4]) { // 构造正规方程: M^T M * solution M^T * b double M[4][3]; double b_x[4], b_y[4]; for (int i 0; i 4; i) { M[i][0] adc_pts[i].x; M[i][1] adc_pts[i].y; M[i][2] 1.0; b_x[i] lcd_pts[i].x; b_y[i] lcd_pts[i].y; } double M_T_M[3][3] {0}; for (int r 0; r 3; r) { for (int c 0; c 3; c) { for (int i 0; i 4; i) { M_T_M[r][c] M[i][r] * M[i][c]; } } } // 计算 M^T * b_x double rhs_x[3] {0}; for (int r 0; r 3; r) { for (int i 0; i 4; i) { rhs_x[r] M[i][r] * b_x[i]; } } // 解 3 元线性方程组使用克拉克算法这里省略求逆过程 double inv[3][3]; InvertMatrix3x3(M_T_M, inv); calib.A inv[0][0]*rhs_x[0] inv[0][1]*rhs_x[1] inv[0][2]*rhs_x[2]; calib.B inv[1][0]*rhs_x[0] inv[1][1]*rhs_x[1] inv[1][2]*rhs_x[2]; calib.C inv[2][0]*rhs_x[0] inv[2][1]*rhs_x[1] inv[2][2]*rhs_x[2]; // Y 轴参数使用同样的 M 矩阵和 rhs_y double rhs_y[3] {0}; for (int r 0; r 3; r) { for (int i 0; i 4; i) { rhs_y[r] M[i][r] * b_y[i]; } } calib.D inv[0][0]*rhs_y[0] inv[0][1]*rhs_y[1] inv[0][2]*rhs_y[2]; calib.E inv[1][0]*rhs_y[0] inv[1][1]*rhs_y[1] inv[1][2]*rhs_y[2]; calib.F inv[2][0]*rhs_y[0] inv[2][1]*rhs_y[1] inv[2][2]*rhs_y[2]; return 0; } uint16_t Touch_To_Pixel_X(uint32_t ax, uint32_t ay) { return (uint16_t)(calib.A * ax calib.B * ay calib.C); }InvertMatrix3x3是标准的 3×3 矩阵求逆函数不需要引第三方库直接手写 30 行即可。这个方法要保护的一点是所有 ADC 值在参与计算前要滤波以消除硬件噪声否则矩阵参数会包含较大随机误差导致四个校准验证点中有一个点误差超过 2mm。滤波我用中值滤波取 5 次采样排序后取中间值对高频抖动抑制效果比均值滤波稳定且不会拖慢系统响应。参数计算完成后把质量好的校准点读回验证如果差的绝对值大于 15 像素就重新校准一次。4.3 参数存储与运行时的边界处理校准参数必须保存起来否则断电后再开机就要重新校准。常见的做法是写入单片机内部 Flash 的最后一页用两个校验字节做 CRC 判断。启动时读取到合法参数就直接跳过触摸校准页面否则显示“点击十字”界面。9325tp.h 里面不会有这种逻辑因为触摸屏型号决定了校准的具体实现方式。我在实际相框项目里做的是开机先读 Flash 中的标志位如果校验失败进入校准界面校准成功后把校准参数写回 Flash并立刻用刚才的四个点做一次命中测试。命中测试代码就是用上节的Touch_To_Pixel_X和Touch_To_Pixel_Y计算点击像素坐标再和已知的校准十字坐标比对。运行时的点击判断不能直接用解析出的坐标等于某个按钮区域因为触摸屏有漂移和手指接触面积的影响。我会用松开的判定延时 30ms然后对点击坐标做 20 像素的容错区间。相框常见的触摸操作是左右划屏切图这个手势通常被实现为按下坐标与松开坐标的差值。如果差值绝对值大于屏幕宽度的 1/4判断为切换手势如果小于这个值且按下时间不足 300ms判断为单击。每一项都要加一个短暂消抖否则误触率很难控制。触摸异常现象产生原因排查方向左上角点击变成右下角响应校准变换矩阵参数计算错误重新执行校准检查 ADC 正负极方向触摸偶尔不响应ADC 采样没有滤波或重复按下增加中值滤波和定时去抖边缘点击偏移明显四线屏在边缘非线性导致的改用多点校准或对边缘区域二次校正开机后校准参数失效Flash 写入时电压不足或未做校验检查写入函数是否等待操作完成5. 从点屏到成品的 3 个细节背光、帧率与排错5.1 用定时器 PWM 调背光兼顾亮度和功耗数码相框长时间开着背光 LED 一直满亮度会让 LCD 面板局部发黄还会加快触摸屏的电阻层老化。用 STM32 定时器输出 PWM 控制一个 NPN 三极管或低边 MOS比较优雅。常用频率是 20kHz 到 30kHz低于 10kHz 的话部分人眼会看到频闪。不建议在初始化里把背光开满后就不关而是在 9325tp.h 的驱动层之外再包一个显示状态结构体保存当前亮度值每次上电恢复上次的亮度。调光曲线用 sqrt 修正因为 LED 的光通量并不随占空比线性变化。亮度的调节可以直接绑定为一个“设置”页面触摸拖动滑条改变 PWM 占空比。5.2 测一次全屏刷图耗时的具体方法优化性能前要先准确知道耗时。这里给一个简单的计时方式在刷图函数前后翻转一个 GPIO用逻辑分析仪或者示波器测量脉冲宽度。差别是第一种开发方式占用的引脚少第二种对屏幕刷新本身的干扰最小。下面是一段标准计时代码void ShowImage_Timed(void) { GPIOE-BSRR GPIO_PIN_1; // 拉高测试引脚 uint32_t t0 TIM2-CNT; ShowBMPFile(/photo_1.bmp); uint32_t t1 TIM2-CNT; GPIOE-BSRR GPIO_PIN_1 16; // 拉低测试引脚 uint32_t elapsed_ms (t1 - t0) / 1000; // 时钟频率 1kHz printf(refresh time %lu ms\r\n, elapsed_ms); }看到refresh time超过了 300ms 时优化顺序是先检查 FSMC 的 DataSetupTime、背光之外的等待延迟再考虑把 JPEG 解码输出的单点写改成批量写。在真实项目里码率从 300ms 下降到 120ms 的常用操作是对齐局部窗口LCD_set_window只设置到本次刷新的图像区域不要整屏重画。比如相框切换时只有下半部分变化就能省接近一半耗时。5.3 白屏和花屏的快速定位顺序白屏先查以下几个方面复位引脚有没有被拉死FSMC 使能是否正确读取设备 ID 是否结果非零。花屏则要区分是颜色错位还是图像错位。颜色错位优先查看 RGB 顺序配置寄存器的值把红色和蓝色对调过来测试图像错位则检查窗口坐标是否超过屏幕范围。最后再看触摸排线如果触摸引脚和 FSMC 数据线在 PCB 上不是等长走线高速刷图时会产生串扰导致 ADC 值跳动。这类问题用迟到滤波和中值滤波解决不了只能重新设计 PCB 或把触摸采样延时加长到 100 微秒以上。整个相框做到这个程度剩下的就是通过 SD 卡批量加载图片和给设备增加一个照片删除功能让相框能真正交给终端用户使用。本文还有配套的精品资源点击获取
返回列表