⚠️本文基于 STM32F103C8T6 + 标准库 V3.5.0,适用于江科大 STM32 入门教程体系。若你使用的是 HAL 库或新版芯片,部分函数名和配置方式会有差异,建议对照参考手册理解原理后迁移。


一、前言

上一篇文章我们讲了 TIM 的输入捕获,用单个通道可以测一个方波的频率。但很多时候,我们不仅要测频率,还想知道这个方波的 占空比 是多少。比如测一个 PWM 信号,你只知道它 1kHz 没用啊,你还得知道它高电平占了多少百分比,对吧?

这时候就有两种思路:

  • 笨办法:用两个定时器,一个通道测上升沿算周期,另一个通道测下降沿算高电平时间,然后手动除一下。

  • 聪明办法(PWMI):只用 一个定时器的两个通道,同时捕获 同一个引脚 的信号。通道 1 负责在上升沿捕获并清零计数器,通道 2 负责在下降沿捕获高电平时间。最后 CCR2 / CCR1 直接就是占空比!

PWMI 是 STM32 输入捕获里一个非常实用的模式。今天这篇文章,我就结合江科大的课程笔记,把 PWMI 的原理、所有相关库函数、以及完整代码实战给大家讲清楚。


二、PWMI 基本原理:两个通道"盯"同一个引脚

2.1 核心思想

PWMI 模式本质上就是 输入捕获 + 从模式(Slave Mode) 的组合技。它的工作流程可以用一句话概括:

TI1FP1 上升沿触发 CCR1 捕获 + CNT 清零,TI1FP2 下降沿触发 CCR2 捕获高电平计数值。

具体怎么走的呢?看下面这个逻辑:

  1. 信号从 GPIO 进来,经过滤波器和边沿检测。

  2. TI1FP1(通道 1 的直连信号)配置为 上升沿触发,一旦发生上升沿,CCR1 就把当前 CNT 的值"拍"下来,同时通过从模式把 CNT 清零。

  3. CNT 从零开始往上计数,直到信号来到 下降沿

  4. TI1FP2(通道 2 的交叉信号)配置为 下降沿触发,CCR2 把下降沿时刻的 CNT 值"拍"下来。

  5. 下一次上升沿到来,CCR1 再次捕获,CNT 再次清零,循环往复。

这样:

  • CCR1 = 一个完整周期的计数值(频率就靠它算)

  • CCR2 = 高电平期间的计数值(占空比就靠它算)

  • 占空比 = CCR2 / CCR1

💡 我的理解:你可以把 PWMI 想象成两个人用同一块秒表测跑步。第一个人(通道1)在起跑线按表并清零,第二个人(通道2)在半程按表记录,然后第一个人又在终点按表记录总时间。这样你同时知道了总时间和半程时间,一除就知道速度比例了。

2.2 为什么能"交叉"捕获?

这里有个关键点:PWMI 模式下,通道 1 和通道 2 捕获的是 同一个物理引脚 的信号,但走的是不同的内部通路。

  • 通道 1 走的是 直连模式(Direct TI):TI1 直接进 IC1。

  • 通道 2 走的是 交叉模式(Indirect TI):TI1 交叉进 IC2。

这个交叉通路是 STM32 硬件设计好的,不需要你额外接线,一根信号线接到 PA6(TIM3_CH1)上,两个通道都能"看到"这个信号。


三、输入捕获七步法(PWMI 版)

不管是普通输入捕获还是 PWMI,配置流程都遵循这七个步骤,建议大家背下来:

表格

步骤 操作内容 对应代码环节
第 1 步 RCC 开启时钟(GPIO + TIM) RCC_APB1PeriphClockCmdRCC_APB2PeriphClockCmd
第 2 步 GPIO 初始化,配置为输入模式(上拉/浮空) GPIO_Init
第 3 步 配置时基单元,让 CNT 跑起来 TIM_TimeBaseInit
第 4 步 配置输入捕获单元(滤波器、极性、通道、分频器) TIM_ICInit / TIM_PWMIConfig
第 5 步 选择从模式触发源 TIM_SelectInputTrigger
第 6 步 选择从模式执行的操作 TIM_SelectSlaveMode
第 7 步 开启定时器 TIM_Cmd

四、所有相关库函数详解

这一节我把 PWMI 会用到的 全部库函数 列出来,一个一个讲清楚参数和用法。建议收藏,写代码的时候对着看。

