**先给大家看看我选用的EC11元器件**

代码在最后,复制可直接食用
在这里插入图片描述


以及我的电路图

在这里插入图片描述

在研究EC11的时序之前首先要了解一点,EC11按旋转的输出动作可以分为两种。一种是转两格,A、B对C端输出一个完整脉冲(转一格就只是由低电平->高电平或由高电平->低电平);另一种就是转一格,A、B对C端输出一个完整脉冲。

一定位一脉冲的EC11按测试电路图的接法,在静止的时候AB两线输出都是高电平。转动一格,AB两线各自输出一个低电平脉冲,然后又回到高电平状态。对应于EC11内部AB两个触点开关的动作为断开–>闭合–>断开。

两定位一脉冲的EC11稍微复杂一些,转动一格只会输出半个脉冲。静止时,AB触点开关可以是断开的也可以是闭合的。若初始状态时AB都是高电平,转动一格就输出从高电平到低电平的下降沿,随后一直输出低电平。对应于EC11内部AB两个触电开关的动作为断开–>闭合。
若初始状态时AB都是低电平,转动一格就输出从低电平到高电平的上升沿,随后一直输出低电平。对应于EC11内部AB两个触点开关的动作为闭合–>断开。
由于两脉冲一定位的EC11会有两种初始状态,写驱动程序就需要考虑多一些情况。再者,这类EC11在转动到内部AB触点一直闭合的时候,就相当于把上拉电阻的另一端接地,无形中加大了系统的功耗(若外接10K上拉电阻到5V电源就是500uA的电流),这对于低功耗应用来说是非常不利的。
因此对于无特殊要求的人机输入应用来说,我都推荐使用一定位一脉冲的EC11。
当然了,有一些质量比较差的EC11会有一些额外的问题要考虑,例如开关的抖动问题,例如转动定位不清晰,静止时AB两个触点都要闭合或者都要断开才对,但是定位点不清晰,转动的角度不到位导致一个触点已经闭合(断开)了,另一个触点却还保持在断开(闭合)。对于这些问题我们在后面再做考虑。

时序图

要写驱动程序,得先了解EC11的工作过程。使用逻辑分析仪(LA)抓取时序可以很方便的从单片机的角度了解EC11的工作过程并依此来编写驱动程序。
  EC11的编码器部分有3个引脚,A,B,和C。通常可以把C端接GND,A,B端接到输入上拉模式的IO口。可以取A或B任意一根线作为时钟线,另一根作为信号输出线。我个人习惯把A线作为时钟线,B线作为信号线。
  本文中出现的逻辑分析仪抓取的时序图中均是最上方通道为EC11的A线,视为时钟;下方一个通道为EC11的B线,视为数据输出。

在这里插入图片描述

就不啰嗦太多,我就直接上代码,我使用STM32F103系列

#include "ec11.h"
#include "sys.h"

void TIM4_Int_Init(u16 arr,u16 psc)
{	
	NVIC_InitTypeDef NVIC_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//TIM2时钟使能    
	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
	TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE ); //使能指定的TIM7中断,允许更新中断

	NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级0
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//子优先级2
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器
	
	TIM_Cmd(TIM4,ENABLE);//开启定时器4
}

//定时器4中断服务程序		    
void TIM4_IRQHandler(void)
{ 	
	if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)//是更新中断
	{	 		
		Encoder_EC11_Analyze(Encoder_EC11_Scan());
		
		TIM_ClearITPendingBit(TIM4, TIM_IT_Update);  //清除TIM4更新中断标志    
	}	    
}

