一,任务通知(Task Notifications)

        使用队列、信号量、事件组等方法时,并不知道对方是谁。使用任务通知时,可以明确指定 -通知哪个任务。

        队列、信号量、事件组需要事先创建对应的结构体,双方通过中间的结构体通信:

   任务通知可直接通知对方(前提是知道对方)

        任务通知中,任务结构体TCB中就包含了内部对象,可直接接收别人发来的“通知”

        涉及内容:通知状态、通知值、使用场合,任务通知的优势

1,特性

        优势:发送事件、数据的效率更高;无需额外创建结构体,更节省内存

        限制:不能发送数据给 ISR,数据只能该任务独享,无法缓冲数据,无法广播给多任务,发送受阻时发送方无法进入阻塞状态等待

通知状态和通知值:

    每个任务都有一个结构体 TCB(Task Control Block),里面有 2 个成员:

  • 一个是 uint8_t 类型,用来表示通知状态
  • 一个是 uint32_t 类型,用来表示通知值
typedef struct tskTaskControlBlock
{
    ...... /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */
    volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    ...... 
} tskTCB;

    通知状态有3种取值:

  • taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
  • taskWAITING_NOTIFICATION:任务在等待通知
  • taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为 pending(待处理)
##define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) /* 也是初始状态 */
##define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 )
##define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 )

    通知值可以有多种类型:计数值、位(类似事件组)、任意数值

2,使用

        任务通知可以实现轻量级的队列(长度为 1)、邮箱(覆盖的队列)、计数型信号量、 二进制信号量、事件组等。

(1)两类函数

        任务通知有两套函数:简化版(使用简单,实际上也由专业版函数实现),专业版(支持很多参数,可实现很多功能)

   【简化版】

    Give 可以给其他任务发送通知:

  • 使得通知值加一
  • 并使通知状态变为“pending”(表示有数据 待处理),即 taskNOTIFICATION_RECEIVED

    Take 可以取出通知值

  • 如果通知 等于0,则阻塞(可指定超时时间)
  • 当通知值 大于0,任务从阻塞态进入就绪态
  • 当 ulTaskNotifyTake 返回之前,还可以做清理工作(通知值减一 或 清零)
使用 ulTaskNotifyTake 函数可以实现轻量级的、高效的二进制信号量、计数型信号量。
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, 
                             BaseType_t   *pxHigherPriorityTaskWoken );

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );

  【专业版】(详细说明见手册15章)

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, 
                        uint32_t ulValue, 
                        eNotifyAction eAction );

BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify, 
                               uint32_t ulValue, 
                               eNotifyAction eAction, 
                               BaseType_t *pxHigherPriorityTaskWoken );

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, 
                            uint32_t ulBitsToClearOnExit, 
                            uint32_t *pulNotificationValue, 
                            TickType_t xTicksToWait );

3,基本操作

         示例:car1运行到终点后,给car2发送轻量级信号量,给car3发送数值。car2等待轻量级信号量, car3等待特定的通知值。(car1用任务通知car2/3,car2任务用xTaskNotifyGive函数通知,car3用xTaskNotify)

        被通知的任务,需要在创建任务时记录任务句柄

40 static TaskHandle_t g_TaskHandleCar2;
41 static TaskHandle_t g_TaskHandleCar3;
/* 省略 */
315 xTaskCreate(Car1Task, "car1", 128, &g_cars[0], osPriorityNormal, NULL);
316 xTaskCreate(Car2Task, "car2", 128, &g_cars[1], osPriorityNormal+2, &g_TaskHandleCar2);
317 xTaskCreate(Car3Task, "car3", 128, &g_cars[2], osPriorityNormal+2, &g_TaskHandleCar3);

        car2 等待轻量级信号量

176 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

        car3 等待通知值位 100

224 uint32_t val;
/* 省略 */
241 do
242 {
243     xTaskNotifyWait(~0, ~0, &val, portMAX_DELAY);
244 } while (val != 100);

        car1 到达终点后,向 car2, car3 发出任务通知

            /* 发出任务通知给car2,car3 */
146 xTaskNotifyGive(g_TaskHandleCar2);
147
148 xTaskNotify(g_TaskHandleCar3, 100,eSetValueWithOverwrite);

