目錄
- 聯盛德 HLK-W806 (一): Ubuntu20.04下的開發環境配置, 編譯和燒錄說明
- 聯盛德 HLK-W806 (二): Win10下的開發環境配置, 編譯和燒錄說明
- 聯盛德 HLK-W806 (三): 免按鍵自動下載和復位
- 聯盛德 HLK-W806 (四): 軟件SPI和硬件SPI驅動ST7735液晶LCD
- 聯盛德 HLK-W806 (五): W801開發板上手報告
- 聯盛德 HLK-W806 (六): I2C驅動SSD1306 128x64 OLED液晶屏
- 聯盛德 HLK-W806 (七): 兼容開發板 LuatOS Air103
- 聯盛德 HLK-W806 (八): 4線SPI驅動SSD1306/SSD1315 128x64 OLED液晶屏
- 聯盛德 HLK-W806 (九): 軟件SPI和硬件SPI驅動ST7789V液晶LCD
- 聯盛德 HLK-W806 (十): 在 CDK IDE開發環境中使用WM-SDK-W806
- 聯盛德 HLK-W806 (十一): 軟件SPI和硬件SPI驅動ST7567液晶LCD
- 聯盛德 HLK-W806 (十二): Makefile組織結構和編譯流程說明
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個, 所以英文字體是比較容易內嵌到代碼中的. 但是中文就不行, 因為中文有幾千個常用漢字, 完整的漢字字庫加上簡體繁體會有四五萬個, 字體的規模要大得多, 通常需要外置存儲來實現.
字體的數據結構
通常有兩種類型
- uint16_t, 單變量: 在英文應用中常用這種, 一個元素對應的就是點陣的一行, 因此字體最大寬度是16像素
- uint8_t, 多變量: 在中文和其它多字節符號中常用, 不管字體多寬都用uint8_t, 一個變量對應點陣一行的一部分, 因此字體的最大寬度不限.
數值的順序也有兩種類型, 在使用字庫時需要區別, 否則無法正常顯示
- 按位從高到低對應像素從左到右,
- 是按位從低到高對應像素從左到右
字體在代碼中的實現
很多網上的代碼例子, 將字庫放在.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