赞
踩
本篇博客介绍了使用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:背光。这里放一下图片(防止广告嫌疑,只有图片,具体可以某宝搜一下就有了)。
4. 博主用的环境:
虚拟机:ubuntu18
硬件:stm32mp157(某原子的开发板) + spi屏幕
开发板内核版本:Linux 5.4.30(开发板带的)
在设备树中,添加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屏幕驱动效果图。
到这里,使用Linux下的spi框架驱动st7789显示芯片基本完成。
三、DRM框架
drm驱动很复杂,kms部分主要有一下部分组成:
写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
最简单的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函数未曾添加进去。这里将驱动加载进去之后会看到这样的字符:
上述代码只是将标准的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到内核之后,会出现这样的字段:
出现了一些小问题,没有vblank和crtc什么的,这个正常,因为还没有完善这个驱动,下面将其完善。
整个DRM虚拟驱动实际上参考了vkms写的,博主根据自己的理解和需要,重写了这个部分。
注:未介绍drm_xxx_funcs、drm_xxx_helper_funcs里面需要自己写的回调函数具体做什么了,大概介绍下,并填写代码。
到这里,我们已经完成了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接口的屏幕。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。