1. 物联网协议栈工程移植的核心逻辑与实践路径

在嵌入式物联网产品开发中,将第三方协议栈(如机智云Gizwits)集成进自有STM32工程,绝非简单的文件拷贝。其本质是一次系统级的接口适配与资源重映射过程——需要在保持协议栈业务逻辑完整性的同时,将其底层通信、时序、复位等硬件依赖项,精准嫁接到目标MCU的外设驱动模型上。本节以STM32F103系列为基准平台,系统阐述从协议栈源码解包到功能闭环的完整工程化路径,所有操作均基于HAL库标准框架,不引入任何非官方抽象层。

1.1 协议栈工程结构解析:识别可移植与不可修改模块

机智云MCU SDK生成的工程包(以 gizwits_product.c/h 为核心)采用分层设计,其文件组织严格遵循“协议逻辑”与“硬件抽象”分离原则。理解各模块职责边界,是避免移植后功能异常的前提。

文件/目录 类型 核心职责 移植要求
gizwits_product.c/h 协议核心 实现数据点(DataPoint)状态机、事件回调注册、云端指令解析、本地控制逻辑封装 必须移植,但禁止修改内部逻辑
gizwits_protocol.c/h 协议封装 完成GAgent通信协议(含包头、校验、压缩)的序列化/反序列化,提供 gizwits_uartWrite() 等API 必须移植,禁止修改协议格式
gizwits_wifi.c/h 硬件适配 提供WiFi模块串口收发、复位、状态查询等底层函数, 此处为移植关键区 必须重写,替换为STM32驱动
ring_buffer.c/h 公共组件 环形缓冲区实现,用于暂存WiFi模块突发下发的多帧数据 直接移植,无需修改
datapoint.c/h 工具函数 数据点压缩/解压、CRC校验等算法实现 直接移植,无需修改

需特别注意: gizwits_product.c 中定义的 dataPoint_t 结构体与 gizwits_product.h 中声明的数据点枚举( DPID_xxx ),与机智云平台创建产品时配置的数据点严格一一对应。若平台端增删数据点,必须同步更新此结构体及枚举定义,否则将导致协议解析失败或数据错位。此为工程一致性铁律,无例外。

1.2 工程目录重构:建立清晰的硬件-协议隔离层

在自有STM32工程(以STM32CubeMX生成的HAL工程为例)中,严禁将协议栈文件混入 Src/Inc 根目录。必须构建物理隔离的目录结构,明确划分职责:

Project/
├── Core/
│   ├── Inc/
│   └── Src/
├── Drivers/
│   ├── STM32F1xx_HAL_Driver/
│   └── BSP/          // 板级支持包
├── Middleware/       // 中间件层(协议栈专属)
│   └── Gizwits/
│       ├── Inc/      // gizwits_*.h, ring_buffer.h, datapoint.h
│       └── Src/      // gizwits_*.c, ring_buffer.c, datapoint.c
├── User/             // 应用层(开发者主战场)
│   ├── Inc/
│   │   └── user_app.h  // 声明应用层回调函数原型
│   └── Src/
│       ├── main.c
│       ├── user_app.c  // 实现gizwitsEventCallback()等回调
│       └── wifi_hal.c  // **关键!** USART2收发、复位、状态检测的HAL封装
└── ...

此结构强制约束: Middleware/Gizwits/Src/ 内所有 .c 文件 仅能调用 Middleware/Gizwits/Inc/ User/Inc/ 中的头文件, 绝对禁止 直接包含 Drivers/STM32F1xx_HAL_Driver/Inc/ Core/Inc/ 下的HAL头文件。所有硬件访问必须通过 User/Src/wifi_hal.c 提供的统一接口(如 wifi_uart_receive() , wifi_uart_transmit() )进行。此举确保协议栈逻辑与具体MCU型号解耦,未来迁移到STM32F4或GD32时,仅需重写 wifi_hal.c 即可。

1.3 头文件路径与依赖注入:构建编译时契约

