ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32驱动TFTLCD实战:FSMC并口初始化与刷屏排查

STM32驱动TFTLCD实战:FSMC并口初始化与刷屏排查

简介:面向嵌入式初学者与课程实验场景的STM32 TFTLCD显示工程,基于STM32F10x平台演示GPIO、SPI等接口与液晶驱动芯片的配合,涵盖屏幕初始化、显示缓冲区管理、画点画线、字符与图像绘制以及屏幕刷新等关键环节,可用于理解裸机驱动LCD的完整流程并动手验证。压缩包共40个文件、约87KB,以18个h头文件和11个c源文件为主体,配套Keil工程配置、启动汇编、hex烧录文件、JLink调试设置以及说明和批处理文件;目录按SYSTEM、USER、HARDWARE等模块划分,串口、延时、按键、LED、OLED、定时器和看门狗等外设代码可一并对照学习。目前已有5190人学习,工程保留可编译与烧录文件,便于从源码梳理驱动分层、时序配置和显示异常排查。读者可据此对照驱动芯片初始化命令、坐标设置与数据写入等细节,快速定位花屏、偏移等常见问题,也能作为课程实验、毕业设计及项目移植的基础模板。

1. 一块 2.4 寸屏点亮的那三秒,比调通串口踏实多了

有位做工业面板的开发者跟我聊过一件事:板子上电,串口打印一切正常,可挂上去的 TFTLCD 屏就是不亮,白得发亮,或者干脆全黑。没有报错,没有日志,屏幕本身就是个黑匣子,它不告诉你 RS 接反了、还是复位时序短了 100 微秒。这类问题最耗时间,也最容易劝退刚摸 STM32 外设的人。这份 TFTLCD 显示程序把从引脚初始化、驱动 IC 时序、开窗刷屏到字符与图片显示的整条链路都写进了工程,编译烧录就能看到画面,属于亲测能跑通的那一类。它适合两类人:一类是刚学完 GPIO、想找个现成模板把屏点亮的;另一类是手上老项目要加显示、不想从零写驱动的。

2. 项目定位与技术选型:这块屏是怎么被点亮的

拿到一个显示工程,先别急着看main.c。正确的顺序是看它怎么和屏通信、驱动的是什么芯片、代码怎么分层。这三件事决定了后面所有调试手段的有效性,也决定你能不能在换屏、换板子的时候少返工。

2.1 并口还是 SPI:两种接法的取舍

STM32 驱动 TFTLCD 的主流做法只有两条路:8080 并口(一般走 FSMC 或 FMSC 这类外部总线控制器)和四线 SPI。这份工程用的是并口方案,原因很直接——刷屏要快。屏幕分辨率的典型值是 240×320,按 RGB565 算一帧 153600 字节,SPI 在 18MHz 下把这一坨数据推出去得几十毫秒量级,如果上层还要画控件,帧率根本不够看;并口下硬件自动拉 RE/WE 时序,CPU 只管往一个地址写数据,同样的全屏填充能压到几毫秒量级。

代价也很明显。并口要吃掉 16 根数据线加若干控制线,走线一多,杜邦线插一排就很容易出现某根接触不良;而且并行总线对布线有要求,板子做小了走线打架是常事。我在选型时一般这么判断:

对比项8080 并口(FSMC)四线 SPI
占用 IO16 位数据 + CS/RS/WR/RD/RST 等4~5 根(SCLK/MOSI/CS/RS/RST)
全屏刷新几毫秒量级十几到几十毫秒量级
CPU 占用低,时序由硬件产生高,每字节都要循环等待
接线难度高,线多且容易接触不良低,插错也能改
适合场景刷图、动画、图形界面参数显示、低频刷新、引脚紧张的小板

工程里的lcd.h顶部通常有一组宏用来切换总线宽度和基地址,这份代码按 16 位并口组织。如果你手上的板子 IO 紧张,不必整个推翻,把写命令、写数据这两个原语换成 SPI 版实现,上层LCD_Fill、LCD_ShowString一行都不用动——这一点在第 6 章抽接口层的时候会具体展开。

