# RGBCW LED 色温调节伽马插值 — 嵌入式实现指南

**产品**：TWBW-RGBCW-DENGDAI (BERSN)  
**版本**：v1.0  
**日期**：2026-04-14  

---

## 一、问题根源分析

### 1.1 为什么线性 PWM 会导致抖动

MCU 输出的 PWM 占空比是**线性值**（0～255），但人眼对亮度的感知遵循**对数曲线**（韦伯-费希纳定律）：

| 线性 PWM | 感知亮度 |
|----------|----------|
| 0        | 0%       |
| 64       | 约 6%    |
| 128      | 约 22%   |
| 192      | 约 52%   |
| 255      | 100%     |

**结论**：低亮度区间每步 +1 的变化，视觉上是大跳变；高亮度区间步进感知极不明显。

### 1.2 色温调节时的具体表现

以 RGBCW 从 3000K → 6500K 为例：

- R 通道：42 → 36（小幅下降，感知平滑）
- B 通道：0 → 17（从零开始增加，**每步 +1 都是感知大跳变**）
- C 通道：59 → 135（大幅增加，低段抖动明显）

**B、C 通道从低值起步时的线性步进，是色温过渡抖动的主要来源。**

---

## 二、解决思路

```
[CCT 参考表（线性）] → 线性空间插值 → 伽马校正 → PWM 驱动
        ↑                    ↑               ↑
   离线校准计算           保证色品准确    保证视觉平滑
```

**核心原则**：
- 色品插值在**线性空间**完成（保证混色准确落在黑体轨迹上）
- **输出端**施加伽马校正（保证人眼感知均匀，消除抖动）
- 两步分离，不可混淆

---

## 三、伽马校正原理

### 3.1 公式

```
PWM_out = 255 × (linear / 255)^(1/γ)
```

其中 γ = 2.2（sRGB 标准，适合大多数 LED 场景）。

### 3.2 逆运算（PWM → 线性，调试用）

```
linear = 255 × (PWM_out / 255)^γ
```

### 3.3 嵌入式做法：预计算查表（LUT）

不在 MCU 上实时调用 `pow()`，PC 端预先计算 256 个值，以数组形式烧录到 Flash。

**Python 生成代码（PC 端执行）**：

```python
import math

GAMMA = 2.2
lut = [round(255 * (i / 255) ** (1.0 / GAMMA)) for i in range(256)]

# 打印 C 数组
print("const uint8_t gamma_lut[256] = {")
for row in range(16):
    vals = ", ".join(f"{lut[row*16+col]:3d}" for col in range(16))
    print(f"    {vals},")
print("};")
```

**生成结果（γ=2.2）**：

```c
const uint8_t gamma_lut[256] = {
      0,   0,   0,   0,   0,   1,   1,   1,   1,   1,   1,   1,   1,   1,   2,   2,
      2,   2,   2,   2,   2,   3,   3,   3,   3,   3,   4,   4,   4,   4,   5,   5,
      5,   5,   6,   6,   6,   7,   7,   7,   8,   8,   8,   9,   9,   9,  10,  10,
     11,  11,  11,  12,  12,  13,  13,  14,  14,  15,  15,  16,  16,  17,  17,  18,
     18,  19,  20,  20,  21,  21,  22,  23,  23,  24,  25,  25,  26,  27,  27,  28,
     29,  30,  30,  31,  32,  33,  34,  34,  35,  36,  37,  38,  39,  40,  40,  41,
     42,  43,  44,  45,  46,  47,  48,  49,  50,  51,  52,  53,  54,  56,  57,  58,
     59,  60,  61,  62,  64,  65,  66,  67,  68,  70,  71,  72,  74,  75,  76,  78,
     79,  80,  82,  83,  85,  86,  87,  89,  90,  92,  93,  95,  96,  98, 100, 101,
    103, 104, 106, 108, 109, 111, 113, 115, 116, 118, 120, 122, 123, 125, 127, 129,
    131, 133, 135, 137, 138, 140, 142, 144, 146, 148, 150, 152, 155, 157, 159, 161,
    163, 165, 167, 170, 172, 174, 176, 179, 181, 183, 185, 188, 190, 192, 195, 197,
    200, 202, 204, 207, 209, 212, 214, 217, 219, 222, 224, 227, 229, 232, 235, 237,
    240, 243, 245, 248, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
};
```

---

## 四、CCT 参考表

由 PC 端校准算法输出，以**线性占空比**（非伽马校正）存储。MCU 用于插值计算，不直接输出。

