T5口袋机摇杆怎样越过死区而不乱跳

2026-10-304
AI 快速预览详细 收起
本文介绍了如何通过优化T5口袋机摇杆的模拟输入代码,解决菜单操作中乱跳的问题。文章详细讲解了中位校准、径向死区、方向迟滞与连发控制等关键技术,并提供了完整的C模块代码。通过这些优化,可以实现更稳定的菜单操作,适用于STEM教育和编程启蒙。

拇指轻轻向右推,菜单应该移动一格;停在右上方,选中项应该稳稳地留在一个方向。若指尖一松,菜单又“溜”了一格,问题可能出在模拟输入转成离散事件的那几行代码里。

T5 口袋机恰好适合拆开这件事。摇杆有两路模拟量,界面只需要上下左右四种动作。中间缺的不是一条大小比较,而是一套有记忆的输入规则。下面实现一个独立 C11 模块,把中位校准、径向死区、方向迟滞与菜单连发放在同一条链路上。

主板、摇杆与电池的装配关系

图1 项目原装配状态。摇杆已经与主板连接,校准应在装配稳定、手指松开时进行;这张照片不代表本文算法已经上板实测。

先把两路输入认对

这次核对的是 T5-Pocket-MB 的 Schematic1_1 / P1,图纸版本 V1.0,更新日期 2025-11-18。软件则固定在文末列出的 TuyaOpen 提交版本,应用配置为 TUYA_T5AI_POCKET。

EDA中重新放大的JOYCON接口

图2 同版 EDA 实际画布局部。CN2 引脚1为供电、2为 ADC_U/D、3为 BTN、4为地、5为 ADC_L/R;C102、C103各为100 nF,分别接在两路模拟输入与地之间。

供电选择也不能凭印象改。图中 R99 为0 Ω,R100标为 NC/0 Ω,分别连接 VDD_3V3 与 VDD_1V1。这是装配选项,不是让两条电源同时短接。换摇杆或改供电前,需要核对实际装配与 ADC 允许输入范围;本文保持硬件不变,只处理已有驱动返回的数据。

主板PCB顶面

图3 同版主板的 EDA 2D 顶面预览,JoyCon 接口位于左侧。图片用于定位接口与布板关系,不是装配后的实物照。

主板PCB底面

图4 同一主板的 EDA 2D 底面预览,观察方向已切换。查接口时先分清顶底面,不能照着屏幕上的左右位置猜 FPC 脚序。

板级代码配置的是 ADC0,X通道15、Y通道14,摇杆按压为GPIO9。tdl_joystick_get_raw_xy()按这两个通道顺序返回原始值;任一路读取失败,整次返回错误。

有一个容易踩的细节:上游 tdl_joystick_calibrated_xy()会先交换两轴,再反相;后续方向判定中,负X表示右、负Y表示上。因此本文定义“正X向右、正Y向上”时,适配层使用 x=raw_y,y=raw_x。这个映射来自所核对的源码,安装后仍要逐向确认,不应把函数参数名直接当物理朝向。

ADC配置中的采样位宽是12,但应用又有自己的量程参数。仅凭“12位”便把 raw API 的中点写死成2048,可能把底层返回单位与ADC码混为一谈。下面始终用同一个 raw API 的中心与跨度做归一化,不依赖它必须返回0~4095。

中位不是量程除以二

松手后分别采一段稳定数据,例如每10 ms取一次、连续64次,得到两轴中心 cx、cy。这64次只是建议的标定窗口,本文没有把它冒充硬件测量结果。之后慢慢推向四个端点,记录每轴的最小值与最大值。

简单对称标定可取 span=min(max-center, center-min),两侧差距大时改用正负两段跨度;本模块采用每轴一个正跨度,并把超范围结果限幅到±1000。若跨度非正,禁止输出。不要在用户正推杆时自动重设中心,否则“向右保持”会逐渐变成新的中位。

归一化计算为 n=(raw-center)*1000/span。减法和乘法先提升到64位,避免极端整数输入溢出。X、Y分别校准,才不会让两轴机械行程差异直接变成判向偏差。

接下来处理两种不同的“犹豫”。第一种发生在中位附近,第二种发生在两个方向之间。

半径管松手,角度管转弯

径向死区看的是 x*x+y*y,而不是两轴各自是否越线。未持有方向时,半径达到250才进入;已经持有方向后,回到180以下才释放。180~250之间保留此前状态,这段间隔就是迟滞。

