ipslcd
一、预备知识
驱动设备:ipstft_lcd屏幕240*135
驱动程序:st7789v(w)
板子:原子mini-linux,im6ull
内核:4.1.15
包含:SPI、Framebuffer
SPI控制器
原子自带的大LCD,芯片内部带有外设LCDIF控制器,采用并行数据
用于控制LCD屏幕显示。
自己买的LCD屏需要SPI驱动,可以:模拟SPI
、硬件SPI
。
- 模拟SPI:使用GPIO口模拟SPI协议。
- 硬件SPI(推荐):芯片内部的外设SPI控制器,带有DMA,传输速度可达几十、几百兆。
SPI驱动的编写也遵循分离与分层
的思想,和platform总线类似,咱们也有spi总线、spi驱动、spi设备。
- 要想能顺利方便的传输数据,总有人要为之负重前行:
drivers/spi/spi-imx.c
中,基于platform框架。
设备:设备树中的spi节点,如ecspi3。
总线:platform总线,设备树匹配。
驱动:spi_imx_driver中的.probe函数,底层的spi_imx_transfer收发函数。 - 前人栽树,后人乘凉:
drivers/spi/spi.c
中,创建了自己的spi框架。
设备:设备树中spi节点下的子节点,自己定义一个设备,如ecspi3/spidev0: ipslcd@0。
总线:spi总线,传统/设备树匹配。
驱动:struct spi_driver结构体声明,用于搭建spi子系统。
Framebuffer
spi控制器的驱动写完了,可以点亮屏幕了。可是咱们这是linux系统,必须高端,framebuffer
机制满足这一高傲条件。
帧缓冲(framebuffer) 是Linux 系统为显示设备提供的一个接口,将内存中的一块儿显存抽象为一种设备,用户可直接通过fb操作函数控制这一块显存,进而反映到屏幕上。
framebuffer是个字符设备,主设备号为29,对应于/dev/fb%d 设备文件。
据说这事成了之后可以直接控制/dev/fbx,挺方便的。
spi+framebuffer
fbtft就是基于spi和framebuffer衍生的一个机制。
参考博客:这是一个链接
主要有以下几个角色文件:
fbtft.h
:连接源文件的头文件,主要的结构体、基本驱动框架、spi/platform框架。fbtft_device.c
:代替了设备树,用于描述硬件信息。st7789v.c
:屏幕IC的驱动部分。fbtft-core.c
:核心层,匹配成功之后的probe函数,实现了一个 frambuffer 设备驱动。- fbtft-bus.c:提供读写寄存器 / 显存的功能。
- fbtft-io.c:提供最底层的 spi 读写功能。
- fbtft-sysfs.c,导出一些调试接口。
基本刷屏流程:
可以看到,fbtft中仍然时使用了一个线程,不断地将显存中的数据通过spi发送到屏幕IC。
二、spi+fb 模块驱动测试
看到很多大神在写屏幕驱动时候,都是先简单的测测spi是否能用,再做进一步打算。
设备树修改
选择自己心仪的引脚,注意此设备树中其他设备就不能使用这些引脚了,要仔细检查哦!!!
设备树整完,就可以简单测试以下,网络启动板子加载设备树,查看/sys/bus/spi
会出现spi2.0(对应的是spi3子节点),手动查看下边儿的一些属性、变量。
/* zxy ips_tftlcd 1.14 research imx6ul-pinfunc.h*/
pinctrl_ecspi3: ipslcd {
fsl,pins = <
MX6UL_PAD_UART3_TX_DATA__GPIO1_IO24 0x10b0 /* RES */
MX6UL_PAD_UART3_RX_DATA__GPIO1_IO25 0x10b0 /* DC */
MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK high slew rate*/
MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO high slew rate*/
MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI high slew rate*/
>;
};
...
/* zxy ips_tftlcd */
&ecspi3 {
fsl,spi-num-chipselects = <1>;
res-gpios = <&gpio1 24 GPIO_ACTIVE_HIGH>;//用于自己的驱动reset
dc-gpios = <&gpio1 25 GPIO_ACTIVE_HIGH>;//用于自己的驱动dc
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; //spi-imx.c中的probe函数识别并使用该片选
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi3>;//pinctrl子系统加载刚才配置的引脚,使其具有复用、电器属性
status = "okay";
/* ipslcd sub-node */
spidev0: ipslcd@0 { //0表示片选
compatible = "zhongjing,ipslcd_st7789v";
spi-max-frequency = <25000000>; //刷新频率有待商榷
reg = <0>;
};
};
驱动编写
此部分为主要角色,一般编写顺序包括:
- 基本驱动框架
- spi驱动框架,匹配成功进probe函数,定义个自己的设备结构体、获取设备节点啥的。
- 按照厂家给的51初始化程序改编为自己的初始化ipslcd程序,注意:屏幕IC是8bit总线,发窗口数据时,可以一股脑的发。
- fb设备创建,用一个线程,循环发窗口数据。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/fb.h>
#include <linux/dma-mapping.h>
#include <linux/sched.h>
#include <linux/wait.h>
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/uaccess.h>
/***************************************************************
文件名 : ipslcd.c
作者 : zxy
版本 : V1.3
***************************************************************/
/* 暂时放到这儿的东西 */
#define LCD_W 240
#define LCD_H 135
//画笔颜色
#define WHITE 0xFFFF
#define BLACK 0x0000
#define BLUE 0x001F
#define BRED 0XF81F
#define GRED 0XFFE0
#define GBLUE 0X07FF
#define RED 0xF800
#define MAGENTA 0xF81F
#define GREEN 0x07E0
#define CYAN 0x7FFF
#define YELLOW 0xFFE0
#define BROWN 0XBC40 //棕色
#define BRRED 0XFC07 //棕红色
#define GRAY 0X8430 //灰色
void show_fb(struct fb_info *fbi, struct spi_device *spi);
struct fb_info * fb_init(struct spi_device *spi);
void fb_del(struct spi_device *spi);
/*---------------------------------------------------------------------------------------------------*/
/* fb设备部分 */
/* 管理spi和thread的一个结构体 */
typedef struct{
struct spi_device *spi; //记录fb_info对象对应的spi设备对象
struct task_struct *thread; //记录线程对象的地址,此线程专用于把显存数据发送到屏的驱动ic
}lcd_data_t;
static int ipslcd_fb_setcolreg(unsigned int regno, unsigned int red,
unsigned int green, unsigned int blue,
unsigned int transp, struct fb_info *info)
{
return 0;
}
/* 对fb的操作函数 以后学习可以自己搞搞*/
struct fb_ops fops = {
.owner = THIS_MODULE,
.fb_setcolreg = ipslcd_fb_setcolreg,
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
};
/* 线程函数 */
int thread_func(void *data)
{
struct fb_info *fbi = (struct fb_info *)data;
lcd_data_t *ldata = fbi->par; //貌似获取了fbi某个成员变量par
while(1)
{
if(kthread_should_stop())
break;
show_fb(fbi, ldata->spi);
}
return 0;
}
/* 此函数在spi设备驱动的probe函数里被调用 */
struct fb_info * fb_init(struct spi_device *spi)
{
struct fb_info *fbi;
u8 *v_addr;
u32 p_addr;
lcd_data_t *ldata;
v_addr = dma_alloc_coherent(NULL, LCD_W * LCD_H * 2, &p_addr, GFP_KERNEL);
fbi = framebuffer_alloc(sizeof(lcd_data_t), NULL);//额外分配lcd_data_t类型空间
ldata = fbi->par; //datal指针指向额外分配的空间
ldata->spi = spi;
/* 设置fbi的变量 RGB这里需要注意长度和偏移*/
fbi->var.xres = LCD_W;
fbi->var.yres = LCD_H;
fbi->var.xres_virtual = LCD_W;
fbi->var.yres_virtual = LCD_H;
fbi->var.bits_per_pixel = 16;//每个像素16bit=5+6+5 2字节
fbi->var.red.offset = 11;//偏移
fbi->var.red.length = 5;//长度
fbi->var.green.offset = 5;
fbi->var.green.length = 6;
fbi->var.blue.offset = 0;
fbi->var.blue.length = 5;
/* 设置fbi的常量 */
strcpy(fbi->fix.id, "fb_ipslcd");
fbi->fix.smem_start = p_addr; //显存的物理地址
fbi->fix.smem_len = LCD_W * LCD_H * 2;
fbi->fix.type = FB_TYPE_PACKED_PIXELS;
fbi->fix.visual = FB_VISUAL_TRUECOLOR;
fbi->fix.line_length = LCD_W * 2;
/* 其他重要的成员 */
fbi->fbops = &fops;
fbi->screen_base = v_addr;//显存虚拟地址
fbi->screen_size = LCD_W * LCD_H * 2;//显存大小
spi_set_drvdata(spi, fbi); //spi成员关联了fbi
register_framebuffer(fbi); //注册初始化完成的fbi
ldata->thread = kthread_run(thread_func, fbi, spi->modalias);//spi和fbi更“深入”的交流
return fbi;
}
/* 此函数在spi设备驱动remove时被调用 */
void fb_del(struct spi_device *spi)
{
struct fb_info *fbi = spi_get_drvdata(spi); //得到之前存在spi设备中的fbi
lcd_data_t *ldata = fbi->par;
kthread_stop(ldata->thread); //让刷图线程退出
unregister_framebuffer(fbi);
dma_free_coherent(NULL, fbi->screen_size, fbi->screen_base, fbi->fix.smem_start);//把物理地址和虚拟地址放进去
framebuffer_release(fbi);
}
/*---------------------------------------------------------------------------------------------------*/
/* spi设备部分 */
/* 开门见山 设备结构体 */
#define IPSLCD_CNT 1
#define IPSLCD_NAME "ipslcd"
struct ipslcd_dev{
dev_t devid; //设备号 unsigned int
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备
struct device_node *nd; //设备节点
int major; //主设备号
void *private_data; //私有数据
int res_gpios; //复位所使用的GPIO编号
int dc_gpios; //命令所使用的GPIO编号
};
struct ipslcd_dev ipslcddev;
/* 写一个8bit到设备 */
static s32 ipslcd_write_regs(struct spi_device *spi, u8 *buf, unsigned int len)
{
int ret;
struct spi_transfer *t;
struct spi_message m;
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
//gpio_set_value(dev->cs_gpio, 0); /* 片选拉低 */
t->tx_buf = buf; /* 要发送的数据 */
t->len = len; /* 字节 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
kfree(t); /* 释放内存 */
//gpio_set_value(dev->cs_gpio, 1);/* 片选拉高,释放ICM20608 */
return ret;
}
/* 写一个命令command */
void ipslcd_write_command(struct spi_device *spi, u8 cmd)
{
gpio_set_value(ipslcddev.dc_gpios, 0); //写命令 低电平
ipslcd_write_regs(spi, &cmd, 1);
gpio_set_value(ipslcddev.dc_gpios, 1); //一般很少写命令,写完命令,就切换为写数据
}
/* 写一个数据8位data */
void ipslcd_write_data8(struct spi_device *spi, u8 data)
{
ipslcd_write_regs(spi, &data, 1);
}
/* 分两次写一个数据16位data */
void ipslcd_write_data16(struct spi_device *spi, u16 data)
{
u8 data_h8,data_l8;
//获取高8位和低8位
data_h8 = data>>8;
data_l8 = (u8)data;
//发送2字节 人家设备就是一次只发8bit,要是一下发16bit就不好了
ipslcd_write_regs(spi, &data_h8, 1);
ipslcd_write_regs(spi, &data_l8, 1);
}
/* ipslcd设置起始地址和结束地址 和横竖屏设置有关,具体查阅源码*/
void ipslcd_address_set(struct spi_device *spi, u16 x1, u16 y1, u16 x2, u16 y2)
{
ipslcd_write_command(spi, 0x2a); //列地址设置
ipslcd_write_data16(spi, x1+40);
ipslcd_write_data16(spi, x2+40);
ipslcd_write_command(spi, 0x2b); //行地址设置
ipslcd_write_data16(spi, y1+53);
ipslcd_write_data16(spi, y2+53);
ipslcd_write_command(spi, 0x2c); //存储器写
}
/* framebuffer线程刷屏函数 fbi显存的数据给spi发送*/
void show_fb(struct fb_info *fbi, struct spi_device *spi)
{
u8 *p = (u8 *)fbi->screen_base;
u16 i;
//创建窗口,
//0x2c存储器可以一下收很多数据,而设置窗口时
//列地址和行地址的设置只能一次收8bit数据
ipslcd_address_set(spi, 0, 0, LCD_W-1, LCD_H-1);
//发送窗口数据时,可以无顾虑的发,一次spi发好多好多
ipslcd_write_regs(spi, p, LCD_W*LCD_H*2);
}
/*---------------------------------------------------------------------------------------------------*/
/* 测试函数 */
/* spi刷屏幕填充函数 一次spi就发一个像素*/
void ipslcd_fill(struct spi_device *spi, u16 x1, u16 y1, u16 x2, u16 y2, u16 color)
{
u16 i,j;
ipslcd_address_set(spi, x1, y1, x2-1, y2-1);//设置显示范围
for(i=y1; i<y2; i++)
{
for(j=x1; j<x2; j++)
{
ipslcd_write_regs(spi, &color, 2);
}
}
}
/* fb设备清屏函数 直接往显存里放数据 非常接近framebuffer思想*/
void ipslcd_fb_fill(struct spi_device *spi, struct fb_info *fbi, u16 color)
{
u8 *p = (u8 *)fbi->screen_base;
u16 i;
//ipslcd_address_set(spi, 0, 0, LCD_W-1, LCD_H-1);
for(i = 0; i < LCD_W * LCD_H; i++)
{
p[2*i] = color>>8; //高8位
p[2*i+1] = (u8)color; //低8位
}
}
/*---------------------------------------------------------------------------------------------------*/
/* ipslcd初始化函数 照着厂家给的51驱动移植*/
void ipslcd_device_init(struct spi_device *spi)
{
gpio_set_value(ipslcddev.res_gpios, 0);
mdelay(500);
gpio_set_value(ipslcddev.res_gpios, 1);
mdelay(500);
/************* Start Initial Sequence **********/
//设置横竖屏显示 可在此查阅源码修改
ipslcd_write_command(spi, 0x11);
mdelay(220);
ipslcd_write_command(spi, 0x36);
ipslcd_write_data8(spi, 0x70);
ipslcd_write_command(spi, 0x3A);
ipslcd_write_data8(spi, 0x05);
ipslcd_write_command(spi, 0xB2);
ipslcd_write_data8(spi, 0x0C);
ipslcd_write_data8(spi, 0x0C);
ipslcd_write_data8(spi, 0x00);
ipslcd_write_data8(spi, 0x33);
ipslcd_write_data8(spi, 0x33);
ipslcd_write_command(spi, 0xB7);
ipslcd_write_data8(spi, 0x35);
ipslcd_write_command(spi, 0xBB);
ipslcd_write_data8(spi, 0x19);
ipslcd_write_command(spi, 0xC0);
ipslcd_write_data8(spi, 0x2C);
ipslcd_write_command(spi, 0xC2);
ipslcd_write_data8(spi, 0x01);
ipslcd_write_command(spi, 0xC3);
ipslcd_write_data8(spi, 0x12);
ipslcd_write_command(spi, 0xC4);
ipslcd_write_data8(spi, 0x20);
ipslcd_write_command(spi, 0xC6);
ipslcd_write_data8(spi, 0x0F);
ipslcd_write_command(spi, 0xD0);
ipslcd_write_data8(spi, 0xA4);
ipslcd_write_data8(spi, 0xA1);
ipslcd_write_command(spi, 0xE0);
ipslcd_write_data8(spi, 0xD0);
ipslcd_write_data8(spi, 0x04);
ipslcd_write_data8(spi, 0x0D);
ipslcd_write_data8(spi, 0x11);
ipslcd_write_data8(spi, 0x13);
ipslcd_write_data8(spi, 0x2B);
ipslcd_write_data8(spi, 0x3F);
ipslcd_write_data8(spi, 0x54);
ipslcd_write_data8(spi, 0x4C);
ipslcd_write_data8(spi, 0x18);
ipslcd_write_data8(spi, 0x0D);
ipslcd_write_data8(spi, 0x0B);
ipslcd_write_data8(spi, 0x1F);
ipslcd_write_data8(spi, 0x23);
ipslcd_write_command(spi, 0xE1);
ipslcd_write_data8(spi, 0xD0);
ipslcd_write_data8(spi, 0x04);
ipslcd_write_data8(spi, 0x0C);
ipslcd_write_data8(spi, 0x11);
ipslcd_write_data8(spi, 0x13);
ipslcd_write_data8(spi, 0x2C);
ipslcd_write_data8(spi, 0x3F);
ipslcd_write_data8(spi, 0x44);
ipslcd_write_data8(spi, 0x51);
ipslcd_write_data8(spi, 0x2F);
ipslcd_write_data8(spi, 0x1F);
ipslcd_write_data8(spi, 0x1F);
ipslcd_write_data8(spi, 0x20);
ipslcd_write_data8(spi, 0x23);
ipslcd_write_command(spi, 0x21);
ipslcd_write_command(spi, 0x29);
//测试刷屏
ipslcd_fill(spi, 0, 0, LCD_W, LCD_H, RED); //红
mdelay(1000);
ipslcd_fill(spi, 0, 0, LCD_W, LCD_H, GREEN); //绿
mdelay(1000);
ipslcd_fill(spi, 0, 0, LCD_W, LCD_H, BLUE); //蓝
mdelay(1000);
printk("ipslcd init finish \t\n");
}
/* 打开设备 */
static int ipslcd_open(struct inode *inode, struct file *filp)
{
filp->private_data = &ipslcddev; /* 设置私有数据 */
ipslcd_device_init(ipslcddev.private_data);
return 0;
}
/* 关闭/释放设备 */
static int ipslcd_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* ipslcd操作函数 */
static const struct file_operations ipslcd_ops = {
.owner = THIS_MODULE,
.open = ipslcd_open,
.release = ipslcd_release,
};
/*spi驱动的probe函数 匹配成功即运行此函数 */
static int ipslcd_probe(struct spi_device *spi)
{
printk("spi_ipslcd match success \t\n");
int ret;
struct fb_info *fbi;
/* 1、构建设备号 */
if(ipslcddev.major) //如果有主设备号的话
{
ipslcddev.devid = MKDEV(ipslcddev.major, 0); //创建设备号
register_chrdev_region(ipslcddev.devid, IPSLCD_CNT, IPSLCD_NAME); //注册设备号
}
else
{
alloc_chrdev_region(&ipslcddev.devid, 0, IPSLCD_CNT, IPSLCD_NAME); //如果没有,还得申请一个设备号
ipslcddev.major = MAJOR(ipslcddev.devid);
}
/* 2、注册设备 */
cdev_init(&ipslcddev.cdev, &ipslcd_ops);
cdev_add(&ipslcddev.cdev, ipslcddev.devid, IPSLCD_CNT);
/* 3、创建类 */
ipslcddev.class = class_create(THIS_MODULE, IPSLCD_NAME);
if (IS_ERR(ipslcddev.class))
{
return PTR_ERR(ipslcddev.class);
}
/* 4、创建设备 */
ipslcddev.device = device_create(ipslcddev.class, NULL, ipslcddev.devid, NULL, IPSLCD_NAME);
if (IS_ERR(ipslcddev.device))
{
return PTR_ERR(ipslcddev.device);
}
/* 1、获取设备树中res dc信号的节点 */
ipslcddev.nd = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000");
if(ipslcddev.nd == NULL)
{
printk("ecspi3 node not find!\r\n");
return -EINVAL;
}
/* 2、 获取设备树中的gpio属性,得到gpio编号 */
ipslcddev.res_gpios = of_get_named_gpio(ipslcddev.nd, "res-gpios", 0);
if(ipslcddev.res_gpios < 0)
{
printk("can't get res-gpios");
return -EINVAL;
}
ipslcddev.dc_gpios = of_get_named_gpio(ipslcddev.nd, "dc-gpios", 0);
if(ipslcddev.dc_gpios < 0)
{
printk("can't get dc-gpios");
return -EINVAL;
}
/* 3、设置GPIO初始化输出 */
ret = gpio_direction_output(ipslcddev.res_gpios, 1);
if(ret < 0)
{
printk("can't set res_gpios!\r\n");
}
ret = gpio_direction_output(ipslcddev.dc_gpios, 1);
if(ret < 0)
{
printk("can't set dc_gpios!\r\n");
}
/*初始化spi_device */
spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
spi_setup(spi);
ipslcddev.private_data = spi;
/* 初始化屏幕 */
ipslcd_device_init(spi);
/* 初始化fb设备 */
fbi = fb_init(spi);
/* 初始化完了 刷个屏吧 */
ipslcd_fb_fill(spi, fbi, RED);
printk("framebuffer RED finish...");
mdelay(500);
ipslcd_fb_fill(spi, fbi, GREEN);
printk("framebuffer GREEN finish...");
mdelay(100);
ipslcd_fb_fill(spi, fbi, BLUE);
printk("framebuffer BLUE finish...");
mdelay(100);
ipslcd_fb_fill(spi, fbi, WHITE);
printk("framebuffer WHITE finish...");
return 0;
}
/*spi驱动的remove函数 */
static int ipslcd_remove(struct spi_device *spi)
{
/* fb设备回收 */
fb_del(spi);
/* 删除设备 */
cdev_del(&ipslcddev.cdev);
unregister_chrdev_region(ipslcddev.devid, IPSLCD_CNT);
/* 注销掉类和设备 */
device_destroy(ipslcddev.class, ipslcddev.devid);
class_destroy(ipslcddev.class);
return 0;
}
/* 传统匹配方式ID列表 */
static const struct spi_device_id ipslcd_id[] = {
{"zhongjing,ipslcd_st7789v", 0},
{}
};
/* 设备树匹配列表 */
static const struct of_device_id ipslcd_of_match[] = {
{ .compatible = "zhongjing,ipslcd_st7789v" },
{ /* Sentinel */ }
};
/*SPI驱动结构体 */
static struct spi_driver ipslcd_driver = {
.probe = ipslcd_probe,
.remove = ipslcd_remove,
.driver = {
.owner = THIS_MODULE,
.name = "ipslcd",
.of_match_table = ipslcd_of_match,
},
.id_table = ipslcd_id,
};
/*驱动入口函数 */
static int __init ipslcd_init(void)
{
return spi_register_driver(&ipslcd_driver);
}
/*驱动出口函数 */
static void __exit ipslcd_exit(void)
{
spi_unregister_driver(&ipslcd_driver);
}
module_init(ipslcd_init);
module_exit(ipslcd_exit);
MODULE_LICENSE("GPL");//GPL协议
MODULE_AUTHOR("zxy");
遇到的问题: 写完驱动测试,屏幕没有刷屏。
解决: spi发送一次数据,cs=0,发数据,cs=1。
没有分清楚屏幕IC想要什么,发命令和简单的配置数据时,只吃8bit数据,多了吃不下;设置窗口发16bit数据时,也要拆成8bit分别发送。
三、fbtft — st7789v内核驱动
测试之后,说明硬件连接啥的没啥问题,下面开始进内核吧。
在/driver/staging/fbtft
中存放专门用于spi驱动lcd屏的驱动。
(staging,意为过时的,也就是停止维护的驱动都放在这里,但这依然不影响我们学习使用)
前面我们学的spi+fb=fbtft
设备树修改
这次设备树只负责引脚的说明,仅仅pinctrl、gpio子系统发挥了作用。
/* zxy ips_tftlcd 1.14 research imx6ul-pinfunc.h*/
pinctrl_ecspi3: ipslcd {
fsl,pins = <
MX6UL_PAD_UART3_TX_DATA__GPIO1_IO24 0x10b0 /* RES */
MX6UL_PAD_UART3_RX_DATA__GPIO1_IO25 0x10b0 /* DC */
MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK high slew rate*/
MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO high slew rate*/
MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI high slew rate*/
>;
};
...
/* zxy ips_tftlcd */
&ecspi3 {
fsl,spi-num-chipselects = <1>;
res-gpios = <&gpio1 24 GPIO_ACTIVE_HIGH>;//用于自己的驱动reset
dc-gpios = <&gpio1 25 GPIO_ACTIVE_HIGH>;//用于自己的驱动dc
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; //spi-imx.c中的probe函数识别并使用该片选
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi3>;//pinctrl子系统加载刚才配置的引脚,使其具有复用、电器属性
status = "okay";
/* ipslcd sub-node */
/*
spidev0: ipslcd@0 { //0表示片选
compatible = "zhongjing,ipslcd_st7789v";
spi-max-frequency = <25000000>; //刷新频率有待商榷
reg = <0>;
};
*/
};
fbtft_device.c
描述设备树未描述完的设备信息,由于/driver/spi
中的驱动自己在设备树中找到了cs片选,这里就不用描述了。
传给struct fbtft_device_display displays[]
这个结构体用于匹配,之后在驱动初始
化函数中申请硬件资源。
//name必须赋值,赋值为你要使用的驱动,这样才能和displays[].name匹配上,否则这里会使用默认的空,然后你就很难找到问题所在
static char *name = "zxy_ipslcd_st7789v";
//分析代码得知,这个必须设置 (即使设置了下面配置列表的busnum也无效,这里应该是原来代码的bug)
static unsigned busnum = 2;
......
//struct fbtft_device_display displays[]中添加自己的设备信息
{ //zxy ipslcd st7789v 设备硬件描述,替代设备树
.name = "zxy_ipslcd_st7789v",
.spi = &(struct spi_board_info) {
.modalias = "fb_st7789v", //用于匹配driver
.max_speed_hz = 25000000, //25MHz
.bus_num = 2, //spi总线编号 ecspi3==spi2.0
//.chip_select = 0, //片选初始化电平
.mode = SPI_MODE_0, //spi模式
.platform_data = &(struct fbtft_platform_data) {
.display = {
.buswidth = 8, //总线宽度
//.backlight = 1, //背光
},
.bgr = false, //背景 一般为false
.gpios = (const struct fbtft_gpio []) { //gpio设置,前提是IO复用和电气属性已经配置
{ "reset", 24 }, //指定reset gpio号
{ "dc", 25 }, //指定dc gpio号
//{ "cs", 20 }, //指定cs gpio号
//{ "led", 3 },
//{},
},
}
}
}
st7789v.c
在这就可以参考之前写的那个测试驱动初始化函数。
里边儿写的这些主要有初始化函数、设置窗口函数。
最后传给struct fbtft_display display
这个结构体变量。
本来也写了个测试刷屏的函数,写完之后启动内核的时候可就炸了,直接failed,吓得咱赶紧去掉,或许是内核的启动内存控制比较严格吧,不小心给弄的栈溢出了。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/slab.h>
#include "fbtft.h"
/*
author : zxy
describe: 搞了好几天也没搞好,正愁着,看了一眼zhihui大佬的驱动,原来这个驱动是叫st7789vw,
调试时候,单独写的驱动模块,屏幕初始化,按照中景源码写的,调试OK
使用fbtft这一部分接近内核,就有点儿陌生了,调了好几天,确定了以下代码,和其他屏幕不同,
如此的小屏幕,还是发命令,发字节啥的都自己写一遍吧,都交给通用源码恐难以消受。
不管怎样,这也算搞好了。
*/
#define DRVNAME "fb_st7789v"
#define WIDTH 240//如果旋转了角度,这里的宽高需要对调
#define HEIGHT 135
#define DEFAULT_GAMMA \
"D0 04 0D 11 13 2B 3F 54 4C 18 0D 0B 1F 23\n" \
"D0 04 0C 11 13 2C 3F 44 51 2F 1F 1F 20 23" //颜色细调,非专业人士请勿靠近,默认
/* 写命令 */
static int ipslcd_write_command(struct fbtft_par *par, u8 cmd)
{
int ret;
gpio_set_value(par->gpio.dc, 0);
ret = par->fbtftops.write(par, &cmd, 1);
if (ret < 0)
dev_err(par->info->device,
"write() failed and returned %d\n", ret);
gpio_set_value(par->gpio.dc, 1);
return ret;
}
/* 写数据 8bit */
static int ipslcd_write_data8(struct fbtft_par *par, u8 data)
{
int ret;
ret = par->fbtftops.write(par, &data, 1);
if (ret < 0)
dev_err(par->info->device,
"write() failed and returned %d\n", ret);
return ret;
}
/* 硬件复位 */
static void reset(struct fbtft_par *par)
{
if(par->gpio.reset == -1)
return;
gpio_set_value(par->gpio.reset, 0);
mdelay(200);
gpio_set_value(par->gpio.reset, 1);
mdelay(200);
printk("Reset screen done.\n");
}
/* st7789初始化函数 */
static int init_display(struct fbtft_par *par)
{
par->fbtftops.reset(par);//硬件复位,防止之前的设置干扰当前配置
printk("**********************************zxy:screen init_display...*************************************************\r\n");
//************* Start Initial Sequence **********//
ipslcd_write_command(par, 0x11);
mdelay(120);
ipslcd_write_command(par, 0x36);
ipslcd_write_data8(par, 0x70);
ipslcd_write_command(par, 0x3A);
ipslcd_write_data8(par, 0x05);
ipslcd_write_command(par, 0xB2);
ipslcd_write_data8(par, 0x0C);
ipslcd_write_data8(par, 0x0C);
ipslcd_write_data8(par, 0x00);
ipslcd_write_data8(par, 0x33);
ipslcd_write_data8(par, 0x33);
ipslcd_write_command(par, 0xB7);
ipslcd_write_data8(par, 0x35);
ipslcd_write_command(par, 0xBB);
ipslcd_write_data8(par, 0x19);
ipslcd_write_command(par, 0xC0);
ipslcd_write_data8(par, 0x2C);
ipslcd_write_command(par, 0xC2);
ipslcd_write_data8(par, 0x01);
ipslcd_write_command(par, 0xC3);
ipslcd_write_data8(par, 0x12);
ipslcd_write_command(par, 0xC4);
ipslcd_write_data8(par, 0x20);
ipslcd_write_command(par, 0xC6);
ipslcd_write_data8(par, 0x0F);
ipslcd_write_command(par, 0xD0);
ipslcd_write_data8(par, 0xA4);
ipslcd_write_data8(par, 0xA1);
ipslcd_write_command(par, 0xE0);
ipslcd_write_data8(par, 0xD0);
ipslcd_write_data8(par, 0x04);
ipslcd_write_data8(par, 0x0D);
ipslcd_write_data8(par, 0x11);
ipslcd_write_data8(par, 0x13);
ipslcd_write_data8(par, 0x2B);
ipslcd_write_data8(par, 0x3F);
ipslcd_write_data8(par, 0x54);
ipslcd_write_data8(par, 0x4C);
ipslcd_write_data8(par, 0x18);
ipslcd_write_data8(par, 0x0D);
ipslcd_write_data8(par, 0x0B);
ipslcd_write_data8(par, 0x1F);
ipslcd_write_data8(par, 0x23);
ipslcd_write_command(par, 0xE1);
ipslcd_write_data8(par, 0xD0);
ipslcd_write_data8(par, 0x04);
ipslcd_write_data8(par, 0x0C);
ipslcd_write_data8(par, 0x11);
ipslcd_write_data8(par, 0x13);
ipslcd_write_data8(par, 0x2C);
ipslcd_write_data8(par, 0x3F);
ipslcd_write_data8(par, 0x44);
ipslcd_write_data8(par, 0x51);
ipslcd_write_data8(par, 0x2F);
ipslcd_write_data8(par, 0x1F);
ipslcd_write_data8(par, 0x1F);
ipslcd_write_data8(par, 0x20);
ipslcd_write_data8(par, 0x23);
ipslcd_write_command(par, 0x21);
ipslcd_write_command(par, 0x29);
//ipslcd_test_fill(par, 0x001F);
printk("**********************************zxy:screen init_display...finish*************************************************\r\n");
return 0;
}
/* st7789设置窗口函数 */
static void set_addr_win(struct fbtft_par *par, int xs, int ys, int xe, int ye)
{
/* adjustment */
xs += 40; xe += 40;
ys += 53; ye += 53;
/* Column address set */
write_reg(par, 0x2A,
(xs >> 8) & 0xFF, xs & 0xFF, (xe >> 8) & 0xFF, xe & 0xFF);
/* Row address set */
write_reg(par, 0x2B,
(ys >> 8) & 0xFF, ys & 0xFF, (ye >> 8) & 0xFF, ye & 0xFF);
/* Memory write */
write_reg(par, 0x2C);
}
/* 一些硬件参数 初始化、窗口 */
static struct fbtft_display display = {
.regwidth = 8, //IC 8bit
.width = WIDTH,
.height = HEIGHT,
.gamma_num = 2, //gamma的暂时默认
.gamma_len = 14,
.gamma = DEFAULT_GAMMA,
.fbtftops = {
.reset = reset,
.init_display = init_display,
.set_addr_win = set_addr_win,
},
};
FBTFT_REGISTER_DRIVER(DRVNAME, "zj,st7789v", &display); //兼容性compatible fbtft.h
//这个带参的宏定义在fbtft.h中,展开来就是基础驱动框架+spi/platform驱动框架
//注意:这里的匹配设备树ID匹配无效,刨根问底就知道了。
//有效的匹配就是name,device中spi->modalias : driver中DRVNAME
MODULE_ALIAS("spi:" DRVNAME);
MODULE_ALIAS("platform:" DRVNAME);
MODULE_ALIAS("spi:st7789v");
MODULE_ALIAS("platform:st7789v");
MODULE_DESCRIPTION("FB driver for the st7789v LCD display controller");
MODULE_AUTHOR("zxy");
MODULE_LICENSE("GPL");
图形界面配置
打开自己添加的驱动加入编译队列,/driver/staging/fbtft
:
- Kconfig
config FB_TFT_ST7789V
tristate "FB driver for the ST7789V LCD Controller"
depends on FB_TFT
help
Generic Framebuffer support for ST7789V
- Makefile
obj-$(CONFIG_FB_TFT_ST7789V) += fb_st7789v.o # 根据Kconfig 告诉编译器,我是否要得到这个文件
-
设置自己的驱动编译进内核
按“Y”,编译进内核。
-
禁止掉原子那个大LCD驱动编译
按“N”,禁止编译。
最后启动内核,会在板子上出现/dev/fb0节点,且显示终端。
遇到的问题:
1、就是那个fbtft_device.c中的开头的内部链接静态变量name没有说明,导致怎么改,都匹配不上,都逼得我在这些代码中加printk了,一次次启动内核,看看到底程序运行到哪,有没有进入probe函数。这个文件应该可以编译为模块,在终端输入参数启动,所以才有这种静态变量的。
2、解决了匹配问题,能进probe函数了,可是没有产生/dev/fbx节点,通过本人设置printk,耐心的观察内核启动信息,找到了错误地方,原来是设备描述中gpio请求失败。应该是fbtft_device.c有其他的驱动使用了这两个引脚,可是不是有匹配的吗,至今也有点儿迷,索性就把其他的引脚都注释了。
/sys/kernel/debug/gpio
可以查看板子上gpio使用情况。
3、结果还是不行,一“hai”解千愁,发现没有给驱动中的display结构体配置reset文件函数,而我的小屏幕这么脆弱,默认的延时肯定很短,承受不起。
看了默认的果然是,默认的中间20us,这咱的小屏幕哪反应的过来呀。
自己写一个吧,反应时间也得200ms,只要你写了的函数,他就不会使用默认值,还是很人性化的。
make V=1 all 2>info.log
可以将编译到终端中有error、warning等信息存到文件中便于查看。
最后我成功了,这次又没有给我劝退,哈哈哈