另外一个容易忽略的点是背光。背光 LED 那条线很多板子直接接 3.3V,代码里根本不管它,结果屏是亮的但内容是黑的,新手第一反应是"驱动没跑起来",实际是背光没有 PWM 控制、亮度被拉满导致对比度看不出。初始化代码里如果有一组LCD_BL相关的 GPIO 操作,记得一起对。

2.2 驱动 IC 与初始化序列的对应关系

屏上那层玻璃只是像素阵列,真正干活的是背后的驱动 IC。市面上 240×320 这个规格常见的型号是 ILI9341、ST7789 这一档,两者初始化寄存器大体相似但细节不同,尤其是 Gamma 校正和部分电源控制寄存器,抄错一组就表现为颜色发灰或者偏色。这份工程的初始化表是配套具体模组的,换屏之后第一件事就是确认这张表对不对得上。

初始化序列本质上是一串"写寄存器地址 + 写参数"的调用,顺序不能乱。典型流程是:硬件复位(拉低 RST 至少 10 微秒,再拉高并等待 120 毫秒)→ 软件复位0x01→ 读 ID 校验 → 电源控制 → 显存访问控制0x36→ 像素格式0x3A→ Gamma → 开显示0x29。其中0x36(MADCTL)决定扫描方向和 RGB/BGR 顺序,是后面所有显示错位的根源;0x3A一般写0x55,表示 16 位 RGB565,如果这里写成 18 位,屏幕上就会看到颜色被"重新分配"了一样的花屏。

这里有个血泪经验:读 ID 那一步很多人直接跳过。读 ID 需要把数据线方向切到输入,而 FSMC 配置成只写模式的话读回来永远是0xFFFF。所以正确做法是先把 ID 打印到串口,确认读到0x9341或0x7789之类的值,再往下走。ID 读不到,后面所有调试都是在猜。

2.3 目录结构与文件职责

工程的组织方式比较朴素,没有上 RTOS,也没有做复杂的抽象层,基本是"一个外设一组文件"的风格。这种结构对新手友好,但也要接受它耦合较紧的毛病。

Project/ ├── CORE/ # 内核启动与系统初始化 ├── STM32F10x_FWLib/ # 标准外设库,含 GPIO / FSMC / RCC 等 ├── HARDWARE/ │ ├── LCD/ │ │ ├── lcd.c # 读写原语、初始化序列、填充与字符 │ │ ├── lcd.h # 总线宏、颜色宏、对外函数声明 │ │ ├── lcdfont.h # ASCII 字模(12x6、16x8、24x12 常用三档) │ │ └── bmp.h # 图片取模数组,按 RGB565 存放 │ └── LED/ # 板载指示灯,用来做点亮反馈 ├── USER/ │ ├── main.c # 应用入口,演示填充、画线、显示文字与图片 │ └── stm32f10x_it.c # 中断服务 └── OBJ/ # 编译输出

分层的边界很清楚:main.c只调用LCD_Init、LCD_Clear、LCD_ShowString这类语义化函数,不去碰寄存器;lcd.c负责把语义翻译成寄存器操作;字模和图片放在独立头文件里,按const数组存进 Flash,不占 RAM。新手常犯的错误是把几百 KB 的图片数组直接塞进main.c,编译后 RAM 溢出,链接阶段就报错——这个结构里把大数组放const是个必须保留的习惯。

lcdfont.h的字模是典型的横向取模、高位在前。取模软件的设置(阴码/阳码、逐行/逐列、C51 格式)必须和LCD_ShowChar里的解析逻辑一致,这属于"玄学"环节:明明数组长度对,显示出来却是乱码,十有八九是取模方向反了。

3. 核心模块拆解:从一条命令到满屏像素

显示驱动的全部秘密就在三个动作里:写命令、写数据、连续写像素。把这三件事讲透,剩下的字符、图形、图片都是在它们之上做循环。

3.1 写命令与写数据:并口时序的两种写法

并口的精髓在于把"命令还是数据"这件事交给地址线。RS 接到某根地址线(常见是 A0 或者 A16),访问不同地址时硬件自动拉高拉低,CPU 侧就退化成了两次内存写。

