CLP嵌入式linux-如何驱动一块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等信息存到文件中便于查看。

最后我成功了,这次又没有给我劝退,哈哈哈

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值