MENU

MAME移植之树莓派驱动TFT屏幕

2024 年 07 月 06 日 • 嵌入式

如果要做一款掌上的街机,需要搞定的其中一个外设就是屏幕。
我这里屏幕一开始调试的时候选用的是1.8英寸的小屏幕,规格如下:

像素:128 * 160 * RGB565
总线:SPI
驱动芯片:ST7735S

使用树莓派来驱动这块屏幕,重点在于树莓派通过SPI来传输数据,好在树莓派上的Linux已具备SPI驱动模块,在配置中打开即可。
具体为

sudo raspi-config
找到Interfacing Options,找到SPI,打开后重启树莓派即可以在/dev/目录下找到SPI设备/dev/spidev0.0,/dev/spidev0.1。分别对应树莓引脚中的两个SPI接口。

Linux中用SPI写入数据比较简单,使用标准读写打开设备即可,这里就不多说了,下面直接上代码,为树莓派Linux驱动ST7735S屏幕的代码。

屏幕和树莓派间的连线如下:
60sXU0

其中SPI接口的连线按照树莓派引脚连接即可,其余DC引脚、REST引脚使用GPIO接口即可,这里驱动GPIO采用的libgpiod.so

在写入数据之前需要设置SPI的配置,分别为工作模式、最大工作频率以及一个字多少字节。

int lcd_spi_init(void)
{
    int fd = open("/dev/spidev0.0", O_RDWR);
    if (fd < 0) {
        printf("failed to open spi device.\n");
    return -1;
    }

    /* mode  */
    uint8_t mode = LCD_SPI_MODE;
    if (ioctl(fd, SPI_IOC_WR_MODE, &mode) != 0) {
        printf("SPI_IOC_WR_MODE: %d\n", errno);
        return -1;
    }
    if (ioctl(fd, SPI_IOC_RD_MODE, &mode) != 0) {
        printf("SPI_IOC_RD_MODE: %d\n", errno);
        return -1;
    }

    /* speed */
    uint32_t speed = LCD_SPI_MAX_SPEED;
    if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) != 0) {
        printf("SPI_IOC_WR_MAX_SPEED_HZ: %d\n", errno);
        return -1;
    }
    if (ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed) != 0) {
        printf("SPI_IOC_RD_MAX_SPEED_HZ: %d\n", errno);
        return -1;
    }

    /* bits per word */
    uint8_t bits = LCD_SPI_BITS_PER_WORD;
    if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) != 0) {
        printf("SPI_IOC_WR_BITS_PER_WORD: %d\n", errno);
        return -1;
    }
    if (ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits) != 0) {
        printf("SPI_IOC_RD_BITS_PER_WORD: %d\n", errno);
        return -1;
    }
    return fd;
}

然后下面是LCD的初始化代码,分为软件复位和硬件复位。软件复位指的是对LCD进行重新配置,硬件复位是对LCD的复位引脚进行设置。(下面的每一行,第一个为控制寄存器,后面的为数据,至于每个控制寄存器是做啥的,可以检索驱动芯片的datasheet,比如当你需要设置横屏、竖屏的时候)。

struct spi_data g_spi_data[] = {
    {0x11, {}, 0},
    {0x26, {0x04}, 1},
    {0xB1, {0x0e, 0x10}, 2},
    {0xC0, {0x08, 0x00}, 2},
    {0xC1, {0x05}, 1},
    {0xC5, {0x38, 0x40}, 2},
    {0x3a, {0x05}, 1},
    {0x36, {0xA8}, 1},
    {0x2A, {0x00, 0x00, 0x00, 0x9F}, 4}, 
    {0x2B, {0x00, 0x00, 0x00, 0x7F}, 4},
    {0xB4, {0x00}, 1},
    {0xf2, {0x01}, 1},
    {0xE0, {0x3f, 0x22, 0x20, 0x30, 0x29, 0x0c, 0x4e, 0xb7, 0x3c, 0x19, 0x22, 0x1e, 0x02, 0x01, 0x00}, 15},
    {0xE1, {0x00, 0x1b, 0x1f, 0x0f, 0x16, 0x13, 0x31, 0x84, 0x43, 0x06, 0x1d, 0x21, 0x3d, 0x3e, 0x3f}, 15},
    {0x29, {}, 0},
    {0x2C, {}, 0},
};

int lcd_reset(int fd)
{
    size_t count = sizeof(g_spi_data) / sizeof(g_spi_data[0]);
    for (size_t i = 0; i < count; i++) {
        lcd_spi_send_cmd(fd, &g_spi_data[i]);
    }
    return 0;
}

void lcd_hard_reset(void)
{
    gpiod_line_set_value(g_lcd_reset, 0);
    usleep(200 * 1000);
    gpiod_line_set_value(g_lcd_reset, 1);
    usleep(500 * 1000);
}

上述向LCD发送控制指令的代码如下:

int lcd_spi_write(int fd, uint8_t *wbuf, size_t size)
{
    int ret = write(fd, wbuf, size);
    if (ret != size) {
        printf("failed to send data.\n");
    }
    return ret;
}

