1. RT-Thread传感器驱动框架设计思想与工程价值

在嵌入式系统开发中,传感器设备的多样性与接口异构性构成了应用层开发的主要障碍。RT-Thread操作系统引入的SENSOR驱动框架,并非简单地封装底层硬件访问逻辑,而是构建了一套面向数据抽象的分层架构模型。其核心工程目标明确: 将传感器物理特性与通信协议细节从应用逻辑中彻底剥离,使上层业务代码仅需关注“获取什么数据”,而非“如何获取数据”

这一设计思想源于典型的工业实践痛点。以温度采集场景为例,DHT11、DS18B20、BME280、SHT3x等器件虽同属温度传感器,但其通信机制截然不同:DHT11采用单总线时序协议,DS18B20依赖1-Wire总线仲裁,BME280支持I²C和SPI双模式,SHT3x则要求特定的CRC校验流程。若每个应用都直接对接具体芯片寄存器,不仅导致代码高度耦合,更使固件升级、硬件替换成本急剧上升。SENSOR框架通过定义统一的数据模型(如 struct rt_sensor_data )和标准化操作集( struct rt_sensor_ops ),在驱动层实现协议适配,在框架层完成数据归一化,最终向应用层暴露一致的 rt_device_read() 接口。这种分层解耦带来的可复用性提升是实质性的——同一套温控逻辑可无缝迁移至不同硬件平台,仅需更换底层驱动模块,无需修改任何业务代码。

该框架的价值不仅体现在开发效率层面,更深刻影响着系统的可维护性与可测试性。当传感器硬件发生变更时,工程师只需验证新驱动模块是否符合框架规范,而无需重新审计整个应用逻辑。在量产阶段,这种架构显著降低了因传感器批次差异或供应链切换引发的固件回归风险。实际项目中,我们曾遇到某环境监测终端因BME280供应商停产,紧急切换至SHT35方案的情况。得益于SENSOR框架的严格接口约束,仅用4小时即完成新驱动开发与集成测试,而应用层代码零修改,充分验证了该设计范式的工程鲁棒性。

2. SENSOR框架核心架构与数据流向解析

RT-Thread的SENSOR驱动框架采用经典的三层架构模型,自下而上分为硬件抽象层(HAL)、驱动适配层(Driver Adapter)和框架服务层(Framework Service)。理解各层职责边界及数据交互路径,是掌握框架工作原理的关键。

2.1 硬件抽象层(HAL)

此层由芯片厂商提供的底层外设驱动构成,负责与物理硬件进行最直接的交互。对于I²C传感器,HAL层需提供 i2c_master_send() i2c_master_recv() 等基础函数;对于SPI设备,则需实现 spi_transfer() 等原子操作。这些函数不感知传感器类型,仅完成总线级数据收发。以MPU6050为例,HAL层代码位于 drivers/i2c/i2c_core.c 中,其 rt_i2c_master_send() 函数封装了STM32 HAL库的 HAL_I2C_Master_Transmit() 调用,屏蔽了不同MCU平台的寄存器操作差异。

2.2 驱动适配层(Driver Adapter)

这是框架中最关键的胶水层,承担着协议解析与数据转换的核心任务。每个具体传感器(如MPU6050、BME280)在此层实现独立的驱动模块,其核心结构体为 struct rt_sensor_device 。该结构体包含两个核心成员:
- ops : 指向 struct rt_sensor_ops 的指针,定义了 fetch_data() control() 等标准操作函数
- config : struct rt_sensor_config 类型的配置结构,描述传感器类型、量程、分辨率等元信息

以MPU6050驱动为例,其 fetch_data() 函数内部逻辑为:调用HAL层I²C读取原始加速度计/陀螺仪寄存器值 → 执行16位补码转换 → 根据量程配置(±2g/±4g/±8g/±16g)进行数值缩放 → 将结果填充至 struct rt_sensor_data 结构体的 data.x16 data.y16 等字段。此过程完全隐藏了I²C地址(0x68/0x69)、寄存器映射(0x3B-0x40)、数据字节序等硬件细节。

2.3 框架服务层(Framework Service)