4.1 时基单元相关函数

① 单独写入预分频器 PSC
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
// 解析:TIMx        —— 指定定时器,如 TIM3
//       Prescaler   —— 预分频值,写 72-1 表示 72 分频
//       TIM_PSCReloadMode —— 更新方式,通常用 TIM_PSCReloadMode_Update,表示在下次更新事件时生效

💡 我的理解:这个函数是"动态改分频"用的。如果你初始化后还想中途改定时器的计数频率,就用它。PWMI 初始化时一般直接用 TIM_TimeBaseInit 一步到位,这个函数了解即可。

② 选择内部时钟源
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
// 解析:TIMx —— 指定定时器,如 TIM3
// 功能:将定时器的时钟源配置为内部时钟(CK_INT),这是默认状态,但显式调用一下更规范
③ 时基单元初始化(最常用)
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
// 解析:TIMx                  —— 指定定时器
//       TIM_TimeBaseInitStruct —— 时基结构体指针,包含 ARR、PSC、计数模式等参数

TIM_TimeBaseInitTypeDef 结构体成员:

成员 含义 常用配置
TIM_ClockDivision 时钟分频(影响滤波器采样频率) TIM_CKD_DIV1(不分频)
TIM_CounterMode 计数模式 TIM_CounterMode_Up(向上计数)
TIM_Period 自动重装载值 ARR 65536 - 1(最大范围,避免溢出)
TIM_Prescaler 预分频值 PSC 72 - 1(72MHz → 1MHz)
TIM_RepetitionCounter 重复计数器(高级定时器才有) 0

⚠️ 踩坑预警:ARR 和 PSC 都要 减 1!因为计数器是从 0 开始数的。写 72 - 1 表示 72 分频,写 65536 - 1 表示计数范围 0~65535。


4.2 输入捕获配置函数

④ 输入捕获单元初始化(单通道配置)
void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
// 解析:TIMx               —— 指定定时器
//       TIM_ICInitStruct   —— 输入捕获结构体指针
// 注意:输出比较是每个通道一个函数(如 TIM_OC1Init),但输入捕获 4 个通道共用一个函数,
//       通过结构体里的 TIM_Channel 成员来区分通道

TIM_ICInitTypeDef 结构体成员:

成员 含义 常用配置
TIM_Channel 选择通道 TIM_Channel_1TIM_Channel_2
TIM_ICFilter 输入滤波器 0xF(最大滤波,消除抖动)
TIM_ICPolarity 捕获极性(边沿选择) TIM_ICPolarity_Rising(上升沿)/ Falling(下降沿)
TIM_ICPrescaler 捕获分频器 TIM_ICPSC_DIV1(每次边沿都捕获)
TIM_ICSelection 输入通道映射 TIM_ICSelection_DirectTI(直连)/ IndirectTI(交叉)
⑤ PWMI 模式快速配置(双通道同时配)
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
// 解析:TIMx               —— 指定定时器
//       TIM_ICInitStruct   —— 输入捕获结构体指针(只需填通道 1 的配置)
// 功能:这个函数是 PWMI 的"一键配置"函数!
//       你只需配置通道 1 的参数,它会自动把通道 2 配成"相反"的设置:
//       - 通道 1 上升沿 → 通道 2 自动下降沿
//       - 通道 1 直连   → 通道 2 自动交叉

💡 我的理解TIM_ICInit 是"单通道手动配",TIM_PWMIConfig 是"双通道自动配"。PWMI 模式下强烈推荐用 TIM_PWMIConfig,省得你自己去算通道 2 该配什么极性和映射关系,不容易出错。

⑥ 给输入捕获结构体赋默认值
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
// 解析:TIM_ICInitStruct —— 输入捕获结构体指针
// 功能:把结构体所有成员填充为默认值,防止未初始化成员导致奇怪问题
// 建议:养成好习惯,定义结构体后先调用这个函数,再修改你需要的成员

4.3 从模式配置函数

⑦ 选择输入触发源(TRGI)
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
// 解析:TIMx                   —— 指定定时器
//       TIM_InputTriggerSource —— 触发源选择
// 常用值:TIM_TS_TI1FP1(TI1 的滤波后信号,PWMI 用这个)
//         TIM_TS_TI2FP2(TI2 的滤波后信号)
//         TIM_TS_ITR0~ITR3(其他定时器级联触发)

