简介:面向Linux驱动开发者与嵌入式显示应用工程师的8080并行接口LCD驱动实现资料,主要解决小尺寸并行接口液晶屏在Linux系统中的驱动编写问题。内容围绕驱动初始化、数据传输、命令发送、设备注册与卸载等核心流程展开,重点剖析字符设备模型、I/O端口操作、设备树配置、模块加载与参数传递等关键知识点,适合需要从零编写或移植LCD驱动的读者,也可作为运维或服务器人员理解底层硬件交互的参考。压缩包共9个文件,整体大小约15KB,以3个C源码文件与2个Markdown文档为主体,另有头文件、Makefile构建脚本、LICENSE授权文件和.gitignore配置,目录清晰、模块划分明确,便于按需阅读和本地编译验证。已有351人学习。源码将驱动核心、I/O操作和ILI9341控制器初始化分层设计,配合文档可完整跟踪从设备注册、内核模块加载到LCD点亮及数据写入的过程;还能学习内存映射提升显示性能、设计用户空间访问接口与控制函数的思路。这些实现骨架可复用到其他并口LCD驱动开发中,提升实际项目排错效率。
1. 8080并行接口液晶屏:为什么它在Linux下比MIPI屏更折腾
手里拿一块4.3寸、8080并行接口的液晶屏,想接到一块跑着Linux、GPIO资源还算富裕的开发板上,第一次做这事的工程师通常都会翻车:MIPI/HDMI屏在设备树里改两行就亮,这块8080屏却连“总线类型”都找不到。Linux下没有一个叫8080的标准总线框架,常见做法是把它当成一颗挂在GPIO或FSMC总线上的显示控制器,用framebuffer机制暴露成/dev/fb0。这篇笔记要解决的就是这类小尺寸并行接口屏的Linux驱动落地:从时序原理讲起,先走FBTFT快速点亮,再拆一份不依赖第三方代码的手写方案,把线路里到底是谁在干活讲明白。
2. 先搞明白8080并口屏在驱动层到底是个什么东西:时序、控制器和你绕不开的三条路
2.1 8080接口的读写时序其实就两个周期:你要驱动的不是屏,是那根WR/RD线
8080接口由Intel 8080总线演化而来,在MCU驱动的小尺寸屏上特别常见。你需要控制的信号不多:片选CS、命令/数据选择DC(有些屏标注RS)、写选通WR、读选通RD、复位RESET,外加D0到D7(或D0到D15)的数据引脚。真正刷新液晶面板的工作由屏上的控制器完成,比如ILI9341、ST7789这些芯片,Linux驱动要做的只是按规定时间“塞数据”给控制器,然后让控制器去刷新面板。
写周期的顺序经常被新手搞反。我一般这样记:先把CS拉低,然后把DC设成我们要发送类别——命令用0,数据用1——再把D0到D7上的电位放稳,之后拉低WR,保持一小段时间,最后拉高WR。数据是在WR的上升沿被锁存进控制器的,所以数据必须先于WR拉低之前稳定,这就是下表中tDS存在的意义。
| 时序参数 | 典型最小值 | 说明 |
|---|---|---|
| tcyc | 66~100ns | 一个完整写周期,ILI9341高速模式可到66ns |
| tAS | 15ns | CS/DC在WR拉低前建立时间 |
| tWR | 15~60ns | WR低电平脉冲宽度 |
| tDS | 10ns | 数据在WR上升沿前的建立时间 |
| tDH | 15ns | WR上升沿后的数据保持时间 |
读周期类似,只是把WR换成RD,数据由屏控制器输出。Linux下绝大多数小屏驱动根本不需要读屏,除非你调试时想去读控制器的ID寄存器,所以日常优化重点全在写路径上。GPIO模拟实现下,单次写操作要经历多次gpiod_set_value,单引脚翻转通常在几十到一百多ns,一次8位写入轻松超过1us。这个数字看起来无所谓,但在320x240全屏刷新时要写约76800个像素,帧率高不高就全靠它了。
2.2 小尺寸屏常见控制器:ILI9341、ST7789、ST7735与命令集差异
8080接口只是物理层面的传输方式,真正决定驱动代码长什么样的,是屏上那颗控制器芯片。小尺寸屏里最常见的是这几颗:ILI9341、ST7789V、ST7735S,偶尔还有三星的S6D02A1一类。它们都接受“命令参数+数据”的格式,但初始化序列、寄存器位段和默认扫描方向各不相同,所以驱动代码没法完全通用。
| 控制器 | 典型分辨率 | 常见数据位宽 | 与ILI9341的明显差异 | 常见坑 |
|---|---|---|---|---|
| ILI9341 | 240x320 | 8/16 | 市场保有量大,资料最全 | 假芯片多,ID不稳定 |
| ST7789V | 240x320 | 8/16 | 内存排列与ILI9341不同,MADCTL表现不一样 | 旋转90度后容易镜像 |
| ST7735S | 128x160 | 8/16 | 初始化命令更繁琐,常用局部窗口 | 部分版本需要额外设置BGR位 |
初始化序列是另一个自带玄学的地方。正规的做法是照着数据手册里给的初始化流程写,但很多“驱动资源包”里直接放一份厂家工程拷出来的init_sequence数组。数组里可能包含Delay(50)这种指令,解析时要格外小心。我拿到的移植代码里,常看到把延迟写在数组里用特殊字节表示,比如0x00后跟毫秒数。遇到这种格式,要确认自己写的解析函数支持,否则后面的命令全部错位。
MADCTL命令是旋转方向的关键,通常命令码是0x36,参数里MX、MY、MV几个位控制行扫描方向和RGB/BGR。做rotate=90时,如果只是简单交换分辨率和宽高,没有同步改MADCTL,轻则显示区域错位,重则画面上下左右倒置。这也是后面FBTFT里rotate参数背后的本质:它替你改了MADCTL和地址窗口,而不是只交换X/Y。
2.3 Linux下三条路:fbdev、DRM/KMS与FBTFT,选哪条取决于你要不要交差
拿到底层是8080并口的小屏,Linux侧有几种成熟路线,选型大多取决于你是要今天下午点亮,还是要做一个长期维护的产品。
| 路线 | 接入成本 | 灵活度 | 能不能进主线 | 适合场景 |
|---|---|---|---|---|
| 自写fbdev驱动 | 中 | 高 | 可以 | 内核版本可控、长期产品 |
| DRM/KMS自写驱动 | 很高 | 极高 | 理论可以 | 需要GPU合成、多屏联动 |
| FBTFT模块 | 低 | 中 | 基本不进主线 | 快速验证、学习调试、小项目 |
fbdev是Linux传统的帧缓冲接口,逻辑简单:分配一块内存当显示缓存,驱动把这块内存映射到用户态,再实现fb_ops里的填充、拷贝、画点函数。小尺寸并口屏用fbdev完全够,因为不涉及GPU合成、多平面叠加、模式切换这类复杂操作。DRM/KMS现在是大屏的绝对主流,但它至少要处理drm_device、connector、encoder、panel四大件,为了控制一颗ILI9341去做这些,有点大炮打蚊子。
FBTFT则是一个专门服务小屏外设模块的社区框架,它把控制器初始化、GPIO模拟时序、fbdev注册都封装好了。大多数情况下你只需要配好设备树,编出fb_ili9341.ko或者fb_st7789.ko,insmod后系统里就多出/dev/fb0。代价是这个框架常年停在staging,随内核版本变化接口经常改,你在网上翻到的旧补丁很可能编不过新版内核,这就是它替代不了自研驱动的原因。
还有一条硬件层面更快的路:如果你的主控内置FSMC/EBI外部总线控制器,把8080屏挂在总线上,让CPU访问一段内存地址就等于操作屏幕。这时Linux驱动连GPIO翻转都省了,直接ioremap后按地址偏移区分命令和数据。后面第4章会讲这条路的细节。
3. 用FBTFT把8080液晶屏点亮:从内核配置到设备树的完整落地步骤
3.1 内核配置:先解决FBTFT“不在主线”的问题,再决定编内核还是编模块
FBTFT不是一个内核config选项就能带出来的普通功能,它经常以补丁形式散布在各类开发板BSP和Buildroot的包里。拿到一块带“驱动资源包”的板子,常见做法是先看它的内核树里有没有drivers/staging/fbtft目录。如果包里的补丁是针对旧内核的,你直接复制到新内核源码里大概率编不过,因为fb_ops、gpiod_*接口都变过。
我的做法是先确认内核版本,再决定是整体编入镜像还是编成模块。在menuconfig里如果能看到Support for small TFT LCD display modules,就进去选中对应控制器,比如ILI9341对应CONFIG_FB_TFT_ILI9341=m。编成模块的好处是调试时不用反复烧内核,用insmod换参数就行;但要注意,如果你选的控制器驱动依赖FBTFT核心模块,需要同时确保CONFIG_FB_TFT=m。
make ARCH=arm menuconfig # Device Drivers -> Graphics support -> Support for small TFT LCD display modules # 选中 FB_TFT、FB_TFT_ILI9341 等,保存退出 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j8 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules -j8这段命令中menuconfig里的路径只对打过FBTFT补丁的内核有效,如果你在主线内核里搜不到,不要惊讶,先去补丁。编译模块后/lib/modules/<内核版本>/kernel/drivers/staging/fbtft/下会出现fbtft.ko和fb_ili9341.ko。find或modinfo能帮你确认模块有没有编出来,这是最直接的验证。
3.2 设备树里描述8080总线:GPIO数据线的命名、复位和控制信号顺序
FBTFT本身支持的设备树写法是:把面板当作一个挂在GPIO上的平台设备,compatible指向控制器驱动,再用dat0-gpios到dat7-gpios描述并口数据线。以下是一个常见的8位8080接口写法,我在全志、NXP这类国产开发板的BSP里都见过类似结构。
&pinctrl { lcd_8080_pins: lcd_8080_pins { pinctrl-single,pins = < 0x14a 0x1b0b0 0x14b 0x1b0b0 >; }; }; &gpio1 { lcd_8080 { compatible = "fbtft,ili9341"; pinctrl-names = "default"; pinctrl-0 = <&lcd_8080_pins>; reset-gpios = <&gpio1 0 GPIO_ACTIVE_LOW>; dc-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>; wr-gpios = <&gpio1 2 GPIO_ACTIVE_LOW>; rd-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; cs-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>; dat0-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; dat1-gpios = <&gpio1 6 GPIO_ACTIVE_HIGH>; dat2-gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>; dat3-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>; dat4-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; dat5-gpios = <&gpio1 10 GPIO_ACTIVE_HIGH>; dat6-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; dat7-gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>; buswidth = <8>; rotate = <0>; status = "okay"; }; };这里一个容易踩坑的地方:dc-gpios在不少屏的丝印上写的是RS,如果你在原理图里找不到DC引脚,就往RS上找。它们是一样的信号,只是命名不同。另一个坑是GPIO_ACTIVE_LOW只对reset这类低有效信号合理,但有些补丁对控制线一律按高有效解析,这时你必须看代码里gpiod_get_value有没有取反。设备树里加了dat0-gpios但驱动报Failed to request dt property 'dat0-gpios',说明这个FBTFT补丁版本太老,根本不支持并口解析,换个新补丁。
3.3 加载模块的常用参数:rotate、buswidth、fps以及如何判断屏真的亮了
如果FBTFT驱动编成了模块,加载方式和参数如下。老式内核里常用fbtft_device配合name=...和gpios=...一步步建设备,新式用设备树后则简单得多。
insmod /lib/modules/$(uname -r)/kernel/drivers/staging/fbtft/fbtft.ko insmod /lib/modules/$(uname -r)/kernel/drivers/staging/fbtft/fb_ili9341.ko如果屏没亮,先看dmesg | tail -20。常见错误是Cannot request GPIO,这往往不是接线错,而是某个引脚被pinmux占用,比如pinctrl把它设成了UART功能。此时返回设备树,把对应引脚从其它外设节点里挪出来,不能只改驱动。
加载后Linux下常用命令里有fbset和fbi:fbset -fb /dev/fb0 -g 320 240 320 240 16,如果返回值正常说明fbdev已经注册。接下来可以用dd if=/dev/urandom of=/dev/fb0 bs=38400 count=2,屏幕应该出现彩色噪点。参数上,rotate=90会同时影响MADCTL和分辨率,FBTFT会自己做换算,不需要你手动改设备树里的宽高;buswidth=16时,数据线要从dat0-gpios一路写到dat15-gpios,8位屏千万别设成16。fps参数有些版本支持,它只是控制一个高精度定时器去周期性回刷屏幕,对GPIO模拟并没有提速作用,别太指望它。
4. 手写一个最简Linux帧缓冲驱动:不依赖FBTFT时的8080屏点亮方案
4.1 把8080时序落到代码:写命令和写数据的GPIO动作顺序
自己动手写一遍,才能真正理解FBTFT替你做了什么。最核心的是两条函数:写命令和写数据,二者的差别只有一个DC引脚电平。
static void lcd_write_cmd(struct lowpan_lcd *lcd, u8 cmd) { gpiod_set_value(lcd->dc, 0); lcd_write_cycle(lcd, cmd); } static void lcd_write_data(struct lowpan_lcd *lcd, u8 data) { gpiod_set_value(lcd->dc, 1); lcd_write_cycle(lcd, data); } static void lcd_write_cycle(struct lowpan_lcd *lcd, u8 val) { int i; gpiod_set_value(lcd->cs, 0); for (i = 0; i < 8; i++) { if (val & (1 << i)) gpiod_set_value(lcd->dat[i], 1); else gpiod_set_value(lcd->dat[i], 0); } gpiod_set_value(lcd->wr, 0); ndelay(40); gpiod_set_value(lcd->wr, 1); ndelay(40); gpiod_set_value(lcd->cs, 1); }这里的动作顺序有讲究:先放数据再拉低WR,保证数据建立时间满足ILI9341手册里的tDS;ndelay(40)在多数GPIO控制器下都会超时,实际延迟会偏大,搜解决办法时经常看到“算了改udelay”的版本。需要提醒的是,ndelay在内核里经常被那几级总线访问延迟支配,不用精确较真,稳定优先。初始化序列就是反复调用这两个函数,发送厂家提供的命令和参数,直到能点亮。
4.2 注册framebuffer:fb_ops、var/fix与screen_base的填充顺序
点亮屏幕之后,要做的第一件事是把这块屏注册成Linux的fb_info。framebuffer_alloc分配结构体,strcpy(info->fix.id, "lcd_8080"),然后填var和fix,最后register_framebuffer挂在/dev/fb0上。
static const struct fb_ops lcd_fb_ops = { .owner = THIS_MODULE, .fb_fillrect = sys_fillrect, .fb_copyarea = sys_copyarea, .fb_imageblit = sys_imageblit, .fb_setcolreg = lcd_setcolreg, .fb_blank = lcd_blank, }; static int lcd_fb_probe(struct platform_device *pdev) { struct lcd_dev *lcd; u32 *screen; lcd = devm_kzalloc(&pdev->dev, sizeof(*lcd), GFP_KERNEL); screen = devm_kzalloc(&pdev->dev, 240 * 320 * 2, GFP_KERNEL); lcd->info = framebuffer_alloc(0, &pdev->dev); lcd->info->screen_base = (char *)screen; lcd->info->screen_size = 240 * 320 * 2; lcd->info->fbops = &lcd_fb_ops; strcpy(lcd->info->fix.id, "lcd_8080"); lcd->info->fix.smem_start = 0; lcd->info->fix.smem_len = 240 * 320 * 2; lcd->info->fix.type = FB_TYPE_PACKED_PIXELS; lcd->info->fix.visual = FB_VISUAL_TRUECOLOR; lcd->info->fix.line_length = 240 * 2; lcd->info->var.xres = 240; lcd->info->var.yres = 320; lcd->info->var.bits_per_pixel = 16; lcd->info->var.red.offset = 11; lcd->info->var.red.length = 5; /* green/blue 同理 */ lcd->fb_work_pending = false; return register_framebuffer(lcd->info); }这段代码里容易漏掉的是fix.line_length,如果漏填,控制台字体写入时会计算出错误偏移。screen_base指向的内存就是真正要刷新到屏上的缓存,控制器的初始化命令则要在register_framebuffer之前完成,否则用户态一打开/dev/fb0,看到的只是一块没有建立地址窗口的RAM。至于fb_ops里为什么可以用sys_fillrect而不自己实现,因为fbdev把所有像素操作都落在screen_base这块内存上,CPU直接操作内存就够了,驱动只负责后续把整块内存搬到屏上。
4.3 用FSMC/EBI总线访问8080屏时,地址映射和信息排序的一个易错点
如果开发板主控带FSMC或EBI外部总线,而且板子上已经用并行地址线连接了8080屏,就不要再用GPIO模拟。常用做法是把DC接到地址线A0上,然后申请一段总线地址。访问基地址时数据被当成命令,访问基地址偏移1时当成数据。Linux驱动里先ioremap拿到虚拟地址,命令寄存器是base + 0,数据寄存器是base + 1:
void *base = ioremap(LCD_BUS_BASE, SZ_4K); u8 cmd = 0x2A; writeb(cmd, base); /* 命令 */ writeb(high_byte, base + 1); /* 数据 */ writeb(low_byte, base + 1); /* 数据 */但这里信息排序是个大坑:8080屏的地址自动递增机制,决定了你写入数据寄存器的先后顺序才是真正的“地址顺序”,跟FSMC访问的地址偏移无关。很多人用FSMC后以为自己可以像内存一样连续memcpy,结果发现LCD控制器的地址窗口没有初始化,于是数据乱七八糟。正确顺序是:先用命令0x2A和0x2B设置列行地址窗口,再逐个写像素。另外,ioremap的内存如果打上了cache属性,CPU会延迟写回,屏幕看起来会抖动甚至丢数据。我一般改用ioremap_wc或加wmb()屏障,本质上就是放弃缓存一致性换时序稳定。
5. 8080并口屏驱动避坑指南:黑屏、花屏、卡死、残影的四个真实排查场景
5.1 现象一:白屏不亮——初始化序列没发出去,先查RESET极性和上电顺序
白屏是最常见的开场。很多人第一反应是驱动代码问题,其实多半是复位和上电顺序没做好。现象:模块加载没有报错,/dev/fb0也出现了,但屏一片白。原因通常是reset引脚极性和FBTFT设备树里的定义不一致。用一个GPIO_ACTIVE_LOW的标注,但板子上复位电路正好是高有效,那么驱动拉高时屏一直被按住复位,控制器始终没有进正常工作态。
处理方式:先看原理图,确认复位信号到底拉低有效还是拉高有效,再改设备树里的reset-gpios。其次,在初始化函数开头增加延时:拉低复位至少10ms,然后拉高,再延时120ms再发第一条命令。这个时间序列不能省,因为屏控制器内部晶振和电压稳定都需要时间。用mdelay(120)在这里没问题,但不要把它放在中断上下文里。
5.2 现象二:花屏且字符错位——时序方向反了,WR上升沿取样出问题
花屏如果伴随明显的文字错位,常见原因是写周期里数据总线和WR的顺序没配对。有一次我把先拉WR再加数据的顺序搞反了,数据在WR上升沿还没稳定,控制器锁存进去的是上一次总线残留,结果屏幕上每隔几个像素就窜色。解决方法是严格执行第4章里的顺序:CS有效,DC稳定,数据稳定,WR拉低,等脉冲宽度,WR拉高。数据释放放在WR拉高之后。
另一个容易忽略的是CS的时序:如果CS和WR同时拉低,有些控制器会把CS的低有效时间按下降沿算,而CS建立时间不够会造成偶尔第一笔丢失。我调试时会用一个逻辑分析仪抓DC、WR和D0到D7的波形,确认WR上升沿时D0到D7已经稳定。没有逻辑分析仪时,先在初始化序列里多发几次0x00作为填充,看是否仍错位。
5.3 现象三:加载驱动后系统卡死——check_var缺失与screen_base未初始化
这个坑会让人怀疑自己把内核写坏了。现象:insmod后,屏幕点亮瞬间系统整个死机,串口也没有响应,只能复位。往往不是显示时序问题,而是用户态工具或内核打开fbdev后,调用fb_check_var、fb_set_par时访问到了空指针或非法分辨率。
在自写驱动里,注册fb_info前必须实现一个简单的check_var回调,它负责对fb_var_screeninfo里的xres、yres、bpp做合法性检查,如果值不在预期范围内直接返回-EINVAL,不让上层把数值改成我们没准备过的模式。还有一种卡死,是screen_base分配得比实际分辨率小,fbi -T 1往里面写入大图片时越界踩了别的内存。dmesg里如果出现Unable to handle kernel paging request,十有八九是它。解决方式是用devm_kzalloc(&pdev->dev, 240*320*2, GFP_KERNEL),并让screen_size与之一致。
5.4 现象四:画面有残影或刷新闪烁——rotate、line_length和电源噪声互相甩锅
残影和水波纹是最难排查的一组,因为时好时坏。现象:显示静态图片正常,滚动或刷视频时画面边缘有拖影。先排除rotate造成的映射错位:当rotate=90时,FBTFT会自动交换行列,但如果你的自写驱动没做MADCTL里的MV位调整,刷新区域和实际方向不一致,就会让人误以为“残影”。再检查fix.line_length,如果它小于xres * bpp / 8,每行像素写入时会覆盖下一行开头,出现整行错位,这是软件层面的“水波纹”。
如果软件参数都对,把耳朵贴近屏背板,听到高频吱吱声,那就是电源纹波喂到了背光或控制器。这时不是调驱动,而是检查3.3V供电是不是从开发板USB口取电,换一个稳压器单独供电后残影会立刻减少。显示屏驱动里纯软件能做的并不多,别把开机时序问题硬扛成软件bug。
6. 验证驱动有没有真正跑起来:setterm/fbi/perf帧率和一夜连续运行的实用检查
点亮和稳定是两回事。我每次拿到一个8080屏驱动,做完insmod后的第一件事是用Linux下常用命令里最基础的几把刷子验一验。
fbset -fb /dev/fb0 -g 320 240 320 240 16 dd if=/dev/urandom of=/dev/fb0 bs=38400 count=2 fbi -T 1 -d /dev/fb0 test.jpgdd if=/dev/urandom能在屏幕上撒出整帧噪点,看是否有错行、左右镜像和颜色通道对调。fbi -T 1把JPEG直接显示在tty1上,能验证用户态到内核的整条路径。想看刷新率,用perf stat统计一次整屏刷新的CPU cycles,比如:
perf stat -e cycles,instructions dd if=/dev/zero of=/dev/fb0 bs=153600 count=10连续跑100遍后读dmesg,看有没有WARNING: CPU stalls等调度延时上报。最后做整夜老化测试,开一个shell循环持续刷新时间戳,同时用serial console挂着看有没有异常。我一般会把验证命令整理成一个check_lcd.sh,每次换屏、换内核都先跑一遍,再丢给同事。
这里有一个我保留到现在的习惯:验证脚本里一定要包含一张专门测试红绿蓝纯色的图片,而不只是彩色照片。纯色能瞬间暴露数据线短路、RGB565高低位颠倒和扫描方向问题。先把这些基础项测完,再测实战画面。8080并口屏本身不复杂,复杂的是驱动长期在补丁里打转;能自己从时序和fbdev底层走过来,以后遇到再冷门的LCD也会有底气。希望帮到你。
本文还有配套的精品资源,点击获取