基于RK3568的多功能触控应用系统开发实践

前言

Github仓库地址:https://github.com/Amireux0013/RK3568_dmeo

随着嵌入式系统在各行各业的广泛应用,基于ARM架构的高性能开发板成为了嵌入式开发的主流平台。本文将详细介绍一个基于RK3568开发板的多功能触控应用系统的设计与实现过程,该系统集成了写字板、图片播放器和音乐播放器三大功能模块,通过触摸屏实现人机交互。通过本项目的开发实践,展示了在嵌入式Linux系统开发、设备驱动操作、多进程管理、图形界面设计等方面的专业技能。

一、系统架构设计

1.1 硬件平台

RK3568是瑞芯微推出的一款高性能ARM Cortex-A55四核处理器,主频高达2.0GHz,集成了Mali-G52 GPU,支持4K视频编解码,具备丰富的外设接口。本项目使用的开发板配置如下:

  • 处理器:RK3568 (四核Cortex-A55,主频2.0GHz)
  • 内存:2GB LPDDR4
  • 存储:16GB eMMC + microSD卡扩展
  • 显示:7英寸1024×600电容触摸屏
  • 音频:内置音频编解码器,支持扬声器输出
  • 接口:USB、HDMI、以太网、GPIO等

1.2 软件架构

系统采用分层设计思想,自底向上分为以下几层:

  1. 操作系统层:基于Linux内核定制,提供设备驱动和系统服务
  2. 设备驱动层:封装LCD显示屏(/dev/fb0)和触摸屏(/dev/input/event2)的底层操作
  3. 基础功能层:实现图形绘制、BMP图片解析、触摸事件处理等基础功能
  4. 应用功能层:实现写字板、图片播放器和音乐播放器三个功能模块
  5. 用户界面层:提供图形化菜单和交互界面

系统采用多进程架构,主进程负责菜单显示和功能切换,各功能模块在独立的子进程中运行,提高了系统的稳定性和模块化程度。

二、底层驱动开发

2.1 LCD驱动操作

在嵌入式Linux系统中,LCD显示通常通过帧缓冲设备来实现。本项目直接操作/dev/fb0设备文件,实现对显示屏的控制。以下是LCD初始化和操作的核心代码:

/**
 * @brief 打开LCD屏幕驱动
 * @return 成功返回文件描述符, 失败返回-1
 */
int lcd_open(void)
{
    int lcd_fd = open("/dev/fb0", O_RDWR);
    if (lcd_fd == -1)
    {
        perror("open /dev/fb0 failed");
        return -1;
    }
    return lcd_fd;
}

通过对帧缓冲区的直接读写,实现了高效的图形显示。在RK3568平台上,LCD分辨率为1024×600,每个像素占用4字节(ARGB格式),因此帧缓冲区大小为1024×600×4字节。

2.2 触摸屏驱动操作

触摸屏通过Linux输入子系统提供的事件接口(/dev/input/event2)进行操作。系统通过读取输入事件,获取触摸坐标和触摸状态:

/**
 * @brief 打开触摸屏驱动
 * @return 成功返回文件描述符, 失败返回-1
 */
int ts_open(void)
{
    int ts_fd = open("/dev/input/event2", O_RDWR);
    if (ts_fd == -1)
    {
        perror("open /dev/input/event2 failed");
        return -1;
    }
    return ts_fd;
}

/**
 * @brief 获取触摸屏坐标 
 * @param ts_fd 触摸屏文件描述符
 * @param x     用于存储x坐标的指针
 * @param y     用于存储y坐标的指针
 */
void get_xy(int ts_fd, int *x, int *y)
{
    struct input_event ts_buf;
    int got_x = 0, got_y = 0;
    while (1)
    {
        bzero(&ts_buf, sizeof(ts_buf));
        read(ts_fd, &ts_buf, sizeof(ts_buf));
        if (ts_buf.type == EV_ABS && ts_buf.code == ABS_X)
        {
            *x = ts_buf.value;
            got_x = 1;
        }
        if (ts_buf.type == EV_ABS && ts_buf.code == ABS_Y)
        {
            *y = ts_buf.value;
            got_y = 1;
        }
        if (ts_buf.type == EV_KEY && ts_buf.code == BTN_TOUCH && ts_buf.value == 0)
        {
            if (got_x && got_y) break;
        }
    }
}

