正点原子 I.MX6ULL MINI 开发板学习笔记:LCD 显示屏与 Framebuffer

分享
Framebuffer 数据流

这篇笔记继续正点原子 I.MX6ULL MINI 开发板裸机学习,主题是LCD 显示屏与 Framebuffer。代码来自实际工程 /home/zhao/code/2026/touch_screen,实现 800x480 RGB LCD 的驱动。

1. 完成什么实验?

驱动 800x480 分辨率的 RGB LCD,配置 I.MX6ULL 的 LCDIF 控制器,通过 Framebuffer 实现画点、清屏、显示字符等基本绘图功能。

2. 什么是 LCDIF?

LCDIF(LCD Interface)是 I.MX6ULL 内置的显示控制器,负责把 Framebuffer 中的像素数据按照 RGB 时序发送给 LCD 屏幕。

3. RGB 接口有哪些信号线?

主要包括:DATA[23:0] 颜色数据总线、CLK 像素时钟、HSYNC 行同步、VSYNC 场同步、ENABLE 数据使能(DE)。

RGB 时序示意图

4. 什么是 Framebuffer?

Framebuffer 是一块连续的内存区域,每个像素对应一个颜色值。LCDIF 会自动把这块内存的数据刷到屏幕上,CPU 只需要修改内存里的像素值就能改变显示内容。

5. 屏幕时序参数有哪些?

代码中配置的 800x480 屏幕时序包括:HSPW=48(行同步脉宽)、HBP=88(行后肩)、HFP=40(行前肩);VSPW=3(场同步脉宽)、VBP=32(场后肩)、VFP=13(场前肩)。有效显示区为 800x480。

6. 像素格式是什么?

代码中每个像素占 4 字节,使用 0x00RRGGBB 格式,即高字节补 0,实际为 24 位 RGB 颜色。

32 位像素格式

7. 真实可用的 LCD 代码

下面是工程中验证过的 lcd.c 核心代码。

void init_lcd_io(void)
{
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0);
    /* DATA02 ~ DATA22 省略,同上 */
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0);

    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0xB9);
    /* 省略 DATA01~DATA22 */
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_CLK_LCDIF_CLK, 0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0xB9);
}

void init_lcd_clk(void)
{
    unsigned int t;
    CCM_ANALOG->PLL_VIDEO_NUM = 0;
    CCM_ANALOG->PLL_VIDEO_DENOM = 0;
    t = CCM_ANALOG->PLL_VIDEO;
    t &= ~(3 << 19);
    t |= (2 << 19);
    t |= (1 << 13);
    t &= ~(0x7F << 0);
    t |= (42 << 0);
    CCM_ANALOG->PLL_VIDEO = t;
    while((CCM_ANALOG->PLL_VIDEO & (1 << 31)) == 0);  /* 等待 PLL 锁定:bit31 为 LOCK,1=已锁定 */

    CCM->CSCDR2 |= (2 << 15) | (3 << 12);
    CCM_ANALOG->MISC2 &= ~(3 << 30);
    CCM->CBCMR |= (7 << 23);
    CCM->CSCDR2 &= ~(7 << 9);
}
struct tftlcd_t
{
    unsigned int width;
    unsigned int height;
    unsigned int pix_size;
    unsigned int hspw;
    unsigned int hbp;
    unsigned int hfp;
    unsigned int vspw;
    unsigned int vbp;
    unsigned int vfp;
    unsigned int framebuffer;
    unsigned int fore_color;
    unsigned int back_color;
};

struct tftlcd_t lcd_dev;

