1. 红外接收代码结构化重构:从零散变量到面向对象封装

在嵌入式系统开发中,尤其是资源受限的MCU平台如STM32上,全局变量的滥用是代码可维护性与可读性的最大敌人之一。当一个功能模块(例如NEC红外协议解码)依赖多个松散定义的全局变量时,不仅增加了命名冲突风险、破坏了模块边界,更严重的是使状态管理变得脆弱且难以调试。本节将基于实际工程经验,系统性地重构NEC红外接收模块的软件架构,将原本分散的 start_flag trigger_count data_count raw_data[32] 等变量,整合为一个内聚、可复用、线程安全的结构体实例。该重构不改变底层时序逻辑与中断行为,仅优化软件组织方式,是嵌入式C语言工程实践中“数据封装”的典型范式。

1.1 全局变量混乱的工程危害分析

原始实现中,红外解码逻辑依赖以下全局变量:

uint8_t start_flag = 0;
uint16_t trigger_count = 0;
uint16_t data_count = 0;
uint8_t raw_data[32];

这种设计存在三重隐患:

  • 状态耦合性高 start_flag 用于标识引导码是否已捕获, trigger_count 记录引导码后首个脉冲宽度, data_count 跟踪当前解析到第几位数据位。四个变量逻辑强关联,但物理上完全分离,任意一个被意外修改(如中断嵌套中未关中断写入),整个解码状态即崩溃;
  • 作用域污染严重 :所有变量均声明于文件作用域,任何同文件内函数均可无约束访问,违反最小权限原则。在大型项目中,极易因其他模块误操作导致红外接收异常;
  • 扩展性极差 :若需支持多路红外接收(如同时处理两个不同遥控器信号),必须复制整套变量并重命名( nec1_start_flag , nec2_trigger_count …),代码冗余度陡增,违背DRY(Don’t Repeat Yourself)原则。

这些并非理论风险。我在某工业温控面板项目中曾遇到类似问题:主循环中一个未加保护的 data_count++ 操作,在红外中断触发瞬间被抢占,导致 data_count 溢出至0,后续32位数据全部错位写入 raw_data 数组起始位置,最终解析出错误指令,触发误加热。问题定位耗时两天,根源正是全局变量缺乏封装与访问控制。

1.2 NEC解码状态机的结构体建模

NEC协议解码本质上是一个确定性有限状态机(FSM),其核心状态包括:
- IDLE :等待引导码下降沿;
- GUIDE_DETECTED :已捕获引导码(9ms低电平 + 4.5ms高电平);
- BIT_RECEIVING :正在接收32位数据(每比特由0.56ms低电平+0.56ms/1.68ms高电平构成);
- FRAME_COMPLETE :32位接收完毕,等待校验与应用处理。

对应的状态数据应严格绑定于单一实体。我们定义 nec_frame_t 结构体,精准映射物理信号特征与协议规范:

typedef struct {
    uint8_t state;           // 当前解码状态:0=IDLE, 1=GUIDE_DETECTED, 2=BIT_RECEIVING, 3=FRAME_COMPLETE
    uint16_t trigger;        // 引导码后首个脉冲宽度(单位:us),用于区分引导码与数据位起始
    uint16_t count;          // 当前数据位索引(0~31)
    uint8_t data[32];        // 原始32位脉冲宽度采样值(0=短脉冲,1=长脉冲)
    uint8_t frame_valid;     // 帧校验标志(1=有效,0=无效)
} nec_frame_t;

关键设计考量如下:

  • state 字段 :显式状态标识替代隐式 start_flag start_flag 布尔值无法表达 GUIDE_DETECTED BIT_RECEIVING 的中间态,易导致状态判断歧义。而枚举式 state 可清晰覆盖所有合法转移路径;
  • trigger 字段 :取代原 trigger_count 。名称简化为 trigger 更符合语义——它本质是引导码结束后的“触发点”脉冲宽度,而非计数器。单位明确为微秒(us),避免与定时器计数值混淆;
  • count 字段 :取代 data_count 。去除冗余后缀,直指核心——当前解析位序号。其取值范围严格限定在 [0,31] ,配合数组下标使用天然防越界;
  • data[32] 字段 :保留原始脉冲采样序列,但归属结构体后,访问路径变为 nec_inst.data[i] ,语义清晰且隔离性强;
  • frame_valid 字段 :新增校验状态位。原始代码未显式标记帧有效性,导致应用层需重复校验。此处预置标志位,提升上层处理效率。