```c
typedef struct {
    uint16_t cct;
    uint8_t  R, G, B, W, C;
} CctPoint;

/* TWBW-RGBCW-DENGDAI 校准参考表（线性值，2026-04-10 校准）
 * 校准实测点：2200 / 2700 / 3000 / 4000 / 5000 K
 * 5700K 以上为多项式外推
 */
const CctPoint CCT_TABLE[] = {
    { 1800,  85, 24,  0, 146,  0 },  // 超色域，最近可达值
    { 2200,  69, 36,  0, 147,  3 },  // 实测校准点
    { 2700,  42, 33,  0, 141, 39 },  // 实测校准点
    { 3000,  42, 33,  0, 121, 59 },  // 实测校准点
    { 4000,  44, 33,  2,  72,104 },  // 实测校准点
    { 5000,  45, 33,  6,  37,134 },  // 实测校准点
    { 5700,  40, 37, 12,  32,134 },  // 外推
    { 6500,  36, 38, 17,  29,135 },  // 外推
    { 7000,  34, 39, 20,  27,135 },  // 外推
    { 8000,  31, 40, 25,  24,135 },  // 外推
    { 9000,  29, 42, 29,  21,134 },  // 外推
    {10000,  28, 42, 32,  19,134 },  // 外推
    {12000,  26, 42, 37,  17,133 },  // 外推
};
#define CCT_TABLE_SIZE  13
```

---

## 五、完整嵌入式算法

### 5.1 全局状态

```c
#include <stdint.h>

// 当前输出 PWM（伽马校正后）
static uint8_t cur_R = 0, cur_G = 0, cur_B = 0, cur_W = 0, cur_C = 0;

// 当前色温（用于平滑过渡起点）
static uint16_t cur_cct = 3000;
```

### 5.2 核心函数：CCT → PWM

```c
/**
 * @brief 根据目标色温计算各通道伽马校正后的 PWM 值
 *
 * 流程：
 *   1. 在 CCT_TABLE 中找到相邻两个参考点 [lo, hi]
 *   2. 计算线性插值系数 t（定点 Q10，避免浮点）
 *   3. 对每个通道做线性插值（线性空间，保证色品准确）
 *   4. 通过 gamma_lut 转换为 PWM 输出（保证视觉平滑）
 *
 * @param target_cct  目标色温（K），范围 1800～12000
 * @param out_R/G/B/W/C  输出 PWM（0～255，已伽马校正）
 */
void cct_to_pwm(uint16_t target_cct,
                uint8_t *out_R, uint8_t *out_G, uint8_t *out_B,
                uint8_t *out_W, uint8_t *out_C)
{
    int lo = 0, hi = CCT_TABLE_SIZE - 1;

    // 夹住边界
    if (target_cct <= CCT_TABLE[0].cct) {
        lo = hi = 0;
    } else if (target_cct >= CCT_TABLE[CCT_TABLE_SIZE - 1].cct) {
        lo = hi = CCT_TABLE_SIZE - 1;
    } else {
        // 找相邻区间
        for (int i = 0; i < CCT_TABLE_SIZE - 1; i++) {
            if (target_cct >= CCT_TABLE[i].cct && target_cct <= CCT_TABLE[i + 1].cct) {
                lo = i;
                hi = i + 1;
                break;
            }
        }
    }

    // 插值系数 t：Q10 定点（0=lo 端，1024=hi 端）
    uint32_t t = 0;
    uint16_t span = CCT_TABLE[hi].cct - CCT_TABLE[lo].cct;
    if (span > 0) {
        t = (uint32_t)(target_cct - CCT_TABLE[lo].cct) * 1024u / span;
    }

    // 线性插值宏（Q10 精度）
    #define LERP(a, b) ((uint8_t)(((uint32_t)(a) * (1024u - t) + (uint32_t)(b) * t + 512u) / 1024u))

    uint8_t lin_R = LERP(CCT_TABLE[lo].R, CCT_TABLE[hi].R);
    uint8_t lin_G = LERP(CCT_TABLE[lo].G, CCT_TABLE[hi].G);
    uint8_t lin_B = LERP(CCT_TABLE[lo].B, CCT_TABLE[hi].B);
    uint8_t lin_W = LERP(CCT_TABLE[lo].W, CCT_TABLE[hi].W);
    uint8_t lin_C = LERP(CCT_TABLE[lo].C, CCT_TABLE[hi].C);

    #undef LERP

    // 伽马校正后输出
    *out_R = gamma_lut[lin_R];
    *out_G = gamma_lut[lin_G];
    *out_B = gamma_lut[lin_B];
    *out_W = gamma_lut[lin_W];
    *out_C = gamma_lut[lin_C];
}
```

