【米尔-芯驰D9国产车规级开发板】6、编写应用层st7789驱动
[复制链接]
本帖最后由 walker2048 于 2023-10-21 09:53 编辑
由于本次试用是计划做LVGL应用的,手上也没有适合这个板子的大屏幕,就先找个1.69寸的小彩屏做一下。
本次使用的屏幕是淘宝上常见的320x240分辨率的LCD彩屏,驱动芯片是st7789。模组图片如下图:
而米尔-芯驰D9国产车规级开发板上跑的是Linux系统,按常规的做法,就是修改设备树和配置fbdev驱动之类的。
这一次并不想这么玩,手上这个屏可以使用4线接线方式,仅仅使用SPI的两个输出引脚和两个GPIO作为CS和DC控制引脚,就能将屏幕点亮了。
D9开发板 |
LCD模组 |
SPI6_SCK |
SCL |
SPI6_MOSI |
SDA |
GPIO.IO80 |
CS |
GPIO.IO81 |
DC |
根据开发板文档里的说明,可以查到可用的引脚,我选择使用SPI6和GPIO.IO80\81这些引脚。
想着以后可以直接简单配置一下SPI设备,控制引脚就能点亮屏幕,这次就做个应用层点屏的驱动,现在就开始吧。
一、封装SPI和GPIO控制语句
既然是跑Linux了,我们就不考虑使用寄存器之类的,直接使用文件读写和ioctl的方式来封装控制语句,这样换了板子也能用。
spi的控制代码:
#include "disp_driver.h"
#include <fcntl.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <unistd.h>
int spi_device;
extern configuration config;
static int g_spifd = 0;
int dirver_spi_init() {
// 打开 SPI 设备
spi_device = open(config.disp_path, O_RDWR);
if (spi_device < 0) {
perror("Error opening SPI device");
return 1;
}
g_spifd = spi_device;
// 设置 SPI 模式、速率等属性
uint8_t mode = config.spi_mode;
uint32_t speed = config.spi_speed_mh * 1000 * 1000;
if (ioctl(spi_device, SPI_IOC_WR_MODE, &mode) < 0) {
perror("Error setting SPI mode");
close(spi_device);
return 1;
}
if (ioctl(spi_device, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) {
perror("Error setting SPI speed");
close(spi_device);
return 1;
}
return 0;
}
void dirver_spi_deinit() {
// 关闭 SPI 设备
close(spi_device);
g_spifd = 0;
}
void spi_write(void *data, uint16_t length) {
if (write(g_spifd, data, length) < 0) {
perror("Error in SPI transfer");
};
}
#define SPI_WRITE_SIZE 4096
void dirver_spi_send_data(void *data, uint16_t length) {
int32_t len = length;
while (len > 0) {
if (len < SPI_WRITE_SIZE) {
spi_write(data, len);
} else {
spi_write(data, SPI_WRITE_SIZE);
}
data = data + SPI_WRITE_SIZE;
len = len - SPI_WRITE_SIZE;
}
}
gpio的控制代码:
#include "stdint.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define GPIO_EXPORT_PATH "/sys/class/gpio/export"
#define GPIO_UNEXPORT_PATH "/sys/class/gpio/unexport"
#define GPIO_DIRECTION_PATH "/sys/class/gpio/gpio%d/direction"
#define GPIO_VALUE_PATH "/sys/class/gpio/gpio%d/value"
void gpio_set_direction(const uint8_t pin, const uint8_t direction) {
char path[100];
snprintf(path, sizeof(path), GPIO_DIRECTION_PATH, pin);
FILE *fd = fopen(path, "w");
if (fd == NULL) {
perror("Error opening GPIO direction file");
exit(1);
}
char dirstr[3];
if(direction == 1){
fprintf(fd, "in");
}else {
fprintf(fd, "out");
}
fflush(fd);
fclose(fd);
}
void gpio_set_level(const uint8_t pin, int value) {
char path[100];
snprintf(path, sizeof(path), GPIO_VALUE_PATH, pin);
FILE *fd = fopen(path, "w");
if (fd == NULL) {
perror("Error opening GPIO value file");
exit(1);
}
if (value == 0) {
fprintf(fd, "0");
} else {
fprintf(fd, "1");
}
fflush(fd);
fclose(fd);
}
int gpio_init(const uint8_t pin) {
int pin_export_fd = open(GPIO_EXPORT_PATH, O_WRONLY);
if (pin_export_fd < 0) {
perror("Error opening GPIO export file");
return 1;
}
char pin_name[3];
snprintf(pin_name, sizeof(pin_name), "%d", pin);
write(pin_export_fd, pin_name, strlen(pin_name));
close(pin_export_fd);
gpio_set_direction(pin, 1); // 设置 GPIO 引脚为输出模式
return 0;
}
int gpio_deinit(const uint8_t pin) {
int pin_unexport_fd = open(GPIO_UNEXPORT_PATH, O_WRONLY);
if (pin_unexport_fd < 0) {
perror("Error opening GPIO unexport file");
return 1;
}
char pin_name[3];
snprintf(pin_name, sizeof(pin_name), "%d", pin);
write(pin_unexport_fd, pin_name, strlen(pin_name));
close(pin_unexport_fd);
return 0;
}
二、使用ini配置文件来保存屏幕设置
上面我们提到,考虑到方便以后移植,那包括屏幕芯片、屏幕参数、设备引脚等等,均使用disp.ini文件存储。
程序运行时直接读取该文件,获取屏幕配置信息,然后再初始化屏幕和完成屏幕内容填充。
以下是disp.ini文件的示范样板:
; disp config file for lvgl panel
[panel] ; disp configuration
path=/dev/spidev6.0
ic=st7789
[pin] ; pin configuration
pin_cs=80
pin_dc=81
[spi] ; spi configuration
speed_mh=50
mode=0
[resolution] ; Resolution configuration
offset_x=0
offset_y=0
height=240
width=320
orientation=0
然后使用github上点赞数比较多的ini.c项目来实现ini文件解析,这样就实现了程序读取ini文件内容,并加载到全局变量config上。
extern configuration config;
static int handler(void *panel, const char *section, const char *name,
const char *value) {
configuration *pconfig = (configuration *)panel;
#define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
// find panel config
if (MATCH("panel", "path")) {
pconfig->disp_path = strdup(value);
} else if (MATCH("panel", "ic")) {
pconfig->disp_ic = strdup(value);
// find pin config
} else if (MATCH("pin", "pin_cs")) {
pconfig->pin_cs = atoi(value);
} else if (MATCH("pin", "pin_dc")) {
pconfig->pin_dc = atoi(value);
} else if (MATCH("pin", "bl_pwm")) {
pconfig->bl_pwm = atoi(value);
} else if (MATCH("pin", "pin_rst")) {
pconfig->pin_rst = atoi(value);
// find spi config
} else if (MATCH("spi", "speed_mh")) {
pconfig->spi_speed_mh = atoi(value);
} else if (MATCH("spi", "mode")) {
pconfig->spi_mode = atoi(value);
// find resolution config
} else if (MATCH("resolution", "offset_x")) {
pconfig->offset_x = atoi(value);
} else if (MATCH("resolution", "offset_y")) {
pconfig->offset_y = atoi(value);
} else if (MATCH("resolution", "width")) {
pconfig->width = atoi(value);
} else if (MATCH("resolution", "height")) {
pconfig->height = atoi(value);
} else if (MATCH("resolution", "orientation")) {
pconfig->orientation = atoi(value);
} else {
return 0; /* unknown section/name, error */
}
return 1;
}
三 、编写屏幕驱动序列和部分功能函数
好了,前期工作准备完毕,编写屏幕驱动代码。我们从esp32-lvgl的屏幕驱动库里,可以找到st7789的屏幕驱动代码。
这里对代码进行重写,定义一个屏幕初始化命令序列类型,添加延时时间属性。这样就可以在序列数组里直接定义延时的时间。
同时为了后期能增加不同的屏幕驱动,添加一个驱动api的数据结构。使用该结构来调用屏幕初始化的命令序列数组和屏幕api结构初始化函数。
/*The LCD needs a bunch of command/argument values to be initialized. They are
* stored in this struct. */
typedef struct {
uint8_t cmd;
uint8_t data[16];
uint8_t databytes; // No of data in data; bit 7 = delay after set; 0xFF =
// end of cmds.
uint8_t delaytime; // delaytime
} lcd_init_cmd_t;
typedef struct {
void (*disp_set_window)(lv_disp_drv_t *drv, const lv_area_t *area);
void (*disp_set_orientation)(uint8_t orientation);
lcd_init_cmd_t *disp_init_cmd;
} disp_driver_api;
然后就是把st7789屏幕的初始化序列数组给完善好,以下是相关代码。
lcd_init_cmd_t st7789_init_cmds[] = {
{0x01, {0}, 0x80, 120},
/* Sleep Out */
{0x11, {0}, 0x80, 120},
/* Memory Data Access Control, MX=MV=1, MY=ML=MH=0, RGB=0 */
{0x36, {0x00}, 1},
/* Interface Pixel Format, 16bits/pixel for RGB/MCU interface */
{0x3A, {0x05}, 1},
#if 0
{0x30, {0x00,0x50,0x01,0x3F}, 4},
{0x12, {0x00}, 0},
#endif
/* Porch Setting */
{0xB2, {0x0c, 0x0c, 0x00, 0x33, 0x33}, 5},
/* Gate Control, Vgh=13.65V, Vgl=-10.43V */
{0xB7, {0x35}, 1},
/* VCOM Setting, VCOM=1.35V */
{0xBB, {0x32}, 1},
// /* LCM Control, XOR: BGR, MX, MH */
// {0xC0, {0x2C}, 1},
/* VDV and VRH Command Enable, enable=1 */
{0xC2, {0x01, 0xFF}, 2},
/* VRH Set, Vap=4.4+... */
{0xC3, {0x15}, 1},
/* VDV Set, VDV=0 */
{0xC4, {0x20}, 1},
/* Frame Rate Control, 60Hz, inversion=0 */
{0xC6, {0x0F}, 1},
/* Power Control 1, AVDD=6.8V, AVCL=-4.8V, VDDS=2.3V */
{0xD0, {0xA4, 0xA1}, 1},
/* Positive Voltage Gamma Control */
{0xE0,
{0xD0, 0x08, 0x0E, 0x09, 0x09, 0x05, 0x31, 0x33, 0x48, 0x17, 0x14, 0x15,
0x31, 0x34},
14},
/* Negative Voltage Gamma Control */
{0xE1,
{0xD0, 0x08, 0x0E, 0x09, 0x09, 0x15, 0x31, 0x33, 0x48, 0x17, 0x14, 0x15,
0x31, 0x34},
14},
/* Display On */
{0x21, {0}, 0},
{0x29, {0}, 0},
{0, {0}, 0xff}};
最后就是编写屏幕api初始化函数、设置刷屏窗口函数、设置屏幕旋转角度函数。
在st7789_api_init函数里,负责将api全局变量的内容初始化,让刷屏代码能找到正确的初始化序列和其他函数。
extern disp_driver_api api;
void st7789_api_init(void){
api.disp_init_cmd = st7789_init_cmds;
api.disp_set_window = st7789_set_window;
api.disp_set_orientation = st7789_set_orientation;
}
/* The ST7789 display controller can drive 320*240 displays, when using a
* 240*240 display there's a gap of 80px, we need to edit the coordinates to
* take into account that gap, this is not necessary in all orientations. */
void st7789_set_window(lv_disp_drv_t *drv, const lv_area_t *area) {
uint8_t data[4] = {0};
uint16_t offsetx1 = area->x1;
uint16_t offsetx2 = area->x2;
uint16_t offsety1 = area->y1;
uint16_t offsety2 = area->y2;
offsetx1 += config.offset_x;
offsetx2 += config.offset_x;
offsety1 += config.offset_y;
offsety2 += config.offset_y;
/*Column addresses*/
disp_send_cmd(ST7789_CASET);
data[0] = (offsetx1 >> 8) & 0xFF;
data[1] = offsetx1 & 0xFF;
data[2] = (offsetx2 >> 8) & 0xFF;
data[3] = offsetx2 & 0xFF;
disp_send_data(data, 4);
/*Page addresses*/
disp_send_cmd(ST7789_RASET);
data[0] = (offsety1 >> 8) & 0xFF;
data[1] = offsety1 & 0xFF;
data[2] = (offsety2 >> 8) & 0xFF;
data[3] = offsety2 & 0xFF;
disp_send_data(data, 4);
/*Memory write*/
disp_send_cmd(ST7789_RAMWR);
}
void st7789_set_orientation(uint8_t orientation) {
const char *orientation_str[] = {"PORTRAIT", "PORTRAIT_INVERTED",
"LANDSCAPE", "LANDSCAPE_INVERTED"};
printf("Display orientation: %s\n", orientation_str[orientation]);
uint8_t data[] = {0xC0, 0x00, 0x60, 0xA0};
disp_send_cmd(ST7789_MADCTL);
disp_send_data((void *)&data[orientation], 1);
}
四、编写显示驱动代码
现在就需要编写显示驱动代码,完成显示驱动的整合工作了。
disp_driver_api api;
configuration config;
void disp_set_backlight(uint8_t level) { gpio_set_level(config.bl_pwm, level); }
void disp_send_cmd(uint8_t cmd) {
gpio_set_level(config.pin_dc, 0);
disp_send_data(&cmd, 1);
gpio_set_level(config.pin_dc, 1);
}
void disp_send_data(void *data, uint16_t length) {
gpio_set_level(config.pin_cs, 0);
dirver_spi_send_data(data, length);
gpio_set_level(config.pin_cs, 1);
}
void disp_driver_init(void) {
if (strcmp(config.disp_ic, "st7789") == 0) {
st7789_api_init();
} else if (strcmp(config.disp_ic, "gc9a01") == 0) {
gc9a01_api_init();
} else if (strcmp(config.disp_ic, "st7735") == 0) {
st7735_api_init();
} else {
printf("can find display name %s\n", config.disp_ic);
exit(1);
}
gpio_init(config.pin_cs);
gpio_set_direction(config.pin_cs, 0);
gpio_init(config.pin_dc);
gpio_set_direction(config.pin_dc, 0);
gpio_init(config.bl_pwm);
gpio_set_direction(config.bl_pwm, 0);
int ret = dirver_spi_init();
if (ret != 0) {
perror("SPI dev init fail!\n");
exit(1);
}
// Send all the commands
uint16_t cmd = 0;
while (api.disp_init_cmd[cmd].databytes != 0xff) {
disp_send_cmd(api.disp_init_cmd[cmd].cmd);
if ((api.disp_init_cmd[cmd].databytes & 0x1F) != 0) {
disp_send_data(api.disp_init_cmd[cmd].data,
api.disp_init_cmd[cmd].databytes & 0x1F);
}
if (api.disp_init_cmd[cmd].databytes & 0x80) {
usleep(api.disp_init_cmd[cmd].delaytime);
}
cmd++;
}
printf("%s init finish.\n", config.disp_ic);
disp_set_backlight(1);
api.disp_set_orientation(config.orientation);
}
void disp_driver_flush(lv_disp_drv_t *drv, const lv_area_t *area,
lv_color_t *color_p) {
// printf("lvgl disp flush\n");
api.disp_set_window(drv, area);
uint32_t size = (area->y2 - area->y1) * (area->x2 - area->x1);
disp_send_data(color_p, size * 2);
}
因为以前有适配LVGL的经验,所以刷屏代码直接按lvgl的风格进行书写。
这样分开写的好处就是,假如需要移植到别的平台(例如mcu之类的),我可以把gpio和spi部分的进行替换适配,就能正常驱动屏幕了。
五 、最终结果
经过两天的编写代码和调试,成功把屏幕点亮了。期间遇到过ini文件解析错误,屏幕初始化失败,spi传输数据过大出错等等错误。
但是都被我一一克服了,例如使用逻辑分析仪检查波形,搜索Linux的spi 应用层函数调用方式等等。
通过这一次的试验,让我学会了Linux的应用层SPI使用方式,也学会了spi的同步和异步写法的区别(异步传输会导致DC引脚信号波形错误,最终无法亮屏)。
因为这个板子是要退还回去给厂家的,我也不好意思直接焊接排针上去,本想着把SPI频率降低一些,插着排针来测试就行了,没想到设置100MHz也没事儿(具体这个板子SPI能跑到最高多少速度我就不知道了,手上没设备去测试)。手上的示波器和逻辑分析仪都只有200MHz的采样率,只适合20MHz以内的频率采样。个人感觉应该也能到50MHz。
|