/* lcd.h —— 16 位并口下最基础的地址映射 */ #define LCD_BASE ((uint32_t)0x6C000000) /* FSMC Bank1 NE1 基地址 */ #define LCD_REG (*(volatile uint16_t *)(LCD_BASE)) /* RS = 0,写命令 */ #define LCD_RAM (*(volatile uint16_t *)(LCD_BASE + 0x2)) /* RS = 1,写数据 */
/* lcd.c —— 全部上层函数最终都落到这两行赋值上 */ void LCD_WR_REG(uint16_t reg) { LCD_REG = reg; /* 地址线决定 RS 电平,硬件自动产生 WR 脉冲 */ } void LCD_WR_DATA(uint16_t data) { LCD_RAM = data; /* 数据宽度由 FSMC 配置决定,必须保持 16 位一致 */ } uint16_t LCD_RD_DATA(void) { return LCD_RAM; /* 读之前要把 FSMC 的数据线方向切到输入,否则恒为 0xFFFF */ }

参数上最容易出错的是那个0x2偏移。16 位总线意味着一个像素占两个字节,地址线 A0 对应 CPU 地址的 bit1,所以"下一根地址线"的偏移是 2 而不是 1。偏移写错,命令和数据会全部落到同一个地址上,现象就是屏上出现无规律的噪点。另外LCD_BASE必须和 FSMC 初始化里的FSMC_NORSRAMInitStructure.FSMC_Bank以及FSMC_DataWidth对齐,库函数里如果配了 8 位,写 16 位数据会被拆成两次访问,屏上就是半屏花。

3.2 初始化序列的逐条含义

初始化表一般写成结构体数组,遍历执行。这样换屏只改表,不改逻辑。

/* 精简后的关键几组,实际工程里会有 30 条左右 */ typedef struct { uint16_t reg; uint16_t val; uint16_t delay_ms; /* 该条执行后需要等待的时间,0 表示不等 */ } lcd_init_cmd_t; static const lcd_init_cmd_t lcd_init_table[] = { {0x01, 0x0000, 120}, /* 软件复位,必须等 120ms 再往下走 */ {0x11, 0x0000, 120}, /* 退出睡眠 */ {0x36, 0x0068, 0}, /* MADCTL:横屏 + BGR 顺序,具体值看模组丝印 */ {0x3A, 0x0055, 0}, /* 像素格式 16bit RGB565 */ {0x29, 0x0000, 20}, /* 开显示 */ }; void LCD_Init(void) { LCD_GPIO_Config(); /* 先把 RST / BL / 数据线全部配好,顺序不能反 */ LCD_FSMC_Config(); /* 总线时序:地址建立、数据保持按屏手册填 */ LCD_RST_LOW(); delay_ms(20); /* 复位脉宽低于 10us 部分模组认不到 */ LCD_RST_HIGH(); delay_ms(120); for (uint32_t i = 0; i < sizeof(lcd_init_table) / sizeof(lcd_init_table[0]); i++) { LCD_WR_REG(lcd_init_table[i].reg); if (lcd_init_table[i].reg != 0x01 && lcd_init_table[i].reg != 0x11) { LCD_WR_DATA(lcd_init_table[i].val); } if (lcd_init_table[i].delay_ms) { delay_ms(lcd_init_table[i].delay_ms); } } LCD_Clear(WHITE); }

delay_ms这一列是初始化表里最容易被删掉的部分。有人觉得等 120 毫秒太久,想省掉,结果就是时好时坏——上电电压爬升慢的时候屏没吃完复位就收到了命令。0x01和0x11不需要写参数,所以代码里单独判断了一下,如果你的表里把它们写成带参数形式,多写的一个字节会被驱动 IC 当成下一条命令,后面整条链就歪了。

0x36的值直接决定坐标原点在哪个角。0x68和0x48的区别就是 X/Y 交换加镜像,屏幕上表现为文字倒了 90 度或者左右反了。这个值不用背,改一次刷一次色块,两分钟就能试出来。

3.3 开窗与连续写像素:刷屏效率的关键

逐点设置坐标再写颜色是最慢的做法,因为每次都要发0x2A、0x2B、0x2C三条命令。正确姿势是先划一个矩形窗口,之后每写一个 16 位数据,驱动 IC 自己把坐标往后推。