二,软件定时器(software timer)

    【定时器的本质是结构体(flag, period, func, params, 链表项(处理多个定时器))】

    软件定时器类似于 “闹钟”,可以完成两类事情:

  • 在 “未来” 某个时间点,运行函数
  • 周期性地运行函数

    FreeRTOS里,可以设很多“软件定时器”,它们基于 系统滴答中断(Tick Interrupt)

    涉及内容:特性,DaemonTask,定时器命令队列,一次性/周期性 定时器差别,定时器操作(创建、启动、复位、修改周期)

1,特性

    添加闹钟时,需要指定时间、指定类型(一次性 or 周期性)、指定做什么事、还有是否有效

  • 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)
  • 指定类型:一次性 One-shot timers(可手工再次启用);自动加载定时器 Auto-reload timers(周期性地调用)
  • 指定做什么事:即回调函数
  • 是否有效:运行(Running、Active),冬眠(Dormant)  (区别只在于是否调用回调函数)

示例:

        Timer1:它是一次性的定时器,在 t1 启动,周期是 6 Tick。经过 6 tick 后,在 t7 执行回调函数。它的回调函数只会被执行一次,然后该定时器进入 冬眠 状态。

        Timer2:它是自动加载的定时器,在 t1 启动,周期是 5 Tick。每经过 5 tick 它的回调函数都被执行,比如在 t6t11t16 都会执行。

2,上下文

(1)守护任务

        软件定时器基于 Tick 来运行,但是 不在 Tick 中断中执行回调函数。        

        FreeRTOS 是 RTOS,它不允许在内核、在中断中执行不确定的代码:如果定时器函数
很耗时,会影响整个系统。而在 RTOS Damemon Task,RTOS守护任务 中执行。(需要FreeRTOS的配置项configUSE_TIMERS为1,则启动调度器时自动创建守护任务 优先级为 configTIMER_TASK_PRIORITY,定时器命令队列的长度为configTIMER_QUEUE_LENGTH
(2)守护任务的调度
        守护任务的调度和普通任务没有差别,当它是当前优先级最高的就绪态任务时,就可以运行。它的工作有两类
  • 处理命令:从命令队列中取出命令 并处理
  • 执行定时器的回调函数

        能否及时处理定时器的命令、回调函数,严重依赖于守护任务的优先级

        下面是优先级较低和较高的不同场景案例:

        注意,定时器的超时时间是基于调用xTimerStart()的时刻tX(本例中的 t2),而不是基于守护任务处 理命令的时刻tY
 
常见处理:把定时器任务设置到足够高 或者 其它任务用事件驱动的方式运行中会阻塞

(CubeMX可配置TimerTask的中断优先级)

(3)回调函数

void ATimerCallback( TimerHandle_t xTimer );

        定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,还要处理其他定时器。【定时器的回调函数不要运行其他人】

  • 要尽快执行,不能阻塞
  • 不要调用会阻塞的 API 函数,如 vTaskDelay()
  • 可以调用xQueueReceive()之类的函数,但是超时时间要设为 0:即刻返回, 不可阻塞

3,函数

        根据定时器的状态转换图,就可以知道所涉及的函数:

(1)创建:动态分配内存、静态分配内存

/* 使用动态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以 Tick 为单位
* uxAutoReload: 类型, pdTRUE 表示自动加载, pdFALSE 表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* 返回值: 成功则返回 TimerHandle_t, 否则返回 NULL
*/
TimerHandle_t xTimerCreate( const char * const pcTimerName, 
                            const TickType_t xTimerPeriodInTicks, 
                            const UBaseType_t uxAutoReload, 
                            void * const pvTimerID, 
                            TimerCallbackFunction_t pxCallbackFunction );

/* 使用静态分配内存的方法创建定时器
* pcTimerName:定时器名字, 用处不大, 尽在调试时用到
* xTimerPeriodInTicks: 周期, 以 Tick 为单位
* uxAutoReload: 类型, pdTRUE 表示自动加载, pdFALSE 表示一次性
* pvTimerID: 回调函数可以使用此参数, 比如分辨是哪个定时器
* pxCallbackFunction: 回调函数
* pxTimerBuffer: 传入一个 StaticTimer_t 结构体, 将在上面构造定时器
* 返回值: 成功则返回 TimerHandle_t, 否则返回 NULL
*/
TimerHandle_t xTimerCreateStatic(const char * const pcTimerName, 
                                 TickType_t xTimerPeriodInTicks, 
                                 UBaseType_t uxAutoReload, 
                                 void * pvTimerID, 
                                 TimerCallbackFunction_t pxCallbackFunction, 
                                 StaticTimer_t *pxTimerBuffer );

        回调函数的类型是:

    void ATimerCallback( TimerHandle_t xTimer ); //通过传入的句柄区分是哪个定时器触发的回调

typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer ); 
         // 给“特定函数指针”起别名TimerCallbackFunction_t,而不用重复写void (*)(TimerHandle_t)

