LCD屏显示练习【二】

题目

设计一个程序,该程序在运行之后自动播放一段开机动画,开机动画结束后可以调转到登录界面,登录界面有2个按钮,分别是登录和退出,点击登录之后可以显示系统主界面。主界面自拟,要求主界面有一个返回按钮,点击返回按钮可以回到登录界面。要求:不可以使用 goto 语句。

题目分析

该题目的主要诉求可总结为:

  1. 开机时需要有一段开机动画,且在开机动画结束后可以之间到达操作主界面
  2. 主界面上会有两个按钮,即切换界面和退出
  3. 界面切换不能使用goto语句

思路解析

  1. 开机动画可以使用裁剪工具GIFtiqu将动态图裁剪为一张张jpeg图片,在将jpeg图片解码循环显示,且可以将登录界面图片放至循环的最后一张。
  2. 触屏按键切换,该功能涉及到读取LCD屏的触摸屏设备信息。需要创建对应格式的结构体变量,并利用read()函数将设备文件中的信息存储进创建的结构体变量中。
  3. 由于读取触摸屏参数不能只读一次,所以采取死循环作为循环,并设置退出键坐标为退出循环或者退出整个程序。

知识点涉及

  1. 开机动画图片循环时,需要使用usleep()控制循环间隙。该函数的单位为微秒,且1s(秒) = 1000ms(毫秒),1ms(毫秒) = 1000μs(微秒)。经过计算,使得图片循环能够满足人眼视觉残留条件,最终达到图片“动”起来的效果。
  2. 由于开机动画使用的是JPEG图片,所以还涉及到JPEG的解码步骤。
  3. 触摸屏的设备信息在linux系统下也是一个文件,所以我们可以通过read()将这些信息读取到特定结构的结构体变量中,再来对获取到的参数做相应的操作。

代码展示

/*******************************************************************
 *
 *	file name:	main.c
 *	author	 :  [email protected]
 *	date	 :  2024/05/14
 *	function :  该案例是掌握LCD屏触摸原理以及显示图片切换过程
 * 	note	 :  None
 *
 *	CopyRight (c)  2023-2024   [email protected]   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	 :  [email protected]
*	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映射空间的,故而导致可以看到明显的从上到下的切换效果。

优化方向:

  1. 优化JPEG解码步骤的循环:使得图片可以更快更好的写入进LCD映射空间内,但是这个方向实现起来较为麻烦。
  2. 将界面切换过程均换成动态图:经过观察开机动画,发现过程中完全看不到刷新效果,可以达到丝滑切换图片的效果,所以初步设想是因为图片切换速度超过人眼观察速度,所以我认为这个方向可以实现。且考虑到切换界面动画的框架可以保持一致,这样省去了大量的准备步骤,还能够获得更好的效果,准备在项目内试验。

问题二:图片的按键位置不能相近或者重合,否则有误触导致执行了别的功能

分析:

​ 这是因为当前程序的架构,是将读取屏幕触屏参数与条件判断直接放在一起循环导致的。read()函数并不会像scanf()函数那样有等待的过程,故而每一次获取参数后会立马进行条件判断。即便界面完成切换,但实际上一个界面按键的触屏位置参数依然生效。

优化方向:

  1. 对程序进行模块化编程:这样可以将各个界面的读取参数与判断间隔开,降低各个界面之间的耦合性。即在这个界面函数结束前,只会进行该界面的按键位置条件判断,从而消除误触执行其他功能的可能性。

问题三:当快速来回点击触摸屏两个位置时,会出现点击位置坐标读取与实际触摸坐标不一致的情况

分析:

​ 初步分析是因为读取触摸坐标的机制导致的,但是目前没有办法证实,期待后续的学习。

优化方向:

  1. 满足一次条件判断后,将存储获取参数的变量清空:经过实测发现,只要在满足条件判断后,将变量内参数情况,再进行读取赋值操作,便可以丝滑进行界面切换,不会出现读取坐标与实际触摸坐标不同的情况。

    同样,原理未知,期待后续学习补充。

热门相关:初恋在台北   给高分的女教授   护士娜丽   龙在少林   假面疑云