嵌入式红外NEC协议的结构体封装与PWM发射实现
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调制,还是协议栈抽象,其价值最终体现在产线不良率的降低、客户投诉的减少、以及你深夜接到电话时那份从容。代码可以重构,方案可以迭代,但对工程本质的敬畏——对时序的苛求、对噪声的防范、对边界的尊重——才是嵌入式工程师最坚硬的铠甲。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)