这里实现了对Linux输入事件的精确解析,通过判断事件类型(type)、事件代码(code)和事件值(value),准确获取触摸坐标和触摸状态。

三、图形显示技术

3.1 BMP图片解析与显示

在嵌入式系统中,图片显示是一个常见需求。本项目实现了BMP格式图片的解析和显示功能,直接操作像素数据,无需依赖第三方图形库,提高了系统效率:

/**
 * @brief 在指定位置显示BMP图片 
 * @param bmp_path 图片路径
 * @param x0, y0   显示的起始坐标
 * @param lcd_fd   LCD文件描述符
 * @return 成功返回0, 失败返回-1
 */
int show_bmp(const char *bmp_path, int x0, int y0, int lcd_fd)
{
    int bmp_fd = open(bmp_path, O_RDONLY);
    if (bmp_fd == -1)
    {
        printf("open bmp %s error\n", bmp_path);
        return -1;
    }

    unsigned char head[54] = {0};
    read(bmp_fd, head, 54);

    int w = *((int *)&head[18]);
    int h = *((int *)&head[22]);

    // 计算每行需要补齐的字节数
    int line_padding = (4 - (w * 3) % 4) % 4;
    int line_size = w * 3 + line_padding;
    
    // 读取BMP像素数据
    unsigned char *bmp_data = malloc(line_size * h);
    if (bmp_data == NULL) {
        printf("malloc for bmp_data failed\n");
        close(bmp_fd);
        return -1;
    }
    lseek(bmp_fd, 54, SEEK_SET);
    read(bmp_fd, bmp_data, line_size * h);
    close(bmp_fd);

    // 转换为LCD像素格式
    int *lcd_buf = malloc(w * h * 4); 
    if(lcd_buf == NULL) {
        printf("malloc for lcd_buf failed\n");
        free(bmp_data);
        return -1;
    }
    for (int y = 0; y < h; y++)
    {
        for (int x = 0; x < w; x++)
        {
            unsigned char b = bmp_data[y * line_size + x * 3];
            unsigned char g = bmp_data[y * line_size + x * 3 + 1];
            unsigned char r = bmp_data[y * line_size + x * 3 + 2];
            lcd_buf[y * w + x] = (0x00 << 24) | (r << 16) | (g << 8) | b;
        }
    }
    free(bmp_data);
    
    // 写入LCD帧缓冲区
    lseek(lcd_fd, 0, SEEK_SET);
    for (int y = 0; y < h; y++)
    {
        if ((y0 + y) >= 600) continue; 
        for (int x = 0; x < w; x++)
        {
            if ((x0 + x) >= 1024) continue;
            lseek(lcd_fd, (1024 * (y0 + y) + (x0 + x)) * 4, SEEK_SET);
            write(lcd_fd, &lcd_buf[(h - 1 - y) * w + x], 4);
        }
    }
    
    free(lcd_buf);
    lseek(lcd_fd, 0, SEEK_SET); 

    return 0;
}

这段代码展示了对BMP文件格式的深入理解,包括文件头解析、像素数据读取、颜色格式转换和显示位置计算等。特别注意的是,BMP图片像素数据是从下到上、从左到右存储的,而LCD显示是从上到下、从左到右,因此在显示时需要进行坐标转换。

3.2 基本图形绘制

除了显示BMP图片外,系统还实现了基本图形绘制功能,如点和矩形:

/**
 * @brief 绘制一个实心矩形
 */
void draw_rect(int lcd_fd, int x0, int y0, int w, int h, int color)
{
    int *rect_buf = malloc(w * h * 4);
    if (!rect_buf) return;
    
    for(int i = 0; i < w * h; i++) {
        rect_buf[i] = color;
    }

    for (int y = 0; y < h; y++) {
        if (y0 + y >= 600) break;
        lseek(lcd_fd, (1024 * (y0 + y) + x0) * 4, SEEK_SET);
        write(lcd_fd, &rect_buf[y * w], w * 4);
    }

    free(rect_buf);
}