/* 设置显示窗口,后续所有像素按从左到右、从上到下的顺序自动填充 */ void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WR_REG(0x2A); /* 列地址 */ LCD_WR_DATA(x0 >> 8); LCD_WR_DATA(x0 & 0xFF); LCD_WR_DATA(x1 >> 8); LCD_WR_DATA(x1 & 0xFF); LCD_WR_REG(0x2B); /* 行地址 */ LCD_WR_DATA(y0 >> 8); LCD_WR_DATA(y0 & 0xFF); LCD_WR_DATA(y1 >> 8); LCD_WR_DATA(y1 & 0xFF); LCD_WR_REG(0x2C); /* 写显存,进入连续写模式 */ } void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t total = (uint32_t)(x1 - x0 + 1) * (y1 - y0 + 1); LCD_SetWindow(x0, y0, x1, y1); while (total--) { LCD_RAM = color; /* 只有地址写,不再有命令开销 */ } }

total一定要用 32 位。全屏 240×320 是 76800,uint16_t装不下,越界之后循环次数变成 76800 取模,屏幕只填了一部分,剩下的保持上一次的内容——这种"填一半"的现象非常像内存被踩,查半天查不出来。参数越界同理,x1大于屏宽时写入会绕回下一行,看起来像是画面错位。

LCD_Clear就是LCD_Fill(0, 0, width - 1, height - 1, color)。这里顺手提一句,工程里如果有LCD_DrawLine、LCD_DrawCircle这类函数,它们内部也是反复调LCD_SetWindow加一次写像素,效率一般,画静态界面够用,要做动画就得换成带显存缓冲的方案。

3.4 字符、图片与取模参数

字符显示是把字模数组按位展开成像素。这份工程用的是 12×6 和 16×8 两种 ASCII 点阵,一个字符占 12 或 16 个字节。

/* 12x6 ASCII:每字符 12 字节,横向取模,高位在前 */ void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t fc, uint16_t bc, uint8_t size) { uint8_t i, j; uint8_t bytes = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); uint32_t off = (uint32_t)(ch - ' ') * bytes; /* 字库从空格开始排 */ const uint8_t *p = asc2_1206 + off; LCD_SetWindow(x, y, x + size / 2 - 1, y + size - 1); for (i = 0; i < bytes; i++) { uint8_t dat = p[i]; for (j = 0; j < 8; j++) { LCD_RAM = (dat & 0x80) ? fc : bc; /* 1 画前景,0 画背景 */ dat <<= 1; } } }

参数fc和bc是前景色与背景色。想叠加显示(比如在图片上写字)就把bc传成透明标志,函数里改成"背景色跳过不写",代价是每个像素都要判断一次,速度掉一截。图片显示则是把一个const uint16_t数组整个推进窗口,长度必须是width * height,多一个少一个都会从第二行开始整体偏移——这类问题用纯色图看不出来,得拿带边框的测试图才暴露。

4. 跑起来:从导入工程到屏幕出现第一行字

代码看懂和跑起来是两件事。这一章按顺序走一遍,每一步都给出"做完应该看到什么",出了问题就停在那一步,别往下冲。

4.1 工程导入与编译环境

工程是标准的 Keil MDK 结构,带.uvprojx工程文件。打开之后先做三件事:确认器件型号和实际芯片一致(选错型号编译能过,但启动文件和 Flash 算法会不匹配)、确认Options for Target里勾了Use MicroLIB(printf重定向到串口的话必须勾)、确认C/C++页的宏定义里有对应的芯片型号宏,标准库靠这个宏来选启动文件。

如果手上有 arm-none-eabi 工具链,也可以直接走 Makefile:

# 清理后重新编译,-j4 按核数并行 make clean make -j4 # 确认产物:elf 用于调试,bin 和 hex 用于烧录 ls -l build/*.elf build/*.bin build/*.hex # 看 Flash 占用,超过 128K 就得改分区或者裁字库 arm-none-eabi-size build/app.elf

arm-none-eabi-size的输出里 text 段是 Flash、data+bss 是 RAM。字库和图片数组都在 text 段,占几十 KB 很正常,但如果 text 突然涨了几百 K,说明有张大图忘了加const,被塞到 RAM 里去了。

