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

共1条 1/1 1 跳转至页

【DSPIC33CURIOSITYPLATFMDEVBRD】4-给台灯装上手机遥控器

菜鸟
2026-10-07 12:24:04     打赏

一、需求:台灯配不上手机,像话吗

前三篇做完,这台灯已经有模有样了:旋钮调亮度、按键切模式、彩虹自动流转。但有一个说不出口的尴尬——想换个颜色,得拧旋钮慢慢找。都什么年代了,调色这事就该在手机上戳两下。

所以 M3 的需求清单:

  1. 网页中间一个圆形调色盘,点哪是哪

  2. 下面一排常用颜色,一键直达

  3. 点色盘或颜色,板子上的 RGB LED 实时同步

  4. 一根亮度滑动条

  5. 通信方式我说了算——串口就挺好:板子上自带 MCP2221A USB 转串口,Mac 上零成本,9600 波特率点一下才传 10 个字节,延迟 10 毫秒级,完全跟手

【配图 1】最终效果三连:手机选红色/琥珀/青,板子 RGB 即时同步(亮度 56%)

二、总体架构:四层链路,各管一段

┌──────────┐   HTTP POST    ┌──────────────┐   串口 9600    ┌────────────┐
│ 手机/电脑 │ ─────────────→ │ Mac 本地服务器 │ ─────────────→ │ dsPIC33A   │
│ H5 调色台 │   /set?r=&g=&b= │ Python 标准库 │  "C r g b\n"  │ UART 解析  │
└──────────┘                └──────────────┘               │ → PWM 驱动 │
                                                            └────────────┘

【配图 2】系统架构图:Mac 侧两跳(网页→服务器),板子侧两跳(UART 解析→PWM 渲染)

4_2_architecture.png

分层的好处是每一层都能单独调试:网页写错了用浏览器控制台看;服务器转发错了用 curl 打;串口通不通用 cat 设备文件看输出;固件解析错了用任意串口工具手动发一条命令——这就是选文本协议的原因,下面细说。

三、通信协议:土办法有时是最好的办法

协议就一条指令:

C 128 0 255\n
│  │  │   └─ 蓝 0~255
│  │  └───── 绿 0~255
│  └──────── 红 0~255
└─────────── 命令字:Color

选它的理由,事后看都成立:

  • 人能读、人能敲。调试的时候打开任何串口工具,手打 C 255 0 0 回车,灯就红——不需要写专用上位机。第 2 篇的教训还在:让程序自己告诉你它看到的世界,文本协议就是这个哲学的延伸

  • 固件解析零依赖。不写 scanf(嵌入式里又重又危险),十几行手动解析:逐字符转数字、空格分隔、超 255 截断、攒到 \n 才算一条。完整代码:

/* 解析 "C r g b"(r/g/b 十进制 0~255),成功返回 true */
static bool parseColorCommand(const char *line, uint8_t *r, uint8_t *g, uint8_t *b)
{
    if (line[0] != 'C' || line[1] != ' ') {
        return false;
    }
    const char *p = line + 2;
    uint32_t vals[3] = {0, 0, 0};
    for (uint8_t i = 0; i < 3; i++) {
        uint32_t v = 0;
        bool hasDigit = false;
        while (*p == ' ') { p++; }
        while (*p >= '0' && *p <= '9') {
            v = v * 10u + (uint32_t)(*p - '0');
            if (v > 255u) { v = 255u; }   /* 防御:越界截断 */
            hasDigit = true;
            p++;
        }
        if (!hasDigit) { return false; }
        vals[i] = v;
    }
    *r = (uint8_t)vals[0]; *g = (uint8_t)vals[1]; *b = (uint8_t)vals[2];
    return true;
}

接收侧用一个行缓冲状态机:中断太麻烦,直接主循环里轮询 UART1_IsRxReady(),攒够一行处理一次,顺手做了超长丢弃防垃圾数据占满缓冲区。

四、固件:加一个"远程模式"

模式机原来有白光/彩色两档,现在加第三档 REMOTE:收到任何合法颜色命令就切进去,颜色完全由串口说了算;按一下 S1 切回白光本地模式。状态指示也顺便升级:

  • LED0 亮 = 彩色模式(原逻辑)

  • LED1 亮 = 远程模式(新增,一眼看出灯被谁控制)

这个设计的用意是控制权永远有明确的归属:本地旋钮不会和网页抢——你在网页上点了颜色,旋钮拧到天上去灯也不理你,直到你按 S1 把控制权拿回来。两种控制源互不打架,这比"谁最后一个动谁赢"的模糊逻辑好调试得多。

顺带一提,这版固件上电会往串口发一句 LAMP READY——一是给串口工具一个"连上了"的提示,二是顺手验证了 TX 通路(MCP2221A 是双向的,别浪费)。

固件侧的接收任务,就是第 3 篇那个"行缓冲状态机"的 UART 版——轮询、攒行、回车结算:

/* UART 接收任务:攒行,收到完整颜色命令返回 true */
static bool uartTask(uint8_t *r, uint8_t *g, uint8_t *b)
{
    static char line[UART_LINE_MAX];
    static uint8_t len = 0;

    while (UART1_IsRxReady()) {
        char c = (char)UART1_Read();
        if (c == '\n' || c == '\r') {
            if (len > 0) {
                line[len] = '\0';
                len = 0;
                return parseColorCommand(line, r, g, b);
            }
        } else if (len < UART_LINE_MAX - 1) {
            line[len++] = c;
        } else {
            len = 0;   /* 超长丢弃,防垃圾数据占满缓冲区 */
        }
    }
    return false;
}

