基于NY8A051的冷暖补光灯程序设计:软件PWM与恒功率调光 简介面向嵌入式开发者的NY8A051微控制器补光灯控制源码包专为低功耗、锂电池供电的便携补光灯设计实现亮度与冷暖色温的双重调节。压缩包共18个文件整体仅300KB包含C源码、头文件、链接映射、makefile、配置截图与bin固件等完整编译产物方便直接查阅逻辑或烧录验证。目前已有869人学习下载。代码中可见定时器与看门狗中断处理相关实现并配有工程配置与烧录选项说明适合需要参考NY8A051端口操作、PWM调光或低功耗中断设计的中初级嵌入式开发者。通过阅读主程序与头文件配置可以快速理清补光灯软件框架掌握亮度调节、冷暖切换及锂电管理的关键流程也可基于现有工程二次开发。1. 为什么补光灯程序要专门为 NY8A051 写一套冷暖逻辑很多补光灯固件看起来只是“点亮两串 LED”但一旦把方案定在 NY8A051 这颗 OTP 单片机上整个设计思路就会反过来程序只有一次烧录机会所以你能依赖的并不是反复改代码而是在第一版就把 PWM 中断、按键状态机、冷暖混光算法全部锁死。Program_补光灯程序_NY8A051_冷暖_ 这个标题背后是大量 LED 台灯、镜子灯、直播灯产品最常用的一条研发路径。适合读这篇内容的人有两类一是手里已经拿到冷白加暖白双色灯板想用最便宜的单片机把样机点亮的硬件工程师二是被“为什么中间色温亮度会变暗”这类问题卡过的固件开发者。结论并不复杂用 NY8A051 做冷暖补光灯核心不是算法而是把有限的 OTP 空间、中断负载和生产校验放在同等地位去考虑。下面从硬件边界开始一步步把代码落到可以烧录的状态。2. NY8A051 的硬件边界与冷暖驱动电路选型NY8A051 的资源相当紧张典型配置是 1K 级 OTP、几十字节 SRAM没有串行 Flash也没有内置 EEPROM。很多人拿它当普通 MCU 用结果第一版就把 IO 复用得过于复杂后面再想改只能换芯片。冷暖补光灯的正确做法是让单片机只输出两路 PWM 电平LED 电流完全由外部 MOSFET 和限流电阻决定程序里不要掺和任何模拟量判断。硬件设计得越干净固件就越简单。下面从时钟、引脚、驱动三部分讲清楚选型理由。2.1 内部 RC 时钟与引脚映射常见做法是直接用内部 RC 振荡器主频设在 4MHz 或 8MHz省掉外部晶振。补光灯不是通信设备PWM 频率误差在 ±3% 以内人眼通常感知不到所以内部时钟完全够用。真正需要注意的是引脚分配两路 PWM 各占一个 IO按键占一个 IO剩余的 IO 尽可能留给烧录和产线测试不要全部做成指示灯。我一般会把冷白输出放在 PB0暖白输出放在 PB1按键输入放在 PB2。这样安排的好处是逻辑分析仪夹子可以直接夹在 0、1 两个引脚上排线方向也顺手。下面是一张初始映射表功能典型引脚方向PCB 设计注意点COLD_PWMPB0输出走线尽量短栅极串 10R 电阻WARM_PWMPB1输出与冷白走线保持对称防串扰KEY_CTRLPB2输入按键接 GND开启内部上拉VDD / VSS电源脚电源靠近芯片放 100nF 和 10uF 电容引脚一旦在硬件上画死固件里的宏就不需要反复调整。OTP 片子的特点是硬件改版一次的成本远高于软件调试所以引脚映射表应该在原理图评审阶段就定下来。2.2 两路 LED 的低边 MOSFET 驱动电路冷暖补光灯最常见的是共阳接法冷白和暖白灯珠的正极并在一起接 LED 电源负极分别接到 MOSFET 的漏极两个 MOSFET 的源极共同接地。单片机只是给栅极提供 PWMLED 的大电流不经过 NY8A051 引脚。选 MOSFET 时要注意它是逻辑电平型即 Vgs 在 2.5V 或 3.3V 时就能完全导通。常用型号是 AO3400 一类的低压 NMOSVds 耐压 20V 到 30V 都够用。栅极串 10R 电阻能抑制振铃栅极对地再放一个 10k 电阻确保单片机上电瞬间 IO 还没初始化时灯不会误亮。限流电阻放在 LED 负极串联位置阻值要根据灯珠压降和电源电压计算不能只看电流表读数。以下是固件里对应的宏和初始化逻辑#define COLD_IO PB0 #define WARM_IO PB1 #define KEY_IO PB2 #define COLD_ON() COLD_IO 1 #define COLD_OFF() COLD_IO 0 #define WARM_ON() WARM_IO 1 #define WARM_OFF() WARM_IO 0 #define KEY_READ() KEY_IO void gpio_init(void) { PB 0; // 所有引脚先输出低避免上电闪白 PBC | (1 COLD_IO) | (1 WARM_IO); // 两个 PWM 脚设为输出 PBIOR | (1 KEY_IO); // 按键设为输入 PBPU | (1 KEY_IO); // 开启内部上拉 }这段代码的核心是“先拉低再定方向”。PBC、PBIOR、PBPU 这类寄存器名称在不同版本 IDE 和芯片封装里可能略有差异正式编译前要对照当前头文件核对但顺序不能变先 PB0再设方向最后开上拉。按键读到 0 表示按下1 表示释放。2.3 影响固件参数的三张底表硬件只要定了固件里的参数就都应该来自这张表而不是临时凑数。PWM 频率、分辨率和上电默认状态这三项是冷暖灯所有逻辑的地基。参数推荐值原因系统主频8MHz 内部 RC中断计时粒度细频率误差可接受PWM 频率1kHz直观无频闪软件中断压力小PWM 级数100 级人眼可分辨代码用一字节存得下最大占空比95%留出死区避免电源满载上电默认暖光 30% 亮度亮度低不会刺激眼睛色温范围3000K 到 6500K由冷暖两路灯珠规格决定这里特别提一下“最大占空比”。如果直接把占空比放到 100%MOSFET 会长时间完全导通LED 电流完全由限流电阻决定这时灯珠温度和电源电压波动会让亮度表现出明显的非线性。限到 95% 以后PWM 每周期会有一小段关断时间平均电流更接近设计值肉眼也看不出亮度损失。3. 用定时器中断把两路软件 PWM 跑起来NY8A051 这类低成本 OTP 单片机不一定都有硬件 PWM 外设即使有用软件 PWM 做双色灯也更灵活。软件 PWM 的思想很简单用一个周期计数器从 0 加到 99周期为 100计数器小于冷白占空比时开冷白小于暖白占空比时开暖白。两个占空比变量独立变化就能在同一个周期里输出不同的组合。3.1 定时器周期计算与初始化要让 PWM 频率稳定在 1kHz并且一周期有 100 级定时器中断频率就要到 100kHz。假设系统时钟 8MHz定时器不分频那么每次中断间隔是 80 个时钟周期。重载值就是 80 减 1得到 79。为什么减 1因为定时器从 79 递增到 255 是循环计数跳回起始值那一条指令本身就占一个计数周期。初始化代码可以写成这样#define PWM_PERIOD 100 /* 0~99一共 100 级 */ #define FREQ_CONST 79 /* 8MHz / (1kHz * 100) - 1 */ void timer0_init(void) { TMR0 FREQ_CONST; T0MD 0x00; /* 内部时钟1:1 分频 */ T0IF 0; /* 先清中断标志 */ T0IE 1; /* 使能定时器中断 */ }这里 T0MD、T0IF、T0IE 是用来说明逻辑的通用寄存器名在九齐官方头文件中可能叫其他名字但核心步骤就是三步放重载值、选时钟源、开中断。要注意的是主时钟从 8MHz 改到 4MHz 时FREQ_CONST 必须同步改成 39否则 PWM 频率会变成 500HzLED 驱动电感容易啸叫。3.2 中断里的 PWM 输出与主循环任务分离中断服务函数里只做比较和翻转不处理按键、不计算 gamma要把所有会变慢的东西挪到主循环。下面是最小可用的中断版本volatile unsigned char cold_duty 30; volatile unsigned char warm_duty 30; volatile unsigned char pwm_index 0; void timer0_isr(void) __interrupt(0) { if (pwm_index PWM_PERIOD) pwm_index 0; if (pwm_index cold_duty) COLD_ON(); else COLD_OFF(); if (pwm_index warm_duty) WARM_ON(); else WARM_OFF(); }这个函数每一步都是比较没有乘法也没有查表执行时间是固定的。cold_duty 和 warm_duty 都由主循环里的按键逻辑修改中断只管读取和翻转。主循环需要有一个 1ms 左右的时基通常由另一个定时器或者中断计数产生按键扫描就挂在那个时基上。PWM 输出的相位在这里是“冷白先开暖白后开”两路不会在同一时刻突然切换EMI 表现比同时翻转好。若用在直播补光灯这种对频闪敏感的场合还可以在中断末尾加上一个很小的死区让两路都先关断 2us。代码里加一个if (pwm_index 99) { COLD_OFF(); WARM_OFF(); }即可但会牺牲一点最大占空比。3.3 按键状态机短按切模式长按连续调按键逻辑要用状态机区分短按和长按不能用简单的 delay 消抖因为 delay 会阻塞 PWM 中断。以下代码假设 key_scan 每 1ms 被调用一次static unsigned int press_time 0; static unsigned char mode 0; /* 0 亮度1 色温 */ static unsigned char key_muted 0; void key_scan(void) { if (KEY_READ() 0) { press_time; if (press_time 15 key_muted 0) { key_muted 1; mode ^ 1; /* 短按切换调节对象 */ } if (press_time 500 (press_time 0x1F) 0) { /* 长按每 32ms 步进一档 */ if (mode 0) change_brightness(1); else change_cct(1); } } else { press_time 0; key_muted 0; } }关键点有两个。第一是key_muted它保证一次按下的第 15ms 只触发一次短按之后长按阶段不会反复切换模式。第二是(press_time 0x1F) 0这表示只有 press_time 是 32 的倍数时才执行相当于按 1ms 的时基每 32ms 响应一次如果主循环时基不是精确 1ms这个频控判断也要等比例调整。这种状态机比“定时查询按键电平”要可靠得多因为它天然地包含消抖和长按判定不需要单独记录上一次电平。调试时最容易出错的是没把 key_scan 放到 1ms 任务里而是放在主循环底部导致按键时快时慢。正确做法是用一个标志位比如if (tick_ms_flag) { tick_ms_flag 0; key_scan(); }。3.4 烧录前不要踩的三个坑OTP 芯片烧错一次就报废所以编译前必须把这三个参数单独确认一遍参数默认值错误后果FREQ_CONST79频率偏移出现电感啸叫或频闪按键消抖时间15ms消抖太短会被抖动误触发占空比上限95%全亮时 LED 电流超过规格占空比上限的实现在主循环或一个 clip 函数里每当 brightness 变化后把 cold_duty 和 warm_duty 都限制到 95 以内。不要只在初始化时限制一次因为长按调节过程中可能调满 100。通常我会写一个#define DUTY_MAX 95然后在change_brightness的末尾执行if (bright_value DUTY_MAX) bright_value DUTY_MAX;。上电瞬间也不要急着开灯至少等 50ms 后再更新 cold_duty 和 warm_duty否则电源还没稳定限流电阻两端电压也不稳。4. 冷暖混合的调光算法CCT 插值、gamma 曲线和亮度平衡硬件 PWM 通了之后另一套坑在“冷暖混合”这个需求上。很多人直接把两路都写成bright * cct / 100结果中间色温时总亮度只有两端的一半。原因很简单两路各按百分比输出总占空比就是两个百分比相加中间档位两份都是 50%总占空比只有 50%当然暗。4.1 恒功率拆分先给总亮度再切色温比正确做法是先确定当前总占空比 brightness再按色温档位切分冷白和暖白。把总占空比看作 100 份能量色温值决定这 100 份里有多少份给冷白剩下的全给暖白static void mix_bright_cct(unsigned char bright, unsigned char cct, unsigned char *duty_warm, unsigned char *duty_cold) { unsigned int cold_part; /* cct: 0全暖, 100全冷 */ cold_part (unsigned int)bright * cct / 100; if (cold_part bright) cold_part bright; *duty_cold (unsigned char)cold_part; *duty_warm (unsigned char)(bright - cold_part); }这段代码保证 cold_duty 加 warm_duty 永远等于 bright。色温在中间时两路各占一半总占空比仍是 100%。如果客户希望中间色温继续体感恒亮而不是严格恒功率之后调整的是 two 路 LED 的限流电阻配比不是改这个函数。这里要提到热搜词 “Program” 在标题里的含义了它不只代表固件也包括这套拆分逻辑。你写出来的程序如果只按照按钮数值简单映射那调的是开关不是补光灯。4.2 gamma 曲线查表低亮度段不再跳变LED 的光通量在大致上随 PWM 平均电流线性变化但人眼对低光强更敏感。直接等比调亮度用户会在前 20% 档位里看到亮度变化很快后面 80% 又没感觉。解决办法是把用户亮度先经过一条非线性曲线再转成 PWM 占空比。下面是一张 16 点 gamma 表对应近似 2.2 到 2.4 的矫正曲线const unsigned char gamma_table[16] { 0, 1, 2, 4, 7, 11, 16, 24, 34, 47, 64, 85, 110, 141, 177, 220 }; unsigned char gamma_apply(unsigned char level) { unsigned char idx level 4; /* 0~15 */ unsigned char frac level 0x0F; /* 0~15 */ unsigned char low gamma_table[idx]; unsigned char high gamma_table[idx 1]; return low ((unsigned int)(high - low) * frac 4); }它是把 0 到 255 的输入亮度映射成 0 到 220 的 PWM 输出。用线性插值而不是直接查表是为了避免 16 档之间出现肉眼可见的亮度断层。若 OTP 空间很紧张可以去掉插值部分只返回gamma_table[idx]代价是低亮度档位可能有 2% 到 3% 的跳变。对补光灯来说这个跳变在 10% 以下区域比较明显因此建议保留插值。实际使用时把上一节 mix_bright_cct 得到的 cold_part 和 warm_part 再套一层 gamma 即可*duty_cold gamma_apply(cold_part); *duty_warm gamma_apply(warm_part);注意这样会让两路各自做 gamma中间色温下的总亮度曲线可能与单路略有不同但整体观感更自然。若要追求绝对恒定应该对 brightness 先做 gamma再乘色温比例不过那会让低亮度下冷暖切换的步进不对称。我的经验是先拆分再分别 gamma在低成本方案里观感更顺。4.3 冷白与暖白的光效差异一个系数搞定冷白 LED 和暖白 LED 即使是同一封装、同一功率光效也可能差 10% 到 25%。如果只做恒功率拆分那么在同一色温档位下冷白偏亮色温测出来会比目标值偏高。常见做法是给冷白一个降额系数让程序里的冷白占空比略低于数学比例。灯珠类型典型光效 lm/W冷白权重 K_COLD暖白权重 K_WARM2835 0.2W 冷白160 左右100782835 0.2W 暖白130 左右1008130W COB110 左右10073权重表的意思是把冷白光效定为基准 100暖白只有它的 78% 光效。为了达到同样的视觉亮度暖白需要分配更多占空比。为了减少运行时计算可以先算出冷白占总功率的比例#define K_COLD 100 #define K_WARM 78 static unsigned char calc_cold_ratio(unsigned char cct) { unsigned long cp (unsigned long)cct * K_COLD; unsigned long wp (unsigned long)(100 - cct) * K_WARM; /* 返回 0~100 的冷白占比 */ return (unsigned char)(cp * 100UL / (cp wp)); }这个函数只需在色温变化时调用一次不需要放在 PWM 中断里。计算出来的 cold_ratio 再传给 mix_bright_cct就能保证中间色温的实际观感不偏冷。产线校准的时候用灯头的色温计测一下 50% 档位如果实测色温低于目标值就把 K_WARM 调大几个数如果偏冷调小 K_WARM。这个系数一般浮动在 70 到 90 之间不需要改电路。5. 上电试灯与 OTP 烧录前的验证方法OTP 单片机最怕的是问题在烧录后才暴露。因此验证要分成两步先用逻辑分析仪确认软件 PWM 输出正确再按烧录清单逐项过最后才把片子从烧录座上拿下来。5.1 用逻辑分析仪核对两路 PWM把逻辑分析仪通道 0 夹在 COLD_PWM通道 1 夹在 WARM_PWM采样率设成 10M抓 2 秒波形。命令行工具可以这样跑sigrok-cli --driver fx2lafw \ --config samplerate10M \ --channels 0cold,1warm \ --time 2s --output cold_warm.csv主要看三件事周期是否稳定在约 1ms占空比是否随按键变化两路是否出现同时恒高的状态。若周期偏移超过 5%优先查 FREQ_CONST 和主时钟配置若两路同时恒高持续超过一个周期说明 mix_bright_cct 里 warm_duty 或 cold_duty 溢出可能跑到了 100 以上。5.2 上电默认状态与记忆功能的取舍NY8A051 没有 EEPROM掉电前状态存不住。常见做法是每次上电固定恢复成“暖光 30%”这样开机不会刺眼也不会有产品状态混乱的问题。如果客户坚持要求记忆上次关灯色温就不要硬在 OTP 上做最好换一颗带 Data Flash 的型号或者在主板上并一颗几毛钱的 24C01。这个选型要在硬件评审前提出不能等程序写完再改。5.3 烧录前最后的检查清单检查项方法通过标准代码空间看编译后的 map 文件剩余至少 16 wordsSRAM 占用看符号表不超过 48 字节PWM 周期示波器量 PB01kHz ± 5%按键步进按住 2 秒亮度/色温平滑变化待机功耗串万用表小于 0.1mA最后一个实用技巧是在样机阶段把烧录脚做成测试点而不是只依赖烧录座。OTP 烧坏了以后用热风枪拆芯片再换新料很容易伤到旁边 LED 和电容。预留邮票孔焊盘和四线测试点烧录失败时直接换一颗备用芯片成本最低。把备用料提前焊在转接板上第一次上电调试会快很多。本文还有配套的精品资源点击获取