该层由 components/drivers/sensors/sensor.c 实现,是整个框架的中枢。它向上提供 rt_device_open() rt_device_read() 等标准设备管理接口,向下通过函数指针调用驱动适配层的 ops 函数。当应用调用 rt_device_read(dev, &data, sizeof(data)) 时,框架层执行以下流程:
1. 验证设备句柄有效性及参数合法性
2. 调用驱动层 sensor_ops->fetch_data() 获取原始数据
3. 对返回的 struct rt_sensor_data 进行时间戳标记与单位标准化
4. 将处理后的数据拷贝至用户缓冲区

此设计确保了所有传感器数据均遵循统一的数据结构与时间基准,为上层数据融合算法(如卡尔曼滤波)提供了可靠输入。值得注意的是,框架层本身不包含任何硬件相关代码,这使其具备极强的跨平台移植能力——同一套框架代码可运行于ARM Cortex-M、RISC-V及ESP32等不同架构平台。

3. 传感器驱动开发全流程实践:以MPU6050为例

开发一个符合RT-Thread SENSOR框架规范的传感器驱动,需严格遵循“配置→实现→注册”三步法。本节以MPU6050六轴运动传感器为实例,完整呈现工程化开发流程。

3.1 开发前技术调研

在编码前,必须完成三项关键调研:
- 传感器类型识别 :确认MPU6050属于 RT_SENSOR_CLASS_ACCEL (加速度计)、 RT_SENSOR_CLASS_GYRO (陀螺仪)复合类传感器,需在驱动中同时声明两种类型
- 通信接口确认 :查阅数据手册,确定其支持I²C(默认地址0x68)与SPI两种模式。本例采用I²C,需确认硬件连接中SDA/SCL引脚及上拉电阻配置
- 关键参数提取 :记录量程配置寄存器(0x1C/0x1B)、数据就绪中断引脚(INT)、输出数据速率(ODR)范围(4Hz-1kHz)等核心参数

3.2 驱动适配层实现

驱动文件通常命名为 mpu6050.c ,位于 drivers/sensors/ 目录下。其实现需包含以下核心组件:

3.2.1 设备私有结构体
struct mpu6050_device
{
    struct rt_sensor_device sensor;
    struct rt_i2c_bus_device *i2c_bus;
    rt_uint8_t i2c_addr;
    rt_uint8_t range_gyro;  // 陀螺仪量程:0=±250dps, 1=±500dps...
    rt_uint8_t range_accel; // 加速度计量程:0=±2g, 1=±4g...
};
3.2.2 数据获取函数(fetch_data)
static rt_size_t mpu6050_fetch_data(struct rt_sensor_device *sensor,
                                   void *buf, rt_size_t len)
{
    struct mpu6050_device *mpu = (struct mpu6050_device *)sensor;
    struct rt_sensor_data *data = (struct rt_sensor_data *)buf;

    // 1. 读取原始数据寄存器(0x3B-0x40共6个16位寄存器)
    rt_uint8_t raw_data[12];
    if (rt_i2c_master_recv(mpu->i2c_bus, mpu->i2c_addr, 
                          0x3B, RT_I2C_ADDR_7BIT | RT_I2C_RD, 
                          raw_data, 12) != 12)
        return 0;

    // 2. 解析加速度计数据(X,Y,Z轴)
    data->data.x16 = (rt_int16_t)((raw_data[0] << 8) | raw_data[1]);
    data->data.y16 = (rt_int16_t)((raw_data[2] << 8) | raw_data[3]);
    data->data.z16 = (rt_int16_t)((raw_data[4] << 8) | raw_data[5]);

    // 3. 解析陀螺仪数据(X,Y,Z轴)
    data->data.x16 += (rt_int16_t)((raw_data[8] << 8) | raw_data[9]) << 16;
    // ... 其他轴处理

    // 4. 单位转换(示例:加速度计±2g量程,1g=16384 LSB)
    data->data.x16 = data->data.x16 * 9807 / 16384; // 转换为cm/s²
    data->data.y16 = data->data.y16 * 9807 / 16384;
    data->data.z16 = data->data.z16 * 9807 / 16384;

    return sizeof(struct rt_sensor_data);
}
3.2.3 控制函数(control)
static rt_err_t mpu6050_control(struct rt_sensor_device *sensor,
                                int cmd, void *arg)
{
    struct mpu6050_device *mpu = (struct mpu6050_device *)sensor;

    switch (cmd)
    {
    case RT_SENSOR_CTRL_GET_ID:
        *(rt_uint32_t*)arg = MPU6050_DEVICE_ID;
        break;
    case RT_SENSOR_CTRL_SET_RANGE:
        {
            struct rt_sensor_range *range = (struct rt_sensor_range*)arg;
            if (range->type == RT_SENSOR_CLASS_ACCEL)
                mpu->range_accel = range->max;
            else if (range->type == RT_SENSOR_CLASS_GYRO)
                mpu->range_gyro = range->max;
            // 写入量程配置寄存器0x1C/0x1B
            rt_i2c_master_send(mpu->i2c_bus, mpu->i2c_addr, 
                              0x1C, RT_I2C_ADDR_7BIT | RT_I2C_WR, 
                              &mpu->range_accel, 1);
        }
        break;
    default:
        return -RT_ERROR;
    }
    return RT_EOK;
}
3.2.4 操作函数集定义
static const struct rt_sensor_ops mpu6050_sensor_ops =
{
    .fetch_data = mpu6050_fetch_data,
    .control = mpu6050_control,
};

