段码 LCD 驱动
段码 LCD(断码屏)驱动是 AC82N(CD09 CPU)上控制段码液晶屏的底层外设驱动,负责通过 P33 电源域协处理器寄存器驱动 LCD 电荷泵、配置 COM/SEG 波形并维护显示缓存,最大支持 4×27 或 6×25 的段点阵。
Purpose and Scope
本文档介绍 sdk/cpu/cd09/segment_code_lcd/ 目录下的段码 LCD 驱动实现,涵盖:
- 驱动与 P33 协处理器寄存器之间的控制通路(电荷泵、VLCD 分压、BIAS、推屏模式等)
- 平台数据(
segment_code_lcd_platform_data)的字段含义与枚举取值 - 初始化、点/数字/字符填充、缓存刷新、帧中断与 IO 复用的完整机制
- Demo 程序展示的典型使用方式
不包含以下内容(属于兄弟页面):
- 点阵 LCD(SPI/并口屏)驱动,如
sdk/apps/common/lcd/lcd_drive.c、lcd_spi_st7735_*.c、lcd_pap_st7789_*.c,请参考「点阵 LCD 驱动」相关页面 - GPIO、时钟、看门狗等通用外设驱动
概述
段码 LCD(Segment LCD,又称断码屏)本身不含控制器,需要 MCU 侧的 LCD 驱动模块在 COM 与 SEG 引脚上产生交流驱动波形,利用液晶分子的电光效应点亮/熄灭交叉点。驱动内部将每个交叉点抽象为 (com, seg) 坐标,通过 LCD_POINT(com, seg) 宏编码为一个字节(高 5 位为 SEG 号,低 3 位为 COM 号)。
该驱动设计上遵循 "CPU 只维护显存、硬件自动推屏" 的思路:
- CPU 在内存中的
disp_buffer[6](每 COM 一行,u32 位图)上"画"点; - 调用
segment_code_lcd_disp_update()时仅把发生变化的 COM 行写入 P33 寄存器; - P33 协处理器中的 LCD 硬件按 COM×SEG 扫描产生波形,无需 CPU 逐帧干预;
- 支持连续推屏(硬件自动循环)与断续推屏(每帧结束产生 PND/中断,供 IO 复用场景释放 SEG 引脚)。
底层所有寄存器操作通过 p33_tx_1byte / p33_rx_1byte / p33_tx_nbyte 访问 P33 电源域寄存器空间(详见头文件中的 P33_CON_SET 等宏),这些寄存器位于 asm/power/p33_sfr.h 定义的地址空间中。
架构
flowchart TD
subgraph sg_App["应用层"]
App["应用程序 / segment_code_lcd_demo"]
end
subgraph sg_Drv["驱动 API 层 (segment_code_lcd_drv.c)"]
Init["segment_code_lcd_init"]
Fill["fill_point / fill_number / fill_char"]
Update["segment_code_lcd_disp_update"]
ISR["segment_code_lcd_isr (帧中断)"]
Pump["segment_code_lcd_pump_init (电荷泵)"]
end
subgraph sg_P33["P33 协处理器寄存器域"]
CHG0["P3_LCD_CHG_CON0/1/2<br/>电荷泵 + LDOB + 电压档位"]
CLK0["P3_CLK_CON0<br/>LCD 时钟选择"]
LCDC0["P3_LCDC_CON0~CON7<br/>推屏模式/BIAS/COM使能/状态"]
SEGDAT["P3_SEG0_DAT0~<br/>像素数据寄存器"]
end
subgraph sg_IO["引脚层"]
COMIOS["COM0~COM5 (IO_PORTC_00~05)"]
SEGIOS["SEG0~SEG26 (IO_PORTC_04~IO_PORTD_14)"]
end
App --> Init
App --> Fill
Fill --> Update
App --> ISR
Init --> Pump
Init --> CHG0
Init --> CLK0
Init --> LCDC0
Update --> SEGDAT
LCDC0 -->|"驱动波形"| COMIOS
SEGDAT -->|"段点亮数据"| SEGIOS
COMIOS --> Panel["段码 LCD 面板 (6COM×25SEG)"]
SEGIOS --> Panel
Panel -->|"帧结束 PND 置位"| LCDC0
LCDC0 -->|"中断"| ISR
ISR -->|"isr_cb 回调"| App
架构说明:应用层通过四个入口与驱动交互——初始化、填充显存、刷新显存、接收帧中断。驱动层将平台数据翻译成 P33 寄存器的位域配置(电荷泵电压、VLCD 系数、BIAS、时钟源、COM 使能),而像素数据则通过 p33_tx_nbyte 批量写入 P3_SEG*_DAT* 数据寄存器。LCD 硬件扫描完成一帧后置位 PND 标志,在断续推屏模式下可触发中断回调 isr_cb,这是实现 IO 复用的关键钩子。
核心实现
硬件资源与引脚映射
驱动用两张静态映射表把逻辑 COM/SEG 索引翻译为 GPIO 引脚号。SEG 共 27 个索引,从 IO_PORTC_04 一直到 IO_PORTD_14;COM 共 6 个索引,位于 IO_PORTC_00 ~ IO_PORTC_05:
//seg index 到 IO的映射表
static const u8 hw_seg2pin_mapping[27] = {
IO_PORTC_04, IO_PORTC_05, IO_PORTC_06, IO_PORTC_07,
IO_PORTC_08, IO_PORTC_09, IO_PORTC_10, IO_PORTC_11,
IO_PORTC_12, IO_PORTC_13, IO_PORTC_14, IO_PORTC_15,
IO_PORTD_00, IO_PORTD_01, IO_PORTD_02, IO_PORTD_03,
IO_PORTD_04, IO_PORTD_05, IO_PORTD_06, IO_PORTD_07,
IO_PORTD_08, IO_PORTD_09, IO_PORTD_10, IO_PORTD_11,
IO_PORTD_12, IO_PORTD_13, IO_PORTD_14,
};
//com index 到 IO的映射表
static const u8 hw_com2pin_mapping[6] = {
IO_PORTC_00, IO_PORTC_01, IO_PORTC_02, IO_PORTC_03, IO_PORTC_04, IO_PORTC_05,
};
Source: segment_code_lcd_drv.c
映射表是固定不变的硬件事实:例如 SEG0 固定为 IO_PORTC_04。应用只需关心 (com, seg) 逻辑坐标,LCD_POINT(com, seg) 宏将其编码为 (seg << 3 | com),其中 INVALID_COM (0x07) 表示该段位不使用(如数字 "1" 不需要 F 段):
//描述一个点
#define LCD_POINT(com, seg) (seg << 3 | com)
//把某个点的com设置成INVALID_COM,则该点无效
#define INVALID_COM 0x07
Source: segment_code_lcd_drv.h
P33 寄存器控制层
驱动不直接操作 GPIO 产生波形,而是把全部控制字通过 P33_CON_SET / p33_tx_1byte 写入 P33 电源域寄存器。寄存器按功能分组:
| 寄存器组 | 关键位域宏 | 作用 |
|---|---|---|
P3_CHG_PUMP | LCDCP_WVBG_EN | 内部带隙基准(VBG)使能,电荷泵电压校准基准 |
P3_LCD_CHG_CON0 | LCDCP_PUMP_EN / LCDCP_LDOB_EN / LCDCP_HFREQ_EN / LCDCP_COMP_EN / LCDCP_PUMPCLK_EN / LCDCP_ADC_DET_EN / LCDCP_STANDBY_SET_BIT / LCDCP_X2MODE_SET_BIT | 电荷泵与 LDOB 主控;X2MODE=1 时 VLCD=2×VDDIO 工作模式 |
P3_LCD_CHG_CON1 | LCDCP_OSCCAP_SEL / LCDCP_PUMPVOL_SEL | 泵振荡电容档位(默认 1.2M)与泵输出电压档(3.0V~4.5V,步进约 100mV) |
P3_LCD_CHG_CON2 | LCDCP_LDOB_SEL | LDOB 输出电压档(2.7V/3.0V/3.3V/3.6V) |
P3_CLK_CON0 | LCD_CLK_SEL / LCD_CLK_EN | LCD 扫描时钟源:OSL 外挂 32K 晶振、WCLK 32K、LRC 200K/6、PAT |
P3_LCDC_CON0 | LCD_VOL_SEL / LCD_BIAS_SEL / LCD_DOT_EN / LCD_ENABLE | VLCD 分压系数(0.886~1.000×LCDCP)、BIAS(1/2、1/3、1/4)、跳秒脚使能、LCD 模块总使能 |
P3_LCDC_CON1 | LCD_SET_CHGDUTY / LCD_CHG_CURR / LCD_SET_CHGMODE | 强充电占空比/电流/充电模式(弱充、强充、半周期、多周期) |
P3_LCDC_CON2 | LCD_DEADBAND_EN / LCD_TEST_EN / LCD_CLK_DIV | 死区控制、测试模式(IO 直出静态电平)、模块时钟预分频 |
P3_LCDC_CON3 | LCD_GET_PENDING / LCD_CLR_PENDING / LCD_SW_KST / LCD_IE_EN / LCD_CTU_EN | 推屏控制核心:帧结束标志、软件 KST 启动位、断续模式帧中断使能、连续推屏使能 |
P3_LCDC_CON4 | LCD_COM_IO_EN | bit0~bit5 对应 COM0~COM5 是否使能 |
P3_LCDC_CON5 | LCD_DOT_SEG_SEL / LCD_DOT_COM_SEL | 跳秒脚(DOT)的 SEG/COM 位置选择 |
P3_LCDC_CON6/7 | LCD_SEG_TEST_FLAG / LCD_COM_TEST_FLAG | 测试模式通道选择,SEG/COM 口直出 VLCD 各分压电平 |
Source: segment_code_lcd_drv.h
其中 P3_LCDC_CON3 的语义决定了整个推屏模型(注释即规范):
#define LCD_SW_KST(en) P33_CON_SET(P3_LCDC_CON3, 4, 1, en) //开启测试模式,IO直接输出LCD静态电平
#define LCD_IE_EN(en) P33_CON_SET(P3_LCDC_CON3, 1, 1, en) //断续推屏模式下,完成一帧后,LCD帧中断使能位
#define LCD_CTU_EN(en) P33_CON_SET(P3_LCDC_CON3, 0, 1, en) //连续推屏使能位:
//0:断续推屏模式(完成一帧后起帧pending,同时停止推屏)
//1:连续推屏模式(完成一帧后不会起帧pending,硬件会自动继续进行下一帧的推屏)
Source: segment_code_lcd_drv.h
平台数据结构与配置
驱动通过 struct segment_code_lcd_platform_data 接收面板配置(Demo 中的完整实例见"使用示例"一节),字段语义如下:
| 字段 | 类型 | 说明 |
|---|---|---|
clk | enum lcd_clk | LCD 扫描时钟源:LCD_CLK_SEL_OSL(外挂 32K 晶振,跳秒功能可用)、LCD_CLK_SEL_WCLK、LCD_CLK_SEL_LRC、LCD_CLK_SEL_PAT |
chgpump_vol | enum lcdcp_pmup_vol | 电荷泵输出电压档,LCDCP_PUMPVOL_SEL_3P0V~_4P5V(默认 3P6V) |
ldob_vol | enum lcdcp_ldob_vol | LDOB 电压档:2.7V/3.0V/3.3V/3.6V(默认 3P3V) |
vlcd | enum lcd_vol | VLCD 分压系数:LCD_VOL_SEL_0P886XLCDCP~_1P000XLCDCP(8 档,步进约 1.6%) |
bias | enum lcd_bias | 偏压比:LCD_BIAS_DISABLE/_1DIV2/_1DIV3/_1DIV4 |
chgmode | enum lcd_chgmode | 充电模式:弱充、强充、半周期强驱、多周期强驱(CHGDUTY+1 个 32K 时钟后撤强驱) |
chgduty | u8 | 多周期充电模式的强驱占空比 |
dot_com / dot_seg | u8 | 跳秒脚(DOT)的 COM/SEG 坐标 |
lcd_com_en | u32 | 位图,bit0~5 使能 COM0~COM5 |
lcd_seg_en | u32 | 位图,bit0~26 使能 SEG0~SEG26 |
ctu_en | u8 | 0:断续推屏(使能帧中断);1:连续推屏 |
isr_cb | 函数指针 | 断续推屏帧中断回调,用于 IO 复用等场景;为 NULL 时 ISR 自动重新推屏 |
初始化流程
segment_code_lcd_init() 是唯一入口,内部按固定顺序完成"清寄存器 → 电荷泵 → 时钟 → IO 接管 → 启动推屏":
void segment_code_lcd_init(const struct segment_code_lcd_platform_data *lcd_dat)
{
u8 temp = 0;
if ((__this->init) || (lcd_dat == NULL)) {
return;
}
__this->usr_data = lcd_dat;
for (temp = P3_LCDC_CON0; temp <= P3_LCDC_CON7; temp++) {
p33_tx_1byte(temp, 0);
}
for (temp = P3_SEG_IO_EN0; temp <= P3_SEG_IO_EN3; temp++) {
p33_tx_1byte(temp, 0);
}
//LCDCP初始化
segment_code_lcd_pump_init();
//配置LCD时钟
LCD_CLK_SEL(__this->usr_data->clk);
if (__this->usr_data->clk == LCD_CLK_SEL_LRC) {
...
Source: segment_code_lcd_drv.c
初始化要点:
- 防重入保护:
__this->init与空指针双重检查,重复调用直接返回; - 寄存器归零:先清空
P3_LCDC_CON0~CON7与P3_SEG_IO_EN0~3,保证从确定状态开始; - 电荷泵上电时序(
segment_code_lcd_pump_init):先把IO_PORTD_15置为输出高电平(模拟模块供电控制),延时 500µs 后使能 VBG;写入LCD_CHG_CON0_INIT(使能 HFREQ/COMP/PUMPCLK,STANDBY/X2MODE 置位),配置振荡电容 1.2M、泵压档、LDOB 档;再把 PD15 恢复高阻,依次打开PUMP_EN(延时 1.5ms)与LDOB_EN——严格的时序保证泵稳定建立后再点亮 LDOB:
static void segment_code_lcd_pump_init(void)
{
gpio_set_mode(IO_PORT_SPILT(IO_PORTD_15), PORT_OUTPUT_HIGH);
delay_us(500);
LCDCP_WVBG_EN(1);//LCDCP_WVBG_EN
delay_us(500);
p33_tx_1byte(P3_LCD_CHG_CON0, LCD_CHG_CON0_INIT);
LCDCP_OSCCAP_SEL(LCDCP_OSCCAP_SEL_1P2M);
#if SEGMENT_CODE_LCD_AUTO_TRIM_ENABLE
LCDCP_PUMPVOL_SEL(0);
#else
LCDCP_PUMPVOL_SEL(__this->usr_data->chgpump_vol);
#endif
LCDCP_LDOB_SEL(__this->usr_data->ldob_vol);
delay_us(500);
gpio_set_mode(IO_PORT_SPILT(IO_PORTD_15), PORT_HIGHZ);
LCDCP_PUMP_EN(1);
delay_us(1500);
LCDCP_LDOB_EN(1);
}
Source: segment_code_lcd_drv.c
- 后续步骤(与头文件使用流程一致):配置时钟源、VLCD 分压与 BIAS、COM/SEG IO 高阻输入(DIE=0、DIEH=0)、写入像素寄存器、配置模式(连续/断续)、开启
LCD_ENABLE、写入软件 KST 启动位,此后 LCD 模块接管 IO 自动推屏。
显示数据路径:双缓存与差分刷新
驱动内部维护两组缓存:
struct segment_code_lcd_var {
u8 init;
u32 disp_buffer[6];//显示缓存,最大6com
u32 save_buffer[6];//记忆当前显示的内容
const struct segment_code_lcd_platform_data *usr_data;
};
static struct segment_code_lcd_var _lcd_var = {0};
#define __this (&_lcd_var)
Source: segment_code_lcd_drv.c
disp_buffer[6]:工作缓存,fill_*系列 API 只改这里,不直接写硬件;save_buffer[6]:镜像"当前已推送到硬件"的内容,用于差分比较。
所有填充 API 最终都收敛到 segment_code_lcd_fill_point()——把 icon 拆成 com/seg,越界直接返回,然后对 disp_buffer[com] 的对应 bit 置位/清零:
void segment_code_lcd_fill_point(u8 icon, u8 isfill)
{
u8 com;
u8 seg;
com = icon & 0x07;
if (com >= LCD_COM_MAX) {
return;
}
seg = icon >> 3;
if (seg >= LCD_SEG_MAX) {
return;
}
if (isfill) {
__this->disp_buffer[com] |= BIT(seg);
} else {
__this->disp_buffer[com] &= ~BIT(seg);
}
}
Source: segment_code_lcd_drv.c
segment_code_lcd_disp_update() 是唯一把缓存同步到硬件的入口,采用逐 COM 差分刷新:只对使能的 COM 行比较 save_buffer 与 disp_buffer,内容相同则跳过(避免无谓的 P33 总线写入,降低功耗);不同则先更新镜像再按 4 字节批量写像素寄存器(前 4 个 COM 映射到 P3_SEG0_DAT0 段,后 2 个映射到 P3_SEG4_DAT0 段):
void segment_code_lcd_disp_update(void)
{
u8 i;
if (__this->usr_data == NULL) {
return;
}
for (i = 0; i < LCD_COM_MAX; i++) {
if (__this->usr_data->lcd_com_en & BIT(i)) {
//缓存和显示不一致时才刷新
if (__this->save_buffer[i] == __this->disp_buffer[i]) {
continue;
}
__this->save_buffer[i] = __this->disp_buffer[i];
if (i < 4) {
p33_tx_nbyte(P3_SEG0_DAT0 + (i - 0) * 4, (u8 *)&__this->disp_buffer[i], 4);
} else {
p33_tx_nbyte(P3_SEG4_DAT0 + (i - 4) * 4, (u8 *)&__this->disp_buffer[i], 4);
}
}
}
}
Source: segment_code_lcd_drv.c
数字与字符段码表
驱动内置 7 段码映射表,避免应用自己拼段:
lcd_seg_number_2_seg[12]:'0'~'9'、'-'、以及下标 11 作为"清除"哨兵(segment_code_lcd_fill_number中num >= 数组大小时按清除处理);lcd_seg_large_letter_2_seg[26]/lcd_seg_small_letter_2_seg[26]:大写/小写字母(0x40表示该字母无法用 7 段表达,只点亮 G 段)。
#define LED_A BIT(0)
#define LED_B BIT(1)
#define LED_C BIT(2)
#define LED_D BIT(3)
#define LED_E BIT(4)
#define LED_F BIT(5)
#define LED_G BIT(6)
//数字'0' ~ '9' '-'显示段码表
static const u8 lcd_seg_number_2_seg[12] = {
(u8)(LED_A | LED_B | LED_C | LED_D | LED_E | LED_F), //'0'
(u8)(LED_B | LED_C), //'1'
...
Source: segment_code_lcd_drv.c
segment_code_lcd_fill_number(ptr, num) 要求 ptr 指向 7 个 LCD_POINT 编码(对应 A~G 段),驱动逐段与 LED_x 做位与后调用 fill_point;fill_char 同理,先按大小写换算成段码表下标。
推屏模式、中断与 IO 复用
连续推屏 vs 断续推屏
segment_code_lcd_set_ctu_en() 可在运行中切换推屏模式,切换逻辑即寄存器语义的直接体现:
void segment_code_lcd_set_ctu_en(u8 ctu_en)
{
if (__this->init == 0) {
return;
}
if (ctu_en) {
LCD_IE_EN(0);//IE=0
LCD_CTU_EN(1);//连续推屏
} else {
LCD_IE_EN(1);//IE=1
LCD_CTU_EN(0);//断续推屏
}
}
Source: segment_code_lcd_drv.c
设计意图:连续推屏(CTU_EN=1)适合独占 LCD 的场景,功耗最优、无中断开销;断续推屏(CTU_EN=0, IE_EN=1)每完成一帧即置位 PND 并停止推屏,让出 SEG 引脚给其他外设(如按键扫描、LED 呼吸),是"LCD 与其他功能共享 IO"的唯一手段。
帧中断处理
中断服务函数 segment_code_lcd_isr() 是断续推屏的核心:检测到 PND 后先清标志、关 LCD_ENABLE(让 COM 保持高阻),然后二选一——存在 isr_cb 则回调应用(应用可安全复用 SEG IO),否则硬件自动重新使能并写 KST 继续推屏:
___interrupt
static void segment_code_lcd_isr(void)
{
//帧中断
if (LCD_GET_PENDING()) {
LCD_CLR_PENDING();
LCD_ENABLE(0);//en=0
if (__this->usr_data->isr_cb) {
__this->usr_data->isr_cb();
} else {
LCD_ENABLE(1);
LCD_SW_KST(1);
}
}
}
Source: segment_code_lcd_drv.c
注意该 ISR 默认关断 LCD 使能——这意味着凡提供 isr_cb 的应用,回调内完成复用功能后必须显式调用 segment_code_lcd_start() 恢复推屏,否则屏幕会停在最后一帧并永久停止刷新。
IO 复用工作流
头文件注释给出了三种场景的标准流程(不复用、复用切换、复用后恢复),核心约束有两条:
- LCD IO 复用其他功能期间,COM 口必须保持高阻输入(由
LCD_ENABLE(0)+ IO 高阻保证); - LCD 驱动过程中引脚电平会周期性翻转,复用的功能协议必须能容忍这种干扰。
驱动为此提供 segment_code_lcd_set_all_io_highz()(按 lcd_com_en/lcd_seg_en 位图把全部 COM/SEG 引脚置高阻)和 segment_code_lcd_start()(重新 LCD_ENABLE(1) + LCD_SW_KST(1) 启动推屏):
void segment_code_lcd_start(void)
{
if (__this->init == 0) {
return;
}
LCD_ENABLE(1);
LCD_SW_KST(1);
}
Source: segment_code_lcd_drv.c
Demo 中的 isr_cb 展示了完整复用模式:帧中断到来 → 用 IO_PORTC_07 模拟一段输出(按键/点灯等)→ 恢复高阻 → segment_code_lcd_start() 继续推屏:
//复用测试
static void segment_code_lcd_isr_cb(void)
{
u8 i;
gpio_set_mode(PORTC, PORT_PIN_7, PORT_OUTPUT_HIGH);
for (i = 0; i < 10; i++) {
gpio_write(IO_PORTC_07, 1);
udelay(5);
gpio_write(IO_PORTC_07, 0);
udelay(5);
}
gpio_set_mode(PORTC, PORT_PIN_7, PORT_HIGHZ);
segment_code_lcd_start();
}
Source: segment_code_lcd_demo.c
跳秒功能
segment_code_lcd_set_dot_en() 控制跳秒脚(DOT)。驱动特意限制:仅当时钟源为 LCD_CLK_SEL_OSL(外挂 32K 晶振)时才允许使能,因为跳秒的精度依赖外部晶振:
void segment_code_lcd_set_dot_en(u8 dot_en)
{
if (__this->init == 0) {
return;
}
if (__this->usr_data->clk != LCD_CLK_SEL_OSL) {
return;
}
LCD_DOT_EN(dot_en);
}
Source: segment_code_lcd_drv.c
核心流程
sequenceDiagram
participant App as 应用
participant Drv as segment_code_lcd_drv
participant P33 as P33 寄存器
participant Panel as LCD 面板
App->>Drv: segment_code_lcd_init(&cfg)
Drv->>P33: 清 LCDC_CON0~7 / SEG_IO_EN0~3
Drv->>Drv: segment_code_lcd_pump_init() 上电时序
Drv->>P33: 时钟/VLCD/BIAS/COM 使能/模式配置
Drv->>P33: LCD_ENABLE(1) + SW_KST(1)
P33-->>Panel: 开始输出 COM/SEG 扫描波形
App->>Drv: fill_number / fill_char / fill_point
App->>Drv: segment_code_lcd_disp_update()
Drv->>P33: 仅刷新 disp_buffer 变化的 COM 行<br/>(p33_tx_nbyte 写 P3_SEG*_DAT*)
P33-->>Panel: 新波形生效
alt 断续推屏模式 (ctu_en = 0)
P33-->>Drv: 完成一帧, PND 置位并触发中断
Drv->>Drv: 清 PND, LCD_ENABLE(0)
alt isr_cb 存在
Drv->>App: isr_cb() 执行复用功能
App->>Drv: segment_code_lcd_start()
else isr_cb 为空
Drv->>P33: LCD_ENABLE(1) + SW_KST(1)
end
P33-->>Panel: 继续下一帧
else 连续推屏模式 (ctu_en = 1)
Note over P33,Panel: 硬件自动循环推屏, 无中断
end
流程解读:初始化完成后硬件即开始推屏,此后应用的所有"画图"操作都只在 RAM 缓存中进行,只有 disp_update() 才触碰 P33 总线;断续模式下每帧结束都经历"停屏 → 复用 → 重启"的循环,isr_cb 的存在与否决定了重启动作由应用还是驱动完成。
推屏模式的状态机可概括为:
flowchart TD
Start([init 完成]) --> Mode{"ctu_en 配置"}
Mode -->|"1 连续推屏"| Cont["硬件自动循环推屏"]
Mode -->|"0 断续推屏"| Disc["完成一帧 -> PND 置位 -> 中断"]
Disc --> CB{"isr_cb 非空?"}
CB -->|"否"| Auto["ISR 自动 LCD_ENABLE(1)+SW_KST(1)"]
CB -->|"是"| CbRun["isr_cb: 复用 SEG IO<br/>执行其他功能"]
CbRun --> ReStart["应用调用 segment_code_lcd_start()"]
ReStart --> Disc
Auto --> Disc
Cont --> Cont
使用示例
配置平台数据
Demo 展示了 6COM×21SEG(lcd_com_en=0x3F、lcd_seg_en=0x7FFFFC)断码屏的完整配置:WCLK 时钟、3.8V 泵压、1/3 bias、强充电模式、断续推屏并使能帧中断:
const struct segment_code_lcd_platform_data lcd_test_data = {
.clk = LCD_CLK_SEL_WCLK,
.chgpump_vol = LCDCP_PUMPVOL_SEL_3P8V,
.ldob_vol = LCDCP_LDOB_SEL_3P3V,
.vlcd = LCD_VOL_SEL_1P000XLCDCP,
.bias = LCD_BIAS_1DIV3,
.chgmode = LCD_CHGMODE_STRONG,
.chgduty = 0,
.dot_com = LCD_COM0,
.dot_seg = LCD_SEG18,
.lcd_com_en = 0x3F, //6com
.lcd_seg_en = 0x7FFFFC, //21seg
.ctu_en = 0, //0:断续推屏(en ie) 1:连续推屏
.isr_cb = segment_code_lcd_isr_cb,
};
Source: segment_code_lcd_demo.c
数字显示与全流程测试
先用 LCD_POINT 定义每个 7 段数字的 A~G 段坐标(INVALID_COM 表示该段不用,如数字 "1" 的 F 段),组织成数组后交给 fill_number;测试流程为"全显 → 清屏 → 显示两组数字 → 更新",最后喂狗死循环:
#define NUM_SYS_HUN_A (LCD_POINT(0, 9))
#define NUM_SYS_HUN_B (LCD_POINT(0, 8))
...
#define NUM_SYS_HUN_G (LCD_POINT(1, 8))
static const u8 NUM_SYS_HUN[7] = {NUM_SYS_HUN_A, NUM_SYS_HUN_B, NUM_SYS_HUN_C, NUM_SYS_HUN_D, NUM_SYS_HUN_E, NUM_SYS_HUN_F, NUM_SYS_HUN_G};
void segment_code_lcd_test(void)
{
segment_code_lcd_init(&lcd_test_data);
segment_code_lcd_disp_all();
segment_code_lcd_disp_update();
mdelay(500);
segment_code_lcd_disp_clear();
segment_code_lcd_fill_number(NUM_SYS_HUN, 1);
segment_code_lcd_fill_number(NUM_SYS_TEN, 2);
segment_code_lcd_fill_number(NUM_SYS_ONE, 3);
segment_code_lcd_fill_number(NUM_DIA_HUN, 1);
segment_code_lcd_fill_number(NUM_DIA_TEN, 5);
segment_code_lcd_fill_number(NUM_DIA_ONE, 7);
segment_code_lcd_disp_update();
while (1) {
wdt_clear();
}
}
Source: segment_code_lcd_demo.c
该示例揭示了两个典型用法约定:填充 API 与更新 API 分离(先批量画点、再一次同步,减少 P33 总线写入);disp_update 后屏幕即由硬件接管,应用只需维持缓存即可。
配置选项
| 选项 | 类型 | 默认/推荐值 | 说明 |
|---|---|---|---|
SEGMENT_CODE_LCD_AUTO_TRIM_ENABLE(编译宏,drv.c L13) | int | 0 | 是否在每次开机时用 ADC(VBG 基准)自动校准 VLCD 电压;开启后初始化会遍历 PUMPVOL±2 档与 8 档 VOL_SEL 寻找最优组合 |
clk | enum | LCD_CLK_SEL_WCLK | 扫描时钟源;跳秒功能要求 LCD_CLK_SEL_OSL |
chgpump_vol | enum | LCDCP_PUMPVOL_SEL_3P6V | 泵输出目标电压(3.0V~4.5V,步进 ~100mV) |
ldob_vol | enum | LCDCP_LDOB_SEL_3P3V | LDOB 稳压输出档位 |
vlcd | enum | LCD_VOL_SEL_1P000XLCDCP | 面板实际工作电压 = VLCDCP × 系数(0.886~1.000) |
bias | enum | LCD_BIAS_1DIV3 | 偏压比,1/2、1/3、1/4 按面板规格选择 |
chgmode | enum | LCD_CHGMODE_STRONG | 充电策略:弱充(省电)/强充/半周期/多周期 |
chgduty | u8 | 0 | 多周期充电模式下强驱持续 chgduty+1 个 32K 时钟 |
dot_com / dot_seg | u8 | LCD_COM0 / LCD_SEG18 | 跳秒脚位置 |
lcd_com_en | u32 | 0x3F | COM0~COM5 使能位图 |
lcd_seg_en | u32 | 0x7FFFFC | SEG0~SEG26 使能位图 |
ctu_en | u8 | 0(断续) | 1 为连续推屏,0 为断续推屏(配合 isr_cb) |
isr_cb | 函数指针 | NULL | 帧中断回调;NULL 时驱动自动重启推屏 |
API 参考
void segment_code_lcd_init(const struct segment_code_lcd_platform_data *lcd_dat)
驱动初始化入口,必须先调用。内部完成寄存器清零、电荷泵上电、时钟/电压/BIAS/模式配置并启动推屏。
- 参数
lcd_dat:平台配置指针,不可为 NULL;重复调用被忽略(init标志保护)。 - 注意:若使用外部晶振(
LCD_CLK_SEL_OSL),须在rtc_dev_init之后初始化本驱动。
void segment_code_lcd_fill_point(u8 icon, u8 isfill)
填充或清除一个交叉点。
- 参数
icon:LCD_POINT(com, seg)编码,高 5 位 SEG、低 3 位 COM;isfill:1 点亮 / 0 熄灭。 - 行为:仅修改
disp_buffer,不写硬件;com/seg 越界时静默返回。
void segment_code_lcd_fill_number(const u8 *ptr, u8 num)
按 7 段码表填充一个数字。
- 参数
ptr:7 个LCD_POINT编码数组(A~G 段顺序);num:0~9 显示对应数字,'-'显示负号,>= 11(数组大小)时清除该位全部段。 - 行为:仅修改
disp_buffer。
void segment_code_lcd_fill_char(const u8 *ptr, u8 cc)
按 7 段码表填充一个字母('a'~'z' 或 'A'~'Z',大小写分别查表)。
void segment_code_lcd_disp_clear(void) / void segment_code_lcd_disp_all(void)
清空整个 disp_buffer / 按 lcd_com_en、lcd_seg_en 位图点亮全部可用点。仅改缓存。
void segment_code_lcd_disp_update(void)
把 disp_buffer 与 save_buffer 的差异逐 COM 写入 P33 像素寄存器(P3_SEG0_DAT0/P3_SEG4_DAT0),完成"缓存 → 硬件"同步。
void segment_code_lcd_set_ctu_en(u8 ctu_en)
运行时切换推屏模式:1 连续推屏(关 IE),0 断续推屏(开 IE)。未初始化时忽略。
void segment_code_lcd_set_dot_en(u8 dot_en)
使能/禁能跳秒脚;仅 clk == LCD_CLK_SEL_OSL 时生效,其余时钟源下静默忽略。
void segment_code_lcd_start(void)
重新启动推屏:LCD_ENABLE(1) + LCD_SW_KST(1)。断续推屏且使用 isr_cb 时,复用功能完成后必须调用,否则屏幕停更。
void segment_code_lcd_set_all_io_highz(void)
把 lcd_com_en/lcd_seg_en 位图对应的全部 COM/SEG 引脚配置为高阻输入,用于复用切换前的释放。
中断服务 static void segment_code_lcd_isr(void)
P33 LCD 帧中断处理:清 PND、关 LCD_ENABLE,然后按 isr_cb 是否注册决定回调应用或自动重启推屏。
失败模式、边界情况与并发
初始化保护
segment_code_lcd_init 同时检查 __this->init 与 lcd_dat == NULL,防止重复初始化覆盖运行中的显示配置,也防止空指针解引用。但驱动不提供反初始化接口——一旦初始化,P33 寄存器配置会保持到掉电。
越界与非法输入
fill_point对com >= LCD_COM_MAX (6)、seg >= LCD_SEG_MAX (27)静默返回,避免写坏disp_buffer内存;fill_number/fill_char对ptr == NULL直接返回;数字超过 9 会按数组边界钳制到"清除"档;disp_update/disp_all/set_all_io_highz均检查usr_data是否已初始化(NULL直接返回),保证未 init 时调用不崩溃。
中断上下文约束
segment_code_lcd_isr 在中断上下文执行,且 isr_cb 被调用前 LCD_ENABLE 已被关闭。因此:
isr_cb内不得长时间阻塞(Demo 中仅做 10 次 5µs 级 GPIO 翻转);若复用功能耗时较长,应记录标志位后在主循环处理,并在主循环中调用segment_code_lcd_start();- 若
isr_cb内再次触发 LCD 相关寄存器操作,需注意与硬件推屏状态的时序; - 忘记调用
segment_code_lcd_start()是最常见的故障模式:表现为屏幕刷新一次后永久定格。
时钟与功能耦合
跳秒功能依赖 LCD_CLK_SEL_OSL,使用 WCLK/LRC 时钟源时 set_dot_en 静默失效——这是硬件精度约束,而非软件缺陷。
并发与原子性
驱动是单实例(静态 _lcd_var)且所有 API 均为非可重入设计:
- 若应用在中断/任务中同时调用填充与更新 API,需要自行保证临界区(例如关中断)——
disp_buffer的读-改-写不是原子的; disp_update的差分比较(读save_buffer、写save_buffer、写 P33)之间同样存在竞态窗口,多任务场景建议串行化调用。
电压校准(可选路径)
SEGMENT_CODE_LCD_AUTO_TRIM_ENABLE=1 时,初始化会通过 ADC 采样 AD_ANA_PMU_CH_VBG 与 AD_ANA_PMU_CH_VLCD_DIV_4 遍历泵压档位寻找最优组合。该路径在关 LCDCP_ADC_DET_EN 前完成,且依赖 adc_get_value_by_blocking——开机阶段会阻塞较长执行时间,对启动时延敏感的产品默认关闭(宏默认 0)。
性能与运维
- 差分刷新:
disp_update只写内容变化的 COM 行,静态画面下 P33 总线零写入,显著降低功耗——这是save_buffer存在的根本原因; - 批量写:每个 COM 行按 4 字节
p33_tx_nbyte一次传输,而非逐 bit 操作,缩短总线占用; - 连续推屏免中断:无 IO 复用需求时用
ctu_en=1,硬件自主刷新,CPU 零中断开销; - 推屏时序:电荷泵建立需要固定延时(PD15 输出 500µs → VBG 500µs → PUMP 1.5ms),初始化期间不可抢占关键延时(
delay_us忙等); - 调试手段:寄存器组提供测试模式(
LCD_TEST_EN+LCD_SEG_TEST_FLAG/LCD_COM_TEST_FLAG),可将 SEG/COM 引脚直接输出 VLCD 各分压电平(VDD/VL2/VL1/VL0/PD),用于硬件排障。
扩展点
isr_cb回调:IO 复用的标准扩展钩子,可在每帧间隙执行 GPIO 复用功能;- 平台数据结构:通过
lcd_com_en/lcd_seg_en/dot_com/dot_seg可适配任意段数、任意跳秒位置的断码屏,无需改驱动; - 段码表:
lcd_seg_number_2_seg、lcd_seg_large_letter_2_seg、lcd_seg_small_letter_2_seg为static const,如需自定义字形(如箭头、特殊符号)可直接修改或按相同模式新增查表 API; SEGMENT_CODE_LCD_AUTO_TRIM_ENABLE:量产屏幕离散性大时可开启开机自动校准,也可移植segment_code_lcd_pump_trim的逻辑做产线校准流程;- 硬件映射:
hw_seg2pin_mapping/hw_com2pin_mapping为芯片固定引脚,换芯片型号时需同步更新这两张表。
测试
segment_code_lcd_demo.c(segment_code_lcd_test)覆盖了以下路径:
- 初始化与电荷泵上电(
segment_code_lcd_init); - 全显/清屏(
disp_all→disp_update→disp_clear); - 6 位数字显示(系统区 123 + 日期区 157),验证
LCD_POINT坐标定义与 7 段码表; - 断续推屏 +
isr_cbIO 复用(GPIO 翻转后segment_code_lcd_start恢复); - 看门狗喂狗(
wdt_clear)保证测试循环不触发复位。
未覆盖(源码中未见):连续推屏模式下的完整回归、set_dot_en 跳秒实际波形验证、自动校准(AUTO_TRIM 默认关闭)路径的板级验证。
相关链接
- 驱动实现:segment_code_lcd_drv.c
- 驱动头文件(寄存器宏与使用流程):segment_code_lcd_drv.h
- Demo 测试程序:segment_code_lcd_demo.c
- 点阵 LCD 驱动(兄弟页面):lcd_drive.c 与 lcd_drive.h