本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目围绕1.44寸LCD液晶串口模块(驱动芯片为ST7735)与STM32单片机的硬件SPI通信展开,重点实现中文字符在小型彩色显示屏上的正确显示。项目提供完整测试源码,涵盖SPI接口初始化、ST7735驱动配置、汉字编码处理及字库调用等关键环节。适用于嵌入式系统开发中对低功耗、小尺寸屏幕中文显示需求的应用场景,帮助开发者掌握LCD驱动原理与实际编程技巧。

ST7735液晶驱动芯片与STM32的深度整合:从时序控制到中文显示全链路解析

在嵌入式系统设计中,图形化人机交互界面正逐渐成为标配。一块小小的1.44寸TFT彩屏,不仅能提升产品档次,还能极大增强用户体验。而在这背后,ST7735作为一款经典且广泛应用的LCD驱动控制器,配合性能强劲又成本低廉的STM32微控制器,构成了无数智能设备的“眼睛”。

你有没有遇到过这样的情况:明明代码写得一丝不苟,引脚也接得清清楚楚,可屏幕就是不亮?或者显示乱码、花屏、闪屏……这些问题往往不是硬件坏了,而是我们对底层时序和协议的理解还不够深入。今天,咱们就来一次彻底的“解剖”,把ST7735+STM32这套组合拳从里到外讲明白—— 不只是告诉你怎么做,更要让你知道为什么必须这么做


芯片内核剖析:ST7735到底是个啥?

别看它只有指甲盖大小,里面可是藏着不少门道。ST7735本质上是一个 高度集成的TFT面板驱动IC ,它的核心任务是将MCU送来的数字信号转换成能点亮像素的模拟电压。听起来简单?其实不然。

这个小家伙内部集成了 132×162 bit 的 GRAM(图形显示内存) ,虽然比不上手机动辄几MB的显存,但对于一个128×128的小屏来说已经绰绰有余了。它支持 16位RGB565色彩格式 ,也就是红5位、绿6位、蓝5位,总共可以显示 65,536 种颜色 🎨。这个选择非常巧妙——视觉效果足够好,数据量又不会太大,完美适配资源有限的MCU平台。

通信方面,它通过标准的 SPI接口 接收命令和数据。没错,就是那个你在传感器、Flash、EEPROM上经常用的SPI。但别小瞧它,这里的每一个字节都有其特定含义,而且顺序不能错!比如发送 0x11 是唤醒屏幕, 0x3A 是设置颜色模式, 0x29 是开启显示。这些命令必须按照严格的时序依次下发,否则轻则黑屏,重则死机。

⚠️ 小贴士:很多初学者以为只要初始化函数写了就能工作,结果忘了关键的一点—— 复位后必须等待足够长时间让内部振荡器稳定 。如果你发现每次上电都要按几次复位键才正常,很可能就是这里没加延时!

更有趣的是,ST7735还支持多种低功耗模式:
- Sleep In/Out :睡眠进出模式,关闭大部分电路;
- Standby Mode :待机模式,连主振荡器都停了;
- Display Off :仅关显示,GRAM仍保留数据;

这些特性让它特别适合电池供电的应用场景,比如智能手环、环境监测仪等。合理利用这些模式,可以让整机电流从几十mA降到μA级别 💡。


硬件连接的艺术:每一根线都不能马虎

再强大的软件也离不开扎实的硬件基础。要想让STM32和ST7735愉快地合作,首先要搞清楚它们之间的“对话语言”是怎么建立的。

屏幕参数知多少?

先来看看这块常见的1.44寸LCD模块的基本参数:

参数 数值
尺寸 1.44英寸(对角线约36mm)
分辨率 128 × 128 像素
色彩深度 16位 RGB565,65K色
显示类型 TFT LCD,有源矩阵
视角范围 水平/垂直 ≥ ±80°
面板技术 IPS 或 TN(依具体模块而定)

看到没?这可不是什么“马赛克屏”,IPS面板带来的宽视角表现,在不同角度下也能保持色彩一致性。不过要注意,并非所有标称“1.44寸”的模块都是128×128的。有些厂商为了省成本,会使用128×160的驱动逻辑,但只点亮中间128×128区域。这时候如果不正确配置MADCTL寄存器,就会出现偏移或裁剪问题 😵。

背光设计:亮度自由掌控的秘密

说到背光,很多人直接把它接到3.3V就完事了。但这有个大问题: 太费电!

