联盛德 HLKW806 (四): 软件SPI和硬件SPI驱动ST7735液晶LCD

目录

ST7735介绍

ST7735是用于驱动最大162x132像素的TFT驱动芯片, 396(128*3色)x162线输出, 可以直接以SPI协议, 或者8位/9位/16位并行连接外部控制器.
显示数据可以存储在片内的132 x 162 x 18 bits内存中, 显示内存的读写不需要外部时钟驱动.

ST7735有几种不同的型号: ST7735, ST7735R, ST7735S, -R和-S型号和初始型号功能一致, 但是增加了垂直滚动, 另外容忍更高的电压(最高到4.8V).

使用ST7735S的128x160 TFT LCD模块

连接

ST7735的LCD模块有128x128, 128x160等不同分辨率, 对外的接线除了VCC和GND外有6根, 接线方式都是一样的

  • SCL SPI时钟, 对应上位机SPI的SCK
  • SDA SPI数据输入, 对应上位机SPI的MOSI
  • RES 重启, 低电平有效, 工作时处于高电平
  • DC 命令模式和数据模式切换位, 低电平为命令模式, 高电平为数据模式
  • CS 片选信号, 对应上位机SPI的CS
  • BL 背光, 高电平亮, 低电平灭

如果使用软件SPI, IO口可以随便选择, 如果是硬件SPI, 其中的CS, SCK, MOSI 和 MISO(ST7735未使用)只能使用特定的IO口, 根据W806的手册有以下可选项

  • CS: B4, B14
  • SCK: B1, B2, B15, B24
  • MOSI: B5, B17, B26, PA7
  • MISO: B0, B3, B16, B25

对应本测试的连接方式为

  • B10 -> RES, RESET
  • B11 -> DC, CD
  • B14 -> CS, Chip Select
  • B15 -> SCK, SCL, CLK, Clock
  • B16 -> BL, Back Light
  • B17 -> MOSI, SDA
  • GND -> GND
  • 3.3V -> VCC

ST7735的控制

ST7735的控制分普通IO部分和命令/数据传输部分

  • 普通IO部分为外围控制相关的接口, 包括 BL背光, RES重置复位, DC命令数据模式切换
  • 命令和数据传输部分为显示控制相关的接口, 有8080并口模式和SPI串口模式, 其中并口还可以区分为8位/9位/16位传输, 因为手里这个模块是串口的, 所以只测试了串口模式. 以下为SPI串口模式的说明

软件SPI方式

软件SPI方式需要初始化全部IO, 都使用GPIO_MODE_OUTPUT+GPIO_NOPULL方式

void ST77XX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    __HAL_RCC_GPIO_CLK_ENABLE();

    GPIO_InitStruct.Pin = ST77XX_CS_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_CS_PORT, &GPIO_InitStruct);
    GPIO_InitStruct.Pin = ST77XX_SCK_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_SCK_PORT, &GPIO_InitStruct);
    GPIO_InitStruct.Pin = ST77XX_MOSI_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_MOSI_PORT, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = ST77XX_BL_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_BL_PORT, &GPIO_InitStruct);
    GPIO_InitStruct.Pin = ST77XX_RES_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_RES_PORT, &GPIO_InitStruct);
    GPIO_InitStruct.Pin = ST77XX_DC_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_DC_PORT, &GPIO_InitStruct);
}

对应的传输实现

static void ST77XX_TransmitByte(uint8_t dat)
{
    uint8_t i;
    ST77XX_CS_LOW;
    for (i = 0; i < 8; i++)
    {
        ST77XX_SCK_LOW;
        if (dat & 0x80)
        {
            ST77XX_MOSI_HIGH;
        }
        else
        {
            ST77XX_MOSI_LOW;
        }
        ST77XX_SCK_HIGH;
        dat <<= 1;
    }
    ST77XX_CS_HIGH;
}

硬件SPI方式

硬件SPI方式只需要初始化IO接口部分

void ST77XX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    __HAL_RCC_GPIO_CLK_ENABLE();

    GPIO_InitStruct.Pin = ST77XX_BL_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_BL_PORT, &GPIO_InitStruct);
    GPIO_InitStruct.Pin = ST77XX_RES_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_RES_PORT, &GPIO_InitStruct);
    GPIO_InitStruct.Pin = ST77XX_DC_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ST77XX_DC_PORT, &GPIO_InitStruct);
}

数据传输直接调用SDK提供的SPI传输方法

static void ST77XX_TransmitByte(uint8_t dat)
{
    ST77XX_CS_LOW;
    HAL_SPI_Transmit(&hspi, &dat, 1, 100);
    ST77XX_CS_HIGH;
}