例如 (180,180)的半径约255,已经越过进入线;若分别要求X或Y达到250,它却会被当成松手。圆形判定能让斜向与正向的触发距离更一致。

第二道迟滞只决定保留哪根轴。已经向右时,Y的绝对值必须超过X的1.25倍,才转成上下;已经向上时,X必须超过Y的1.25倍,才转成左右。同一象限中,切换角约为51.34°与38.66°,而不是都挤在45°。

C模块角度往返输出

图5 直接调用本文C模块生成的合成输入结果。角度逐步增大与减小时,方向切换位置不同;色带内输出取决于进入前的方向。这不是摇杆采样波形。

迟滞也有代价:方向切换会晚一点。1.25是本例的设计值,需要结合菜单密度与手感调整;它适合四向菜单,不适合原样拿去做八向角色移动或连续速度控制。

连发应该数事件,不要补课

新方向先输出一次。保持300 ms后开始连发,后续每100 ms最多输出一次。如果线程卡顿到5000 ms才再次运行,这次只发一个事件,并把计时基准放到当前时刻;不把漏掉的几十格一口气补给菜单。

半径迟滞与连发测试

图6 同一C模块的合成输入验证。上图空心圈表示仍持有方向,不代表每次都发事件;下图展示每次调用的实际返回值,5000 ms故意调用两次,第二次不再输出。5010 ms读取失败会释放,5020 ms恢复后视为重新进入。

读取失败时也不能沿用上次有效摇杆值。模块立即释放内部方向,避免失联后持续翻页。uint32_t毫秒时间以无符号减法求经过时长,支持计数器回绕;调用间隔仍应远短于一次完整回绕周期。

下面是完整的 joy_filter.h。Joy由单个输入线程持有,调用者保证指针有效;非零返回值才是一个新的菜单动作,不能把内部的 dir状态在每次轮询时都再发一遍。

/* SPDX-License-Identifier: MIT */
#ifndef JOY_FILTER_H
#define JOY_FILTER_H
#include <stdint.h>
#include <stdbool.h>

enum { J_NONE, J_RIGHT, J_LEFT, J_UP, J_DOWN };
typedef struct {
    int cx, cy, sx, sy, dir;
    uint32_t since, last;
    bool repeating, ready;
} Joy;

static inline bool joy_init(Joy *j, int cx, int cy, int sx, int sy)
{
    *j = (Joy){.cx=cx, .cy=cy, .sx=sx, .sy=sy};
    j->ready = sx > 0 && sy > 0;
    return j->ready;
}

static inline int joy_norm(int raw, int center, int span)
{
    int64_t n = ((int64_t)raw - center) * 1000 / span;
    return n > 1000 ? 1000 : n < -1000 ? -1000 : (int)n;
}

/* x positive = right; y positive = up. A nonzero return is one event. */
static inline int joy_step(Joy *j, bool valid, int rx, int ry, uint32_t ms)
{
    if (!j->ready || !valid) {
        j->dir = J_NONE;
        j->repeating = false;
        return J_NONE;
    }
    int x = joy_norm(rx, j->cx, j->sx);
    int y = joy_norm(ry, j->cy, j->sy);
    int ax = x < 0 ? -x : x, ay = y < 0 ? -y : y;
    int radius2 = x*x + y*y;
    int threshold = j->dir == J_NONE ? 250 : 180;
    if (radius2 < threshold*threshold) {
        j->dir = J_NONE;
        j->repeating = false;
        return J_NONE;
    }
    int horizontal = x >= 0 ? J_RIGHT : J_LEFT;
    int vertical = y >= 0 ? J_UP : J_DOWN;
    int next = ax >= ay ? horizontal : vertical;
    if (j->dir == horizontal && ay*4 <= ax*5) next = horizontal;
    if (j->dir == vertical && ax*4 <= ay*5) next = vertical;
    if (next != j->dir) {
        j->dir = next;
        j->since = j->last = ms;
        j->repeating = false;
        return next;
    }
    uint32_t elapsed = ms - (j->repeating ? j->last : j->since);
    if (elapsed >= (j->repeating ? 100u : 300u)) {
        j->last = ms;
        j->repeating = true;
        return next;
    }
    return J_NONE;
}
#endif

接到T5输入链路时只留一个事件出口

下面的适配段保留 raw API 的错误状态,并交换中心、跨度与两路输入。传入的参数必须由当前设备标定获得,不能照抄别人的ADC中心值。

#include "joy_filter.h"
#include "tdl_joystick_manage.h"

static Joy menu_joy;

