ST7789 SPI TFT KMOD 开发记录

便宜的小分辨率TFT, 能显示点东西. 垃圾中景园的模块不带CS, 改线出来接触不太好, STM32能用换到Linux上就用不了. 随便买过了一块带CS的放上去好了(

 

外设准备

一块寄存器还算蛮复杂的芯片. 下次用Regmap API玩玩.
照数据手册包装所有寄存器操作到函数操作. 留空写入寄存器的操作.
st7789.h
st7789.c

模块基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/err.h>

#include <linux/slab.h>
#include <linux/of.h>
#include <linux/spi/spi.h>

#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/fs.h>
#include <linux/delay.h>

#include "linux/types.h"
#include "linux/device.h"

#include "linux/fb.h"
#include "linux/vmalloc.h"

#include "ST7789.h"

#define speed_w 50000000 // V3s maxium spi speed 60MHz

MODULE_LICENSE("GPL");

OF匹配表

1
2
3
4
5
6
7
8
9
10
11
static const struct of_device_id st7789_spi_ids[] = {
{.compatible = "sitronix,st7789"},
{},
};

static const struct spi_device_id st7789_spi_id [] = {
{"st7789", 0},
{},
};
MODULE_DEVICE_TABLE(spi, st7789_spi_id);

1
2
3
4
5
6
7
8
9
10
11
&spi0 {
st7789: st7789@0{
status = "disabled";
compatible = "sitronix,st7789";
spi-max-frequency = <50000000>;
reg = <0>;
size = /bits/16 <128 128>;
rst-gpios = <&pio 1 6 GPIO_ACTIVE_HIGH>;
dc-gpios = <&pio 1 7 GPIO_ACTIVE_HIGH>;
};
};

SPI驱动表

1
2
3
4
5
6
7
8
9
10
11
12
static struct spi_driver st7789_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "st7789",
.of_match_table = of_match_ptr(st7789_spi_ids),
},
.probe = st7789_probe,
.remove = st7789_remove,
.id_table = st7789_spi_id
};

module_spi_driver(st7789_driver);

设备结构体

1
2
3
4
5
6
7
8
9
10
11
12
struct st7789_spi {	
struct gpio_desc *rst, *dc;
struct spi_device* spi;
struct fb_info *info;
struct st7789 scr; // defined in st7789.h
struct mutex lock;
dma_addr_t tx_buf_dma; // not used, but need to initialize framebuffer
uint8_t* tx_buf; // buffer for read
uint16_t* wr_buf; // buffer for write
u32 buffer_size;
bool dc_status; // data/command. save time.
};

ST7789.C extern 函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
static void st7789_send(uint8_t* data, uint32_t len, uint8_t dc){
struct spi_message msg = {0, };
struct spi_transfer xfer = {0, };

xfer.tx_nbits = 1;
xfer.tx_buf = data;
xfer.bits_per_word = 8;
xfer.len = len;
xfer.speed_hz = speed_w;

spi_message_init(&msg);
spi_message_add_tail(&xfer, &msg);
mutex_lock(&ins.lock);
// set DC to target value
gpiod_set_value(ins.dc, dc);
// send data
spi_sync(ins.spi, &msg);
mutex_unlock(&ins.lock);
}

void st7789_hw_rst(){
gpiod_set_value(ins.rst, 0);
mdelay(120); // 120ms wakeup from power down
gpiod_set_value(ins.rst, 1);
mdelay(120);
}

void st7789_wait_ms(long time){
mdelay(time);
}

int st7789_write_cmd_single(uint8_t cmd){
// send one data with dc pin LOW
st7789_send(&cmd, 1, 0);
return 0;
}
int st7789_write_data_single(uint8_t data){
// send one data with dc pin HIGH
st7789_send(&data, 1, 1);
return 0;
}
int st7789_write_data(void* data, long length){
// send data with DC pin HIGH
st7789_send(data, length, 1);
return 0;
}
int st7789_write_data16(void* data, long length){
// send 16bit BE data with DC pin LOW
register long i = length;
uint16_t* tmp2 = data;
uint16_t* wr_buf = ins.wr_buf;
while(i--)
wr_buf[i] = __builtin_bswap16(tmp2[i]);
st7789_write_data(wr_buf, length << 1);
return 0;
}
int st7789_write_data_dma(void* data, long length){
return st7789_write_data16(data, length >> 1);
}
// used when using fill function.
int st7789_write_same_data_dma(uint16_t data, long length){
uint16_t* pdata;
pdata = kmalloc(sizeof(uint16_t)*length, GFP_KERNEL);
pr_info("send...\n");
if(pdata == NULL) {
pr_info("nomem %ld\n", length);
return -ENOMEM;
}
data = __builtin_bswap16(data);
memset16(pdata, data, length);
st7789_send((uint8_t*)pdata, length<<1, 1);
kfree(pdata);
return 0;
}