Middleware/Gizwits/Inc/ 添加至工程Include Path是基础,但更关键的是建立正确的头文件依赖链。 gizwits_protocol.c 的编译成功,依赖于三个层级的头文件正确包含:

  1. 协议栈自身依赖 gizwits_protocol.h 必须包含 gizwits_product.h (提供数据点定义)和 ring_buffer.h (提供缓冲区操作)。
  2. 硬件抽象层注入 gizwits_wifi.h 中声明的函数(如 wifi_uart_transmit() )其定义在 User/Src/wifi_hal.c 中,因此 gizwits_protocol.c 需包含 gizwits_wifi.h ,而 gizwits_wifi.h 本身 不得 包含任何HAL头文件。
  3. HAL驱动桥接 User/Src/wifi_hal.c 是唯一允许包含 stm32f1xx_hal.h usart.h 的文件,它负责将 gizwits_wifi.h 声明的抽象接口,翻译为具体的HAL函数调用(如 HAL_UART_Transmit() )。

典型错误是直接在 gizwits_protocol.c #include "stm32f1xx_hal_usart.h" ,这会破坏分层,导致协议栈无法跨平台。正确的头文件包含顺序应为:

// gizwits_protocol.c
#include "gizwits_protocol.h"  // 主协议头
#include "gizwits_product.h"  // 数据点定义
#include "ring_buffer.h"      // 缓冲区操作
#include "gizwits_wifi.h"     // 硬件抽象接口(关键!)

1.4 编译验证:首个里程碑的达成

完成文件拷贝与目录结构搭建后,执行首次编译( Build )。此时预期结果应为 零错误,若干警告(可忽略) 。若出现 undefined reference 错误,99%源于头文件路径缺失或 gizwits_wifi.h 未被正确包含;若出现 unknown type name ,则检查 gizwits_product.h 是否被 gizwits_protocol.h 正确引用。此阶段不追求功能运行,仅验证协议栈代码在编译器眼中是语法合法的C代码。这是工程移植的基石,必须稳固。

2. UART2接收中断适配:构建可靠的数据摄入管道

WiFi模块(如ESP8266/ESP32)与STM32的通信,本质是异步串行数据流。云端下发的指令、APP发送的控制命令,均以不定长、不定时的字节流形式经UART2抵达MCU。协议栈 gizwits_protocol.c 中的 gizwits_uartReceive() 函数,正是处理此数据流的入口。其正确工作,完全依赖于一个健壮的、永不丢包的底层接收机制。

2.1 环形缓冲区(Ring Buffer):应对数据洪峰的必然选择

为何不能在UART2中断服务程序(ISR)中直接调用 gizwits_uartReceive() ?根本原因在于协议栈解析是计算密集型操作,若在ISR中执行,将极大延长中断响应时间,导致后续接收中断被覆盖(Overrun),丢失数据。解决方案是引入环形缓冲区作为“数据蓄水池”。

ring_buffer.c 实现了一个经典的生产者-消费者模型:
* 生产者(Producer) :UART2的接收中断服务程序(ISR)。其唯一职责是: 快速地 将接收到的单个字节( uint8_t )存入环形缓冲区的 write_index 位置,并原子性地递增 write_index 。此过程耗时极短(<1μs),确保中断及时退出。
* 消费者(Consumer) :主循环( while(1) )或独立任务中调用的 gizwits_uartReceive() 。它从环形缓冲区的 read_index 位置 批量读取 连续字节(通常一次读取 buffer_size 字节),并递增 read_index 。读取后,协议栈立即对这批数据进行解析。

环形缓冲区的关键参数:
* BUFFER_SIZE :缓冲区长度,单位字节。建议值≥256。过小易溢出,过大浪费RAM。
* write_index , read_index :两个无符号整型索引,指向缓冲区首尾。利用模运算( index % BUFFER_SIZE )实现环形。

2.2 UART2中断服务程序(ISR)的精确注入

假设UART2已由STM32CubeMX配置为 Asynchronous 模式, Interrupt 方式使能,且 HAL_UART_Receive_IT() 已在 MX_USART2_UART_Init() 中调用。此时, USART2_IRQHandler 已被HAL库生成。我们需要在此ISR中,精准插入环形缓冲区写入逻辑。

标准HAL ISR模板(位于 stm32f1xx_it.c ):

void USART2_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart2); // HAL库标准处理:清除标志、触发回调
}

注入后的ISR(关键修改):

