DRM驱动移植spi显示屏(st7789芯片驱动)

引言

        本篇博客介绍了使用DRM驱动开发spi屏幕的开发过程。主要包括:

        1、spi框架驱动屏幕

        2、DRM虚拟驱动

        3、DRM开发spi屏幕显示驱动

一、总体介绍

        1. 学习DRM也点时间了,前段时间被其他事情耽误了,最近才有时间写一下DRM驱动。本篇博客主要介绍了spi屏幕的驱动、虚拟DRM驱动、spi屏幕添加到DRM驱动中,下面分别介绍。     

        2. DRM驱动是linux显示子系统,一个比较复杂的驱动子系统,博主也仅仅只是一些了解,博客里面记录的是我自己对他学习的过程。具体的可以参考博主其他的文章或者其他博主的文章。这里放一下博主以前写的介绍DRM驱动的链接。DRM驱动介绍

        之前对他的学习很不充分,从这篇文章开始,将记录DRM驱动移植到spi接口的st7789屏幕上。

        3. st7789是一个显示芯片,博主使用的是spi接口的,分辨率为240×240,七针接口,3.3v供电。分别是:电源:gdn、vcc;spi通信:sck、sda;  复位:res;dc:数据命令切换;blk:背光。这里放一下图片(防止广告嫌疑,只有图片,具体可以某宝搜一下就有了)。

DRM驱动移植spi显示屏(st7789芯片驱动)_第1张图片

         4. 博主用的环境:

                虚拟机:ubuntu18

                硬件:stm32mp157(某原子的开发板) + spi屏幕

                开发板内核版本:Linux 5.4.30(开发板带的)

二、使用spi框架驱动spi屏幕

                在设备树中,添加spi屏幕的部分,这里给出我自己的设备节点写的代码。

spidev: htqst7789@0 {
        compatible = "htq,st7789";
        reg = <0>; /* CS #0 */
        scl-gpio = <&gpiof 6  GPIO_ACTIVE_LOW>;
        sda-gpio = <&gpiof 7  GPIO_ACTIVE_LOW>;
        res-gpio = <&gpiof 8  GPIO_ACTIVE_LOW>;
        dc-gpio  = <&gpiof 9  GPIO_ACTIVE_LOW>;
        blk-gpio = <&gpiof 10 GPIO_ACTIVE_LOW>;
        spi-max-frequency = <8000000>;
    };

        最开始,使用的是模拟的spi协议,所以设备节点就用了这个,后面改成硬件spi时,把scl、sda接到硬件spi引脚上,res、dc、blk均没动。这部分跟之前使用spi框架驱动icm20608类似,spi框架部分,详细可以参考博主以前写的spi框架部分。spi框架驱动icm20608,想深入了解spi框架运行部分的,也可以参考博主的这个博文。spi框架分析

        驱动部分也是类似,将之前写的代码拿过来改动下,不同之处在于spi的驱动方式。st7789使用的是SPI_MODE_3,需要配置好,其他的基本类似。

spi->mode = SPI_MODE_3;
spi_setup(spi);

这里将博主写的spi驱动st7789的底层读写函数放一下,代码只是用于学习用的,很多未曾考虑到,比如互斥之类的。

struct st7789_device{
	dev_t                dev_id;             //设备号
	int                  major;              //主设备号
	int                  minor;              //次设备号
	struct cdev	         cdev;               //字符设备
	struct class         *class;             //类
	struct device        *device;            //设备
	struct device_node   *device_node;       //设备节点
	
	void                 *privative_data;    //私有数据
	int                  scl_gpio;           
    int                  sda_gpio;           
    int                  res_gpio;           
    int                  dc_gpio;            
    int                  blk_gpio;           
};

static struct st7789_device  st7789_device; 

#define SPI_RST_L()  { gpio_set_value(st7789_device.res_gpio, 0);}
#define SPI_RST_H()  { gpio_set_value(st7789_device.res_gpio, 1);}

