STM8S平台DALI灯光函数库:曼彻斯特编码与帧时序全解析
2026/9/13 0:32:39 网站建设 项目流程

简介:面向STM8S系列微控制器的DALI协议固件库,为智能照明、商业楼宇和公共设施等场景的灯光控制提供完整开发支持,开发者可基于此快速实现灯具寻址、亮度调节、场景切换与故障检测等DALI标准功能,适合具备C语言和嵌入式基础的软硬件工程师使用。压缩包共83个文件:36个C源码与35个头文件组成核心协议栈与外设驱动,辅以2个HTML说明文档、工程配置和位图文件,整体仅259KB,C/H文件覆盖DALI命令收发、CRC校验及STM8S标准外设接口。HTML与工程文件便于查阅版本说明和导入EWSTM8/STVD开发环境。资源上线以来已有200人学习下载,库中提供的API函数、工程模板和响应解析示例,能有效降低DALI协议在STM8S平台上的移植门槛,帮助开发者跳过底层寄存器配置,直接聚焦灯具控制逻辑。目录按DALIStack、STM8S_StdPeriph_Driver和Project等模块划分,便于快速定位与二次开发。

1. DALI 灯光函数库:STM8S 上把 1200 baud 的曼彻斯特码变成可调用 API

DALI(Digital Addressable Lighting Interface)是照明行业最常见的调光总线,两根线、1200 baud、曼彻斯特编码,一个正向帧只有 19 个比特位,却要覆盖从开关、调光到寻址、故障查询的整套灯具控制逻辑。STM8S 这类 8 位 MCU 算力完全够,真正的门槛在时序:一个位时间只有 833 µs,帧收发的时序参数差一个微秒,整帧就作废。stm8s_dali_lib 这类 Dali 灯光函数库要解决的正是这个问题——把物理层收发、帧解析、应答窗口、命令封装按层次整理成可移植代码,让做灯具驱动或 DALI 网关的工程师不用从曼彻斯特编码的脉宽计算开始重写。适合正在做低成本 DALI 从站选型、或准备把协议栈移植到 STM8S 的开发者继续往下看。

2. DALI 帧结构拆解:19 位正向帧、11 位应答帧与曼彻斯特编码规则

2.1 正向帧和应答帧:主站先发,从站在 7~22 位时间内回答

DALI 是半双工单主机总线。主站先发正向帧:1 位起始 + 8 位地址 + 8 位命令 + 2 位停止,共 19 位;从站对需要应答的命令返回 11 位应答帧:1 位起始 + 8 位数据 + 2 位停止,地址和数据都是先发最高位。总线空闲为高,从站没有主动上报能力,灯亮不亮、有没有故障,全要靠主站逐条查询。

主站发完正向帧后,从站必须在停止位之后的 7~22 个位时间内把应答帧发出来,也就是约 5.8~18.3 ms 的窗口。这个窗口直接决定两件事:主站侧要有接收超时定时器,窗口过了还没数据就按无应答处理;从站侧负责回包的代码必须在这个窗口内完成执行,不能等主循环慢慢轮询。DALI 帧没有校验和,合法性全靠曼彻斯特编码的连续性和位时间容差来保证。

参数数值换算关系
位时间833.33 µs1/1200 s
正向帧总时长约 15.8 ms19 位 × 833 µs
应答帧总时长约 9.2 ms11 位 × 833 µs
应答窗口7~22 位时间停止位后 5.8~18.3 ms
帧间最小空闲8 位时间约 6.7 ms

2.2 曼彻斯特编码判定:高电平 2/3 是 1,高电平 1/3 是 0

DALI 的曼彻斯特编码和以太网的 50% 占空比翻转不一样,它把一个位时间三等分:

  • 逻辑 1:前 2/3 为高电平,后 1/3 为低电平
  • 逻辑 0:前 1/3 为高电平,后 2/3 为低电平

两种逻辑都以高电平开头,所以接收端不需要锁相环,只要量每个位的高电平脉宽:实测 555 µs 判 1,278 µs 判 0,阈值取两者中点约 416 µs。IEC 62386 允许位时间有 ±10% 偏差,也就是 555 µs 高脉宽最差能落到 500~611 µs,278 µs 最差落到 250~306 µs,和阈值之间仍有约 80 µs 余量,8 位机上用定时器捕获做分类足够,不必上模拟解调。

