LCD驱动程序(非标准)
1. LCD 驱动原理
1.1 LCD 操作原理
- LCD 屏幕上的点称为像素
- 屏幕后面有电子枪【红绿蓝】:一边移动,一边发出颜色
- 每来一个clk,移动一个像素
- R、G、B 三组线确定颜色
- 接收到 HSYNC(水平同步信号)脉冲,电子枪从最右边跳到最左边。(跳到下一行)
- 接收到 VSYNC [垂直同步信号]从最下边跳到最上边。(跳到原点)
1.2 驱动 LCD 原理
LCD 驱动的原理如上图所示:
TFT LCD 的显示原理是 LCD 内有一个电子枪,每次会绘制一个像素点。
- 当信号 VDEN 有效时,会根据数据线 VD 上的 RGB 颜色情况,绘制一个像素点;
- VCLK 时钟信号,控制电子枪水平移动到下一个像素点(从左至右);
- HSYNC 同步信号,使电子枪往下移动一行,并回到该行的起始位置;
- VSYNC/FRAME 同步信号,则使电子枪回到第一个像素点;
- 当我们只移动 VCLK\HSYNC\VSYNC 信号,而使 VDEN 为无效时,就会显示出黑屏;
对于 RGB 色彩的数据是存储在 SDRAM 里划出来的一块显存空间中,由 S3C2440 的 LCDCDMA 这个 LCD 专用的 DMA 控制器自动从 SDRAM 里面去读取,然后通过 VIDPRCS 寄存器 发送给 TFT LCD 屏幕。
而 VIDEO MUX 模块则控制时序等操作。
2. Linux 内核驱动程序框架
在编写驱动程序之前,我们需要先了解驱动程序的框架。驱动程序的框架参考 Linux内核 LCD 驱动程序框架_Bin Watson的博客-CSDN博客 这篇文章。
3. jz2440 LCD相关硬件分析
jz2440_LCD硬件分析_Bin Watson的博客-CSDN博客
4. 驱动程序(非标准实现)
4.1 驱动程序的入口函数
分配一个 struct fb_info 结构体,用于注册 framebuffer 时使用。并且完成该结构体的初始化工作。
-
分配一个 fb_info 结构体,然后往该结构体里面填充数据。
static struct fb_info *s3c2440_info; static int __init s3c2440_lcd_init(void) { int ret; /* 1. 分配一个fb_info结构体 */ s3c2440_info = framebuffer_alloc(0, NULL); /* 不需要额外的空间 */ if (!s3c2440_info) { return -ENOMEM; }
-
设置 fb_info 结构体的数据,先设置固定参数 fb_fix_screeninfo
/* 2. 设置 */ /* 2.1 设置固定的参数 */ strcpy(s3c2440_info->fix.id, "s3c2440-lcd"); /* identification string */ // s3c2440_info->fix.smem_start = ; /* Start of frame buffer mem (physical address) */ s3c2440_info->fix.smem_len = FRAME_BUFFER_LEN; /* Length of frame buffer mem */ s3c2440_info->fix.type = FB_TYPE_PACKED_PIXELS; s3c2440_info->fix.type_aux = 0; /* INTERLEAVED_PLANES 才设置 */ s3c2440_info->fix.visual = FB_VISUAL_TRUECOLOR; /* 颜色模式为真彩色 */ s3c2440_info->fix.xpanstep = 0; s3c2440_info->fix.ypanstep = 0; s3c2440_info->fix.ywrapstep = 0; s3c2440_info->fix.line_length = FRAME_LINE_LENGTH; /* 一行的长度,以字节为单位 */
smem_start 指定的是显存的物理地址的起始地址,我们在后面进行设置。
smem_len 指定的是显示的物理地址的大小。
其中 FRAME_BUFFER_LEN 等宏定义如下,我们使用的是 A043-24-TT-11 这款 LCD 屏幕,其分辨率为 480 * 272。
#define FRAME_X_RES 480 /* 表示屏幕一行的像素点个数 */ #define FRAME_Y_RES 272 /* 表示屏幕一列的像素点个数 */ #define FRAME_PIXEL_RED 5 /* 红色占5个位 */ #define FRAME_PIXEL_GREE 6 /* 绿色占6个位 */ #define FRAME_PIXEL_BLUE 5 /* 蓝色占5个位 */ #define FRAME_PIXEL (FRAME_PIXEL_RED + FRAME_PIXEL_GREE + FRAME_PIXEL_BLUE) #define FRAME_BUFFER_LEN (FRAME_X_RES * (FRAME_PIXEL/8) * FRAME_Y_RES) #define FRAME_LINE_LENGTH (FRAME_X_RES * (FRAME_PIXEL/8))
再根据 jz2440 的原理图
可知,我们的 LCD 插座只接了 16 根数据线(其中红色 R1 ~ R5,绿色 G0 ~ G5,蓝色 B1~ B5)。
也就是说我们使用的是 S3C2440A 芯片的 LCD 控制器中的 16bpp 的显示方式。
而根据 S3C2440A 的 LCD 控制器位设置模式
我们将 红色设置为高5位、绿色设置为中间6位、蓝色设置为低5位。定义在了上方的宏 FRAME_PIXEL_RED、FRAME_PIXEL_GREE 和 FRAME_PIXEL_BLUE 中。
-
设置可变参数 fb_var_screeninfo
/* 2.2 设置可变的参数 */ s3c2440_info->var.xres = FRAME_X_RES; /* 水平分辨率 */ s3c2440_info->var.yres = FRAME_Y_RES; /* 垂直分辨率 */ s3c2440_info->var.xres_virtual = FRAME_X_RES; /* 水平虚拟分辨率 */ s3c2440_info->var.yres_virtual = FRAME_Y_RES; /* 垂直虚拟分辨率 */ s3c2440_info->var.xoffset = 0; /* 虚拟分辨率与真实分辨率之间从偏移值 */ s3c2440_info->var.yoffset = 0; /* 虚拟分辨率与真实分辨率之间从偏移值 */ s3c2440_info->var.bits_per_pixel = FRAME_PIXEL; /* 每个像素用多少位 */ s3c2440_info->var.red.offset = FRAME_PIXEL_BLUE + FRAME_PIXEL_GREE; s3c2440_info->var.red.length = FRAME_PIXEL_RED; s3c2440_info->var.green.offset = FRAME_PIXEL_BLUE; s3c2440_info->var.green.length = FRAME_PIXEL_GREE; s3c2440_info->var.blue.offset = 0; s3c2440_info->var.blue.length = FRAME_PIXEL_BLUE; s3c2440_info->var.activate = FB_ACTIVATE_NOW; s3c2440_info->var.height = (__u32)53.856; s3c2440_info->var.width = (__u32)95.04;
对于其中参数的说明参考 Linux内核 LCD 驱动程序框架_Bin Watson的博客-CSDN博客
我们对 LCD 的操作时序进行分析:
我们可以得出:
描述 取值 运算 VBPD 1 2 Hsync - 1 LINEVAL 271 272 Hsync - 1 VFPD 1 2 Hsync - 1 VSPW 9 10 Hsync - 1 HBPD 1 2CLK - 1 HOZVAL 479 480 - 1 HFPD 1 2CLK - 1 HSPW 40 41CLK - 1 VCLK 10 MHz 为了方便后面计算 CLKVAL 的值,我们取一个 LCD 控制器能够接受的值。 VCLK 能够取的 VCLK 的值在 5 MHz ~ 12 MHz 之间。
于是我们就可以设置 left_margin 等变量了
s3c2440_info->var.pixclock = 0; /* pixel clock in ps (pico seconds) */ s3c2440_info->var.left_margin = 1; /* HFPD */ s3c2440_info->var.right_margin = 1; /* HBPD */ s3c2440_info->var.upper_margin = 1; /* VFPD */ s3c2440_info->var.lower_margin = 1; /* VBPD */ s3c2440_info->var.hsync_len = 40; /* HSPW */ s3c2440_info->var.vsync_len = 9; /* VSPW */ s3c2440_info->var.sync = FB_SYNC_HOR_HIGH_ACT; /* see FB_SYNC_* */ s3c2440_info->var.vmode = FB_VMODE_NONINTERLACED; /* see FB_VMODE_* */
-
接着我们在设置 fb_info 的硬件操作函数
/* 2.3 设置操作函数 */ s3c2440_info->fbops = &s3c2440_lcd_fb_ops; /* 2.4 其它设置 */ s3c2440_info->pseudo_palette = pseudo_platte; // s3c2440_info->screen_base = ; /* Virtual address 显存的虚拟地址 */ s3c2440_info->screen_size = FRAME_BUFFER_LEN; /* Amount of ioremapped VRAM or 0 */
screen_size 指定的屏幕的显存的虚拟地址大小。
screen_base 指定显存的虚拟地址的起始地址,我们在后面进行设置。
s3c2440_lcd_fb_ops 的定义如下:
static struct fb_ops s3c2440_lcd_fb_ops = { .owner = THIS_MODULE, .fb_setcolreg = s3c2440_lcd_setcolreg, .fb_fillrect = cfb_fillrect, /* 内核实现 */ .fb_copyarea = cfb_copyarea, /* 内核实现 */ .fb_imageblit = cfb_imageblit, /* 内核实现 */ };
cfb_fillrect、cfb_copyarea、cfb_imageblit 已经由内核实现好了,它们会根据 fb_info 中设置的 fb_fix_screeninfo 和 fb_var_screeninfo 两个结构体中的信息,帮助我们往 LCD 屏幕上绘制图像。
s3c2440_lcd_setcolreg 是一个虚假调色板:
static u32 pseudo_platte[16]; static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf) { chan &= 0xffff; chan >>= 16 - bf->length; return chan << bf->offset; } static int s3c2440_lcd_setcolreg(unsigned int regno, unsigned int red, unsigned int green, unsigned int blue, unsigned int transp, struct fb_info *info) { unsigned int val; /* dprintk("setcol: regno=%d, rgb=%d,%d,%d\n", regno, red, green, blue); */ if (regno >= 16) return 1; val = chan_to_field(red, &info->var.red); val |= chan_to_field(green, &info->var.green); val |= chan_to_field(blue, &info->var.blue); ((u32*)(info->pseudo_palette))[regno] = val; return 0; }
上面我们就大致完成了 fb_info 的设置,下面是对 S3C2440 LCD 控制器的设置,同时还有要配置一些 GPIO 引脚工作为 LCD 使用的引脚模式。
-
配置 GPIO硬件用于 LCD
上面的原理图说明了用于驱动 LCD 屏幕的引脚设置,包括数据引脚、控制引脚。而要使 LCD 屏幕能够显示出来,我们还需要打开 LCD 的背光灯。背光灯的控制由 KEYBORAD 引脚来控制,其连接到 GPB0 引脚上。
/* 3. 硬件相关的操作 */ /* 3.1 配置GPIO用于LCD */ /* LEND:GPC0 * VCLK:GPC1 * VLINE:GPC2 * VFRAME/VSYNC:GPC3 * VM:GPC4 * LCD_LPCOE/GPC5 * LCD_LPCREV/GPC6 * LCD_LPCREVB/GPC7 * VD0~VD7: GPC8~GPC15 * VD8~VD23: GPD0~GPD15 * LCD_PWREN/EINT12/GPG4 * TOUT0/GPB0:KEYBOARD 背光灯 */ gpbcon = (volatile unsigned long *)ioremap(0x56000010, 8); gpbdat = gpbcon + 1; gpccon = (volatile unsigned long *)ioremap(0x56000020, 4); gpdcon = (volatile unsigned long *)ioremap(0x56000030, 4); gpgcon = (volatile unsigned long *)ioremap(0x56000060, 4); *gpccon = 0xaaaaaaaa; /* 配置为LCD引脚 */ *gpdcon = 0xaaaaaaaa; /* 配置为LCD引脚 */ *gpbcon &= ~(3); *gpbcon |= 1; *gpbdat &= ~(1); /* 输出低电平,关闭背光 */ *gpgcon |= (3 << (4 * 2)); /* 配置为LCD_PWRDN */
-
配置 LCD 控制器
先设置 VCLK 时钟,再设置 LCD 显色模式。并初始化 LCD 屏幕为关闭状态。
/* 3.2 根据LCD手册设置LCD控制器 */ /* 3.2.1 时序设置 */ lcd_regs = (struct s3c2440_lcd_regs *)ioremap(0X4D000000, sizeof(struct s3c2440_lcd_regs)); /* bit[17:8]: VCLK = HCLK / [(CLKVAL+1) x 2] * 10MHz = 100MHz / [(CLKVAL + 1) x 2] * ==> CLKVAL = 4 * bit[6:5] : 11 = TFT LCD panel * bit[4:1] : 1100 = 16 bpp for TFT * bit[0] : 1 = Enable the video output and the LCD control signal. */ lcd_regs->lcdcon1 &= ~(0x7fff); /* 清零 */ lcd_regs->lcdcon1 |= ((4 << 8) | (3 << 5) | (0x0c << 1));
接着设置控制时序和设置时序极性
/* LCDCON2: * VBPD [31:24] : 1 = 2 Hsync - 1 * LINEVAL [23:14] : 271 = 272 - 1 * VFPD [13:6] : 1 = 2 Hsync - 1 * VSPW [5:0] : 9 = 10Hsync - 1 * LCDCON3: * HBPD [25:19] : 1 = 2CLK - 1 * HOZVAL [18:8] : 479 = 480 - 1 * HFPD [7:0] : 1 = 2CLK - 1 * LCDCON4: * HSPW [7:0] : 40 = 41CLK - 1 **/ lcd_regs->lcdcon2 = ((1 << 24) | (271 << 14) | (1 << 6) | (9)); lcd_regs->lcdcon3 = ((1 << 19) | (479 << 8) | (1)); lcd_regs->lcdcon4 = 40; /* 3.2.2 极性设置 */ /* BPP24BL[12] : 0 = LSB valid * FRM565[11] : 1 = 5:6:5 Format * INVVCLK[10] : 0 = The video data is fetched at VCLK falling edge * INVVLINE[9] : 1 = Inverted. This bit indicates the VLINE/HSYNC pulse polarity. * INVVFRAME[8] : 1 = Inverted. This bit indicates the VFRAME/VSYNC pulse polarity. * INVVD[7] : 0 = Normal. This bit indicates the VD (video data) pulse polarity. * INVVDEN[6] : 0 = Normal. This bit indicates the VDEN signal polarity. * INVPWREN[5] : 0 = Normal. This bit indicates the PWREN signal polarity. * INVLEND[4] : 0 = Normal. This bit indicates the LEND signal polarity. * PWREN[3] : 0 = Disable PWREN signal. LCD_PWREN output signal enable/disable. * BSWP [1] : 0 = Swap Disable. Byte swap control bit. * HWSWP[0] : 1 = Swap Enable. Half-Word swap control bit. **/ lcd_regs->lcdcon5 = ((1 << 11) | (0 << 10) | (1 << 9) | (1 << 8) | (1));
-
然后还需要设置 LCD 的显存,是的 LCDCDMA 可以访问到我们的显存地址。
/* 3.3 分配显存(Frame buffer),并告知LCD控制器 */ s3c2440_info->screen_base = dma_alloc_writecombine(NULL, s3c2440_info->fix.smem_len, (u32*)&s3c2440_info->fix.smem_start, GFP_KERNEL); lcd_regs->lcdsaddr1 = ((s3c2440_info->fix.smem_start >> 1) & ~(3 << 30)); lcd_regs->lcdsaddr2 = ((s3c2440_info->fix.smem_start + s3c2440_info->fix.smem_len) >> 1) & 0x1fffff; lcd_regs->lcdsaddr3 = (FRAME_X_RES * FRAME_PIXEL/16); /* 一行的长度,单位是半字,两字节 */
dma_alloc_writecombine
的原型如下:/** * dma_alloc_writecombine - allocate writecombining memory for DMA * @dev: valid struct device pointer, or NULL for ISA and EISA-like devices * @size: required memory size * @handle: bus-specific DMA address * * Allocate some uncached, buffered memory for a device for * performing DMA. This function allocates pages, and will * return the CPU-viewed address, and sets @handle to be the * device-viewed address. */ void * dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
返回值为显存的虚拟地址,我们正好设置在 screen_base 中。同时还会设置 &s3c2440_info->fix.smem_start 指向显存的物理地址。
设置完 S3C2440 芯片相关的寄存器配置后,我们同时也完善了 fb_info 的设置,接着就可以将其注册进 framebuffer 中了
/* 4. 注册 */
ret = register_framebuffer(s3c2440_info);
if (ret < 0) {
printk(KERN_ERR "Failed to register framebuffer device: %d\n", ret);
goto free_memory;
}
最后是启动 LCD
/* 5. 启动LCD */
lcd_regs->lcdcon1 |= (1 << 0); /* 使能LCD控制器 */
lcd_regs->lcdcon5 |= (1 << 3); /* 使能LCD本身 */
*gpbdat |= 1; /* 输出高电平,开启背光 */
return 0;
free_memory:
framebuffer_release(s3c2440_info);
return ret;
}
4.2 出口函数
完成注销工作
static void __exit s3c2440_lcd_exit(void)
{
/* 卸载驱动程序 */
lcd_regs->lcdcon1 &= ~1; /* 关LCD */
*gpbdat &= ~1; /* 关背光 */
unregister_framebuffer(s3c2440_info);
dma_free_writecombine(NULL, s3c2440_info->fix.smem_len,
s3c2440_info->screen_base, s3c2440_info->fix.smem_start);
iounmap(lcd_regs);
iounmap(gpgcon);
iounmap(gpdcon);
iounmap(gpccon);
iounmap(gpbcon);
framebuffer_release(s3c2440_info);
}
5. 测试
ubuntu
首先去掉 Linux 内核中原来的驱动程序
make menuconfig # 配置内核
-> Device Drivers
-> Graphics support
<M> S3C2410 LCD framebuffer support # 设置成模块
重新编译内核,并且编译所有的模块:
make uImage
make modules
然后将 cfbcopyarea.ko、cfbfillrect.ko、cfbimgblt.ko 三个模块拷贝到 nfs 目录下:
cp drivers/video/cfbcopyarea.ko drivers/video/cfbfillrect.ko drivers/video/cfbimgblt.ko ~/nfs_rootfs/
开发板
将新编译的 uImage 下载到 30000000 这个地址上,然后引导 uboot 启动新的内核。
usb 1 30000000
bootm 30000000
启动内核后,挂载 ubuntu 的 nfs_rootfs,并且加载模块:
mount -t nfs -o intr,nolock,rsize=1024,wsize=1024 192.168.137.3:/home/binwatson/nfs_rootfs /mnt
cd /mnt
insmod cfbcopyarea.ko
isnmod cfbfillrect.ko
insmod cfbimgblt.ko
insmod s3c2440_lcd.ko
然后设置输出到屏幕
echo hello > /dev/tty1