前言
本篇记录:如何在linux应用层开发一个APP,在开发板显示一张指定的图片。
一、准备工作
1.开发环境
- 虚拟机环境为Ubuntu16.04,安装了交叉编译器 aarch64-poky-linux-gcc。
- 开发板为迅为i.MX8MM,使用的屏幕是 7.0寸MIPI屏 ,烧录的文件系统是yocto。
- 正常编译uboot,kernel,dtbs,yocto后,下载到开发板,uboot启动并设置MIPI屏工作。
- 此时开发板正常显示yocto系统界面,准备工作OK!
以上都是开发板使用手册的教程内容
2.文件准备
- 在虚拟机下合适目录下创建目录
/test
,并在/test
目录下创建app.c
和app.h
文件。
- 准备一张像素尺寸为
1280*800
的 testpic.png
的图片,利用工具或网页将其转化为.bmp
格式,保存到/test
目录下。
二、伪代码分析
思路:将图片读取到数组中,然后打开DRM
设备填充/dev/fb0
进行显示。
先伪代码形式分析,最后贴出完整代码!
1.读取图片数据到数组
bmp格式的图片文件的头部包含了一些info
数据,这些数据并不是真正的显示画面内容,送到framebuffer
中是没有用的,会导致整个显示不正确。因此用二进制打开图片后,需要先将头部的info
数据进行剔除,留下干净的显示像素信息。
伪代码形式分析
int bmp2fb_buff(uint8_t *r_buff, uint32_t r_size, const char *bmp_path)
{
/* 二进制打开文件*/
fp = fopen( bmp_path, "rb" );
/* 提取图片头部信息*/
/* ....*/
/* 跳转到数据区*/
fseek(fp, offsets, SEEK_SET);
/* 读取真实图片数据*/
fread(buf,1,total_length,fp);
/* bmp倒叙转换到数组*/
cursor_bitmap_format_convert(r_buff, bmp_buf);
return 0;
}
这里有一个很重要概念,bmp图片数据存储方式是从下到上的,是倒着存放的,因此需要使用 cursor_bitmap_format_convert()
函数数据倒置装入framebuffer
.
2.打开DRM设备,创建fb
接下来就是使用libdrm
库,操作drm
设备创建显示缓冲区fb
,详细可参考 最简单的DRM应用程序
int main(int argc, char **argv)
{
/* 打开drm设备*/
fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
/* 获取设配 crtc/encoder/connector id */
res = drmModeGetResources(fd);
/* 获得 connector*/
conn = drmModeGetConnector(fd, conn_id);
/* 为 CRTC 创建扫描帧缓冲区 --> framebuffer*/
modeset_create_fb(fd, &buf);
/* Set a CRTC configuration,开始显示输出显示*/
drmModeSetCrtc(fd, crtc_id, buf.fb_id,
0, 0, &conn_id, 1, &conn->modes[0]);
}
3.填充framebuffer
/* 绘制图片将fb区填充图片数组 */
memcpy(frame_buf->vaddr, g_screen_buff, frame_buf->size);
三、完整代码
使用完整代码前请阅读【一、准备工作】
1.app.h
#ifndef __APP_H
#define __APP_H
/* 构建一个结构体,用于存放DRM KMS配置的参数*/
struct buffer_object {
uint32_t width;
uint32_t height;
uint32_t pitch;
uint32_t handle; //
uint32_t size;
uint32_t *vaddr;
uint32_t fb_id;
};
//14byte文件头
typedef struct
{
char cfType[2];//文件类型,"BM"(0x4D42)
int cfSize;//文件大小(字节)
int cfReserved;//保留,值为0
int cfoffBits;//数据区相对于文件头的偏移量(字节)
}__attribute__((packed)) BITMAPFILEHEADER; //告诉编译器取消结构在编译过程中的优化对齐
//40byte信息头
typedef struct
{
char ciSize[4];//BITMAPFILEHEADER所占的字节数
int ciWidth;//宽度
int ciHeight;//高度
char ciPlanes[2];//目标设备的位平面数,值为1
int ciBitCount;//每个像素的位数
char ciCompress[4];//压缩说明
char ciSizeImage[4];//用字节表示的图像大小,该数据必须是4的倍数
char ciXPelsPerMeter[4];//目标设备的水平像素数/米
char ciYPelsPerMeter[4];//目标设备的垂直像素数/米
char ciClrUsed[4]; //位图使用调色板的颜色数
char ciClrImportant[4]; //指定重要的颜色数,当该域的值等于颜色数时(或者等于0时),表示所有颜色都一样重要
}__attribute__((packed)) BITMAPINFOHEADER;
typedef struct
{
unsigned char blue;
unsigned char green;
unsigned char red;
unsigned char reserved;
}__attribute__((packed)) PIXEL;//颜色模式RGB
#endif //__APP_H
2.app.c
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <linux/fb.h>
#include "app.h"
/*
DRM 设备应用程序实现步骤
1.打开 DRM 设备
2.查找适配的'crtc + encoder + connector'
3.为 CRTC 创建扫描帧缓冲区
4. KMS 模式配置
5.绘制图像
*/
#define PER_PIXEL_BYTES 4 //一个像素点4个字节
#define SCREEN_BUFF_SIZE (1280*800*4) //u32计算
uint8_t g_screen_buff[SCREEN_BUFF_SIZE]; //屏幕是1280*800大小,每个pixel由4字节组成,实际只有3字节24位,这里用uint32_t存储即可
BITMAPFILEHEADER FileHead;
BITMAPINFOHEADER InfoHead;
static char *fbp = 0;
static int xres = 0;
static int yres = 0;
static int bits_per_pixel = 0;
int width, height;
int fbfd = 0;
/* bmp数据倒叙转换*/
static int cursor_bitmap_format_convert(char *dst,char *src)
{
int i ,j ,k;
char *psrc = src ;
char *pdst = dst;
char *p = psrc;
/* 由于bmp存储是从后面往前面,所以需要倒序进行转换 */
pdst += (width * height * PER_PIXEL_BYTES);
for(i=0;i<height;i++){
p = psrc + (i+1) * width * PER_PIXEL_BYTES;
for(j=0;j<width;j++){
pdst -= PER_PIXEL_BYTES; //Damon modify
p -= PER_PIXEL_BYTES; //Damon modify
for(k=0;k<PER_PIXEL_BYTES;k++)
{
pdst[k] = p[k]; //Damon modify
}
}
}
return 0;
}
int bmp2fb_buff(uint8_t *r_buff, uint32_t r_size, const char *bmp_path)
{
int i;
FILE *fp;
int rc;
char *bmp_buf = NULL;
char *buf = NULL;
int flen = 0;
int ret = -1;
int total_length = 0;
if(bmp_path == NULL)
{
printf("bmp_path Error,return\n");
return -1;
}
printf("img path = %s\n", bmp_path);
fp = fopen( bmp_path, "rb" );
if(fp == NULL){
printf("load cursor file open failed\n");
return -1;
}
/* 求解文件长度 */
fseek(fp,0,SEEK_SET);
fseek(fp,0,SEEK_END);
flen = ftell(fp);
bmp_buf = (char*)calloc(1,flen - 54);
if(bmp_buf == NULL){
printf("load > malloc bmp out of memory!\n");
return -1;
}
/* 再移位到文件头部 */
fseek(fp,0,SEEK_SET);
rc = fread(&FileHead, sizeof(BITMAPFILEHEADER),1, fp);
if ( rc != 1)
{
printf("read header error!\n");
fclose( fp );
return( -2 );
}
//检测是否是bmp图像
if (memcmp(FileHead.cfType, "BM", 2) != 0)
{
printf("it's not a BMP file\n");
fclose( fp );
return( -3 );
}
//获取Info
rc = fread( (char *)&InfoHead, sizeof(BITMAPINFOHEADER),1, fp );
if ( rc != 1)
{
printf("read infoheader error!\n");
fclose( fp );
return( -4 );
}
width = InfoHead.ciWidth;
height = InfoHead.ciHeight;
total_length = width * height * PER_PIXEL_BYTES;
printf("file info:\n");
printf("\t image size:\t%d\n", flen); //图片大小
printf("image info:\n");
printf("\t image size:\t%d\n",FileHead.cfSize); //文件大小
printf("\t image pixel:\twidth=%d ,height=%d\n", InfoHead.ciWidth, InfoHead.ciHeight);
printf("\t pixel bits:\t%d\n", InfoHead.ciBitCount); //
printf("\t buffer bytes:\t%d\n", total_length); //
//跳转的数据区
fseek(fp, FileHead.cfoffBits, SEEK_SET);//fb 从SEEK_SET跳转cfoffBits字节
//读取bmp数据
buf = bmp_buf;
while ((ret = fread(buf,1,total_length,fp)) >= 0) {
if (ret == 0) {
usleep(100);
continue;
}
printf("\t read bytes:\t%d\n", ret);
buf = ((char*) buf) + ret;
total_length = total_length - ret;
if(total_length == 0)
break;
}
//bmp倒叙转换到数组
cursor_bitmap_format_convert(r_buff, bmp_buf);
//内存释放
free(bmp_buf);
fclose(fp);
return 0;
}
/* 创建fb缓存,显示图片*/
static int modeset_create_fb(int fd, struct buffer_object *frame_buf)
{
struct drm_mode_create_dumb dumb_buf = {}; //创建显示缓存 显存
struct drm_mode_map_dumb dum_map = {}; //映射到内存空�?
uint8_t depth = 24; //每个通道的每个像素分量的有效比特数
uint8_t bpp = 32; //bit per pixel 指每个像素所占用的有效比特数
uint32_t i;
/* create a dumb-buffer, the pixel format is XRGB888 */
dumb_buf.width = frame_buf->width;
dumb_buf.height = frame_buf->height;
dumb_buf.bpp = bpp;
drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &dumb_buf);
/* bind the dumb-buffer to an FB object */
frame_buf->pitch = dumb_buf.pitch;
frame_buf->size = dumb_buf.size;
frame_buf->handle = dumb_buf.handle;
drmModeAddFB(fd, frame_buf->width, frame_buf->height, depth, bpp, frame_buf->pitch,
frame_buf->handle, &frame_buf->fb_id);
/* map the dumb-buffer to userspace */
dum_map.handle = dumb_buf.handle;
drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &dum_map);
frame_buf->vaddr = mmap(0, dumb_buf.size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, dum_map.offset);
/* 绘制图片将fb区填充图片数组 */
memcpy(frame_buf->vaddr, g_screen_buff, frame_buf->size);
// for (i = 0; i < (frame_buf->size/4 ); i++)
// {
// frame_buf->vaddr[i] = pic_buff[i];
// }
return 0;
}
/* 销毁fb 缓存*/
static void modeset_destroy_fb(int fd, struct buffer_object *frame_buf)
{
struct drm_mode_destroy_dumb destroy = {};
drmModeRmFB(fd, frame_buf->fb_id);
munmap(frame_buf->vaddr, frame_buf->size);
destroy.handle = frame_buf->handle;
drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy);
}
int main(int argc, char **argv)
{
struct buffer_object buf;
int fd;
int ret;
drmModeConnector *conn;
drmModeRes *res;
uint32_t conn_id;
uint32_t crtc_id;
//读取图片,处理成fb数组
bmp2fb_buff(g_screen_buff,SCREEN_BUFF_SIZE,"./testpic.bmp"); //bmp图片转换为g_screen_buff
/* 打开drm设备*/
fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
if(fd<0)
{
printf("open card0 failed!\n");
}
/* 获取设配 crtc/encoder/connector id */
res = drmModeGetResources(fd);
crtc_id = res->crtcs[0];
conn_id = res->connectors[0];
printf("crtc id=%d\n",crtc_id);
printf("crtc id=%d\n",conn_id);
/* 获得 connector*/
conn = drmModeGetConnector(fd, conn_id);
buf.width = conn->modes[0].hdisplay;
buf.height = conn->modes[0].vdisplay;
printf("buf width=%d\n",buf.width);
printf("buf height=%d\n",buf.height);
/* 为 CRTC 创建扫描帧缓冲区 --> framebuffer*/
modeset_create_fb(fd, &buf);
/* Sets a CRTC configuration,这之后就会开始在 crtc0 + connector0 pipeline 上进行以 mode0 输出显示*/
drmModeSetCrtc(fd, crtc_id, buf.fb_id,
0, 0, &conn_id, 1, &conn->modes[0]);
getchar(); //Enter 退出
modeset_destroy_fb(fd, &buf);
drmModeFreeConnector(conn);
drmModeFreeResources(res);
close(fd);
return 0;
}
四、编译与运行
图片文件准备好以后,代码也写好后,Ubuntu中的目录结构如下
/test
--app.c
--app.h
--testpic.bmp
1.编译
在bmp/
目录下顺序执行以下命令,使用交叉编译器编译app.c
export ARCH=arm64
. /opt/fsl-imx-xwayland/4.14-sumo/environment-setup-aarch64-poky-linux
aarch64-poky-linux-gcc app.c -I/usr/include/drm/ -ldrm -o app --sysroot=/opt/fsl-imx-xwayland/4.14-sumo/sysroots/aarch64-poky-linux
交叉编译命令与【迅为开发指南】给出的有一些不一致,原因是因为使用交叉编译器编译使用libdrm
库的app.c
文件时会报错:找不到include <drm.h>头文件
,于是在编译指令中加入了-I/usr/include/drm/ -ldrm
。
2.运行
- 开发板上使用
killall weston
命令杀死yocto
占用的屏幕进程,建议执行多次。
- 将
/test
文件夹整个拷贝到开发板合适目录下,比如nfs
服务绑定目录/mnt
下,运行./app
。
拷贝整个文件夹的目的:如果开发板使用wifi
利用 nfs
与 windows
进行文件共享,那么传输testpic.bmp
资源需要花费一段时间,可能导致图片显示不出来。
五、总结
本文参考文章:
感谢两位高人的代码!