起始位和停止位都是逻辑 1。总线空闲为高,因此主站发出起始位后,接收方看到的第一个下降沿出现在起始位高电平结束处,状态机用这个下降沿做帧同步即可。解码时若发现脉宽落在两个合法区间之外,或者帧位数不对,整帧丢弃,我一般会在 phy 层同时做这两项合法性检查。一个完整的正向帧示例如下:

帧 = 起始位(1) | 地址字节 | 命令字节 | 停止位(11) 对 1 号灯发 DAP=0xFE: 逐位序列 = 1 00000001 11111110 11 (19 位) 每个位按 1/3-2/3 高低相位展开,停止位之后总线恢复高

2.3 地址字节与命令字节:短地址、组地址、广播的字节布局

正向帧的地址字节不是普通的 0~255 取值,而是三种形态复用:

地址形态字节值说明
短地址0x00~0x3Fbit7=0,低 6 位为地址 0~63
组地址0x80~0x8Fbit7=1、bit6=0,低 4 位为组号 0~15
广播0xFE所有从站同时响应
特殊命令0xFF配合 DTR 等配置流程使用

命令字节复用同一个字节,0x00~0x1F 是控制命令,0x20~0xFE 是 DAP(Direct Arc Power)直接调光电平:

命令字节值说明
OFF0x00输出降到底部后关断
GO TO LAST ACTIVE LEVEL0x07恢复上次亮度,等效开灯
GO TO SCENE 0~150x08~0x17调出预存场景
DAP 直接调光0x20~0xFE0xFE 约满亮,值越大越亮

DAP 是使用频率最高的命令,灯光函数库通常把它单独封装成 dali_set_level(addr, level),把控制命令封装成 dali_send_cmd(addr, cmd),避免调用时把 0x00 当成"亮度零"。DAP 的 0x20~0xFE 覆盖约 12%~100% 输出,0x01~0x1F 虽然也是合法电平值,但通常留给控制命令区间,具体以灯具驱动固件的实现为准。

3. STM8S 物理层实现:定时器做时基,输入捕获量脉宽

3.1 为什么不用 UART 发 DALI 帧:时序差异与中断负载

一开始容易想到用 UART 的波特率发生器来逼近 1200 baud,但 DALI 的曼彻斯特码不是标准 NRZ,UART 的起止位结构和 DALI 帧也不对应。市面上用 UART 模拟的写法多半要牺牲容差,还要在驱动里额外处理应答窗口,代码省不了多少。常见且可靠的做法是 GPIO + 定时器:发送用定时器中断逐相位翻转电平,接收用输入捕获量高电平脉宽。

以 STM8S103F3 为例,HSI 16 MHz 内部 RC 做主时钟,一个计数周期 62.5 ns,833 µs 位时间对应 13328 个计数,16 位定时器轻松容纳。中断负载上,接收每比特最多两个边沿事件,1200 baud 下约 2400 次/秒,对 8 位内核可以忽略;发送端每相位一个中断,也是约 2400 次/秒。定时器预分频的选择直接影响量化精度:

预分频计数频率量化步长833 µs 对应计数适用场景
/116 MHz62.5 ns13328通用,精度最好
/82 MHz500 ns1666降低功耗或定时器资源紧张

3.2 发送端实现:帧展开成相位序列,TIM2 中断逐电平输出

发送端把每个数据位展开成前后两个相位,逻辑 1 是"高 555 µs + 低 278 µs",逻辑 0 是"高 278 µs + 低 555 µs"。帧先准备成一个位序列缓冲区,缓冲区的 bit0 对应帧的起始位。每进入一次 TIM2 中断就推进一个相位,更新定时器装载值并设置 GPIO 电平:

/* dali_phy_tx.c —— TIM2 中断驱动的曼彻斯特发送 */ #define F_CPU 16000000UL #define US_TO_TICK(us) ((uint16_t)((uint32_t)(us) * F_CPU / 1000000UL)) /* 相位时长表:索引 0=逻辑0,1=逻辑1 */ static const uint16_t phase_hi_tick[2] = { US_TO_TICK(278), US_TO_TICK(555) }; static const uint16_t phase_lo_tick[2] = { US_TO_TICK(555), US_TO_TICK(278) }; static uint8_t tx_buf[3]; /* 位序:buf 的 bit0 最先发送 */ static uint8_t tx_bits_total; /* 帧总位数,例如 19 */ static uint8_t tx_phase_idx; /* 0~37,每个位两个相位 */ void dali_phy_send_frame(uint8_t *bits, uint8_t bit_cnt) { uint8_t i; for (i = 0; i < (bit_cnt + 7) / 8; i++) tx_buf[i] = bits[i]; /* 复制帧到位缓冲区 */ tx_bits_total = bit_cnt; tx_phase_idx = 0; TIM2->ARRH = (uint8_t)(phase_hi_tick[1] >> 8); /* 起始位恒为逻辑1 */ TIM2->ARRL = (uint8_t)(phase_hi_tick[1]); DALI_TX_HIGH(); /* 起始位前相位输出高 */ TIM2->SR1 &= ~TIM2_SR1_UIF; TIM2->CR1 |= TIM2_CR1_CEN; /* 启动定时器 */ } void TIM2_UPD_OVF_IRQHandler(void) { uint8_t bit_no, bit_val; TIM2->SR1 &= ~TIM2_SR1_UIF; tx_phase_idx++; if (tx_phase_idx >= tx_bits_total * 2) { TIM2->CR1 &= ~TIM2_CR1_CEN; /* 发完停止位低相位 */ DALI_TX_HIGH(); /* 释放总线回空闲高 */ return; } bit_no = tx_phase_idx >> 1; bit_val = (tx_buf[bit_no >> 3] >> (7 - (bit_no & 7))) & 1; if (tx_phase_idx & 1) { TIM2->ARRH = (uint8_t)(phase_lo_tick[bit_val] >> 8); TIM2->ARRL = (uint8_t)(phase_lo_tick[bit_val]); DALI_TX_LOW(); } else { TIM2->ARRH = (uint8_t)(phase_hi_tick[bit_val] >> 8); TIM2->ARRL = (uint8_t)(phase_hi_tick[bit_val]); DALI_TX_HIGH(); } }

逻辑说明:中断先判断相位序号是否走完,走完就停定时器并把总线置回高,这一步不能省——若停在低电平,总线一直拉低,接收方永远等不到下一个起始位。中间相位按"位号 → 位值 → 查表"三步展开,位的先后顺序由缓冲区 bit0 最先发送的约定决定。参数上注意两点:一是起始位恒为逻辑 1,所以第一次装载的是 phase_hi_tick[1],不能写成索引 0;二是 ARR 装载必须在清除 UIF 之后,否则同周期可能多触发一次中断。

提示:中断函数名在不同编译器下写法不同,SDCC 用interrupt(15)这类修饰,Cosmic 和 IAR 用启动文件里的向量名;移植时先查对应启动文件的 IRQ 向量表,把函数名对齐。

3.3 接收端实现:输入捕获状态机与高脉宽阈值

接收端利用"所有位都以高电平开头"这一特性,只需要量每个位的高电平脉宽。状态机分三段:RX_IDLE 等第一个下降沿(起始位的 2/3 处),RX_WAIT_RISE 等起始位低相位结束,RX_BITS 逐位测量 16 个数据位加 2 个停止位的高脉宽。用 TIM1 输入捕获,上升沿和下降沿都产生捕获中断,下降沿时用当前计数值减上次计数值得出高脉宽:

/* dali_phy_rx.c —— TIM1 输入捕获解码 */ #define RX_BIT_CNT 18 /* 起始位之后的 16 数据位 + 2 停止位 */ #define RX_THRESHOLD_TICK US_TO_TICK(450) /* 判 1/0 阈值,留余量 */ typedef enum { RX_IDLE, RX_WAIT_RISE, RX_BITS } rx_state_t; static volatile rx_state_t rx_st; static volatile uint8_t rx_bit_cnt; static volatile uint8_t rx_buf[3]; static volatile uint16_t rx_last_tick; void TIM1_CAP_COM_IRQHandler(void) { uint16_t now, diff; if (!(TIM1->SR1 & TIM1_SR1_CC1IF)) return; now = (uint16_t)((TIM1->CCR1H << 8) | TIM1->CCR1L); diff = now - rx_last_tick; /* 相邻两次边沿的间隔 */ rx_last_tick = now; if (rx_st == RX_IDLE) { /* 第一个下降沿:起始位高相位结束,帧同步 */ rx_bit_cnt = 0; rx_st = RX_WAIT_RISE; } else if (rx_st == RX_WAIT_RISE) { /* 上升沿:起始位低相位结束,准备收数据位 */ rx_st = RX_BITS; } else { /* 下降沿:刚量完一个位的高脉宽 */ if (diff > RX_THRESHOLD_TICK) rx_buf[rx_bit_cnt >> 3] |= (0x80 >> (rx_bit_cnt & 7)); rx_bit_cnt++; if (rx_bit_cnt >= RX_BIT_CNT) { rx_st = RX_IDLE; /* 停止位收完,回空闲 */ dali_phy_frame_done(rx_buf); /* 交 proto 层解析 */ } } TIM1->SR1 &= ~TIM1_SR1_CC1IF; }