ST7735的初始化

ST7735 和 ST7735R/ST7735S的初始化有细微差别, 示例代码中已经区分

使用BUFFER提升刷新速度

在硬件SPI下, 因为接口实现是以批量数据为基础的, 批量写入数据比按单字节或双字节写入效率要高得多, 在代码中可以申请一小段内存增加buffer, 在绘制中通过buffer去调用SPI, 可以充分利用硬件SPI的传输速度.

声明buffer区

static uint8_t st7735_buf[ST77XX_BUF_SIZE];
static uint16_t st7735_buf_pt = 0;

写入buffer和清空buffer

static void ST77XX_WriteBuff(uint8_t* buff, size_t buff_size)
{
    while (buff_size--)
    {
        st7735_buf[st7735_buf_pt++] = *buff++;
        if (st7735_buf_pt == ST77XX_BUF_SIZE)
        {
            ST77XX_Transmit(st7735_buf, st7735_buf_pt, HAL_MAX_DELAY);
            st7735_buf_pt = 0;
        }
    }
}

static void ST77XX_FlushBuff(void)
{
    if (st7735_buf_pt > 0)
    {
        ST77XX_Transmit(st7735_buf, st7735_buf_pt, HAL_MAX_DELAY);
        st7735_buf_pt = 0;
    }
}

在区域填充中使用buffer

void ST77XX_Fill(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint16_t color)
{
    uint16_t i,j;
    ST77XX_SetAddrWindow(x_start, y_start, x_end - 1, y_end - 1);
    for(i = y_start; i < y_end; i++)
    {
        for( j = x_start; j < x_end; j++)
        {
            ST77XX_WriteBuff((uint8_t *)&color, 2);
        }
    }
    ST77XX_FlushBuff();
}

LCD中的字体

LCD是一种点阵, LCD显示的就是字符对应的点阵, 不管是ASCII代表的字符, 还是GBK, UTF-8编码对应的汉字, 都需要转化为点阵才能显示. 字体就是显示某个具体符号的点阵, 字体在数据中存储的是字节数组. 字库就是这些字体的集合. 对应不同font和不同size, 要准备不同的字库, 每个字库在代码中是一个数组, 数组中每个元素代表了一个字符对应的点阵.

因为英文加上数字和标点符号只有不到100个, 所以英文字体是比较容易内嵌到代码中的. 但是中文就不行, 因为中文有几千个常用汉字, 完整的汉字字库加上简体繁体会有四五万个, 字体的规模要大得多, 通常需要外置存储来实现.

字体的数据结构

通常有两种类型

  1. uint16_t, 单变量: 在英文应用中常用这种, 一个元素对应的就是点阵的一行, 因此字体最大宽度是16像素
  2. uint8_t, 多变量: 在中文和其它多字节符号中常用, 不管字体多宽都用uint8_t, 一个变量对应点阵一行的一部分, 因此字体的最大宽度不限.

数值的顺序也有两种类型, 在使用字库时需要区别, 否则无法正常显示

  1. 按位从高到低对应像素从左到右,
  2. 是按位从低到高对应像素从左到右

字体在代码中的实现

很多网上的代码例子, 将字库放在.h头文件中, 因为字库本身是一个大数组, 因此放在头文件中只能在调用的最外层include, 不能多处使用, 否则编译会包变量重复定义错误.

正确的方法应当是: 在.c文件中定义字库, 在.h文件中将其声明为extern, 并且加上字体属性, 这样就可以多处调用了. 例如:

fonts.h 中声明字体类型

typedef struct {
    const uint8_t width;
    const uint8_t height;
    const uint8_t order;
    const uint8_t *data;
} FontDef;

extern FontDef Font_6x12;

font.c 中定义

#include "st7735_fonts.h"

static const uint8_t Font6x12 [] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /*" ",0*/
0x00,0x00,0x04,0x04,0x04,0x04,0x04,0x00,0x00,0x04,0x00,0x00, /*"!",1*/
0x14,0x14,0x0A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /*""",2*/
...
};

图片转换为代码

使用工具Img2Lcd.exe(For Windows).

先将图片剪切缩放为128x160尺寸, 然后用这个工具导出为.h文件. 色彩使用16bit RGB, 宽和高要设置对, 需要勾选MSB选项. 如果不放心, 可以将勾选和不勾选MSB的都输出备用.

演示代码的使用

演示代码的地址 wm-sdk-w806/tree/dev/demo/spi/st77xx_lcd, 文件结构为