💡 我的理解:这个函数告诉定时器:"你听谁的命令?" PWMI 模式下我们让通道 1 的上升沿来当"老大",所以选 TIM_TS_TI1FP1

⑧ 选择输出触发源(TRGO)
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
// 解析:TIMx           —— 指定定时器
//       TIM_TRGOSource —— 输出触发源选择
// 功能:配置本定时器向其他定时器发送触发信号的方式
// 注意:PWMI 模式用不到这个函数,它是给定时器级联(主从模式)用的

💡 我的理解:这个函数是"反方向"的——⑦ 是"我听谁的",⑧ 是"我告诉谁"。PWMI 里暂时用不到,但要知道它的存在,以后学定时器主从级联会用到。

⑨ 选择从模式执行的操作
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
// 解析:TIMx          —— 指定定时器
//       TIM_SlaveMode —— 从模式选择
// 常用值:TIM_SlaveMode_Reset(触发后清零 CNT,PWMI 用这个)
//         TIM_SlaveMode_Gated(门控模式)
//         TIM_SlaveMode_Trigger(触发模式)
//         TIM_SlaveMode_External1(外部时钟模式 1)

💡 我的理解:这个函数是 PWMI 的灵魂!TIM_SlaveMode_Reset 表示:每当触发源(TI1FP1 上升沿)到来时,自动把 CNT 清零。这样 CCR1 捕获的就是完整周期,而不是从上一次清零到现在的累积值。


4.4 单独配置通道分频器

⑩ 单独配置通道 1~4 的捕获分频器
void TIM_SetIC1Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC); // 配置通道 1
void TIM_SetIC2Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC); // 配置通道 2
void TIM_SetIC3Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC); // 配置通道 3
void TIM_SetIC4Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC); // 配置通道 4
// 解析:TIMx      —— 指定定时器
//       TIM_ICPSC —— 捕获分频值
// 常用值:TIM_ICPSC_DIV1(每 1 个边沿捕获一次)
//         TIM_ICPSC_DIV2(每 2 个边沿捕获一次)
//         TIM_ICPSC_DIV4(每 4 个边沿捕获一次)
//         TIM_ICPSC_DIV8(每 8 个边沿捕获一次)
// 注意:这个也可以在 TIM_ICInit 结构体里配置,两者效果一样

💡 我的理解:这几个函数是"事后修改"用的。如果你初始化时已经通过 TIM_ICInitStructure.TIM_ICPrescaler 配好了,就不需要再调用它们。但如果运行中想动态改变捕获频率(比如从"每个周期都测"变成"每 8 个周期测一次"),就用这几个函数。


4.5 读取捕获值函数

⑪ 读取四个通道的 CCR 值
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx); // 读取通道 1 的 CCR1
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx); // 读取通道 2 的 CCR2
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx); // 读取通道 3 的 CCR3
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx); // 读取通道 4 的 CCR4
// 解析:TIMx —— 指定定时器
// 返回值:当前捕获/比较寄存器的值(输入捕获模式下是只读的)
// 注意:输出比较模式下 CCR 是只写的(用 SetCompare 写),
//       输入捕获模式下 CCR 是只读的(用 GetCapture 读)

💡 我的理解:PWMI 里我们主要用 TIM_GetCapture1 读周期(CCR1)和 TIM_GetCapture2 读高电平时间(CCR2)。记住这个对应关系,别读反了!


4.6 定时器开关

⑫ 使能/失能定时器
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
// 解析:TIMx      —— 指定定时器
//       NewState —— ENABLE(开启)或 DISABLE(关闭)
// 功能:定时器的总开关,所有配置完成后最后调用

五、完整代码实战:PWMI 测频率 + 占空比

下面是完整的 PWMI 初始化代码和频率/占空比计算函数,基于 TIM3 通道 1(PA6)。我把每一行都加上了详细解析,方便大家对照理解。

5.1 PWMI 初始化函数

#include "stm32f10x.h"

/**
  * @brief  PWMI 模式初始化(TIM3_CH1,PA6)
  * @param  无
  * @retval 无
  * @note   适用 STM32F103C8T6,标准库 V3.5.0
  *         输入信号接 PA6,同时测量频率和占空比
  */