// 绘制点函数
void draw_point(int lcd_fd, int x, int y, int d)
{
    int color = 0x00FF0000; // 红色
    for (int m = y - d; m <= y + d; m++)
    {
        for (int n = x - d; n < x + d; n++)
        {
            if (m >= 0 && m < 600 && n >= 0 && n < 1024)
            {
                lseek(lcd_fd, (1024 * m + n) * 4, SEEK_SET);
                write(lcd_fd, &color, 4);
            }
        }
    }
}

这些函数通过直接操作帧缓冲区,实现了高效的图形绘制。特别是在绘制矩形时,采用了一次性生成像素缓冲区,然后批量写入的方式,提高了绘制效率。

四、功能模块实现

4.1 写字板模块

写字板模块实现了一个简单的触摸绘图功能,用户可以在屏幕上用手指绘制图形:

void run_drawing_pad(int lcd_fd, int ts_fd){
    printf("启动 [写字板] 功能...\n");

    // 初始化白色背景
    int white_buf[1024 * 600];
    for (int i = 0; i < 1024 * 600; i++)
    {
        white_buf[i] = 0X00FFFFFF;
    }
    lseek(lcd_fd, 0, SEEK_SET);
    write(lcd_fd, white_buf, sizeof(white_buf));

    // 在右上角画一个退出按钮
    int gray_color = 0x00808080;
    for (int y = 0; y < 40; y++) {
        for (int x = 984; x < 1024; x++) {
            white_buf[1024 * y + x] = gray_color;
        }
    }
    lseek(lcd_fd, 0, SEEK_SET);
    write(lcd_fd, white_buf, sizeof(white_buf));
    
    flush_ts_events(ts_fd);

    // 处理触摸事件
    struct input_event ts_buf;
    int x = -1, y = -1;
    int is_touching = 0; 

    while (1)
    {
        read(ts_fd, &ts_buf, sizeof(ts_buf));
        switch(ts_buf.type)
        {
            case EV_ABS: 
                if (ts_buf.code == ABS_X) {
                    x = ts_buf.value;
                } else if (ts_buf.code == ABS_Y) {
                    y = ts_buf.value;
                }
                break;
            
            case EV_KEY: 
                if (ts_buf.code == BTN_TOUCH) {
                    if (ts_buf.value == 1) { 
                        is_touching = 1;
                    } else if (ts_buf.value == 0) { 
                        is_touching = 0;
                        // 检查是否点击了退出区域
                        if (x > 984 && y < 40) {
                            return;
                        }
                    }
                }
                break;

            case EV_SYN: 
                if (ts_buf.code == SYN_REPORT) {
                    if (is_touching) {
                        if (x >= 0 && y >= 0) {
                            draw_point(lcd_fd, x, y, 5);
                        }
                    }
                }
                break;
        }
    }
}

这段代码展示了对Linux输入事件的精确处理,通过区分不同类型的事件(EV_ABS、EV_KEY、EV_SYN),实现了触摸绘图功能。特别是对SYN_REPORT事件的处理,确保了绘图的实时性和流畅性。

4.2 图片播放器模块

图片播放器模块实现了自动循环播放BMP图片的功能:

void run_slideshow(int lcd_fd, int ts_fd)
{
    printf("启动 [循环切换图片] 功能...\n");
    char bmp_path[5][15] = {"1.bmp", "dog.bmp", "small.bmp", "school1.bmp", "school2.bmp"};
    int i = 0;

    fd_set rdfs;
    struct timeval tv;
    
    flush_ts_events(ts_fd);

    while (1)
    {
        show_bmp(bmp_path[i], 0, 0, lcd_fd);
        i = (i + 1) % 5;

        tv.tv_sec = 1;
        tv.tv_usec = 0;
        
        FD_ZERO(&rdfs);
        FD_SET(ts_fd, &rdfs);

        int ret = select(ts_fd + 1, &rdfs, NULL, NULL, &tv);
        if (ret > 0 && FD_ISSET(ts_fd, &rdfs))
        {
            printf("检测到触摸,退出图片循环。\n");
            break; 
        }
    }
}