实测数据显示,整个模块在全亮状态下电流可能超过 100mA ,其中背光电流占了50~90mA!对于靠纽扣电池运行的设备来说,这简直是“电量杀手”。

聪明的做法是什么?当然是 PWM调光 啦!用STM32的一个定时器输出PWM信号去控制BLK引脚,既能实现多级亮度调节,又能大幅降低平均功耗。

// 使用TIM3_CH1 输出PWM 控制背光
void MX_TIM3_PWM_Init(void) {
    __HAL_RCC_TIM3_CLK_ENABLE();

    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 83;        // 84MHz APB1 -> 1MHz计数频率
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 99;           // 10kHz PWM 频率
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}

void Set_Backlight_Level(uint8_t level) {
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, level); // level: 0~100
}

这样,你可以轻松实现三档亮度切换:

Set_Backlight_Level(25);   // 低亮,节能模式
Set_Backlight_Level(50);   // 中等,日常使用
Set_Backlight_Level(100);  // 全亮,阳光下可视

💡 经验之谈:PWM频率建议设在 10kHz以上 ,避免人耳听到“滋滋”声。低于1kHz时会有明显可闻噪声,影响体验。

引脚详解:谁是谁的“翻译官”

来看这张经典的接线表:

LCD引脚 STM32引脚 功能备注
VCC 3.3V 不可接5V!会烧毁模块 ❌
GND GND 必须共地 ✅
SCK PA5 SPI1_SCK
MOSI PA7 SPI1_MOSI
CS PB0 软件控制片选
DC PB1 数据/命令标志
RST PC13 可选硬件复位
BLK PA6 连接TIM3_CH1用于PWM调光

注意几个易错点:
- VCC只能接3.3V ,绝对不要图方便接开发板的5V输出,否则分分钟变“冒烟实验”;
- CS、DC、RST都是普通GPIO ,不需要复用功能,但要确保初始化时拉高,防止误触发;
- BLK是否需要外部驱动 取决于模块设计,有的内部已上拉,默认常亮;有的则需外部驱动才能点亮。

下面这张mermaid流程图清晰展示了连接关系:

graph TD
    A[STM32F103] -->|SPI1_SCK| B(SCK)
    A -->|SPI1_MOSI| C(MOSI)
    A -->|PB0| D(CS)
    A -->|PB1| E(DC)
    A -->|PC13| F(RST)
    A -->|PA6/PWM| G(BLK)
    H[VCC 3.3V] --> A
    I[GND] --> A
    B --> J[ST7735]
    C --> J
    D --> J
    E --> J
    F --> J
    G --> J
    style J fill:#f9f,stroke:#333

是不是一目了然?这种结构化的表达方式比纯文字描述直观多了 👍。

对应的GPIO初始化代码如下:

void LCD_GPIO_Init(void) {
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();

    GPIO_InitTypeDef gpio = {0};

    // SCK & MOSI: AF_PP, 复用推挽
    gpio.Pin = GPIO_PIN_5 | GPIO_PIN_7;
    gpio.Mode = GPIO_MODE_AF_PP;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &gpio);

    // CS, DC: Output PP
    gpio.Pin = GPIO_PIN_0;
    gpio.Mode = GPIO_MODE_OUTPUT_PP;
    HAL_GPIO_Init(GPIOB, &gpio);

    gpio.Pin = GPIO_PIN_1;
    HAL_GPIO_Init(GPIOB, &gpio);

    // RST: PC13
    gpio.Pin = GPIO_PIN_13;
    HAL_GPIO_Init(GPIOC, &gpio);

    // 默认状态
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // CS=H
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); // DC=H
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // RST=H
}

重点来了: PA5和PA7必须配置为AF_PP(复用推挽输出) ,这样才能启用SPI1的硬件功能;而其他控制线用普通推挽即可。顺序也不能颠倒——一定要先开时钟,再配引脚!


SPI通信的灵魂:模式匹配与时序精准度

你以为SPI就是随便发几个字节那么简单?Too young too simple!

SPI有四种工作模式,由 CPOL(Clock Polarity) CPHA(Clock Phase) 决定:

模式 CPOL CPHA 描述
Mode 0 0 0 空闲低电平,上升沿采样 ✅
Mode 1 0 1 空闲低电平,下降沿采样
Mode 2 1 0 空闲高电平,下降沿采样
Mode 3 1 1 空闲高电平,上升沿采样 ⚠️