烧录两种方式都行。Keil 里点 Download 最省事;命令行可以这样:

# ST-Link 走 OpenOCD 烧录,等价于 IDE 里的下载按钮 openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \ -c "program build/app.bin 0x08000000 verify reset exit"

0x08000000是内部 Flash 起始地址,如果你的板子做了 IAP 分区,得改成应用区偏移,比如0x08008000,否则程序跑起来会卡在中断向量表里。

4.2 引脚、时钟与复用功能检查

这一步是纯检查,不写代码,但能省掉 80% 的调试时间。需要对照的原理图信息有四项:FSMC 的数据线和控制线接到哪几组 IO、背光控制脚是哪个、复位脚是哪个、有没有用外部晶振。

FSMC 的引脚是固定的复用功能,配错了不会报错,只会静默不输出。常见翻车点是把NE1接到了片选脚却把FSMC_Bank配成NE2,读写的地址段对不上;或者FSMC_DataWidth配 8 位而硬件是 16 位接线。检查方法很土但有效:初始化完之后用万用表或者示波器看WR引脚,全屏填充的时候它应该在持续跳变,不跳说明总线根本没工作。

时钟也要看一眼。FSMC 挂在 AHB 上,如果SystemInit里把主频配到 72MHz,读写一个像素大概几十纳秒;配到 24MHz 也能跑,但刷屏肉眼可见地慢。另外总线时序那几个参数(地址建立时间、数据保持时间)必须按屏的手册填,填太快屏跟不上,现象是随机丢像素。

4.3 分级验证:先色块,再文字,最后图片

main.c里的演示顺序建议照这个来,一层通过再进下一层。

int main(void) { delay_init(); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); uart_init(115200); /* 先把串口调好,后面所有调试都靠它 */ LCD_Init(); /* 内部已含一次 LCD_Clear(WHITE) */ /* 第一级:三色块,验证开窗和颜色顺序 */ LCD_Fill(0, 0, 119, 159, RED); LCD_Fill(120, 0, 239, 159, GREEN); LCD_Fill(0, 160, 239, 319, BLUE); /* 第二级:字符,验证字模方向和取模方式 */ LCD_ShowString(20, 20, "TFTLCD OK", RED, WHITE, 16, 0); /* 第三级:图片,验证数组长度和 RGB565 字节序 */ LCD_ShowPicture(0, 0, 240, 320, gImage_test); while (1) { LED0 = !LED0; delay_ms(500); } }

第一级如果红绿蓝位置不对,那是0x36的扫描方向问题,改 MADCTL 值;如果三个块都出来但颜色是蓝红互换,那是 RGB/BGR 顺序反了,同样在0x36里改一位。第二级出乱码,去查取模软件设置。第三级出现整体平移或者只有上半屏,去查数组长度和窗口高度是否一致。板载 LED 闪烁是最后一道保险,它说明主循环没卡死,画面问题是纯显示层的事。

5. 避坑与排查:白屏、花屏、半屏错位的真实原因

5.1 白屏与全黑

现象:上电之后屏幕均匀发白,看不到任何图案,串口打印正常。

原因基本锁定在三个地方:复位没生效、背光被拉满导致对比度看不出、初始化序列没跑完就退出。有些模组的 RST 引脚在模块内部已经有 RC 复位电路,外部再拉低反而冲突;另一些模组的 RST 悬空,全靠软件复位,这时候0x01那条命令的 120 毫秒等待就必须保留。

解决顺序:先用万用表量背光脚电压,再在LCD_Init结束后加一句LCD_Fill刷个纯黑,黑能出来说明驱动活了,只是没画东西;黑出不来就回头查 RST 和 FSMC 的片选。全黑且背光不亮的情况更简单,直接查背光供电那条线,跟驱动代码没关系。

5.2 花屏、错位与颜色反相

现象:能显示内容,但满屏雪花点、或者图像整体偏移几十个像素、或者颜色明显发灰发蓝。