extern Ring_Buffer_t gizwits_rx_buf; // 声明全局环形缓冲区实例

void USART2_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart2); // 此调用会触发HAL_UART_RxCpltCallback()
}

// HAL库回调函数,由HAL_UART_IRQHandler()在接收完成时自动调用
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if(huart->Instance == USART2) // 确保是USART2的回调
  {
    uint8_t rx_data;
    // 1. 从USART2接收数据寄存器读取一字节
    rx_data = (uint8_t)(huart->Instance->DR & 0xFF);
    // 2. 将该字节写入环形缓冲区
    ring_buffer_write(&gizwits_rx_buf, &rx_data, 1);
    // 3. 立即启动下一次单字节接收,维持接收流水线
    HAL_UART_Receive_IT(huart, &rx_data, 1);
  }
}

核心要点解析:
* 为何用 HAL_UART_RxCpltCallback 而非直接在 USART2_IRQHandler 中写? 因为 HAL_UART_IRQHandler() 内部已完成了接收标志清除、错误处理等繁琐工作, RxCpltCallback 是HAL推荐的、安全的用户扩展点。
* rx_data 变量的作用域? 必须为 static 或全局,因为 HAL_UART_Receive_IT() 需要其地址。局部变量在中断返回后即失效,会导致接收数据错乱。
* HAL_UART_Receive_IT() 的持续调用 :这是维持“永远在线”接收的关键。每次接收完一字节,立刻发起下一次接收,确保没有接收间隙。

2.3 协议栈接收入口的绑定与验证

gizwits_protocol.c 中, gizwits_uartReceive() 函数原型为:

uint16_t gizwits_uartReceive(uint8_t *buf, uint16_t len)

其内部逻辑是:从环形缓冲区 gizwits_rx_buf 中尝试读取最多 len 个字节到 buf ,并返回实际读取长度。

User/Src/wifi_hal.c 中,需提供此函数的实现(或直接在 gizwits_protocol.c 中调用 ring_buffer_read() ):

// wifi_hal.c
#include "ring_buffer.h"
extern Ring_Buffer_t gizwits_rx_buf; // 同上

uint16_t gizwits_uartReceive(uint8_t *buf, uint16_t len)
{
  return ring_buffer_read(&gizwits_rx_buf, buf, len);
}

验证方法: main() 函数的 while(1) 循环中,周期性(如每100ms)调用 gizwits_uartReceive() ,并将返回值打印至调试串口。当WiFi模块向MCU发送AT指令或GAgent协议包时,此函数应返回非零值,证明数据已成功从UART2流入环形缓冲区,并被协议栈捕获。这是数据摄入管道贯通的第一个实证。

3. UART2发送逻辑实现:构建可控的数据输出通道

MCU向WiFi模块发送数据,是另一条关键通路。 gizwits_protocol.c 中的 gizwits_uartWrite() 函数,负责将协议栈封装好的、符合GAgent规范的完整数据包(含包头、数据、校验)发送出去。其实现必须满足两个硬性要求: 阻塞式发送保证完整性 协议兼容性保证正确性

3.1 阻塞式发送的必要性与实现原理

为何不能使用 HAL_UART_Transmit_IT() (中断发送)或 HAL_UART_Transmit_DMA() (DMA发送)?因为GAgent协议要求:一个完整的数据包(例如 0x55 0xAA ... 0xFF )必须作为一个不可分割的整体发送。若采用中断或DMA发送,在发送中途若被更高优先级中断打断,或DMA传输被意外终止,将导致数据包被截断,WiFi模块无法识别,进而引发握手失败或连接中断。

因此, gizwits_uartWrite() 必须是一个 忙等待(Busy-Waiting) 的阻塞函数。其核心逻辑是:循环查询USART2的发送数据寄存器(TDR)是否为空( TXE 标志),一旦为空,立即将下一个字节写入TDR,直至整个 buf 发送完毕。

3.2 符合GAgent协议的发送函数实现

根据字幕提示, gizwits_uartWrite() 的实现需特别注意一个协议细节:当待发送数据中连续出现多个 0x55 字节时,WiFi模块要求将其替换为 0x55 0x55 (即字节转义)。但字幕中提到的“第二后面的buffer都是0sf的话”存在明显口误,结合GAgent官方文档,正确的规则是: 对数据内容中的每一个 0x55 字节,均需在前面插入一个额外的 0x55 字节进行转义

