这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【DSPIC33CURIOSITYPLATFMDEVBRD】3-给台灯加灵魂:按

共1条 1/1 1 跳转至页

【DSPIC33CURIOSITYPLATFMDEVBRD】3-给台灯加灵魂:按键消抖的两次迭代与HSV彩虹算法

菜鸟
2026-10-06 20:47:45     打赏

一、新需求:从「调光灯」到「氛围灯」

白光调光只是台灯,能变彩才是氛围灯。这次加的功能:按一下 S1,在两种模式之间切换——

  • 白光模式:拧旋钮调亮度(第 2 篇的全部成果原样保留)

  • 彩色模式:灯自己哗哗地变颜色(HSV 彩虹),旋钮改管变色速度

硬件上新登场两个小角色:S1 按键(RB5,低电平有效,板载有上拉)和绿色 LED0(RC3),后者当模式指示灯用。

【配图 1】板载按键与 RGB LED 位置图(S1/S2/S3 + LED0 + RGB LED)

3_1_buttons_led.jpg

二、消抖第一版:一个教科书级的反面教材

按键消抖这东西,写单片机的谁没写过?我也随手写了一个,然后就被教做人了。现象非常魔幻:

按住 S1 不动是白光,一松手反而变彩灯。

理论上应该是「按一下切一次」,结果变成了「模式跟着按键状态走」——这说明一次按下触发了偶数次切换。看看第一版代码是怎么翻车的:

// 错误版本:释放沿没有消抖
if (raw == lastRaw) { if (stableCount < N) stableCount++; }
else { stableCount = 0; }
lastRaw = raw;

if (stableCount >= N && !pressed) { pressed = true; return true; }  // 按下沿
if (stableCount >= N) { pressed = true; }
else if (stableCount == 0) { pressed = false; }   // ← 释放瞬间立刻复位,抖动穿透!

问题在哪?机械按键的抖动,按下和释放的时候都会有。 这版代码对按下沿倒是做了 N 次确认,但对释放沿却是「电平一变立刻复位」——于是,只要你还按着,触点随便抖一下(stableCount 瞬间清零再涨回来),状态机就以为你完成了一次「按下→释放→再按下」,在你手指头纹丝不动的时候,偷偷多切了一次模式。一次按下到底触发奇数次还是偶数次切换,纯属看缘分。用户看到的「按住变白、松手变彩」,就是几次多余切换叠加出来的迷惑表象。

三、第二版:换个朴素但靠谱的思路

正确的姿势其实更"笨":消抖后的稳定状态,必须连续 N 次采样一致才允许翻转;事件只在稳定状态从「释放」变「按下」的那一瞬间产生一次:

static bool buttonS1JustPressed(void)
{
    static uint8_t changeCount = 0;
    static bool stableState = false;      // 消抖后的稳定状态

    bool raw = (PORTBbits.RB5 == 0);      // 低有效
    bool event = false;

    if (raw != stableState) {
        if (++changeCount >= BUTTON_STABLE) {   // 连续 N 次一致才翻转
            stableState = raw;
            changeCount = 0;
            event = raw;                        // 仅翻转瞬间可能为 true
        }
    } else {
        changeCount = 0;
    }
    return event;
}

这个写法的精髓在于:抖动只会让 changeCount 反复清零,永远够不到翻转的门槛;真正的按下和释放都是几十毫秒级的稳定电平,过线轻轻松松。而且一旦翻转,stableState == raw,后面的抖动全是白搭——按一次,恰好切一次,童叟无欺。

【配图 2】(可选)消抖时序示意图:原始电平抖动 vs 稳定状态翻转时机建议:手绘或波形图,标注「抖动被 changeCount 清零吸收」


3_2_debounce_timing.png


哦对了,还有个坑得提一句:MCC 默认把 RB5 配成了模拟输入(ANSELB5=1),你要是不改,读 PORTB 永远是 0,看起来就像「按键一直被按着」,一开局方向就带偏了。改数字口、顺便把 LED0 配成输出:

/* 按键 RB5 在 MCC 里被配成模拟输入,需改回数字口才能读电平 */
ANSELBbits.ANSELB5 = 0;
/* LED0 (RC3) 作模式指示灯,配为输出 */
TRISCbits.TRISC3 = 0;
LATCbits.LATC3 = 0;

四、HSV 彩虹:不要浮点,不要查表

