简介:这份资源是基于STM32 HAL库驱动HT1621B液晶显示模块的完整示例工程,面向嵌入式初学者或需要快速实现段式LCD驱动的开发者,解决STM32与HT1621B之间SPI通信配置及显示控制问题。压缩包仅3个文件,分别为HT1621B的C源码、头文件及README说明,整体大小仅3KB,代码精简、结构清晰,适合直接移植到自己的项目中。已有1391人学习下载。通过该示例可学习HAL库下SPI初始化、引脚复用配置、命令与数据发送函数编写,以及清屏、字符显示等基本操作,并了解HT1621B内部振荡器、电荷泵等特性。对照代码可快速掌握用STM32驱动128段点阵LCD的完整流程,为后续开发类似显示应用提供可复用的底层驱动模板。
1. 从一次显示异常说起:为什么 HT1621B 不能用标准 SPI 硬核驱动
手头有个表头项目,主控是 STM32F103C8T6,显示部分选了 HT1621B 驱动的段式 LCD 屏。最初想当然地认为芯片带串行接口就能直接用 SPI 外设,结果初始化后屏幕要么全亮、要么乱码,用逻辑分析仪抓波形才发现 HT1621B 虽然只有 CS、WR、DATA 三根线,但它的时序和标准 SPI 的 CPOL/CPHA 组合对不上,数据格式也不是 SPI 的 8 位连续帧,而是芯片自定义的 3 线串行协议。这篇博文的内容全部基于HT1621B_STM32_HAL-master这个工程展开,工程里 HT1621B.c/h 的写法很有代表性,不依赖具体型号,HAL 库的封装让 GPIO 模拟时序变得非常干净。适合正在用 STM32 驱动段式 LCD、或者想搞明白 HT1621B 时序细节的开发者,通篇会给出可直接抄的代码和参数说明,避免重走我当初对着手册干瞪眼的老路。
2. HT1621B 的指令结构与 RAM 映射机制
2.1 芯片选型依据:128 段是怎么算出来的
HT1621B 属于 Holtek 的 LCD 驱动器家族,内部框图大致分三块:振荡器与偏压发生器、指令解码器、显示 RAM。芯片最大支持 32x4 的扫描配置,也就是最多 128 个段(SEG)和 4 个公共端(COM)。常见的段式屏型号定义成 8x8 点阵或 4 位数字加符号,本质都是在这 128 个段里做分配。
选型上需要注意,HT1621B 和 HT1621 并不完全等同。HT1621B 工作电压范围更宽,典型 2.4V~5.2V,内部 RC 振荡器频率约 32kHz,这决定了显示帧率。如果你的屏面积大、段数多,32kHz 驱动 4 个 COM 口时刷新率大约 64Hz,肉眼看不到闪烁。而用外部时钟模式时,WDT 溢出时间会变化,这一点在低功耗场景里要算清楚,后面第 3 章会给出具体计算。
工程里 README 提到通过 SPI 或 I2C 控制 HT1621B,这个说法容易误导。HT1621B 硬件上并没有 I2C 模块,它只有 CS、WR、DATA 三根线组成的专用 3 线接口。说 SPI 驱动只是因为它和 SPI 的 MOSI、SCK、CS 有物理上的对应关系,但实际时序是 HT1621B 自己定义的。
2.2 命令分类:101 命令与 110 命令的差异
HT1621B 的命令通过 DATA 线逐位发送,所有命令都以一个识别码开头。命令识别码有两类,这一点在工程代码里体现得非常明显:
| 识别码 | 命令类型 | 具体命令 | 位数 |
|---|---|---|---|
| 101 | 系统命令 | SYS DIS、SYS EN、LCD OFF、LCD ON | 9 位 |
| 101 | 系统命令 | RC 256K、RC 32K、EXT 256K、EXT 32K | 9 位 |
| 101 | 系统命令 | 偏压设置 BIAS 1/2、BIAS 1/3 | 9 位 |
| 110 | 显示命令 | 清屏、当前地址、4 位数据 | 11 位 |
| 100 | 读命令 | 读取当前地址的 4 位数据 | 11 位 |
101 开头的命令只有 9 位,前 3 位是识别码,后 6 位是命令内容。110 开头的命令是写显示数据,前 3 位识别码,跟着 6 位 RAM 地址,最后 4 位是数据。100 开头的读命令特殊一点,需要 CS 拉低后先发识别码和地址,再切换 DATA 方向为输入读取 4 位数据。
工程里的 HT1621B.c 对命令发送的封装思路是统一的底层 write 函数加高层命令函数。比如HT1621B_WriteCmd内部就是按位操作 DATA 引脚,先发 3 位识别码再发命令内容,CS、WR 的拉高拉低顺序严格按照数据手册的时序图来。这里有个细节容易漏:写命令和写数据在 WR 上升沿锁存,而读数据是在 WR 下降沿之后读,方向切换前必须把 DATA 引脚配置成输入模式。
2.3 RAM 地址与段-COM 交叉关系
HT1621B 的显示 RAM 有 32x4 位,地址从 0x00 到 0x1F,每个地址对应 4 个 COM 口上的段。注意地址的第 5 位(A4)决定访问的是普通显示 RAM 还是时间基准生成器,如果写入地址大于 0x1F,数据会落到时间基准模块而不是显示缓冲区,这是新手容易踩的雷。
段和 COM 的交叉关系是:地址每增加 1,对应到物理 SEG 引脚加 1,同一个地址内的 4 位分别对应 COM0 到 COM3。所以当你要点亮一个特定位置的段时,不能只发一个字节,而要按 COM 维度分别写 4 个 bit。工程里如果实现了HT1621B_DisplayNumber这类函数,它内部一定有一个把用户数据拆成 COM 位平面的步骤,这一点后续驱动代码章节会具体展开。
3. STM32 工程搭建与低层 GPIO 时序实现
3.1 基于 CubeMX 的引脚初始化配置
工程基于 STM32 HAL 库,用 CubeMX 生成初始化代码。HT1621B 三根线推荐用普通 GPIO 输出模式,多数人习惯选推挽输出、无上下拉、输出速度 Low,因为 WR 和 DATA 的翻转频率通常只有几百 kHz,不需要 High 档。引脚分配建议:
| 功能 | 引脚 | GPIO 模式 |
|---|---|---|
| CS | PB0 | 推挽输出,初始高 |
| WR | PB1 | 推挽输出,初始高 |
| DATA | PB2 | 推挽输出,初始高 |
有人会把 DATA 配置成复用开漏模式以兼容读操作,但 HT1621B 的读命令用得少,而且切换方向需要重新配置 MODER 寄存器,不如保持推挽输出,读数据时动态改成输入模式更直观。在 CubeMX 里配置好后,生成代码中会看到MX_GPIO_Init函数统一设置这三个引脚。
初始化函数里需要特别注意三根线的初始电平。CS 在空闲时必须保持高电平,WR 和 DATA 也建议初始化为高。如果 CS 上电后是低电平,HT1621B 可能进入异常状态,表现为屏幕乱码或功耗偏高。
3.2 位级时序封装:三种基础操作函数
工程底层会封装三个基础函数:HT1621B_WriteBit、HT1621B_WriteByte、HT1621B_SendCmd。HT1621B_WriteBit实现的是最底层的时序,每次写一个 bit 需要 4 个动作:DATA 置为对应电平、WR 拉低、延时、WR 拉高。代码如下:
void HT1621B_WriteBit(uint8_t bit_val) { HAL_GPIO_WritePin(HT1621B_DATA_PORT, HT1621B_DATA_PIN, bit_val); HAL_GPIO_WritePin(HT1621B_WR_PORT, HT1621B_WR_PIN, GPIO_PIN_RESET); HT1621B_Delay_us(1); // 数据建立时间,手册要求 tSU 至少 0.1us HAL_GPIO_WritePin(HT1621B_WR_PORT, HT1621B_WR_PIN, GPIO_PIN_SET); HT1621B_Delay_us(1); // WR 高电平持续时间,tHIGH 至少 0.6us }这里的HT1621B_Delay_us工程里一般用DWT->CYCCNT实现精确延时。如果不方便用 DWT,可以用空循环for (volatile uint8_t i = 0; i < 10; i++);代替,但要注意不同主频下延时不一致,影响极端时序下的可靠性。写命令和写数据的区别只是 bit 数不同,命令是 9 位,数据是 11 位(3 位识别码加 8 位地址/数据组合),发送前先拉低 CS,发完再拉高 CS:
void HT1621B_SendCmd(uint8_t cmd) { HAL_GPIO_WritePin(HT1621B_CS_PORT, HT1621B_CS_PIN, GPIO_PIN_RESET); for (uint8_t i = 0; i < 8; i++) { HT1621B_WriteBit((cmd >> (7 - i)) & 0x01); } HT1621B_WriteBit(0); // 命令位比数据少,末尾补齐一位 HAL_GPIO_WritePin(HT1621B_CS_PORT, HT1621B_CS_PIN, GPIO_PIN_SET); }注意命令发送结束要补一个 bit,原因是 HT1621B 的命令时钟格式是固定长度的,9 位命令识别码加命令内容总位数不足时,芯片后续可能把下一个操作误认为是数据。实际上标准的命令定义是101 + 6 位命令,共 9 位,上面的函数发送的字节需要配合cmd的高 6 位有效。
3.3 写数据与读回:方向切换的注意事项
写显示数据时,格式是 3 位识别码110,后面 6 位是 RAM 地址,再后面 4 位是要写入的数据,总共 13 位。工程代码里的实现:
void HT1621B_WriteData(uint8_t addr, uint8_t data) { HAL_GPIO_WritePin(HT1621B_CS_PORT, HT1621B_CS_PIN, GPIO_PIN_RESET); for (uint8_t i = 0; i < 3; i++) { HT1621B_WriteBit(1); // 110 识别码前两位 } HT1621B_WriteBit(0); // 110 识别码第三位 for (uint8_t i = 0; i < 6; i++) { HT1621B_WriteBit((addr >> (5 - i)) & 0x01); } for (uint8_t i = 0; i < 4; i++) { HT1621B_WriteBit((data >> (3 - i)) & 0x01); } HAL_GPIO_WritePin(HT1621B_CS_PORT, HT1621B_CS_PIN, GPIO_PIN_SET); }这段代码有几个容易写错的地方。第一,地址位顺序是高位在前,如果从低位开始移位,地址映射会颠倒;第二,data 只取低 4 位,如果外部传入了高 4 位有值的内容,显示会错乱;第三,CS 必须在整个传输期间保持低电平,不能在每个字节之间拉高。
读回操作用100识别码开头的命令。执行读命令时,DATA 引脚要切换为输入模式,代码里需要调用HAL_GPIO_ReadPin之前先修改 GPIO 的 MODER 寄存器。HAL 库没有直接提供切换方向的接口,常见做法是:
GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = HT1621B_DATA_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(HT1621B_DATA_PORT, &GPIO_InitStruct);读完之后还要切回输出模式。频繁切换会降低效率,所以实际项目中读回功能一般只在自检时使用,正常显示流程不会调用。
4. 驱动层设计:从初始化序列到显示缓冲
4.1 初始化序列的完整顺序
HT1621B 的初始化顺序是工程里 README 没有细说、但实际最容易出错的部分。按照数据手册的推荐顺序,初始化序列如下:
void HT1621B_Init(void) { HT1621B_WriteCmd(HT1621B_CMD_SYS_DIS); // 先关闭系统 HT1621B_WriteCmd(HT1621B_CMD_RC_32K); // 选择内部 32kHz RC 振荡器 HT1621B_WriteCmd(HT1621B_CMD_BIAS_1_3); // 设置 1/3 偏压,4 COM 模式 HT1621B_WriteCmd(HT1621B_CMD_LCD_ON); // 打开 LCD 驱动输出 HT1621B_Clear(); // 清屏并关闭看门狗 }HT1621B_CMD_SYS_DIS的值是 0x00,注意它是 9 位命令里的高 6 位有效,8 位变量发送时低位补零,所以一个字节表示正好。先发 SYS DIS 的目的是让芯片在上电后处于完全关闭状态,避免内部升压电路和偏压发生器的状态不确定。
RC_32K命令选择内部振荡器,对应命令字节的高 6 位是000000还是000001需要看数据手册的表,不同版本手册命令码有差异。工程里HT1621B.h定义宏HT1621B_CMD_RC_32K 0x18,换算成二进制是000110,这个值对应该命令在数据位中的排列方式。使用 32kHz 内部振荡器时,BIAS_1_3的偏压设置决定 LCD 的对比度,1/3 偏压适合大多数 TN 屏,1/2 偏压适合低电压场景,但对比度会偏浅。
初始化完成后的清屏操作需要遍历 32 个地址写 0:
void HT1621B_Clear(void) { for (uint8_t addr = 0; addr < 32; addr++) { HT1621B_WriteData(addr, 0x00); } }清屏的另一层作用是关闭看门狗。HT1621B 内部有一个集成 WDT,上电默认开启,溢出时间取决于振荡器频率。如果不喂狗或关闭它,芯片会周期性复位,表现为屏幕每隔一段时间闪一下。工程里如果HT1621B_Clear里只写了数据没提 WDT,需要补一条HT1621B_WriteCmd(HT1621B_CMD_WDT_DIS),命令值是 0x02 还是 0x03,务必对照手上的原版头文件宏定义。
4.2 显示缓冲区与段映射函数
HT1621B 一次只能写 4 位数据,但实际应用往往以字节为单位显示 ASCII 字符或自定义符号,所以驱动层需要建一个 16 字节的显示缓冲,把逻辑上的字符映射到物理段地址,再刷新到芯片。这层映射关系建议这样设计:
uint8_t ht1621b_buffer[16]; // 高 4 位对应 COM3~COM0,低 4 位对应另一组 void HT1621B_Refresh(void) { for (uint8_t addr = 0; addr < 16; addr++) { HT1621B_WriteData(addr, ht1621b_buffer[addr] & 0x0F); HT1621B_WriteData(addr + 0x10, (ht1621b_buffer[addr] >> 4) & 0x0F); } }这种双地址映射方式的依据是 HT1621B 的 RAM 地址 0x00 到 0x1F 直接对应 SEG 引脚顺序,如果屏的引脚走线把高 4 位设计到了 SEG16 以后,就要用地址 +0x10 的方式把高位数据写到另一组地址。实际上 32x4 的 RAM 中地址 0x00~0x0F 和 0x10~0x1F 分别对应不同 SEG 段,所以上面这个函数覆盖了全部 32 个地址。
如果后续要更新数字显示,直接改 buffer 再调用 Refresh 就行。这种方式有额外的好处:统一了内部逻辑和硬件引脚之间的关系,调整屏幕布局时只改映射函数,上层业务代码不用动。
4.3 字符取模与段码转换
共阴极段式 LCD 的字符码与共阳极数码管类似但不完全相同。HT1621B 的段驱动输出是对称的,没有共阴共阳之分,只要给出 SEG 与 COM 的交叉矩阵就能确定字符形状。工程里通常会带一张段码表:
const uint8_t seg_code[10] = { 0x7E, // 0 0x30, // 1 0x6D, // 2 0x79, // 3 0x33, // 4 0x5B, // 5 0x5F, // 6 0x70, // 7 0x7F, // 8 0x7B // 9 };上面这组段码假设数字的 7 段分别映射到 COM0~COM3 的某几位,实际上要根据屏幕的引脚定义调整。具体做法是:拿一颗万用表或直接用 HT1621B 的测试模式,依次点亮每个地址的每个 bit,在屏幕上记下对应的段位置,再反推段码表。这个工作看起来很原始,但比对着原理图猜要准确得多。
显示一个两位数时,先把数字转换成段码存进 buffer 的对应字节,再统一刷新。这里要注意高位数字和低位数字的地址偏移量,通常两个数字之间隔着 4 个 SEG 即一个字节,如果布局中使用的是 COM0~COM3 中不同的位,还需要做位拼接处理。
5. 进阶功能与常见坑:对比度、闪烁和命令时序
5.1 对比度调节与偏压的选择误区
HT1621B 的对比度主要受偏压设置和 LCD 供电电压影响,软件层面能控制的只有BIAS_1_2和BIAS_1_3两个命令。偏压比例决定 LCD 驱动波形的 VON/VOFF 比,1/3 偏压下 VON 约为 LCD 电压的 2/3,1/2 偏压下 VON 约为 1/2,后者功耗更低但对比度弱。
很多工程习惯把偏压设置放在初始化序列里写死,这会导致一个问题:3.3V 供电下 1/3 偏压可能显示过深,5V 供电下 1/2 偏压又偏浅。我一般会在头文件里留一个宏定义,不同的屏幕型号改一个宏即可。
另外注意 LCD 偏压发生器的启动时间,命令LCD_ON发出后大约需要 100~300ms 电压才能稳定,此时立刻刷新显示数据会出现前几帧颜色偏淡。工程里如果有开机 logo 的显示流程,最好在LCD_ON后加个 200ms 延时再刷内容。
5.2 显示闪烁的两个隐藏来源:WDT 与刷新频率
前面提到看门狗会周期性复位芯片,这是隐藏的闪烁来源之一。另一个来源是刷新频率不足或刷新间隙不均匀。HT1621B 的显示刷新是芯片自己循环扫描的,外部只需要在数据变化时写入新内容,理论上不会闪烁。但如果用了软件定时器刷新整个 buffer,而刷新函数里带着长延时或复杂运算,就会出现刷新间隙内某一帧数据被截断。
解决方式是不要频繁全量刷新,尽量在数据变化时只更新对应地址:
void HT1621B_UpdateDigit(uint8_t position, uint8_t number) { uint8_t addr = position * 2; // 假设每个数字占 2 个地址 HT1621B_WriteData(addr, seg_code[number] & 0x0F); HT1621B_WriteData(addr + 1, (seg_code[number] >> 4) & 0x0F); }如果一定要定时刷新,比如实现闪烁效果,用好的做法是写两个 buffer,切换显示内容时先修改影子缓冲再一次性刷新,避免单个字节写入时屏幕出现半更新状态。
5.3 工程代码中 SPI 配置错误的分析
不少初次接触 HT1621B 的开发者会在 CubeMX 里直接配置 SPI 外设,接到 HT1621B 上后屏幕不工作。根本原因是 SPI 的数据帧和 HT1621B 的时序不兼容。HT1621B 要求 CS 在整个命令传输期间保持低电平,而 SPI 外设每个字节传输之间会自动拉高 CS(如果是硬件 NSS 管理)或者需要软件控制拉高拉低,这就破坏了命令的连续性。
另一个问题是字节对齐。HT1621B 的命令和数据的位长不是 8 的整数倍,比如写数据是 3+6+4=13 位,SPI 一次只能发 8 位或 16 位,拆成 16 位发送时高位数据位和低位数据位会错位。也有一种做法是用 SPI 的 8 位模式连续发两个字节,然后手动控制 CS,但此时仍要对齐位流,代码复杂度反而比 GPIO 模拟更高。
工程采用 GPIO 模拟时序是更通用的方案,因为它不依赖 SPI 外设的存在,任何有 GPIO 的 MCU 都能移植。如果换用其他芯片,只要把HT1621B_Delay_us改成对应平台的微秒延时函数即可。
6. 验证技巧:用逻辑分析仪和回读命令检查驱动状态
手持逻辑分析仪是验证 HT1621B 驱动最直接的设备。接好 CS、WR、DATA 三根线后,采样率设置 1MHz 以上,触发条件选 CS 下降沿,观察一次完整命令传输的波形,检查各电平之间的建立时间是否满足手册要求。重点关注 CS 低电平期间是否存在毛刺,WR 上升沿与 DATA 变化是否同时发生,如果 WR 与 DATA 同时变化,芯片会采到不确定电平。
数据波形确认无误后,可以用回读命令做 RAM 级别的验证。先写入已知数据到某个地址,比如地址 0x00 写入 0x05,再发100 + 000000的读命令,把 DATA 切换成输入后用示波器看返回波形:
uint8_t HT1621B_ReadData(uint8_t addr) { uint8_t data = 0; // 切 DATA 为输入 // 发 3 位识别码 100 和 6 位地址 // 随后 DATA 线上会返回 4 位数据 return data; }回读时地址还是高位在前。这个方法不仅能验证驱动正确性,还能发现 PCB 上 SEG/COM 走线是否短路或断路。点亮屏幕所有段后再逐个回读,数值和写入不一致的地址大概率就是硬件问题。
硬件排障没有逻辑分析仪时,用示波器单通道看 WR 引脚波形也能判断时序大致是否正确。如果 WR 频率明显偏低,说明延时函数吃掉太多时间,可以把微秒延时降到手册允许的下限附近,提高整体刷新速度。
另一个容易被忽略的验证点:工作电流。HT1621B 在 LCD 全亮和非全亮时电流差异很小,但 SYS DIS 与 SYS EN 状态下的电流差异很大。如果初始化后实测电流异常高,优先怀疑偏压电路是否正常工作,再看是否误开了外部时钟模式导致振荡器电流过大。工程如果带了低功耗模式,SYS DIS 命令必须放在进入 STOP 模式之前,这样 HT1621B 自己也会进入低功耗态,整机待机电流能降一个数量级。
本文还有配套的精品资源,点击获取