3.3 驱动注册与初始化

驱动需在板级支持包(BSP)中完成注册。典型流程如下:

3.3.1 BSP层初始化函数

bsp/stm32/libraries/HAL_Drivers/drv_mpu6050.c 中实现:

int rt_hw_mpu6050_init(void)
{
    struct mpu6050_device *mpu;

    // 1. 分配设备内存
    mpu = rt_calloc(1, sizeof(struct mpu6050_device));
    if (!mpu) return -RT_ENOMEM;

    // 2. 初始化I²C总线(假设使用I2C1)
    mpu->i2c_bus = rt_i2c_bus_device_find("i2c1");
    if (!mpu->i2c_bus) 
        goto __exit;

    mpu->i2c_addr = 0x68; // 默认I²C地址

    // 3. 初始化MPU6050硬件(配置寄存器、校准等)
    if (mpu6050_hardware_init(mpu) != RT_EOK)
        goto __exit;

    // 4. 配置传感器信息
    mpu->sensor.config.type = RT_SENSOR_CLASS_ACCEL | RT_SENSOR_CLASS_GYRO;
    mpu->sensor.config.unit = RT_SENSOR_UNIT_M_S2; // 加速度单位
    mpu->sensor.config.intf_type = RT_SENSOR_INTF_I2C;
    mpu->sensor.config.range_max = 16; // ±16g
    mpu->sensor.config.period_min = 1000; // 最小采样周期1ms

    // 5. 注册到SENSOR框架
    if (rt_sensor_register(&mpu->sensor, "mpu6050", 
                          RT_DEVICE_FLAG_RDWR, RT_NULL) != RT_EOK)
    {
        rt_kprintf("mpu6050 register failed!\n");
        goto __exit;
    }

    rt_kprintf("mpu6050 init success!\n");
    return RT_EOK;

__exit:
    rt_free(mpu);
    return -RT_ERROR;
}
INIT_DEVICE_EXPORT(rt_hw_mpu6050_init);
3.3.2 Kconfig配置启用

bsp/stm32/Kconfig 中添加:

config BSP_USING_MPU6050
    bool "Enable MPU6050 sensor"
    depends on RT_USING_SENSOR && BSP_USING_I2C1
    default n
    select RT_USING_I2C_BITOPS if !BSP_USING_I2C1

3.4 应用层调用示例

应用代码无需感知底层细节,仅通过标准设备接口操作:

#include <rtdevice.h>
#include <drivers/sensor.h>

int sensor_app_sample(void)
{
    rt_device_t dev;
    struct rt_sensor_data data;

    // 1. 查找设备
    dev = rt_device_find("mpu6050");
    if (!dev) 
    {
        rt_kprintf("mpu6050 device not found!\n");
        return -1;
    }

    // 2. 打开设备
    if (rt_device_open(dev, RT_DEVICE_OFLAG_RDONLY) != RT_EOK)
    {
        rt_kprintf("open mpu6050 failed!\n");
        return -1;
    }

    // 3. 读取数据(阻塞方式)
    while (1)
    {
        if (rt_device_read(dev, 0, &data, sizeof(data)) > 0)
        {
            rt_kprintf("Accel: x=%d y=%d z=%d\n", 
                      data.data.x16, data.data.y16, data.data.z16);
        }
        rt_thread_mdelay(100);
    }

    rt_device_close(dev);
    return 0;
}
MSH_CMD_EXPORT(sensor_app_sample, sensor application sample);

