FreeRTOS核心机制工程实践:任务管理、事件同步与临界区设计
实时操作系统(RTOS)是嵌入式系统实现多任务并发与确定性响应的基础框架。其核心原理在于通过任务调度器、同步原语和中断管理机制,保障时间敏感操作的原子性与可预测性。FreeRTOS作为轻量级RTOS代表,以任务控制块(TCB)、软件定时器、事件标志组和临界区等关键技术,支撑工业控制、传感器采集、通信协议栈等典型应用场景。其中,动态任务创建与删除机制支持故障自恢复设计;事件标志组提供高效位图化多事件
1. FreeRTOS核心机制的工程化理解与实践
FreeRTOS作为嵌入式领域最广泛采用的实时操作系统之一,其价值不仅在于提供任务调度能力,更在于它构建了一套可预测、可裁剪、可验证的并发执行框架。在实际工程项目中,开发者若仅停留在 xTaskCreate 和 vTaskStartScheduler 的调用层面,极易陷入“能跑但不可控、能用但难维护”的困境。本节将从工程实现视角出发,系统梳理任务生命周期管理、软件定时器、事件标志组及临界区等关键机制的设计意图、底层原理与典型应用场景,摒弃概念堆砌,聚焦真实问题的解决路径。
1.1 任务的动态创建与删除:面向故障恢复的弹性设计
在裸机编程中,通信异常往往需要手动复位外设、重置状态机、重新初始化缓冲区,代码逻辑耦合度高,恢复路径长。而FreeRTOS提供的动态任务管理能力,为构建具备自我修复能力的系统提供了轻量级基础设施。
任务创建( xTaskCreate )与删除( vTaskDelete )的本质,是运行时对任务控制块(TCB)及栈空间的内存分配与回收。当一个通信任务因串口帧错误、DMA传输超时或协议解析失败而进入不可恢复状态时,直接销毁该任务并重建,比在原任务上下文中处理所有异常分支更为简洁可靠。例如,在基于USART2的Modbus RTU从机实现中,若连续三次接收到CRC校验失败的报文,可触发以下逻辑:
// 假设通信任务句柄为 xCommTaskHandle
if (ulFailedCount >= 3) {
vTaskDelete(xCommTaskHandle);
xCommTaskHandle = NULL;
// 等待调度器切换,确保旧任务完全退出
vTaskDelay(1);
// 重新创建任务,自动完成所有初始化
xTaskCreate(vCommTask, "COMM", configMINIMAL_STACK_SIZE * 4, NULL,
tskIDLE_PRIORITY + 3, &xCommTaskHandle);
}
此处的关键工程考量在于: 任务删除并非简单地“杀掉”一个线程,而是触发TCB内存释放与栈空间回收,并强制调度器重新评估就绪列表 。因此,在调用 vTaskDelete 后必须插入 vTaskDelay(1) 或使用同步机制(如信号量),以确保调度器完成上下文切换,避免对已释放TCB的非法访问。此外,栈大小配置需预留足够余量——通信任务需容纳协议解析栈、环形缓冲区指针、临时数据结构等, configMINIMAL_STACK_SIZE * 4 是经验下限,实际项目中常需根据编译器栈分析工具(如ARM GCC的 -fstack-usage )进行校准。
任务删除的代价在于内存碎片风险。FreeRTOS默认使用静态内存分配( configSUPPORT_DYNAMIC_ALLOCATION 设为0)时, pvPortMalloc / vPortFree 依赖于 heap_4.c 或 heap_5.c 实现。 heap_4.c 采用首次适配算法,频繁分配/释放不同大小的栈空间易导致碎片; heap_5.c 支持多段不连续内存池,更适合长期运行的工业设备。若项目对可靠性要求极高,建议禁用动态分配,改用 xTaskCreateStatic 配合预分配的TCB数组与栈数组,彻底规避内存管理不确定性。
1.2 任务挂起与恢复:状态机驱动的交互控制
挂起( vTaskSuspend )与恢复( xTaskResumeFromISR 或 vTaskResume )机制,是实现人机交互、流程暂停、资源独占等场景的底层支撑。其本质是修改任务状态为 eSuspended ,使其脱离就绪列表与延时列表,不再参与调度竞争,但保留全部上下文与TCB状态。
以机械臂运动控制为例,当操作员按下物理暂停键(连接至EXTI9_5中断线),中断服务程序(ISR)需以最小延迟响应。此时绝不能在ISR中执行复杂逻辑,而应通过 xTaskResumeFromISR 唤醒一个专用的“暂停管理任务”,由该任务完成后续动作:
// 暂停键中断服务函数
void EXTI9_5_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (__HAL_GPIO_EXTI_GET_FLAG(GPIO_PIN_5)) {
__HAL_GPIO_EXTI_CLEAR_FLAG(GPIO_PIN_5);
// 仅向暂停管理任务发送恢复信号
xTaskResumeFromISR(xPauseManagerTask);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
// 暂停管理任务主体
void vPauseManagerTask(void *pvParameters) {
for (;;) {
// 初始状态:等待被唤醒
vTaskSuspend(NULL); // 挂起自身,进入休眠
// 被唤醒后,执行暂停逻辑
vMotorStopAll(); // 停止所有电机驱动
vSetLEDStatus(LED_PAUSE); // 更新状态指示灯
// 进入等待解除状态
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) == GPIO_PIN_SET) {
// 检测解除键(PA6),持续轮询
vTaskDelay(10);
}
vSetLEDStatus(LED_RUNNING); // 恢复运行指示
// 重新启用运动控制任务
vTaskResume(xMotionControlTask);
}
}
此设计的关键在于职责分离: 中断服务程序只做最简化的信号传递,所有耗时操作移交至任务上下文执行 。 vTaskSuspend(NULL) 挂起当前任务自身,使其成为“被动等待者”; xTaskResumeFromISR 则在中断上下文中安全地将目标任务状态切回就绪态。需特别注意, vTaskSuspend / vTaskResume 是非阻塞操作,其执行时间恒定,符合硬实时中断响应要求。而轮询解除键的状态虽看似低效,实则是为避免引入额外中断源导致的优先级反转风险——在单核MCU上,若解除键也配置为中断,两个中断的嵌套与优先级协调将显著增加系统复杂度。
1.3 软件定时器:解耦时间敏感逻辑的虚拟计时单元
FreeRTOS软件定时器( TimerHandle_t )并非硬件外设的映射,而是由一个专用的定时器服务任务( prvTimerTask )统一管理的软件抽象。该任务以 configTIMER_TASK_PERIODICITY 为周期(通常设为1~10ms)扫描定时器列表,检查到期计数并触发回调。其核心优势在于: 将时间判断逻辑与应用任务解耦,避免每个任务都维护独立的 xTaskGetTickCount 比较逻辑,降低CPU负载与代码重复度 。
软件定时器分为一次性( pdTRUE )与周期性( pdFALSE )两类。在传感器数据采集场景中,常需以固定间隔(如200ms)读取温湿度传感器,并在数据有效时触发上报任务。若在采集任务中直接使用 vTaskDelay(200) ,则任务将完全阻塞,无法响应其他事件;而采用软件定时器,可实现非阻塞式调度:
// 定义定时器回调函数
void vTempReadCallback(TimerHandle_t xTimer) {
static uint8_t ucRetryCount = 0;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (HAL_I2C_Master_Transmit(&hi2c1, TEMP_SENSOR_ADDR<<1,
&ucCommand, 1, 100) == HAL_OK) {
if (HAL_I2C_Master_Receive(&hi2c1, TEMP_SENSOR_ADDR<<1,
aucData, 4, 100) == HAL_OK) {
// 数据有效,向上报任务发送通知
xQueueSendFromISR(xReportQueue, &aucData, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
} else {
// I2C通信失败,重试三次后停止定时器
if (++ucRetryCount >= 3) {
xTimerStop(xTempTimer, 0);
}
}
}
// 初始化阶段创建定时器
xTempTimer = xTimerCreate("TEMP_READ", pdMS_TO_TICKS(200),
pdFALSE, 0, vTempReadCallback);
if (xTempTimer != NULL) {
xTimerStart(xTempTimer, 0);
}
此处体现的工程原则是: 定时器回调函数必须短小精悍,严禁执行耗时操作(如浮点运算、大数组拷贝、阻塞式I/O) 。所有数据处理、协议封装、网络发送均应通过队列、信号量或事件组交由专用任务完成。回调中仅做原子性状态检查与轻量级通知。同时, xTimerStop 的调用需谨慎——若在回调中停止自身定时器,需确保 xTimerStop 的 xTicksToWait 参数设为0,否则可能因等待定时器服务任务处理而死锁。
1.4 事件标志组:多事件协同的位图化状态管理
事件标志组( EventGroupHandle_t )是FreeRTOS提供的高效同步原语,其底层是一个32位无符号整数( EventBits_t ),每一位代表一个独立事件。相比信号量(单一资源计数)与队列(数据传递),事件标志组的核心价值在于 支持“等待多个事件中的任意一个”( evWAIT_ANY )或“等待多个事件全部发生”( evWAIT_ALL )的复合条件,且无内存拷贝开销 。
在智能电表固件中,一个计量任务需同时监控三个异步事件:① 电压过压(由ADC比较器中断触发)、② 电流突变(由高速定时器捕获中断触发)、③ 通信模块接收新指令(由UART空闲中断触发)。传统方案需为每个事件创建独立队列,任务循环 xQueueReceive 并解析事件类型,逻辑冗长。而事件标志组可将三者映射为三个比特位:
#define EVENT_OVER_VOLTAGE (1UL << 0) // Bit 0
#define EVENT_CURRENT_SPIKE (1UL << 1) // Bit 1
#define EVENT_NEW_COMMAND (1UL << 2) // Bit 2
// 在ADC比较器中断中设置过压事件
void COMP1_IRQHandler(void) {
if (__HAL_COMP_COMP1_EXTI_GET_FLAG()) {
__HAL_COMP_COMP1_EXTI_CLEAR_FLAG();
xEventGroupSetBitsFromISR(xMeterEventGroup, EVENT_OVER_VOLTAGE, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
// 在计量任务中等待任意事件发生
void vMeteringTask(void *pvParameters) {
EventBits_t uxBits;
for (;;) {
// 等待任意一个事件,超时100ms
uxBits = xEventGroupWaitBits(xMeterEventGroup,
EVENT_OVER_VOLTAGE | EVENT_CURRENT_SPIKE | EVENT_NEW_COMMAND,
pdTRUE, // 清除已等待到的位
pdFALSE, // 等待任意一个
pdMS_TO_TICKS(100));
if ((uxBits & EVENT_OVER_VOLTAGE) != 0) {
vHandleOverVoltage();
}
if ((uxBits & EVENT_CURRENT_SPIKE) != 0) {
vHandleCurrentSpike();
}
if ((uxBits & EVENT_NEW_COMMAND) != 0) {
vProcessNewCommand();
}
}
}
xEventGroupWaitBits 的 xClearOnExit 参数设为 pdTRUE ,确保事件被消费后对应比特位自动清零,避免重复处理。而 xEventGroupSetBitsFromISR 在中断中安全地设置比特位,其内部通过 portSET_INTERRUPT_MASK_FROM_ISR / portCLEAR_INTERRUPT_MASK_FROM_ISR 实现临界区保护,无需开发者手动关中断。这种位图化设计,使事件处理逻辑高度内聚,状态转换清晰可溯,极大提升了多源异步事件系统的可维护性。
2. FreeRTOS工程文件结构与CMSIS-RTOS V2接口规范
在STM32CubeMX生成的FreeRTOS工程中,文件组织并非随意堆砌,而是严格遵循CMSIS-RTOS V2标准定义的分层架构。理解这一结构,是进行跨平台移植、组件复用及深度调试的前提。
2.1 标准化文件布局及其工程意义
CubeMX自动生成的FreeRTOS相关文件位于 Core/Inc 与 Core/Src 目录下,典型结构如下:
| 文件路径 | 作用 | 工程实践要点 |
|---|---|---|
freertos.c |
FreeRTOS内核配置入口,包含 MX_FREERTOS_Init() 函数 |
此处初始化所有用户定义的任务、队列、信号量、事件组等,是系统启动的“总控台”。务必确保初始化顺序符合依赖关系(如队列需在使用前创建) |
freertos.h |
FreeRTOS配置宏定义头文件,包含 FreeRTOSConfig.h 的包含与自定义宏声明 |
FreeRTOSConfig.h 是FreeRTOS的“宪法”,其中 configUSE_TIMERS 、 configUSE_EVENT_GROUPS 等宏决定内核功能裁剪。修改后必须重新编译整个内核 |
cmsis_os.h |
CMSIS-RTOS V2 API头文件,提供标准化的 osThreadNew 、 osEventFlagsNew 等接口 |
强烈建议在新项目中优先使用CMSIS-RTOS V2接口而非原生FreeRTOS API 。其好处在于:① 接口命名统一,降低学习成本;② 为未来切换至其他RTOS(如RT-Thread、Zephyr)预留平滑迁移路径;③ CubeMX GUI配置直接生成CMSIS-RTOS V2代码,减少手动编码错误 |
cmsis_os.c |
CMSIS-RTOS V2接口的FreeRTOS后端实现,将标准调用转译为FreeRTOS原生API | 此文件由CubeMX自动生成,开发者通常无需修改。但需知晓其存在——当调试 osThreadNew 失败时,问题可能源于 cmsis_os.c 中对 xTaskCreate 返回值的错误处理 |
freertos.c 中的 MX_FREERTOS_Init() 函数是工程启动的枢纽。其内部调用顺序隐含了严格的依赖链:先创建互斥锁(用于保护共享资源),再创建消息队列(用于任务间通信),最后创建任务(依赖前述同步原语)。若在任务函数中提前使用未初始化的队列句柄,将导致HardFault。因此,在编写任务函数时,必须假定所有依赖的同步对象均已由 MX_FREERTOS_Init() 完成初始化。
2.2 CMSIS-RTOS V2:面向可移植性的抽象层
CMSIS-RTOS V2规范由ARM提出,旨在为不同RTOS供应商提供统一的应用编程接口(API)。其核心思想是: 将操作系统内核视为一个可插拔的“设备驱动”,上层应用代码仅与标准接口交互,不感知底层实现细节 。
以创建一个优先级为4、栈大小为512字节的任务为例,原生FreeRTOS写法与CMSIS-RTOS V2写法对比:
// 原生FreeRTOS(绑定特定内核)
TaskHandle_t xTaskHandle;
xTaskCreate(vUserTask, "USER", 512, NULL, 4, &xTaskHandle);
// CMSIS-RTOS V2(抽象层,可移植)
osThreadAttr_t attr = {
.name = "USER",
.stack_size = 512,
.priority = (osPriority_t) osPriorityNormal4
};
osThreadId_t tid = osThreadNew(vUserTask, NULL, &attr);
二者在FreeRTOS环境下功能等价,但CMSIS-RTOS V2的优势在跨平台时凸显。假设项目后期需迁移到RT-Thread,仅需更换 cmsis_os.c 的实现文件(由RT-Thread提供),并链接其内核库,所有 osThreadNew 、 osMessageQueuePut 等调用无需修改一行代码。这种“接口与实现分离”的设计,正是大型嵌入式项目保障长期可维护性的基石。
值得注意的是,CMSIS-RTOS V2并非对FreeRTOS的简单封装,而是对其功能的子集标准化。例如,FreeRTOS原生支持的 xTaskNotify (任务通知)机制,在CMSIS-RTOS V2中无直接对应API,需通过事件标志组或信号量模拟。因此,在选择接口时,需权衡标准化收益与功能完备性——对于新项目、团队协作或有明确移植计划的项目,CMSIS-RTOS V2是首选;而对于极致性能要求或深度定制场景,原生API仍具价值。
3. 临界区:保障原子操作的硬件级防护
在多任务与中断共存的环境中,“读-改-写”类操作(如全局变量自增、寄存器位操作)若未加防护,极易因上下文切换或中断嵌套导致数据损坏。FreeRTOS提供的临界区(Critical Section)机制,是确保此类操作原子性的终极手段,其底层直接操控CPU的中断使能状态。
3.1 任务临界区与中断临界区的本质区别
FreeRTOS定义了两种临界区进入/退出宏:
-
任务临界区 :
taskENTER_CRITICAL()/taskEXIT_CRITICAL()
底层调用vTaskSuspendAll()/xTaskResumeAll(),暂停调度器,禁止任务切换,但 允许中断发生 。适用于保护仅被任务上下文访问的共享数据,如任务间传递的环形缓冲区指针。 -
中断临界区 :
portENTER_CRITICAL()/portEXIT_CRITICAL()
底层直接执行__disable_irq()/__enable_irq()(Cortex-M系列), 完全关闭所有可屏蔽中断 。适用于保护被中断服务程序与任务共同访问的资源,如ADC转换完成标志、定时器捕获值。
二者选择的关键在于 访问资源的上下文范围 。以一个典型的ADC采样结果处理为例:
// 全局变量:ADC转换完成标志与结果
volatile uint32_t ulADCResult = 0;
volatile uint8_t ucADCReady = 0;
// ADC转换完成中断服务程序
void ADC1_IRQHandler(void) {
if (__HAL_ADC_GET_FLAG(&hadc1, ADC_FLAG_EOC)) {
__HAL_ADC_CLEAR_FLAG(&hadc1, ADC_FLAG_EOC);
ulADCResult = HAL_ADC_GetValue(&hadc1);
// 此处更新标志,必须保证原子性——中断与任务均可访问
portENTER_CRITICAL(); // 关中断,防止任务在此刻读取
ucADCReady = 1;
portEXIT_CRITICAL();
}
}
// 任务中读取ADC结果
void vADCTask(void *pvParameters) {
for (;;) {
// 等待ADC就绪
if (ucADCReady) {
// 读取结果,同样需原子性
portENTER_CRITICAL();
uint32_t ulLocalResult = ulADCResult;
ucADCReady = 0; // 清除就绪标志
portEXIT_CRITICAL();
vProcessADCValue(ulLocalResult);
}
vTaskDelay(10);
}
}
若在中断中使用 taskENTER_CRITICAL() ,则仅暂停调度器,ADC中断仍可被更高优先级中断抢占, ucADCReady = 1 的赋值操作可能被截断,导致任务读取到中间状态。唯有 portENTER_CRITICAL() 能确保从 __HAL_ADC_CLEAR_FLAG 到 ucADCReady = 1 的整个序列不被任何中断打断。
3.2 临界区使用的黄金法则与常见陷阱
临界区虽强大,但滥用将严重损害系统实时性。遵循以下法则可规避绝大多数问题:
-
范围最小化 :临界区代码必须精炼,仅包含绝对必要的操作。禁止在其中调用
printf、malloc、HAL_Delay等耗时或可能阻塞的函数。上述ADC示例中,portENTER_CRITICAL()包裹的仅为单条赋值语句,执行时间在纳秒级。 -
禁止嵌套失配 :
portENTER_CRITICAL()与portEXIT_CRITICAL()必须成对出现,且在同一函数内匹配。编译器无法检查此错误,失配将导致中断永久关闭,系统僵死。CubeMX生成的代码中,所有临界区调用均严格配对,手动添加时务必使用编辑器括号高亮功能辅助检查。 -
中断优先级约束 :Cortex-M内核的BASEPRI寄存器用于屏蔽低于指定优先级的中断。FreeRTOS要求所有RTOS内核中断(如SysTick、PendSV、SVCall)的优先级必须高于
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(通常设为5)。这意味着,若将某个外设中断(如USART1)优先级设为4,则其ISR内调用xQueueSendFromISR是安全的;若设为6,则可能破坏RTOS调度器,引发不可预知行为。此配置在FreeRTOSConfig.h中通过configLIBRARY_LOWEST_INTERRUPT_PRIORITY与configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY宏精确控制。 -
替代方案优先 :对于简单的计数器或状态标志,优先考虑
atomic_t(GCC内置原子操作)或硬件支持的位带(Bit-Band)区域。例如,STM32F4/F7系列的SRAM与外设寄存器均支持位带别名区,对GPIOA->ODR的某一位操作可编译为单条STR指令,天然原子,无需临界区。
4. 工程实践中的认知升级与工具链演进
嵌入式开发已告别“手写寄存器+查手册”的原始阶段。现代高效开发,本质上是 对工具链能力的深度认知与主动驾驭 。FreeRTOS的学习曲线,不应止步于API调用,而应延伸至工具如何重塑开发范式。
4.1 CubeMX:从配置生成器到系统架构师助手
STM32CubeMX远不止于引脚配置与时钟树生成。其FreeRTOS配置页面(Middleware → FreeRTOS)实质上是一个可视化的系统架构设计工具:
-
任务属性可视化 :可直观设置每个任务的名称、优先级、栈大小、入口函数,并自动生成
xTaskCreate调用。更重要的是,它强制开发者在编码前思考任务的 职责边界与资源需求 ——栈大小的滑块不是随意拖动,而是需结合函数调用深度、局部变量大小、中断嵌套层数进行预估。 -
中间件集成自动化 :勾选“CMSIS-RTOS V2”后,CubeMX自动生成完整的
cmsis_os.h/cmsis_os.c,并配置好所有依赖的内核宏。这消除了手动配置FreeRTOSConfig.h时常见的宏冲突(如configUSE_MUTEXES与configUSE_RECURSIVE_MUTEXES的依赖关系),让开发者聚焦于业务逻辑。 -
代码差异对比 :当修改FreeRTOS配置(如新增一个队列)后,CubeMX的“Project Manager”页签会高亮显示哪些文件将被覆盖。点击“Generate Code”前,开发者可预览
freertos.c中新增的xQueueCreate行,确认其参数符合预期。这种“所见即所得”的反馈,大幅降低了配置错误率。
4.2 AI辅助开发:从概念澄清到代码生成的范式转移
AI工具(如Copilot、CodeWhisperer)在嵌入式领域的价值,已超越简单的代码补全。其核心赋能在于:
-
概念速查与类比解释 :当遇到“事件标志组与消息队列的区别”这类抽象问题时,AI能即时给出贴近工程场景的对比表格(如“事件标志组适合状态通知,消息队列适合数据传递”),其解释效率远超翻阅数百页官方文档。
-
错误诊断加速 :将编译错误信息(如
undefined reference to 'vPortSVCHandler')连同startup_stm32f407xx.s片段提交给AI,可快速定位到FreeRTOSConfig.h中configUSE_MPU_WRAPPERS宏的误开启,省去数小时的二分排查。 -
模板代码生成 :输入自然语言需求:“为SPI Flash编写一个线程安全的擦除函数,使用FreeRTOS互斥锁保护”,AI可输出包含
xSemaphoreTake/xSemaphoreGive、错误处理、超时机制的完整C函数框架,开发者只需填充具体的SPI驱动调用。
然而,AI输出必须经受“工程师的二次验证”。例如,AI生成的定时器回调函数若包含 vTaskDelay ,则必须手动修正为 xTimerChangePeriod 或通过队列通知任务;AI建议的临界区范围若包含 HAL_UART_Transmit ,则需立即否决——这些是AI无法理解的实时性硬约束。 AI是超级搜索引擎与代码草稿机,而最终的工程决策权,永远属于手握示波器与逻辑分析仪的工程师 。
在江科大老学长的实践中,AI已成为其教程文案构思、概念图解生成、甚至初版驱动框架搭建的得力助手。但这绝不意味着可以放弃对 xTaskCreate 参数含义的深究,或对 portENTER_CRITICAL() 底层汇编的敬畏。技术工具的进化,最终服务于工程师对系统本质理解的深化,而非替代这一过程。
我曾在开发一款工业网关时,为解决CAN总线在高负载下的丢帧问题,反复调整FreeRTOS任务优先级与CAN接收中断优先级,却收效甚微。直到借助CubeMX的“System Configuration”视图,发现SysTick中断(RTOS心跳)与CAN RX中断被错误地配置为同一优先级组,导致抢占失效。一个工具的可视化洞察,胜过三天的手动寄存器调试。这印证了一个朴素真理:真正的嵌入式高手,既精通底层硬件,也善用顶层工具,二者缺一不可。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)