普冉单片机PY32F002AF15P6TU + 0.96寸TFT ST7735s 80*160显示屏,使用软件SPI显示字符串“Hello World!”

书接上文使用软件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);
        }
    }
}

编译烧录测试。这次修改了两个关键点:

  1. 字体数据格式纠正 :从"列优先、每列一个 uint16_t"改为**"行优先、每行一个 uint16_t"**,每个字符占 height 个数据(不是 width 个)
  2. 位扫描顺序 :有效位在 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
相关推荐
踏着七彩祥云的小丑1 小时前
嵌入式测试学习第 10天:主控、外设、传感器、通信模块
单片机·嵌入式硬件
三佛科技-134163842122 小时前
FT32F072系列 FT32F072RBAT7/CBAT7/KBBT7/KBBU7/KBCW7单片机共性与区别详细分析
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
星夜夏空992 小时前
STM32单片机学习(11)——GPIO输入实验
stm32·单片机·学习
LingLong_roar2 小时前
手搓温湿度仪(单片机普冉PY32F002AF15P6TU + 温湿度传感器 SHT40-AD1B-R2 + 0.96寸TFT IPS 显示屏)软件实现
单片机·嵌入式硬件
黑白园2 小时前
STM32F103ZET6移植-电机2804-驱动板SimpleFOC Mini实现速度开环_位置开环控制(二、代码移植及功能实现)
stm32·单片机·嵌入式硬件
深圳市晨芯阳科技有限公司2 小时前
HC9623晨芯阳400mA带载、18V耐压、低压差快速响应LDO
单片机·嵌入式硬件·ldo线性稳压ic·深圳市晨芯阳科技有限公司
chengpei1473 小时前
电信创维E900盒子固件备份刷机
嵌入式硬件
星夜夏空9912 小时前
STM32单片机学习(10)——GPIO输入
stm32·单片机·学习
secondyoung14 小时前
Arm架构解析:Cortex-R系列架构概览
arm开发·单片机·嵌入式硬件·mcu·arm