4. 通信总线配置深度解析:I²C/SPI/UART的桥接机制

SENSOR框架的灵活性高度依赖于其通信总线抽象机制。框架本身不直接操作硬件总线,而是通过 struct rt_sensor_config 中的 intf_type 字段与 user_data 指针,将总线初始化责任下放至BSP层。这种设计避免了框架层对具体总线协议的硬编码依赖,是实现跨平台兼容性的关键。

4.1 总线配置结构体解析

struct rt_sensor_config 定义如下:

struct rt_sensor_config
{
    rt_uint8_t type;          // 传感器类型:RT_SENSOR_CLASS_ACCEL等
    rt_uint8_t unit;          // 数据单位:RT_SENSOR_UNIT_M_S2等
    rt_uint8_t intf_type;     // 接口类型:RT_SENSOR_INTF_I2C/SPI/UART
    rt_uint8_t period_min;    // 最小采样周期(ms)
    rt_uint16_t range_max;    // 量程上限
    union {
        struct {
            rt_uint8_t addr;      // I²C从机地址
            const char *bus_name; // I²C总线名称
        } i2c;
        struct {
            rt_uint8_t cs_pin;    // SPI片选引脚
            const char *bus_name; // SPI总线名称
        } spi;
        struct {
            rt_uint32_t baud_rate; // UART波特率
            const char *uart_name; // UART设备名称
        } uart;
    } intf;
    void *user_data;           // 用户私有数据指针
};

其中 intf 联合体根据 intf_type 动态选择对应配置项, user_data 则用于传递更复杂的上下文信息(如DMA句柄、中断号等)。

4.2 BSP层总线初始化实践

以STM32平台I²C总线初始化为例,需在 bsp/stm32/port/sensor_port.c 中实现:

// 定义I²C总线设备映射表
static const struct sensor_i2c_info i2c_info_table[] = 
{
    { "mpu6050", "i2c1", 0x68 },
    { "bme280",  "i2c1", 0x76 },
    { "sht35",   "i2c2", 0x44 },
};

// 通用I²C传感器初始化函数
int rt_hw_sensor_i2c_init(const char *name, struct rt_sensor_device *sensor)
{
    rt_uint8_t i;
    struct rt_i2c_bus_device *i2c_bus;

    // 1. 查找匹配的I²C总线配置
    for (i = 0; i < sizeof(i2c_info_table)/sizeof(i2c_info_table[0]); i++)
    {
        if (rt_strcmp(name, i2c_info_table[i].name) == 0)
        {
            // 2. 获取I²C总线设备句柄
            i2c_bus = rt_i2c_bus_device_find(i2c_info_table[i].bus_name);
            if (!i2c_bus) 
                return -RT_ERROR;

            // 3. 填充传感器配置
            sensor->config.intf_type = RT_SENSOR_INTF_I2C;
            sensor->config.intf.i2c.addr = i2c_info_table[i].addr;
            sensor->config.intf.i2c.bus_name = i2c_info_table[i].bus_name;

            // 4. 将I²C总线句柄存入user_data供驱动使用
            ((struct mpu6050_device*)sensor)->i2c_bus = i2c_bus;
            return RT_EOK;
        }
    }
    return -RT_ERROR;
}

// 在驱动初始化函数中调用
int rt_hw_mpu6050_init(void)
{
    // ... 前置代码 ...

    // 调用BSP层总线初始化
    if (rt_hw_sensor_i2c_init("mpu6050", &mpu->sensor) != RT_EOK)
        goto __exit;

    // ... 后续注册代码 ...
}

4.3 多总线混合部署策略

在复杂系统中,常需混合使用多种总线。例如某无人机飞控板同时集成:
- MPU6050(I²C1,地址0x68):提供姿态解算原始数据
- MS5611(SPI1,CS引脚PA4):提供高精度气压数据
- GPS模块(UART3,波特率9600):提供经纬度与时间信息

