ARM-lcd屏幕点亮

之前学习了I2C、AT24C02、LM75温度传感器等外设,今天完成IMX6ULL片上eLCDIF驱动RGB LCD屏幕,点亮4.7寸800*480屏幕,在屏幕上打点、画线,实时显示LM75采集的温度。

本次是RGB并行同步LCD裸机驱动,不使用Linux内核驱动,直接操作寄存器。

一、RGB LCD基础原理

1. 屏幕基础参数

本次使用4.7寸LCD,分辨率 800×480

像素格式:ARGB8888,每个像素4字节,R/G/B/A各8bit,范围0~255;

色域还有RGB565(2字节),占用内存更小,本次实验用32bit色;

刷新率:由PCLK像素时钟 + 前后廊时序共同决定。

2. 硬件连接:eLCDIF与LCD

IMX6ULL内置eLCDIF(LCD控制器),连接LCD的信号分为两大类:

  1. 24根并行数据线 LCD_DATA0~LCD_DATA23:R8bit + G8bit + B8bit,传输像素颜色;
  2. 4根控制信号线
    PCLK:像素时钟,每来1个PCLK,传输1个像素;
    DE:数据使能,高电平代表当前数据有效;
    HSYNC:行同步信号,一行扫描结束,触发行同步;
    VSYNC:场同步信号,一整帧画面扫描完成,触发场同步。
    数据通过DMA,直接从内存帧缓存区搬运到eLCDIF,不需要CPU逐个搬运像素,减轻CPU负担。帧缓存地址我们设_FRAME_RAM_ADDR_。

3. 行场时序(H/V 同步 + 前后廊)

这是RGB LCD最核心的知识点,一张完整图像扫描不是直接800×480,每一行、每一帧都包含同步脉冲 + 前廊(FP) + 有效像素 + 后廊(BP)

行时序 HSYNC

HSPW:行同步脉冲宽度;

HFP:行前廊,行同步结束到有效像素开始的间隔;

HBP:行后廊,有效像素结束到下一次行同步。

一行总PCLK = HSPW + HFP + 800(有效宽度) + HBP

场时序 VSYNC

VSPW:场同步脉冲宽度;

VFP:场前廊;

VBP:场后廊。

一帧总行数 = VSPW + VFP + 480(有效高度) + VBP

只有时序参数和屏幕手册严格匹配,屏幕才能正常点亮,时序不对会出现花屏、黑屏、闪烁。

4. 实验整体流程

  1. LCD引脚IOMUX配置:复用为eLCDIF功能,配置PAD电气属性;
  2. 配置LCD像素时钟PCLK(PLL5视频PLL,分频得到目标PCLK);
  3. LCDIF控制器软件复位;
  4. 填充时序参数(HSPW/HFP/HBP、VSPW/VFP/VBP、分辨率);
  5. 设置帧缓存地址,告诉eLCDIF从内存哪个地址读取像素;
  6. 使能eLCDIF,开始输出图像;
  7. 封装底层API:打点、清屏、画水平线;
  8. 应用层:读取LM75温度,LCD屏幕实时刷新温度字符串。

二、完整源码解析

lcd.h 对应的 lcd.c 完整代码

c 复制代码
#include "lcd.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "gpt.h"
#include "stdio.h"

void lcd_pad_init(void)
{
//复用功能,全部引脚设置为LCDIF功能
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA02_LCDIF_DATA02, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA03_LCDIF_DATA03, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA04_LCDIF_DATA04, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA05_LCDIF_DATA05, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA06_LCDIF_DATA06, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA07_LCDIF_DATA07, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA08_LCDIF_DATA08, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA09_LCDIF_DATA09, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA10_LCDIF_DATA10, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA11_LCDIF_DATA11, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA12_LCDIF_DATA12, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA13_LCDIF_DATA13, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA14_LCDIF_DATA14, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA15_LCDIF_DATA15, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA16_LCDIF_DATA16, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA17_LCDIF_DATA17, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA18_LCDIF_DATA18, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA19_LCDIF_DATA19, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA20_LCDIF_DATA20, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA21_LCDIF_DATA21, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA22_LCDIF_DATA22, 0);
	IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0);
    IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 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_GPIO1_IO08_GPIO1_IO08, 0);

