简介:这是一个基于STM32 HAL库驱动HT1621B液晶显示模块的源码项目,主要面向嵌入式初学者、电子竞赛选手以及需要在产品中快速集成段式LCD的开发者。HT1621B是一款可驱动128段的LCD驱动芯片,常用于显示字符与简单图形,本例通过STM32的SPI接口完成对其控制,覆盖GPIO复用配置、SPI时序初始化、写命令与写数据函数的实现等关键步骤。压缩包体积非常小巧,仅3KB,包含3个文件:驱动源文件、头文件和README说明文档,没有多余工程配置,简洁易读,方便直接移植或二次修改。目前已有1391人学习下载,适合作为学习STM32 HAL库与LCD驱动结合的入门范例。源码中针对HT1621B的写命令和写数据函数均做了清晰封装,调用方式直观,配合README中的编译与接线说明,能够快速复现实验现象;读者可从中掌握HAL库外设初始化流程、LCD驱动时序,并参照示例实现清屏、显示字符及点阵图形等基础功能,为后续开发更丰富的显示界面奠定基础。
1. 为什么搜HT1621B_STM32_HAL而不是直接用STM32的片内LCD控制器
HT1621B是一颗非常传统的段式LCD驱动芯片,四线串行接口,最大能驱动128个段(32 SEG × 4 COM)。在电表、血糖仪、风扇面板、手持万用表这类低功耗小屏场景里,它依然活跃。STM32自带LCD外设的型号有限(比如STM32F407VG的部分封装、STM32L496的LCD-TFT控制器),而且片内LCD控制器驱动段码屏时,管脚分配和COM/SEG映射都得按硬件手册来,灵活性反而低。HT1621B_STM32_HAL-master这类工程之所以被搜,不是因为它实现了多么复杂的算法,而是它给了你一条路径:用HAL库把GPIO配成普通推挽输出,然后用软件时序把数据送进HT1621B的RAM。整个过程不需要额外的SPI外设,也不需要占用定时器通道,硬件连线极其干净。这篇文章会把HT1621B的RAM映射、指令集、时序参数拆开讲清楚,再给出STM32 HAL库下的驱动代码和几个调试时容易被忽略的坑。
2. HT1621B的引脚定义与STM32最小接线,先说清楚再由HAL帮你配置
2.1 一颗芯片上最常用的几个引脚到底在干什么
HT1621B常见的封装为LQFP48或LQFP44,引脚按功能分四组:电源与偏压(VDD、VLCD、VSS)、时钟与数据(CS、WR、DATA)、振荡器(OSC)、以及驱动输出(SEG0~SEG31、COM0~COM3)。实际往STM32上接时,大部分引脚都可以悬空或者直接按固定电平处理,真正需要MCU控制的只有CS、WR、DATA这三个。
需要特别留意的是VLCD引脚,它决定LCD的偏置电压。HT1621B内部有偏压发生器,默认能输出1/3偏压和1/4占空比,但如果你想要更高的对比度,可以在VLCD和VDD之间接一个电容(常见值为0.1uF~1uF),让内部电荷泵把电压抬高。有些人直接把VLCD接到VDD上,这样做屏幕会很暗甚至不显示,这是很多第一次用HT1621B的工程师最容易遇到的问题。
电源部分,HT1621B的工作电压范围是2.4V到5.2V,对STM32F103这种3.3V供电的MCU来说,直接共用3.3V电源即可,IO电平也兼容。VSS是数字地,所有信号都以它为参考。
2.2 在CubeMX里配置GPIO的三个步骤
STM32 HAL库的配置逻辑是先通过STM32CubeMX生成工程,再在main.c之外挂自己的驱动文件。打开CubeMX,选择你手头的MCU型号(比如STM32F103C8T6),把PA0用作CS、PA1用作WR、PA2用作DATA,都是GPIO_Output模式,推挽输出,速度设为Low或Medium即可。HT1621B的通信速率本身不高(几百kHz),用Low就够,没必要开到High,速度开太快会在信号线上产生过冲。
生成工程后,需要确认初始化代码里这三个引脚的初始电平状态。建议在MX_GPIO_Init函数里把CS置高、WR置高、DATA置低。原因是HT1621B在CS为高时忽略WR上的所有信号,上电时先把CS拉高可以防止乱时序误写寄存器。
| 引脚 | STM32 | 模式 | 说明 |
|---|---|---|---|
| CS | PA0 | 推挽输出 | 片选,低有效 |
| WR | PA1 | 推挽输出 | 写时钟,上升沿锁存数据 |
| DATA | PA2 | 推挽输出 | 双向数据线,写数据时为输出 |
| VDD | 3.3V | - | 芯片电源 |
| VLCD | 接电容 | - | 产生LCD偏压 |
| VSS | GND | - | 数字地 |
2.3 上电顺序和复位序列
HT1621B没有专门的复位引脚。上电后,内部电路默认所有显示RAM为0,系统关闭状态。要让屏幕开始工作,需要依次写入两条命令:系统使能(101)和LCD偏压开启(101)。常见做法是上电后延时10ms,然后发三条命令:系统关闭、系统开启、LCD开启。顺序不能反,否则LCD偏压先开而系统没开,会出现短暂的不稳定显示,在电池供电设备上表现为上电闪屏。
3. 在HAL库下用GPIO模拟HT1621B时序:代码可抄,但要知道为什么加延时
3.1 一个最简单的写字节函数
HT1621B的数据写入是以CS拉低为起点,WR引脚产生上升沿时DATA上的电平被锁存。所以用GPIO模拟时序时,核心动作就是操作三个引脚的电平翻转顺序。下面这个函数按一个字节8位从高位开始发送:
void HT1621_WriteByte(uint8_t data, uint8_t cnt) { uint8_t i; for (i = 0; i < cnt; i++) { HAL_GPIO_WritePin(HT1621_DATA_GPIO_Port, HT1621_DATA_Pin, (data & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(HT1621_WR_GPIO_Port, HT1621_WR_Pin, GPIO_PIN_SET); delay_us(2); HAL_GPIO_WritePin(HT1621_WR_GPIO_Port, HT1621_WR_Pin, GPIO_PIN_RESET); delay_us(2); data <<= 1; } }这个函数里cnt参数允许你只发送6位或4位数据,因为HT1621B的指令分为三种长度:100+6位命令、101+9位数据写、110+3位读。把cnt设为6,可以用来发命令;设为8,则是连续RAM数据写入的第一个字节。
逻辑说明:先拉WR低,然后设置DATA,再拉WR高完成一次位写入。这里每个位之间延时2us,对应HT1621B数据手册里WR高电平最小脉宽和建立时间。如果你用HAL_GPIO_WritePin的话,这个函数本身有调用开销,再加上2us延时,实际一个位的周期大约在4~5us。换算下来每秒能送200kbit以上,段码屏一帧数据最多128bit,刷新率妥妥够用。
提示:延时函数建议用DWT实现,不要用HAL_Delay。HAL_Delay基于SysTick,延时粒度1ms,在us级场景下完全不可用。如果你用的是CubeMX生成的工程,可以在system_stm32xx.c里定义DWT的初始化,或者直接用以下代码:
static inline void delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); }3.2 读操作与DATA引脚方向切换
HT1621B支持从RAM读取数据,用于校验写入是否正确。读时序的第一步是发送读命令101+6位地址,在最后一位地址发送完成后,需要把DATA引脚切换为输入模式,随后MCU在WR的下降沿读取数据。STM32 HAL库做引脚方向切换很简单,只需要调用:
GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = HT1621_DATA_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(HT1621_DATA_GPIO_Port, &GPIO_InitStruct);读回一个字节后,再次切换回输出模式。需要注意,从读命令最后一位到DATA变成输入,至少留出2us的缓冲时间,否则会捕获到高阻态的不稳定电平。很多时候你用示波器看DATA线上的毛刺,都是切换太急导致的。
3.3 用HAL库发送指令:系统开启和偏压设置
HT1621B的命令码定义非常简洁。最高位为1表示命令模式,后两位为命令类型,剩余位为参数。常用的三条指令:
| 指令 | 二进制码 | 十六进制 | 作用 |
|---|---|---|---|
| 系统关闭 | 100000 | 0x20 | 关显示,RAM数据保留 |
| 系统开启 | 100001 | 0x21 | 打开内部振荡器和显示 |
| 偏压1/3 | 101010 | 0x2A | 设置1/3偏压、1/4占空比 |
发送指令的代码很简单,CS拉低,发送命令字节(8位),CS拉高即可。唯一需要注意的是,命令模式和数据模式的区别在于CS低电平期间首先发送的头两位。命令模式下以10开头,数据模式下以101开头,所以不能复用同一个发送函数。封装一个WriteCommand函数:
void HT1621_WriteCommand(uint8_t cmd) { HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_RESET); HT1621_WriteByte(0x80 | (cmd & 0x3F), 8); HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_SET); }0x80把最高位置1,即命令标志位。HT1621B协议规定,命令模式下第一个bit必须为1,后两位是00、01或10,表示不同命令类别,最后6位是具体内容。如果你直接把完整命令字节发出去,而不进行或运算,芯片会把它误判为数据写入,导致显示内容错乱。
4. 段码表、COM口扫描对照和显示函数封装,搞清RAM的bit怎么刷到屏幕上
4.1 HT1621B内部RAM地址与SEG/COM的映射关系
HT1621B内部有32×4的显示RAM,每个SEG对应一个4位的地址空间(4个COM位)。写入数据时,地址按SEG编号递增。也就是说,地址0对应SEG0的COM0~COM3,共4位;地址1对应SEG1。这个映射关系和市面上很多LCD驱动IC不同,有些芯片是COM地址在前,SEG地址在后,写段码前必须搞清楚,否则会发现数字显示乱序。
在STM32 HAL库驱动中,一个典型显示函数是:先发写RAM命令(101+地址),然后连续写入多个字节。比如要向SEG0到SEG15写入16个4位数据,可以一次写完:
void HT1621_WriteAllRAM(uint8_t *data, uint8_t len) { HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_RESET); HT1621_WriteByte(0xA0, 3); // 101,写RAM命令 HT1621_WriteByte(0x00, 6); // 起始地址000000 for (uint8_t i = 0; i < len; i++) { HT1621_WriteByte(data[i], 8); } HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_SET); }这里有个细节:写RAM命令和起始地址是分开发送的。0xA0只有3位,发送的是101这三个bit,紧接着是6位地址。有些初学驱动的人会把这两个参数合并成一个字节发出去,结果芯片收到的数据错位,显示出现欢迎乱码的效果。
4.2 7段码的段码表与共阴共阳的坑
HT1621B驱动的是段式LCD,不是数码管。段式LCD的7段含义和共阴数码管不一样,它没有公共阳极或阴极,而是每个段由COM和SEG之间的交流电压驱动。所以你不能直接套用数码管的那种0~9段码表,得按LCD实际走线来定义。如果你用的是现成的LCD面板,厂家一般会提供SEG与数字位的对应关系表。没有表的情况下,可以用穷举法:每段依次点亮,拍照记录位置,再合成段码表。
对于常见的大字库LCD面板,0到9的段码结构通常是a段为最低位(bit0),b、c、d依次排列。下面是一份最常见的共型段码表,适配大多数7段LCD面板:
const uint8_t LCD_7SEG_TABLE[] = { 0x3F, // 0: a b c d e f 0x06, // 1: b c 0x5B, // 2: a b d e g 0x4F, // 3: a b c d g 0x66, // 4: b c f g 0x6D, // 5: a c d f g 0x7D, // 6: a c d e f g 0x07, // 7: a b c 0x7F, // 8: all 0x6F // 9: a b c d f g };围绕这份段码表,显示一个数字到指定位置的函数可以写成:
void HT1621_DisplayDigit(uint8_t pos, uint8_t digit) { uint8_t addr = pos * 2; // 每位数占2个SEG地址 uint8_t segData = LCD_7SEG_TABLE[digit]; HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_RESET); HT1621_WriteByte(0xA0, 3); // 写RAM命令 HT1621_WriteByte(addr & 0x3F, 6); // 起始地址 HT1621_WriteByte(segData, 8); // 写入4位COM数据 HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_SET); }为什么一个数字占2个SEG地址?因为4个COM位对应LCD的4个公共端,而每个位(digit)需要多个SEG来表示7段+小数点。如果面板的数字是3位,那么SEG0~SEG13被占用,SEG14开始可以分配给自定义符号(电池、WiFi图标等)。
4.3 在HAL库工程里初始化HT1621B的完整序列
初始化函数不是简单发一条System Enable就完事的。稳妥的初始化流程是:清空所有RAM、系统关闭、系统开启、偏压设置。清空RAM这一步尤其建议单独做,因为如果LCD面板上一版显示的是调试信息,不清空的话会残留残影。
void HT1621_Init(void) { uint8_t blank[16] = {0}; HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(HT1621_WR_GPIO_Port, HT1621_WR_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(HT1621_DATA_GPIO_Port, HT1621_DATA_Pin, GPIO_PIN_RESET); delay_us(1000); HT1621_WriteCommand(0x20); // 系统关闭 HT1621_WriteCommand(0x21); // 系统开启 HT1621_WriteCommand(0x2A); // 偏压1/3,占空比1/4 HT1621_WriteCommand(0x29); // 内部RC振荡器,关闭看门狗 HT1621_WriteAllRAM(blank, 16); }命令0x29表示内部RC振荡器模式,同时关掉内置看门狗。这里有个容易忽略的点:如果你在初始化后不久又发送了大量数据,而看门狗没有喂狗操作,HT1621B内部看门狗超时时会强制复位显示控制器,表现就是屏幕每隔几百毫秒闪一下。多数人调试时遇到的闪烁问题,根源不在刷新频率,而是没关看门狗。
5. 调试HT1621B驱动的3个技巧:写寄存器前先读回、用TIM延时替代盲等、把PB4/PB3留给SWD
5.1 写数据前先读RAM,判断SPI电平是否真的到了芯片
HT1621B允许CPU读取RAM内容,这是调试时最有力的工具。你写完一个字节后,立刻用读命令把同一地址的数据读回来,比对是否一致。如果不一致,问题大概率出在DATA引脚方向切换或GPIO速度配置上。具体做法:
uint8_t HT1621_ReadRAM(uint8_t addr) { uint8_t i; uint8_t rxData = 0; HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_RESET); HT1621_WriteByte(0x60, 3); // 110读命令 HT1621_WriteByte(addr & 0x3F, 6); // 切换DATA为输入 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = HT1621_DATA_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(HT1621_DATA_GPIO_Port, &GPIO_InitStruct); delay_us(2); for (i = 0; i < 8; i++) { rxData <<= 1; HAL_GPIO_WritePin(HT1621_WR_GPIO_Port, HT1621_WR_Pin, GPIO_PIN_RESET); delay_us(1); HAL_GPIO_WritePin(HT1621_WR_GPIO_Port, HT1621_WR_Pin, GPIO_PIN_SET); delay_us(1); if (HAL_GPIO_ReadPin(HT1621_DATA_GPIO_Port, HT1621_DATA_Pin) == GPIO_PIN_SET) { rxData |= 0x01; } } // 恢复DATA为输出 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(HT1621_DATA_GPIO_Port, &GPIO_InitStruct); HAL_GPIO_WritePin(HT1621_CS_GPIO_Port, HT1621_CS_Pin, GPIO_PIN_SET); return rxData; }读回值如果一直为0xFF,检查DATA引脚是否虚焊或模式未切换;如果读出值偶发正确偶发错误,通常是WR信号有毛刺,需要考虑在WR引脚上串一个330Ω电阻做阻尼。HAL_GPIO_ReadPin在高速翻转时的返回值延迟大约几十ns,不影响微秒级时序。
5.2 用TIM延时替代盲等,释放CPU让显示刷新更稳定
软件延时的弊端在于CPU被占满。如果你在同一颗STM32上还跑着PID控制或Modbus通信,每次刷新HT1621B时要连续发送128bit数据,持续占用CPU时间片,可能导致其他任务抖动。常见的做法是把显示刷新交给定时器中断:TIM6配置为中断模式,在中断回调里发送一位或一个字节,主循环只负责维护显示缓冲数组。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint8_t bitIndex = 0; static uint8_t byteIndex = 0; if (htim->Instance == TIM6) { // 每次中断发送一位 if (bitIndex < 8) { // 取dispBuf[byteIndex]的当前位并写到DATA引脚 bitIndex++; } else { bitIndex = 0; byteIndex++; } } }这样CPU只在中断里做几次引脚操作,其余时间可以跑其他逻辑。代价是需要计算好中断频率:一个字节8位,128位全发完需要128次中断。如果定时器频率设在50kHz,一次完整刷屏需要2.56ms,对刷新率60fps的显示屏来说绰绰有余。
5.3 让出PB4和PB3,避免JTAG封印导致版本发布后无法下载程序
STM32F103的PB3、PB4在默认状态下是JTAG的TDO和NJTRST,CubeMX生成工程时,如果这两根引脚被当作普通GPIO使用,代码第一次下载没问题,但第二次下载时调试器可能连不上。很多人搜索HT1621B_STM32_HAL时下载的工程模板里,为了节省引脚会把显示数据线放到PB3/PB4上,量产烧录时就会出现硬件版和固件版本不匹配的诡异问题。使用PB3/PB4前必须在CubeMX里把SYS Debug选项设置为Serial Wire,并且确认代码里调用了HAL_GPIO_ConfigPin或者__HAL_AFIO_REMAP_SWJ_NOJTAG才安全。
如果你的设计还有空余引脚,尽量不要把HT1621B的CS分配到PB4,因为很多STM32的最小系统板在调试器连接时会对这个引脚做额外钳位。PA0到PA3这一组最稳妥,但如果你要用PWM捕获,就优先考虑PC0、PC1、PC2这类不带复用功能的引脚。提醒一下,在做STM32芯片包安装或芯片型号迁移时,部分国产替代APM32、GD32在PB3/PB4的默认复用上和ST原厂存在差异,一样会踩这个坑。
本文还有配套的精品资源,点击获取