#define SPI_DC_L()   { gpio_set_value(st7789_device.dc_gpio, 0);}
#define SPI_DC_H()   { gpio_set_value(st7789_device.dc_gpio, 1);}

#define SPI_BLK_H() {;}   //背光直接接到3.3v了


void st7789_spi_send_byte(unsigned  char byte)				
{  
	int ret = 0;
	unsigned char tx_data[1];
	
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)st7789_device.privative_data;

	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
	if(t == NULL){
		return -1;
	}
	
	//先发送寄存器地址,后发送数据
	tx_data[0] = byte;  //写数据的时候寄存器地址bit8要清零
	t->tx_buf = tx_data;       //要发送的数据
	t->len = 1;  

	spi_message_init(&m);        //初始化spi消息
	spi_message_add_tail(t,&m);   //将要发送的数据添加到message消息队列
	ret = spi_sync(spi,&m);       //发送数据
	kzfree(t);
}

void st7789_send_cmd(unsigned char cmd)
{
	SPI_DC_L();
  st7789_spi_send_byte(cmd);
}

void st7789_send_data(unsigned  char data)
{ 
	SPI_DC_H();
	st7789_spi_send_byte(data);   
}

void st7789_send_color(uint16_t color)
{ 
	SPI_DC_H();

	int ret = 0;
	unsigned char g_tx_data[2];
	
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)st7789_device.privative_data;

	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
	if(t == NULL){
		return -1;
	}	

    g_tx_data[0] = color>>8; 
	g_tx_data[1] = color;
	t->tx_buf = g_tx_data;       //要发送的数据
	t->len = sizeof(g_tx_data);  
	spi_message_init(&m);        //初始化spi消息
	spi_message_add_tail(t,&m);   //将要发送的数据添加到message消息队列
	ret = spi_sync(spi,&m);       //发送数据
	
	kzfree(t);  
}

        剩下的,对屏幕的寄存器进行初始化什么的就不放了,网上一大堆,也可以用资料里面提供的,博主的这个代码本身就是在单片机的环境下调试好之后,再拿过来改成Linux下的。

        到这里基本上spi屏幕就能正常驱动了,但是,这样写速度十分十分慢。对spi框架熟悉的同学应该知道,使用spi框架发送数据,需要配置spi_transfer,将其放到spi_message中,最终放到spi队列中发送。spi框架会调用spi_pump_messages这个内核线程发送数据,在这个函数里面,调用ctrl->transfer_one_messgae完成最终的发送,并将发送结果返回。这个过程小量数据还可以接受,但spi刷新一帧需要240×240×2B=115200B,对于硬件spi来说,这个数据量并不高。但使用spi框架一次只发生2B,中间框架消耗太多性能,因此需要改动这部分代码,简单的说就是一次多发生一些数据。在这里,博主又添加了GRAM用于存放一帧屏幕的显示数据。改动之后的发送函数如下:

uint16_t* st7789_gram;
st7789_gram = kmalloc(2 * 240 *240, GFP_KERNEL);  //probe函数中分配内存


inline void st7789_draw_point(int x, int y,uint16_t color)
{
	uint16_t c = 0;	  //将颜色数据改成spi屏幕的
	c = color << 8;
	c |= (color>>8 & 0x00ff);
	st7789_gram[y * 240 + x] = c; 

}

void st7789_full_color(unsigned int color)
{
  unsigned int x,y;	

  for(y = 0;y < 240; y++){           
	for(x = 0;x < 240 ; x++){		
		st7789_draw_point(x,y,color);
    }
  }
}

//发送n行数据
void st7789_send_lines(uint16_t* color, int n)
{
	SPI_DC_H();

	int ret = 0;

	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)st7789_device.privative_data;

	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
	if(t == NULL){
		return -1;
	}	

	t->tx_buf = color;       //要发送的数据
	t->len = n * 2 * 240;  
	spi_message_init(&m);        //初始化spi消息
	spi_message_add_tail(t,&m);   //将要发送的数据添加到message消息队列
	ret = spi_sync(spi,&m);       //发送数据

	kzfree(t);  

}