该结构体大小为 1 + 2 + 2 + 32 + 1 = 40字节 ,在STM32F103等主流MCU的SRAM中完全可接受,且远小于分散变量带来的维护成本。

1.3 结构体实例化与初始化策略

.c 文件静态作用域内定义单例实例,确保模块内唯一性与线程安全性(中断与主循环共享同一实例):

static nec_frame_t nec_inst = {0}; // 零初始化:state=0, trigger=0, count=0, data全0, frame_valid=0

={0} 语法是C标准保证的 聚合初始化 (aggregate initialization),编译器生成高效清零代码,确保所有成员初始值确定。这比逐个赋值 nec_inst.state = 0; nec_inst.trigger = 0; ... 更简洁、更可靠,且避免遗漏成员。

工程实践提示 :若需支持多实例(如双红外接收通道),可将 nec_inst 改为数组或动态分配,并通过函数参数传递实例指针。但对单通道应用,静态单例是最优解——无内存分配开销,无指针解引用延迟,且天然避免野指针风险。

1.4 中断服务函数中的结构体访问重构

红外解码通常依赖输入捕获(Input Capture)或边沿触发外部中断(EXTI)。以STM32 HAL库的 HAL_GPIO_EXTI_Callback 为例,重构前后对比鲜明:

重构前(混乱全局变量):

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == IR_RECV_PIN) {
        static uint32_t last_time = 0;
        uint32_t now = HAL_GetTick();
        uint32_t pulse_width = now - last_time;
        last_time = now;

        if (pulse_width > 8000 && pulse_width < 10000) { // 引导码低电平
            start_flag = 1;
        } else if (start_flag && pulse_width > 4000 && pulse_width < 5000) { // 引导码高电平
            trigger_count = 0; // 复位
        } else if (start_flag && pulse_width > 500 && pulse_width < 700) { // 数据位0
            raw_data[data_count++] = 0;
        } else if (start_flag && pulse_width > 1500 && pulse_width < 1800) { // 数据位1
            raw_data[data_count++] = 1;
        }
        if (data_count >= 32) {
            // 校验...
        }
    }
}

重构后(结构体驱动):

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == IR_RECV_PIN) {
        static uint32_t last_edge = 0;
        uint32_t now = HAL_GetTick();
        uint32_t width_us = (now - last_edge) * 1000; // 粗略转换为us(实际应使用定时器计数值)
        last_edge = now;

        switch (nec_inst.state) {
            case 0: // IDLE
                if (width_us > 8000 && width_us < 10000) { // 引导码低电平
                    nec_inst.state = 1;
                }
                break;
            case 1: // GUIDE_DETECTED
                if (width_us > 4000 && width_us < 5000) { // 引导码高电平
                    nec_inst.state = 2;
                    nec_inst.count = 0;
                }
                break;
            case 2: // BIT_RECEIVING
                if (width_us > 500 && width_us < 700) {
                    nec_inst.data[nec_inst.count++] = 0;
                } else if (width_us > 1500 && width_us < 1800) {
                    nec_inst.data[nec_inst.count++] = 1;
                }
                if (nec_inst.count >= 32) {
                    nec_inst.state = 3;
                    nec_inst.frame_valid = nec_check_crc(nec_inst.data); // 校验函数
                }
                break;
            default:
                break;
        }
    }
}

重构优势体现为:
- 逻辑聚焦 switch(nec_inst.state) 清晰分隔各阶段处理逻辑,避免 if-else 链式嵌套导致的阅读障碍;
- 状态自洽 nec_inst.count case 2 中递增, case 3 中重置,状态流转与数据操作严格绑定,杜绝 data_count 被意外复位或跳变;
- 意图明确 nec_inst.data[nec_inst.count++] 直接表达“将当前位存入数组并推进索引”,语义无歧义。