查ST7735手册可知,它默认支持 Mode 0 和 Mode 3 ,大多数模块出厂设定为 Mode 0 。也就是说:
- SCK空闲时为 低电平
- 数据在 上升沿被采样
- MOSI应在下降沿改变数据以保证建立时间。

所以你的SPI配置必须这么写:

hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;     // CPOL = 0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;         // CPHA = 0 → Mode 0
hspi1.Init.NSS = SPI_NSS_SOFT;                 // 软件控制CS
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;

其中最关键的是:
- CLKPolarity = LOW :SCK空闲为低;
- CLKPhase = 1EDGE :第一个边沿(上升沿)采样;
- NSS = SOFT :禁用硬件NSS,由软件控制CS;
- FirstBit = MSB :高位先行,符合协议要求。

如果通信失败,别急着换线,先试试改成Mode 3看看能不能通。有时候个别模块确实有点“个性” 😂。

波特率怎么选?速度与稳定的博弈

SPI速率也不是越快越好。ST7735官方文档写着最大支持 15MHz (部分版本可达27MHz),但我们得留足裕量。

假设STM32主频72MHz,APB2总线也是72MHz(SPI1挂载于此),那么预分频后的实际SCK频率如下:

预分频值 实际SCK频率 是否推荐
2 36 MHz ❌ 超限,绝对不行
4 18 MHz ⚠️ 边缘试探,风险高
8 9 MHz ✅ 安全稳妥
16 4.5 MHz ✅ 低速可靠

我强烈建议使用 SPI_BAUDRATEPRESCALER_8 ,即9MHz。既快又稳,兼顾效率与兼容性。

hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 72MHz / 8 = 9MHz

除非你在极端恶劣的电磁环境下工作(比如电机旁边),否则没必要降到16甚至32。那样刷新率会严重受限,动画卡顿感明显。


系统启动的“交响乐”:时钟→GPIO→SPI的执行序曲

嵌入式系统的初始化就像一场精密的交响乐演奏,每个乐器(外设)何时登场都有严格规定。一旦节奏乱了,整首曲子就废了。

第一步:敲响时钟的钟声

一切始于RCC(复位和时钟控制)。没有正确的时钟,CPU都无法运行,更别说外设了。

通常我们会先启用HSI(8MHz内部RC振荡器),然后通过PLL倍频到72MHz系统主频。这是F1系列的经典操作:

RCC_OscInitTypeDef oscConfig = {0};
RCC_ClkInitTypeDef clkConfig = {0};

oscConfig.OscillatorType = RCC_OSCILLATORTYPE_HSI;
oscConfig.HSIState = RCC_HSI_ON;
oscConfig.PLL.PLLState = RCC_PLL_ON;
oscConfig.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;  // HSI/2 = 4MHz
oscConfig.PLL.PLLMUL = RCC_PLL_MUL16;               // 4MHz * 16 = 64MHz ← 注意这里是64MHz!

if (HAL_RCC_OscConfig(&oscConfig) != HAL_OK) {
    Error_Handler();
}

clkConfig.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK |
                     RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clkConfig.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clkConfig.AHBCLKDivider = RCC_SYSCLK_DIV1;
clkConfig.APB1CLKDivider = RCC_HCLK_DIV2;
clkConfig.APB2CLKDivider = RCC_HCLK_DIV1;

if (HAL_RCC_ClockConfig(&clkConfig, FLASH_LATENCY_2) != HAL_OK) {
    Error_Handler();
}

⚠️ 注意一个常见误区:有人想当然地认为 HSI_DIV2 * MUL9 = 72MHz ,但实际上HSI是8MHz,除以2是4MHz,乘以9才36MHz!想要接近72MHz,得用 MUL16 得到64MHz,或者改用HSE晶振。

下面是完整的初始化流程图:

graph TD
    A[上电复位] --> B{是否启用外部晶振?}
    B -->|否| C[启动HSI 8MHz]
    B -->|是| D[启动HSE 8MHz]
    C --> E[配置PLL: HSI/2 * MUL]
    D --> F[配置PLL: HSE * MUL]
    E --> G[切换SYSCLK至PLLCLK]
    F --> G
    G --> H[设置AHB/APB分频器]
    H --> I[Flash等待周期配置]
    I --> J[系统主频就绪]

第二步:铺好GPIO的道路

