书接上文使用软件SPI进行颜色填充,现在来实现进行字符串的显示。
MADCTL 寄存器说明
对于 0.96 寸 80×160 的 ST7735S 屏幕,要实现横屏正常显示(文字正立),通常需要 MV=1 来交换行列。0x36命令下的数据每位的作用如下:
| 位 | 1的效果 | 0的效果 | 说明 |
|---|---|---|---|
| MY (bit7) | 行地址从下到上递增 | 行地址从上到下递增(默认) | Row Address Order |
| MX (bit6) | 列地址从右到左递增 | 列地址从左到右递增(默认) | Column Address Order |
| MV (bit5) | 交换行列(横屏) | 不交换(竖屏,默认) | Row/Column Exchange |
| ML (bit4) | 刷新方向从下到上 | 刷新方向从上到下(默认) | Vertical Refresh Order |
| BGR (bit3) | BGR 顺序 | RGB 顺序(默认) | RGB/BGR Order |
| MH (bit2) | 刷新方向从右到左 | 刷新方向从左到右(默认) | Horizontal Refresh Order |
| bit1 | 保留 | ||
| bit0 | 保留 |
MV=1 的效果:ST7735S 内部会交换行列映射,使得 80×160 的屏幕以竖屏方式显示(160列 × 80行),文字方向正确正立。