void init_lcd(void)
{
    init_lcd_io();
    reset_lcd();
    init_lcd_clk();
    delay_ms(500);

    lcd_dev.width = 800;
    lcd_dev.height = 480;
    lcd_dev.pix_size = 4;
    lcd_dev.hspw = 48;
    lcd_dev.hbp = 88;
    lcd_dev.hfp = 40;
    lcd_dev.vspw = 3;
    lcd_dev.vbp = 32;
    lcd_dev.vfp = 13;
    lcd_dev.framebuffer = FRAME_BUFFER_ADDRESS;
    lcd_dev.fore_color = 0x00FF0000;
    lcd_dev.back_color = 0x00FFFFFF;

    LCDIF->CTRL |= (1 << 19) | (1 << 17) | (3 << 10) | (3 << 8) | (1 << 5);
    LCDIF->CTRL1 &= ~(0x0F << 16);
    LCDIF->CTRL1 |= (7 << 16);
    LCDIF->TRANSFER_COUNT = (lcd_dev.height << 16) | lcd_dev.width;
    LCDIF->CUR_BUF = lcd_dev.framebuffer;
    LCDIF->NEXT_BUF = lcd_dev.framebuffer;

    LCDIF->VDCTRL0 = (1 << 28) | (1 << 24) | (1 << 21) | (1 << 20) | lcd_dev.vspw;
    LCDIF->VDCTRL1 = lcd_dev.height + lcd_dev.vspw + lcd_dev.vbp + lcd_dev.vfp;
    LCDIF->VDCTRL2 = (lcd_dev.hspw << 18) | (lcd_dev.hspw + lcd_dev.hbp + lcd_dev.hfp + lcd_dev.width);
    LCDIF->VDCTRL3 = ((lcd_dev.hspw + lcd_dev.hbp) << 16) | (lcd_dev.vspw + lcd_dev.vbp);
    LCDIF->VDCTRL4 = (1 << 18) | (lcd_dev.width);
    LCDIF->CTRL |= (1 << 0);

    delay_ms(20);
    printf("lcd init OK\n");
    lcd_clear(lcd_dev.back_color);
}
void lcd_clear(unsigned int color)
{
    unsigned int (*p)[lcd_dev.width] = (unsigned int (*)[lcd_dev.width])lcd_dev.framebuffer;
    int i, j;
    for(i = 0;i < lcd_dev.height;++i)
    {
        for(j = 0;j < lcd_dev.width;++j)
        {
            p[i][j] = color;
        }
    }
}

void lcd_drawpoint(int x, int y, unsigned int color)
{
    unsigned int *p = (unsigned int *)(lcd_dev.framebuffer);
    if(x >= lcd_dev.width || y >= lcd_dev.height)
    {
        return;
    }
    *(p + lcd_dev.width * y + x) = color;
}

下面是 framebuffer.c 中的字符串显示函数示例。

void lcd_fill(unsigned short x0, unsigned short y0,
              unsigned short x1, unsigned short y1, unsigned int color)
{
    unsigned short x, y;
    if(x0 < 0) x0 = 0;
    if(y0 < 0) y0 = 0;
    if(x1 >= lcd_dev.width) x1 = lcd_dev.width - 1;
    if(y1 >= lcd_dev.height) y1 = lcd_dev.height - 1;

    for(y = y0; y <= y1; y++)
    {
        for(x = x0; x <= x1; x++)
            lcd_drawpoint(x, y, color);
    }
}

void lcd_show_string(unsigned short x, unsigned short y,
                     unsigned short width, unsigned short height,
                     unsigned char size, const char *p)
{
    unsigned char x0 = x;
    width += x;
    height += y;
    while((*p <= '~') && (*p >= ' '))
    {
        if(x >= width) { x = x0; y += size; }
        if(y >= height) break;
        lcd_showchar(x, y, *p, size, 0);
        x += size / 2;
        p++;
    }
}