FBDEV驱动声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
struct fb_ops st7789_fbops = {
.owner = THIS_MODULE,
.fb_read = st7789_buf_read,
.fb_write = st7789_buf_write,
/* refresh */
.fb_pan_display = st7789_pan_disp,
/* general purpose function */
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
};
static void st7789_deferred_io(struct fb_info *info, struct list_head *pagelist);
static int st7789_pan_disp(struct fb_var_screeninfo *var,
struct fb_info *info);
static int st7789_fbdev_init(struct st7789_spi *screen);


static struct st7789_spi ins= {0};
/* read/write function. */
static ssize_t st7789_buf_read(struct fb_info *info, char __user *buf,
size_t count, loff_t *ppos)
{
struct st7789_spi *ins = info->par;
if(*ppos + count > ins->buffer_size)
return -EINVAL;
if(copy_to_user(buf, ins->tx_buf + *ppos, count) != 0)
return -EFAULT;
return count;
}

static ssize_t st7789_buf_write(struct fb_info *info, const char __user *buf,
size_t count, loff_t *ppos)
{
struct st7789_spi *ins = info->par;
if(*ppos + count > ins->buffer_size)
return -EINVAL;
if(copy_from_user(ins->tx_buf + *ppos, buf, count) != 0)
return -EFAULT;
return count;
}
static void st7789_deferred_io(struct fb_info *info, struct list_head *pagelist)
{
struct st7789_spi *scr = info->par;
/* ignore partial refresh, just flush entire screen */
struct rect area = {
{0, 0},
{scr->scr.width - 1, scr->scr.height - 1}
};
/* external function in ST7789.c */
st7789_disp_buf_at(&area, scr->tx_buf);
}
/* flush screen */
static int st7789_pan_disp(struct fb_var_screeninfo *var,
struct fb_info *info)
{
st7789_deferred_io(info, NULL);
return 0;
}
static int st7789_fbdev_init(struct st7789_spi *screen)
{
struct st7789 *scr = &screen->scr;
struct fb_info *info;
struct fb_deferred_io *defio;
if(ins.tx_buf_dma == 0){
ins.buffer_size = scr->width*scr->height*2+100;
#if 0 // if not use deferred IO
ins.tx_buf = devm_kmalloc(&screen->spi->dev,
ins.buffer_size,
GFP_KERNEL);
#endif
ins.tx_buf = vzalloc(ins.buffer_size);
if(ins.tx_buf == NULL)
return -ENOMEM;
}
ins.info = devm_kzalloc(&screen->spi->dev,sizeof(struct fb_info),
GFP_KERNEL);

defio = devm_kzalloc(&screen->spi->dev,
sizeof(struct fb_deferred_io),
GFP_KERNEL);
/* copy from fbtft */
info = ins.info;

defio->delay = HZ;
defio->deferred_io = st7789_deferred_io;

info->screen_buffer = ins.tx_buf;
info->fbops = &st7789_fbops;
info->fbdefio = defio;
fb_deferred_io_init(info);

info->fix.type = FB_TYPE_PACKED_PIXELS;
info->fix.visual = FB_VISUAL_TRUECOLOR;
info->fix.xpanstep = 0;
info->fix.ypanstep = 0;
info->fix.ywrapstep = 0;
info->fix.line_length = scr->width * 2;
info->fix.accel = FB_ACCEL_NONE;
info->fix.smem_len = ins.buffer_size;

info->var.rotate = 0;
info->var.xres = scr->width;
info->var.yres = scr->height;
info->var.xres_virtual = info->var.xres;
info->var.yres_virtual = info->var.yres;
info->var.bits_per_pixel = 16;
info->var.nonstd = 1;

/* RGB565 */
info->var.red.offset = 11;
info->var.red.length = 5;
info->var.green.offset = 5;
info->var.green.length = 6;
info->var.blue.offset = 0;
info->var.blue.length = 5;
info->var.transp.offset = 0;
info->var.transp.length = 0;
info->flags = FBINFO_FLAG_DEFAULT | FBINFO_VIRTFB;
info->par = &ins;

return register_framebuffer(info);
}