//*******************************************************************/
//功能:初始化EC11旋转编码器相关参数
//形参:EC11旋转编码器的类型-->>  unsigned char Set_EC11_TYPE  <<--  :0----一定位对应一脉冲;1(或非0)----两定位对应一脉冲。
//返回:无
//详解:对EC11旋转编码器的连接IO口做IO口模式设置。以及将相关的变量进行初始化
//*******************************************************************/
void EC11_Init(unsigned char Set_EC11_TYPE)
{
	GPIO_InitTypeDef  GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;        //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz
	GPIO_Init(GPIOB, &GPIO_InitStructure);

	GPIO_SetBits(GPIOB,GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9);	

	//EC11类型选择:0-一定位一脉冲;1-两定位一脉冲
	if (Set_EC11_TYPE == 0)
	{
		EC11_Type = 0;
	}
	else
	{
		EC11_Type = 1;
	}
	//避免上电时EC11旋钮位置不确定导致一次动作误判
	EC11_A_Last = EC11_A_Now;
	EC11_B_Last = EC11_B_Now;

	//--------清除按键计数器和标志位--------//
	EC11_KEY_COUNT = 0;                     //EC11按键动作计数器
	EC11_KEY_DoubleClick_Count = 0;         //EC11按键双击动作计数器
	FLAG_EC11_KEY_ShotClick = 0;            //EC11按键短按动作标志
	FLAG_EC11_KEY_LongClick = 0;            //EC11按键长按动作标志
	FLAG_EC11_KEY_DoubleClick = 0;          //EC11按键双击动作标志
		
	TIM4_Int_Init(9,7199);	//初始化定时器4 1ms中断 
}