此时需在 sensor_port.c 中扩展配置表:

static const struct sensor_spi_info spi_info_table[] = 
{
    { "ms5611", "spi1", GPIOA_PIN4 }, // PA4作为SPI1片选
};

static const struct sensor_uart_info uart_info_table[] = 
{
    { "gps", "uart3", 9600 },
};

并在 rt_hw_sensor_spi_init() rt_hw_sensor_uart_init() 中分别实现对应总线初始化逻辑。这种模块化设计使新增传感器仅需扩展配置表,无需修改框架核心代码,极大提升了系统可扩展性。

5. 高级特性实现:中断模式与数据融合支持

SENSOR框架不仅支持轮询(Polling)模式,还完整实现了中断(Interrupt)与FIFO(First-In-First-Out)两种高级数据采集模式。这些特性对实时性要求高的应用场景至关重要,如无人机姿态控制、工业振动监测等。

5.1 中断模式驱动开发

中断模式的核心在于利用传感器的数据就绪(Data Ready)引脚,在数据有效时触发MCU中断,避免CPU空转轮询。以MPU6050为例,其INT引脚在新数据写入FIFO后产生下降沿脉冲。

5.1.1 中断驱动改造要点
  1. 硬件连接确认 :将MPU6050的INT引脚连接至MCU任意GPIO(如STM32的PB0),并配置为外部中断输入
  2. 中断配置寄存器 :在 mpu6050_hardware_init() 中写入寄存器0x37(INT_PIN_CFG)和0x6B(PWR_MGMT_1),启用数据就绪中断
  3. 中断服务函数(ISR) :在BSP层实现GPIO中断处理,设置事件标志位
  4. 数据读取优化 fetch_data() 函数改为非阻塞模式,仅在ISR触发后执行一次读取
5.1.2 中断服务函数实现
// 在bsp/stm32/drivers/drv_gpio.c中
void EXTI0_IRQHandler(void)
{
    // 清除EXTI0中断标志
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);

    // 设置传感器数据就绪事件
    rt_event_send(sensor_event, SENSOR_EVENT_DATA_READY);
}

// 在驱动中创建事件对象
static struct rt_event sensor_event;

int rt_hw_mpu6050_init(void)
{
    // ... 前置初始化 ...

    // 创建事件对象
    rt_event_init(&sensor_event, "mpu6050_evt", RT_IPC_FLAG_FIFO);

    // 初始化GPIO中断(PB0)
    HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);

    // ... 后续注册 ...
}
5.1.3 中断模式fetch_data实现
static rt_size_t mpu6050_fetch_data_interrupt(struct rt_sensor_device *sensor,
                                             void *buf, rt_size_t len)
{
    struct mpu6050_device *mpu = (struct mpu6050_device *)sensor;
    struct rt_sensor_data *data = (struct rt_sensor_data *)buf;

    // 等待数据就绪事件(超时100ms)
    if (rt_event_recv(&sensor_event, SENSOR_EVENT_DATA_READY,
                      RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
                      RT_WAITING_FOREVER, RT_NULL) != RT_EOK)
        return 0;

    // 执行一次数据读取
    return mpu6050_fetch_data_polling(sensor, buf, len);
}

5.2 FIFO数据缓存与批量读取

高端传感器(如BMI270、ICM20948)内置FIFO,可存储数十至数百组采样数据。SENSOR框架通过 RT_SENSOR_CTRL_SET_FIFO 控制命令支持FIFO配置,驱动需实现 fifo_read() 回调函数。

5.2.1 FIFO驱动扩展
static rt_err_t mpu6050_control_fifo(struct rt_sensor_device *sensor,
                                    int cmd, void *arg)
{
    switch (cmd)
    {
    case RT_SENSOR_CTRL_SET_FIFO:
        {
            struct rt_sensor_fifo_config *cfg = (struct rt_sensor_fifo_config*)arg;
            // 配置MPU6050 FIFO(寄存器0x23, 0x24)
            rt_i2c_master_send(mpu->i2c_bus, mpu->i2c_addr, 
                              0x23, RT_I2C_ADDR_7BIT | RT_I2C_WR, 
                              &cfg->watermark, 1);
        }
        break;
    }
    return RT_EOK;
}

