这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【DSPIC33CURIOSITYPLATFMDEVBRD】2-电位器调光台灯实

共1条 1/1 1 跳转至页

【DSPIC33CURIOSITYPLATFMDEVBRD】2-电位器调光台灯实战

菜鸟
2026-10-06 20:34:41     打赏

一、需求不复杂,先把路摸清楚

我申请时候的计划中写了:拧板上的旋钮,RGB LED 的白光亮度跟着平滑变化——台灯的最简形态。

动手前先对着原理图把通路捋了一遍:

环节连接说明

输入电位器 → RA7/AN612 位 ADC(ADC1 通道 0)
处理dsPIC33AK128MC106EMA 滤波 + 占空比映射
输出RC2/RD0/RD2 → RGB LED三路 SCCP PWM,周期 65535

【配图 1】原理图相关部分截图:RGB LED 与电位器接线(DS70005562 附录 A)

2_1_schematic.jpg

软件这边,我没有从零配外设,而是拿官方 OOB 演示的 MCC 配置改的(时钟、ADC、PWM、引脚初始化都是 MCC 生成的,靠谱),应用层自己写。核心逻辑就一个循环:

uint16_t sample = readPotentiometer();          // ADC 采样(软件触发 + 轮询)
filtered = emaFilter(sample);                   // EMA 指数滑动平均,Q8 定点
uint32_t duty = mapToDuty(filtered);            // 12 位 → 16 位占空比
SCCP1_PWM_DutyCycleSet(duty);                   // 红
SCCP2_PWM_DutyCycleSet(duty);                   // 绿
SCCP3_PWM_DutyCycleSet(duty);                   // 蓝

思路是不是很顺?烧进去一看——翻车了。

二、Bug 现场:「能调,但基本看不出变化」

第一版烧上去,旋钮从头拧到尾,亮度就轻轻变了一点。不对劲啊,ADC 是 12 位、PWM 是 16 位,动态范围绰绰有余,怎么会这样?

这种时候,继续盯着代码瞎猜是最低效的路子。我决定先让程序自己开口说话:加串口输出(板子上自带 MCP2221A USB 转串口,9600 8-N-1,Mac 上 stty 设好波特率直接 cat 设备文件就能看到),每轮把 adc / filtered / duty 三个值打出来。

【配图 2】串口输出「案发现场」:adc 只有个位数,filtered 却反复爆到 1677 万

2_2_serial_crime.png

数据一出来,案子就破了。这是旋钮拧在最小位附近的实录——adc 只有个位数的微小抖动,filtered 却反复炸到 1677 万:

adc=11 filtered=16777205 duty=1048400 ← filtered 爆到 1677 万,duty 越界 16 倍!
adc=7  filtered=16777081 duty=1046416
adc=8  filtered=2        duty=32      ← 偶尔"正常"一行
adc=5  filtered=16746964 duty=564544
adc=9  filtered=16552555 duty=588656

看见没,adc 明明稳在个位数,filtered 却时不时窜到 1600 多万,再慢慢爬回来——滤波器自己在抽风。哪怕只有 ±2 的向下抖动,都够触发一次爆炸。

三、根因:一个无符号减法,把滤波器送上了天

问题就出在这一行(Q8 定点 EMA,y += (x - y) / 32):

// 错误版本:uint32_t 运算
filtered = filtered + ((((uint32_t)sample << 8) - filtered) >> 5);

坑在哪?无符号减法下溢。当你把旋钮往低处转,目标值 sample << 8 会小于当前的 filtered,这时候 (sample << 8) - filtered 在 uint32_t 的世界里不是个负数——而是下溢成一个接近 2³² 的巨型正数。右移 5 位依然巨大,加回去,filtered 当场起飞。然后滤波值慢慢"爬"回正常区间,直到下一次下溢,再炸。数据里那种"爆炸—回落—再爆炸"的振荡形态,就是这么来的。

典型的"这行代码看着挺对"系列。C 语言里无符号运算的坑,教科书都写烂了,真栽进去的时候还是调了半天。

数据里还顺带暴露了另一个小问题:adc 偶尔会读到 65535(0xFFFF)。轮询读 ADC 时偶尔会采到无效值,单看不算大事(滤波器真正的元凶是下溢),但也顺手治了——超量程的采样直接丢掉,沿用上一次有效值。