这段代码展示了对select系统调用的灵活应用,实现了定时切换图片和触摸检测的双重功能。通过设置超时时间为1秒,系统每秒自动切换一次图片,同时监听触摸事件,一旦检测到触摸,立即退出播放模式。

4.3 音乐播放器模块

音乐播放器模块是系统中最复杂的部分,它通过调用外部程序mplayer实现MP3音乐播放,并通过命名管道(FIFO)实现与mplayer的通信:

void run_music_player(int lcd_fd, int ts_fd)
{
    printf("启动 [音乐播放器] 功能...\n");
    show_bmp("./music_pic.bmp", 0, 0, lcd_fd);
    
    unlink("pipe"); 
    if (mkfifo("pipe", 0666) == -1) {
        perror("mkfifo failed");
        return;
    }

    int music_vol = 100;
    int x, y;
    pid_t mplayer_pid = -1; 

    flush_ts_events(ts_fd); 

    while (1)
    {
        mplayer_pid = fork();
        if (mplayer_pid == 0) 
        {
            printf("子进程(mplayer)启动中, PID: %d\n", getpid());
            execlp("mplayer", "mplayer", "-slave", "-quiet", "-input", "file=./pipe", "-ao", "alsa:device=hw=0.0", music_name[music_num], NULL);
            perror("execlp mplayer failed");
            exit(1);
        }
        else if (mplayer_pid > 0) 
        {
            printf("父进程(控制)运行中, PID: %d, mplayer PID: %d\n", getpid(), mplayer_pid);
            usleep(500000); 
            int pipe_fd = open("./pipe", O_WRONLY);
            if (pipe_fd == -1)
            {
                perror("open pipe failed");
                kill(mplayer_pid, SIGKILL); 
                break;
            }

            int should_exit_player = 0;
            while (!should_exit_player)
            {
                get_xy(ts_fd, &x, &y);
                printf("触摸坐标: (%d, %d)\n", x, y);
                
                // 处理各种触摸控制
                // 暂停/播放
                if (x > 297 && x < 402 && y > 194 && y < 317)
                {
                    printf("指令: 暂停/播放\n");
                    send_pcm(pipe_fd, "pause\n");
                }
                // 其他控制代码...
            }
            close(pipe_fd);
            if (should_exit_player) break;
        }
    }
    unlink("pipe"); 
}

这段代码展示了对进程控制、进程间通信和外部程序调用的深入理解。通过fork()创建子进程运行mplayer,通过命名管道发送控制命令,实现了对音乐播放的精确控制。

五、多进程管理

系统采用多进程架构,主进程负责菜单显示和功能切换,各功能模块在独立的子进程中运行。这种设计提高了系统的稳定性和模块化程度:

int main()
{
    int lcd_fd = lcd_open();
    if (lcd_fd < 0) return -1;

    int ts_fd = ts_open();
    if (ts_fd < 0) {
        close(lcd_fd);
        return -1;
    }

    while (1)
    {
        draw_main_menu(lcd_fd);

        int x, y;
        get_xy(ts_fd, &x, &y);
        printf("主菜单触摸坐标: (%d, %d)\n", x, y);

        pid_t pid = -1;
        
        // 写字板功能
        if (x > 112 && x < 412 && y > 100 && y < 280) {
            pid = fork();
            if (pid == 0) { 
                run_drawing_pad(lcd_fd, ts_fd);
                exit(0); 
            }
        }
        // 图片播放器功能
        else if (x > 612 && x < 912 && y > 100 && y < 280) {
            pid = fork();
            if (pid == 0) { 
                run_slideshow(lcd_fd, ts_fd);
                exit(0); 
            }
        }
        // 音乐播放器功能
        else if (x > 112 && x < 412 && y > 320 && y < 500) {
            pid = fork();
            if (pid == 0) { 
                run_music_player(lcd_fd, ts_fd);
                exit(0); 
            }
        }
        // 退出程序
        else if (x > 612 && x < 912 && y > 320 && y < 500) {
            printf("程序退出。\n");
            break; 
        }

        // 等待子进程退出
        if (pid > 0) {
            wait(NULL); 
            printf("子进程已退出, 返回主菜单。\n");
        } else if (pid == -1 && (x > 112 && x < 912)) { 
             perror("fork failed");
        }
    }

    // 清屏并关闭设备
    draw_rect(lcd_fd, 0, 0, 1024, 600, 0x00000000);
    close(lcd_fd);
    close(ts_fd);

    return 0;
}