(2)删除:动态分配的定时器,不再需要时可以删除掉以回收内存

/* 删除定时器
* xTimer: 要删除哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL 表示"删除命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );

        定时器的很多 API 函数,都是通过发送"命令"到命令队列,由守护任务来实现。(队列满时"命令"就无法即刻写入队列,可以指定一个超时时间xTicksToWait

(3)启动 / 停止

/* 启动定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL 表示"启动命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );

/* 启动定时器(ISR 版本) 
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, 
* 如果守护任务的优先级比当前任务的高, 
* 则"*pxHigherPriorityTaskWoken = pdTRUE", 
* 表示需要进行任务调度
* 返回值: pdFAIL 表示"启动命令"无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer, 
                               BaseType_t *pxHigherPriorityTaskWoken );
/* 停止定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL 表示"停止命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );

/* 停止定时器(ISR 版本) * xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, 
* 如果守护任务的优先级比当前任务的高, 
* 则"*pxHigherPriorityTaskWoken = pdTRUE", 
* 表示需要进行任务调度
* 返回值: pdFAIL 表示"停止命令"无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer, 
                              BaseType_t *pxHigherPriorityTaskWoken );

注意:

  • xTicksToWait不是定时器本身的超时时间,不是定时器本身的"周期"
  • 创建定时器时,设置了它的周期(period)xTimerStart()函数是用来启动定时器。假设 调用xTimerStart()的时刻是 tX ,定时器的周期是 n ,那么在 tX+n 时刻定时器的回调函数被调用。
  • 如果定时器已经被启动,但是它的函数尚未被执行,再次执行xTimerStart()函数相当 于执行xTimerReset(),重新设定它的启动时间。

(4)复位:

  • 使用 xTimerReset()函数可以让定时器的状态从冬眠 态转换为运行态,相当于使用 xTimerStart()函数。
  • 如果定时器已经处于运行态,使用xTimerReset()函数就相当于重新确定超时时间。
/* 复位定时器
* xTimer: 哪个定时器
* xTicksToWait: 超时时间
* 返回值: pdFAIL 表示"复位命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );

/* 复位定时器(ISR 版本)
* xTimer: 哪个定时器
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, 
* 如果守护任务的优先级比当前任务的高,
* 则"*pxHigherPriorityTaskWoken = pdTRUE", 
* 表示需要进行任务调度
* 返回值: pdFAIL 表示"停止命令"无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer, 
                               BaseType_t *pxHigherPriorityTaskWoken );

(5)修改周期:使用 xTimerChangePeriod()函数,处理能修改它 的周期外,还可以让定时器的状态从冬眠态转换为运行态。(假 设 调 用 xTimerChangePeriod() 函数的时间tX,新的周期是n,则tX+n就是新的超时时间。)

/* 修改定时器的周期
* xTimer: 哪个定时器
* xNewPeriod: 新周期
* xTicksToWait: 超时时间, 命令写入队列的超时时间
* 返回值: pdFAIL 表示"修改周期命令"在 xTicksToWait 个 Tick 内无法写入队列
* pdPASS 表示成功
*/
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, 
                               TickType_t xNewPeriod, 
                               TickType_t xTicksToWait );