//*******************************************************************/
//功能:对EC11旋转编码器的动作进行分析,并作出相应的动作处理代码
//形参:无
//返回:char AnalyzeResult = 0;目前无用。若在该函数里做了动作处理,则函数的返回值无需理会
//详解:对EC11旋转编码器的动作进行模式分析,是单击还是双击还是长按松手还是一直按下。形参从 [ char Encoder_EC11_Scan(unsigned char Set_EC11_TYPE) ] 函数传入。在本函数内修改需要的动作处理代码
//*******************************************************************/
char Encoder_EC11_Analyze(char EC11_Value)
{
	char AnalyzeResult = 0;
  static unsigned int TMP_Value = 0;  //中间计数值,用于连续长按按键的动作延时间隔
  //>>>>>>>>>>>>>>>>编码器正转处理程序<<<<<<<<<<<<<<<<//
  if(EC11_Value == 1) //正转
  {
      //--------编码器正转动作代码--------//
     printf("正转!!")
  }
  //>>>>>>>>>>>>>>>>编码器反转处理程序<<<<<<<<<<<<<<<<//
  if(EC11_Value == 8)    //反转
  {
      //--------编码器反转动作代码--------//
     printf("反转!!");
  }

	//>>>>>>>>>>>>>>>>编码器按键按下并正转处理程序<<<<<<<<<<<<<<<<//
	if(EC11_Value == 3)
	{
		//--------编码器按键按下并正转动作代码--------//
			
	}

	//>>>>>>>>>>>>>>>>编码器按键按下并反转处理程序<<<<<<<<<<<<<<<<//
	if(EC11_Value == 9)
	{
		//--------编码器按键按下并反转动作代码--------//
//		printf("按下反转!!");
	}
	//>>>>>>>>>>>>>>>>编码器按键按下处理程序<<<<<<<<<<<<<<<<//
	if(EC11_Value == 2)     //====检测到按键按下====//
	{
		if(EC11_KEY_COUNT<10000)    //打开按键按下时间定时器
			 EC11_KEY_COUNT++;
		if(EC11_KEY_COUNT == KEY_COUNT_DESHAKING)   //按下按键时间到达消抖时间时
		{                                           //置位短按按键标志
			FLAG_EC11_KEY_ShotClick = 1;	
		}
		if((EC11_KEY_DoubleClick_Count > 0)&&(EC11_KEY_DoubleClick_Count <= KEY_COUNT_DUALCLICKTIME))   //松开按键后,又在定时器在双击时间内按下按键
		{                                                                                               //置位双击按键标志
			FLAG_EC11_KEY_DoubleClick = 1;
		}

		if(EC11_KEY_COUNT == KEY_COUNT_LONGTIME)    //按下按键时间到达长按时间
		{                                           //置位长按按键标志并复位短按按键标志
			FLAG_EC11_KEY_LongClick = 1;
			FLAG_EC11_KEY_ShotClick = 0;
		}
	}
	else                    //====检测到按键松开====//     
	{
		if(EC11_KEY_COUNT < KEY_COUNT_DESHAKING)    //没到消抖时长就松开按键,复位所有定时器和按键标志
		{	
			EC11_KEY_COUNT = 0;
			FLAG_EC11_KEY_ShotClick = 0;
			FLAG_EC11_KEY_LongClick = 0;
			FLAG_EC11_KEY_DoubleClick = 0;
			EC11_KEY_DoubleClick_Count = 0;
		}
    else
    {     
			if(FLAG_EC11_KEY_ShotClick == 1)        //短按按键定时有效期间
      {
				if((FLAG_EC11_KEY_DoubleClick == 0)&&(EC11_KEY_DoubleClick_Count >= 0)) 
						EC11_KEY_DoubleClick_Count++;
				if((FLAG_EC11_KEY_DoubleClick == 1)&&(EC11_KEY_DoubleClick_Count <= KEY_COUNT_DUALCLICKTIME))   //如果在规定双击时间内再次按下按键
        {                                                                                               //认为按键是双击动作
					FLAG_EC11_KEY_DoubleClick = 2;
        }   
        if((FLAG_EC11_KEY_DoubleClick == 0)&&(EC11_KEY_DoubleClick_Count > KEY_COUNT_DUALCLICKTIME))    //如果没有在规定双击时间内再次按下按键
            FLAG_EC11_KEY_ShotClick = 0;                                                                //认为按键是单击动作
       }
       if(FLAG_EC11_KEY_LongClick == 1)        //检测到长按按键松开
			 {
        FLAG_EC11_KEY_LongClick = 0;
			 }
     }
	}
    //>>>>>>>>>>>>>>>>编码器按键分析处理程序<<<<<<<<<<<<<<<<//
	
	if(EC11_KEY_COUNT > KEY_COUNT_DESHAKING)    //短按按键延时到了时间
	{
		//短按按键动作结束代码
		if((FLAG_EC11_KEY_ShotClick == 0)&&(EC11_KEY_DoubleClick_Count > KEY_COUNT_DUALCLICKTIME)&&(EC11_KEY_COUNT < KEY_COUNT_LONGTIME))   //短按按键动作结束代码
    {
      //--------短按按键动作结束代码--------//
			EC11_NUM_SW++;
			if(EC11_NUM_SW >= 4)
				 EC11_NUM_SW = 1;
			AnalyzeResult = 1;
			
			//--------清除标志位--------//
			EC11_KEY_COUNT = 0;
			EC11_KEY_DoubleClick_Count = 0;
			FLAG_EC11_KEY_DoubleClick = 0;
		}
    //双击按键动作结束代码
		if((FLAG_EC11_KEY_DoubleClick == 2)&&(EC11_KEY_DoubleClick_Count > 0)&&(EC11_KEY_DoubleClick_Count <= KEY_COUNT_DUALCLICKTIME)) //双击按键动作结束代码
    {
			//--------双击按键动作结束代码--------//
			if(EC11_NUM_SW == 5)
				 EC11_NUM_SW = 0;
			if(EC11_NUM_SW == 4)
				 EC11_NUM_SW = 5;
			if(EC11_NUM_SW <4)
			{
				EC11_NUM_SW = 4;
			}
			AnalyzeResult = 2;
			//--------清除标志位--------//
			EC11_KEY_COUNT = 0;
			EC11_KEY_DoubleClick_Count = 0;
			FLAG_EC11_KEY_ShotClick = 0;
			FLAG_EC11_KEY_DoubleClick = 0;    
		}

		//连续长按按键按下代码
		if((FLAG_EC11_KEY_LongClick == 1)&&(EC11_KEY_COUNT >= KEY_COUNT_LONGTIME))  //连续长按按键按下代码
    {
			TMP_Value ++;
			if(TMP_Value % KEY_LONG_REPEAT_TIME == 0)
			{
				TMP_Value = 0;
				//-------连续长按按键按下代码--------//
				AnalyzeResult = 4;
			}
		}

		//长按按键动作结束代码
		if((FLAG_EC11_KEY_LongClick == 0)&&(EC11_KEY_COUNT >= KEY_COUNT_LONGTIME))  //长按按键动作结束代码
		{                                                                           
			//--------长按按键按下动作结束代码--------//
			EC11_NUM_SW = 0;
			AnalyzeResult = 3;
			//--------清除标志位--------//
			EC11_KEY_COUNT = 0;
		}
	}
	return AnalyzeResult;
}