彩色模式的核心是 HSV→RGB 转换。色相 h 用 8 位(0~255 对应 0°~360°),饱和度、亮度都拉满,整个算法纯整数——几十条指令搞定:

static void hsvToRgbFull(uint8_t h, uint8_t *r, uint8_t *g, uint8_t *b)
{
    const uint8_t region = h / 43u;                  // 6 个色相区间
    const uint8_t rem = (uint8_t)((h - region * 43u) * 6u);     // 区间内插值 0..252

    switch (region) {
        case 0: *r = 255; *g = rem;       *b = 0;       break;  // 红→黄
        case 1: *r = 255 - rem; *g = 255; *b = 0;       break;  // 黄→绿
        case 2: *r = 0;   *g = 255; *b = rem;       break;  // 绿→青
        case 3: *r = 0;   *g = 255 - rem; *b = 255; break;  // 青→蓝
        case 4: *r = rem; *g = 0;   *b = 255; break;  // 蓝→紫
        default: *r = 255; *g = 0;   *b = 255 - rem; break;  // 紫→红
    }
}

主循环里把两条数据通路合起来——采样和滤波是共用的,模式只决定「映射函数」长什么样:

uint32_t level = filtered >> 8;   /* 滤波后的电位器值 0..4095 */

if (level < ADC_DEAD_ZONE) {
    dutyR = dutyG = dutyB = 0;                    /* 两种模式下都能拧灭 */
} else if (mode == MODE_WHITE) {
    uint32_t duty = mapToDuty(level);             /* 白光:旋钮调亮度 */
    dutyR = dutyG = dutyB = duty;
} else {
    /* 彩色:电位器控制流转速度 0..7 档,色相按节拍自转 */
    if (++hueTick >= COLOR_HUE_DIVIDER) {
        hueTick = 0;
        hue = (uint8_t)(hue + (uint8_t)(level >> 9u));
    }
    uint8_t r, g, b;
    hsvToRgbFull(hue, &r, &g, &b);
    dutyR = (uint32_t)r * 257u;                   /* 8 位颜色 → 16 位占空比 */
    dutyG = (uint32_t)g * 257u;
    dutyB = (uint32_t)b * 257u;
}

SCCP1_PWM_DutyCycleSet(dutyR);   /* 红 */
SCCP2_PWM_DutyCycleSet(dutyG);   /* 绿 */
SCCP3_PWM_DutyCycleSet(dutyB);   /* 蓝 */

/* 按键任务按节拍调度,消抖逻辑见上一节 */
if (++tick >= BUTTON_DIVIDER) {
    tick = 0;
    if (buttonS1JustPressed()) {
        mode = (mode == MODE_WHITE) ? MODE_COLOR : MODE_WHITE;
        LATCbits.LATC3 = (mode == MODE_COLOR) ? 1u : 0u;  /* LED0 指示 */
    }
}

彩色模式的手感:旋钮拧到最左,彩虹几乎冻住不动;越往右流转越快,拧到底大约 0.3 秒转完一圈。8 位的颜色值乘 257 就扩展到 16 位,直接喂给 PWM 占空比寄存器,一路顺畅。

【配图 3】彩色模式实拍:


五、收个尾

至此「电位器调光台灯」的全部功能收工:

功能实现要点

白光调光12 位 ADC → EMA(Q8 定点) → 16 位 PWM,死区保证彻底熄灭
模式切换S1 标准消抖状态机,按一次切一次,LED0 指示
彩色渐变8 位 HSV 整数算法,旋钮控制流转速度 0~7 档

回头看这个小项目,有效代码也就 200 行出头,但折腾出三个货真价实的调试故事——烧录后程序不跑、滤波器下溢失控、按键消抖漏沿。每一个都是嵌入式开发的老生常谈,但真自己撞上一次,比看十篇教程都记得牢。说到底,嵌入式的难度从来不在算法,而在对硬件细节的敬畏:保护电阻会让 ADC 到不了 0,模拟复用的引脚不读数字电平,机械触点的抖动画风比你想象的狂野。

这个项目也顺便验证了 Mac 当嵌入式开发机完全没问题(第 1 篇)。等哪天换上 PIC32A 模块玩 32 位内核,这套流程和大部分代码能直接搬过去——DIM 模块化设计的好处,到这时候就体现出来了。

【配图 4】最终效果:白光最亮态 vs 彩色模式某瞬间(S1控制)





关键词: DSPIC33CURIOSITYPLATFMDEV    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]