### 5.3 平滑过渡函数

```c
/**
 * @brief 从当前色温平滑过渡到目标色温（消除切换抖动）
 *
 * 在 PWM（已伽马）空间做过渡，视觉步长均匀。
 *
 * @param target_cct  目标色温（K）
 * @param steps       过渡步数，推荐 50～200
 * @param delay_ms    每步延迟（ms），推荐 10～20
 */
void smooth_transition(uint16_t target_cct, int steps, int delay_ms)
{
    uint8_t tgt_R, tgt_G, tgt_B, tgt_W, tgt_C;
    cct_to_pwm(target_cct, &tgt_R, &tgt_G, &tgt_B, &tgt_W, &tgt_C);

    for (int i = 1; i <= steps; i++) {
        // 在 PWM 空间线性插值（视觉等步长）
        uint8_t pwm_R = (uint8_t)(cur_R + (int)(tgt_R - cur_R) * i / steps);
        uint8_t pwm_G = (uint8_t)(cur_G + (int)(tgt_G - cur_G) * i / steps);
        uint8_t pwm_B = (uint8_t)(cur_B + (int)(tgt_B - cur_B) * i / steps);
        uint8_t pwm_W = (uint8_t)(cur_W + (int)(tgt_W - cur_W) * i / steps);
        uint8_t pwm_C = (uint8_t)(cur_C + (int)(tgt_C - cur_C) * i / steps);

        set_pwm(pwm_R, pwm_G, pwm_B, pwm_W, pwm_C);  // ← 调用平台 PWM 驱动
        delay(delay_ms);
    }

    // 更新状态
    cur_R = tgt_R; cur_G = tgt_G; cur_B = tgt_B;
    cur_W = tgt_W; cur_C = tgt_C;
    cur_cct = target_cct;
}
```

### 5.4 初始化

```c
/**
 * @brief 上电初始化，直接跳到默认色温（无过渡）
 */
void led_init(void)
{
    cur_cct = 3000;
    cct_to_pwm(cur_cct, &cur_R, &cur_G, &cur_B, &cur_W, &cur_C);
    set_pwm(cur_R, cur_G, cur_B, cur_W, cur_C);
}
```

### 5.5 对外接口（调用示例）

```c
// 上电
led_init();

// 用户调节色温到 6500K，100步平滑过渡，每步15ms（总耗时约1.5秒）
smooth_transition(6500, 100, 15);

// 快速切换（50步，10ms，约0.5秒）
smooth_transition(2700, 50, 10);

// 直接跳变（不平滑，用于测试）
cct_to_pwm(4000, &cur_R, &cur_G, &cur_B, &cur_W, &cur_C);
set_pwm(cur_R, cur_G, cur_B, cur_W, cur_C);
```

---

## 六、参数调优指南

| 参数 | 推荐值 | 效果 |
|------|--------|------|
| γ (GAMMA) | 2.2 | 通用，适合大多数 LED |
| γ = 2.0 | 略亮 | 若高色温偏暗可尝试 |
| γ = 2.5 | 略暗 | 若低色温过亮可尝试 |
| steps | 100 | 1.5秒完成，平滑自然 |
| steps | 50  | 0.5秒完成，较快 |
| delay_ms | 15 | 约 67fps，人眼极限 |
| delay_ms | 10 | 约 100fps，更流畅 |

**调优顺序**：先确认 γ=2.2 效果，再微调 steps 和 delay_ms 匹配产品手感。

---

## 七、分工说明

| 角色 | 任务 |
|------|------|
| **算法工程师（PC）** | 运行校准脚本生成 `CCT_TABLE[]`；生成 `gamma_lut[]`；更新后提供给嵌入式团队 |
| **嵌入式工程师（MCU）** | 将两个数组烧录 Flash；实现 `set_pwm()` 驱动接口；集成 `cct_to_pwm()` 和 `smooth_transition()`；调优 steps/delay 参数 |

---

## 八、注意事项

1. **CCT_TABLE 存 Flash，不要放 RAM**——13×6 = 78 字节，`const` 修饰即可
2. **gamma_lut 同样存 Flash**——256 字节
3. **5700K 以上为外推值**，若实测与预期偏差大，补充实测数据后重新校准
4. **1800K 超出色域**，B 通道为 0，是正常现象，非 bug
5. **set_pwm() 需原子操作**——避免各通道更新不同步导致闪烁（用 DMA 或同步更新寄存器）