//*******************************************************************/
//功能:扫描EC11旋转编码器的动作并将参数返回给动作分析函数使用
//形参:EC11旋转编码器的类型-->>  unsigned char Set_EC11_TYPE  <<--  :0----一定位对应一脉冲;1(或非0)----两定位对应一脉冲
//返回:EC11旋转编码器的扫描结果-->>  char ScanResult  -->>  0:无动作;1:正转; -1:反转;2:只按下按键;3:按着按键正转;-3:按着按键反转
//详解:只扫描EC11旋转编码器有没有动作,不关心是第几次按下按键或长按或双击。返回值直接作为形参传给 [ void Encoder_EC11_Analyze(char EC11_Value); ] 函数使用
//*******************************************************************/
char Encoder_EC11_Scan()
{
		//以下储存A、B上一次值的变量声明为静态全局变量,方便对EC11对应的IO口做初始化
		//  static char EC11_A_Last = 0;
		//  static char EC11_B_Last = 0;
	char ScanResult = 0;    //返回编码器扫描结果,用于分析编码器的动作
                          //返回值的取值:   0:无动作;      1:正转;           8:反转;  
                          //                 2:只按下按键;  3:按着按键正转;   9:按着按键反转
                          //======================================================//
	if(EC11_Type == 0)      //================一定位对应一脉冲的EC11================//
	{                     	//======================================================//
		if(EC11_A_Now != EC11_A_Last)   //以A为时钟,B为数据。正转时AB反相,反转时AB同相
		{
			if(EC11_A_Now == 0)
			{
				if(EC11_B_Now ==1)      //只需要采集A的上升沿或下降沿的任意一个状态,若A下降沿时B为1,正转                    
					 ScanResult = 1;     //正转
				else                    //反转
					 ScanResult = 8;
			}
			EC11_A_Last = EC11_A_Now;   //更新编码器上一个状态暂存变量
			EC11_B_Last = EC11_B_Now;   //更新编码器上一个状态暂存变量
		}
	}   
                          //======================================================//
	else                    //================两定位对应一脉冲的EC11================//
	{                       //======================================================//
		if(EC11_A_Now !=EC11_A_Last)        //当A发生跳变时采集B当前的状态,并将B与上一次的状态进行对比。
		{                                   //若A 0->1 时,B 1->0 正转;若A 1->0 时,B 0->1 正转;
                                        //若A 0->1 时,B 0->1 反转;若A 1->0 时,B 1->0 反转
			if(EC11_A_Now == 1)     					//EC11_A和上一次状态相比,为上升沿
			{
				if((EC11_B_Last == 1)&&(EC11_B_Now == 0))   //EC11_B和上一次状态相比,为下降沿
						ScanResult = 1;                         //正转
        if((EC11_B_Last == 0)&&(EC11_B_Now == 1))   //EC11_B和上一次状态相比,为上升沿               
            ScanResult = 8;                        //反转
                //>>>>>>>>>>>>>>>>下面为正转一次再反转或反转一次再正转处理<<<<<<<<<<<<<<<<//
        if((EC11_B_Last == EC11_B_Now)&&(EC11_B_Now == 0))  //A上升沿时,采集的B不变且为0
            ScanResult = 1;                                 //正转
        if((EC11_B_Last == EC11_B_Now)&&(EC11_B_Now == 1))  //A上升沿时,采集的B不变且为1
            ScanResult = 8;                                 //反转
			}
			else                    //EC11_A和上一次状态相比,为下降沿
			{
				if((EC11_B_Last == 1)&&(EC11_B_Now == 0))   //EC11_B和上一次状态相比,为下降沿
						ScanResult = 8;                        //反转
				if((EC11_B_Last == 0)&&(EC11_B_Now == 1))   //EC11_B和上一次状态相比,为上升沿
						ScanResult = 1;                         //正转
				//>>>>>>>>>>>>>>>>下面为正转一次再反转或反转一次再正转处理<<<<<<<<<<<<<<<<//
				if((EC11_B_Last == EC11_B_Now)&&(EC11_B_Now == 0))  //A上升沿时,采集的B不变且为0
						ScanResult = 8;                                //反转
				if((EC11_B_Last == EC11_B_Now)&&(EC11_B_Now == 1))  //A上升沿时,采集的B不变且为1   
						ScanResult = 1;                                 //正转
			}               
				EC11_A_Last = EC11_A_Now;   //更新编码器上一个状态暂存变量
				EC11_B_Last = EC11_B_Now;   //更新编码器上一个状态暂存变量
		}
	}                                                                       
	if(EC11_Key == 0)   //如果EC11的按键按下,并且没有EC11没有转动,
	{
//		if(ScanResult == 0)         //按下按键时未转动
			 ScanResult = 2;         //返回值为2
//		else
//		{
//			if(ScanResult == 1)     //按下按键时候正转
//				 ScanResult = 3;     //返回值为3
//			if(ScanResult == 8)    //按下按键时候反转
//				 ScanResult = 9;    //返回值为-3
//		}
	}
    return ScanResult;      //返回值的取值:   0:无动作;      1:正转;           8:反转;
}                           //             		 2:只按下按键;  3:按着按键正转;   9:按着按键反转