逻辑说明:状态机在 RX_BITS 里只取下降沿事件,diff 的起止点分别是上一次上升沿和本次下降沿,正好等于一个位的高电平脉宽。阈值取 450 µs 而不是理论中点 416 µs,是因为内部 RC 有约 ±2% 温漂,光耦和隔离器还会引入边沿抖动,向 555 µs 一侧偏一点更稳。RX_WAIT_RISE 状态不消费位数据,因为起始位的低相位不属于任何数据位。

补充两点边界:16 位定时器回绕用差值计算天然免疫,只要任意两个相邻边沿间隔小于 65535 计数(约 4 ms),而 DALI 最长合法脉宽 555 µs 远小于这个值;帧完成回调要尽快执行,从站应答需要在停止位后 5.8~18.3 ms 内发出,如果 proto 层处理耗时不可控,我一般会把应答帧预组装放在中断里,主循环只负责启动发送。

3.4 电气接口与极性:MCU 引脚怎么接 DALI 总线

DALI 总线两根线没有极性要求,空闲时由总线电源经电阻拉高,典型空载电压约 16 V,远高于 MCU 电平,必须隔离。常见接口是:MCU 的 TX 引脚经三极管或光耦的集电极把总线拉低,总线高电平对应 MCU 输出 1(释放),总线低电平对应 MCU 输出 0(拉低);RX 侧用光耦或比较器把总线电平读回 3.3 V 逻辑。

这一层差异建议全部收口在 dali_hal.c 的三个宏里:DALI_TX_HIGH()、DALI_TX_LOW()、DALI_RX_LEVEL()。若 RX 光耦是反相接法,DALI_RX_LEVEL 里取反一次即可。调试时最容易翻车的是 TX 引脚逻辑和总线实际电平相反——先拿示波器分别量 MCU 引脚和总线,确认输出 1 时总线为高、输出 0 时总线为低,再做时序分析。

4. dali 灯光函数库的分层与调用:cfg、hal、phy、proto、app

4.1 库的分层结构与文件职责

工程里常见的 DALI 灯光函数库按五层拆分,每层只依赖下一层,换 MCU 或换灯具时改动范围可以很小:

文件职责关键接口
配置dali_cfg.h引脚、时钟、本机地址、电平范围DALI_OWN_ADDR 等宏
硬件抽象dali_hal.cGPIO/定时器初始化、电平宏封装dali_hal_init()
物理层dali_phy.c帧收发、应答窗口、接收回调dali_phy_send_frame()
协议层dali_proto.c地址匹配、命令表、查询应答dali_proto_poll()
应用层dali_app.c场景、DTR、灯状态机dali_app_task()

phy 层不应当知道命令含义,它只负责把地址加命令拼成 19 位帧并按时序发出;应答窗口的超时统计也放在 phy 层,因为那是时间问题,不是业务问题。proto 层才解析地址形态,决定这帧是不是发给自己的、要不要回包、回什么内容。同一套库的主站和从站差别只在上层:主站调 dali_query 等主动接口,从站跑 dali_proto_poll 被动响应。

4.2 初始化顺序与主循环:中断收帧、循环跑状态机

从站的主循环结构通常是这样的:

void main(void) { dali_hal_init(); /* 时钟、GPIO、TIM1/TIM2 */ dali_phy_init(); /* 状态机复位到 RX_IDLE */ dali_proto_init(); /* 清空命令表状态 */ enableInterrupts(); while (1) { dali_proto_poll(); /* 处理收到的帧并准备应答 */ dali_app_task(); /* 灯状态机、场景计时 */ } }

初始化顺序有一条硬约束:先保证 F_CPU 宏与实际时钟一致,再初始化定时器。STM8S 默认跑 HSI 16 MHz,若代码里把 F_CPU 写成 8 MHz,US_TO_TICK 算出的装载值会整体翻倍,波形位时间变成约 1.67 ms,远超 ±10% 容差。dali_proto_poll 只处理已经由中断收好的帧,不在循环里做阻塞等待,这样从站才能保证应答窗口不超时。若业务里有耗时操作(比如 EEPROM 擦写),必须分片执行,或者放到应答帧已经发出之后再处理。

4.3 常用 API 与参数:DAP 调光、查询应答、DTR 写配置

主站侧常见接口:

