ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM8S平台DALI灯光函数库:曼彻斯特编码与帧时序全解析

STM8S平台DALI灯光函数库:曼彻斯特编码与帧时序全解析 简介面向STM8S系列微控制器的DALI协议固件库为智能照明、商业楼宇和公共设施等场景的灯光控制提供完整开发支持开发者可基于此快速实现灯具寻址、亮度调节、场景切换与故障检测等DALI标准功能适合具备C语言和嵌入式基础的软硬件工程师使用。压缩包共83个文件36个C源码与35个头文件组成核心协议栈与外设驱动辅以2个HTML说明文档、工程配置和位图文件整体仅259KBC/H文件覆盖DALI命令收发、CRC校验及STM8S标准外设接口。HTML与工程文件便于查阅版本说明和导入EWSTM8/STVD开发环境。资源上线以来已有200人学习下载库中提供的API函数、工程模板和响应解析示例能有效降低DALI协议在STM8S平台上的移植门槛帮助开发者跳过底层寄存器配置直接聚焦灯具控制逻辑。目录按DALIStack、STM8S_StdPeriph_Driver和Project等模块划分便于快速定位与二次开发。1. DALI 灯光函数库STM8S 上把 1200 baud 的曼彻斯特码变成可调用 APIDALIDigital 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 ms2.2 曼彻斯特编码判定高电平 2/3 是 1高电平 1/3 是 0DALI 的曼彻斯特编码和以太网的 50% 占空比翻转不一样它把一个位时间三等分逻辑 1前 2/3 为高电平后 1/3 为低电平逻辑 0前 1/3 为高电平后 2/3 为低电平两种逻辑都以高电平开头所以接收端不需要锁相环只要量每个位的高电平脉宽实测 555 µs 判 1278 µs 判 0阈值取两者中点约 416 µs。IEC 62386 允许位时间有 ±10% 偏差也就是 555 µs 高脉宽最差能落到 500~611 µs278 µs 最差落到 250~306 µs和阈值之间仍有约 80 µs 余量8 位机上用定时器捕获做分类足够不必上模拟解调。起始位和停止位都是逻辑 1。总线空闲为高因此主站发出起始位后接收方看到的第一个下降沿出现在起始位高电平结束处状态机用这个下降沿做帧同步即可。解码时若发现脉宽落在两个合法区间之外或者帧位数不对整帧丢弃我一般会在 phy 层同时做这两项合法性检查。一个完整的正向帧示例如下帧 起始位(1) | 地址字节 | 命令字节 | 停止位(11) 对 1 号灯发 DAP0xFE 逐位序列 1 00000001 11111110 11 19 位 每个位按 1/3-2/3 高低相位展开停止位之后总线恢复高2.3 地址字节与命令字节短地址、组地址、广播的字节布局正向帧的地址字节不是普通的 0~255 取值而是三种形态复用地址形态字节值说明短地址0x00~0x3Fbit70低 6 位为地址 0~63组地址0x80~0x8Fbit71、bit60低 4 位为组号 0~15广播0xFE所有从站同时响应特殊命令0xFF配合 DTR 等配置流程使用命令字节复用同一个字节0x00~0x1F 是控制命令0x20~0xFE 是 DAPDirect 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 的曼彻斯特码不是标准 NRZUART 的起止位结构和 DALI 帧也不对应。市面上用 UART 模拟的写法多半要牺牲容差还要在驱动里额外处理应答窗口代码省不了多少。常见且可靠的做法是 GPIO 定时器发送用定时器中断逐相位翻转电平接收用输入捕获量高电平脉宽。以 STM8S103F3 为例HSI 16 MHz 内部 RC 做主时钟一个计数周期 62.5 ns833 µ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逻辑01逻辑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、app4.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 MHzUS_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 判断地址形态。levelDAP 值 0x20~0xFE0xFE 对应满亮。做百分比映射时按 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、示波器实测三个数对齐能省掉大部分假故障。本文还有配套的精品资源点击获取
返回列表