void IC_Init(void)
{
    /* ========== 第 1 步:RCC 开启时钟 ========== */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);   // 解析:开启 TIM3 时钟,TIM3 在 APB1 总线上
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  // 解析:开启 GPIOA 时钟,PA6 需要它
    
    /* ========== 第 2 步:GPIO 初始化 ========== */
    GPIO_InitTypeDef GPIO_InitStructure;                   // 解析:定义 GPIO 初始化结构体
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;        // 解析:配置为上拉输入模式,防止浮空导致误触发
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;            // 解析:选择 PA6 引脚,对应 TIM3_CH1
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    // 解析:输入模式下这个参数其实不影响,但习惯性写上
    GPIO_Init(GPIOA, &GPIO_InitStructure);               // 解析:调用初始化函数,将配置写入 PA6
    
    /* ========== 第 3 步:配置时基单元 ========== */
    TIM_InternalClockConfig(TIM3);                         // 解析:选择 TIM3 内部时钟源(72MHz),显式调用更规范
    
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;      // 解析:定义时基初始化结构体
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;   // 解析:时钟不分频,滤波器采样频率 = 72MHz
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 解析:向上计数模式,CNT 从 0 加到 ARR
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;     // 解析:ARR = 65535,计数范围 0~65535,最大可测频率约 1MHz / 2 = 500kHz(留余量)
    TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;    // 解析:PSC = 71,72 分频,72MHz → 1MHz,CNT 每 1us 加 1
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;  // 解析:重复计数器,仅高级定时器有效,通用定时器写 0
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);   // 解析:将时基参数写入 TIM3 寄存器
    
    /* ========== 第 4 步:配置输入捕获单元(PWMI 一键配置) ========== */
    TIM_ICInitTypeDef TIM_ICInitStructure;                 // 解析:定义输入捕获结构体
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;      // 解析:选择通道 1(对应 PA6 / TI1)
    TIM_ICInitStructure.TIM_ICFilter = 0xF;               // 解析:输入滤波器设为最大(0xF),滤除毛刺和抖动
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 解析:通道 1 上升沿触发捕获
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 解析:不分频,每个有效边沿都捕获
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 解析:通道 1 直连模式(TI1 → IC1)
    TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);           // 解析:【PWMI 核心】自动将通道 2 配为下降沿 + 交叉模式(TI1 → IC2)
    
    /* ========== 第 5 步:选择从模式触发源 ========== */
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);          // 解析:选择 TI1FP1 作为从模式触发源(即通道 1 的滤波后信号)
    
    /* ========== 第 6 步:选择从模式操作 ========== */
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);       // 解析:【关键】触发后自动清零 CNT,保证 CCR1 始终是一个完整周期的计数值
    
    /* ========== 第 7 步:开启定时器 ========== */
    TIM_Cmd(TIM3, ENABLE);                                // 解析:启动 TIM3,开始捕获
}

5.2 频率和占空比计算函数

/**
  * @brief  获取输入信号的频率
  * @param  无
  * @retval 频率值,单位 Hz
  * @note   公式:Freq = 1MHz / (CCR1 + 1)
  *         +1 是为了修正计数器从 0 开始导致的 1 个计数误差
  */
uint32_t IC_GetFreq(void)
{
    // 解析:计数器时钟频率 = 72MHz / 72 = 1MHz,即每个计数 = 1us
    //       CCR1 记录的是一个周期的计数个数,所以周期 = (CCR1 + 1) us
    //       频率 = 1MHz / (CCR1 + 1) = 1000000 / (CCR1 + 1) Hz
    return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}

/**
  * @brief  获取输入信号的占空比
  * @param  无
  * @retval 占空比值,单位 %(0~100)
  * @note   公式:Duty = (CCR2 + 1) * 100 / (CCR1 + 1)
  *         CCR2 是高电平期间计数值,CCR1 是周期计数值
  */
uint32_t IC_GetDuty(void)
{
    // 解析:CCR2 在下降沿捕获,记录的是高电平期间的 CNT 值
    //       占空比 = 高电平时间 / 周期 = CCR2 / CCR1
    //       乘以 100 转为百分比,+1 修正计数误差
    return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);
}

5.3 主函数调用示例

#include "stm32f10x.h"
#include "OLED.h"

int main(void)
{
    OLED_Init();    // 解析:初始化 OLED,用于显示结果
    IC_Init();      // 解析:初始化 PWMI 模式
    
    while (1)
    {
        // 解析:在 OLED 上显示频率和占空比,实际项目中这里可以改成串口打印
        OLED_ShowString(1, 1, "Freq:00000Hz");
        OLED_ShowNum(1, 6, IC_GetFreq(), 5);
        
        OLED_ShowString(2, 1, "Duty:000%");
        OLED_ShowNum(2, 6, IC_GetDuty(), 3);
        
        Delay_ms(100);  // 解析:每 100ms 刷新一次显示
    }
}