int lcd_spi_send_cmd(int fd, struct spi_data *spi_data)
{
    gpiod_line_set_value(g_lcd_dc, 0);
    lcd_spi_write(fd, &spi_data->cmd, 1);
    if (spi_data->data_len == 0) {
        return 0;
    }
    gpiod_line_set_value(g_lcd_dc, 1);
    lcd_spi_write(fd, spi_data->data, spi_data->data_len);
    return 0;
}

对LCD进行发送数据(像素信息)的代码如下。这里每次在发送数据的时候,需要将LCD的DC引脚设置为数据模式告知LCD接下来发送的都是显示数据。显示数据需要按照LCD显示的像素类型和扫描模式来发送。

我的LCD像素格式为RGB565(即一个像素占据2个字节,其中红色5bit,绿色6bit,蓝色5bit),横屏模式。所以需要显示的时要连续发送屏幕宽占的像素 * 屏幕长占的像素,每个像素发送2个字节。


void lcd_spi_send_data(int fd, uint8_t *data, size_t len)
{
    gpiod_line_set_value(g_lcd_dc, 1);

    size_t pos = 0;
    int left_len = len;
    while (left_len > 0) {
        size_t send = MAX_SPI_BUF;
        if (left_len < MAX_SPI_BUF) {
            send = left_len;
        }
        lcd_spi_write(fd, data + pos, send);
        pos += send;
        left_len -= send;
    }
}

void test_lcd(int fd)
{
    size_t size = LCD_SCREEN_WIDTH * LCD_SCREEN_HEIGHT;
    struct rgb565 *data = malloc(size * sizeof(struct rgb565));
    if (data == NULL) {
        printf("malloc failed\n");
        return;
    }
    memset(data, 0XFF, size * sizeof(struct rgb565));
    for (int i = 0; i < size; i++) {
        if (i / LCD_SCREEN_WIDTH < 64) {
            data[i].r = 0;
            data[i].g = 0xFF;
            data[i].b = 0;
        } else if ((i / LCD_SCREEN_WIDTH >= 64) && (i / LCD_SCREEN_WIDTH < 100)) {
            data[i].r = 0xFF;
            data[i].g = 0;
            data[i].b = 0;
        } else {
            data[i].r = 0;
            data[i].g = 0;
            data[i].b = 0xFF;
        }
    }
    lcd_spi_send_data(fd, (uint8_t *)data, size * sizeof(struct rgb565));
}

下面附上我买的另外一个屏幕的驱动程序,也是通过SPI接口来使用

像素:480 * 320 * RGB565
总线:SPI
驱动芯片:ST7796U

连线基本相同,只是初始化的寄存器和数值不一样。
还有就是这块屏幕是带有GRAM的屏幕,每次发送数据前需要先写入寄存器需要写入的位置,然后再发送数据,也就是下面的lcd_set_window函数,需要先调用该函数,再发送数据,发送的数据需要和窗口的大小一致。
另外一个需要注意的点是字节序,如果颜色和设置的不一致,考虑字节序是不是错了。

struct spi_data g_spi_data[] = {
    {0x11, {}, 0},
    {0x36, {0x48}, 1},
    {0x3A, {0x55}, 1},
    {0xF0, {0xC3}, 1},
    {0xF0, {0x96}, 1},
    {0xB4, {0x01}, 1},
    {0xB7, {0xC6}, 1},
    {0xB9, {0x02, 0xE0}, 2},
    {0xC0, {0x80, 0x07}, 2},
    {0xC1, {0x15}, 1},
    {0xC2, {0xA7}, 1},
    {0xC5, {0x07}, 1},
    {0xE8, {0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8},
    {0xE0, {0xF0, 0x04, 0x0E, 0x03, 0x02, 0x13, 0x34, 0x44, 0x4A, 0x3A, 0x15, 0x15, 0x2F, 0x34}, 14},
    {0xE1, {0xF0, 0x0F, 0x16, 0x0C, 0x09, 0x05, 0x34, 0x33, 0x4A, 0x35, 0x11, 0x11, 0x2C, 0x32}, 14},
    {0xF0, {0x3C}, 1},
    {0xF0, {0x69}, 1},
    {0x21, {}, 0},
    {0x29, {}, 0},
    {0x36, {0x20}, 1},
}

void lcd_set_window(int fd, uint16_t xstart, uint16_t ystart,
    uint16_t xend, uint16_t yend)
{
    struct spi_data data = { 0 };
    data.cmd = 0x2A;
    data.data[0] = xstart >> 8;
    data.data[1] = xstart & 0xFF;
    data.data[2] = xend >> 8;
    data.data[3] = xend & 0xFF;
    data.data_len = 4;
    lcd_spi_send_cmd(fd, &data);

    data.cmd = 0x2B;
    data.data[0] = ystart >> 8;
    data.data[1] = ystart & 0xFF;
    data.data[2] = yend >> 8;
    data.data[3] = yend & 0xFF;
    data.data_len = 4;
    lcd_spi_send_cmd(fd, &data);

    data.cmd = 0x2C;
    data.data_len = 0;
    lcd_spi_send_cmd(fd, &data);
}

屏幕点亮如下,下面中间应该是绿色,因为RGB565的字节序前后反了导致的:
qGO9yz

完整代码参见 https://github.com/hizilla/mame-draft/tree/master/lcd