LV18-01-LCD应用编程-01-FrameBuffer设备
本文主要是LCD应用编程——FrameBuffer设备基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) | |
linux内核 | linux-4.15(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 | |
其他网站 | kernel - Linux source code (v4.15) - Bootlin | linux内核源码在线查看 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
https://elixir.bootlin.com/linux/latest/source | 在线阅读linux kernel源码 | |
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) |
一、什么是 FrameBuffer
Framebuffer - Wikipedia,Frame 是帧的意思, buffer 是缓冲的意思,所以 Framebuffer 就是帧缓冲, 其实 Framebuffer 就是一块内存,里面保存着一帧图像。帧缓冲(framebuffer)是 Linux 系统中的一种显示驱动接口,它将显示设备(如 LCD) 进行抽象、 屏蔽了不同显示设备硬件的实现,对应用层抽象为一块显示内存(显存),它允许上层应用程序直接对显示缓冲区进行读写操作,而用户不必关心物理显存的位置等具体细节,这些都由Framebuffer 设备驱动来完成。
所以在 Linux 系统中,显示设备被称为 FrameBuffer 设备(帧缓冲设备),所以 LCD 显示屏自然而言就是 FrameBuffer 设备。
FrameBuffer 设备对应的设备文件为/dev/fbX(X 为数字, 0、 1、 2、 3 等) , Linux下可支持多个 FrameBuffer 设备,最多可达 32 个,分别为/dev/fb0 到/dev/fb31, 开发板出厂系统中, /dev/fb0设备节点便是 LCD 屏。
应用程序读写/dev/fbX 就相当于读写显示设备的显示缓冲区(显存),如 LCD 的分辨率是 800*480,每一个像素点的颜色用 24 位(如 RGB888)来表示,那么这个显示缓冲区的大小就是 800 x 480 x 24 / 8 =1152000 个字节。 例如执行下面这条命令将 LCD 清屏,也就是将其填充为黑色(假设 LCD 对应的设备节点是/dev/fb0,分辨率为 800*480, RGB888 格式):
1 | dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125 |
这条命令的作用就是将 1125x1024 个字节数据全部写入到 LCD 显存中,并且这些数据都是 0x0。
二、LCD基础知识
LCD基础知识在驱动开发中学习,这里暂时先不关心。
三、LCD应用编程
应用程序通过对 LCD 设备节点/dev/fb0(假设 LCD 对应的设备节点是/dev/fb0)进行 I/O 操作即可实现对 LCD 的显示控制,实质就相当于读写了 LCD 的显存,而显存是 LCD 的显示缓冲区, LCD 硬件会从显存中读取数据显示到 LCD 液晶面板上。
在应用程序中,操作/dev/fbX 的一般步骤如下:
①、首先打开/dev/fbX 设备文件。
②、 使用 ioctl()函数获取到当前显示设备的参数信息,如屏幕的分辨率大小、像素格式,根据屏幕参数计算显示缓冲区的大小。
③、通过存储映射 I/O 方式将屏幕的显示缓冲区映射到用户空间(mmap)。
④、映射成功后就可以直接读写屏幕的显示缓冲区,进行绘图或图片显示等操作了。
⑤、完成显示后, 调用 munmap()取消映射、并调用 close()关闭设备文件。
1. ioctl()获取屏幕参数信息
当打开 LCD 设备文件之后,需要先获取到 LCD 屏幕的参数信息,如 LCD 的 X 轴分辨率、 Y 轴分辨率以及像素格式等信息,通过这些参数计算出 LCD 显示缓冲区的大小。
通过 ioctl() 函数来获取屏幕参数信息,对于 Framebuffer 设备来说,常用的 request 包括FBIOGET_VSCREENINFO、FBIOPUT_VSCREENINFO、 FBIOGET_FSCREENINFO。 这几个宏定义以及相关的结构体都定义在<linux/fb.h>(fb.h - include/uapi/linux/fb.h - Linux source code v4.15 - Bootlin)头文件中,所以在我们的应用程序中需要包含该头文件。这几个宏定义如下所示:
1 |
1.1 FBIOGET_VSCREENINFO
表示获取 FrameBuffer 设备的可变参数信息,可变参数信息使用 struct fb_var_screeninfo 结构体来描述,所以此时ioctl()需要有第三个参数,它是一个 struct fb_var_screeninfo *指针,指向 struct fb_var_screeninfo 类型对象, 调用 ioctl()会将 LCD 屏的可变参数信息保存在 struct fb_var_screeninfo 类型对象中,如下所示:
1 | struct fb_var_screeninfo fb_var; |
struct fb_var_screeninfo 结构体(fb.h - include/uapi/linux/fb.h - Linux source code v4.15 - Bootlin)内容如下所示:
1 | struct fb_var_screeninfo { |
通过 xres、 yres 获取到屏幕的水平分辨率和垂直分辨率, bits_per_pixel 表示像素深度 bpp,即每一个像素点使用多少个 bit 位来描述它的颜色,通过 xres * yres * bits_per_pixel / 8 计算可得到整个显示缓存区的大小。
red、 green、 blue 描述了 RGB 颜色值中 R、 G、 B 三种颜色通道分别使用多少 bit 来表示以及它们各自的偏移量,通过 red、 green、 blue 变量可知道 LCD 的 RGB 像素格式,例如是 RGB888 还是 RGB565,亦或者是 BGR888、 BGR565 等。 struct fb_bitfield 结构体(fb.h - include/uapi/linux/fb.h - Linux source code v4.15 - Bootlin)如下所示:
1 | struct fb_bitfield { |
1.2 FBIOPUT_VSCREENINFO
表示设置 FrameBuffer 设备的可变参数信息,既然是可变参数,那说明应用层可对其进行修改、重新配置,当然前提条件是底层驱动支持这些参数的动态调整,如在我们的 Windows 系统中,用户可以修改屏幕的显示分辨率,这就是一种动态调整。同样此时 ioctl()需要有第三个参数, 也是一个 struct fb_var_screeninfo *指针,指向 struct fb_var_screeninfo 类型对象, 表示用 struct fb_var_screeninfo 对象中填充的数据设置 LCD, 如下所示:
1 | struct fb_var_screeninfo fb_var = {0}; |
1.3 FBIOGET_FSCREENINFO
表示获取 FrameBuffer 设备的固定参数信息,既然是固定参数,那就意味着应用程序不可修改。固定参数信息使用struct fb_fix_screeninfo结构体来描述,所以此时ioctl()需要有第三个参数,它是一个 struct fb_fix_screeninfo *指针,指向 struct fb_fix_screeninfo 类型对象,调用 ioctl()会将 LCD 的固定参数信息保存在 struct fb_fix_screeninfo 对象中,如下所示:
1 | struct fb_fix_screeninfo fb_fix; |
struct fb_fix_screeninfo 结构体(fb.h - include/uapi/linux/fb.h - Linux source code v4.15 - Bootlin)内容如下所示:
1 | struct fb_fix_screeninfo { |
smem_start 表示显存的起始地址,这是一个物理地址,当然在应用层无法直接使用; smem_len 表示显存的长度,这个长度并一定等于 LCD 实际的显存大小。 line_length 表示屏幕的一行像素点有多少个字节,通常可以使用 line_length * yres 来得到屏幕显示缓冲区的大小。
1.4 使用实例
1.4.1 代码编写
1 |
|
1.4.2 开发板测试
我们编译完直接执行以下命令:
1 | ./app_demo |
然后便会有如下打印信息:
打印出像素格式为 R<11 5> G<5 6> B<0 5>, 分别表示 R、 G、 B 三种颜色分量对应的偏移量和长度,第一个数字表示偏移量,第二个参数为长度, 从打印的结果可知, 16bit 颜色值中高 5 位表示 R 颜色通道、中间 6 位表示 G 颜色通道、低 5 位表示 B 颜色通道, 所以这是一个 RGB565 格式的显示设备。
Tips:正点原子的 RGB LCD 屏幕,包括 4.3 寸 800*480、 4.3 寸 480*272、 7 寸 800*480、 7 寸 1024*600 以及 10.1 寸 1280*800 硬件上均支持 RGB888, 但 ALPHA 开发板出厂系统中, LCD 驱动程序将其实现为一个 RGB565 格式的显示设备, 用户可修改设备树使其支持 RGB888,或者通过 ioctl 修改。
2. 使用 mmap()将显示缓冲区映射到用户空间
2.1 存储映射I/O
进程是无法直接访问物理地址的,能够访问的只有虚拟地址。这里需要通过存储映射I/O(memory-mapped I/O) 将显示器的显示缓冲区(显存)映射到进程的虚拟地址空间中,这样应用程序便可直接对显示缓冲区进行读写操作。
什么是存储映射 I/O(memory-mapped I/O) ?它就是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中, 当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操作) ,将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作) 。这样就可以在不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。
为什么这里需要使用存储映射 I/O 这种方式呢? 其实使用普通的 I/O 方式(如直接 read、 write)也是可以的, 只是, 当数据量比较大时,普通 I/O 方式效率较低。 假设某一显示器的分辨率为 1920 * 1080,像素格式为 ARGB8888,针对该显示器,刷一帧图像的数据量为 1920 x 1080 x 32 / 8 = 8294400 个字节(约等于 8MB),这还只是一帧的图像数据,而对于显示器来说,显示的图像往往是动态改变的,意味着图像数据会被不断更新。在这种情况下, 数据量是比较庞大的, 使用普通 I/O 方式必然导致效率低下,所以才会采用存储映射I/O 方式。
2.2 相关函数
这里会用到两个函数:
1 |
|
2.2.1 mmap()
在 linux 下可以使用 man 命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。 实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
【函数参数】
addr: 参数 addr 用于指定映射到内存区域的起始地址。通常将其设置为 NULL,这表示由系统选择该映射区的起始地址,这是最常见的设置方式;如果参数 addr 不为 NULL,则表示由自己指定映射区的起始地址,此函数的返回值是该映射区的起始地址。
length: 参数 length 指定映射长度, 表示将文件中的多大部分映射到内存区域中,以字节为单位,如length=1024 * 4,表示将文件的 4K 字节大小映射到内存区域中。
offset: 文件映射的偏移量,通常将其设置为 0,表示从文件头部开始映射;所以参数 offset 和参数 length 就确定了文件的起始位置和长度, 将文件的这部分映射到内存区域中,如图 所示。
fd: 文件描述符,指定要映射到内存区域中的文件。
prot: 参数 prot 指定了映射区的保护要求(可取值为PROT_EXEC, 映射区可执行;PROT_READ, 映射区可读;PROT_WRITE, 映射区可写,PROT_NONE, 映射区不可访问 )。 可将 prot 指定为为 PROT_NONE,也可将其设置为 PROT_EXEC、 PROT_READ、PROT_WRITE 中一个或多个(通过按位或运算符任意组合)。对指定映射区的保护要求不能超过文件 open()时的访问权限,例如如,文件是以只读权限方式打开的,那么对映射区的不能指定为 PROT_WRITE。
flags: 参数 flags 可影响映射区的多种属性, 参数 flags 必须要指定以下两种标志之一:
MAP_SHARED: 此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映射区中的数据更新到文件中,并且允许其它进程共享。
MAP_PRIVATE: 此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-onwrite),对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写。
点击查看补充内容
除此之外,还可将以下标志中的 0 个或多个组合到参数 flags 中,通过按位或运算符进行组合:
MAP_FIXED: 在未指定该标志的情况下,如果参数 addr 不等于 NULL,表示由调用者自己指定映射区的起始地址,但这只是一种建议、而并非强制,所以内核并不会保证使用参数 addr 指定的值作为映射区的起始地址;如果指定了 MAP_FIXED 标志,则表示要求必须使用参数 addr 指定的值作为起始地址,如果使用指定值无法成功建立映射时,则放弃。通常,不建议使用此标志,因为这不利于移植。
MAP_ANONYMOUS: 建立匿名映射, 此时会忽略参数 fd 和 offset,不涉及文件,而且映射区域无法和其它进程共享。
MAP_ANON: 与 MAP_ANONYMOUS 标志同义,不建议使用。
MAP_DENYWRITE: 该标志被忽略。
MAP_EXECUTABLE: 该标志被忽略。
MAP_FILE: 兼容性标志,已被忽略。
MAP_LOCKED: 对映射区域进行上锁。
除了以上标志之外,还有其它一些标志,这里便不再介绍,可通过 man 手册进行查看。 在众多标志当中,通常情况下,参数 flags 中只指定了 MAP_SHARED。
【返回值】成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1, 通常使用MAP_FAILED 来表示, 并且会设置 errno 来指示错误原因。
【使用格式】
【注意事项】
(1)
对于 mmap()函数,参数 addr 和 offset 在不为 NULL 和 0 的情况下, addr 和 offset 的值通常被要求是系统页大小的整数倍,可通过 sysconf()函数获取页大小,如下所示(以字节为单位):
1 | sysconf(_SC_PAGE_SIZE) |
虽然对 addr 和 offset 有这种限制,但对于参数 length 长度来说,却没有这种要求,如果映射区的长度不是页长度的整数倍时,会怎么样呢?对于这个问题的答案,我们首先需要了解到,对于 mmap()函数来说,当文件成功被映射到内存区域时,这段内存区域(映射区)的大小通常是页大小的整数倍,即使参数 length并不是页大小的整数倍。如果文件大小为 96 个字节,我们调用 mmap()时参数 length 也是设置为 96,假设系统页大小为 4096 字节(4K),则系统通常会提供 4096 个字节的映射区,其中后 4000 个字节会被设置为0,可以修改后面的这 4000 个字节,但是并不会影响到文件。 但如果访问 4000 个字节后面的内存区域,将会导致异常情况发生,产生 SIGBUS 信号。对于参数 length 任需要注意,参数 length 的值不能大于文件大小,即文件被映射的部分不能超出文件。
与映射区相关的两个信号 :
SIGSEGV: 如果映射区被 mmap()指定成了只读的,那么进程试图将数据写入到该映射区时,将会产生 SIGSEGV 信号,此信号由内核发送给进程。在第八章中给大家介绍过该信号,该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。
SIGBUS: 如果映射区的某个部分在访问时已不存在,则会产生 SIGBUS 信号。 例如,调用 mmap()进行映射时,将参数 length 设置为文件长度,但在访问映射区之前,另一个进程已将该文件截断(例如调用 ftruncate()函数进行截断),此时如果进程试图访问对应于该文件已截去部分的映射区,进程将会受到内核发送过来的 SIGBUS 信号,同样,该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。
(2)如果 mmap()指定了 MAP_PRIVATE 标志,在解除映射之后,进程对映射区的修改将会丢弃。
2.2.2 munmap()
在 linux 下可以使用 man 命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数munmap( )用来取消参数addr所指的映射内存起始地址,参数length则是欲取消的内存大小。当进程结束或利用exec相关函数来执行其他程序时,映射内存会自动解除,但关闭对应的文件描述符时不会解除映射。
【函数参数】
- addr:已经映射的虚拟地址。
- length:映射区大小。
【返回值】 int 类型,成功返回0,失败返回-1。
【使用格式】
【注意事项】
(1)munmap()系统调用解除指定地址范围内的映射, 参数 addr 指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍;参数 length 是一个非负整数,指定了待解除映射区域的大小(字节数) , 被解除映射的区域对应的大小也必须是系统页大小的整数倍,即使参数 length 并不等于系统页大小的整数倍,与mmap()函数相似。
(2)当进程终止时也会自动解除映射(如果程序中没有显式调用 munmap()),但调用 close()关闭文件时并不会解除映射。
(3)通常将参数 addr 设置为 mmap()函数的返回值,将参数 length 设置为 mmap()函数的参数 length,表示解除整个由 mmap()函数所创建的映射。
(4)munmap()函数并不影响被映射的文件,也就是说,当调用 munmap()解除映射时并不会将映射区中的内容写到磁盘文件中。如果 mmap()指定了 MAP_SHARED 标志,对于文件的更新,会在我们将数据写入到映射区之后的某个时刻将映射区中的数据更新到磁盘文件中,由内核根据虚拟存储算法自动进行。
2.2.3 msync()
在 linux 下可以使用 man 命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】
read()和 write()系统调用在操作磁盘文件时不会直接发起磁盘访问(读写磁盘硬件),而是仅仅在用户空间缓冲区和内核缓冲区之间复制数据,在后续的某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘中,所以由此可知,调用 write()写入到磁盘文件中的数据并不会立马写入磁盘,而是会先缓存在内核缓冲区中,所以就会出现 write()操作与磁盘操作并不同步,也就是数据不同步。
对于存储 I/O 来说亦是如此,写入到文件映射区中的数据也不会立马刷新至磁盘设备中,而是会在我们将数据写入到映射区之后的某个时刻将映射区中的数据写入磁盘中。 所以会导致映射区中的内容与磁盘文件中的内容不同步。我们可以调用 msync()函数将映射区中的数据刷写、更新至磁盘文件中(同步操作) ,系统调用 msync()类似于 fsync()函数,不过 msync()作用于映射区。
【函数参数】
- addr:需同步的内存区域的起始地址 。
- length:需同步的内存区域的大小。
- 参数 flags 应指定为 MS_ASYNC 和 MS_SYNC 两个标志之一, 除此之外,还可以根据需求选择是否指定 MS_INVALIDATE 标志,作为一个可选标志。
MS_ASYNC: 以异步方式进行同步操作。调用 msync()函数之后,并不会等待数据完全写入磁盘之后才返回。
MS_SYNC: 以同步方式进行同步操作。调用 msync()函数之后,需等待数据全部写入磁盘之后才返回。
MS_INVALIDATE: 是一个可选标志, 请求使同一文件的其它映射无效(以便可以用刚写入的新值更新它们) 。
【返回值】 int 类型,成功情况下返回 0;失败将返回-1、并设置 errno。
【使用格式】
【注意事项】
(1)对于参数 addr 来说,同样也要求必须是系统页大小的整数倍, 也就是与系统页大小对齐。例如,调用 msync()时,将 addr 设置为 mmap()函数的返回值,将 length 设置为 mmap()函数的 length 参数,将对文件的整个映射区进行同步操作。