/* 修改定时器的周期
* xTimer: 哪个定时器
* xNewPeriod: 新周期
* pxHigherPriorityTaskWoken: 向队列发出命令使得守护任务被唤醒, 
* 如果守护任务的优先级比当前任务的高, 
* 则"*pxHigherPriorityTaskWoken = pdTRUE", 
* 表示需要进行任务调度
* @return: pdPASS - 修改周期的命令成功写入定时器命令队列;
 *         pdFAIL - 在定时器命令队列满的情况下,命令无法写入(ISR 版本无等待时间,立即返回)
 */
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer, 
                                      TickType_t xNewPeriod, 
                                      BaseType_t *pxHigherPriorityTaskWoken );

(6)定时器 ID:定时器结构体中有一项 pvTimerID

    怎么使用定时器ID,完全由程序来决定:

  • 可以用来标记定时器,表示自己是什么定时器
  • 可以用来保存参数,给回调函数使用
    其初始值在创建定时器时由xTimerCreate()这类函数传入,后续可以使用这些函数 来操作:
  • 更新 ID:使用 vTimerSetTimerID() 函数
  • 查询 ID:查询 pvTimerGetTimerID() 函数

   【这两个函数不涉及命令队列,它们是直接操作定时器结构体。

/* 获得定时器的 ID
* xTimer: 哪个定时器
* 返回值: 定时器的 ID
*/
void *pvTimerGetTimerID( TimerHandle_t xTimer );

/* 设置定时器的 ID
* xTimer: 哪个定时器
* pvNewID: 新 ID
* 返回值: 无
*/
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID )

4,示例:实现游戏音效

        在game1游戏中,什么时 候发出声音?球与挡球板、转块碰撞时发出声音。什么时候停止声音?发出声音后,过一阵子就应该停止声音。通过使用软件定时器来实现。

  • 挡球板任务,参考音乐任务了解蜂鸣器设置(初始化后 修改频率发声)
  • 创建beep.c实现蜂鸣器操作,不同功能操作中对应不同buzzer_buzz(频率, 时间)
  • 启动PWM发出声音,启动定时器;定时器的时间到后,停止PWM以静音

        详见手册 16.4

三,中断管理 (Interrupt Management)

   中断的处理流程如下:

  • CPU 跳转到固定地址(中断向量)去执行代码,跳转由硬件实现。
  • 执行代码完成:保存现场 (各类寄存器值等),分辨中断、调用 ISR处理函数 (interrupt service routine),恢复现场 (并继续运行任务 或 运行优先级更高的任务)

ISR在内核中被调用,要尽量快(ISR 执行过程中用户的任务无法执行),否则:

  • 其他低优先级的中断无法被处理:实时性无法保证
  • 用户任务无法被执行:系统显得卡顿
  • 不利于中断嵌套

    对于复杂的中断处理,要分为两部分

  • ISR:尽快做些清理、记录工作,然后触发任务
  • 任务:处理复杂的事件【需要 ISR 与 任务 间的通信】

【 FreeRTOS 中用中断的原则:】

  • 把任务认为是硬件无关的,任务优先级由程序员决定(运行由调度器)
  • ISR 用软件实现的,但被认为是硬件特性(跟硬件密切相关)
  • ISR 的优先级高于任务(任务只在没有中断的情况下,才执行)

1,两套 API 函数

        任务函数中,可以调用各类 API 函数,但是在 ISR 中直接使用会导致问题,所以另外引入了 ISR 中使用的 API 函数:

很多 API 函数会导致任务阻塞 (如写队列时,队列已满,阻塞等待)

ISR 不能进入阻塞状态

BaseType_t xQueueSend(...)
{
    if (is_in_isr())
    {
        /* 把数据放入队列 */

        /* 不管是否成功都直接返回 */
    }
    else /* 在任务中 */
    {
        /* 把数据放入队列 */

        /* 不成功就等待一会再重试 */
    }
}
FreeRTOS 使用两套函数,而不是使用一套函数的原因:
  • 使用同一套函数的话,需要增加额外的判断代码、增加额外的分支
  • 在任务、ISR 中调用时,需要的参数不一样
  • 移植 FreeRTOS 时,还需要提供监测上下文的函数
  • 有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于 ISR

        使用两套函数可以让程序更高效,但是在用第三方库时有麻烦(如ISR中调用库函数)可用以下方法解决:
  • 把中断的处理推迟到任务中进行(Defer interrupt processing)
  • 尝试在库函数中使用"FromISR"函数【在任务中、在 ISR 中都可以调用"FromISR"函数
  • 第三方库函数也许会提供 OS 抽象层【判断 任务 / ISR】