gizwits_protocol.c gizwits_uartWrite() 的典型实现如下(位于 User/Src/wifi_hal.c 中):

#include "stm32f1xx_hal.h"
#include "usart.h"

// 声明外部HAL UART句柄
extern UART_HandleTypeDef huart2;

uint16_t gizwits_uartWrite(const uint8_t *buf, uint16_t len)
{
  uint16_t i = 0;
  uint16_t sent = 0;
  uint8_t tx_byte;

  while(i < len)
  {
    tx_byte = buf[i];
    // GAgent协议:对0x55字节进行转义,发送0x55 0x55
    if(tx_byte == 0x55)
    {
      // 发送第一个0x55
      while(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TXE) == RESET); // 等待TDR空
      huart2.Instance->DR = 0x55;
      sent++;

      // 发送第二个0x55(转义符)
      while(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TXE) == RESET);
      huart2.Instance->DR = 0x55;
      sent++;
    }
    else
    {
      // 发送原始字节
      while(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TXE) == RESET);
      huart2.Instance->DR = tx_byte;
      sent++;
    }
    i++;
  }

  // 等待最后一个字节完成发送(TC: Transmission Complete)
  while(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TC) == RESET);

  return sent;
}

关键点说明:
* __HAL_UART_GET_FLAG() :直接读取USART状态寄存器(SR),比调用 HAL_UART_GetState() 更高效,适合忙等待场景。
* huart2.Instance->DR :直接操作数据寄存器,绕过HAL库的开销,确保最快速度。
* UART_FLAG_TC 等待:确保整个发送过程彻底结束,避免后续操作干扰。

3.3 调试与性能权衡

阻塞式发送的最大缺点是占用CPU时间。对于一个100字节的包,若波特率为115200bps,理论发送时间约为8.7ms。在此期间,MCU无法响应其他中断(除非更高优先级)。实践中,可通过以下方式优化:
1. 降低发送频率 :确保 gizwits_uartWrite() 只在必要时调用(如数据点变化、心跳包),避免轮询式发送。
2. 提升波特率 :在WiFi模块支持的前提下,将UART2波特率提升至921600bps或更高,可将发送时间压缩至亚毫秒级。
3. 硬件流控(可选) :若硬件支持RTS/CTS,可在高负载时启用,让WiFi模块主动通知MCU暂停发送,但这增加了硬件复杂度。

4. 毫秒级系统定时器:为协议栈提供精准的时间刻度

GAgent协议栈的正常运转,高度依赖一个稳定、精确的毫秒级(ms)时间源。其核心用途包括:
* 心跳包(Keep-Alive)定时 :每隔固定毫秒(如30000ms)向云端发送心跳,维持长连接。
* 超时重传机制 :当发送指令后未在规定毫秒内收到ACK,触发重发。
* 数据点上报间隔控制 :传感器数据按设定毫秒周期上报。
* 事件去抖动(Debounce) :对按键等输入信号进行毫秒级延时确认。

4.1 SysTick vs TIM2:选择最适合的定时器

STM32F103提供了多种定时器资源。选择依据是: 是否与FreeRTOS或其他系统级调度器冲突
* SysTick :是Cortex-M3内核的系统滴答定时器,FreeRTOS默认使用它作为心跳。若项目已使用FreeRTOS,则 绝对禁止 将SysTick用于GAgent协议栈,否则将导致RTOS调度紊乱。
* TIM2 :通用定时器,资源独立,是本项目的首选。其时钟源为APB1总线时钟(通常为36MHz),通过预分频器(PSC)和自动重装载寄存器(ARR)可精确配置为1ms中断。

4.2 TIM2毫秒定时器的HAL配置与中断服务