void st7789_refresh(void)
{ 
  int n = 120, x, y;
  st7789_send_cmd(0x2a);     //Column address set
  st7789_send_data(0x00);    //start column
  st7789_send_data(0x00); 
  st7789_send_data(0x00);    //end column
  st7789_send_data(0xF0);

  st7789_send_cmd(0x2b);     //Row address set
  st7789_send_data(0x00);    //start row
  st7789_send_data(0x00); 
  st7789_send_data(0x00);    //end row
  st7789_send_data(0xF0);
  st7789_send_cmd(0x2C);     //Memory write

  st7789_send_lines((uint16_t *)(&st7789_gram[0]), 120);   
  st7789_send_lines((uint16_t *)(&st7789_gram[1  * 240 * 120]), 120);   

}

这里实测,一次spi_transfer无法发送完一帧数据量,因此,改成两次发送,速度比之前快多了。放一张spi屏幕驱动效果图。

DRM驱动移植spi显示屏(st7789芯片驱动)_第2张图片

 到这里,使用Linux下的spi框架驱动st7789显示芯片基本完成。

三、DRM框架

        drm驱动很复杂,kms部分主要有一下部分组成:   DRM驱动移植spi显示屏(st7789芯片驱动)_第3张图片

 写DRM驱动,主要也是围绕这几部分来做。

1、最简单的drm驱动

        将之前的spi代码改动下,在spi框架下添加drm框架。在spi probe函数里面,注册drm:

static const struct file_operations htq_st7789_driver_fops = {
    .owner		= THIS_MODULE,
    .open		= drm_open,
    .release	= drm_release,
    .unlocked_ioctl	= drm_ioctl,
    .compat_ioctl	= drm_compat_ioctl,
    .poll		= drm_poll,
    .read		= drm_read,
    .llseek		= noop_llseek,
    .mmap		= drm_gem_cma_mmap,
};

static struct drm_driver htq_st7789_driver = {
    .name = "htq_st7789",
    .desc = "htq drm st7789 driver by htq",
	.date = "20220401",
	.major = 1,
	.minor = 0,
    .fops			= &htq_st7789_driver_fops,
};
static int st7789_probe(struct spi_device *spi)
{
	int ret = 0;
    struct device *dev = &spi->dev;
    struct drm_device *ddev;

    ddev = drm_dev_alloc(&htq_st7789_driver, dev);  //分配一个drm_device结构体
    drm_dev_register(ddev, 0);  //注册drm

	return 0;

}

这样,一个最简单的DRM驱动就完成了(可能还可以再精简,博主未测试),这个驱动什么都不能做,只是演示了drm驱动,将驱动insmod到内核之后,可以使用ls /dev/dri/card0看到有这个节点。

 使用cat /sys/kernel/debug/dri/0/name可以看到,htq_st7789是前面设置的drm驱动名字。

 加载了DRM驱动之后,会在/dev/dri/下面生成对应的card0,用于用户空间应用程序打开设备,控制驱动。驱动加载进入之后,drm会自动生成如下节点(这部分参考别人的):

/dev/dri/card0

/sys/kernel/debug/dri/0

/sys/class/drm/card0

2、添加objects

        最简单的DRM驱动什么也做不了,需要添加plane、crtc、encoder、plane等objects才能完成对应的功能。各个objects什么意思,这里就不详细说明了,具体的请看博客上面,有博主的介绍这部分的博客链接。先放代码。

struct st7789_device {
	struct drm_device drm;
    struct drm_plane primary;
    struct drm_crtc crtc;
    struct drm_encoder encoder;
    struct drm_connector connector;
    struct hrtimer vblank_hrtimer;

};

int st7789_dumb_create(struct drm_file *file, struct drm_device *dev,
		     struct drm_mode_create_dumb *args)
{
	unsigned int min_pitch = DIV_ROUND_UP(args->width * args->bpp, 8);