// 新增FIFO读取函数
static rt_size_t mpu6050_fifo_read(struct rt_sensor_device *sensor,
                                   void *buf, rt_size_t len)
{
    // 读取FIFO寄存器(0x74)获取FIFO长度
    // 读取FIFO数据寄存器(0x72)获取批量数据
    // 解析并填充至buf指向的struct rt_sensor_data数组
}

5.3 数据融合框架集成

RT-Thread提供 sensor_fusion 组件,支持基于卡尔曼滤波的姿态解算。SENSOR框架通过 RT_SENSOR_CLASS_FUSION 类型标识融合传感器,其 fetch_data() 返回的是经过算法处理的融合数据(如欧拉角、四元数),而非原始传感器值。

// 融合传感器驱动示例
static rt_size_t fusion_fetch_data(struct rt_sensor_device *sensor,
                                  void *buf, rt_size_t len)
{
    struct fusion_device *fusion = (struct fusion_device *)sensor;
    struct rt_sensor_data *data = (struct rt_sensor_data *)buf;

    // 调用卡尔曼滤波库更新状态
    kalman_update(&fusion->kf, &fusion->accel_raw, &fusion->gyro_raw);

    // 获取融合后的欧拉角
    data->data.x16 = (rt_int16_t)(fusion->kf.euler_roll * 100); // 单位:0.01°
    data->data.y16 = (rt_int16_t)(fusion->kf.euler_pitch * 100);
    data->data.z16 = (rt_int16_t)(fusion->kf.euler_yaw * 100);

    return sizeof(struct rt_sensor_data);
}

6. 常见问题诊断与实战调试技巧

在SENSOR驱动开发过程中,约70%的问题集中于通信链路故障与数据解析错误。以下为经过大量项目验证的调试方法论。

6.1 I²C通信故障定位

rt_device_read() 返回0字节时,按以下步骤排查:
1. 硬件层验证 :使用逻辑分析仪捕获SDA/SCL波形,确认起始条件、地址帧(0x68写/读)、应答位(ACK/NACK)是否正常。常见问题包括上拉电阻阻值过大(>10kΩ)、线路过长导致信号反射、电源噪声干扰
2. 总线占用检测 :在 rt_i2c_master_recv() 调用前后插入 rt_kprintf() ,确认函数是否卡死。若卡死,大概率存在总线被其他设备长期占用或从机未响应
3. 寄存器读写验证 :编写最小测试函数,直接读取MPU6050的WHO_AM_I寄存器(0x75),预期值为0x68。若读取失败,说明I²C通信链路未建立

6.2 数据解析错误修正

原始数据异常(如全0、溢出、跳变)的典型原因:
- 字节序错误 :MPU6050采用大端序(MSB在前),而某些MCU平台默认小端序,需显式转换
- 补码处理缺失 :16位寄存器值为有符号数,需强制转换为 int16_t 而非 uint16_t
- 量程配置不匹配 :驱动中 range_accel 值与寄存器0x1C实际写入值不一致,导致数值缩放错误

6.3 实战调试技巧

  • 寄存器快照工具 :在驱动中添加 dump_registers() 函数,循环读取0x00-0x75所有寄存器并打印,快速定位配置错误
  • 时间戳注入 :在 fetch_data() 开头添加 rt_tick_get_millisecond() ,验证采样周期是否符合预期
  • 数据流追踪 :在框架层 sensors/sensor.c rt_sensor_read() 函数中插入日志,观察从应用调用到驱动执行的完整路径
  • 压力测试 :使用 rt_thread_mdelay(1) 进行1000Hz高频采样,验证驱动在极限负载下的稳定性

我在实际项目中曾遇到MPU6050在高温环境下数据漂移的问题。通过在 fetch_data() 中增加温度补偿计算(读取片内温度传感器寄存器0x41-0x42),将陀螺仪零偏随温度变化的曲线拟合为线性函数,最终将漂移量从±5°/s降低至±0.5°/s。这印证了SENSOR框架的灵活性——只要遵循接口规范,即可在驱动层无缝集成复杂的算法逻辑。

Logo

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

更多推荐