uint8_t dali_set_level(uint8_t addr, uint8_t level); /* level 取 0x20~0xFE */ uint8_t dali_send_cmd(uint8_t addr, uint8_t cmd); /* cmd 取 0x00~0x1F */ uint8_t dali_query(uint8_t addr, uint8_t query, uint8_t *resp); uint8_t dali_store_dtr(uint8_t value);

参数说明:

  • addr:短地址 0x00~0x3F、组地址 0x80~0x8F、广播 0xFE 直接传入,库内部按 bit7/bit6 判断地址形态。
  • level:DAP 值 0x20~0xFE,0xFE 对应满亮。做百分比映射时按 254 等分换算即可,但灯具驱动内部可能带调光曲线,精确控制前先读 QUERY ACTUAL LEVEL 校准。
  • dali_query:内部会等待应答窗口约 19 ms,超时返回 DALI_TIMEOUT,调用方需要区分"灯不在线"和"查询命令不支持"两种失败。

从站的查询应答在 proto 层完成,常见写法:

/* dali_proto.c —— QUERY STATUS (0xA0) 应答组装 */ static uint8_t query_status(void) { uint8_t status = 0; if (app_light_state != 0) status |= 0x01; /* 灯亮 */ if (app_dim_error) status |= 0x02; /* 调光器故障 */ if (app_thermal_over) status |= 0x04; /* 过温 */ return status; }

状态字节的位定义由灯具厂商和驱动固件约定,库只负责把这个字节原样放进应答帧,不替业务层解释位含义。查询类命令的常见值:QUERY STATUS 0xA0、QUERY ACTUAL LEVEL 0xAE、QUERY DEVICE TYPE 0xA9,具体映射以库头文件里的命令表为准。

写配置类命令(比如最大亮度、最小亮度、淡入时间)走 DTR 两步流程:先 dali_store_dtr 把配置值写入灯具内的数据转移寄存器,再发 STORE 命令让灯具把 DTR 内容固化到对应配置项。两条命令之间至少要保留 6.7 ms 帧间空闲。

注意:主站对广播命令不需要等待应答,但仍要按最大应答窗口等 18.3 ms 再发下一条,否则部分从站可能来不及处理上一条命令,丢掉配置。

5. 波形验证与三个容易踩的边界条件

5.1 回环测试:一条查询命令验证整条收发链路

先把 TX 和 RX 在逻辑电平侧短接,发一条 QUERY STATUS。正常流程是主站发出 19 位正向帧,从站收到后在 5.8~18.3 ms 内回一个 11 位应答帧;回环场景下即便没有真实灯具,主站也能通过"发出查询后收到任何合法应答"确认物理层收发链路是通的。若收不到应答,先用示波器确认 TX 引脚确实有波形,再确认 RX 引脚能读到同样的电平变化,两步能定位出问题在发送侧还是接收侧。

5.2 示波器必量的三个时间点

波形验证不靠肉眼数脉冲,量三个时间点就够了:正向帧第一个下降沿(起始位结束)到停止位最后一位结束的总时长应为 15.8 ms 左右;逻辑 1 的高电平脉宽应在 500~611 µs,逻辑 0 的高电平脉宽应在 250~306 µs;查询命令停止位到应答帧起始位下降沿的间隔应在 5.8~18.3 ms。第三个点超时最常见,直接指向从站主循环阻塞或应答准备过晚。

5.3 三个容易翻车的边界条件

发送结束不释放总线。帧最后一位是停止位的低相位,若中断里发完就停定时器而不再把 TX 置高,总线会一直维持低电平,接收方永远等不到起始位。所有实现里发送完成分支都必须有 DALI_TX_HIGH(),并保证之后至少 6.7 ms 空闲。

阈值卡在理论中点 416 µs。内部 RC 的温漂和光耦的传输延迟会把脉宽推偏,我一般建议阈值取 430~460 µs,代码里用 US_TO_TICK(450) 并注释清楚按 555 µs 一侧留余量。取 450 µs 时距离 555 µs 还有 105 µs,距离 278 µs 有 172 µs,两侧都不冒险。

时钟宏和实际不符。STM8S 上电默认 16 MHz HSI,但若代码里开了外部晶振或改了 CLK_CKDIVR 分频,而 F_CPU 宏没同步,发送位时间会整体偏长或偏短,且所有波形都表现出同一规律的异常——这种问题示波器一眼能看出,但排查时容易先怀疑协议而不是先查时钟。把 F_CPU、CLK_CKDIVR、示波器实测三个数对齐,能省掉大部分假故障。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询