接线#
在开始之前,首先确定你的屏幕的引脚定义,下面是我买到的屏幕:
通过查阅店家给的屏幕资料,得出引脚定义如下:
引脚 | 定义 |
---|---|
BLK | 背光 |
CS | SPI 片选 |
DC | 屏幕数据 |
RES | 屏幕重置 |
SDA | SPI 数据 |
SCL | SPI 时钟 |
VCC | 电源 |
GND | GND |
通过 Milk-V Duo 的文档可以看出,Milk-V Duo 有一个硬件 SPI,除此之外需要两个引脚来接 DC
跟 RES
,这里我选择 GPIOA24 跟 GPIOA23,由于屏幕不需要输出数据,所以只需的数据引脚接到开发板的 SPI2_TX
即可。
这里我直接将 3V3 接到 BLK 引脚,无需 PWM 调光。
于是我的接线如下:
屏幕 开发板
BLK --- 3V3
CS --- SPI2_CSn
SDA --- SPI2_TX
SCL --- SPI2_CLK
DC --- GPIOA24
RES --- GPIOA23
修改引脚定义#
由于 Milk-V Duo 开发板的 SPI2 是复用引脚且默认功能不是 SPI2,所以需要修改引脚定义。
Milk-V Duo 的引脚定义在 build/boards/cv180x/cv1800b_milkv_duo_sd/u-boot/cvi_board_init.c
文件中的 cvi_board_init
内声明,这里我们直接把 SD1_CLK
, SD1_CMD
, SD1_D0
, SD1_D3
直接修改为 SPI2 的对应引脚(参考上面 Milk-V Duo 引脚定义图)。
PINMUX_CONFIG(SD1_CLK, SPI2_SCK); // Pin 9
PINMUX_CONFIG(SD1_CMD, SPI2_SDO); // Pin 10
PINMUX_CONFIG(SD1_D0, SPI2_SDI); // Pin 11
PINMUX_CONFIG(SD1_D3, SPI2_CS_X); // Pin 12
内核配置#
首先配置 device tree
:
&spi2 {
status = "okay";
/delete-node/ spidev@0;
st7789v: st7789v@0{
compatible = "sitronix,st7789v";
reg = <0>;
status = "okay";
spi-max-frequency = <48000000>;
spi-cpol;
spi-cpha;
rotate = <0>;
fps = <60>;
rgb;
buswidth = <8>;
dc = <&porta 24 GPIO_ACTIVE_HIGH>;
reset = <&porta 23 GPIO_ACTIVE_HIGH>;
debug = <0x0>;
};
};
这里把 dc
reset
设置成你自己接的 GPIO 端口号。
然后启用内核中的 ST7789V 支持跟 FB 支持,如果你跟我一样使用官方的 buildroot 的话可以直接修改 build/boards/cv180x/cv1800b_milkv_duo_sd/linux/cvitek_cv1800b_milkv_duo_sd_defconfig
这个文件。
这里我们不动别的配置,只增加 FB_TFT 的支持跟 ST7789V 的驱动:
CONFIG_FB_TFT=y
CONFIG_FB_TFT_ST7789V=y
接下来修改 cv180x 的 dtsi,文件默认在 build/boards/default/dts/cv180x/cv180x_base.dtsi
。
将 SPI 引脚默认上拉:
spi2:spi2@041A0000 {
compatible = "snps,dw-apb-ssi";
reg = <0x0 0x041A0000 0x0 0x10000>;
clocks = <&clk CV180X_CLK_SPI>;
#address-cells = <1>;
#size-cells = <0>;
bias-pull-up; // 上拉
};
修改驱动#
首先修改 linux_5.10/drivers/staging/fbtft/fbtft-core.c
中的 fbtft_request_one_gpio
函数,这里的修改依据 https://github.com/notro/fbtft/blob/e9fc10a080a6c52e46e004c4c2bc9fd3cf3d7445/fbtft-core.c#L166-L204
来,如果不修改在初始化屏幕时候会报错:
static int fbtft_request_one_gpio(struct fbtft_par *par,
const char *name, int index,
struct gpio_desc **gpiop)
{
struct device *dev = par->info->device;
struct device_node *node = dev->of_node;
int gpio, flags, ret = 0;
enum of_gpio_flags of_flags;
if (of_find_property(node, name, NULL))
{
gpio = of_get_named_gpio_flags(node, name, index, &of_flags);
if (gpio == -ENOENT)
return 0;
if (gpio == -EPROBE_DEFER)
return gpio;
if (gpio < 0)
{
dev_err(dev,
"failed to get '%s' from DT\n", name);
return gpio;
}
// active low translates to initially low
flags = (of_flags & OF_GPIO_ACTIVE_LOW) ? GPIOF_OUT_INIT_LOW : GPIOF_OUT_INIT_HIGH;
ret = devm_gpio_request_one(dev, gpio, flags,
dev->driver->name);
if (ret)
{
dev_err(dev,
"gpio_request_one('%s'=%d) failed with %d\n",
name, gpio, ret);
return ret;
}
*gpiop = gpio_to_desc(gpio);
fbtft_par_dbg(DEBUG_REQUEST_GPIOS, par, "%s: '%s' = GPIO%d\n",
__func__, name, gpio);
}
return ret;
}
同时在文件最上面引入头文件:
#include "fbtft.h"
#include "internal.h"
接下来修改 st7789v
的驱动,也就是 linux_5.10/drivers/staging/fbtft/fb_st7789v.c
,这里先修改 fbtft_display
中的屏幕配置,将 width 跟 height 改为你屏幕的大小:
static struct fbtft_display display = {
.regwidth = 8,
.width = 240,
.height = 280,
.gamma_num = 2,
.gamma_len = 14,
.gamma = HSD20_IPS_GAMMA,
.fbtftops = {
.init_display = init_display,
.set_var = set_var,
.set_gamma = set_gamma,
.blank = blank,
.set_addr_win = set_addr_win
},
};
接着按照屏厂给的资料,修改初始化函数:
static int init_display(struct fbtft_par *par)
{
par->fbtftops.reset(par);
mdelay(100);
/* turn off sleep mode */
write_reg(par, MIPI_DCS_EXIT_SLEEP_MODE);
mdelay(120);
/* set pixel format to RGB-565 */
write_reg(par, MIPI_DCS_SET_PIXEL_FORMAT, MIPI_DCS_PIXEL_FMT_16BIT);
write_reg(par, VCMOFSET, 0x1A);
if (HSD20_IPS)
write_reg(par, PORCTRL, 0x05, 0x05, 0x00, 0x33, 0x33);
else
write_reg(par, PORCTRL, 0x08, 0x08, 0x00, 0x22, 0x22);
/*
* VGH = 12.2V
* VGL = -10.43V
*/
if (HSD20_IPS)
write_reg(par, GCTRL, 0x05);
else
write_reg(par, GCTRL, 0x35);
/* VCOM */
write_reg(par, VCOMS, 0x3F);
/*
* VDV and VRH register values come from command write
* (instead of NVM)
*/
write_reg(par, VDVVRHEN, 0x01);
/*
* VRH = 4.3V + (VCOM + VCOM offset + VDV)
*/
if (HSD20_IPS)
write_reg(par, VRHS, 0x0F);
else
write_reg(par, VRHS, 0x0B);
/* VDV = 0V */
write_reg(par, VDVS, 0x20);
/* 111 Hz */
write_reg(par, FRC, 0x01);
/*
* AVDD = 6.8V
* AVCL = -4.8V
* VDS = 2.3V
*/
write_reg(par, PWCTRL1, 0xA4, 0xA1);
/* 卖屏幕的人给你参考代码里的 我也不知道哪里来的 */
write_reg(par, 0xE8, 0x03);
write_reg(par, ETC, 0x09, 0x09, 0x08);
write_reg(par, 0xE0, 0xD0, 0x05, 0x09, 0x09, 0x08, 0x14, 0x28, 0x33, 0x3F, 0x07, 0x13, 0x14, 0x28, 0x30);
write_reg(par, 0xE1, 0xD0, 0x05, 0x09, 0x09, 0x08, 0x03, 0x24, 0x32, 0x32, 0x3B, 0x14, 0x13, 0x28, 0x2F);
if (HSD20_IPS)
write_reg(par, MIPI_DCS_ENTER_INVERT_MODE);
write_reg(par, MIPI_DCS_SET_DISPLAY_ON);
return 0;
}
如果你的屏幕跟我一样大小与 linux kernel 默认不一样,那么还需要参考屏厂的资料设置屏幕 offset:
static void set_addr_win(struct fbtft_par *par, int xs, int ys, int xe,
int ye)
{
unsigned int x = xs, y = xe;
write_reg(par, MIPI_DCS_SET_COLUMN_ADDRESS,
(x >> 8), x, (y >> 8), y);
x = ys + 20;
y = ye + 20;
write_reg(par, MIPI_DCS_SET_PAGE_ADDRESS,
(x >> 8), x, (y >> 8), y);
write_reg(par, MIPI_DCS_WRITE_MEMORY_START);
}
检测屏幕是否工作#
如果你的操作没错的话,在上电 ssh 上去之后执行 dmesg | grep st7789v
应该可以看出 fb0 被正确的初始化了:
[root@milkv-duo]~# dmesg | grep st7789v
[ 0.813120] fb_st7789v spi0.0: fbtft_property_value: buswidth = 8
[ 0.819555] fb_st7789v spi0.0: fbtft_property_value: debug = 0
[ 0.825703] fb_st7789v spi0.0: fbtft_property_value: rotate = 0
[ 0.831919] fb_st7789v spi0.0: fbtft_property_value: fps = 60
[ 1.357745] graphics fb0: fb_st7789v frame buffer, 240x280, 131 KiB video memory, 4 KiB buffer memory, fps=62, spi0.0 at 48 MHz
执行 cat /dev/random > /dev/fb0
给 fb0 内写随机数据,这时候看屏幕应该是像这样的:
再执行 cat /dev/zero > /dev/fb0
,向 fb0 内全写 0,现在屏幕应该会回到黑屏状态。