简介:这是一份面向STM32嵌入式学习者的TFT彩屏驱动资料包,围绕STM32F103VET6通过FSMC接口驱动2.8英寸ILI9341彩色液晶屏展开,同时给出标准库与HAL库两套实现,适合从寄存器操作过渡到抽象API的开发者对照学习。包内共524个文件,以135个.h头文件和88个.c源文件为核心,配合Keil工程、调试配置文档及PDF手册,压缩包约29.62MB,目录覆盖驱动源码、工程构建与运行产物。已有1586人学习,说明其在FSMC彩屏入门场景中具备较高参考度。读者可据此理解GPIO与FSMC数据、控制线连接,FSMC_Bank1时序参数配置,ILI9341初始化命令序列,屏幕方向与分辨率设置,显示窗口和颜色模式切换,并通过双库代码对照掌握驱动移植与排错思路,形成从硬件接口到显示输出的完整实践链路。
1. 一块 2.8 寸屏插上 F103:FSMC 直驱 ILI9341 到底省了多少事
最早我拿 GPIO 模拟 8080 时序点亮这块 TFT2.8,刷新一屏 240×320 要将近 0.8 秒,肉眼能看着颜色从上往下淌。换成 STM32F10x 的 FSMC 硬件时序后,同一块 ILI9341、同一套画点逻辑,整屏填充直接掉到十几毫秒——代码几乎没改,只是把「软件翻转电平」换成了「往一个地址写数据」。压缩包里的东西就是这条链路的完整实现:标准库和 HAL 两套 FSMC 初始化、ILI9341 上电命令表、开窗填充、ASCII 与汉字显示,还有一份可以直接烧进去的工程骨架。适合手里已经有 F103 板子和并口彩屏、想把这屏真正用起来的人,也适合想把 FSMC 这块黑匣子拆开看的人。
2. 硬件映射与工程骨架:NE4 片选、RS 地址线和两套库的目录
先把「屏幕为什么能当内存写」这件事讲透,后面所有代码才有落脚点。ILI9341 的 8080-16 并口一共就那几根线:CS 片选、RS 数据/命令选择、WR 写、RD 读、D0~D15 数据,加上 RST 复位和背光。而 STM32F10x 的 FSMC 在驱动 NOR/SRAM 时,恰好能产出同样的时序:NE[x] 当地址选通、NWE 当写使能、NOE 当读使能、地址线 A[x] 当 RS、16 位数据总线接 D0~D15。两边一对上,屏幕在 CPU 眼里就是一个挂在外部存储区的 16 位设备,写命令是写地址 0,写数据是写地址 1。
2.1 片选决定基址:为什么是 0x6C000000
FSMC 的 Bank1 被切成四块,每块 64MB,NE1 到 NE4 分别对应 0x60000000、0x64000000、0x68000000、0x6C000000。绝大多数 2.8 寸模块的转接板把 CS 引到 NE4 上,所以基址是 0x6C000000;如果你的板子焊的是 NE1,那整段代码里的地址全要改成 0x60000000,这一点错一步就是全屏无反应。
标准库下的初始化长这样,重点是时序参数和 16 位总线宽度,两处配错都表现为花屏:
/* 标准库:Bank1 的 NOR/SRAM4,16 位数据总线,读写共用一套时序 */ void LCD_FSMC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; FSMC_NORSRAMInitTypeDef FSMC_InitStructure; FSMC_NORSRAMTimingInitTypeDef Timing; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); /* 数据线 D0~D15 与 NOE/NWE/NE4 必须配成复用推挽,配成普通输出一定读不到 ID */ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_14 | GPIO_Pin_15; /* PD 上的数据线 */ GPIO_Init(GPIOD, &GPIO_InitStructure); /* PE7~PE15 的数据线、PD4(NOE)、PD5(NWE)、PD7(NE4) 同理,此处省略重复配置 */ /* HCLK=72MHz,一拍约 13.9ns;先给保守值把屏点亮,再往下压时序提速度 */ Timing.FSMC_AddressSetupTime = 0x01; /* 地址建立 2 拍 */ Timing.FSMC_AddressHoldTime = 0x00; /* 地址保持 1 拍 */ Timing.FSMC_DataSetupTime = 0x03; /* 数据建立 4 拍,约 55ns */ Timing.FSMC_BusTurnAroundDuration = 0x00; Timing.FSMC_CLKDivision = 0x00; Timing.FSMC_DataLatency = 0x00; Timing.FSMC_AccessMode = FSMC_AccessMode_B; /* SRAM/液晶常用模式 B */ FSMC_InitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM4; FSMC_InitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; FSMC_InitStructure.FSMC_MemoryType = FSMC_MemoryType_SRAM; FSMC_InitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b; FSMC_InitStructure.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable; FSMC_InitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low; FSMC_InitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable; FSMC_InitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState; FSMC_InitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable; FSMC_InitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable; FSMC_InitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable; /* 读写同时序 */ FSMC_InitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable; FSMC_InitStructure.FSMC_ReadWriteTimingStruct = &Timing; FSMC_InitStructure.FSMC_WriteTimingStruct = &Timing; FSMC_NORSRAMInit(&FSMC_InitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE); }FSMC_MemoryType_SRAM是关键,选成 NOR Flash 会等一个根本不存在的就绪信号,直接卡死。ExtendedMode_Disable让读写共用一套时序,省事;如果你后面发现读 ID 老读成 0xFFFF,再打开扩展模式给读操作单独放宽DataSetupTime,这是两条不同的路。
2.2 RS 接哪根地址线,偏移就是多少
命令和数据靠地址偏移区分。指针写法最省事:
/* RS 接 FSMC_A0 时:16 位总线内部地址右移一位,HADDR[1] 才落到 A0 上 */ #define LCD_BASE ((uint32_t)0x6C000000) #define LCD_CMD (*(__IO uint16_t *)(LCD_BASE)) /* RS = 0 */ #define LCD_DATA (*(__IO uint16_t *)(LCD_BASE + 0x02)) /* RS = 1 */换算规则记牢:8 位总线偏移 = 1 << n,16 位总线偏移 = 2 << n,n 是 RS 所接的 FSMC_Ax 编号。RS 接 A6 就是 0x80,接 A16 就是 0x20000。很多人抄了别人的0x1E、0x02就往上怼,结果命令和数据全写进了同一个地址,屏幕表现为背光亮、内容全黑或满屏一个颜色,查半天以为是初始化序列错了。__IO不能省,它是volatile的封装,不加的话编译器可能把连续写合并掉。
2.3 目录结构与两套库的分工
包里的工程按驱动分层组织,标准库和 HAL 各占一个目录,共用同一份 ILI9341 命令表:
| 路径 | 作用 |
|---|---|
User/main.c | 上电初始化、测试画面、主循环 |
Hardware/lcd.c/lcd.h | 读写原语、初始化序列、开窗填充、字符与图片 |
Hardware/lcd_fsmc.c | FSMC 与 GPIO 的底层配置(标准库版) |
Hardware/font.h | 8×16 ASCII 字模与 16×16 汉字字模 |
STM32F10x_FWLib/ | 标准外设库源码 |
HAL/ | CubeMX 生成的工程,Core/Src/stm32f1xx_hal_msp.c里放 FSMC 引脚复用 |
HAL 那一套用的是结构体句柄,逻辑和标准库一一对应:
/* HAL:与上面标准库版本等价,参数名不同而已 */ SRAM_HandleTypeDef hsram; static void MX_FSMC_Init(void) { FSMC_NORSRAM_TimingTypeDef t = {0}; hsram.Instance = FSMC_NORSRAM_DEVICE; hsram.Extended = FSMC_NORSRAM_EXTENDED_DEVICE; hsram.Init.NSBank = FSMC_NORSRAM_BANK4; hsram.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; hsram.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; hsram.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16; hsram.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; hsram.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE; hsram.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE; t.AddressSetupTime = 1; t.AddressHoldTime = 0; t.DataSetupTime = 3; t.AccessMode = FSMC_ACCESS_MODE_B; HAL_SRAM_Init(&hsram, &t, &t); /* 内部会调 HAL_SRAM_MspInit 开时钟、配 GPIO */ }坑在HAL_SRAM_MspInit:CubeMX 有时只给你配了 FSMC 的时钟,GPIO 复用那一坨要自己补进stm32f1xx_hal_msp.c,不然HAL_SRAM_Init返回 HAL_OK,屏幕却一点反应没有。
3. 驱动层拆解:从写命令到 0x2C 灌 GRAM 的完整链路
FSMC 配好之后,ILI9341 的所有操作都能归纳成两种动作:发一条命令、给命令喂参数。屏幕内部有个命令寄存器和一个数据寄存器,RS 拉低写入的就是命令码,拉高写入的就是参数或显存数据。把这两个动作封装成内联函数,上层写起来就跟调普通 API 一样。
3.1 两个内联函数撑起整个驱动
/* 写命令:RS=0,一次 16 位写,FSMC 自动完成 NE4 拉低、NWE 脉冲 */ static inline void LCD_WriteCmd(uint16_t cmd) { LCD_CMD = cmd; } /* 写数据:RS=1,参数和显存颜色都走这里 */ static inline void LCD_WriteData(uint16_t dat) { LCD_DATA = dat; }看着简单,但它是整份代码性能的分水岭。函数体只有一次赋值,加static inline之后编译器会内联展开,填充一屏就是几百次指针写,中间没有任何函数调用开销。我第一次写的时候把这两个写成普通函数还传了结构体,编译出来每写一个像素要压栈弹栈,速度直接掉三成。参数上唯一的讲究是:cmd只有低 8 位有效,但总线是 16 位,高 8 位随便写、屏幕会忽略,别为此专门做移位。
3.2 ILI9341 初始化序列:每条命令都在配什么
初始化是这份代码里最长也最容易被当成玄学的一段。其实它分四组:电源、方向与像素格式、伽马、显示开关。
void LCD_Init(void) { LCD_HardwareReset(); /* RST 低 10ms,高电平后等 120ms,先讲后做 */ LCD_WriteCmd(0xCF); /* 电源控制 B */ LCD_WriteData(0x00); LCD_WriteData(0xC1); LCD_WriteData(0x30); LCD_WriteCmd(0xED); /* 电源序列,防止上电瞬间浪涌把屏打花 */ LCD_WriteData(0x64); LCD_WriteData(0x03); LCD_WriteData(0x12); LCD_WriteData(0x81); LCD_WriteCmd(0xE8); /* 驱动时序控制 A */ LCD_WriteData(0x85); LCD_WriteData(0x00); LCD_WriteData(0x78); LCD_WriteCmd(0xCB); /* 电源控制 A */ LCD_WriteData(0x39); LCD_WriteData(0x2C); LCD_WriteData(0x00); LCD_WriteData(0x34); LCD_WriteData(0x02); LCD_WriteCmd(0xF7); /* 泵比调节,跟着上面的电源参数走 */ LCD_WriteData(0x20); LCD_WriteCmd(0x36); /* 显存访问方向:行列交换、BGR 顺序都在这一位里 */ LCD_WriteData(0x48); LCD_WriteCmd(0x3A); /* 像素格式:0x55 = 16 位 RGB565 */ LCD_WriteData(0x55); LCD_WriteCmd(0xB1); /* 帧率:分频与占空比,刷太快会闪、太慢会拖影 */ LCD_WriteData(0x00); LCD_WriteData(0x18); LCD_WriteCmd(0x11); /* 退出睡眠,这条之后必须等 120ms */ LCD_Delay_ms(120); LCD_WriteCmd(0x29); /* 打开显示 */ }逐条看下来其实没什么神秘的:0xCF、0xED、0xE8、0xCB、0xF7这五条是屏幕内部电荷泵的配置,参数是厂家给的推荐值,改了就容易出现亮度不均或上电闪一下;0x36控制显示方向,0x48是横屏加 BGR 顺序,纵屏改0x08或0x88;0x3A必须给0x55,给它0x65是 18 位色,一个像素占三个字节,你后面按 16 位发的颜色就会整体错位一条。
注意:
0x11之后的 120ms 延时不能省,也不能用for循环糊弄。不同优化等级下空循环的耗时差好几倍,用 SysTick 做的毫秒延时最稳。
3.3 复位、背光与延时:三件小事决定能不能点亮
static void LCD_HardwareReset(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0; /* RST 接 PB0,背光接 PB1 */ gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); GPIO_ResetBits(GPIOB, GPIO_Pin_0); /* 拉低复位 */ LCD_Delay_ms(10); /* 手册要求最小 10us,实际给 10ms 更保险 */ GPIO_SetBits(GPIOB, GPIO_Pin_0); LCD_Delay_ms(120); /* 复位后等内部稳压稳定 */ GPIO_SetBits(GPIOB, GPIO_Pin_1); /* 背光最后再开,避免看到初始化过程中的花屏 */ }三个细节值得单独说。RST 和背光千万别接在 FSMC 占用的引脚上,PB0/PB1 这种普通 IO 最省心。背光放到初始化末尾再打开,这样即使初始化序列有问题,你看到的也是黑屏而不是满屏雪花,排查时一眼就能分清是「没亮」还是「亮了但没内容」。延时函数用 SysTick 计数实现,别用__NOP()堆循环。
4. 绘图与字库:开窗填充、ASCII/汉字显示和图片刷新的实现
到这一层才算真正能干活。屏幕的显存是 240×320×16bit,你没法随机寻址某个像素,只能先告诉 ILI9341 一个矩形窗口,然后从这个窗口的左上角开始按行连续写,写满一行自动换到下一行。这个机制叫开窗,是后面所有绘图函数的地基。
4.1 开窗加连续写:填充速度全看这个循环
/* 设定矩形窗口,x0<=x1、y0<=y1,闭区间 */ static void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); /* 列地址设置 */ LCD_WriteData(x0 >> 8); LCD_WriteData(x0 & 0xFF); LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteCmd(0x2B); /* 行地址设置 */ LCD_WriteData(y0 >> 8); LCD_WriteData(y0 & 0xFF); LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteCmd(0x2C); /* 写显存,之后的数据全部落到窗口里 */ } /* 矩形填充:一次开窗,然后死循环写颜色 */ void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t n = (uint32_t)(x1 - x0 + 1) * (y1 - y0 + 1); LCD_SetWindow(x0, y0, x1, y1); while (n--) { LCD_WriteData(color); /* 每写一次窗口指针自动 +1,满一行自动换行 */ } }n必须是 32 位,240×320 = 76800 用 16 位装不下,会溢出成负数直接死循环。这里的性能主要来自while(n--)这个紧凑循环加上内联后的单次指针写,实测在 72MHz、时序参数保守(DataSetupTime=3)的情况下,整屏填充 25ms 左右;把DataSetupTime压到 1 能进 15ms 以内,但前提是排线短、板子干净,不然等着看花屏。
4.2 字符显示:8×16 点阵和 16×16 汉字怎么接进来
ASCII 用 8×16 的点阵,一个字符 16 字节,每字节一行,高位在左。字模表从空格' '开始按 ASCII 顺序排,所以索引就是ch - ' ':
/* 8x16 ASCII:逐行扫描,每一位决定画前景色还是背景色 */ void LCD_ShowChar8x16(uint16_t x, uint16_t y, char ch, uint16_t fc, uint16_t bc) { uint8_t row, col, bits; const uint8_t *p = &asc2_1608[(uint8_t)ch - ' '][0]; LCD_SetWindow(x, y, x + 7, y + 15); /* 一次开窗写完整个字符 */ for (row = 0; row < 16; row++) { bits = p[row]; for (col = 0; col < 8; col++) { LCD_WriteData((bits & (0x80 >> col)) ? fc : bc); } } }汉字是 16×16,一个汉字 32 字节,每行两字节:
/* 16x16 汉字:hz 指向 32 字节字模,高位在左 */ void LCD_ShowCN16(uint16_t x, uint16_t y, const uint8_t *hz, uint16_t fc, uint16_t bc) { uint8_t row, col; uint16_t bits; LCD_SetWindow(x, y, x + 15, y + 15); for (row = 0; row < 16; row++) { bits = ((uint16_t)hz[row * 2] << 8) | hz[row * 2 + 1]; for (col = 0; col < 16; col++) { LCD_WriteData((bits & (0x8000 >> col)) ? fc : bc); } } }注意取模软件的设置:阴码、逐行式、顺向(高位在前)、十六进制、C51 格式。这四项只要是「阳码」或者「逆向」,显示出来就是反色或者左右镜像,我见过不少人以为是 MADCTL 配错了,其实是取模选项点歪了。
4.3 图片与局部刷新:240×320 的 RGB565 怎么塞进去
图片就是一片连续的 RGB565 数据,配合开窗一次性灌进去:
/* 图片数据按大端存放:高字节在前,与取模软件的输出顺序对应 */ void LCD_ShowPicture(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint8_t *pic) { uint32_t i, n = (uint32_t)w * h; LCD_SetWindow(x, y, x + w - 1, y + h - 1); for (i = 0; i < n; i++) { LCD_WriteData(((uint16_t)pic[i * 2] << 8) | pic[i * 2 + 1]); } }如果你的取模工具输出的是小端,就把这两个字节换一下,否则整张图会变成噪点。至于刷新策略,480×320 全屏重绘在这个平台上不现实,常见做法是把界面切成状态栏、数据区、按钮几个矩形,每次只重绘变了的区域,用LCD_Fill加LCD_ShowChar8x16拼出来,刷新率能从个位数提到十几帧。
5. 排查记录:白屏、花屏、颜色反转和坐标错位的五种典型翻车
5.1 背光亮但一片白
现象:上电后背光正常,屏幕纯白,串口打印的初始化流程都跑完了。原因多半是 FSMC 根本没把数据送出去,或者 RS 地址偏移算错,命令和数据都写到了同一个地址。先拿万用表量 NE4 有没有被拉低,再用调试器看LCD_CMD = 0x2C之后LCD_DATA的地址是不是比它大 2。还有一种情况是 GPIO 配成了GPIO_Mode_Out_PP而不是GPIO_Mode_AF_PP,数据线根本没交给 FSMC。
5.2 花屏、雪花、颜色块乱跳
现象:能显示,但满屏噪点,或者画一个矩形出来边缘毛毛的。原因基本是时序太快。杜邦线拉到 15cm 以上、DataSetupTime还给 1,信号建立时间不够,数据线采样到的是上一拍的残留。把DataSetupTime从 1 加到 3 甚至 5,花屏立刻消失;如果非要用长排线又想要速度,就降 HCLK 或者缩短线长,别指望软件补偿。
5.3 红色显示成蓝色
现象:填0xF800出来的是一片蓝。原因是 MADCTL 的 bit3(RGB/BGR 顺序位)和屏体实际排线不匹配。0x36的参数在0x08和0x48之间换一下,或者在填充前把color的 R、B 通道互换。这是最容易被误判成「颜色常量算错了」的一个坑,其实常量没错,是屏幕理解的顺序不一样。
5.4 右侧一条噪点、坐标整体偏移
现象:240 宽的屏,最右边十几列是雪花,画的框整体往左偏。原因有两个:一是开窗时x1超过了 239,ILI9341 的列地址是 0~239,写 240 会绕回;二是买到的模块其实是 320×480 的驱动 IC 配 2.8 寸屏体,分辨率参数在lcd.h里写成了 240×320。先读0xD3拿芯片 ID 确认型号,再核对LCD_WIDTH/LCD_HEIGHT两个宏。
5.5 读 ID 永远是 0xFFFF
现象:想读 0x9341 验证接线,结果读到全 1。这是 FSMC 读时序的问题:写操作只需要 NWE,读操作还要 NOE 有效,而ExtendedMode_Disable下读写共用一套时序,读往往不够。打开扩展模式给FSMC_WriteTimingStruct单独配一份更宽松的时序,同时确认 RD 引脚真的接到了 NOE 上——有些转接板为了省成本把 RD 悬空,那就彻底别想读,只能靠写命令盲驱动。
6. 二次开发:把驱动抽成换屏不改业务的接口层
这份代码最容易踩的二次开发坑,是把 ILI9341 的命令码散落在业务逻辑里。等你换一块 ST7789 或者换成 SPI 屏,就得满工程搜0x2C。我一般会先抽一层设备结构体,把「怎么发命令」和「画什么」彻底分开:
/* 屏幕抽象:上层只认这几件事,换 IC 只改这一层的实现 */ typedef struct { uint16_t width; uint16_t height; void (*init)(void); void (*set_window)(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); void (*write_pixels)(const uint16_t *buf, uint32_t n); /* 批量写,比逐点快得多 */ void (*set_dir)(uint8_t dir); /* 0~3 四个方向 */ } lcd_dev_t; extern const lcd_dev_t lcd_ili9341; /* 换屏时只换这一个符号 */配上write_pixels之后,填充可以改成一次几十像素的批量循环,编译器更容易把指针写向量化,实测比逐点写再快两成左右。再往上,把绘图 API 固定成lcd_fill_rect、lcd_draw_text、lcd_draw_bitmap三个,业务层就完全不知道底下是并口还是 SPI。
两套库的取舍也有讲究:
| 对比项 | 标准库版本 | HAL 版本 |
|---|---|---|
| 代码体积 | 小,只有用到的外设 | 略大,带句柄和状态机 |
| 移植成本 | 换芯片要重写底层 | CubeMX 重新生成即可 |
| FSMC 配置位置 | lcd_fsmc.c里一把梭 | 分散在MX_FSMC_Init与HAL_SRAM_MspInit |
| 适合场景 | 老工程维护、追求极致体积 | 新项目、需要快速换型号 |
我自己的习惯是:先用标准库版本把屏点亮点透,确认时序和地址偏移都没问题,再把这两组参数原样搬进 CubeMX 的 HAL 工程。这样出问题时能立刻判断是硬件接线、还是 HAL 的 MSP 没配对,不至于两个变量一起动。
最后留一条血泪经验。从那以后我每次接一块新屏,都强制先在纸上写三行:CS 接哪个 NE、RS 接哪个 Ax、总线是 8 位还是 16 位,把基址和偏移算出来写进宏,再去碰初始化序列。这三行没写清楚就动手,白屏和花屏会轮流教你做人。希望帮到你。
本文还有配套的精品资源,点击获取