类型在任务中在 ISR 中
队列 (queue)xQueueSendToBackxQueueSendToBackFromISR
xQueueSendToFrontxQueueSendToFrontFromISR
xQueueReceivexQueueReceiveFromISR
xQueueOverwritexQueueOverwriteFromISR
xQueuePeekxQueuePeekFromISR
信号量 (semaphore)xSemaphoreGivexSemaphoreGiveFromISR
xSemaphoreTakexSemaphoreTakeFromISR
事件组 (event group)xEventGroupSetBitsxEventGroupSetBitsFromISR
xEventGroupGetBitsxEventGroupGetBitsFromISR
任务通知 (task notification)xTaskNotifyGivevTaskNotifyGiveFromISR
xTaskNotifyxTaskNotifyFromISR
软件定时器 (software timer)xTimerStartxTimerStartFromISR
xTimerStopxTimerStopFromISR
xTimerResetxTimerResetFromISR
xTimerChangePeriodxTimerChangePeriodFromISR

        xHigherPriorityTaskWoken 参数:是否由更高优先级的任务被唤醒。(若为 pdTRUE,则意味着后面要进行任务切换;不想用时此参数可设为 NULL ) 

/* 用法示例 */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;

/* 可被多次调用(多次切换为TRUE, 且不影响效率) */
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);

/* 最后再决定是否进行任务切换 */
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
        在任务中调用 API函数可能导致任务阻塞、任务切换,这叫做"context switch",上下文切换。这个函数可能很长时间才返回,在函数内部实现任务切换。
        在 ISR 中的函数也可能导致任务切换,但不在函数内部进行,而是返回一个参数表示是否切换。(切换涉及到寄存器修改,耗时的操作不在中断中进行)
【提高 ISR 内运行效率、可控性强、可移植性好、Tick 中断中调用 vApplicationTickHook() 只能用"FromISR"的函数】

【切换任务】

    用两个宏进行任务切换:    ( 这两个宏做的事情是完全一样的 )

portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
或
portYIELD_FROM_ISR( xHigherPriorityTaskWoken )'
void XXX_ISR()
{
    int i;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    for (i = 0; i < N; i++)
    {
        xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
    }

    /* 最后再决定是否进行任务切换
    * xHigherPriorityTaskWoken 为 pdTRUE 时才切换
    */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

2,中断的延迟处理

        中断的延迟处理 可防止处理中断太过耗时的问题(ISR 尽快做清理、记录然后触发任务,任务中处理更复杂的事情)。

3,中断与任务间的通信

        即前面讲解过的 队列、信号量、互斥量、事件组、任务通知 等等方法。要注意 ISR 中用的函数要有“FromISR”的后缀。

4,示例:优化实时性

static void DispatchKey(struct ir_data *pidata)
{
#if 0
    extern QueueHandle_t g_xQueueCar1;
    extern QueueHandle_t g_xQueueCar2;
    extern QueueHandle_t g_xQueueCar3;
    xQueueSendFromISR(g_xQueueCar1, pidata, NULL);
    xQueueSendFromISR(g_xQueueCar2, pidata, NULL);
    xQueueSendFromISR(g_xQueueCar3, pidata, NULL);
#else
    int i;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    for (i = 0; i < g_queue_cnt; i++)
    {
        xQueueSendFromISR(g_xQueues[i], pidata, &xHigherPriorityTaskWoken);
    }
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
#endif
}

        此场景是想要优先级较低的 任务A 在运行过程中发生中断,中断中调用 DispatchKey 函数写了队列,使得任务B 被唤醒(优先级较高,中断后马上运行)。 &xHigherPriorityTaskWoken 若为 NULL,则会把任务B 调整为就绪态,但不会发起一次调度

        上述代码通过把 &xHigherPriorityTaskWoken 设为 pdTRUE,发起一次调度,让任务B 被唤醒。(修改后感觉不出,时间差异太快了,对高精度设备很有用)

        不发起调度的问题:直到 tick 中断后才切换,不符合 rtos 的定义

Logo

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

更多推荐