1773|5

239

帖子

2

TA的资源

纯净的硅(初级)

楼主
 

【米尔-芯驰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。

 

此帖出自汽车电子论坛

最新回复

好的,感谢大佬   详情 回复 发表于 2023-12-26 19:54
点赞 关注

回复
举报

6822

帖子

0

TA的资源

五彩晶圆(高级)

沙发
 

有适配LVGL的经验,所以刷屏代码直接按lvgl的风格进行书写

这个是楼主的功底啊,厉害

还有逻辑分析仪可以用

此帖出自汽车电子论坛
 
 

回复

248

帖子

0

TA的资源

纯净的硅(初级)

板凳
 

驱动思路很清晰(๑•̀ㅂ•́)و✧

此帖出自汽车电子论坛
 
 
 

回复

248

帖子

0

TA的资源

纯净的硅(初级)

4
 

麻烦请教一下,extern configuration config;这个结构体楼主是从哪个文件引用出来的。还有那个你编写的handler函数是怎么来解析.ini 文件的


此帖出自汽车电子论坛

点评

解析ini文件在文章第二节内容的第二段,全局config这个在driver.c文件。https://gitee.com/walker2048/lvgl_cmake,项目已开源,现在在搞TI的东西,暂时没空更新    详情 回复 发表于 2023-12-26 13:46
 
 
 

回复

239

帖子

2

TA的资源

纯净的硅(初级)

5
 
qiao--- 发表于 2023-12-26 10:05 麻烦请教一下,extern configuration config;这个结构体楼主是从哪个文件引用出来的。还有那个你 ...

解析ini文件在文章第二节内容的第二段,全局config这个在driver.c文件。https://gitee.com/walker2048/lvgl_cmake,项目已开源,现在在搞TI的东西,暂时没空更新

 

此帖出自汽车电子论坛

点评

好的,感谢大佬  详情 回复 发表于 2023-12-26 19:54
 
 
 

回复

248

帖子

0

TA的资源

纯净的硅(初级)

6
 
walker2048 发表于 2023-12-26 13:46 解析ini文件在文章第二节内容的第二段,全局config这个在driver.c文件。https://gitee.com/walker2048/lv ...

好的,感谢大佬

此帖出自汽车电子论坛
 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/8 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表