前言
这个代码之所以可以写得这么短,写得这么方便,原因在于LCD的驱动程序已经写好了,并且这个驱动程序符号Framebuffer的标准,这才使得我们在实现上层应用时变得很方便。
源代码:
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
static int fd_fb;
static struct fb_var_screeninfo var; /* Current var */
static int screen_size;
static unsigned char *fb_base;
static unsigned int line_width;
static unsigned int pixel_width;
/**********************************************************************
* 函数名称: lcd_put_pixel
* 功能描述: 在LCD指定位置上输出指定颜色(描点)
* 输入参数: x坐标,y坐标,颜色
* 输出参数: 无
* 返 回 值: 会
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/05/12 V1.0 zh(angenao) 创建
***********************************************************************/
void lcd_put_pixel(int x, int y, unsigned int color)
{
unsigned char *pen_8 = fb_base+y*line_width+x*pixel_width;
unsigned short *pen_16;
unsigned int *pen_32;
unsigned int red, green, blue;
pen_16 = (unsigned short *)pen_8;
pen_32 = (unsigned int *)pen_8;
switch (var.bits_per_pixel)
{
case 8:
{
*pen_8 = color;
break;
}
case 16:
{
/* 565 */
red = (color >> 16) & 0xff;
green = (color >> 8) & 0xff;
blue = (color >> 0) & 0xff;
color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
*pen_16 = color;
break;
}
case 32:
{
*pen_32 = color;
break;
}
default:
{
printf("can't surport %dbpp\n", var.bits_per_pixel);
break;
}
}
}
int main(int argc, char **argv)
{
int i;
fd_fb = open("/dev/fb0", O_RDWR);
if (fd_fb < 0)
{
printf("can't open /dev/fb0\n");
return -1;
}
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("can't get var\n");
return -1;
}
line_width = var.xres * var.bits_per_pixel / 8;
pixel_width = var.bits_per_pixel / 8;
screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
if (fb_base == (unsigned char *)-1)
{
printf("can't mmap\n");
return -1;
}
/* 清屏: 全部设为白色 */
memset(fb_base, 0xff, screen_size);
/* 随便设置出100个为红色 */
for (i = 0; i < 100; i++)
lcd_put_pixel(var.xres/2+i, var.yres/2, 0xFF0000);
munmap(fb_base , screen_size);
close(fd_fb);
return 0;
}
需要的一堆头文件
问:能不能介绍下基于IMUX6LL的Linux开发中用到的以下头文件各自的作用?
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
在基于 i.MX6ULL 的 Linux 开发中,上述头文件的作用如下。它们涵盖了文件操作、内存映射、硬件控制等功能,是嵌入式开发中的常见组合。
1. <sys/mman.h>
- 作用:提供内存映射相关的接口。
- 主要功能:
- 文件或设备映射到内存(
mmap
)。 - 解除映射(
munmap
)。 - 修改映射区域的权限(
mprotect
)。 - 用于文件 I/O 优化和硬件寄存器的直接操作。
- 文件或设备映射到内存(
- 典型用法:
- 将
/dev/fb0
(Frame Buffer)映射到用户空间内存以直接操作显示设备。 - 操作内存共享或匿名内存。
- 将
在本实验的代码中,下面这句代码要用到这个头文件:
fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
2. <sys/types.h>
- 作用:定义常用的系统数据类型。
- 主要功能:
- 包含了基本的类型定义,比如
size_t
,pid_t
,off_t
,ssize_t
。 - 提供类型支持给许多标准函数接口,例如文件操作和进程管理。
- 包含了基本的类型定义,比如
3. <sys/stat.h>
- 作用:用于获取文件的属性信息。
- 主要功能:
- 提供函数
stat
和fstat
,获取文件的元信息(文件大小、权限、类型等)。 - 定义了文件权限和类型的宏(如
S_IFREG
,S_IFDIR
)。
- 提供函数
- 典型用法:
- 检测文件是否存在。
- 获取文件的大小以用于内存映射或文件操作。
4. <unistd.h>
- 作用:定义 POSIX 系统调用接口。
- 主要功能:
- 文件操作函数:
read
,write
,close
,lseek
。 - 文件描述符操作:
dup
,dup2
。 - 进程控制:
fork
,_exit
,exec
系列函数。 - 获取系统信息:
getpid
,getppid
,getuid
。
- 文件操作函数:
- 典型用法:
- 用于标准输入输出操作,例如从设备文件中读取数据。
- 控制子进程的执行流程。
5. <linux/fb.h>
- 作用:用于操作 Frame Buffer(帧缓冲设备)的接口定义。
- 主要功能:
- 提供帧缓冲设备的结构体和常量定义,例如
struct fb_var_screeninfo
,struct fb_fix_screeninfo
。 - 定义 IOCTL 命令,如
FBIOGET_VSCREENINFO
和FBIOPUT_VSCREENINFO
,用于获取或设置屏幕参数。
- 提供帧缓冲设备的结构体和常量定义,例如
- 典型用法:
- 获取屏幕分辨率、颜色深度等信息。
- 直接操作屏幕的帧缓冲数据以绘制图形。
6. <fcntl.h>
- 作用:定义文件控制相关接口。
- 主要功能:
- 提供
open
和fcntl
函数,用于打开文件和控制文件描述符。 - 定义文件访问模式和标志,如
O_RDONLY
,O_WRONLY
,O_RDWR
,O_CREAT
,O_TRUNC
。
- 提供
- 典型用法:
- 打开设备文件(如
/dev/fb0
,/dev/mem
)。 - 控制文件的非阻塞操作或文件锁。
- 打开设备文件(如
后面的代码中,代码:
fd_fb = open("/dev/fb0", O_RDWR);
就会用到这个头文件的声明。
7. <stdio.h>
- 作用:提供标准输入输出功能。
- 主要功能:
- 格式化输入输出:
printf
,scanf
,fgets
,sprintf
。 - 文件流操作:
fopen
,fclose
,fwrite
,fread
。 - 错误处理:
perror
。
- 格式化输入输出:
- 典型用法:
- 打印调试信息。
- 从标准输入读取配置数据。
8. <string.h>
- 作用:提供字符串和内存操作函数。
- 主要功能:
- 字符串操作:
strlen
,strcmp
,strcpy
,strcat
。 - 内存操作:
memset
,memcpy
,memcmp
,memmove
。
- 字符串操作:
- 典型用法:
- 初始化或比较内存数据块。
- 操作字符串,用于解析命令行参数或处理文件名。
9. <sys/ioctl.h>
- 作用:提供设备 I/O 控制接口。
- 主要功能:
- 定义
ioctl
函数,用于通过文件描述符控制设备。 - 与
linux/fb.h
搭配使用,通过 IOCTL 命令对帧缓冲设备进行控制。
- 定义
- 典型用法:
- 调用
ioctl(fd, FBIOGET_VSCREENINFO, &var_info)
获取帧缓冲设备信息。 - 用于控制串口、网络设备、音频设备等硬件。
- 调用
综合示例:Frame Buffer 操作
以下示例展示了如何使用这些头文件进行基本的帧缓冲操作:
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
int main() {
int fb_fd = open("/dev/fb0", O_RDWR);
if (fb_fd == -1) {
perror("Error opening framebuffer device");
return 1;
}
// 获取固定屏幕信息
struct fb_fix_screeninfo fix_info;
if (ioctl(fb_fd, FBIOGET_FSCREENINFO, &fix_info) == -1) {
perror("Error reading fixed information");
close(fb_fd);
return 1;
}
// 获取可变屏幕信息
struct fb_var_screeninfo var_info;
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &var_info) == -1) {
perror("Error reading variable information");
close(fb_fd);
return 1;
}
printf("Resolution: %dx%d, Bits Per Pixel: %d\n",
var_info.xres, var_info.yres, var_info.bits_per_pixel);
// 映射帧缓冲到内存
size_t screensize = var_info.yres_virtual * fix_info.line_length;
char *fb_data = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
if (fb_data == MAP_FAILED) {
perror("Error mapping framebuffer device to memory");
close(fb_fd);
return 1;
}
// 简单绘制:清屏为黑色
memset(fb_data, 0, screensize);
// 解除映射并关闭设备
munmap(fb_data, screensize);
close(fb_fd);
return 0;
}
运行流程:
- 打开
/dev/fb0
。 - 通过
ioctl
获取帧缓冲信息。 - 使用
mmap
将帧缓冲设备映射到内存。 - 清屏操作后解除映射并关闭文件。
这些头文件组合使用,为嵌入式开发提供强大的接口支持,适用于设备文件操作、内存映射、硬件交互等场景。
main函数的第一个参数 argc的作用
问:基于IMUX6LL的Linux开发中的main函数的输入参数的代码如下:
int main(int argc, char **argv)
请问第一个参数argc有什么作用?
答:
在 Linux 开发中,main
函数的第一个参数 argc
的作用是 表示传递给程序的命令行参数的个数。
详细解释
-
参数含义
argc
是 “Argument Count” 的缩写。- 它的值为程序执行时传递的命令行参数的数量,包括程序本身的名字。
-
值的范围
argc
的值至少为 1,因为程序的名称(argv[0]
)始终会作为第一个参数被传递。
-
典型的命令行输入
- 如果执行以下命令:
./my_program arg1 arg2 arg3
argc
的值为 4。- 对应的
argv
内容:argv[0] = "./my_program"
(程序名)。argv[1] = "arg1"
。argv[2] = "arg2"
。argv[3] = "arg3"
。
- 如果执行以下命令:
代码fd_fb = open("/dev/fb0", O_RDWR);
的作用
问:
基于IMUX6LL的Linux开发中下面这句代码请详细解释一下:
fd_fb = open("/dev/fb0", O_RDWR);
要想理解这句代码,首先要理解文件操作基本函数中的open函数,关于open函数的详细介绍,见我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/144488241
open函数理解后,我们再来理解参数"/dev/fb0"
- 在Linux 系统中,所有硬件设备都被抽象为文件,通常存放在 /dev 目录下。
/dev/fb0:表示帧缓冲(Framebuffer)设备的第一个实例(0)。 - 帧缓冲设备用于直接访问显存,支持直接绘图或更新显示内容。
- 在 i.MX6ULL 中的含义: 该设备文件通常由 Linux 内核的 Framebuffer 驱动程序提供。 对应显卡或 LCD 控制器的帧缓冲区。
问:
在实际的程序编写中,我们怎么知道我们要打开的设备是fb0还是fb1或是fb2呢?即我们怎么知道我们要操作的帧缓冲设备是第几个实例呢?
详细的答案见我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/144488919
代码ioctl(fd_fb, FBIOGET_VSCREENINFO, &var
解释下面这段代码的作用:
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("can't get var\n");
return -1;
}
这段代码涉及到的是与Linux帧缓冲(framebuffer)设备交互的操作,尤其是在嵌入式系统中通过/dev/fb0
设备与显示屏进行通信。让我们逐行解释这段代码:
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("can't get var\n");
return -1;
}
ioctl
函数
ioctl
是一个系统调用,用于向设备发送控制命令或请求设备特定的操作。ioctl
是 “Input/Output Control” 的缩写,它可以与各种设备进行交互,比如网络设备、文件系统、显卡等。
函数的原型是:
int ioctl(int fd, unsigned long request, ...);
fd
是文件描述符,通常由open()
系统调用返回,表示一个打开的设备。request
是设备特定的命令,告诉驱动程序要执行什么操作。...
是传递给设备控制命令的参数,具体依赖于设备。
参数解释
-
fd_fb
:fd_fb
是通过open("/dev/fb0", O_RDWR)
打开的帧缓冲设备文件的文件描述符。它代表了与/dev/fb0
设备的连接,允许程序对设备进行读写操作。
-
FBIOGET_VSCREENINFO
:- 这是一个控制命令,通常用于获取显示屏的基本信息,如屏幕的分辨率、颜色深度、显示模式等。
FBIOGET_VSCREENINFO
是一个宏定义,表示查询显示设备的详细信息。它会填充一个fb_var_screeninfo
结构体(通常是var
变量)来描述显示屏的各种参数。
-
&var
:- 这是
fb_var_screeninfo
结构体的指针,ioctl
调用会通过此结构体返回获取到的显示设备信息。 var
结构体包含屏幕的具体设置,比如分辨率、颜色深度、刷新的频率等。
- 这是
解释 if
语句
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("can't get var\n");
return -1;
}
ioctl(fd_fb, FBIOGET_VSCREENINFO, &var)
调用返回一个整数值。若返回值非零,则表示执行失败。ioctl
失败的原因可能是设备不可用、权限不足,或者其他硬件相关问题。- 如果
ioctl
返回非零值,程序会打印 “can’t get var” 的错误信息,并返回-1
表示出错。
总结
这段代码的目的是通过 ioctl
获取 /dev/fb0
帧缓冲设备的显示信息。如果获取失败,程序会输出错误信息并终止执行。
代码line_width = var.xres * var.bits_per_pixel / 8;
这行代码的目的是计算 每一行的字节宽度(line width),即每行显示所需要的内存空间。
让我们逐行详细解释它:
line_width = var.xres * var.bits_per_pixel / 8;
解释各个变量
-
var.xres
:这是一个存储屏幕水平分辨率的变量。xres
表示显示设备的水平像素数(即屏幕宽度上的像素数)。例如,假设屏幕的分辨率是 1920x1080,那么
var.xres
的值就是 1920。 -
var.bits_per_pixel
:这是一个存储每个像素占用的比特数的变量。bits_per_pixel
表示屏幕上每个像素的色彩深度,通常为 16、24 或 32 位。- 如果屏幕的颜色深度是 24 位(即每个像素由 3 字节组成),那么
var.bits_per_pixel
的值为 24。 - 如果屏幕的颜色深度是 32 位(即每个像素由 4 字节组成),那么
var.bits_per_pixel
的值为 32。
- 如果屏幕的颜色深度是 24 位(即每个像素由 3 字节组成),那么
-
/ 8
:这表示将比特(bit)转换为字节(byte)。因为每个字节是 8 位(bit),所以在计算像素的宽度时,必须将每个像素的比特数除以 8 来得到它的字节数。
代码的功能
line_width = var.xres * var.bits_per_pixel / 8;
这行代码的目的是计算 每一行的字节宽度(line width),即每行显示所需要的内存空间。
计算步骤:
var.xres
给出了每行的像素数。var.bits_per_pixel
给出了每个像素的比特数,表示一个像素所占的内存大小。- 通过将每个像素的比特数除以 8,转换为字节单位。
- 将每行的像素数乘以每个像素占用的字节数,得到一行所需的内存大小(以字节为单位)。
例子:
假设:
var.xres = 1920
(即屏幕的分辨率是 1920 像素宽)var.bits_per_pixel = 32
(每个像素占 32 比特,即 4 字节)
那么,每一行的字节宽度:
line_width = 1920 * 32 / 8 = 1920 * 4 = 7680 字节。
这意味着每一行的像素数据占用 7680 字节的内存空间。
总结
这行代码计算了每一行屏幕图像数据的内存占用大小,以字节为单位。它是处理图像数据时非常重要的一个参数,通常用于确定如何在内存中访问和处理每行的像素数据。
代码pixel_width = var.bits_per_pixel / 8;
代码 pixel_width = var.bits_per_pixel / 8;
的作用是计算 每个像素的字节宽度,即每个像素占用的内存空间,单位是字节(byte)。具体的解释如下:
变量解释:
var.bits_per_pixel
:每个像素的比特数。它表示显示屏上每个像素的颜色深度,通常是 16 位、24 位、32 位等。例如,如果每个像素使用 32 位表示,则var.bits_per_pixel
为 32。
计算:
/ 8
:这个操作将比特转换为字节,因为 1 字节(byte) = 8 比特(bit)。所以通过除以 8,var.bits_per_pixel
就从比特转换为字节,表示每个像素所占的内存字节数。
示例:
假设:
var.bits_per_pixel = 32
(每个像素使用 32 位来表示,即 4 字节)
那么:
pixel_width = var.bits_per_pixel / 8 = 32 / 8 = 4 字节
这意味着每个像素需要 4 字节的内存空间。
作用:
pixel_width
变量保存了每个像素的字节数,它可以用于后续的内存操作,如计算图像的内存总大小、访问特定像素的位置等。每个像素占用的字节数在图像数据处理中是一个非常重要的参数,尤其是在涉及像素数据的读写、图像处理和缓冲区管理时。
代码fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
这行代码的作用是将 /dev/fb0 设备的内存映射到进程的虚拟内存空间。通过这种方式,你可以直接访问Framebuffer的内存,并对其进行操作(例如清屏、绘制像素等),而不需要显式的读写操作。接下来,你可以通过 fb_base 指针来访问和修改Framebuffer内容。
详细介绍见:
https://blog.csdn.net/wenhao_ir/article/details/144489705
代码if (fb_base == (unsigned char *)-1)
if (fb_base == (unsigned char *)-1)
{
printf("can't mmap\n");
return -1;
}
这段代码:
if (fb_base == (unsigned char *)-1)
{
printf("can't mmap\n");
return -1;
}
是用于检查 mmap()
调用是否成功的错误处理代码。
解释过程:
-
mmap()
函数调用:
之前的代码段中,调用了mmap()
来将 framebuffer 设备(即/dev/fb0
)映射到进程的虚拟内存中:fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
mmap()
的返回值是映射的内存区域的起始地址。如果映射成功,它会返回映射区域的地址(一个指针)。如果映射失败,它会返回(void *)-1
,表示映射失败。 -
fb_base == (unsigned char *)-1
:mmap()
如果失败,返回值为(void *)-1
。但是,在 C 语言中,(void *)-1
会被强制转换为(unsigned char *)
类型,因此判断条件是fb_base == (unsigned char *)-1
。- 这是在检查
mmap()
是否失败。如果fb_base
的值等于(unsigned char *)-1
,则说明映射失败。
-
错误处理:
- 如果
mmap()
返回(void *)-1
,代码进入这个if
语句块,打印错误信息"can't mmap"
,表示内存映射失败。 - 然后,
return -1;
会结束main()
函数并返回-1
,表示程序执行失败,无法继续进行后续操作。
- 如果
总结:
这段代码的作用是检查 mmap()
是否成功。如果返回值是 (void *)-1
,说明内存映射失败,那么程序会打印错误信息并返回错误代码 -1
,表示程序无法继续执行。如果 mmap()
成功,fb_base
将包含映射的内存区域的地址,程序可以继续执行后续操作。
代码unsigned char *pen_8 = fb_base+y*line_width+x*pixel_width;
这句代码是算出某个像素点的内存偏移量,(x, y)这个坐标显然是从0开始的,而不是从1开始的。
代码pen_16 = (unsigned short *)pen_8;
这行代码:
pen_16 = (unsigned short *)pen_8;
的作用是将指针 pen_8
转换为 unsigned short *
类型的指针,并将其赋值给 pen_16
。为了更好地理解其含义,我们需要分析以下几点:
-
pen_8
是一个unsigned char *
类型的指针:
在函数lcd_put_pixel
中,pen_8
是指向某个像素位置的指针,类型为unsigned char *
。它表示的是一个字节大小的内存地址,也就是 1 个字节的地址。unsigned char *pen_8 = fb_base + y * line_width + x * pixel_width;
其中,
fb_base
是指向显存的基地址(内存映射的起始地址),y * line_width
是该行的偏移量,x * pixel_width
是该列的偏移量。通过加和计算,pen_8
指向了(x, y)
坐标对应的像素位置。 -
pen_16
是一个unsigned short *
类型的指针:
pen_16
是一个指向unsigned short
类型的指针,也就是说每次操作时,它会以 2 字节为单位访问内存(unsigned short
占 2 个字节)。在这个上下文中,unsigned short
通常用来表示 16 位颜色信息(比如 RGB565 格式,其中每个像素用 2 字节表示)。unsigned short *pen_16;
-
类型转换:
通过这行代码,将pen_8
(unsigned char *
)转换为unsigned short *
(pen_16
)。这种转换的意义在于,你希望以unsigned short
类型访问内存,并对内存进行 16 位(2 字节)操作。为什么要做这个转换呢?因为显示屏上不同的颜色模式会使用不同的位深度。如果当前的屏幕颜色深度是 16 位(即每个像素使用 2 个字节),则你需要以 2 字节为单位来访问该位置的内存,而不是 1 字节。因此,
pen_16
是为访问 16 位颜色(通常是 RGB565 格式)而存在的。 -
为什么需要转换:
pen_8
指向的是一个字节(8 位)的内存地址,如果直接操作它,会按字节进行处理。但对于 16 位的显示模式,你需要按 16 位(2 字节)来存储颜色值。通过将pen_8
转换成pen_16
,你可以以 16 位为单位来读写内存,从而确保 16 位颜色值能够正确存储。
总结一下,这行代码的作用是将一个字节指针 pen_8
转换为一个短整型指针 pen_16
,以便后续能够按 16 位操作内存。这在处理不同位深度的显示模式时是必要的,因为每个像素所占的内存大小不同。
上机实验
首先交叉编译源码,按照博文 Ubuntu中安装配置交叉编译工具并进行测试 中第04部分的方法进行交叉编译。
在 /home/book/mycode 目录中建立目录C0002_show_pixel,并进入:
cd /home/book/mycode
mkdir C0002_show_pixel
cd /home/book/mycode/C0002_show_pixel
然后把文件:show_pixel.c 复制到目录mycode中,这个代码的百度网盘下载地址:
https://pan.baidu.com/s/1Oq5FpAs1RMdVxoyvSZ1olg?pwd=wugb
执行下面的命令生成可执行文件:
arm-buildroot-linux-gnueabihf-gcc -o show_pixel show_pixel.c
然后再参考博文 IMX6ULL开发板挂载 Ubuntu 的 NFS 目录,并以交叉编译得到的hello程序进行测试 把生成的程序在开发板上进行测试
cd mycode
cp /mnt/show_pixel ./show_pixel
添加执行权限
chmod +x show_pixel
执行程序:
./show_pixel
执行程序前的屏幕:
执行程序后的屏幕:
可见中间划了一道红线。
总结
这个代码之所以可以写得这么短,写得这么方便,原因在于LCD的驱动程序已经写好了,并且这个驱动程序符号Framebuffer的标准,这才使得我们在实现上层应用时变得很方便。