使用SPI子系统去点亮ST7789(1)
最近手头有一块ST7789 1.54寸的SPI屏幕,准备在泰山派(RK3566)上进行点亮,并且运行LVGL
操作流程
- 编写正确的SPI设备树
- 编写驱动
- 移植LVGL相关
以上就是运行的思路,先完成设备树和驱动相关的部分
SPI设备树
rk3566.dtsi->rk3568.dtsi,从而确定最终根设备树来自rk3568.dtsi,而 rk3566.dtsi是关闭一些在rk3566上没有的东西
我们可以看看rockchip对SPI设备树的写法
spi3: spi@fe640000 {
compatible = "rockchip,rk3066-spi";
reg = <0x0 0xfe640000 0x0 0x1000>;
interrupts = <GIC_SPI 106 IRQ_TYPE_LEVEL_HIGH>;
#address-cells = <1>;
#size-cells = <0>;
clocks = <&cru CLK_SPI3>, <&cru PCLK_SPI3>;
clock-names = "spiclk", "apb_pclk";
dmas = <&dmac0 26>, <&dmac0 27>;
dma-names = "tx", "rx";
pinctrl-names = "default", "high_speed";
pinctrl-0 = <&spi3m0_cs0 &spi3m0_cs1 &spi3m0_pins>;
pinctrl-1 = <&spi3m0_cs0 &spi3m0_cs1 &spi3m0_pins_hs>;
num-cs = <2>;
status = "disabled";
};
官方的dts是一家确定好了对spi的io映射和cs脚的数目选择,所以我们只需要在自己的设备树文件下开启spi和使用我们所需要的io就可以了
这是我写的设备树
&spi3 {
status = "okay";
max-freq = <80000000>;
dma-names = "tx","rx";
pinctrl-names = "default", "high_speed";
pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;
pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>;
spi_lcd@10 {
device_type = "spi_lcd";
compatible = "lcd-st7789";
spi-max-frequency = <80000000>;
reg = <0x00>;//cs single
//spi-cpha;
// spi-cpol;
status = "okay";
};
};
首先是开启spi的设备树节点,其次再追加时钟频率和我们所需要的io映射
然后再添加spi_lcd的子节点,用于驱动去匹配该节点
其中需要说明的是,reg属性是spi的cs信号,不同于单片机,soc上的spi往往会有多个cs来去控制多个spi设备,reg里的参数是该子节点使用哪个cs的io,我使用的是cs0,所以reg里的参数写0即可
设备树完成了,编译好后上传板子,确定在spi3的节点下有spi_lcd节点即可
SPI驱动编写
Makefile的编写参考我关于gpio子系统的章节,修改编译的文件和源码目录即可
接下来就是关于spi驱动的注册
首先是要使用 spi_register_driver,需要填入一个 struct spi_driver的结构体
然后我们再去填入 spi_driver所有需要的内容,分别是
- probe函数
- remove函数
- driver结构体
- id_table
其中非常重要的是driver当中的 of_match_table结构体,这个结构中的 compatible属性就是我们来匹配设备树节点的属性
这里不再赘述,贴上代码
驱动的init函数
/* 4.注册驱动函数 */
static int __init st7789_init(void)
{
int ret;
int err;
ret = spi_register_driver(&spi_st7789);
printk("start spi regist driver\n");
if(ret < 0){
printk("st7789 set up fail , ret = %d\n",ret);
return ret;
}
else{
ret = gpio_request(GPIO_ST7789_RST, "lcd-rst");
printk("start GPIO_ST7789_RST regist driver\n");
if(ret < 0){
//gpio get faile
spi_unregister_driver(&spi_st7789);
printk("gpio get faile , ret = %d",ret);
}
else{
gpio_direction_output(GPIO_ST7789_RST, 0);
ret = gpio_request(GPIO_ST7789_DC, "lcd-dc");
printk("start GPIO_ST7789_DC regist driver\n");
if(ret < 0){
//gpio get faile
spi_unregister_driver(&spi_st7789);
gpio_free(GPIO_ST7789_RST);
printk("gpio get faile , ret = %d",ret);
}
else{
gpio_direction_output(GPIO_ST7789_DC, 1);
/* 创建驱动类 */
st7789_class = class_create(THIS_MODULE, "st7789_class");
err = PTR_ERR(st7789_class);
if (IS_ERR(st7789_class)){
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
/* 注册驱动,led是驱动文件名称 */
major = register_chrdev(0, "st7789", &st7789_file_operations);
/* 设备创建 */
device_create(st7789_class,NULL,MKDEV(major, 0), NULL, "st7789_class");
printk("start LCD_Init\n");
LCD_Init();
printk("end LCD_Init\n");
}
}
}
printk("kfifo_avail=%d,kfifo_len=%d\n",kfifo_avail(&st7789_fifo),kfifo_len(&st7789_fifo));
th = kthread_run(st7789_flush_thread , 0 , "st7789_flush_thread");
if (IS_ERR(th)) {
printk("setup thread fail\r\n");
return -1;
}
return ret;
}
其中和kthread相关的可以先不看,是负责刷新屏幕的线程
spi_driver结构体
struct spi_driver spi_st7789 = {
.probe = st7789_probe,
.remove = st7789_remove,
.driver = {
.name = "st7789",//随便起
.owner = THIS_MODULE,
.of_match_table = st7789_of_match_table,
},
.id_table = st7789_id_table
};
probe函数
int st7789_probe(struct spi_device *spi){
spi_dev = spi;
printk("st7789_probe\n");
printk("st7789 cs = %d,max_speed_hz = %d,mode=%d\n",spi_dev->cs_gpio,spi_dev->max_speed_hz,spi_dev->mode);
return 0;
}
remove函数
int st7789_remove(struct spi_device *spi){
return 0;
}
spi_device_id结构体
const struct spi_device_id st7789_id_table[] = {
{"st7789"},
{}
};
of_device_id结构体
const struct of_device_id st7789_of_match_table[] = {
{.compatible = "lcd-st7789"},//需要和设备节点中的名字一样
{}//表示match_table结束
};
LCD屏幕的初始化
和spi相关的内容改好了,接下来是修改和屏幕相关的驱动,驱动的源码可以从相关的屏幕厂家得到,但是大多数都是正对单片机的,但是我们完成和spi相关的内容后,其实只需要修改和屏幕相关的spi发送接口,即可完成对应的操作
spi发送需要几个步骤
- 1.写入发送内容
- 2.将发送内容写入spi_message
- 3.同步发送message
这里直接贴上我修改的驱动内容
#define LCD_W 240
#define LCD_H 240
#define USE_HORIZONTAL 0
#define DC_Set() gpio_set_value(GPIO_ST7789_DC,1)
#define DC_Clr() gpio_set_value(GPIO_ST7789_DC,0)
#define RST_Clr() gpio_set_value(GPIO_ST7789_RST,0)
#define RST_Set() gpio_set_value(GPIO_ST7789_RST,1)
void LCD_Writ_Bus(uint8_t dat)
{
spi_write(spi_dev , &dat , 1);
}
void LCD_WR_DATA8(uint8_t dat)
{
DC_Set();
LCD_Writ_Bus(dat);
}
static inline void LCD_WR_DATA32(uint8_t* dat)
{
int ret;
struct spi_message m;
struct spi_transfer t = {
.tx_buf = dat,
.len = 4,
};
DC_Set();
spi_message_init(&m);/* 初始化 spi_message */
spi_message_add_tail(&t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
ret = spi_sync(spi_dev, &m);/* 同步传输 */
}
static inline void LCD_WR_REG(uint8_t dat)
{
int ret;
struct spi_message m;
struct spi_transfer t = {
.tx_buf = &dat,
.len = 1,
};
printk("dc clr\n");
DC_Clr();
spi_message_init(&m);/* 初始化 spi_message */
spi_message_add_tail(&t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
ret = spi_sync(spi_dev, &m);/* 同步传输 */
printk("sync result = %d\n",ret);
}
static inline void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
uint8_t x_value[4];
uint8_t y_value[4];
x_value[0] = x1>>8;
x_value[1] = x1;
x_value[2] = x2>>8;
x_value[3] = x2;
y_value[0] = y1>>8;
y_value[1] = y1;
y_value[2] = y2>>8;
y_value[3] = y2;
//x cmd
LCD_WR_REG(0x2a);
//x value
LCD_WR_DATA32(x_value);
//y cmd
LCD_WR_REG(0x2b);
//y value
LCD_WR_DATA32(y_value);
//end cmd
LCD_WR_REG(0x2c);
}
到这一步,ST7789的初始化已经结束了,接下来就是修改对刷屏部分进行优化,为移植lvgl打下基础
linux中spi的发送耗费的时间大多数都是组帧和打包,写入硬件fifo,这些过程相当的花费时间,但是spi发送的过程占用的时间却很少,所以刷新屏幕的时候,我们直接写入一整个屏幕的数据,或者缓存多帧的屏幕数据一起发出去,这样效率就比较高
直接贴代码
static void LCD_Fill(uint8_t* buf , int size)
{
unsigned long old_ns,now_ns;
int ret;
struct spi_message m;
int queue_length;
int i;
int ava_len;
struct spi_transfer* t = (struct spi_transfer*)vzalloc(sizeof(struct spi_transfer)*10);
#define one_buf_size 0xffff
queue_length = size/one_buf_size + 1;
printk("queue_length=%d\n",queue_length);
ava_len = size;
old_ns= ktime_get();
DC_Set();
spi_message_init(&m);/* 初始化 spi_message */
for(i = 0 ; i < queue_length ; i++)
{
t[i].tx_buf = &buf[i*one_buf_size];
if(ava_len > one_buf_size) t[i].len = one_buf_size;
else t[i].len = ava_len;
ava_len = ava_len - one_buf_size;
spi_message_add_tail(&t[i], &m);/* 将 spi_transfer 添加到 spi_message 队列 */
}
ret = spi_sync(spi_dev, &m);/* 同步传输 */
now_ns = ktime_get();
vfree(t);
printk("tranmit size = %d , tranmit tim = %d , ret=%d\n", size , (int)(now_ns - old_ns),ret);
}
驱动部分相关的代码完成,下章将关于LVGL的移植