	args->pitch = roundup(min_pitch, 128); //128 Byte对齐,优化传输
	args->height = roundup(args->height, 4);
	//调用CMA API中的函数创建显存
	return drm_gem_cma_dumb_create_internal(file, dev, args);
}

static struct drm_driver htq_st7789_driver = {
    .driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC,
    .name = "htq_st7789",
    .desc = "htq drm st7789 driver by htq",
	.date = "20220401",
	.major = 1,
	.minor = 0,
    .fops			= &htq_st7789_driver_fops,

    .dumb_create	= st7789_dumb_create,
    .gem_vm_ops		= &drm_gem_cma_vm_ops,
	.gem_free_object_unlocked = drm_gem_cma_free_object, 
};

//初始化plane、crtc、encoder、connector
static int st7789_modeset_init(struct st7789_device *st7789_device)
{
	struct drm_device *dev = (struct drm_device *)st7789_device->drm;
    int ret = 0;
	drm_mode_config_init(dev);
	dev->mode_config.funcs = &st7789_mode_funcs;   //modeset回调函数
	dev->mode_config.min_width = 0;       //显示区域的最大、最小范围 
	dev->mode_config.min_height = 0;
	dev->mode_config.max_width = 240;
	dev->mode_config.max_height = 240;
	dev->mode_config.preferred_depth = 16;        //颜色深度,16位
	dev->mode_config.helper_private = &st7789_mode_config_helpers;  //helper回调函数


    //初始化plane
    ret = drm_universal_plane_init(dev, &st7789_device->primary, 0, &st7789_plane_funcs, 
                    st7789_formats, ARRAY_SIZE(st7789_formats), 
                    NULL, DRM_PLANE_TYPE_PRIMARY, NULL);  //主图层

    //初始化crtc
    printk("drm_crtc_init_with_planes\n");
    ret = drm_crtc_init_with_planes(dev, &st7789_device->crtc, &st7789_device->primary, NULL, &st7789_crtc_funcs, NULL);

    //初始化encoder
    ret = drm_encoder_init(dev, &st7789_device->ncoder, &st7789_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);  //虚拟的encoder
    
	//初始化connector
    ret = drm_connector_init(dev, &st7789_device->connector, &st7789_connector_funcs, DRM_MODE_CONNECTOR_SPI);

    ret = drm_connector_attach_encoder(&st7789_device->connector, &st7789_device->encoder);

	drm_mode_config_reset(dev);
    return 0; //vkms_output_init(vkmsdev, 0);
};

        这部分太多了,简单的说下就是,对plane、crtc、encoder、connector进行初始化,添加对应的回调函数,回调函数里面写的才是真正的驱动底层显示器的函数(这里就是spi屏幕部分)。实际上,这里初始化的只是标准的objects,还有一些xxx_helper_func函数未曾添加进去。这里将驱动加载进去之后会看到这样的字符:

 

3、添加xxx_helper_func和atomic_xxx相关代码

        上述代码只是将标准的objects添加到代码中,实际上,DRM框架还需要具体的Soc、屏幕相关的代码,这部分代码DRM框架中留下回调函数,使用xxx_helper_func相关函数注册到DRM框架中。除此之外,还需要atomic_xxx部分,这里博主都写在这里了。

这里放一个完整的,比较多,而且由于博主是写在多个文件里面的,可能比较乱(忍一下吧0^0)。

static const struct drm_plane_funcs st7789_plane_funcs = {
	.update_plane		= drm_atomic_helper_update_plane,
	.disable_plane		= drm_atomic_helper_disable_plane,
	.destroy			= drm_plane_cleanup,
	.reset				= drm_atomic_helper_plane_reset,
	.atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
	.atomic_destroy_state	= drm_atomic_helper_plane_destroy_state,
};

static const struct drm_plane_helper_funcs st7789_plane_helper_funcs = {
	.atomic_update		= st7789_plane_atomic_update,
};

