一,任务通知(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) xQueueSendToBack xQueueSendToBackFromISR
xQueueSendToFront xQueueSendToFrontFromISR
xQueueReceive xQueueReceiveFromISR
xQueueOverwrite xQueueOverwriteFromISR
xQueuePeek xQueuePeekFromISR
信号量 (semaphore) xSemaphoreGive xSemaphoreGiveFromISR
xSemaphoreTake xSemaphoreTakeFromISR
事件组 (event group) xEventGroupSetBits xEventGroupSetBitsFromISR
xEventGroupGetBits xEventGroupGetBitsFromISR
任务通知 (task notification) xTaskNotifyGive vTaskNotifyGiveFromISR
xTaskNotify xTaskNotifyFromISR
软件定时器 (software timer) xTimerStart xTimerStartFromISR
xTimerStop xTimerStopFromISR
xTimerReset xTimerResetFromISR
xTimerChangePeriod xTimerChangePeriodFromISR

        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 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