雪花点优先怀疑总线。数据线某一位虚焊或者杜邦线松动,写进去的数据就会随机翻转,表现出来就是噪点。判断方法是刷一个固定色块,噪点跟着出现就说明是数据通路。错位则八成是LCD_SetWindow的起止坐标写反了,x0 > x1时驱动 IC 会按自己的规则处理,结果是整块内容被折到另一边。

颜色反相分两种。整体发灰通常是 Gamma 校正那几条用了别的屏的参数,把 Gamma 段整段注释掉再看一眼就能确认;蓝红互换是 RGB/BGR 位,改0x36。这里有个后悔药:把0x36的候选值写成一个数组,用按键或者串口命令轮流切换,比反复烧录快得多。

5.3 刷屏慢、撕裂与复位时序

现象:画面能显示,但整屏刷新肉眼可见地一行一行扫下来,或者刷新过程中出现上下半屏不同步。

慢的原因通常是两个:一是逐点设坐标而没有开窗,命令开销远大于数据;二是 FSMC 时序参数填得过保守,地址建立时间给了几百纳秒。前者改成LCD_Fill那种连续写模式,后者按屏手册的下限往上调一档。撕裂是另一回事,它来自"边写边显示",驱动 IC 正在扫描的时候你改了显存,上下半屏就来自两个时刻。彻底的解法是双缓冲或者等垂直消隐再写,成本较高;工程里的做法是尽量把整屏更新压缩在一次窗口写入内完成,减少中间被打断的机会。

还有一种很隐蔽的情况:屏幕偶尔闪一下就恢复正常,重启又好了。这类问题多半是电源纹波,背光瞬间电流把 3.3V 拉低,MCU 复位了但你没察觉。加个大电容,或者在初始化里多等一会儿,往往就消失了。

6. 二次开发:把显示层抽成可替换的 GUI 底座

这份代码最值得动手改的地方,不是加更多绘图函数,而是把lcd.c里那堆直接操作寄存器的函数收进一个驱动结构体。原因很实际:你迟早要换屏。240×320 换成 320×480,或者并口换成 SPI,如果没有这层抽象,main.c里几百行调用全得改一遍。

我一般这么抽:

/* lcd_drv.h —— 上层只认这组接口,换屏只改注册的那一份实现 */ typedef struct { void (*init)(void); void (*set_window)(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void (*write_pixel)(uint16_t color); void (*fill)(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color); uint16_t width; uint16_t height; uint8_t dir; /* 0 竖屏 1 横屏,决定坐标要不要交换 */ } lcd_drv_t; /* 具体实现文件里导出唯一的实例,上层通过指针访问 */ extern const lcd_drv_t lcd_ili9341_8080; extern const lcd_drv_t lcd_st7789_spi; void lcd_register(const lcd_drv_t *drv); /* 启动时注册一次 */ void lcd_draw_text(uint16_t x, uint16_t y, const char *s, uint16_t fc, uint16_t bc);

抽完之后,LCD_ShowString这类函数只依赖set_window和write_pixel两个原语,坐标换算统一放在lcd_register里做一次,横竖屏切换就不用改任何业务代码。实际改造时按这个顺序验证,一步一验,别一次改完再烧:

改造点改动位置验证方法
总线层替换lcd_port_xxx.c里的两个写原语读 ID 能读到正确值
屏幕参数化width/height/dir字段四角色块刚好铺满不越界
上层解耦业务代码改用lcd_drv_t *换一份实现重新编译,业务代码零改动
字库裁剪lcdfont.h只保留用到的字号编译后 text 段明显缩小

验证手段上有个小技巧:在fill里加一个计数器,统计每次刷新的像素数,串口打出来跟(x1-x0+1)*(y1-y0+1)对一下,对不上就是坐标换算写错了。这比盯着屏幕数格子靠谱得多,尤其在横屏模式下。

最后说个我自己的教训。早些年调一块新屏,花屏花了两个晚上,把初始化表逐条注释、把时序参数从慢到快试了个遍,最后发现是排线第 7 根虚接——那种插针式的 FPC 座子,看着插进去了,其实只进了一半。从那以后我每次上电前都强制走一遍"先量通断、再刷纯色、最后上字模"的三步,不管这块板子是不是刚焊好的。硬件这行的坑,多半不在代码里。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表