8. 关键点说明

  • 引脚复用:LCD_DATA00~23、CLK、HSYNC、VSYNC、ENABLE 全部复用为 LCDIF 功能。
  • 像素时钟:由 PLL_VIDEO 和 CSCDR2 分频得到,代码中按屏幕规格配置。
  • PLL 锁定等待:PLL_VIDEO 的 bit31 是 LOCK(1=已锁定),等待循环应写成 == 0(未锁定就继续等)。工程原代码误写为 != 0,只是 PLL 锁定很快碰巧没出问题,上文已修正。
  • Framebuffer 地址:FRAME_BUFFER_ADDRESS 定义为 0x89000000。
  • 像素格式:每个像素 4 字节,实际颜色为 0x00RRGGBB。
  • 时序参数:HSPW/HBP/HFP、VSPW/VBP/VFP 必须与 LCD 屏手册匹配,否则花屏。
  • 自动刷新:配置 CUR_BUF 和 NEXT_BUF 后,LCDIF 自动循环输出。

9. 实验现象

程序运行后,屏幕先被清成白色背景,随后可以在屏幕上画点、画矩形、显示字符串。例如调用 lcd_fill(100, 100, 300, 200, 0x00FF0000) 会在屏幕左上角画一个红色矩形。

总结

问题答案
完成什么实验?驱动 800x480 RGB LCD,配置 LCDIF,通过 Framebuffer 绘图
什么是 LCDIF?I.MX6ULL 内置 LCD 接口控制器
RGB 信号线?DATA[23:0]、CLK、HSYNC、VSYNC、ENABLE
什么是 Framebuffer?一块内存,像素数据存在这里,LCDIF 自动刷到屏幕
时序参数?HSPW=48, HBP=88, HFP=40, VSPW=3, VBP=32, VFP=13
像素格式?32 位 ARGB,实际颜色 0x00RRGGBB

LCD 显示为后续人机交互打下了基础。下一篇进入触摸输入:GT9147 电容触摸屏驱动。

继续阅读

嵌入式开发面试知识手册

嵌入式开发工程师面试知识手册 适用对象:零基础起步、目标是通过嵌入式软件/驱动/BSP 方向面试的读者 编写依据:赵哲简历全部知识点 + 智能车 AGV 控制平台完整项目代码(i.MX6ULL Linux 端 + STM32F407 固件)+ Linux 驱动练习代码包 + embedded-agent 嵌入式开发智能体仓库 + 真实面试手撕代码与追问记录 使用方法:每篇按"通俗解释 → 原理细节 → 命令/API → 代码实例 → 项目结合 → 记忆口诀 → 面试问答"组织;口诀用于快速记忆,问答用于自测。第9篇是讲项目的演练场,第10篇是手撕代码实战题库。 总目录 * 第1篇 C语言与数据结构 * 1.1 编译链接全过程 * 1.2 数据类型与内存

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:Linux 驱动架构详解——编译、树内树外与运行时效果

这是 Linux 驱动小系列的总结篇。前三篇把 LED 驱动从内核模块写到 misc 设备,属于“会用”;这篇把背后的架构讲透:驱动到底怎么编译、内核树内和树外两种方式的区别、以及驱动里每个操作在系统跑起来之后到底产生了什么效果。内容整理自我的学习笔记(note.pdf)和实际工程 code/2026/20260803。 1. 驱动代码到底怎么编译的? 不管哪种方式,最终都靠内核的 kbuild 系统。核心就三个文件的关系: * Kconfig:定义 menuconfig 里的选项(如 CONFIG_MYMODULE),三态 y/m/n——y 编进内核,m 编成模块,n 不编; * .config:menuconfig 的选择结果,保存在源码顶层; * Makefile:

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:misc 设备——字符设备驱动的快捷方式

上两篇分别用手动 mknod 和 class_create+device_create 注册字符设备,调用链一长串。这篇用 misc(混杂)设备 一步搞定。代码来自实际工程 code/2026/20260803/05_misc_led,已验证可用。 1. 完成什么实验? 用 misc_register 重写 LED 驱动:一步完成设备号分配、字符设备注册、/dev/myled0 节点创建,同时保留上一篇的 sysfs 属性 led_status。 2. 什么是 misc 设备? misc(混杂)设备是 Linux 给“不好归类的小字符设备”

作者:azhezhezhe