简介:面向嵌入式开发者的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_PWM | PB0 | 输出 | 走线尽量短,栅极串 10R 电阻 |
| WARM_PWM | PB1 | 输出 | 与冷白走线保持对称,防串扰 |
| KEY_CTRL | PB2 | 输入 | 按键接 GND,开启内部上拉 |
| VDD / VSS | 电源脚 | 电源 | 靠近芯片放 100nF 和 10uF 电容 |
引脚一旦在硬件上画死,固件里的宏就不需要反复调整。OTP 片子的特点是,硬件改版一次的成本远高于软件调试,所以引脚映射表应该在原理图评审阶段就定下来。
2.2 两路 LED 的低边 MOSFET 驱动电路
冷暖补光灯最常见的是共阳接法:冷白和暖白灯珠的正极并在一起接 LED 电源,负极分别接到 MOSFET 的漏极,两个 MOSFET 的源极共同接地。单片机只是给栅极提供 PWM,LED 的大电流不经过 NY8A051 引脚。
选 MOSFET 时要注意它是逻辑电平型,即 Vgs 在 2.5V 或 3.3V 时就能完全导通。常用型号是 AO3400 一类的低压 NMOS,Vds 耐压 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 和芯片封装里可能略有差异,正式编译前要对照当前头文件核对,但顺序不能变:先 PB=0,再设方向,最后开上拉。按键读到 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 频率会变成 500Hz,LED 驱动电感容易啸叫。
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_CONST | 79 | 频率偏移,出现电感啸叫或频闪 |
| 按键消抖时间 | 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_WARM |
|---|---|---|---|
| 2835 0.2W 冷白 | 160 左右 | 100 | 78 |
| 2835 0.2W 暖白 | 130 左右 | 100 | 81 |
| 30W COB | 110 左右 | 100 | 73 |
权重表的意思是,把冷白光效定为基准 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 samplerate=10M \ --channels 0=cold,1=warm \ --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 words |
| SRAM 占用 | 看符号表 | 不超过 48 字节 |
| PWM 周期 | 示波器量 PB0 | 1kHz ± 5% |
| 按键步进 | 按住 2 秒 | 亮度/色温平滑变化 |
| 待机功耗 | 串万用表 | 小于 0.1mA |
最后一个实用技巧是:在样机阶段把烧录脚做成测试点,而不是只依赖烧录座。OTP 烧坏了以后,用热风枪拆芯片再换新料,很容易伤到旁边 LED 和电容。预留邮票孔焊盘和四线测试点,烧录失败时直接换一颗备用芯片,成本最低。把备用料提前焊在转接板上,第一次上电调试会快很多。
本文还有配套的精品资源,点击获取