在嵌入式开发中,LCD显示驱动是常见需求,而HT1621作为一款性价比较高的段码LCD驱动芯片,被广泛应用于各种小型设备。但很多开发者在实现基础功能后,往往止步于"能用就行"的层面,忽略了代码的可维护性和可移植性。本文将带你从工程化角度重构HT1621驱动,打造一个接口清晰、易于移植的驱动库。
任何优秀的驱动代码都始于合理的硬件抽象。对于HT1621这类通过GPIO模拟通信协议的芯片,宏定义的设计直接影响代码的可读性和可维护性。
c复制/* 引脚控制宏 */
#define HT1621_CS_HIGH() GPIO_SetHigh(HT1621_CS_PORT, HT1621_CS_PIN)
#define HT1621_CS_LOW() GPIO_SetLow(HT1621_CS_PORT, HT1621_CS_PIN)
#define HT1621_WR_HIGH() GPIO_SetHigh(HT1621_WR_PORT, HT1621_WR_PIN)
#define HT1621_WR_LOW() GPIO_SetLow(HT1621_WR_PORT, HT1621_WR_PIN)
#define HT1621_DATA_HIGH() GPIO_SetHigh(HT1621_DATA_PORT, HT1621_DATA_PIN)
#define HT1621_DATA_LOW() GPIO_SetLow(HT1621_DATA_PORT, HT1621_DATA_PIN)
相比简单的#define CS_HIGH,这种带硬件信息的宏定义有三大优势:
c复制/* 命令码定义 */
typedef enum {
HT1621_CMD_BIAS = 0x50,
HT1621_CMD_SYS_EN = 0x02,
HT1621_CMD_LCD_ON = 0x06,
HT1621_CMD_RC256 = 0x30,
HT1621_CMD_WDT_DIS = 0x0A,
HT1621_CMD_TIME_DIS = 0x08
} HT1621_Command;
使用枚举而非#define定义命令码,可以利用编译器的类型检查功能,减少参数传递错误的风险。同时,添加前缀HT1621_CMD_避免了命名空间的污染。
HT1621采用类似SPI的3线通信协议,但时序要求更为严格。我们需要在保证功能正确的前提下,实现高效的通信函数。
c复制static void HT1621_WriteBits(uint8_t data, uint8_t bits, bool msbFirst) {
for(uint8_t i = 0; i < bits; i++) {
HT1621_WR_LOW();
Delay_us(2);
uint8_t bitVal;
if(msbFirst) {
bitVal = (data & (1 << (bits - 1 - i))) ? 1 : 0;
} else {
bitVal = (data & (1 << i)) ? 1 : 0;
}
bitVal ? HT1621_DATA_HIGH() : HT1621_DATA_LOW();
Delay_us(2);
HT1621_WR_HIGH();
Delay_us(2);
}
}
这个通用位写入函数具有以下特点:
基于通用位写入函数,我们可以构建更高层的通信函数:
c复制void HT1621_WriteCommand(HT1621_Command cmd) {
HT1621_CS_LOW();
Delay_us(2);
HT1621_WriteBits(0x80, 4, true); // 命令模式标识
HT1621_WriteBits(cmd, 8, true); // 命令数据
HT1621_CS_HIGH();
}
void HT1621_WriteData(uint8_t addr, uint8_t data) {
addr <<= 2; // HT1621地址需要左移2位
HT1621_CS_LOW();
Delay_us(2);
HT1621_WriteBits(0xA0, 3, true); // 数据模式标识
HT1621_WriteBits(addr, 6, true); // 地址数据
HT1621_WriteBits(data, 4, false);// 数据内容(LSB first)
HT1621_CS_HIGH();
}
良好的API设计应该隐藏实现细节,提供简洁明了的接口。以下是推荐的.h文件设计:
c复制// HT1621_Driver.h
#ifndef __HT1621_DRIVER_H
#define __HT1621_DRIVER_H
#include <stdint.h>
#include <stdbool.h>
typedef enum {
HT1621_BIAS_1_2,
HT1621_BIAS_1_3
} HT1621_Bias;
typedef enum {
HT1621_CLOCK_RC256,
HT1621_CLOCK_EXTERNAL
} HT1621_ClockSource;
typedef struct {
uint8_t cs_port;
uint8_t cs_pin;
uint8_t wr_port;
uint8_t wr_pin;
uint8_t data_port;
uint8_t data_pin;
} HT1621_Config;
void HT1621_Init(const HT1621_Config *config, HT1621_Bias bias, HT1621_ClockSource clock);
void HT1621_WriteSegment(uint8_t segAddr, uint8_t value);
void HT1621_ClearAll(void);
void HT1621_TestPattern(void);
#endif
这种设计具有以下优势:
HT1621驱动需要在不同MCU平台间移植时,主要考虑以下方面:
为支持不同平台,我们需要实现硬件相关的GPIO操作函数。以下是一个抽象接口示例:
c复制// HT1621_HAL.h
#ifndef __HT1621_HAL_H
#define __HT1621_HAL_H
#include <stdint.h>
typedef struct {
uint8_t port;
uint8_t pin;
} GPIO_Pin;
void GPIO_SetHigh(uint8_t port, uint8_t pin);
void GPIO_SetLow(uint8_t port, uint8_t pin);
void Delay_us(uint32_t microseconds);
#endif
针对不同平台,只需实现这些基础函数即可完成移植:
STM32实现示例:
c复制#include "stm32f1xx_hal.h"
#include "HT1621_HAL.h"
void GPIO_SetHigh(uint8_t port, uint8_t pin) {
GPIO_TypeDef *GPIOx = (port == 0) ? GPIOA :
(port == 1) ? GPIOB : GPIOC;
HAL_GPIO_WritePin(GPIOx, 1 << pin, GPIO_PIN_SET);
}
void Delay_us(uint32_t us) {
HAL_Delay(us / 1000);
// 更精确的实现可以使用定时器
}
ESP32实现示例:
c复制#include "driver/gpio.h"
#include "HT1621_HAL.h"
void GPIO_SetHigh(uint8_t port, uint8_t pin) {
gpio_set_level(pin, 1);
}
void Delay_us(uint32_t us) {
ets_delay_us(us);
}
不同厂家的LCD屏段码排列可能不同,建议在驱动层添加映射表:
c复制static const uint8_t segmentMap[] = {
// 根据实际LCD屏定义
0x00, // SEG0 -> 实际物理段A
0x01, // SEG1 -> 实际物理段B
// ...
};
void HT1621_WriteSegment(uint8_t segAddr, uint8_t value) {
uint8_t physicalAddr = segmentMap[segAddr];
HT1621_WriteData(physicalAddr, value);
}
c复制void HT1621_WriteMultiple(const HT1621_Segment *segments, uint8_t count) {
HT1621_CS_LOW();
Delay_us(2);
for(uint8_t i = 0; i < count; i++) {
uint8_t addr = segmentMap[segments[i].addr] << 2;
HT1621_WriteBits(0xA0, 3, true);
HT1621_WriteBits(addr, 6, true);
HT1621_WriteBits(segments[i].value, 4, false);
}
HT1621_CS_HIGH();
}
c复制static uint8_t displayBuffer[32]; // 假设32个4位段
void HT1621_WriteSegmentBuffered(uint8_t segAddr, uint8_t value) {
if(displayBuffer[segAddr] != value) {
displayBuffer[segAddr] = value;
HT1621_WriteSegment(segAddr, value);
}
}
完善的驱动库应该包含自检和调试功能:
c复制void HT1621_RunSelfTest(void) {
// 全屏点亮测试
for(uint8_t i = 0; i < 32; i++) {
HT1621_WriteData(i, 0x0F);
}
Delay_ms(1000);
// 逐段扫描测试
for(uint8_t i = 0; i < 32; i++) {
HT1621_WriteData(i, 0x00);
if(i > 0) HT1621_WriteData(i-1, 0x0F);
Delay_ms(100);
}
// 清屏
HT1621_ClearAll();
}
c复制bool HT1621_CheckCommunication(void) {
HT1621_CS_LOW();
Delay_us(2);
// 发送测试模式命令
HT1621_WriteBits(0x80, 4, true);
HT1621_WriteBits(0x28, 8, true); // TEST1命令
// 切换DATA引脚为输入
GPIO_Init(HT1621_DATA_PORT, HT1621_DATA_PIN, GPIO_MODE_INPUT);
// 读取测试模式返回值
bool result = false;
HT1621_WR_LOW();
Delay_us(2);
if(GPIO_Read(HT1621_DATA_PORT, HT1621_DATA_PIN)) {
result = true;
}
HT1621_WR_HIGH();
// 恢复DATA引脚为输出
GPIO_Init(HT1621_DATA_PORT, HT1621_DATA_PIN, GPIO_MODE_OUTPUT);
HT1621_CS_HIGH();
return result;
}
在实际项目中,我发现这种模块化设计的HT1621驱动可以显著提高代码复用率。最近在一个使用STM32和ESP32的双MCU项目中,同一套驱动代码只需实现不同的HAL层,就能够在两个平台上无缝工作,大大缩短了开发周期。