//电气特性配置,所有PAD配置0xB9
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA02_LCDIF_DATA02, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA03_LCDIF_DATA03, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA04_LCDIF_DATA04, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA05_LCDIF_DATA05, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA06_LCDIF_DATA06, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_LCDIF_DATA07, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA08_LCDIF_DATA08, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA09_LCDIF_DATA09, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA10_LCDIF_DATA10, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA11_LCDIF_DATA11, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA12_LCDIF_DATA12, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA13_LCDIF_DATA13, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA14_LCDIF_DATA14, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_LCDIF_DATA15, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA16_LCDIF_DATA16, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA17_LCDIF_DATA17, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA18_LCDIF_DATA18, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA19_LCDIF_DATA19, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA20_LCDIF_DATA20, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA21_LCDIF_DATA21, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA22_LCDIF_DATA22, 0xB9);
	IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 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_GPIO1_IO08_GPIO1_IO08, 0xB9);

//背光引脚GPIO1_IO08设置为输出,高电平点亮背光
    GPIO1->GDIR |= (1 << 8);
    GPIO1->DR |= (1 << 8);
}

void lcd_clk_init(void)
{
//PLL5视频PLL配置,先进入旁路,断电
    CCM_ANALOG->PLL_VIDEO |= (1 << 16);  //BYPASS=1
    CCM_ANALOG->PLL_VIDEO |= (1 << 12);  //POWERDOWN=1

//PLL5计算公式:744MHZ = 24M *31
    CCM_ANALOG->PLL_VIDEO_NUM = 0;
    CCM_ANALOG->PLL_VIDEO_DENOM = 1;

unsigned int r = CCM_ANALOG->PLL_VIDEO;
    r &= ~(3 << 19);
    r |= (2 << 19);
    r &= ~(0x7F << 0);
    r |= (31 << 0);
    CCM_ANALOG->PLL_VIDEO = r;

//上电,等待PLL锁频
    CCM_ANALOG->PLL_VIDEO &= ~(1 << 12);
    CCM_ANALOG->PLL_VIDEO |= (1 << 13);
while ((CCM_ANALOG->PLL_VIDEO & (1 << 31)) == 0){
printf("Not locked \n");
    };
printf("Is locked \n");

//关闭旁路,使用PLL5输出
    CCM_ANALOG->PLL_VIDEO &= ~(1 << 16);  //BYPASS=0

//配置CCM时钟选择,PLL5作为LCDIF时钟源
    CCM_ANALOG->MISC2 &= ~(3 << 30);
    r = CCM->CSCDR2;
//CSCDR2[17:15] LCDIF1_PRE_CLK_SEL 010选择PLL5
    r &= ~(7 << 15);
    r |= (2 << 15);
//预分频PRED
    r &= ~(7 << 12);
    r |= (3 << 12);     
//CSCDR2[LCDIF1_CLK_SEL]
    r &= ~(7 << 9);
    CCM->CSCDR2 = r;

//后分频PODF
    CCM->CBCMR &= ~(7 << 23);
    CCM->CBCMR |= (5 << 23);

//打印时钟链路信息,调试查看分频
unsigned int cscdr2 = CCM->CSCDR2;
unsigned int cbcmr = CCM->CBCMR;
printf("PRE_CLK_SEL=%d, PRED=%d(div-%d), CLK_SEL=%d\n",
           (cscdr2 >> 15) & 7,
           (cscdr2 >> 12) & 7, ((cscdr2 >> 12) & 7) + 1,
           (cscdr2 >> 9) & 7);
printf("PODF=%d(div-%d)\n",
           (cbcmr >> 23) & 7, ((cbcmr >> 23) & 7) + 1);
}

void lcd_reset(void)
{
//LCDIF控制器软件复位
    LCDIF->CTRL |= (1 << 31);
    delay_ms(20);
    LCDIF->CTRL &= ~(1 << 31);
    LCDIF->CTRL &= ~(1 << 30);
}