更详细的工程代码可看:STM32_Project: 基于江科大的STM32的教程编写的程序


六、我的理解

刚开始学 PWMI 的时候,我一直有个疑惑:为什么通道 2 能"看到"通道 1 的信号?后来看了江科大的视频才明白,这是 STM32 内部硬件的 交叉映射 设计。TI1 信号不仅可以直接连到 IC1,还可以通过内部通路交叉连到 IC2,而且 TIM_PWMIConfig 这个函数会自动帮你把通道 2 配成"相反"的极性,真的是非常贴心。

还有一个让我豁然开朗的点:从模式的 Reset 操作。之前学普通输入捕获的时候,CCR 捕获的是累积值,需要用两次捕获值相减才能得到周期。但 PWMI 模式下,每次上升沿都自动清零 CNT,所以 CCR1 直接就是一个周期的值,不需要做减法,代码简洁了很多!

关于公式里的 +1,我一开始也不理解为什么要加。后来自己画了个时序图:假设信号周期正好是 1us,CNT 从 0 数到 0 就捕获了,CCR1 = 0,如果不加 1,算出来频率就是无穷大,显然不对。加 1 是为了修正"从 0 开始计数"带来的误差,这个细节江科大在视频里讲过,但我第一次听的时候没注意,写代码时踩了坑。


七、踩坑记录

踩坑点 现象 原因 解决方法
1. GPIO 模式选错 捕获值乱跳,频率显示不稳定 用了浮空输入,引脚容易受干扰 改成 上拉输入(IPU)下拉输入(IPD),根据实际信号电平选择
2. ARR 设得太小 测低频信号时 CNT 溢出,频率值异常 信号周期超过 ARR 对应的计数范围 ARR 设为 65536 - 1(最大),PSC 根据待测频率范围调整。测低频用大 PSC,测高频用小 PSC
3. 公式忘记 +1 测 1kHz 信号显示 1010Hz 左右,有偏差 CNT 从 0 开始计数,实际计数个数 = CCR + 1 频率公式和占空比公式都要 +1
4. 从模式触发源选错 CCR1 值越来越大,不清零 触发源选成了 TIM_TS_TI2FP2 或其他 PWMI 必须选 TIM_TS_TI1FP1,和通道 1 对应
5. 信号源共地问题 OLED 显示 0Hz,完全测不到 信号发生器/舵机信号和 STM32 没共地 确保信号源 GND 和 STM32 GND 连接在一起!

八、总结表格

项目 说明
PWMI 本质 输入捕获 + 从模式 Reset,用两个通道同时捕获同一引脚
通道 1 职责 上升沿捕获,触发 CNT 清零,CCR1 = 周期计数值
通道 2 职责 下降沿捕获(交叉映射),CCR2 = 高电平计数值
占空比公式 Duty = (CCR2 + 1) * 100 / (CCR1 + 1)
频率公式 Freq = 1MHz / (CCR1 + 1)(PSC = 72-1 时)
核心函数 TIM_PWMIConfigTIM_SelectInputTriggerTIM_SelectSlaveMode
关键配置 触发源 = TI1FP1,从模式 = Reset,GPIO = IPU
适用边界 标准库 V3.5.0 + STM32F103C8T6;待测信号频率建议 1Hz ~ 500kHz
注意事项 必须共地!ARR 尽量设大!公式记得 +1!

九、结语

写这篇文章的时候,我把江科大的视频又回顾了一遍,重新梳理了 PWMI 的时序和每个函数的作用。PWMI 模式真的是 STM32 定时器里一个非常巧妙的设计,用一根线就能同时搞定频率和占空比,比用两个定时器分别测要优雅太多了。

希望这篇文章能帮你理清 PWMI 的思路。如果你在配置过程中遇到了其他问题,欢迎在评论区交流,我们一起踩坑、一起进步!

📌 开发环境:Keil5 + STM32F103C8T6最小系统板 + ST-Link
📌 参考资料:江科大STM32入门教程、《STM32F10xxx参考手册》
📌 前置文章STM32定时器超详解:从时钟树到影子寄存器,一文吃透定时中断

如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