简介:一套面向嵌入式开发者的EFM32与ILI9342驱动资源,围绕EBI外部总线接口与TFT彩屏连接展开,涵盖ILI9342的引脚定义、寄存器配置及基本驱动框架,适合在EFM32平台快速移植或调试320×240分辨率显示屏。压缩包体积仅4KB,包含2个文件,分别为C源码与配套头文件,代码量精简,便于直接查阅与二次开发。资源对ILI9342的D0-D15数据线、CS片选、RS命令/数据选择及读写使能等关键引脚做了清晰梳理,并给出初始化序列、显示控制命令与底层读写函数的实现参考,可帮助理解高速、低功耗的EBI总线如何操作LCD,同时避免与ILI9431型号混淆导致的接线错误。目前已有278人学习,适合物联网、低功耗显示终端等项目的嵌入式开发者参考借鉴,也可作为相关课程设计的入门模板。
1. 一开是给 SPI 屏,结果被 ILI9342 的并行总线卡住了
很多人第一次接触 ILI9342 是因为从压缩包里翻到了ili9342.c和ili9342.h,然后照着网上的 ILI9342 引脚图把 16 根数据线、WR、RD、CS、RS 全部接好,复位也拉过了,屏幕上却一个字都不出。问题往往不在接线,而在总线本身:GPIO 模拟的 8080 时序可以靠延时强撑,但 EFM32 的 EBI(External Bus Interface)把外部设备当作内存来访问,地址建立时间、等待状态、数据保持时间的配置但凡有一点偏差,后续所有命令都进不了控制器,屏自然白得干净。
这篇文章围绕 EFM32 通过 EBI 驱动 ILI9342 这条主线展开,内容覆盖信号对接、bank 映射、等待状态参数、初始化序列和实际驱动代码。适合已经把屏点亮过、但想摆脱 SPI 刷屏速度瓶颈的嵌入式工程师;也适合第一次接手并行接口 LCD 的新手,跟着寄存器配置走一遍,比直接套驱动例程更不容易踩坑。
2. EFM32 EBI 的 Bank 映射与 ILI9342 信号对接
2.1 把 LCD 当成 SRAM:EBI 的核心工作方式
网上能搜到的 stlink v2 引脚图、树莓派 4B 引脚功能图这类资料,按图接线基本就能跑;但 EBI 这类并行总线不一样,引脚图只是起点,真正决定设备能否工作的是总线时序和地址映射。EFM32 的 EBI 接口把外部设备映射到统一地址空间,每个 bank 对应一个固定基地址,读写操作在 CPU 看来就是向某个内存地址写入数据,而芯片会自动产生片选、读使能、写使能信号。
这个特性对 ILI9342 特别有用。ILI9342 是 16 位并行接口的 TFT-LCD 控制器,支持 8080 时序,信号线包括数据线 D0-D15、片选 CS、命令/数据选择 RS、写使能 WR 和读使能 RD。如果把 LCD 映射到 EBI 的一个 bank,那么只要改变写入的地址位,RS 信号就会自动变化,编程时不需要手动翻转 GPIO 来切换命令和数据类型。常见做法是把 EBI 的一根地址线接到 RS 引脚,用地址的最低位区分命令周期和数据周期,这样驱动代码看起来就像是在操作一块 SRAM。
以 EFM32LG 系列为例,EBI 提供 4 个 bank,Bank0 基地址为 0x60000000,Bank1 为 0x64000000,每个 bank 空间足够容纳多个外设。要把 ILI9342 挂上去,需要先确认芯片型号对应的 EBI 引脚组,再配置映射关系。需要注意,EBI 的地址、数据、控制信号是复用引脚,初始化时不能按普通 GPIO 的 push-pull 方式去配置,必须交给 EBI 外设接管,这部分最容易出错,后面代码里会给出完整的配置顺序。
2.2 16 位模式下的引脚对接与地址线选择
实际接线时,我会把 ILI9342 配置成 16 位数据模式,这样一次写入就能完成一个像素的 RGB565 数据,刷屏效率比 8 位模式高一倍。下表是常见的对接关系,适用于 EFM32 系列中带有 EBI 的型号,具体引脚组以参考手册的 EBI 引脚连接表为准。
| EBI 信号 | ILI9342 引脚 | 说明 |
|---|---|---|
| EBI_AD15..0 | DB15..DB0 | 16 位像素数据线,传输 RGB565 格式 |
| EBI_CS0 | CS | Bank0 片选,低有效 |
| EBI_A1 | RS (D/CX) | 地址线复用为命令/数据选择 |
| EBI_WEn | WR | 写使能,上升沿锁存数据 |
| EBI_REn | RD | 读使能,用于读 ID 和状态 |
| GPIO 输出 | RESET | 复位信号,低有效,可由任意 GPIO 控制 |
RS 引脚不建议接 EBI_A0,因为 EBI 按字节地址编址,而 16 位数据访问要求地址按 2 字节对齐,挂在 A0 上会产生奇数地址访问,处理不当会触发对齐异常或导致数据错位。把 RS 接到 EBI_A1 是更稳妥的方案,写命令时访问基地址 0x60000000,写数据时访问 0x60000002,两个地址相差 2,既满足了 16 位对齐,又正好让 A1 在高低电平之间切换。
复位引脚单独用一个 GPIO 控制是有意为之。很多引脚图资料里把 RESET 和系统复位直接并在一起,这样上电顺序没有被严格保证,ILI9342 内部上电复位电路对 VCI 和 IOVCC 的时序有要求,独立控制可以让软件在电源稳定后再拉高复位,尽量减少初始化失败的概率。
2.3 EBI 初始化参数:先给足等待状态,跑通后再压缩
EBI 初始化涉及地址建立时间、地址保持时间、读/写周期和等待状态这几个关键参数。常见做法是先设置比较宽松的时序,确保屏幕能够响应,然后再逐步压缩等待状态,提升刷屏速度。下面这段代码基于 emlib 库给出基本配置框架,寄存器名字会因为芯片系列不同略有差异,以工程中实际使用的头文件为准。
EBI_Init_TypeDef ebiInit = EBI_INIT_DEFAULT; ebiInit.mode = ebiModeD16A8; /* 16 位数据线,8 位地址线 */ ebiInit.aleMode = ebiALEInactive; /* 不使用地址锁存 */ ebiInit.bankEnable = EBI_BANK0; /* 启用 Bank0 映射 */ ebiInit.addrSetup = ebiAddrSetup1; /* 地址建立时间 1 个 EBI 时钟周期 */ ebiInit.addrHold = ebiAddrHold2; /* 地址保持时间 2 个周期 */ ebiInit.readSetup = ebiReadSetup2; /* 读信号建立时间 2 个周期 */ ebiInit.readHold = ebiReadHold1; /* 读信号保持时间 1 个周期 */ ebiInit.writeSetup = ebiWriteSetup1; /* 写信号建立时间 1 个周期 */ ebiInit.writeHold = ebiWriteHold2; /* 写信号保持时间 2 个周期 */ ebiInit.waitStates = 4; /* 等待状态 4 个周期,先慢后快 */ EBI_Init(&ebiInit); GPIO_PinModeSet(gpioPortF, 0, gpioModePushPull, 0); /* 其余 EBI 引脚按相同方式配置 */代码里的关键是把 waitStates 和读写保持时间先放大。EBI 外设的时钟来自 HFCORECLK,如果系统主频是 48MHz,一个周期大约是 20ns,4 个等待状态加上地址建立和保持,总共能给 WR 信号留出约 120ns 的有效时间,满足 ILI9342 对写周期不小于 100ns 的典型要求。此时屏幕应该能正常响应命令。之后再逐步减小 waitStates,每次改完都刷一屏测试图案,直到出现噪点或花屏,再回退一个档位作为稳定运行的值。
初始化完成后不要急着画像素,先用逻辑分析仪观察 EBI_WEn 和 EBI_CS0 的波形,确认每一次写访问都产生了完整的低电平脉冲。很多人在这里忘记配置引脚复用功能,GPIO 还在普通输出模式,结果是总线信号根本没有被驱动到引脚上,后面所有调试都是浪费时间。
3. ILI9342 初始化序列与读写时序:读 ID 识别真实屏芯片
3.1 初始化序列不是复制粘贴:每条命令都必须能解释
ILI9342 的初始化序列和 ILI9341 很接近,但寄存器地址和默认值并不完全相同,直接搬 9341 的模板不一定能点亮。正规的初始化流程应当包括软件复位、Sleep Out、像素格式、内存访问控制和伽马校正。下面是我在一颗常见 ILI9342 模组上使用的基础命令序列,共 6 条,足以让屏正常显示颜色。
| 命令 | 参数 | 延时 | 作用 |
|---|---|---|---|
| 0x01 Soft Reset | 无 | 120ms | 软件复位,控制器恢复到上电默认状态 |
| 0x11 Sleep Out | 无 | 150ms | 退出睡眠模式,内部振荡器启动 |
| 0x36 MADCTL | 0x48 | 0ms | 设置扫描方向,0x48 对应竖屏 RGB 顺序 |
| 0x3A Pixel Format | 0x55 | 0ms | 16 位像素格式,RGB565 每像素 2 字节 |
| 0x2A Column Address | 0x00 0x00 0x00 0xEF | 0ms | 列地址 0 到 239,实际宽 240 像素 |
| 0x2B Page Address | 0x00 0x00 0x01 0x3F | 0ms | 页地址 0 到 319,实际高 320 像素 |
这些命令通过 EBI 总线发送时有一个共同规则:命令字节写入命令地址,参数写入数据地址,每一帧字节的长短由控制器内部寄存器自动判断,软件只需要保证在正确的地址写入正确的值。0x2A 命令后必须连续写入 4 个字节的列起始和结束地址,如果中间插入其他操作,窗口设置就会错位,画面会出现颜色串行或图像偏移。
像素格式设置为 0x55 时,后续传输的每个像素占 16 位,EBI 的 16 位数据总线正好一次传输一个像素。如果把像素格式设成 0x66 即 RGB666,控制器会按 3 字节解析,总线宽度和数据包不吻合,写在代码里就会出现颜色通道错位,这也是花屏的常见来源之一,排查时优先检查这条命令的数值。
3.2 读 ID 区分 ILI9342、ILI9341 与 ILI9431
ILI9342、ILI9341、ILI9431 这三个型号在网络上经常被混在一起,下载的引脚图可能标注的是 ILI9342,实际买到手却是 ILI9341 或者 ILI9431 的模组。区分的方法不是靠目测走线,而是通过 0x04 Read ID 命令读取控制器型号。读取流程如下:写入命令 0x04,然后从数据地址连续读 3 个字节,分别对应制造商 ID、驱动版本和模块 ID,具体响应因屏厂封装而异,但版本号一般能区分出不同型号。
uint8_t lcd_read_id(void) { uint8_t id[3] = {0}; LCD_REG = 0x04; /* 向命令地址写入 Read ID 命令 */ delay_us(50); /* 给控制器内部准备时间,典型值 20us 以上 */ id[0] = LCD_RAM; /* 连续读回三个字节 */ id[1] = LCD_RAM; id[2] = LCD_RAM; return id[1]; /* 驱动版本号通常可以区分型号 */ }读取时序里有几个值得注意的细节。第一,命令写入后必须等待一段稳定时间,不能立刻读,部分控制器在收到 Read ID 命令后需要几十微秒才会在数据线上输出有效电平。第二,每次读回一个字节,EBI 会主动产生一个 RD 脉冲,控制器内部的数据指针自动递增,因此连续读三个数据地址即可。第三,读回结果如果全为 0xFF 或全为 0x00,说明控制器可能没有响应,优先检查 RD 信号是否真正被 EBI 驱动,以及 CS 是否被选中的 bank 拉低。
识别出真实型号后,初始化序列要做相应调整。如果是 ILI9341,可以把第 3 章的 6 条命令原样保留;如果是 ILI9431,需要向屏厂索取初始化代码,因为列地址和页地址的范围可能不同。网上搜索 ILI9342 引脚图时经常混入多个型号的资料,下载驱动文件时要核对文件内部的寄存器定义,优先使用与实物 ID 匹配的那一份代码。
4. 驱动实现:用地址差代替 GPIO 翻转来点亮 320x240
4.1 命令/数据地址的 C 宏设计
驱动代码的基础是两个地址宏,分别代表命令寄存器和数据寄存器。按照第 2 章的接线,RS 接到 EBI_A1,命令地址是 0x60000000,数据地址是 0x60000002,这两个地址在物理上是同一个外设,只是因为地址线 A1 的电平不同而访问到控制器的不同内部区域。
#define LCD_BASE ((volatile uint16_t *)0x60000000) #define LCD_REG (*(volatile uint16_t *)(LCD_BASE + 0x0000)) /* 命令地址 */ #define LCD_RAM (*(volatile uint16_t *)(LCD_BASE + 0x0001)) /* 数据地址,偏移 1 个 16 位单位 */ static void lcd_write_cmd(uint16_t cmd) { LCD_REG = cmd; /* 命令写入命令地址,A1 保持低电平 */ } static void lcd_write_data(uint16_t data) { LCD_RAM = data; /* 数据写入数据地址,A1 被拉高 */ }这里的地址偏移计算要看得仔细。指针类型是uint16_t,偏移 1 对应的实际字节地址是 2,也就是 0x60000002,正好让 EBI_A1 输出高电平。如果指针类型换成uint32_t或者偏移写错,访问到的实际地址就会偏离预期,RS 信号不翻转,命令和数据全部写进同一个寄存器,屏幕的表现通常是不显示任何内容或者显示出完全混乱的色块。
写命令和写数据函数内部的单行赋值语句被编译成一次 16 位总线写操作。EBI 外设会自动把这次访问转换成 WR 低脉冲,数据在上升沿被 ILI9342 锁存。整个过程没有 GPIO 翻转,也没有延时循环,CPU 开销只有一条写指令,这也是 EBI 方案相对于 IO 模拟最大的优势。
4.2 窗口设置与画点函数
驱动第二部分实现的是窗口设置和像素填充。ILI9342 的工作方式是在设定窗口内顺序写入像素数据,写完一个像素后地址指针自动加一,因此画一个点其实分三步:设置列地址范围、设置页地址范围、写入 RGB565 颜色值。
static void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { lcd_write_cmd(0x2A); /* 设置列地址 */ lcd_write_data(x0 >> 8); /* 起始列高 8 位 */ lcd_write_data(x0 & 0xFF); /* 起始列低 8 位 */ lcd_write_data(x1 >> 8); /* 结束列高 8 位 */ lcd_write_data(x1 & 0xFF); /* 结束列低 8 位 */ lcd_write_cmd(0x2B); /* 设置页地址 */ lcd_write_data(y0 >> 8); /* 起始页高 8 位 */ lcd_write_data(y0 & 0xFF); /* 起始页低 8 位 */ lcd_write_data(y1 >> 8); /* 结束页高 8 位 */ lcd_write_data(y1 & 0xFF); /* 结束页低 8 位 */ lcd_write_cmd(0x2C); /* 内存写命令,后续数据进入显存 */ } void lcd_draw_pixel(uint16_t x, uint16_t y, uint16_t color) { if (x >= 240 || y >= 320) return; /* 坐标越界保护 */ lcd_set_window(x, y, x + 1, y + 1); /* 开 1x1 窗口 */ LCD_RAM = color; /* 写入 RGB565 颜色值 */ }参数x0、y0是窗口左上角坐标,x1、y1是右下角坐标,含端点在内。每次画点都要调用lcd_set_window,虽然会产生 9 次总线写操作,但在调试阶段这个代价可以接受,因为代码逻辑直观,任何一根数据线接错都能在画点时快速暴露出来。窗口起始和结束地址的字节序是高字节在前,写反了会导致显示区域完全错乱,常见症状是颜色铺满全屏但位置完全不对。
清屏函数可以复用窗口函数,把整个屏幕设成一个窗口,然后用循环连续写入 240 乘 320 个像素。优化方法是把颜色值拆成两个字节直接写入数据地址,因为控制器在窗口模式下会连续递增地址,中间不需要再发命令,总线利用率明显高于逐点设置窗口。
4.3 缓冲与 DMA:性能瓶颈在哪里
不使用 DMA 时,清一屏 240x320 的 RGB565 数据需要写入 76800 次,每次写操作大约消耗几个系统时钟周期,屏看起来是一行一行刷上去的。要提升刷新率,最直接的方法是把一整行像素放到 RAM 缓冲区中,然后让 DMA 从缓冲区把数据搬运到LCD_RAM地址,CPU 只负责准备下一行数据。
static uint16_t line_buffer[240]; void lcd_fill_line(uint16_t y, uint16_t color) { for (int i = 0; i < 240; i++) { line_buffer[i] = color; /* 填充一行缓冲区 */ } lcd_set_window(0, y, 239, y); /* 只开一行窗口 */ DMA_Transfer(EBI_DATA_ADDR, line_buffer, 240); /* DMA 写入数据地址,示例函数 */ }DMA 传输方向是从内存到外设,目标地址就是数据寄存器地址,传输宽度设置为 16 位,传输次数为 240。使用 DMA 后,一次行填充只需要等待 DMA 完成中断,CPU 可以在传输期间处理其他任务。需要注意,ILI9342 的数据写入是连续的,一旦窗口设置完毕,后续所有总线写入都会按照控制器内部的地址递增顺序落入显存,因此 DMA 传输期间不能插入任何其他 EBI 访问,否则会破坏当前窗口内的数据流。
缓冲区大小可以根据 RAM 资源调整,常见做法是准备两个 240 字的缓冲区,用 ping-pong 方式交替填充和传输,让 DMA 和 CPU 并行工作。如果 RAM 足够大,可以直接准备整帧缓冲区,代价是 76800 字节的存储空间,在 EFM32 这类低功耗 MCU 上一般只用于静态图片显示,动画场景还是用逐行传输更现实。
5. 白屏、花屏、颜色错乱的定位与等待状态校准
5.1 症状对应的排查方向
我经手过的 ILI9342 调试问题,最后大多集中在下面这张表列出的几个方向。屏幕不亮优先查硬件,屏幕亮了但内容不对优先查命令序列和时序参数。
| 现象 | 最可能原因 | 检查方式 |
|---|---|---|
| 全白屏 | 初始化命令未生效 | 用示波器看 CS 和 WR 是否在复位后有脉冲 |
| 全黑屏 | 背光未打开或像素格式错误 | 检查背光引脚,确认 0x3A 参数为 0x55 |
| 雪花噪点 | EBI 等待状态过小 | 把 waitStates 增大到 4 再观察 |
| 颜色偏绿或偏红 | RGB 顺序或 RGB565 字节序错误 | 修改 MADCTL 的 BGR 位 |
| 图像上下颠倒 | 扫描方向配置错误 | 调整 0x36 命令的 MX/MY 位 |
| 画面整体错位 | 窗口地址命令字节序写反 | 检查 0x2A/0x2B 高 8 位和低 8 位顺序 |
颜色错乱这一项容易被误判为硬件接错线。16 位数据模式下如果 D0-D15 中有两根线接反,颜色偏差会呈现出固定的规律,而不是随机噪点。RGB565 格式中高 5 位是红色,中间 6 位是绿色,低 5 位是蓝色,当字节序写反时,红蓝通道会交换,画面整体偏蓝或偏红,这时候改 MADCTL 的 BGR 位不一定有效,必须检查数据线。
5.2 用逻辑分析仪核对单次总线写周期
示波器或逻辑分析仪应挂在 CS、WR、A1 三根信号上,触发条件设为 CS 下降沿,观察写入命令地址和写入数据地址时 A1 的电平是否分别为低和高。如果 A1 始终为低或者始终为高,说明地址偏移宏写错了;如果 WR 脉冲宽度明显小于数据手册要求的 100ns,说明等待状态配置过小。ILA9342 对写周期的最小脉宽要求可以在具体模组的数据手册里查到,不同封装批次略有差异,预留 20% 余量比较保险。
5.3 扫描色块法:找到稳定的等待状态边界
一个可行的校准方法是写一个渐变色填充函数,从暗到亮逐列变化,然后在不同的 waitStates 值下反复刷屏。肉眼能明显看出色带断层、横向噪点或亮度闪烁的位置,出现这些现象时说明当前等待状态不够。记录下稳定运行的最小值,把它再加上 1,作为最终配置写入初始化代码。
void lcd_fill_gradient_test(void) { for (int x = 0; x < 240; x++) { uint16_t color = (x << 3) | ((x & 0x07) << 5); /* 低字节渐变,保留高位色彩 */ for (int y = 0; y < 320; y++) { lcd_draw_pixel(x, y, color); } } }色块扫描的判定标准是渐变过渡是否平滑。如果屏幕在高亮度区域出现横向细线或闪烁点,说明数据传输在部分周期内没有满足建立时间要求。常见做法是从 waitStates 等于 4 开始,每次减 1,直到渐变出现噪点,再回退一个档位。如果回退之后仍然偶发花屏,还要检查 HFCORECLK 频率是否有动态变化,低功耗模式下时钟切换会让 EBI 时序随主频漂移,这种情况必须固定系统时钟源,不能只改等待状态。
本文还有配套的精品资源,点击获取