struct tftlcd_t lcd_dev;
void lcd_init(void)
{
//第一步:引脚PAD初始化
    lcd_pad_init();
//第二步:配置LCD像素时钟PCLK
    lcd_clk_init();

//填充LCD设备结构体,屏幕时序参数
    lcd_dev.width = 800;
    lcd_dev.height = 480;
    lcd_dev.pix_size = 4;  //ARGB8888 4字节/像素
    lcd_dev.vspw = 3;
    lcd_dev.vfp = 13;
    lcd_dev.vbp = 32;
    lcd_dev.hspw = 48;
    lcd_dev.hfp = 40;
    lcd_dev.hbp = 88;
    lcd_dev.frame_addr = _FRAME_RAM_ADDR_; //帧缓存起始地址
    lcd_dev.fore_color = 0x00FF0000;
    lcd_dev.back_color = 0x00FFFFFF;

//复位LCDIF
    lcd_reset();
    delay_ms(50);

//配置eLCDIF控制寄存器
    LCDIF->CTRL |= (1 << 19) | (1 << 17) | (3 << 10) | (3 << 8) | (1 << 5);
    LCDIF->CTRL1 = (0x07 << 16);
    LCDIF->TRANSFER_COUNT = (lcd_dev.height << 16) | (lcd_dev.width << 0);
    LCDIF->CUR_BUF = lcd_dev.frame_addr;
    LCDIF->NEXT_BUF = lcd_dev.frame_addr;

//行场时序寄存器配置
    LCDIF->VDCTRL0 = (1 << 28) | (1 << 24) | (1 << 21) | (1 << 20) | (lcd_dev.vspw);
    LCDIF->VDCTRL1 = (lcd_dev.height + lcd_dev.vspw + lcd_dev.vfp + lcd_dev.vbp);
    LCDIF->VDCTRL2 = (lcd_dev.hspw << 18) | ((lcd_dev.width + lcd_dev.hspw + lcd_dev.hfp + lcd_dev.hbp) << 0);  
    LCDIF->VDCTRL3 = ((lcd_dev.hspw + lcd_dev.hbp) << 16) | ((lcd_dev.vspw + lcd_dev.vbp) << 0);
    LCDIF->VDCTRL4 = (1 << 18) | (lcd_dev.width << 0);

//使能eLCDIF,开始输出画面
    LCDIF->CTRL |= (1 << 0);
    delay_ms(50);
}

//清屏函数:遍历所有像素填充颜色
void screen_clear(unsigned int color)
{
int i = 0, j = 0;
for(i = 0; i < lcd_dev.height; i++)
    {
for (j = 0; j < lcd_dev.width; j++)
        {
            lcd_drawpoint(j, i, color);
        }
    }
}

//底层画点函数,向帧缓存对应位置写入颜色
void lcd_drawpoint(int x, int y, unsigned int color)
{
//边界检查,防止越界访问
if ((x >= lcd_dev.width || x < 0)  || (y >= lcd_dev.height || y < 0))
    {
return;
    }
unsigned int (* pF)[800] =  (unsigned int (*)[800])lcd_dev.frame_addr;
     pF[y][x] = color;
}

//画水平线API
void lcd_h_drawline(int x, int y, unsigned int length, unsigned int color)
{
int i = 0;
for (i = x; i <= x + length; i++)
    {
        lcd_drawpoint(i, y, color);
    }
}

main.c 主函数代码

c 复制代码
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "interrupt.h"
#include "clock.h"
#include "epit.h"
#include "gpt.h"
#include "uart.h"
#include "stdio.h"
#include "string.h"
#include "i2c.h"
#include "at24c02.h"
#include "lm75.h"
#include "lcd.h"
#include "framebuffer.h"

int main(void)
{
    system_interrupt_init();
    clock_init();
    led_init();
    beep_init();
    key_init();
//epit1_init();
    gpt1_init();
    uart1_init();
    at24c02_init();

float temp = 0;
char buffer[128] = {0}; 

    lcd_init();
    screen_clear(0x00FFFFFF); //清屏,填充白色背景

//lcd_h_drawline(100, 100, 200, 0x00FF0000);
//extern void lcd_show_string(unsigned short x,unsigned short y, unsigned short width, unsigned short height, unsigned char size,char *p);
//lcd_show_string(200,200, 300, 32, 32, "hello world!");

while(1)
    {
        temp =  get_temp(); //读取LM75温度
int m = 0, n = 0;
        m = (int)(temp * 1000) / 1000;
        n = (int)(temp * 1000) % 1000;
//把浮点数温度转为字符串
sprintf(buffer,"temp = %d.%d\n", m, n);
//LCD屏幕显示字符串
        lcd_show_string(200, 200, 300, 32, 32, buffer);
        delay_ms(1000);
    }
return 0;
}

