当前位置: 首页 > news >正文

普冉单片机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 值说明
0xF80011111 000000 00000红色(R=31, G=0, B=0)
0x07E000000 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颜色顺序
0RGB(默认)
1BGR

当前 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

http://www.jsqmd.com/news/822093/

相关文章:

  • 2026郑州市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • 3步配置Chrome for Testing:解决Web自动化测试的版本兼容难题
  • 2026庆阳市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • Python函数零配置转Web API:FuncToWeb原理、实战与避坑指南
  • 苏州蔷薇吊装搬运:苏州搬家搬厂哪家技术强 - LYL仔仔
  • 2026韶关市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • 2026 土工布厂家哪家口碑好:恒全土工布口碑出众 - 13724980961
  • 3分钟从图片到3D模型:Wonder3D如何让AI建模变得如此简单?
  • 3步掌握Awesome Public Datasets:从海量公开数据中挖掘商业洞察的实战指南
  • 2026韶山市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • Redis 6.0.6 在 Windows 下的另类‘安装’:不装服务,只做开发测试环境
  • 在金华婺城区卖金踩了两次坑,最后在润富黄金回收把钱拿回来了 - 润富黄金珠宝行
  • 2025现代Web技术栈:Vite、React、TypeScript与全栈类型安全实践
  • 值得收藏!银泰百货卡回收心得及避免陷阱的关键技巧 - 团团收购物卡回收
  • 400mA,最低输出电压0.8V,可调,高速LDO晨芯阳HC9624系列
  • 2026邛崃市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • 六足机器人深度解析:基于Linkit 7697的18自由度运动控制系统实战指南
  • 2026邵阳市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • 【笔记记录】windows server 2016不明缘由关机案例查证
  • 如何快速使用智能图像分层工具:新手完整入门指南
  • 终极中文版SSH客户端:安卓Termius汉化版完全使用指南
  • 2026年论文降AI工具亲测:20款横比,一篇看懂怎么选 - 降AI实验室
  • 2026琼海市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • 深圳品牌首饰梵克雅宝回收全维度测评:2026年5月五家机构实力大盘点 - 奢侈品回收测评
  • 2026曲阜市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • 1. logstash 常用命令
  • 2026绍兴市黄金回收白银回收铂金回收店铺实力排行榜TOP5; K金+金条+银条+首饰回收靠谱门店及联系方式推荐_转自TXT - 盛世金银回收
  • SAP ABAP 调试器进阶:从断点设置到高效排错实战
  • ROS新手避坑:在Gazebo里把小车和机械臂拼起来,结果模型‘穿模’了怎么办?
  • 2026年5月欧姆龙服务商最新推荐:PLC、传感器、自动化配件优选指南 - 海棠依旧大