#ifndef __ec11_H
#define __ec11_H


#include "sys.h"
#include "stm32f10x.h"

//----------------IO口定义----------------//
#define EC11_A_Now           PBin(9)                             //EC11的A引脚,视为时钟线
#define EC11_B_Now           PBin(8)                           	 //EC11的B引脚,视为信号线
#define	EC11_Key             PBin(7)                           	 //EC11的按键


//----------------编码器动作代码相关定义----------------//

static unsigned char EC11_NUM_SW = 0;

//----------------编码器参数微调宏定义----------------//
#define EC11_SCAN_PERIOD_MS            1                            //EC11编码器扫描周期
#define KEY_COUNT_DESHAKING         ( 10/EC11_SCAN_PERIOD_MS)       //按键消抖时间
#define KEY_COUNT_LONGTIME          (150/EC11_SCAN_PERIOD_MS)       //长按按键判断时间
#define KEY_COUNT_DUALCLICKTIME     (150/EC11_SCAN_PERIOD_MS)       //双击按键判断时间
#define KEY_LONG_REPEAT_TIME        (200/EC11_SCAN_PERIOD_MS)       //长按按键的回报率的倒数,即一直长按按键时响应的时间间隔

//----------------局部文件内变量列表----------------//
static  char    EC11_A_Last = 0;                        //EC11的A引脚上一次的状态
static  char    EC11_B_Last = 0;                        //EC11的B引脚上一次的状态
static  char    EC11_Type = 1;                          //定义变量暂存EC11的类型---->>>>----  0:一定位对应一脉冲;  1:两定位对应一脉冲
//所谓一定位对应一脉冲,是指EC11旋转编码器每转动一格,A和B都会输出一个完整的方波。
//而  两定位对应一脉冲,是指EC11旋转编码器每转动两格,A和B才会输出一个完整的方波,只转动一格只输出A和B的上升沿或下降沿