这段代码展示了对多进程编程的深入理解,通过fork()创建子进程,通过wait()等待子进程退出,实现了功能模块的独立运行和资源回收。

六、技术难点与解决方案

6.1 帧缓冲区操作优化

在嵌入式系统中,显示性能是一个关键指标。本项目通过以下方式优化了帧缓冲区操作:

  1. 批量写入:在绘制矩形等大面积图形时,先在内存中生成像素缓冲区,然后一次性写入帧缓冲区,减少了I/O操作次数。
  2. 局部更新:在更新显示内容时,只更新变化的部分,而不是整个屏幕,减少了数据传输量。
  3. 内存映射:虽然本项目使用了read/write系统调用操作帧缓冲区,但在实际产品中,可以使用mmap将帧缓冲区映射到进程地址空间,进一步提高性能。

6.2 触摸事件处理

触摸事件处理是交互系统的核心。本项目实现了完整的触摸事件处理流程:

  1. 事件解析:准确解析Linux输入事件,区分不同类型的事件(EV_ABS、EV_KEY、EV_SYN)。
  2. 坐标转换:将触摸屏坐标系转换为LCD显示坐标系。
  3. 事件缓冲区清空:在功能切换时,清空触摸事件缓冲区,避免残留事件影响新功能的使用。
  4. 非阻塞模式:在需要的场景下,使用非阻塞模式读取触摸事件,提高系统响应性。

6.3 进程间通信

在音乐播放器模块中,需要与外部程序mplayer进行通信。本项目使用命名管道(FIFO)实现了进程间通信:

  1. 创建命名管道:使用mkfifo创建命名管道。
  2. 发送控制命令:通过向命名管道写入文本命令,控制mplayer的行为。
  3. 资源清理:在退出时,使用unlink删除命名管道文件,避免资源泄漏。

这种方式简单高效,适合嵌入式系统的资源限制。

七、项目亮点

7.1 无依赖设计

本项目直接操作底层设备驱动,不依赖第三方图形库和多媒体库(除了mplayer),具有以下优势:

  1. 系统资源占用低:适合资源受限的嵌入式系统。
  2. 启动速度快:无需加载大型库,启动时间短。
  3. 定制性强:可以根据具体需求定制功能,不受第三方库的限制。

7.2 模块化架构

系统采用模块化设计,各功能模块在独立的子进程中运行,具有以下优势:

  1. 稳定性高:一个模块的崩溃不会影响其他模块和主程序。
  2. 可扩展性强:可以方便地添加新功能模块,而不影响现有功能。
  3. 开发效率高:不同模块可以由不同开发者并行开发,提高团队效率。

7.3 资源管理

系统注重资源管理,避免内存泄漏和资源浪费:

  1. 内存分配与释放:每次malloc后都有对应的free,避免内存泄漏。
  2. 文件描述符管理:及时关闭不再使用的文件描述符,避免资源耗尽。
  3. 进程管理:使用wait等待子进程退出,避免僵尸进程。
  4. 信号处理:在需要的场景下,使用kill发送信号终止子进程。

八、总结

本项目展示了在嵌入式Linux系统上开发多功能触控应用的完整过程,涵盖了设备驱动操作、图形显示、触摸控制、多进程管理等多个技术领域。通过直接操作底层设备驱动,实现了高效的图形显示和触摸控制;通过多进程架构,实现了功能模块的独立运行和稳定性保障;通过命名管道等IPC机制,实现了与外部程序的通信和控制。

这些技术在嵌入式系统开发中具有广泛的应用价值,可以用于工业控制、智能家居、医疗设备等多个领域。通过本项目的开发实践,不仅掌握了嵌入式Linux系统开发的核心技能,还积累了丰富的实战经验,为未来更复杂的嵌入式系统开发奠定了坚实基础。

作者信息

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