主循环里的集成也就三行——收到命令进远程模式,S1 切回本地,控制权永远单一:

if (uartTask(&remoteR, &remoteG, &remoteB)) {   /* 收到串口颜色命令 */
    mode = MODE_REMOTE;                          /* 网页接管 */
    LATCbits.LATC4 = 1;                          /* LED1 = 远程模式指示 */
}
/* S1 按下:任何模式 -> 白光(旋钮夺回控制权) */

五、网页:一个文件写完全部前端

整个前端就一个 index.html,原生 Canvas + Pointer Events,没引任何库——台灯控制台要什么框架。

色盘绘制是核心,HSV 色轮的本质就一句话:角度决定色相,半径决定饱和度:

var hue = (Math.atan2(dy, dx) * 180 / Math.PI + 360) % 360;
var sat = Math.min(1, dist / R);

逐像素填进 ImageData:圆心白(饱和度 0),外沿艳(饱和度 1),绕一圈 360° 彩虹。交互用 Pointer Events 而不是分别写 touch/mouse——一套代码通吃鼠标和触屏,手机拖动选色跟手顺滑。

色轮的交互核心:Pointer Events 一套代码通吃鼠标和触屏,sendThrottled 把拖动限流在 25 次/秒(9600 波特打得起,灯也跟手):

function pickColor(clientX, clientY) {
    var rect = canvas.getBoundingClientRect();
    var x = clientX - rect.left - rect.width / 2;
    var y = clientY - rect.top - rect.height / 2;
    var dist = Math.sqrt(x * x + y * y);
    if (dist > rect.width / 2) return;        // 点在圆外忽略
    var hue = (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
    var sat = Math.min(1, dist / (rect.width / 2));   // 角度=色相,半径=饱和度
    var rgb = hsvToRgb(hue, sat, 1);
    state.r = rgb[0]; state.g = rgb[1]; state.b = rgb[2];
    sendThrottled();                           // 40ms 节流后发 POST /set
}
canvas.addEventListener('pointerdown', e => { dragging = true; pickColor(e.clientX, e.clientY); });
canvas.addEventListener('pointermove', e => { if (dragging) pickColor(e.clientX, e.clientY); });

亮度条的实现更偷懒但有效:网页端把亮度和颜色相乘之后再下发。板子永远只收到最终 RGB,不需要知道"亮度"这个概念——状态越简单,出错的地方越少。

服务器是 50 行 Python,全是标准库:HTTP 服务绑 0.0.0.0(手机才能访问),收到 POST /set?r=&g=&b= 就拼一条 C r g b\n 写串口。串口配置直接用 termios 模块设 9600 8-N-1,连 stty 都不用敲。

服务器核心就两个方法:收到 POST 就拼串口命令,串口配置直接用 termios(连 stty 都省了):

def send_color(r: int, g: int, b: int) -> bool:
    cmd = f"C {r} {g} {b}\n".encode("ascii")
    with _serial_lock:
        return os.write(_serial_fd, cmd) > 0

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):                       # 网页调色台打过来的请求
        q = parse_qs(urlparse(self.path).query)
        r = max(0, min(255, int(q["r"][0])))   # 越界钳位,不信任任何输入
        g = max(0, min(255, int(q["g"][0])))
        b = max(0, min(255, int(q["b"][0])))
        ok = send_color(r, g, b)
        self._respond(200 if ok else 503, b"ok")

六、踩坑实录:色盘为什么缺了个角

功能做完,手机上一看——色盘渲染成了个"缺角泪滴",下面还拖着一大圈残影:

【配图 3】Bug 现象实拍:色盘缺角成"泪滴"

4_3_wheel_bug.png

原因是个经典的 Canvas 高分屏陷阱。为了 Retina 屏上的清晰度,我把画布 backing store 设成了 CSS 尺寸的 2 倍,打算用坐标变换放大绘制。但 putImageData() 不吃 setTransform 那一套——它永远按原始设备像素写入。结果就是:我算的像素数据只填满了缓冲的四分之一,显示出来缺角又带残影。

修复就一条路:从头到尾都在设备像素坐标系里干活,放弃变换矩阵:

canvas.width = Math.round(css * dpr);   // backing store = 设备像素
// 逐像素计算时,R、dx、dy 全部用 canvas.width 这套设备像素
ctx.putImageData(img, 0, 0);            // 不再依赖任何变换

教训通用:用了 putImageData,变换矩阵就是摆设。要么全程设备像素(性能还好),要么改用 drawImage 走变换管线。

【配图 4】修复后的手机网页截图:完整圆形色盘 + 亮度条 + 常用颜色

4_4_fixed_wheel.jpg

七、效果与小结

现在这台灯有三重身份:

控制方式适用场景
旋钮 + 按键本地随手调,断电也能用
彩虹自动流转氛围模式,当背景灯
手机网页调色台要精确颜色的时候,躺床上调

回头看,这一篇的技术密度其实不高——没有寄存器、没有滤波算法,难点全在"把四层链路打通":每一层单独都简单,串起来不出错靠的是文本协议带来的可观测性。做嵌入式网关类项目时,"人能读懂的协议 + 每层可单独调试"永远是最省时间的路线。





关键词: DSPIC33CURIOSITYPLATFMDEV    

共1条 1/1 1 跳转至页

回复

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