新增显示字符串函数
| 函数 | 功能 |
|---|---|
LCD_DrawPixel(x, y, color) |
在指定坐标画单个像素 |
LCD_DrawChar(x, y, ch, font, color, bgColor) |
显示单个字符(前景色 + 背景色) |
LCD_DrawString(x, y, str, font, color, bgColor) |
显示字符串 |
/* ========== 画单个像素 ========== */
static void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color)
{
/* 设置写RAM窗口 (单个像素) */
LCD_WriteCmd(0x2A); /* Column Address Set */
LCD_WriteData8(0x00); LCD_WriteData8((uint8_t)(x >> 8));
LCD_WriteData8((uint8_t)x); LCD_WriteData8(0x00);
LCD_WriteCmd(0x2B); /* Row Address Set */
LCD_WriteData8(0x00); LCD_WriteData8((uint8_t)(y >> 8));
LCD_WriteData8((uint8_t)y); LCD_WriteData8(0x00);
LCD_WriteCmd(0x2C); /* Memory Write */
LCD_WriteData16(color);
}
/* ========== 显示单个字符 ========== */
static void LCD_DrawChar(uint16_t x, uint16_t y, char ch, FontDef *font, uint16_t color, uint16_t bgColor)
{
uint32_t i, j, b;
uint16_t tmp;
/* 字体数据: font->width 列, 每列一个 uint16_t, 高 font->height 位有效 */
for (i = 0; i < font->width; i++)
{
tmp = font->data[(ch - 32) * font->width + i];
for (j = 0; j < font->height; j++)
{
b = tmp & (1 << (font->height - 1 - j));
LCD_DrawPixel(x + i, y + j, b ? color : bgColor);
}
}
}
/* ========== 显示字符串 ========== */
static void LCD_DrawString(uint16_t x, uint16_t y, const char *str, FontDef *font, uint16_t color, uint16_t bgColor)
{
uint16_t curX = x;
while (*str != '\0')
{
LCD_DrawChar(curX, y, *str, font, color, bgColor);
curX += font->width;
str++;
}
}
色块显示
/* ========== 主函数 ========== */
int main(void)
{
HAL_Init();
SystemClock_Config();
/* 使能GPIO时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
MX_USART1_UART_Init();
LOG_INFO("System Init Done!!!");
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/* PA1(SCK)、PA4(DC)、PA7(MOSI) → 推挽输出 */
GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_7;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* PB0(CS)、PB1(BLK) → 推挽输出 */
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* 初始电平 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); /* BLK = 1 (点亮背光) */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); /* CS = 1 (未选中) */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* DC = 1 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); /* SCK = 0 (空闲低) */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET); /* MOSI = 0 */
LOG_INFO("GPIO Init Done!!!");
/* 初始化屏幕 */
LCD_Init();
/* 全屏白色背景 (横屏模式下需写满内部显存) */
LCD_FillColor(0xFFFF); /* 全白 */
/* 测试2: 在可视区域左上角画黑色色块,验证坐标 */
/* 0.96寸屏幕: 可视区域列24~103(80列), 行1~160(160行) */
LCD_DrawBlock(24, 1, 20, 10, 0x0000); /* 黑色20x10色块,在左上角 */
/* 静态显示,不再循环刷新 */
}
色块出现了,但在左下角而不是左上角!说明坐标偏移有问题。黑色色块在左下角意味着行方向(RASET)的偏移是反的------数据被写到了屏幕底部。
问题出在:默认竖屏模式下,可视区域行偏移是 1,但 ST7735 的行扫描方向可能和我们预期的相反。让我调整------把行偏移从 1 改到靠近可视区域底部的位置,或者直接在中间位置画色块来快速定位。
/* 测试2: 在屏幕正中间画黑色色块 */
/* 可视区域列24~103(80列), 行1~160(160行) */
/* 正中间: 列=(24+103)/2≈60, 行=(1+160)/2≈80 */
LCD_DrawBlock(60, 80, 20, 10, 0x0000); /* 屏幕正中间,黑色20x10 */
色块在正中间,但长边(20像素)与屏幕短边(80像素宽)平行 。这说明列和行的映射是反的------CASET 实际控制的是屏幕的短边方向(Y),RASET 控制的是长边方向(X)。
这完全符合 0.96 寸屏幕的物理特性:屏幕物理是 80(短边)×160(长边),而内部 RAM 是 132×162。列偏移 24 对应的是屏幕短边方向。
所以实际情况是:
- CASET(列)→ 屏幕短边方向(80像素,偏移24),RASET(行)→ 屏幕长边方向(160像素,偏移1)
- 色块 width=20 沿列方向=短边方向,height=10 沿行方向=长边方向
这正好是默认竖屏模式!色块长边和短边平行是因为 width=20 > height=10,而列方向只有 80 像素,所以视觉上 20 像素占了短边的 1/4,看起来是"长"的。
字符串显示
LCD_FillColor(0xFFFF); /* 全白 */
/* 在可视区域左上角画字符串 */
/* 列方向(短边80px): 偏移24, 有效范围24~103 */
/* 行方向(长边160px): 偏移1, 有效范围1~160 */
/* 字符串沿列方向排列,从列24开始 */
LCD_DrawString(24, 1, "Hi", &Font_7x10, 0x0000, 0xFFFF); /* 黑字白底 */
没有字符串显示
每个字符确实是 10 个 uint16_t = height=10!所以字体数据是行优先,不是列优先!每行一个 uint16_t,width=7 位有效数据存在高 7 位。
这就是问题!LCD_DrawChar 里按 font->width 遍历列,但数据实际上是按行排列的!
/* ========== 显示单个字符 (窗口写入模式,高效) ========== */
static void LCD_DrawChar(uint16_t x, uint16_t y, char ch, FontDef *font, uint16_t color, uint16_t bgColor)
{
uint32_t i, j, b;
uint16_t tmp;
/* 设置写RAM窗口: 列从 x 到 x+width-1, 行从 y 到 y+height-1 */
LCD_WriteCmd(0x2A); /* Column Address Set */
LCD_WriteData8((uint8_t)(x >> 8));
LCD_WriteData8((uint8_t)(x & 0xFF));
LCD_WriteData8((uint8_t)((x + font->width - 1) >> 8));
LCD_WriteData8((uint8_t)((x + font->width - 1) & 0xFF));
LCD_WriteCmd(0x2B); /* Row Address Set */
LCD_WriteData8((uint8_t)(y >> 8));
LCD_WriteData8((uint8_t)(y & 0xFF));
LCD_WriteData8((uint8_t)((y + font->height - 1) >> 8));
LCD_WriteData8((uint8_t)((y + font->height - 1) & 0xFF));
LCD_WriteCmd(0x2C); /* Memory Write */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* DC = 1 */
/* 字体数据: 行优先存储, 每行一个uint16_t, font->height个数据/字符 */
/* 有效位在高位: bit(16-width) ~ bit15 */
for (j = 0; j < font->height; j++)
{
tmp = font->data[(ch - 32) * font->height + j];
for (i = 0; i < font->width; i++)
{
b = tmp & (1 << (15 - i));
if (b)
LCD_WriteData16(color);
else
LCD_WriteData16(bgColor);
}
}
}
编译烧录测试。这次修改了两个关键点:
- 字体数据格式纠正 :从"列优先、每列一个 uint16_t"改为**"行优先、每行一个 uint16_t"**,每个字符占
height个数据(不是width个) - 位扫描顺序 :有效位在 uint16_t 的高位,用
1 << (15 - i)从 bit15 开始扫描
字符串颠倒
字符"Hi"显示效果如下,沿着短边排列,是因为 LCD_DrawString 在 x 方向(列/CASET)递增,而 x 对应的是短边。
要实现横屏显示(字符沿长边水平排列),需要 交换行列,让:
- CASET(列)对应屏幕的长边(160像素)
- RASET(行)对应屏幕的短边(80像素)
这通过设置 MADCTL = 0xA0(MV=1,交换行列)来实现。同时需要调整偏移量:
- 横屏时,长边(160)的偏移是 1(原RASET偏移)
- 短边(80)的偏移是 24(原CASET偏移)