在STM32CubeMX中配置TIM2:
* Clock Source : Internal Clock
* Prescaler (PSC) : 36000 - 1 = 35999 (若APB1=36MHz,则 36MHz / 36000 = 1kHz
* Counter Period (ARR) : 1000 - 1 = 999 1kHz / 1000 = 1ms
* Trigger Event : Update Event
* NVIC Settings : Enable TIM2 global interrupt

生成代码后, stm32f1xx_it.c 中会自动生成 TIM2_IRQHandler 。在此中断中,只需调用 gizTimerMs() 函数即可:

extern void gizTimerMs(void); // 声明协议栈的毫秒计时器回调

void TIM2_IRQHandler(void)
{
  HAL_TIM_IRQHandler(&htim2);
}

// HAL库回调,在TIM2更新中断发生时被调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if(htim->Instance == TIM2)
  {
    gizTimerMs(); // 通知协议栈:又过去1ms
  }
}

4.3 gizTimerMs() 函数的声明与链接

gizTimerMs() gizwits_protocol.c 内部定义的函数,其作用是维护一个全局毫秒计数器 gizwitsTimerMs 。为了让 HAL_TIM_PeriodElapsedCallback() 能够调用它,必须在 gizwits_protocol.h 中声明:

// gizwits_protocol.h
#ifndef __GIZWITS_PROTOCOL_H__
#define __GIZWITS_PROTOCOL_H__

// ... 其他已有声明 ...

// 新增:声明毫秒定时器回调函数
void gizTimerMs(void);

#endif

此声明确保了编译器在链接阶段能找到 gizTimerMs 的定义。若遗漏此声明,链接器将报 undefined reference to 'gizTimerMs' 错误。这是工程移植中极易忽略的“链接时契约”,必须严格执行。

5. MCU复位函数实现:赋予协议栈对硬件的最终控制权

在物联网设备的生命周期中,“复位”是最强力的故障恢复手段。GAgent协议栈在检测到严重通信异常(如WiFi模块无响应、固件版本不匹配)时,会调用 mcuRestart() 函数,期望MCU执行一次硬件复位,重启整个系统,以期恢复连接。这是一个典型的“协议栈请求,硬件执行”的权限移交。

5.1 mcuRestart() 的语义与实现要求

mcuRestart() 函数在 gizwits_protocol.c 中通常被声明为 void mcuRestart(void) ,其内部初始实现为空( {} )。移植者必须将其重写为一个 真正触发MCU硬件复位 的函数。其行为必须满足:
* 不可逆性 :一旦执行,MCU必须进入复位流程,无法被软件中断或取消。
* 最小化副作用 :复位过程应尽可能干净,不依赖于任何可能已损坏的外设状态(如未初始化的GPIO)。
* 标准性 :采用ARM Cortex-M3标准复位方式,确保跨平台兼容。

5.2 基于NVIC的系统复位实现

最可靠、最标准的复位方式是触发ARM内核的 SYSRESETREQ 信号。此信号由NVIC(嵌套向量中断控制器)的 Application Interrupt and Reset Control Register (AIRCR) 控制。其优势在于:不依赖于任何外设时钟,即使在系统时钟严重异常时仍能工作。

User/Src/wifi_hal.c 中的实现如下:

#include "stm32f1xx_hal.h"

void mcuRestart(void)
{
  // 1. 解锁AIRCR寄存器(写入密钥0x05FA)
  SCB->AIRCR = (0x05FA << 16) | SCB_AIRCR_SYSRESETREQ;
  // 2. 执行DSB(数据同步屏障)和ISB(指令同步屏障)确保指令顺序
  __DSB();
  __ISB();
  // 3. 此后代码永不执行,MCU开始复位流程
}

关键寄存器说明:
* SCB->AIRCR :系统控制块(System Control Block)的AIRCR寄存器。
* 0x05FA << 16 :ARM规定的写入密钥,用于防止意外复位。
* SCB_AIRCR_SYSRESETREQ :宏定义,值为 0x04000000 ,即设置 SYSRESETREQ 位。

5.3 验证复位功能:从协议栈发起的终极测试

验证 mcuRestart() 是否有效,最直接的方法是在 user_app.c 的应用层回调中主动触发:

// user_app.c
#include "gizwits_product.h"

// 假设有一个数据点DPID_RESET,当APP发送"1"时触发复位
bool ICACHE_FLASH_ATTR gizwitsEventProcess(eventInfo_t *info, uint8_t wifiDataPoint)
{
  switch(info->event)
  {
    case EVENT_RESET:
      if(info->data[0] == 1)
      {
        // APP发送了复位指令
        mcuRestart(); // 立即执行复位
      }
      break;
    // ... 其他事件
  }
  return true;
}