bool menu_joy_configure(int raw_x_center, int raw_y_center,
                        int raw_x_span, int raw_y_span)
{
    return joy_init(&menu_joy, raw_y_center, raw_x_center,
                    raw_y_span, raw_x_span);
}

int menu_joy_poll(TDL_JOYSTICK_HANDLE handle, uint32_t now_ms)
{
    int raw_x = 0, raw_y = 0;
    bool ok = tdl_joystick_get_raw_xy(handle, &raw_x, &raw_y) == OPRT_OK;
    return joy_step(&menu_joy, ok, raw_y, raw_x, now_ms);
}

在已有摇杆句柄初始化成功后配置一次,由输入线程约每10~20 ms调用 menu_joy_poll(),将非零事件投递到UI队列。若这条新链路接管菜单,应撤掉同一菜单上的旧方向回调;若界面框架自带长按重复,也要避免再叠一层连发。读取驱动与UI更新放在各自合适的线程,不能在ADC轮询里直接阻塞绘屏。

这里完成了独立输入模块与主机验证,适配段按固定版本API编写,尚未进行完整T5固件编译和烧录。因此上板前需要验证四向映射、标定数据、线程同步和UI事件消费,不能把主机测试通过等同于整机验收。

不接开发板也能复现事件节奏

把上面的头文件保存为 joy_filter.h,再保存以下 demo.c,两者放在同一目录。

#include "joy_filter.h"
#include <stdio.h>

int main(void)
{
    Joy j;
    if (!joy_init(&j, 0, 0, 1000, 1000)) return 1;
    const uint32_t ms[] = {0, 299, 300, 399, 400, 5000, 5000};
    for (unsigned i = 0; i < sizeof ms / sizeof ms[0]; ++i) {
        int event = joy_step(&j, true, 800, 0, ms[i]);
        printf("%u %d\n", (unsigned)ms[i], event);
    }
    return 0;
}

用支持C11的编译器运行;本次实际使用Zig提供的C编译器,Windows命令如下。独立模块不依赖TuyaOpen SDK。

zig cc -std=c11 -Wall -Wextra -Werror -Wconversion -pedantic -O2 demo.c -o demo.exe
./demo.exe

输出应为下列时间与事件值,1表示向右,0表示本次没有新事件。

0 1
299 0
300 1
399 0
400 1
5000 1
5000 0

本次还实际执行了13组主机测试,覆盖中位偏移、圆形边界、斜向噪声、双向角度切换、快速反向、连发时间、调度停顿、ADC失败、毫秒回绕、极端整数输入,以及10万组中心偏移与比例缩放不变性样本,全部通过。测试中的断言明确启用,没有用关闭断言的发布编译假装验收。

实际调试时先看原始值,再看归一化值,最后看事件。中位持续越线就查机械回中、供电与标定;方向颠倒就查轴顺序与符号;一推跳两格先查是否有两个事件生产者;停顿后猛翻页则检查是否有人把时间差换算成一串补发动作。沿着这条链路排查,比反复加大死区更容易找到问题。

模拟摇杆的手感,是中心、半径、角度和时间四种规则共同决定的。把每条规则的输入与输出留下来,即使暂时没有整机,也能先把容易出错的边界算清楚、跑通。

资料与验证范围

硬件与装配图片来自TuyaOpen项目《TuyaOpen 多模态 AI 口袋机》,项目发布者TuyaOpen,MIT许可:
https://oshwhub.com/tuyaopen/graffiti-t5pocket-pocket-machine

硬件母图取自同项目实际嘉立创EDA编辑器,主板V1.0;正文仅裁剪、缩放和适度锐化,未重绘连线与丝印。装配照为项目原状态。未采用项目旧框图中的器件列表来推断本版硬件。

软件核对依据为TuyaOpen,根LICENSE署名为Copyright (c) Tuya.inc 2019,驱动文件另有Copyright (c) 2021-2025 Tuya Inc.署名,适用Apache-2.0许可。固定提交为 695e571fb4f62932dbf86a86fde664fcb7f2c0b4,核对BSP、摇杆驱动和口袋机应用配置:
https://github.com/tuya/TuyaOpen/tree/695e571fb4f62932dbf86a86fde664fcb7f2c0b4/apps/tuya_t5_pocket

本文新增输入整形模块以MIT许可提供;曲线来自该C实现的合成输入运行结果。完成范围为工程接口核对、独立算法实现与主机测试,未进行整机编译、烧录或实体手感测试。

技术交流,欢迎关注微信公众号:美男子玩编程。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -