如果要做一款掌上的街机,需要搞定的其中一个外设就是屏幕。
我这里屏幕一开始调试的时候选用的是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屏幕的代码。
屏幕和树莓派间的连线如下:
其中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的字节序前后反了导致的:
完整代码参见 https://github.com/hizilla/mame-draft/tree/master/lcd