将设备接入APP,向 DPID_RESET 数据点下发值 1 。若MCU在1秒内重启(表现为LED熄灭再亮起,或调试串口输出重启日志),则证明 mcuRestart() 实现成功。这是协议栈获得对硬件最终控制权的标志性时刻。

6. 综合调试与常见问题排查:走向稳定运行的最后一步

完成上述五大步骤后,工程已具备基本通信能力,但距离稳定运行仍有距离。此阶段需借助系统化调试手段,定位并解决隐性问题。

6.1 分层调试法:从物理层到应用层逐级验证

调试应遵循“自底向上”原则,每一层验证通过后再进入上一层:
1. 物理层(UART2) :使用USB-TTL转换器连接PC,发送AT指令给WiFi模块,确认其能正常响应。同时,用逻辑分析仪抓取MCU的USART2_TX引脚波形,验证发送的字节序列是否符合预期(特别是 0x55 转义)。
2. 驱动层(HAL) :在 HAL_UART_RxCpltCallback() gizwits_uartWrite() 中添加 printf 或LED闪烁,确认中断和发送函数被正确调用。
3. 协议栈层(Gizwits) :启用 gizwits_protocol.c 中的 DEBUG_PRINT 宏(若存在),观察协议解析日志。重点关注 GIZWITS_LOG("Receive: %d bytes", len) GIZWITS_LOG("Send: %d bytes", sent) 输出。
4. 应用层(User) :在 gizwitsEventProcess() 中打印接收到的事件类型和数据,确认APP下发的指令能被正确解析并触发回调。

6.2 典型问题与根因分析

现象 最可能根因 排查路径
编译报 undefined reference gizwits_wifi.h 未被 gizwits_protocol.c 包含;或 gizwits_protocol.h 中未声明 gizTimerMs() 检查头文件包含链、函数声明、链接器错误信息
WiFi模块无响应 gizwits_uartWrite() 未正确实现,或 0x55 转义逻辑错误 用逻辑分析仪抓TX波形,对比协议文档
云端显示“离线” gizTimerMs() 未被调用,导致心跳包无法发送 检查TIM2中断是否使能、 HAL_TIM_PeriodElapsedCallback 是否被触发
APP下发指令无反应 gizwits_uartReceive() 未被调用,或环形缓冲区 read_index/write_index 不同步 在主循环中打印 ring_buffer_get_length() ,确认有数据流入
MCU频繁复位 mcuRestart() 被意外调用(如指针越界、堆栈溢出) mcuRestart() 开头添加唯一标识LED闪烁,确认触发源

6.3 我的经验之谈:几个容易踩坑的细节

  • ring_buffer.c 的临界区保护 :若 gizwits_uartReceive() 在主循环中调用,而 ring_buffer_write() 在中断中调用,两者对 read_index write_index 的访问必须是原子的。 ring_buffer.c 通常使用 __disable_irq() / __enable_irq() 进行简单保护,但在FreeRTOS环境下,应改用 taskENTER_CRITICAL() / taskEXIT_CRITICAL()
  • gizwits_product.c dataPoint_t 初始化 :此结构体必须在 main() HAL_Init() 之后、 MX_GPIO_Init() 之前进行清零( memset(&currentDataPoint, 0, sizeof(currentDataPoint)) )。否则,未初始化的内存可能包含随机值,导致协议栈误判数据点状态。
  • 时钟树配置陷阱 :若在CubeMX中更改了APB1或APB2总线时钟,必须重新计算TIM2的PSC和ARR值,并同步更新 gizwits_protocol.c 中所有基于毫秒的常量(如心跳间隔)。最好将这些常量定义为宏,集中管理。

gizwits_uartReceive() 开始稳定返回非零值, gizwits_uartWrite() 能发出合规数据包, gizTimerMs() 被规律调用,且 mcuRestart() 能成功重启,这个移植工程便已越过最关键的门槛。后续的入网配网、数据点映射、APP联调,都将在这一坚实基础上水到渠成。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