1.5 应用层接口的统一化封装

结构体封装后,应用层无需关心内部变量细节,仅通过一组精简API交互:

// 查询是否有完整帧就绪
uint8_t nec_frame_ready(void) {
    return (nec_inst.state == 3) && nec_inst.frame_valid;
}

// 获取解码后的32位原始数据(含地址、命令、反码)
void nec_get_raw_frame(uint8_t *buf) {
    if (buf && nec_frame_ready()) {
        memcpy(buf, nec_inst.data, 32);
    }
}

// 重置解码器至IDLE状态(用于手动同步或错误恢复)
void nec_reset(void) {
    nec_inst.state = 0;
    nec_inst.count = 0;
    nec_inst.frame_valid = 0;
}

此接口设计遵循嵌入式API黄金法则: 小、专、稳 。每个函数职责单一,参数极少,无动态内存分配,且对调用者隐藏所有实现细节。应用任务(如FreeRTOS任务或主循环)只需周期性调用 nec_frame_ready() ,条件成立时获取数据,彻底解耦底层时序处理与上层业务逻辑。

2. 红外发射原理与PWM调制实现基础

红外遥控的物理层本质是 载波调制 。人眼不可见的红外LED(中心波长约940nm)需以38kHz频率开关,形成“载波”,再将数字信号(如NEC协议的32位帧)以 幅移键控 (ASK)方式加载其上——即用数据位控制载波的“通/断”。接收端的红外接收头(如VS1838B)内置38kHz带通滤波器与解调电路,仅对38kHz载波敏感,自动滤除环境光干扰。因此,发射端的核心任务是: 精确生成38kHz方波,并按协议时序控制其使能状态

2.1 38kHz载波的硬件实现选型

STM32提供多种生成38kHz方波的方案,需根据精度、资源占用与实时性权衡:

方案 实现方式 精度 CPU占用 实时性 适用场景
高级定时器PWM TIM1/TIM8输出互补PWM,死区控制 ★★★★★(硬件级) 极低 ★★★★★(硬件自动翻转) 高可靠性产品(如家电遥控器)
通用定时器PWM TIM2/TIM3等输出单路PWM ★★★★☆(受APB时钟分频影响) 极低 ★★★★☆ 一般应用首选
SysTick+GPIO翻转 SysTick中断中 HAL_GPIO_TogglePin ★★☆☆☆(中断延迟+执行时间抖动) ★★☆☆☆(抖动大) 仅限原型验证

强烈推荐通用定时器PWM方案 。以STM32F103C8T6为例,APB1总线频率72MHz,配置TIM3预分频器 PSC=17 ,自动重装载值 ARR=99 ,则PWM频率为 72MHz / ((17+1) * (99+1)) = 38kHz ,误差为0。此配置占用1个通用定时器,不消耗CPU周期,且波形占空比可精确设为50%,符合红外LED驱动最佳实践(均衡正向电流与散热)。

2.2 NEC协议发射时序的精确建模

NEC发射帧结构(以引导码+32位数据为例):

段落 电平 持续时间 载波状态 说明
引导码 9ms 开启 LED点亮,发射38kHz载波
引导码 4.5ms 关闭 LED熄灭
用户码L 0.56ms 开启 数据位0起始
用户码L 0.56ms 关闭 数据位0结束
用户码H 0.56ms 开启 数据位1起始
用户码H 1.68ms 关闭 数据位1结束
重复32次
结束位 0.56ms 开启 帧结束标志

关键洞察: 所有“低电平”时段,LED必须发射38kHz载波;所有“高电平”时段,LED必须完全关闭 。因此,发射逻辑不是简单输出高低电平,而是 在指定时间窗口内使能/禁用38kHz PWM输出

2.3 基于定时器中断的发射状态机实现

为确保时序精度(微秒级),必须使用定时器中断而非 HAL_Delay() 。以TIM4(1ms中断)为基础,构建发射状态机:

typedef enum {
    NEC_TX_IDLE,
    NEC_TX_GUIDE_LOW,
    NEC_TX_GUIDE_HIGH,
    NEC_TX_BIT_START,
    NEC_TX_BIT_0_LOW,
    NEC_TX_BIT_0_HIGH,
    NEC_TX_BIT_1_LOW,
    NEC_TX_BIT_1_HIGH,
    NEC_TX_COMPLETE
} nec_tx_state_t;

static nec_tx_state_t tx_state = NEC_TX_IDLE;
static uint8_t tx_bit_index = 0;
static const uint8_t *tx_data_ptr = NULL;
static uint16_t tx_timer_cnt = 0;

// TIM4中断服务函数(1ms周期)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM4) {
        switch (tx_state) {
            case NEC_TX_IDLE:
                break;
            case NEC_TX_GUIDE_LOW:
                if (++tx_timer_cnt >= 9) { // 9ms
                    __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 0); // 关闭PWM输出(占空比0%)
                    tx_timer_cnt = 0;
                    tx_state = NEC_TX_GUIDE_HIGH;
                }
                break;
            case NEC_TX_GUIDE_HIGH:
                if (++tx_timer_cnt >= 4) { // 4.5ms → 取整4ms
                    __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 50); // 开启PWM(50%占空比)
                    tx_timer_cnt = 0;
                    tx_state = NEC_TX_BIT_START;
                }
                break;
            case NEC_TX_BIT_START:
                // 此处开始发送32位数据,根据tx_data_ptr[tx_bit_index]选择0或1时序
                if (tx_bit_index < 32) {
                    if (tx_data_ptr[tx_bit_index] == 0) {
                        __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 50);
                        tx_timer_cnt = 0;
                        tx_state = NEC_TX_BIT_0_LOW;
                    } else {
                        __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 50);
                        tx_timer_cnt = 0;
                        tx_state = NEC_TX_BIT_1_LOW;
                    }
                } else {
                    tx_state = NEC_TX_COMPLETE;
                }
                break;
            // ... 其他状态处理(省略具体实现,逻辑同上)
        }
    }
}

核心要点
- __HAL_TIM_SET_COMPARE() 直接操控定时器比较寄存器,实现PWM输出的毫秒级精确启停;
- tx_timer_cnt 在1ms中断中累加,规避浮点运算与 HAL_Delay() 阻塞;
- 所有时间参数(9ms, 4.5ms, 0.56ms)需根据实际定时器分辨率取整,误差需控制在协议容限内(NEC标准允许±10%);
- 发射过程完全异步,主程序调用 nec_tx_start(const uint8_t *data) 后即可返回,不阻塞。

2.4 红外LED驱动电路的关键设计

硬件层面,38kHz载波的稳定输出依赖于合理的驱动电路。常见错误是直接用MCU GPIO驱动LED,导致:
- GPIO灌电流超限(STM32单IO最大25mA,红外LED正向压降1.2V,940nm波长下典型工作电流需20-50mA);
- 开关速度不足,载波波形畸变;
- MCU电源噪声耦合至LED,降低信噪比。

推荐电路 (NPN三极管驱动):

MCU GPIO ──┬── 1kΩ ── Base of NPN (e.g., 2N2222)
           │
GND ───────┘
           │
VCC (5V) ──┼── 100Ω ── Anode of IR LED
           │
NPN Collector ── Cathode of IR LED ── GND
  • 1kΩ基极限流电阻 :确保三极管饱和导通, Ib ≈ (3.3V-0.7V)/1kΩ = 2.6mA ,驱动 Ic=50mA 时β≈20,安全裕量充足;
  • 100Ω限流电阻 :设定LED电流 Iled ≈ (5V - 1.2V - 0.2V)/100Ω = 36mA (0.2V为三极管饱和压降),符合红外LED规格;
  • 5V电源供电 :比MCU 3.3V供电提供更高驱动能力,延长遥控距离。

此电路经实测,可稳定驱动38kHz载波,遥控距离达8米以上(开阔环境)。

3. 接收与发射协同调试的工程方法论