note: deferred_io 可以实现局刷. 局刷前注意flush脏内存.

Probe & Remove

一般使用DMA, 设定好触发源. 可惜V3s在主线Linux的SPI驱动里面并没有添加DMA支持. 注册一小段DMA内存空间给fbdev使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
static int st7789_probe(struct spi_device *spi){
int err;
uint16_t buffer[2];
/* screen size */
struct rect area = {
{0, 0},
{120, 120}
};

ins.spi = spi;
ins.dc = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_HIGH);
ins.rst = devm_gpiod_get(&spi->dev, "rst", GPIOD_OUT_HIGH);

of_property_read_u16_array(spi->dev.of_node, "size", buffer, 2);

gpiod_set_value(ins.dc, 1);
gpiod_set_value(ins.rst, 1);
ins.scr.width = buffer[0] + 1;
ins.scr.height = buffer[1] + 1;
pr_info("screen size %d, %d\n", ins.scr.width, ins.scr.height);

/* check DMA, alloc DMA memory. Unfortunately V3s SPI drvier not suppore DMA */
if(dma_supported(&spi->dev, DMA_BIT_MASK(32)) < 0){
pr_err("dma not supported\n");
}
err = dma_set_mask(&spi->dev, DMA_BIT_MASK(32));
if(err){
pr_err("dma not supported. can't set mask : %d\n", err);;
}
else{
ins.tx_buf = dma_alloc_coherent(&spi->dev,
ins.scr.width*ins.scr.height*2,
&ins.tx_buf_dma,
GFP_KERNEL);
if(ins.tx_buf_dma)
ins.buffer_size = ins.scr.width*ins.scr.height*2;
}
/* setup SPI */
spi->mode = SPI_MODE_0;
spi->max_speed_hz = speed_w;
spi->bits_per_word = 8;
err = spi_setup(spi);
if(err < 0){
pr_err("spi init error %d\n", err);
return err;
}

pr_info("init st7789, dma addr %ld, %ld\n", (long)ins.tx_buf, (long)ins.tx_buf_dma);
ins.wr_buf = devm_kmalloc(&spi->dev,
ins.buffer_size,
GFP_KERNEL);

st7789_sw_rst();
st7789_init(&ins.scr);
/* using RGB mode the red channel is replaced with blud channel. dont know why. */
st7789_memory_access(MADCTL_BIT_ORDER_BGR);
/* RGB565 Black value 0x0000 */
st7789_fill(&area, 0x0000);
if(0)
st7789_fbdev_init(&ins);
return 0;
}
static int st7789_remove(struct spi_device *spi){
// fb_deferred_io_cleanup(ins.info);
if(ins.tx_buf_dma)
dma_free_coherent(&spi->dev,
ins.scr.width*ins.scr.height*2,
ins.tx_buf,
ins.tx_buf_dma);
// else if(ins.tx_buf)
vfree(ins.tx_buf);
// unregister_framebuffer(ins.info);
mutex_destroy(&ins.lock);
return 0;
}

Debug

实际一堆KernelOops和Segment Fault. 写内存空间超出边界这种事情加个判断就好了. Deferred IO必须使用虚拟内存空间.
使用ffmpeg直接对fb1写入基本没大问题.
没有DMA支持确实是个大问题啊, 下次自己写SPI的DMA支持(恼
垃圾中景园做模块不给留CS有点过分. 改线出来接触好像不太好, STM32能用Linux用不了. 买过一块等的时间都够写一个声卡了(
不使用deferred IO就没有显示, 看buffer确实是有写进去但是不进刷新函数. 没找到原因.
设置RGB565输出也是RGB565显示结果Red通道和Blue通道互换. 给屏幕配置成BGR565就好了. 实际上ffmpeg好像没办法往fbdev输出565格式的数据.


Ref: linux/drivers/staging/fbtft