.
├── ascii_fonts.c # 英文字体文件
├── ascii_fonts.h # 英文字体头文件
├── main.c        # 程序入口
├── st7735.c      # ST7735的初始化方法
├── st7735.h      # ST7735的头文件
├── st7789.c      # ST7789的初始化方法
├── st7789.h      # ST7789的头文件
├── st77xx.c      # ST7735和ST7789的公共方法
├── st77xx.h      # ST7735和ST7789的公共方法头文件
├── testimg.h     # 测试图片, 尺寸为128x128
├── wm_hal_msp.c  # 外设初始化方法
└── wm_it.c       # 中断处理方法

将项目 app/src 目录下的文件除了Makefile全部删除, 将演示代码复制到 app/src

根据设备配置选项

公共部分

st77xx.h

在这里设置

  • ST77XX_BUF_SIZE 缓冲的大小, 单位为字节
  • ST77XX_HARDWARE_SPI 是否使用硬件SPI, 0:否, 1:是
  • ST77XX__PORT 和 ST77XX__PIN 各pin脚的选择
#define ST77XX_BUF_SIZE         1024
#define ST77XX_HARDWARE_SPI     1

// CS: B4, B14
#define ST77XX_CS_PORT      GPIOB
#define ST77XX_CS_PIN       GPIO_PIN_14
// SCK: B1, B2, B15, B24
#define ST77XX_SCK_PORT     GPIOB
#define ST77XX_SCK_PIN      GPIO_PIN_15
// MOSI: B5, B17, B26, PA7
#define ST77XX_MOSI_PORT    GPIOB
#define ST77XX_MOSI_PIN     GPIO_PIN_17
// MISO: B0, B3, B16, B25

#define ST77XX_RES_PORT     GPIOB
#define ST77XX_RES_PIN      GPIO_PIN_10
#define ST77XX_DC_PORT      GPIOB
#define ST77XX_DC_PIN       GPIO_PIN_11
#define ST77XX_BL_PORT      GPIOB
#define ST77XX_BL_PIN       GPIO_PIN_16

在下面的液晶屏尺寸和显示方向色彩格式中选择适合自己屏幕的配置, 并取消注释(注意不要重复定义), 如果没有合适的就需要自己定义

#define ST77XX_WIDTH  128
#define ST77XX_HEIGHT 160
#define ST77XX_XSTART 2
#define ST77XX_YSTART 1
#define ST77XX_ROTATION (ST77XX_MADCTL_MX | ST77XX_MADCTL_MY | ST77XX_MADCTL_RGB)

ST7735的配置

因为ST7735存在多个型号, 对于ST7735R和ST7735S, 使用默认的初始化方法

st7735.c

void ST77XX_Init(void)
{
    ST77XX_Reset();
    ST77XX_ExecuteCommandList(init_cmds_r);
    ST77XX_ExecuteCommandList(init_cmds2);
    ST77XX_ExecuteCommandList(init_cmds3);
}

对于ST7735和ST7735B, 需要改为

void ST77XX_Init(void)
{
    ST77XX_Reset();
    ST77XX_ExecuteCommandList(init_cmds_b);
    ST77XX_ExecuteCommandList(init_cmds2);
    ST77XX_ExecuteCommandList(init_cmds3);
}

在项目中使用

在main.c中引入

#include "st7735.h"
#include "testimg.h"

在main()方法中初始化


static void SPI_Init(void)
{
    hspi.Instance = SPI;
    hspi.Init.Mode = SPI_MODE_MASTER;
    hspi.Init.CLKPolarity = SPI_POLARITY_LOW;
    hspi.Init.CLKPhase = SPI_PHASE_1EDGE;
    hspi.Init.NSS = SPI_NSS_SOFT;
    hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
    hspi.Init.FirstByte = SPI_LITTLEENDIAN;

    if (HAL_SPI_Init(&hspi) != HAL_OK)
    {
        Error_Handler();
    }
}

void main(void)
{
    //...
    ST77XX_GPIO_Init();
    SPI_Init();
    ST77XX_Init();
    //...
}

在wm_hal_msp.c中增加SPI的初始化方法

wm_hal_msp.c

void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
{
    __HAL_RCC_SPI_CLK_ENABLE();
    __HAL_AFIO_REMAP_SPI_CS(ST77XX_CS_PORT, ST77XX_CS_PIN);
    __HAL_AFIO_REMAP_SPI_CLK(ST77XX_SCK_PORT, ST77XX_SCK_PIN);
    __HAL_AFIO_REMAP_SPI_MOSI(ST77XX_MOSI_PORT, ST77XX_MOSI_PIN);
}