修复就两行的事:

// 正确版本:有符号差值
int32_t delta = ((int32_t)sample << 8) - (int32_t)filtered;
filtered = (uint32_t)((int32_t)filtered + (delta >> EMA_SHIFT));

四、修好之后,再抠两个细节

修完再烧,旋钮全程的亮度变化已经相当顺滑了。不过拧到最左的时候,LED 还剩一点微光,不灭。翻底板原理图找到原因:电位器两端各串了一个 20Ω 的保护电阻,ADC 最低只能读到 5 上下(到不了真正的 0),映射出来占空比约 0.12%——LED 就是 faintly 亮着。

台灯嘛,拧到底就该灭,这才符合直觉。于是加了个死区:滤波值低于 40(大约 1% 量程)直接输出占空比 0。这个数也不能太大,不然"刚拧一点灯就该亮"的响应就没了。

采样和映射的完整代码贴出来:

#define ADC_MAX       4095u   /* 12 位 ADC 满量程 */
#define ADC_DEAD_ZONE 40u     /* 死区:低于此值视为 0,让 LED 彻底熄灭 */

static uint16_t lastValidSample = 0;  /* 上一次有效采样 */

/* ADC 采样:软件触发 + 轮询等待,顺手丢掉偶发的 0xFFFF 无效值 */
static uint16_t readPotentiometer(void)
{
    uint32_t raw;
    ADC1.SoftwareTriggerEnable();
    while (!ADC1.IsConversionComplete(ADC1_Channel0)) { }
    raw = ADC1.ConversionResultGet(ADC1_Channel0);
    if (raw > ADC_MAX) {
        return lastValidSample;   /* 毛刺:沿用上一次有效值 */
    }
    lastValidSample = (uint16_t)raw;
    return lastValidSample;
}

/* 死区 + 线性映射:12 位 ADC → 16 位 PWM 占空比 */
static uint32_t mapToDuty(uint32_t level)
{
    if (level < ADC_DEAD_ZONE) {
        return 0u;                /* 死区内彻底熄灭 */
    }
    return (level * (PWM_PERIOD + 1u)) / (ADC_MAX + 1u);
}

两个调参的参考值,省得你再摸一遍:

  • EMA_SHIFT = 5(也就是 1/32):消抖效果和响应速度的平衡点。太小,灯跟着旋钮抖;太大,灯"追不上手"

  • ADC_DEAD_ZONE = 40:底板保护电阻决定的下限之上,留点余量

最后把这次调试的功臣也亮个相——串口工具函数。没有 printf 重定向,就直接拿寄存器级的 UART 驱动手搓了一套(9600 8-N-1 对着板载 MCP2221A):

static void uartPutc(char c) { UART1_Write((uint8_t)c); }

static void uartPutU32(uint32_t v)     /* 无 printf 环境下的整数打印 */
{
    char buf[10]; uint8_t i = 0;
    if (v == 0) { uartPutc('0'); return; }
    while (v > 0 && i < sizeof(buf)) { buf[i++] = (char)('0' + (v % 10u)); v /= 10u; }
    while (i > 0) { uartPutc(buf[--i]); }
}

/* 用法:每 N 轮主循环打印一次,别把 9600 波特率打爆了 */
uartPuts("adc=");  uartPutU32(sample);
uartPuts(" filtered="); uartPutU32(filtered >> 8);
uartPuts(" duty="); uartPutU32(duty);
uartPuts("\r\n");

修复 + 死区之后,再来一次旋钮全程扫频,数据干干净净:

【配图 3】修复后的串口数据:旋钮 min→max→min 全程,filtered 平滑跟随

2_3_serial_fixed.png

【配图 4】调光效果三连拍:最左(灭)→ 中间 → 最右(最亮)

五、这次调试的心得

回头看,这次排障最值得记下来的不是 bug 本身,而是路径:没有串口打印之前,看哪儿都像有问题;有了数据之后,几分钟就锁定了真凶。 嵌入式调试里,"让程序告诉你它看到的世界"永远比"盯着代码硬想"高效。一个 9600 的串口 + stty + cat,就是 Mac 上零成本的调试终端。





关键词: DSPIC33CURIOSITYPLATFMDEV    

共1条 1/1 1 跳转至页

回复

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