时钟准备好之后,赶紧把要用的GPIO配置好。记住一句话: 外设初始化前,相关引脚必须完成配置

特别是SPI引脚,必须提前设为复用推挽输出模式:

__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitTypeDef gpioInit = {0};
gpioInit.Pin = GPIO_PIN_5 | GPIO_PIN_7;
gpioInit.Mode = GPIO_MODE_AF_PP;          // 复用推挽
gpioInit.Speed = GPIO_SPEED_FREQ_HIGH;    // 高速
HAL_GPIO_Init(GPIOA, &gpioInit);

其他控制线如CS、DC、RST则设为普通输出即可:

gpioInit.Pin = GPIO_PIN_0 | GPIO_PIN_1;
gpioInit.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOB, &gpioInit);

千万别忘了使能对应端口的时钟!否则配置无效。可以用这张表快速对照:

外设 对应使能宏 所属总线
GPIOA~G __HAL_RCC_GPIOX_CLK_ENABLE() AHB
SPI1 __HAL_RCC_SPI1_CLK_ENABLE() APB2
USART1 __HAL_RCC_USART1_CLK_ENABLE() APB2
I2C1 __HAL_RCC_I2C1_CLK_ENABLE() APB1

第三步:激活SPI外设

最后才是调用 HAL_SPI_Init()

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

整个顺序必须是:

SystemClock_Config();           // 1. 配置系统主频
MX_GPIO_Init();                 // 2. 配置引脚
MX_SPI1_Init();                 // 3. 初始化SPI
LCD_ST7735_Init();              // 4. 发送LCD初始化命令

任何颠倒都会导致失败。比如你先初始化SPI,但此时GPIO还没设成复用模式,那SPI根本驱动不了物理引脚!


时序的生命线:每一个ns都至关重要

别以为现在就能开始画图了?NO!真正的挑战才刚刚开始。

ST7735对时序的要求极为苛刻。哪怕只是差了几百纳秒,也可能导致命令丢失或数据错位。让我们来看几个关键参数:

参数 含义 最小值
Tcss 片选有效前建立时间 100 ns
Tcsh 片选无效后保持时间 100 ns
Tdsu 数据建立时间(相对SCK上升沿) 10 ns
Tdhd 数据保持时间 55 ns

什么意思呢?举个例子:当你拉低CS准备通信时,必须确保至少提前100ns就把DC和其他信号准备好;传输结束后,CS拉高后还得维持100ns以上的高电平才算结束。

下面是命令写入的完整时序图:

sequenceDiagram
    participant MCU
    participant ST7735
    Note over MCU,ST7735: SPI Command Write Timing
    MCU->>ST7735: CS↓ (Tcss ≥ 100ns)
    MCU->>ST7735: DC=0 (提前设置)
    loop SCK Clock Cycle
        MCU->>ST7735: SCK↑ → MOSI valid (Tdsu≥10ns)
        MCU->>ST7735: SCK↓ → hold data (Tdhd≥55ns)
    end
    MCU->>ST7735: CS↑ (Tcsh≥100ns)

虽然HAL库做了封装,但在高频下仍可能出现竞争条件。因此建议在调试阶段加入超时保护:

HAL_StatusTypeDef SPI_WriteByte(uint8_t byte) {
    uint32_t tickstart = HAL_GetTick();
    while (!(__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXE))) {
        if ((HAL_GetTick() - tickstart) > 100) {
            return HAL_TIMEOUT;
        }
    }
    hspi1.Instance->DR = byte;
    tickstart = HAL_GetTick();
    while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_BSY)) {
        if ((HAL_GetTick() - tickstart) > 100) {
            return HAL_TIMEOUT;
        }
    }
    return HAL_OK;
}

监控这些标志位能有效防止程序卡死,尤其是在电源不稳定或干扰较强的环境中。


中文显示的魔法:如何让汉字跃然屏上?

终于到了最激动人心的部分——让我们的屏幕不再只是英文和数字的天下,而是真正支持中文!

编码体系的选择

首先得搞清楚你要处理的是哪种编码:
- GB2312 :双字节,简体中文常用,适合固定文本;
- GBK :扩展版,包含繁体字;
- UTF-8 :国际化首选,汉字一般3字节。

判断一个字符是不是汉字也很简单:

uint8_t is_chinese(uint8_t high_byte, uint8_t low_byte) {
    return (high_byte >= 0xB0 && high_byte <= 0xF7) &&
           (low_byte >= 0xA1 && low_byte <= 0xFE);
}