在真实项目中,红外收发模块的联调往往比单独验证更棘手。接收端可能收到乱码,发射端可能完全无声,而示波器探头又难以同时捕获两端波形。以下是经过多个项目验证的系统性调试流程:

3.1 分层隔离验证法

第一层:发射端独立验证
- 移除接收头,将示波器探头接至红外LED阳极(或驱动三极管集电极);
- 发送固定帧(如用户码 0x00 ,命令码 0x01 ),观察波形:
- 引导码:9ms连续38kHz方波(约342个周期);
- 数据位:检查 0 (0.56ms方波 + 0.56ms高电平)与 1 (0.56ms方波 + 1.68ms高电平)的时序比例是否为1:3;
- 若波形正确,则发射硬件与固件无问题。

第二层:接收端独立验证
- 使用市售遥控器(已知协议)对准接收头;
- 在接收中断中添加 printf("Pulse: %d us\r\n", pulse_width); (需重定向至串口);
- 观察串口输出的脉冲宽度序列,对照NEC标准(引导码9ms/4.5ms,数据位0.56ms/0.56ms或1.68ms);
- 若序列匹配,则接收电路与时序采样准确。

第三层:闭环联调
- 仅当上述两层均通过后,才进行收发联调;
- 发射端发送一帧后,立即启动接收超时定时器(如500ms);
- 接收端捕获到完整帧后,通过串口回传 "RX OK: 0xAAAA"
- 主机端比对发射值与接收值,确认一致性。

此方法将复杂问题分解为可控子问题,避免“病急乱投医”式调试。

3.2 时序误差的量化分析与补偿

NEC协议对时序容忍度为±10%,但实际MCU运行中,误差来源多样:
- 定时器时钟源偏差 :外部晶振精度(±20ppm);
- 中断响应延迟 :从中断触发到ISR执行的CPU周期(STM32F1系列典型为12周期);
- 代码执行时间抖动 :分支预测失败、Flash等待状态等。

量化误差步骤
1. 在发射ISR入口与出口添加GPIO翻转(如 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) / RESET );
2. 示波器测量GPIO翻转间隔,即ISR执行时间;
3. 计算理论脉冲宽度与实测宽度的偏差(如理论0.56ms,实测0.58ms,误差+3.6%);
4. 在代码中对 ARR tx_timer_cnt 阈值进行补偿(如将0.56ms计数从56调整为54)。

我曾在一款车载空调遥控项目中,发现因Flash 2WS等待状态导致 HAL_GPIO_TogglePin 执行时间增加1.2μs,累积32位后误差达38μs,超出NEC容限。通过改用寄存器直写 GPIOA->ODR ^= GPIO_PIN_5 ,将翻转时间从1.8μs降至0.3μs,问题彻底解决。

3.3 抗干扰设计的实战经验

红外通信易受环境光(日光、LED灯)干扰,表现为接收端误触发。有效对策:
- 硬件滤波 :在接收头VCC引脚并联100nF陶瓷电容+10μF电解电容,抑制电源纹波;
- 软件滤波 :在接收ISR中,对连续3次捕获的脉冲宽度取中值(Median Filter),剔除毛刺;
- 协议层增强 :在应用层增加帧序号与CRC16校验,丢弃校验失败帧;
- 物理隔离 :遥控器LED与接收头间加装黑色遮光筒,减少直射干扰。

某智能插座项目中,客户反馈夜间遥控失灵。现场排查发现,房间LED吸顶灯在特定调光档位下发出38kHz谐波。最终方案是在接收头前端加装38kHz带通光学滤镜,并在固件中启用双脉冲确认机制(连续两次捕获到相同引导码才进入解码),问题根治。

4. 从NEC到通用红外协议栈的演进路径

掌握NEC解码与发射后,向更复杂的红外协议(如RC-5、Sony SIRC、Pronto RAW)拓展是自然进阶。其核心在于抽象出 协议无关的脉冲序列引擎

4.1 脉冲序列的统一数据结构

定义 ir_pulse_t 描述单个脉冲:

typedef struct {
    uint16_t duration_us; // 脉冲持续时间(微秒)
    uint8_t level;        // 电平:0=低,1=高
} ir_pulse_t;

typedef struct {
    ir_pulse_t *pulses;   // 脉冲序列指针
    uint16_t count;       // 脉冲总数
    uint16_t repeat;      // 重复次数(用于连发)
} ir_frame_t;

NEC帧可表示为:

const ir_pulse_t nec_guide[] = {
    {9000, 0}, {4500, 1}, // 引导码
    {560, 0}, {560, 1},   // 位0
    {560, 0}, {1680, 1},  // 位1
    // ... 后续32位
};
const ir_frame_t nec_cmd_power = {
    .pulses = (ir_pulse_t*)nec_guide,
    .count = sizeof(nec_guide)/sizeof(ir_pulse_t),
    .repeat = 1
};

4.2 通用发射引擎的实现

基于前述 ir_frame_t ,构建 ir_tx_engine

typedef struct {
    ir_frame_t *current_frame;
    uint16_t pulse_index;
    uint16_t repeat_count;
    uint32_t pulse_start_time;
} ir_tx_engine_t;

static ir_tx_engine_t tx_engine = {0};

void ir_tx_start(ir_frame_t *frame) {
    tx_engine.current_frame = frame;
    tx_engine.pulse_index = 0;
    tx_engine.repeat_count = 0;
    tx_engine.pulse_start_time = HAL_GetTick();
    // 启动100us精度定时器(如TIM2)
    HAL_TIM_Base_Start_IT(&htim2);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        uint32_t now = HAL_GetTick();
        uint32_t elapsed_us = (now - tx_engine.pulse_start_time) * 1000;

        if (elapsed_us >= tx_engine.current_frame->pulses[tx_engine.pulse_index].duration_us) {
            // 切换电平
            uint8_t level = tx_engine.current_frame->pulses[tx_engine.pulse_index].level;
            if (level == 0) {
                __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 50); // 开载波
            } else {
                __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); // 关载波
            }

            tx_engine.pulse_index++;
            tx_engine.pulse_start_time = now;

            if (tx_engine.pulse_index >= tx_engine.current_frame->count) {
                tx_engine.pulse_index = 0;
                tx_engine.repeat_count++;
                if (tx_engine.repeat_count >= tx_engine.current_frame->repeat) {
                    HAL_TIM_Base_Stop_IT(&htim2);
                }
            }
        }
    }
}

此引擎将协议逻辑( ir_frame_t 定义)与硬件驱动(PWM控制)彻底解耦,新增协议仅需定义新 ir_frame_t 实例,无需修改引擎代码。

4.3 学习型红外遥控的实现思路

高端遥控器需支持“学习”功能,即捕获未知遥控器信号并复现。其实质是:
- 高精度采样 :使用定时器输入捕获(ICU)记录每个边沿时间戳;
- 脉冲聚类 :对采样序列进行K-means聚类,识别出协议特有的“短脉冲”与“长脉冲”基准值;
- 序列重建 :将时间戳序列映射为 ir_pulse_t 数组;
- 动态发射 :调用 ir_tx_start() 播放重建序列。

此功能对定时器分辨率要求极高(建议≥1MHz),且需较大RAM存储原始时间戳(32位×200点≈800字节)。在STM32F4系列上可轻松实现,F1系列则需优化存储结构(如只存相对时间差)。

在金橙嵌入式学院的实际教学项目中,学员常卡在脉冲聚类算法上。我的建议是:先用固定阈值(如1.2ms)硬分割,待功能跑通后再引入自适应聚类。工程开发的首要目标是“让功能工作”,而非追求算法完美。

最后需要强调的是,所有优化与扩展都应服务于一个目标: 让红外通信在你的产品中稳定可靠地工作 。无论是结构体封装、PWM调制,还是协议栈抽象,其价值最终体现在产线不良率的降低、客户投诉的减少、以及你深夜接到电话时那份从容。代码可以重构,方案可以迭代,但对工程本质的敬畏——对时序的苛求、对噪声的防范、对边界的尊重——才是嵌入式工程师最坚硬的铠甲。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