static const struct drm_crtc_funcs st7789_crtc_funcs = {
	.set_config = drm_crtc_helper_set_config,
	.page_flip = st7789_crtc_page_flip,
	.destroy = drm_crtc_cleanup,
	.reset                  = drm_atomic_helper_crtc_reset,
	.atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
	.atomic_destroy_state   = drm_atomic_helper_crtc_destroy_state,
};

static const struct drm_crtc_helper_funcs st7789_crtc_helper_funcs = {
	.atomic_enable	= st7789_crtc_atomic_enable,
	.atomic_disable	= st7789_crtc_atomic_disable,
	.atomic_flush	= st7789_crtc_atomic_flush,
};

static const struct drm_connector_funcs st7789_connector_funcs = {
	.fill_modes = drm_helper_probe_single_connector_modes,
	.destroy = drm_connector_cleanup,
	.reset = drm_atomic_helper_connector_reset,
	.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
	.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,

};

static const struct drm_connector_helper_funcs st7789_connector_helper_funcs = {
	.get_modes = st7789_connector_get_modes,
};

static const struct drm_encoder_funcs st7789_encoder_funcs = {
	.destroy = drm_encoder_cleanup,
};


//modeset初始化
static int st7789_modeset_init(struct st7789_device *st7789_device)
{
	struct drm_device *dev = (struct drm_device *)st7789_device->drm;
    int ret = 0;
	drm_mode_config_init(dev);
	dev->mode_config.funcs = &st7789_mode_funcs;   //modeset回调函数
	dev->mode_config.min_width = 0;       //显示区域的最大、最小范围 
	dev->mode_config.min_height = 0;
	dev->mode_config.max_width = 240;
	dev->mode_config.max_height = 240;
	dev->mode_config.preferred_depth = 16;        //颜色深度,16位
	dev->mode_config.helper_private = &st7789_mode_config_helpers;  //helper回调函数


    //初始化plane
    ret = drm_universal_plane_init(dev, &st7789_device->primary, 0, &st7789_plane_funcs, 
                    st7789_formats, ARRAY_SIZE(st7789_formats), 
                    NULL, DRM_PLANE_TYPE_PRIMARY, NULL);  //主图层
	drm_plane_helper_add(&st7789_device->primary, &st7789_plane_helper_funcs);


    //初始化crtc
    ret = drm_crtc_init_with_planes(dev, &st7789_device->crtc, &st7789_device->primary, NULL, &st7789_crtc_funcs, NULL);
    drm_crtc_helper_add(&st7789_device->crtc, &st7789_crtc_helper_funcs);

    //初始化encoder
    ret = drm_encoder_init(dev, &st7789_device->ncoder, &st7789_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);  //虚拟的encoder

	//初始化connector
    ret = drm_connector_init(dev, &st7789_device->connector, &st7789_connector_funcs, DRM_MODE_CONNECTOR_SPI);

    drm_connector_helper_add(&st7789_device->connector, &st7789_connector_helper_funcs);
    ret = drm_connector_attach_encoder(&st7789_device->connector, &st7789_device->encoder);

	drm_mode_config_reset(dev);

    return 0; 
};

上面代码中st7789_xxx部分,是需要我们手动实现的,跟具体的屏幕有关系,我这里就不放代码里,相关函数都是空的。无非就是写好对应的回调函数,将函数放到对应的结构体里面,将结构体注册到对应的objects里面,说着感觉很简单。将驱动调试好,insmod到内核之后,会出现这样的字段:

DRM驱动移植spi显示屏(st7789芯片驱动)_第4张图片

 出现了一些小问题,没有vblank和crtc什么的,这个正常,因为还没有完善这个驱动,下面将其完善。

4、完善的DRM虚拟驱动

         整个DRM虚拟驱动实际上参考了vkms写的,博主根据自己的理解和需要,重写了这个部分。

注:未介绍drm_xxx_funcs、drm_xxx_helper_funcs里面需要自己写的回调函数具体做什么了,大概介绍下,并填写代码。