static   int    EC11_KEY_COUNT = 0;                     //EC11按键动作计数器
static   int    EC11_KEY_DoubleClick_Count = 0;         //EC11按键双击动作计数器
static  char    FLAG_EC11_KEY_ShotClick = 0;            //EC11按键短按动作标志
static  char    FLAG_EC11_KEY_LongClick = 0;            //EC11按键长按动作标志
static  char    FLAG_EC11_KEY_DoubleClick = 0;          //EC11按键双击动作标志

//----------------函数快速调用(复制粘贴)列表----------------//
//
/*******************************************************************
void Encoder_EC11_Init(unsigned char Set_EC11_TYPE);        //初始化EC11旋转编码器IO口和类型以及变量初始化
char Encoder_EC11_Scan();                                   //扫描旋转编码器的动作
void Encoder_EC11_Analyze(char EC11_Value);                 //分析EC11旋转编码器的动作以及动作处理代码
******************************************************************/
//-------->>>>>>>>--------注意事项:EC11旋转编码器的扫描时间间隔控制在1~4ms之间,否则5ms及以上的扫描时间在快速旋转时可能会误判旋转方向--------<<<<<<<<--------//
//-------->>>>>>>>--------注意事项:EC11旋转编码器的扫描时间间隔控制在1~4ms之间,否则5ms及以上的扫描时间在快速旋转时可能会误判旋转方向--------<<<<<<<<--------//
//-------->>>>>>>>--------注意事项:EC11旋转编码器的扫描时间间隔控制在1~4ms之间,否则5ms及以上的扫描时间在快速旋转时可能会误判旋转方向--------<<<<<<<<--------//

//----------------函数声明列表----------------//
//
//*******************************************************************/
//功能:初始化EC11旋转编码器相关参数
//形参:EC11旋转编码器的类型-->>  unsigned char Set_EC11_TYPE  <<--  :0----一定位对应一脉冲;1(或非0)----两定位对应一脉冲。
//返回:无
//详解:对EC11旋转编码器的连接IO口做IO口模式设置。以及将相关的变量进行初始化
//*******************************************************************/
void EC11_Init(unsigned char Set_EC11_TYPE);

//*******************************************************************/
//功能:扫描EC11旋转编码器的动作并将参数返回给动作分析函数使用
//形参:EC11旋转编码器的类型-->>  unsigned char Set_EC11_TYPE  <<--  :0----一定位对应一脉冲;1(或非0)----两定位对应一脉冲
//返回:EC11旋转编码器的扫描结果-->>  char ScanResult  -->>  0:无动作;1:正转; -1:反转;2:只按下按键;3:按着按键正转;-3:按着按键反转
//详解:只扫描EC11旋转编码器有没有动作,不关心是第几次按下按键或长按或双击。返回值直接作为形参传给 [ void Encoder_EC11_Analyze(char EC11_Value); ] 函数使用
//*******************************************************************/

char Encoder_EC11_Scan(void);

//*******************************************************************/
//功能:对EC11旋转编码器的动作进行分析,并作出相应的动作处理代码
//形参:无
//返回:char AnalyzeResult = 0;目前无用。若在该函数里做了动作处理,则函数的返回值无需理会
//详解:对EC11旋转编码器的动作进行模式分析,是单击还是双击还是长按松手还是一直按下。形参从 [ char Encoder_EC11_Scan(unsigned char Set_EC11_TYPE) ] 函数传入。在本函数内修改需要的动作处理代码
//*******************************************************************/
char Encoder_EC11_Analyze(char EC11_Value);

#endif

这个程序我只做了正转,反转和按下,有需要自行修改,我这里利用的是定时器1毫秒扫描,所以刷新函数Encoder_EC11_Analyze(Encoder_EC11_Scan());我写在中断里面,初始化函数 EC11_Init(1);在这里需要注意一定位对应一脉冲;1(或非0)----两定位对应一脉冲,前面有说过了,放心食用

转自:https://www.jianshu.com/p/41fa67ecb248

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