三、代码模块讲解

  1. lcd_pad_init:IOMUX引脚复用。24根RGB数据线 + 4根控制信号线全部复用为LCDIF外设功能,PAD电气属性统一配置0xB9;GPIO1_IO08作为LCD背光GPIO,输出高电平点亮背光。

    这里背光是GPIO控制,只有亮灭两种状态,后面PWM实验我们就改成PWM驱动背光,实现亮度可调。

  2. lcd_clk_init:配置PLL5视频锁相环,分频产生LCD像素时钟PCLK。等待PLL锁频成功,配置CCM选择时钟源与分频系数,串口打印分频信息用于调试。

  3. lcd_reset:eLCDIF控制器软件复位,上电必须复位,保证寄存器处于稳定状态。

  4. lcd_init

    填充tftlcd_t结构体,存放屏幕分辨率、前后廊时序、帧缓存地址、前景背景色;

    配置eLCDIF的VDCTRL系列寄存器,写入行场同步时序;

    设置CUR_BUF当前帧缓存地址,eLCDIF会从这块内存读取像素;

    使能LCDIF,屏幕开始输出图像。

  5. 屏幕绘图API

    lcd_drawpoint:最底层接口,根据(x,y)坐标,计算帧缓存内存地址,写入颜色值;增加边界判断,防止越界访问内存;

    screen_clear:双重循环遍历全部像素,填充统一颜色,清屏;

    lcd_h_drawline:循环调用打点,画一条水平直线。

  6. main函数

    初始化顺序:中断 → 系统时钟 → LED、蜂鸣器、按键 → GPT → UART → AT24C02 → LCD。

    清屏为白色,主循环:读取LM75温度,sprintf将浮点温度转为字符串,调用lcd_show_string在屏幕上打印温度,1秒刷新一次。

四、实验现象

  1. 开发板上电,LCD背光点亮,屏幕清为纯白色;
  2. 屏幕(200,200)位置每秒刷新一次LM75采集的温度 temp = xx.xxx;
  3. 取消注释lcd_h_drawline可以在屏幕画红色水平线;
  4. 取消注释lcd_show_string可以直接在屏幕打印hello world。

总结

  1. 时序参数必须严格匹配屏幕手册,HBP/HFP/VBP/VFP、同步脉冲设置错误,直接黑屏或者花屏。
  2. 帧缓存地址不能随便设置,要确保是空闲的内存区域,不能覆盖程序代码、栈空间。
  3. PLL锁频:必须等待PLL的lock标志位,不等待直接使用时钟,PCLK不稳定,屏幕闪烁、乱码。
  4. 引脚复用:24根RGB数据线、HSYNC/VSYNC/PCLK/DE,任何一根引脚复用错,屏幕都会异常。
  5. 浮点数LCD打印:LCD不支持printf,用sprintf把数值转字符串,调用字库函数lcd_show_string显示。
  6. 背光GPIO:背光引脚忘记设置输出高电平,屏幕背光不亮,屏幕漆黑,但eLCDIF其实已经正常输出图像。
相关推荐
傲世仙尊3 小时前
序列化与自定义协议-把结构体拆成字节流再拼回来
linux·服务器·网络·arm开发
脚踏实地,坚持不懈!3 小时前
Linux 内核源码解析:从 secondary_startup_64 到 pick_eevdf 的完整调用栈分析
android·linux·arm开发
斑马1394 小时前
ARM学习笔记(七)——LCD液晶显示
arm开发·笔记·学习
M78佐菲15 小时前
ARM学习笔记(11)
linux·arm开发·笔记·嵌入式硬件·学习
M78佐菲15 小时前
ARM学习笔记(10)
linux·arm开发·笔记·嵌入式硬件·学习
小诗懂技术17 小时前
【裸机lcd】framebuffer的配置与小型图像库的移植
arm开发·单片机·嵌入式硬件·裸机
SKH.18 小时前
ARM-时钟、定时器
arm开发
dadaobusi1 天前
学习: ARM SMMU stream id
arm开发·学习
AOI小白新手上路1 天前
韦东山《ARM 架构与编程》3-2 GPIO 引脚操作方法概述 · 学习笔记
arm开发·学习·架构