本篇博客介绍了使用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驱动,主要也是围绕这几部分来做。
将之前的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接口的屏幕。