横屏模式下的坐标映射
| 参数 | 竖屏模式 (0x00) | 横屏模式 (0xA0) |
|---|---|---|
| CASET(列) | 短边80px,偏移24 | 长边160px,偏移1 |
| RASET(行) | 长边160px,偏移1 | 短边80px,偏移24 |
| 字符串排列方向 | 沿短边(竖排) | 沿长边(横排) |
/* Memory Access Control - 横屏模式, 交换行列 */
LCD_WriteCmd(0x36);
LCD_WriteData8(0xA0); /* MV=1(交换行列), MX=0, MY=1, ML=0, RGB */
/* 0xA0 = 1010 0000: MY=1, MX=0, MV=1, ML=0, RGB */
/* 横屏模式: 交换行列后 */
/* CASET(列) 现在对应原长边160px, 偏移1, 有效范围1~160 */
/* RASET(行) 现在对应原短边80px, 偏移24, 有效范围24~103 */
/* 字符串沿CASET(列)方向排列,即沿屏幕长边水平排列 */
LCD_DrawString(1, 24, "Hi", &Font_7x10, 0x0000, 0xFFFF); /* 黑字白底, 从(1,24)开始 */
编译烧录后预期效果
"Hi" 应该沿着屏幕的长边(160像素方向)水平显示,字符从左到右排列。
颜色显示
颜色对应关系如下:
| 颜色 | RGB565 值 | 说明 |
|---|---|---|
| 0xF800 | 11111 000000 00000 |
红色 (R=31, G=0, B=0) |
| 0x07E0 | 00000 111111 00000 |
绿色 (R=0, G=31, B=0) |
RGB565 的格式是 5位R + 6位G + 5位B,所以:
- 0xF800 就是纯红
- 0x07E0 就是纯绿
尝试用红色显示"Hello",用绿色显示"World!",代码如下:
/* 横屏模式: 交换行列后 */
/* CASET(列) 现在对应原长边160px, 偏移1, 有效范围1~160 */
/* RASET(行) 现在对应原短边80px, 偏移24, 有效范围24~103 */
/* 字符串沿CASET(列)方向排列,即沿屏幕长边水平排列 */
__disable_irq();
LCD_DrawString(1, 24, "Hello", &Font_11x18, 0xF800, 0xFFFF); /* 红字白底, 从(1,24)开始 */
__enable_irq();
__disable_irq();
LCD_DrawString(55, 24, " World!", &Font_11x18, 0x07E0, 0xFFFF); /* 绿字白底, 从(55,24)开始 */
__enable_irq();
颜色显示异常
代码中预期使用 0xF800 红色显示"Hello",用0x07E0绿色显示"World!",实际显示的是Hello""蓝色
|-------------------|------|------|
| | 预期颜色 | 实际颜色 |
| "Hello" (0xF800) | 红色 | 蓝色 |
| "World!" (0x07E0) | 绿色 | 绿色 |
说明 0xF800 确实显示了蓝色。但 0xF800 按 RGB565 标准是红色。这意味着 屏幕确实是 RGB 和 BGR 顺序反了(R和B交换了),所以红色变成了蓝色。
这可以通过 MADCTL 的 bit3(RGB/BGR) 来修正:
| MADCTL bit3 | 颜色顺序 |
|---|---|
| 0 | RGB(默认) |
| 1 | BGR |
当前 MADCTL=0x60,bit3=0(RGB 模式)。如果你实际看到 R 和 B 互换了,说明这个屏幕需要 BGR 模式。把 bit3 置1:
0x60 | 0x08 = 0x68
让我看一下当前代码然后修改:
修改为 MADCTL = 0x68,唯一变化是 bit3 从 0 改为 1(RGB → BGR)。
这样 0xF800 就会正确显示为红色 ,0x07E0 显示为绿色。
常见问题
重新烧录固件后,上次显示的字符串没有被清空
原因:残留可能是上一次烧录(0xE0 模式)的显存数据,而你这次(0x60 模式)虽然 Fill 了全屏,但 DrawString 在不同位置又写了数据,因为屏幕 RESET 接 VDD 不会复位,MCU 复位但屏幕显存里的旧数据还在。
解决:断电重上电
最终代码实现
#include "st7735.h"
#include "main.h"
#include "fonts.h"
#include <stdio.h>
#include <string.h>
#define AUTO_BAUD_MODE0 /*自动波特率检测模式选择,从start位开始测量;若屏蔽选择下降沿到下降沿测量*/
// 外设句柄
I2C_HandleTypeDef hi2c1;
SPI_HandleTypeDef hspi1;
UART_HandleTypeDef UartHandle; // 全局变量,名称必须和py32f0xx_it.c中一致
// UART1初始化函数
void MX_USART1_UART_Init(void)
{
/* USART1初始化 */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
UartHandle.Instance = USART1;
UartHandle.Init.BaudRate = 115200;
UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
UartHandle.Init.StopBits = UART_STOPBITS_1;
UartHandle.Init.Parity = UART_PARITY_NONE;
UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle.Init.Mode = UART_MODE_TX_RX;
UartHandle.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_AUTOBAUDRATE_INIT;
UartHandle.AdvancedInit.AutoBaudRateEnable = UART_ADVFEATURE_AUTOBAUDRATE_ENABLE; /* 自动波特率使能 */
#ifdef AUTO_BAUD_MODE0
UartHandle.AdvancedInit.AutoBaudRateMode = UART_ADVFEATURE_AUTOBAUDRATE_ONSTARTBIT; /* 自动波特率检测模式从start位开始测量波特率,上位机发送0x7f即可 */
#else
UartHandle.AdvancedInit.AutoBaudRateMode = UART_ADVFEATURE_AUTOBAUDRATE_ONFALLINGEDGE; /* 自动波特率检测模式下降沿到下降沿测量,上位机发送0x55即可 */
#endif
if (HAL_UART_DeInit(&UartHandle) != HAL_OK)
{
Error_Handler();
}
if (HAL_UART_Init(&UartHandle) != HAL_OK)
{
Error_Handler();
}
/* 使能USART1中断 */
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
/*
* 纯软件SPI测试版本
* 不初始化SPI/UART/I2C,只用GPIO模拟SPI时序驱动ST7735
* 目的:排除硬件SPI外设问题,验证屏幕是否正常工作
*
* 接线:
* PA1 = SCK (软件模拟时钟)
* PA7 = MOSI (软件模拟数据)
* PA4 = DC (命令/数据选择)
* PB0 = CS (片选,低电平有效)
* PB1 = BLK (背光,高电平点亮)
*/
/* ========== 简易延时 ========== */
static void Delay_us(uint32_t us)
{
/* HSI 8MHz, 约125ns per loop */
volatile uint32_t count = us * 2U;
while(count--) { __NOP(); }
}
static void Delay_ms(uint32_t ms)
{
while(ms--) { Delay_us(1000); }
}
/* ========== 软件SPI底层 ========== */
/* 软件SPI发送1字节 (MSB first, SPI Mode 0: CPOL=0, CPHA=0) */
static void SoftSPI_WriteByte(uint8_t data)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
/* 设置数据位 (MSB first) */
if (data & 0x80)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);
/* SCK上升沿,ST7735采样数据 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
/* SCK下降沿,准备下一位 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
data <<= 1;
}
}
/* 发送命令 (DC=0) */
static void LCD_WriteCmd(uint8_t cmd)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); /* DC = 0 (命令) */
SoftSPI_WriteByte(cmd);
}
/* 发送数据 (DC=1) */
static void LCD_WriteData8(uint8_t data)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* DC = 1 (数据) */
SoftSPI_WriteByte(data);
}
/* 发送16位数据 */
static void LCD_WriteData16(uint16_t data)
{
LCD_WriteData8((uint8_t)(data >> 8));
LCD_WriteData8((uint8_t)(data & 0xFF));
}
/* ========== ST7735初始化命令 ========== */
static void LCD_Init(void)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); /* CS = 0 (选中) */
/* Software Reset */
LCD_WriteCmd(0x01);
Delay_ms(150);
/* Out of Sleep */
LCD_WriteCmd(0x11);
Delay_ms(255);
/* Frame Rate Control (Normal Mode) */
LCD_WriteCmd(0xB1);
LCD_WriteData8(0x01);
LCD_WriteData8(0x2C);
LCD_WriteData8(0x2D);
/* Frame Rate Control (Idle Mode) */
LCD_WriteCmd(0xB2);
LCD_WriteData8(0x01);
LCD_WriteData8(0x2C);
LCD_WriteData8(0x2D);
/* Frame Rate Control (Partial Mode) */
LCD_WriteCmd(0xB3);
LCD_WriteData8(0x01);
LCD_WriteData8(0x2C);
LCD_WriteData8(0x2D);
LCD_WriteData8(0x01);
LCD_WriteData8(0x2C);
LCD_WriteData8(0x2D);
/* Display Inversion Control */
LCD_WriteCmd(0xB4);
LCD_WriteData8(0x07);
/* Power Control 1 */
LCD_WriteCmd(0xC0);
LCD_WriteData8(0xA2);
LCD_WriteData8(0x02);
LCD_WriteData8(0x84);
/* Power Control 2 */
LCD_WriteCmd(0xC1);
LCD_WriteData8(0xC5);
/* Power Control 3 */
LCD_WriteCmd(0xC2);
LCD_WriteData8(0x0A);
LCD_WriteData8(0x00);
/* Power Control 4 */
LCD_WriteCmd(0xC3);
LCD_WriteData8(0x8A);
LCD_WriteData8(0x2A);
/* Power Control 5 */
LCD_WriteCmd(0xC4);
LCD_WriteData8(0x8A);
LCD_WriteData8(0xEE);
/* VCOM Control */
LCD_WriteCmd(0xC5);
LCD_WriteData8(0x0E);
/* Inversion Off - 先不反色,测试基础颜色 */
LCD_WriteCmd(0x20); /* Inversion Off */
/* Memory Access Control - 横屏模式, 交换行列 */
LCD_WriteCmd(0x36);
LCD_WriteData8(0x68); /* MV=1(交换行列), MY=0, MX=1, ML=0, BGR */
/* 0x68 = 0110 1000: MY=0, MX=1, MV=1, ML=0, BGR(bit3=1) */
/* Color Mode: 16-bit */
LCD_WriteCmd(0x3A);
LCD_WriteData8(0x05);
/* 先不设CASET/RASET范围,让填充函数自己设 */
/* Gamma Correction (Positive) */
LCD_WriteCmd(0xE0);
LCD_WriteData8(0x02); LCD_WriteData8(0x1C);
LCD_WriteData8(0x07); LCD_WriteData8(0x12);
LCD_WriteData8(0x37); LCD_WriteData8(0x32);
LCD_WriteData8(0x29); LCD_WriteData8(0x2D);
LCD_WriteData8(0x29); LCD_WriteData8(0x25);
LCD_WriteData8(0x2B); LCD_WriteData8(0x39);
LCD_WriteData8(0x00); LCD_WriteData8(0x01);
LCD_WriteData8(0x03); LCD_WriteData8(0x10);
/* Gamma Correction (Negative) */
LCD_WriteCmd(0xE1);
LCD_WriteData8(0x03); LCD_WriteData8(0x1D);
LCD_WriteData8(0x07); LCD_WriteData8(0x06);
LCD_WriteData8(0x2E); LCD_WriteData8(0x2C);
LCD_WriteData8(0x29); LCD_WriteData8(0x2D);
LCD_WriteData8(0x2E); LCD_WriteData8(0x2E);
LCD_WriteData8(0x37); LCD_WriteData8(0x3F);
LCD_WriteData8(0x00); LCD_WriteData8(0x00);
LCD_WriteData8(0x02); LCD_WriteData8(0x10);
/* Normal Display On */
LCD_WriteCmd(0x13);
Delay_ms(10);
/* Display On */
LCD_WriteCmd(0x29);
Delay_ms(100);
/* 关闭睡眠模式,防止自动息屏 */
LCD_WriteCmd(0x38); /* Idle Mode Off */
Delay_ms(10);
}
/* ========== 全屏填充颜色 ========== */
static void LCD_FillColor(uint16_t color)
{
uint16_t i;
uint32_t total = 162U * 132U; /* 写满整个内部显存,避免偏移计算误差 */
/* 设置写RAM窗口 (内部显存全范围 132x162) */
/* 横屏模式下 MV=1, 行列已交换, 但这里写的是内部显存地址, 不变 */
LCD_WriteCmd(0x2A); /* Column Address Set */
LCD_WriteData8(0x00); LCD_WriteData8(0x00); /* XS = 0 */
LCD_WriteData8(0x00); LCD_WriteData8(0xA1); /* XE = 161 */
LCD_WriteCmd(0x2B); /* Row Address Set */
LCD_WriteData8(0x00); LCD_WriteData8(0x00); /* YS = 0 */
LCD_WriteData8(0x00); LCD_WriteData8(0x83); /* YE = 131 */
LCD_WriteCmd(0x2C); /* Memory Write */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* DC = 1 */
for (i = 0; i < total; i++)
{
SoftSPI_WriteByte((uint8_t)(color >> 8));
SoftSPI_WriteByte((uint8_t)(color & 0xFF));
}
}
/* ========== 显示单个字符 (窗口写入模式,高效) ========== */
static void LCD_DrawChar(uint16_t x, uint16_t y, char ch, FontDef *font, uint16_t color, uint16_t bgColor)
{
uint32_t i, j, b;
uint16_t tmp;
/* 设置写RAM窗口: 列从 x 到 x+width-1, 行从 y 到 y+height-1 */
LCD_WriteCmd(0x2A); /* Column Address Set */
LCD_WriteData8((uint8_t)(x >> 8));
LCD_WriteData8((uint8_t)(x & 0xFF));
LCD_WriteData8((uint8_t)((x + font->width - 1) >> 8));
LCD_WriteData8((uint8_t)((x + font->width - 1) & 0xFF));
LCD_WriteCmd(0x2B); /* Row Address Set */
LCD_WriteData8((uint8_t)(y >> 8));
LCD_WriteData8((uint8_t)(y & 0xFF));
LCD_WriteData8((uint8_t)((y + font->height - 1) >> 8));
LCD_WriteData8((uint8_t)((y + font->height - 1) & 0xFF));
LCD_WriteCmd(0x2C); /* Memory Write */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* DC = 1 */
/* 字体数据: 行优先存储, 每行一个uint16_t, font->height个数据/字符 */
/* 有效位在高位: bit(16-width) ~ bit15 */
for (j = 0; j < font->height; j++)
{
tmp = font->data[(ch - 32) * font->height + j];
for (i = 0; i < font->width; i++)
{
b = tmp & (1 << (15 - i));
if (b)
LCD_WriteData16(color);
else
LCD_WriteData16(bgColor);
}
}
}
/* ========== 显示字符串 ========== */
static void LCD_DrawString(uint16_t x, uint16_t y, const char *str, FontDef *font, uint16_t color, uint16_t bgColor)
{
uint16_t curX = x;
while (*str != '\0')
{
LCD_DrawChar(curX, y, *str, font, color, bgColor);
curX += font->width;
str++;
}
}
/* ========== 画测试像素块 (用于验证显示是否正常) ========== */
static void LCD_DrawBlock(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color)
{
uint32_t i, total = (uint32_t)w * h;
LCD_WriteCmd(0x2A);
LCD_WriteData8((uint8_t)(x >> 8));
LCD_WriteData8((uint8_t)(x & 0xFF));
LCD_WriteData8((uint8_t)((x + w - 1) >> 8));
LCD_WriteData8((uint8_t)((x + w - 1) & 0xFF));
LCD_WriteCmd(0x2B);
LCD_WriteData8((uint8_t)(y >> 8));
LCD_WriteData8((uint8_t)(y & 0xFF));
LCD_WriteData8((uint8_t)((y + h - 1) >> 8));
LCD_WriteData8((uint8_t)((y + h - 1) & 0xFF));
LCD_WriteCmd(0x2C);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* DC = 1 */
for (i = 0; i < total; i++)
{
LCD_WriteData16(color);
}
}
/* ========== 系统时钟 ========== */
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_OscInitStruct.OscillatorType = 0x01U;
RCC_OscInitStruct.HSIState = 0x01U;
RCC_OscInitStruct.HSIDiv = 0x00U;
RCC_OscInitStruct.HSICalibrationValue = 0x00U;
RCC_OscInitStruct.LSIState = 0x00U;
if(HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { while(1); }
RCC_ClkInitStruct.ClockType = 0x07U;
RCC_ClkInitStruct.SYSCLKSource = 0x00U;
RCC_ClkInitStruct.AHBCLKDivider = 0x00U;
RCC_ClkInitStruct.APB1CLKDivider = 0x00U;
if(HAL_RCC_ClockConfig(&RCC_ClkInitStruct, 0x00U) != HAL_OK) { while(1); }
}
/* ========== 主函数 ========== */
int main(void)
{
HAL_Init();
SystemClock_Config();
/* 使能GPIO时钟 */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
MX_USART1_UART_Init();
LOG_INFO("System Init Done!!!");
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/* PA1(SCK)、PA4(DC)、PA7(MOSI) → 推挽输出 */
GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_7;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* PB0(CS)、PB1(BLK) → 推挽输出 */
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* 初始电平 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); /* BLK = 1 (点亮背光) */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); /* CS = 1 (未选中) */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); /* DC = 1 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); /* SCK = 0 (空闲低) */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET); /* MOSI = 0 */
LOG_INFO("GPIO Init Done!!!");
/* 初始化屏幕 */
LCD_Init();
/* 全屏白色背景 (横屏模式下需写满内部显存) */
__disable_irq();
LCD_FillColor(0xFFFF); /* 全白 */
__enable_irq();
/* 横屏模式: 交换行列后 */
/* CASET(列) 现在对应原长边160px, 偏移1, 有效范围1~160 */
/* RASET(行) 现在对应原短边80px, 偏移24, 有效范围24~103 */
/* 字符串沿CASET(列)方向排列,即沿屏幕长边水平排列 */
__disable_irq();
LCD_DrawString(1, 24, "Hello", &Font_11x18, 0xF800, 0xFFFF); /* 黑字白底, 从(1,24)开始 */
__enable_irq();
__disable_irq();
LCD_DrawString(55, 24, " World!", &Font_11x18, 0x07E0, 0xFFFF); /* 黑字白底, 从(55,24)开始 */
__enable_irq();
/* 静态显示,不再循环刷新 */
}
// 错误处理
void Error_Handler(void)
{
while(1)
{
HAL_Delay(100U);
}
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line)
{
}
#endif