效率极高,遍历字符串时一秒几千次没问题。

字库生成与存储策略

汉字无法像ASCII那样枚举,必须依赖字模。工具推荐 PCtoLCD2002 ,可以导出16×16、24×24等格式的横向取模数组。

例如“中”字的16×16点阵:

const unsigned char chinese_zhong_16x16[] = {
    0x00,0x00,0x3F,0xFC,0x20,0x08,0x20,0x08,...
};

所有字模组织成结构体数组:

typedef struct {
    uint16_t code;
    const uint8_t *matrix;
} chinese_font_t;

const chinese_font_t font_table[] = {
    {0xB0A1, chinese_zhong_16x16},
    {0xB0A2, chinese_guo_16x16},
    // ...
};

根据需求选择存储方式:

存储方式 容量上限 访问速度 成本 适用场景
内部Flash数组 ~100–300字 极快 固定提示文本
外部SPI Flash 数千至上万字 中等 较高 多语言UI、电子书
动态加载 无限 OTA更新、网络终端

高效绘制技巧

单个像素点逐个画太慢!我们应该按行批量写入:

void ShowChinese(uint16_t x, uint16_t y, uint16_t unicode) {
    const uint8_t *p = get_font_data(unicode);
    if (!p) return;

    for (int row = 0; row < 16; row++) {
        LCD_SetWindow(x, y + row, x + 15, y + row);
        for (int col = 0; col < 16; col++) {
            uint8_t byte = p[row * 2 + (col / 8)];
            uint8_t bit = (byte >> (7 - (col % 8))) & 0x01;
            LCD_FillColor(bit ? RED : BLACK);
        }
    }
}

更进一步,引入DMA传输和帧缓冲机制,实现滚动文本:

#define FB_WIDTH  128
#define FB_HEIGHT 16
uint16_t frame_buffer[FB_WIDTH * FB_HEIGHT];

void ScrollText(const char *text) {
    static int offset = 0;
    memset(frame_buffer, 0, sizeof(frame_buffer));
    PrintToBuffer(frame_buffer, text + offset);
    LCD_WriteFrameDMA(frame_buffer);
    offset = (offset + 1) % strlen(text);
}

搭配定时器中断,每100ms滚动一像素,CPU占用率从90%降至不到10%,简直不要太爽 😎。

数据流路径如下:

graph TD
    A[文本字符串] --> B{是否汉字?}
    B -->|是| C[查字模表]
    B -->|否| D[ASCII字体渲染]
    C --> E[生成点阵]
    D --> F[写入帧缓冲]
    E --> F
    F --> G[DMA+SPI传输]
    G --> H[LCD显示]

最终验证:点亮属于你的第一行中文

万事俱备,只欠东风。main函数就这么写:

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();
    LCD_Init();
    LCD_Clear(BLACK);
    ShowChinese(10, 10, 0x4E2D);    // “中”
    ScrollText("欢迎使用嵌入式中文显示系统");

    while (1) {
        HAL_Delay(100);
    }
}

编译烧录步骤:
1. 使用 Keil MDK 或 STM32CubeIDE;
2. 包含 st7735.c 和字库头文件;
3. 设置 Flash 下载算法;
4. 编译并烧录;
5. 上电观察效果!

🎉 成功那一刻,你会觉得之前所有的调试和等待都值得。


结语:从像素到体验的跨越

一块小小的屏幕,背后凝聚的是软硬件协同设计的智慧。从ST7735的寄存器配置,到SPI时序的精准把控,再到中文显示的优雅实现,每一步都在考验工程师的耐心与理解力。

希望这篇文章不仅帮你点亮了屏幕,更能点燃你对嵌入式开发的热情 🔥。下次当你看到设备上的文字缓缓滑过时,你会知道——那是你亲手写下的诗。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目围绕1.44寸LCD液晶串口模块(驱动芯片为ST7735)与STM32单片机的硬件SPI通信展开,重点实现中文字符在小型彩色显示屏上的正确显示。项目提供完整测试源码,涵盖SPI接口初始化、ST7735驱动配置、汉字编码处理及字库调用等关键环节。适用于嵌入式系统开发中对低功耗、小尺寸屏幕中文显示需求的应用场景,帮助开发者掌握LCD驱动原理与实际编程技巧。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