三、spi屏幕添加到DRM驱动

        到这里,我们已经完成了spi屏幕的驱动和DRM虚拟驱动,剩下的需要将二者结合起来。写了DRM虚拟驱动之后,相信对各个objects做什么、各个drm_xxx_funcs和drm_xxx_helper_funcs做什么有个比较清晰的理解了,剩下的就是将drm_xxx_funcs、drm_xxx_helper_funcs里面的各个回调函数写好,调试好。

        后面发现了drm_mipi_dbi.c是专门为spi等接口屏幕出的drm框架,这一部分代码重写,参考了内核的drm_mipi_dbi.c开发的,mipi_dbi框架支持spi接口的屏幕。我发现我想写的,人家已经写好了,0.0,可以读一下这个代码,1k行多点。

1、plane

static const struct drm_plane_funcs st7789_plane_funcs = {
	.update_plane		= drm_atomic_helper_update_plane,
	.disable_plane		= drm_atomic_helper_disable_plane,
	.destroy			= drm_plane_cleanup,
	.reset				= drm_atomic_helper_plane_reset,
	.atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
	.atomic_destroy_state	= drm_atomic_helper_plane_destroy_state,
};

这里的st7789_plane_funcs 结构体基本上用的都是drm提供的api写的,vkms对后面三个成员重写了,博主参考了其他的驱动,可以直接用drm_atomic_helper_plane_xxx写。

2、crtc

struct drm_crtc_helper_funcs st7789_crtc_helper_funcs = {

	.mode_valid = st7789_crtc_mode_valid,    //检查模式是否支持
	.mode_fixup = st7789_crtc_mode_fixup,   //验证模式
	.mode_set_nofb = st7789_crtc_mode_set_nofb,

	.atomic_enable	= st7789_crtc_atomic_enable,
	.atomic_disable	= st7789_crtc_atomic_disable,
	.atomic_flush	= st7789_crtc_atomic_flush,
};

struct drm_crtc_funcs st7789_crtc_funcs = {
	.set_config = drm_crtc_helper_set_config,
	.destroy = drm_crtc_cleanup,
	.page_flip = drm_atomic_helper_page_flip,
	.reset                  = drm_atomic_helper_crtc_reset,
	.atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
	.atomic_destroy_state   = drm_atomic_helper_crtc_destroy_state,
	// .enable_vblank		= st7789_enable_vblank,
	// .disable_vblank		= st7789_disable_vblank,
};

enable_vblank、disable_vblank使能/关闭消影,这个暂时没有用到。

3、encoder

struct drm_encoder_funcs st7789_encoder_funcs = {
	.destroy = drm_encoder_cleanup,
};

4、connector

struct drm_connector_helper_funcs st7789_connector_helper_funcs = {
	.get_modes = st7789_connector_get_modes,
};


struct drm_connector_funcs st7789_connector_funcs = {
	.fill_modes = drm_helper_probe_single_connector_modes,
	.destroy = drm_connector_cleanup,
	.reset = drm_atomic_helper_connector_reset,
	.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
	.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,

};

st7789_connector_get_modes这个应该是比较重要的了,用于获取屏幕的参数,是drm_display_mode结构体,看其他博客的说明,这个东西不能仅仅理解为一些屏幕参数,需要理解为屏幕的时序,想来应该是和屏幕通信、显示时用到了。回调函数里面使用drm_mode_probed_add(connector, mode);将drm_display_mode添加到connector中。

四、总结

        DRM开发还算比较好理解:将KMS中几个obj初始化,包括plane、crtc、encoder、connector等,之后将对应的回调函数写入注册到系统中,就像上面提到的,包括drm_xxx_funcs、drm_xxx_helper_funcs。这次开发未涉及到内存方面,都是用CMA相关函数,博主对这部分还不太了解,下一次再更新相关的。

参考驱动:

        vkms.c 、drm_mipi_dbi.c、ili9341.c,vkms时Linux虚拟驱动,后面两个跟spi屏幕有关系,ili9341时spi接口的屏幕。

你可能感兴趣的:(linux驱动学习,驱动开发,linux,arm开发,嵌入式硬件)