LCD屏显示练习【二】
目录
题目
设计一个程序,该程序在运行之后自动播放一段开机动画,开机动画结束后可以调转到登录界面,登录界面有2个按钮,分别是登录和退出,点击登录之后可以显示系统主界面。主界面自拟,要求主界面有一个返回按钮,点击返回按钮可以回到登录界面。要求:不可以使用 goto 语句。
题目分析
该题目的主要诉求可总结为:
- 开机时需要有一段开机动画,且在开机动画结束后可以之间到达操作主界面
- 主界面上会有两个按钮,即切换界面和退出
- 界面切换不能使用goto语句
思路解析
- 开机动画可以使用裁剪工具GIFtiqu将动态图裁剪为一张张jpeg图片,在将jpeg图片解码循环显示,且可以将登录界面图片放至循环的最后一张。
- 触屏按键切换,该功能涉及到读取LCD屏的触摸屏设备信息。需要创建对应格式的结构体变量,并利用read()函数将设备文件中的信息存储进创建的结构体变量中。
- 由于读取触摸屏参数不能只读一次,所以采取死循环作为循环,并设置退出键坐标为退出循环或者退出整个程序。
知识点涉及
- 开机动画图片循环时,需要使用usleep()控制循环间隙。该函数的单位为微秒,且1s(秒) = 1000ms(毫秒),1ms(毫秒) = 1000μs(微秒)。经过计算,使得图片循环能够满足人眼视觉残留条件,最终达到图片“动”起来的效果。
- 由于开机动画使用的是JPEG图片,所以还涉及到JPEG的解码步骤。
- 触摸屏的设备信息在linux系统下也是一个文件,所以我们可以通过read()将这些信息读取到特定结构的结构体变量中,再来对获取到的参数做相应的操作。
代码展示
/*******************************************************************
*
* file name: main.c
* author : 790557054@qq.com
* date : 2024/05/14
* function : 该案例是掌握LCD屏触摸原理以及显示图片切换过程
* note : None
*
* CopyRight (c) 2023-2024 790557054@qq.com All Right Reseverd
*
* *****************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <linux/input.h>
#include "jpeglib.h"
/********************************************************************
*
* name : read_JPEG_file
* function : 实现完成libjpeg库的移植,实现在LCD上的任意位置
显示一张任意大小的jpg图片,并且对可能越界的情况做错误处理。
* argument :
* @filename :需要解码的jpg图片
@start_x :图片显示初始位置的横坐标
@start_y :图片显示初始位置的纵坐标
@lcd_mp :LCD屏内存映射空间的地址
*
* retval : 调用成功返回1,调用失败返回0
* author : 790557054@qq.com
* date : 2024/05/13
* note : 学习JPEG的解码过程,以及JPEG存储颜色分量的方式
*
* *****************************************************************/
int read_JPEG_file(char *filename, int start_x, int start_y, int *lcd_mp)
{
/*[1]:创建解码对象,并且对解码对象进行初始化,另外需要创建错误处理对象,并和解码对象进行关联*/
// 创建解码对象,其是一个结构体变量
struct jpeg_decompress_struct cinfo;
// 创建错误处理对象
struct jpeg_error_mgr jerr;
// 将错误处理对象与解码对象相关联
cinfo.err = jpeg_std_error(&jerr);
// 对解码对象进行初始化
jpeg_create_decompress(&cinfo);
/*[2]:打开待解码的jpg图片,使用fopen的时候需要添加选项”b”,以二进制方式打开文件!*/
FILE *infile; // 接收打开文件的文件指针
unsigned char *buffer; // 输出行缓冲区
int row_stride; // buffer一行的像素点数量,即图片的宽度
// 以二进制方式打开图片,并进行错误处理
if ((infile = fopen(filename, "rb")) == NULL)
{
fprintf(stderr, "can't open %s\n", filename);
return 0;
}
// 把打开的文件的文件指针和解码对象进行绑定
jpeg_stdio_src(&cinfo, infile);
/*[3]:读取待解码图片的文件头,并把图像信息和解码对象进行关联,通过解码对象对jpg图片进行解码*/
(void)jpeg_read_header(&cinfo, TRUE);
/*[4]:可以选择设置解码参数,如果打算以默认参数对jpg图片进行解码,则可以省略该步骤!*/
/* 在该习题要求中,并不涉及图片缩放等问题,所以我们可以省略该步骤
* jpeg_read_header(),
*/
/*[5]:开始对jpg图片进行解码,调用函数之后开始解码,可以得到图像宽、图像高等信息!*/
// 我们只需要调用该函数,将图像信息放入解码对象中,无需注意其的返回值
(void)jpeg_start_decompress(&cinfo);
/*[6]:开始设计一个循环,在循环中每次读取1行的图像数据,并写入到LCD中*/
// 计算图像一行的大小
row_stride = cinfo.output_width * cinfo.output_components;
// 为自定义缓冲区申请堆内存,注意申请的内存空间大小应为图像一行的大小
buffer = calloc(1, row_stride);
// 定义一个int类型变量,用于存放颜色分量数据
int data = 0;
/*定义一个循环,用于循环写入一行的图像数据;
使用解码对象当前扫描行数与图像的高比较结果作为循环条件,当两者相等,即图像数据写入完后退出循环*/
while (cinfo.output_scanline < cinfo.output_height)
{
/*调用jpeg_read_scanlines函数,读取解码对象中的图像一行数据,并存放进自定义缓冲区中
且cinfo.output_scanline会随着调用该函数而增加1,保证while循环能够正常退出*/
(void)jpeg_read_scanlines(&cinfo, &buffer, 1); // 从上到下,从左到右 RGB RGB RGB RGB
// 将缓冲区中存储的数据逐一写入LCD的内存映射空间中
for (int i = 0; i < cinfo.output_width; ++i) // 012 345
{
/*由于图片没有透明度,所以一个像素点大小为3byte,而data为int类型变量,所以需要
借助"|=" 使得颜色分量顺序存储正确;又因为JEPG存储颜色分量顺序为RGB,所以进行下面算法*/
data |= buffer[3 * i] << 16; // R
data |= buffer[3 * i + 1] << 8; // G
data |= buffer[3 * i + 2]; // B
/*把像素点写入到LCD的指定位置。其中800*start_y + start_x控制的是用户自定义的图片显示初始位置;
800*(cinfo.output_scanline-1)控制的是写入图像数据的行数切换;+ i控制的是写入图像数据的列数切换*/
lcd_mp[800 * start_y + start_x + 800 * (cinfo.output_scanline - 1) + i] = data;
// 最后需将data内部清零,避免对下一次循环的颜色分量写入造成影响
data = 0;
}
}
/*[7]:在所有的图像数据都已经解码完成后,则调用函数完成解码即可,然后释放相关资源!(不要遗漏打开的图像文件)*/
// 解码完成
(void)jpeg_finish_decompress(&cinfo);
// 释放解码对象
jpeg_destroy_decompress(&cinfo);
// 关闭打开的图像文件
fclose(infile);
return 1;
}
int main(int argc, char const *argv[])
{
// 1.打开LCD open
int lcd_fd = open("/dev/fb0", O_RDWR);
if(lcd_fd == -1)
{
perror("open file of /dev/fb0 is fail");
return -1;
}
// 读取用户触摸坐标
// 1.打开触摸屏
int ts_fd = open("/dev/input/event0", O_RDWR);
int cnt = 0; // 记录收到的触摸函数值
int x, y; // 定义两个存放横坐标与纵坐标值的变量
struct input_event ts_event; // 定义触摸屏输入信息的结构体变量
// 2.对LCD进行内存映射 mmap
int *lcd_mp = (int *)mmap(NULL, 800 * 480 * 4, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0);
if(lcd_mp == MAP_FAILED)
{
perror("memory map LCd is fail");
return -1;
}
// 3.显示开机动画
char gif_path[128] = {0};
// 开机动画总共有60张图片+一张主页面图片;使得开机动画结束后能够之间进入
for (int i = 0; i < 61; ++i)
{
sprintf(gif_path, "./gif/Frame%d.jpg", i); // 构造jpg图片的路径
read_JPEG_file(gif_path, 0, 0, lcd_mp); // 在LCD上显示
/*FPS = 60HZ
usleep的单位是μs微秒,且1s(秒) = 1000 ms(毫秒) , 1ms(毫秒) = 1000μs(微秒)
1HZ = 1000 / 1000μs*/
usleep(1000 * 16);
}
// 4.死循环:在用户点退出之前无需退出操作界面
while (1)
{
// 5.分析读取的设备信息 (type + code + value)
// 获取信息
read(ts_fd, &ts_event, sizeof(ts_event));
// 分析读取的设备信息 (type + code + value)
if (ts_event.type == EV_ABS) // 说明是触摸屏
{
if (ts_event.code == ABS_X) // 说明是X轴
{
cnt++;
x = ts_event.value * 800 / 1024;
}
if (ts_event.code == ABS_Y) // 说明是Y轴
{
cnt++;
y = ts_event.value * 480 / 600;
}
if (cnt >= 2)
{
printf("x = %d\t", x); // 输出X轴坐标
printf("y = %d\n", y); // 输出Y轴坐标
cnt = 0;
}
}
// 6.实时判断读取到的触摸屏坐标,施行相应的功能
// 登录界面
if (x >= 336 && x <= 450 && y >= 396 && y <= 465)
{
read_JPEG_file("./pic/login.jpg", 0, 0, lcd_mp);
x = 0;
y = 0;
}
// 返回主界面
else if ((x >= 646 && x <= 773) && (y >= 33 && y <= 120))
{
cnt = 0;
read_JPEG_file("./gif/Frame60.jpg", 0, 0, lcd_mp);
x = 0;
y = 0;
}
// 退出
else if ((x >= 628 && x <= 733) && (y >= 397 && y <= 454))
{
read_JPEG_file("./pic/close.jpg", 0, 0);
break;
}
}
return 0;
}
优化思考
问题一:观察界面切换效果,可明显观察到界面切换时有明显的刷新效果,有点影响使用效果
分析:
初步分析是因为JPEG解码后,是通过一行一行像素点写入LCD映射空间的,故而导致可以看到明显的从上到下的切换效果。
优化方向:
- 优化JPEG解码步骤的循环:使得图片可以更快更好的写入进LCD映射空间内,但是这个方向实现起来较为麻烦。
- 将界面切换过程均换成动态图:经过观察开机动画,发现过程中完全看不到刷新效果,可以达到丝滑切换图片的效果,所以初步设想是因为图片切换速度超过人眼观察速度,所以我认为这个方向可以实现。且考虑到切换界面动画的框架可以保持一致,这样省去了大量的准备步骤,还能够获得更好的效果,准备在项目内试验。
问题二:图片的按键位置不能相近或者重合,否则有误触导致执行了别的功能
分析:
这是因为当前程序的架构,是将读取屏幕触屏参数与条件判断直接放在一起循环导致的。read()函数并不会像scanf()函数那样有等待的过程,故而每一次获取参数后会立马进行条件判断。即便界面完成切换,但实际上一个界面按键的触屏位置参数依然生效。
优化方向:
- 对程序进行模块化编程:这样可以将各个界面的读取参数与判断间隔开,降低各个界面之间的耦合性。即在这个界面函数结束前,只会进行该界面的按键位置条件判断,从而消除误触执行其他功能的可能性。
问题三:当快速来回点击触摸屏两个位置时,会出现点击位置坐标读取与实际触摸坐标不一致的情况
分析:
初步分析是因为读取触摸坐标的机制导致的,但是目前没有办法证实,期待后续的学习。
优化方向:
-
满足一次条件判断后,将存储获取参数的变量清空:经过实测发现,只要在满足条件判断后,将变量内参数情况,再进行读取赋值操作,便可以丝滑进行界面切换,不会出现读取坐标与实际触摸坐标不同的情况。
同样,原理未知,期待后续学习补充。