void HAL_SPI_MspDeInit(SPI_HandleTypeDef* hspi)
{
    __HAL_RCC_SPI_CLK_DISABLE();
    HAL_GPIO_DeInit(ST77XX_CS_PORT, ST77XX_CS_PIN);
    HAL_GPIO_DeInit(ST77XX_SCK_PORT, ST77XX_SCK_PIN);
    HAL_GPIO_DeInit(ST77XX_MOSI_PORT, ST77XX_MOSI_PIN);
}

然后就可以调用显示方法了

控制背光

ST77XX_BackLight_On();
ST77XX_BackLight_Off();

填充色块

ST77XX_Fill(0, 0, ST77XX_WIDTH, ST77XX_HEIGHT, ST77XX_RED);

输出字符

ST77XX_DrawString(5, y, (const char *)"0123456789ABCDE", Font_6x12, ST77XX_YELLOW, ST77XX_RED);

输出图片

ST77XX_DrawImage(0, 0, 128, 160, (uint16_t *)testimage1);

在示例中的调用

ST77XX_BackLight_On();
HAL_Delay(500);
ST77XX_Fill(0, 0, ST77XX_WIDTH, ST77XX_HEIGHT, ST77XX_RED);
HAL_Delay(500);
ST77XX_Fill(0, 0, ST77XX_WIDTH, ST77XX_HEIGHT, ST77XX_YELLOW);
HAL_Delay(500);
ST77XX_Fill(0, 0, ST77XX_WIDTH, ST77XX_HEIGHT, ST77XX_BLUE);
HAL_Delay(500);
ST77XX_Fill(0, 0, ST77XX_WIDTH, ST77XX_HEIGHT, ST77XX_GREEN);
HAL_Delay(500);

y = 10;

ST77XX_DrawString(5, y, (const char *)"0123456789ABCDE", Font_6x12, ST77XX_YELLOW, ST77XX_RED);
HAL_Delay(500);
y += Font_6x12.height + 2;

ST77XX_DrawString(5, y, (const char *)"0123456789ABCDE", Font_7x10, ST77XX_YELLOW, ST77XX_BLUE);
HAL_Delay(500);
y += Font_7x10.height + 2;

ST77XX_DrawString(5, y, (const char *)"0123456789ABCDE", Font_8x16, ST77XX_YELLOW, ST77XX_BLUE);
HAL_Delay(500);
y += Font_8x16.height + 2;

ST77XX_DrawString(5, y, (const char *)"0123456ABC", Font_11x18, ST77XX_YELLOW, ST77XX_BLUE);
HAL_Delay(500);
y += Font_11x18.height + 2;

ST77XX_DrawString(5, y, (const char *)"0123456AB", Font_12x24, ST77XX_YELLOW, ST77XX_BLUE);
HAL_Delay(500);
y += Font_12x24.height + 2;

ST77XX_DrawString(5, y, (const char *)"ST7735", Font_16x26, ST77XX_WHITE, ST77XX_BLUE);
HAL_Delay(500);
y += Font_16x26.height + 2;

ST77XX_DrawString(0, y, (const char *)"W806 SDK", Font_16x32, ST77XX_WHITE, ST77XX_BLUE);
HAL_Delay(3000);

ST77XX_DrawImage(0, 0, 128, 128, (uint16_t *)test_img_128x128);
HAL_Delay(2000);

W806 SPI驱动ST7735的刷新率

在ST7735S 128x160 LCD中, 通过以下代码在主进程中测试两幅128x160图片无间断循环刷新

for (uint16_t i = 0; i < 1000; i++)
{
    ST77XX_DrawImage(0, 0, 128, 160, (uint16_t *)testimage1);
    ST77XX_DrawImage(0, 0, 128, 160, (uint16_t *)testimage2);
}
printf("done");

记录得到的结果为

[2021-11-28 08:06:33.267]
RX:done
[2021-11-28 08:07:12.256]
RX:done
[2021-11-28 08:07:51.232]
RX:done
[2021-11-28 08:08:30.204]
RX:done
[2021-11-28 08:09:09.181]
RX:done
[2021-11-28 08:09:48.166]
RX:done
[2021-11-28 08:10:27.145]
RX:done
[2021-11-28 08:11:06.113]
RX:done
[2021-11-28 08:11:45.145]
RX:done
[2021-11-28 08:12:24.073]
RX:done

可以看到间隔基本上在39秒左右, 根据这个结果计算得全屏刷新率为 2000/39 = 51.2 FPS

视频演示

视频演示: https://www.bilibili.com/video/BV1vq4y1B7uL

参考

原文地址:https://www.cnblogs.com/milton/p/15614304.html