framebuffer编程
一、引入
在linux下,想要玩转一块屏幕,最基本的还是要学会framebuffer,以及对应的编程套路,接下来我们就开始详细介绍一下framebuffer。完整的工程放在第四章了,大家可以作为参考。
二、一些基本概念和问题
1. framebuffer
所谓的Framebuffer(帧缓冲区)其实是用于存储图像数据的一块内存区域。我们可以将我们想要显示的图像数据写到framebuffer中,驱动程序每隔一段时间会自动的去读取Framebuffer中的图像数据,并根据读取到的图像数据在屏幕上显示对应的图像。
2. 颜色的表示
我们知道一幅图像其实是由一个一个的像素点组成的,诸多的不同颜色的像素最终组成了一幅图像,因此上面提到的图像数据指的就是这一幅图像中每个像素点的颜色数据。那么问题来了,在framebuffer中一个像素的颜色是如何表示的呢?
在美术中,将红绿蓝三种颜色称为三原色,通过将不同亮度的三元色进行组合,就可以调配出任何一种颜色。在计算机中颜色数据的表示同样利用了这个原理。在计算机中,颜色通常用 RGB(红绿蓝)模型来表示。RGB 模型将颜色描述为三个分量的组合,即红色、绿色和蓝色分量的亮度值。而根据每个分量所占的位数不同,又产生了三种不同的颜色格式,分别为:RGB888 、RGB565、RGB555。
-
RGB888:
-
RGB888 格式使用 24 位来表示一个像素的颜色信息,其中 8 位用于红色通道、8 位用于绿色通道、8 位用于蓝色通道。这意味着每个颜色通道可以有 256(2^8)种不同的亮度级别,共同组合形成一个广泛的颜色范围。
-
-
RGB565:
- RGB565 格式使用 16 位来表示一个像素的颜色信息,其中 5 位用于红色通道、6 位用于绿色通道、5 位用于蓝色通道。这种格式相比 RGB888 来说更加节省存储空间,但同时牺牲了一定的颜色精度。
-
RGB555:
-
RGB555 格式也使用 16 位来表示一个像素的颜色信息,其中 5 位用于红色通道、5 位用于绿色通道、5 位用于蓝色通道。与 RGB565 相比,RGB555 在绿色通道上减少了一位,因此在绿色调性上可能会略显不足,但整体颜色表现仍比较均衡。
-
3.如何计算屏幕上某一像素在framebuffer中对应的地址
在编程时,当我们想要把坐标为(x,y)的位置显示为某周颜色时我们应该如何确定这个点在framebuffer的地址呢?
我们可以使用以下公式进行计算:
(x,y)像素起始地址=fb_base+(xres*bpp/8)*y + x*bpp/8
fb_base: framebuffer的首地址
xres: 屏幕x轴的最大宽度
bbp: 每一个像素点的由几位数据表示(RGB888是32位 RGB565和RGB555 是16位)
三、编程套路
操作framebuffer是有固定的套路的,一般可以分为一下的步骤:
- 定义结构体用来存储屏幕的相关信息
- 使用open函数打开framebuffer
- 使用ioctl函数获取屏幕的相关信息(bbp 、屏幕的最大宽度xres、屏幕的最大高度yres等)并保存到结构体中
- 使用mmap函数将framebuffer映射到进程的地址空间,得到一个指针,通过操作这个指针就可以对framebuffer的内容进行修改了
- 封装画点函数,该函数的功能是在屏幕的任意坐标的像素点上显示你设定的颜色
- 在画点函数的基础上封装图片显示函数、文字显示函数…
上面将各个步骤列在一起,希望大家对总体的思路有个把握,接下来详细介绍各个步骤。
1.定义结构体
typedef enum {
rgb888 = 0,
rgb565,
rgb555,
} ecolor_type_t;
//屏幕信息结构体
typedef struct {
int x_res; // x轴的像素大小
int y_res; // y轴的像素大小
int bbp; // 每个像素数据的大小(单位:bit)
int size; // frambuffer的大小
void *base_addr; // framebuffer 的基地址
int fd; // framebuffer的文件描述符
ecolor_type_t colortype; // 颜色类型
} screen_info_t;
screen_info_t g_screen_info;
1.打开framebuffer
linux的基本思想是linux内一切皆文件,framebuffer也不例外,它被以文件的形式存储在/dev/中,其中的fb0就是它,我们只要使用open函数以可读可写的方式打开它即可:
int g_screen_info.fd = open("/dev/fb0", O_RDWR);
if (g_screen_info.fd)
{
printf("can't open /dev/fb0\n");
return -1;
}
2.获取屏幕的相关信息
linux的LCD驱动为给应用程序的两类参数:可变参数 struct fb_var_screeninfo 和 固定参数struct fb_fix_screeninfo,这两个结构体被定义在#include <linux/fb.h>中,他们的结构如下
可变参数
可变参数中xres、yres、bits_per_pixel,green这三个参数是我们需要使用的参数,
固定参数
固定参数我们用的不多,这里不做过多介绍
两个参数我们可以通过ioctl结合FBIOGET_VSCREENINFO或 FBIOGET_FSCREENINFO两个宏来获得,具体用法如下
//记得包含这个文件夹
#include <linux/fb.h>
//获取可变参数
struct fb_var_screeninfo screen_var_info;
if (ioctl(fd, FBIOGET_VSCREENINFO, &screen_var_info))
{
printf("can't get var\n");
return -1;
}
//获取固定参数,这个结构体没有用到,但还作为例子贴出来,实际代码中可以删除
struct fb_fix_screeninfo screen_fix_info;
if (ioctl(fd, FBIOGET_VSCREENINFO, &screen_fix_info))
{
printf("can't get fix\n");
return -1;
}
/****************************将相关信息并保存到结构体中***************************************/
screen_info_t g_screen_info = {0};
g_screen_info.x_res = screen_var_info.xres; //长度
g_screen_info.y_res = screen_var_info.yres; //宽度
g_screen_info.bbp = screen_var_info.bits_per_pixel; //bbp
g_screen_info.size =
g_screen_info.x_res * g_screen_info.y_res * g_screen_info.bbp / 8; //计算framebuffer的大小
//判断颜色格式
if (screen_var_info.bits_per_pixel == 32)
{
g_screen_info.colortype = rgb888;
}
else if (screen_var_info.bits_per_pixel == 16)
{
if (screen_var_info.green.length == 6)
{
g_screen_info.colortype = rgb565;
}
else if (screen_var_info.green.length == 5)
{
g_screen_info.colortype = rgb555;
}
}
3.映射framebuffer
//记得包含这个头文件
#include <sys/mman.h>
g_screen_info.base_addr =
mmap(NULL, g_screen_info.size, PROT_READ | PROT_WRITE, MAP_SHARED,g_screen_info.fd, 0);
//PROT_READ | PROT_WRITE 以可读可写方式映射过来
//MAP_SHARED 所写入的数据会直达驱动而不是保存在缓存中
4. 封装打点函数
/********************************************************************
* @brief 打点函数
* @param x x轴坐标
* @param y y轴坐标
* @param color 颜色数据(rgb888格式)
* @return none
*********************************************************************/
void screen_draw_point(int x, int y, unsigned int color)
{
unsigned int red, blue, green;
unsigned color_16 = 0;
//计算(x,y)在frambuffer的偏移
int offset = (g_screen_info.x_res * y + x) * g_screen_info.bbp / 8;
switch (g_screen_info.colortype)-
{
case rgb565:
//转成rgb565格式
red = (color >> 16) & (0xff);
green = (color >> 8) & (0xff);
blue = (color) & (0xff);
color_16 = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
//写入数据
*(unsigned short *)(g_screen_info.base_addr + offset) = color_16;
break;
case rgb555:
//转成rgb555格式
red = (color >> 16) & (0xff);
green = (color >> 8) & (0xff);
blue = (color) & (0xff);
color_16 = ((red >> 3) << 10) | ((green >> 3) << 5) | (blue << 3);
//写入数据
*(unsigned short *)(g_screen_info.base_addr + offset) = color_16;
break;
case rgb888:
//写入数据
*(unsigned int *)(g_screen_info.base_addr + offset) = color;
break;
default:
//不支持的颜色格式
printf("not suport this color format\n");
break;
}
}
该函数实现的基本思路是:
-
计算对应坐标的在framebuffer的偏移(可参考第二小节的公式)
-
根据屏幕支持的颜色格式将传入的RGB888颜色数据转成对应的格式
-
通过g_screen_info保存的指针以及计算的偏移,将颜色数据保存到frambuffer对应的内存中
5.封装图片显示函数、文字显示函数等函数
这部分的内容比较多,为了避免博客篇幅过长大家看的烦了,在下一篇博客介绍。
四、例子
这部分的贴出完整的代码,大家也可以直接下载我的工程编译运行一下看一下效果,工程链接我放在最后了:
1. 完整代码:
这个代码的效果是在屏幕的(100,100)处显示一个黑色的正方形
screen.h
#ifndef __SCREEN_H
#define __SCREEN_H
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
rgb888 = 0,
rgb565,
rgb555,
} ecolor_type_t;
// 图片结构体
typedef struct {
unsigned int Picture_Width;
unsigned int Picture_Height;
unsigned char *PicTure_Data;
} sPicture_t;
typedef struct {
int x_res; // x轴的像素大小
int y_res; // y轴的像素大小
int bbp; // 每个像素数据的大小(单位:bit)
int size; // frambuffer的大小
void *base_addr; // framebuffer 的基地址
int fd; // framebuffer的文件描述符
ecolor_type_t colortype; // 颜色类型
} screen_info_t;
extern void screnn_init(void);
extern void screen_draw_point(int x, int y, unsigned int color);
#ifdef __cplusplus
}
#endif
#endif
sreen.c
#include "screen.h"
#include <fcntl.h>
#include <linux/fb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
screen_info_t g_screen_info = {0};
/********************************************************************
* @brief 屏幕信息打印
* @param void
* @return none
*********************************************************************/
static void screen_information_print(void) {
printf("*********************screen "
"information******************************\n");
printf("resolution : x_res: %d y_res:%d\n", g_screen_info.x_res,
g_screen_info.y_res);
printf("colortype: ");
switch (g_screen_info.colortype) {
case rgb555:
printf("rgb555\n");
break;
case rgb565:
printf("rgb565\n");
break;
case rgb888:
printf("rgb888\n");
break;
}
printf("*********************end*********************************************"
"\n");
}
/********************************************************************
* @brief 初始化屏幕
* @param void
* @return none
*********************************************************************/
void screnn_init(void) {
// 打开文件描述符
g_screen_info.fd = open("/dev/fb0", O_RDWR);
if (g_screen_info.fd < 0) {
fprintf(stderr, "open error at %s %s %d\n", __FILE__, __FUNCTION__,
__LINE__);
exit(-1);
}
// 获取屏幕的分辨率 bpp 和frambuffer的开始地址
struct fb_var_screeninfo screen_var_info = {0};
if (ioctl(g_screen_info.fd, FBIOGET_VSCREENINFO, &screen_var_info) < 0) {
fprintf(stderr, "get var screen informatiob error at %s %s %d\n", __FILE__,
__FUNCTION__, __LINE__);
exit(-1);
}
g_screen_info.x_res = screen_var_info.xres;
g_screen_info.y_res = screen_var_info.yres;
g_screen_info.bbp = screen_var_info.bits_per_pixel;
g_screen_info.size =
g_screen_info.x_res * g_screen_info.y_res * g_screen_info.bbp / 8;
// 颜色类型初始化
if (screen_var_info.bits_per_pixel == 32)
{
g_screen_info.colortype = rgb888;
}
else if (screen_var_info.bits_per_pixel == 16)
{
if (screen_var_info.green.length == 6)
{
g_screen_info.colortype = rgb565;
}
else if (screen_var_info.green.length == 5)
{
g_screen_info.colortype = rgb555;
}
}
// mmap映射地址
g_screen_info.base_addr = mmap(NULL, g_screen_info.size, PROT_READ | PROT_WRITE, MAP_SHARED, g_screen_info.fd, 0);
screen_information_print();
// 清屏
memset(g_screen_info.base_addr, 0xff, g_screen_info.size);
}
/********************************************************************
* @brief 打点函数
* @param x x轴坐标
* @param y y轴坐标
* @param color 颜色数据(rgb888格式)
* @return none
*********************************************************************/
void screen_draw_point(int x, int y, unsigned int color) {
unsigned int red, blue, green;
unsigned color_16 = 0;
//计算(x,y)在frambuffer的偏移
int offset = (g_screen_info.x_res * y + x) * g_screen_info.bbp / 8;
switch (g_screen_info.colortype)
{
case rgb565:
//转成rgb565格式
red = (color >> 16) & (0xff);
green = (color >> 8) & (0xff);
blue = (color) & (0xff);
color_16 = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
//写入数据
*(unsigned short *)(g_screen_info.base_addr + offset) = color_16;
break;
case rgb555:
//转成rgb555格式
red = (color >> 16) & (0xff);
green = (color >> 8) & (0xff);
blue = (color) & (0xff);
color_16 = ((red >> 3) << 10) | ((green >> 3) << 5) | (blue << 3);
//写入数据
*(unsigned short *)(g_screen_info.base_addr + offset) = color_16;
break;
case rgb888:
//写入数据
*(unsigned int *)(g_screen_info.base_addr + offset) = color;
break;
default:
//不支持的颜色格式
printf("not suport this color format\n");
break;
}
}
main.c
#include "screen.h"
int main(int argc, char *argv[]) {
screnn_init();
//在(100,100)画一个黑色的正方形
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++)
{
screen_draw_point(100 + i, 100 + j, 0);
}
}
return 0;
}
2.工程链接和工程的编译
- 链接:
百度网盘链接:工程链接
提取码:qu05
- 编译方法
-
解压工程进入工程文件
tar -xvf exmpleframebuffer.tar.gz cd exmpleframebuffer
-
修改Makefile中的CC变量,将其修改为你所使用的交叉编译工具,具体可以参考我写的开箱即用的Makefile的第四章,链接如下
具体可以参考我写的编写一个开箱即用的Makefile的第四章,链接如下:
-
在命令行中执行make命令就可以得到一个可执行文件
-
将可执行文件放到开发板上运行,我使用的开发板是S5P6818,运行效果如下