当前位置: 首页 > article >正文

STM32—旋转编码器控制直流电机(标准库)

本文使用 KY-040旋转编码器 通过TC1508A电机驱动模块来控制直流电机正转和反转(Speed:0-100),代码部分基于标准库,使用定时器输出比较两个通道来控制PWM输出。

一、KY-040旋转编码器

下图为KY-040旋转编码器,它有5个引脚:CLK、DT、SW、VCC、GND

作者是这么理解的:CLK是时钟,DT是数据。这两个引脚输出的时序反映了旋转编码器所产生的中断信号。SW是编码器按键,设置GPIO为上拉输入,当编码器被按下时,SW输出低电平。VCC正极,GND负极。

CLK和DT相当于编码器时序的A相和B相,如上图,设置这两个引脚的GPIO为下拉输入,上升沿触发中断。当正时针旋转编码器时,红色笔迹显示,在CLK(TI1)触发中断时,DT(TI2)为低电平。绿色笔迹显示,当DT(TI2)触发中断时,CLK(TI1)为高电平。 

CLK引脚产生的中断处理函数如下:

DT引脚产生的中断处理函数如下: 

按下编码器可以来设置正反转,这里我使用了LED灯的亮灭来反映正反转的状态,正亮,反灭。每按下一次,灯的状态翻转。再通过检测灯的状态来控制正反转(这一步按自己设计代码来,就相当于按键控制LED灯。我这里将这段代码写在main函数里,因为需要初始化LED

 我的代码不够简练,作为小白什么都不懂,只知道到处碰壁。

Encoder.c:

#include "stm32f10x.h"                  // Device header
#include "Encoder.h" 

int16_t Encoder_Count;					//全局变量,用于计数旋转编码器的增量值

/**
  * 函    数:旋转编码器初始化
  * 参    数:无
  * 返 回 值:无
  */
void Encoder_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(Encoder_GPIO_CLK, ENABLE);		    //开启GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		//开启AFIO的时钟,外部中断必须开启AFIO的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
	GPIO_InitStructure.GPIO_Pin = Encoder_GPIO_Pin_CLK | Encoder_GPIO_Pin_DT | Encoder_GPIO_Pin_SW;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(Encoder_GPIO_Port, &GPIO_InitStructure);						//将PB0和PB1引脚初始化为上拉输入

	/*AFIO选择中断引脚*/
	GPIO_EXTILineConfig(Encoder_GPIO_Port_Source, GPIO_PinSource0);//将外部中断的0号线映射到GPIOB,即选择PB0为外部中断引脚
  GPIO_EXTILineConfig(Encoder_GPIO_Port_Source, GPIO_PinSource1);//将外部中断的1号线映射到GPIOB,即选择PB1为外部中断引脚
	
	/*EXTI初始化*/
	EXTI_InitTypeDef EXTI_InitStructure;						//定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;		//选择配置外部中断的0号线和1号线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;					//指定外部中断线使能
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;			//指定外部中断线为中断模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;		//指定外部中断线为下降沿触发
	EXTI_Init(&EXTI_InitStructure);								//将结构体变量交给EXTI_Init,配置EXTI外设
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;			//选择配置NVIC的EXTI0线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设

	NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;			//选择配置NVIC的EXTI1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;			//指定NVIC线路的响应优先级为2
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
}

/**
  * 函    数:旋转编码器获取增量值
  * 参    数:无
  * 返 回 值:自上此调用此函数后,旋转编码器的增量值
  */
int16_t Encoder_Get(void)
{
	/*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/
	/*在这里,也可以直接返回Encoder_Count
	  但这样就不是获取增量值的操作方法了
	  也可以实现功能,只是思路不一样*/
	int16_t Temp;
	Temp = Encoder_Count;
	Encoder_Count = 0;
	return Temp;
}



/**
  * 函    数:EXTI0外部中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void EXTI0_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line0) == SET)		//判断是否是外部中断0号线触发的中断
	{
		/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
  if (GPIO_ReadInputDataBit(Encoder_GPIO_Port, Encoder_GPIO_Pin_CLK) == 1)
		{
			if (GPIO_ReadInputDataBit(Encoder_GPIO_Port, Encoder_GPIO_Pin_DT) == 0)		//下降沿触发中断,此时检测另一相电平,目的是判断旋转方向
			{
				Encoder_Count++;
        
			}
    }
		EXTI_ClearITPendingBit(EXTI_Line0);			//清除外部中断0号线的中断标志位
		
	}


}

///**
//  * 函    数:EXTI1外部中断函数
//  * 参    数:无
//  * 返 回 值:无
//  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
//  *           函数名为预留的指定名称,可以从启动文件复制
//  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
//  */
void EXTI1_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line1) == SET)		//判断是否是外部中断0号线触发的中断
	{
    if (GPIO_ReadInputDataBit(Encoder_GPIO_Port, Encoder_GPIO_Pin_DT) == 1)
		{
			if (GPIO_ReadInputDataBit(Encoder_GPIO_Port, Encoder_GPIO_Pin_CLK) == 0)	//下降沿触发中断,此时检测另一相电平,目的是判断旋转方向
			{
				Encoder_Count--;			//此方向定义为反转,计数变量自减
        
      }
    }

		EXTI_ClearITPendingBit(EXTI_Line1);			//清除外部中断0号线的中断标志位
		
	}

}

Encoder.h:

#ifndef __ENCODER_H
#define __ENCODER_H

#define Encoder_GPIO_CLK      RCC_APB2Periph_GPIOD
#define Encoder_GPIO_Port     GPIOD
#define Encoder_GPIO_Pin_CLK  GPIO_Pin_0
#define Encoder_GPIO_Pin_DT   GPIO_Pin_1
#define Encoder_GPIO_Pin_SW   GPIO_Pin_2
#define Encoder_GPIO_Port_Source   GPIO_PortSourceGPIOD

void Encoder_Init(void);
int16_t Encoder_Get(void);

#endif

二、TC1508电机驱动模块

  • IN1和IN2用来控制motor-A
  • IN3和IN4用来控制motor-B

 如下图,IN1输入PWM,配合IN2输入0,可实现直流电机正转;IN1输入0,配合IN2输入PWM,即可实现直流电机反转。PWM为调节速度所用。

于是在旋转编码器部分设置好后,再开始写PWM波形代码,PWM部分我使用的是定时器TIM3,TIM3有4个通道,我使用了 Channel1和Channel2 两个通道来分别输出PWM。

由于我的开发板上两个引脚PA6、PA7、PB4、PB5都被占用,所以使用TIM3完全重映像来设置PC6和PC7来输出IN1、IN2。

完全重映像代码

PWM设置两个通道的时候需要注意,先TIM_OC1Init,再GPIO_Init。如果顺序弄反,通道一和通道二无法对应PC6和PC7,我也不知道为什么。希望有大神在评论区解答一下,谢谢!

PWM_Init:

void PWM_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
  
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

    GPIO_PinRemapConfig(GPIO_FullRemap_TIM3,ENABLE);
	
	TIM_InternalClockConfig(TIM3);
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStruct.TIM_Period = 100-1;
	TIM_TimeBaseInitStruct.TIM_Prescaler = 720-1;
	TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
	
	TIM_OCInitTypeDef TIM_OCInitStruct;
	TIM_OCStructInit(&TIM_OCInitStruct);
	TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
	TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
	TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
	TIM_OCInitStruct.TIM_Pulse = 0;
	
	TIM_OC1Init(TIM3, &TIM_OCInitStruct);
	TIM_OC2Init(TIM3, &TIM_OCInitStruct);

	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &GPIO_InitStruct);
	
	TIM_Cmd(TIM3, ENABLE);
	
}
 

 然后连线,连线需要注意的是,TC1508A电机驱动模块的正负极容易接触不良。可以接完正负极,MOTORA连接电机后,通过一根线连接5v和IN1(或者IN2)测试电机是否正常启动。

主函数可以按自己想法来,作者这里的思路是:

1,在while之前,编码器初始化、PWM初始化。

2,while中,通过编码器转动产生中断获取速度的加减,通过按键中断翻转LED灯,再通过LED状态判断是正转还是反转。

3,如果是正转,通过TIM_SetCompare1(TIM3, Compare); 来设置速度,如果是反转,通过TIM_SetCompare2(TIM3, Compare); 来设置速度。

三、各模块代码部分

Encoder.c:

#include "stm32f10x.h"                  
#include "Encoder.h" 

int16_t Encoder_Count;					//全局变量,用于计数旋转编码器的增量值


void Encoder_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(Encoder_GPIO_CLK, ENABLE);		//开启GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		//开启AFIO的时钟,外部中断必须开启AFIO的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
	GPIO_InitStructure.GPIO_Pin = Encoder_GPIO_Pin_CLK | Encoder_GPIO_Pin_DT | Encoder_GPIO_Pin_SW;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(Encoder_GPIO_Port, &GPIO_InitStructure);						//将PB0和PB1引脚初始化为上拉输入

	/*AFIO选择中断引脚*/
	GPIO_EXTILineConfig(Encoder_GPIO_Port_Source, GPIO_PinSource0);//将外部中断的0号线映射到GPIOB,即选择PB0为外部中断引脚
  GPIO_EXTILineConfig(Encoder_GPIO_Port_Source, GPIO_PinSource1);//将外部中断的1号线映射到GPIOB,即选择PB1为外部中断引脚
	
	/*EXTI初始化*/
	EXTI_InitTypeDef EXTI_InitStructure;						//定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;		//选择配置外部中断的0号线和1号线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;					//指定外部中断线使能
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;			//指定外部中断线为中断模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;		//指定外部中断线为下降沿触发
	EXTI_Init(&EXTI_InitStructure);								//将结构体变量交给EXTI_Init,配置EXTI外设
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;			//选择配置NVIC的EXTI0线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设

	NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;			//选择配置NVIC的EXTI1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;			//指定NVIC线路的响应优先级为2
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
}


int16_t Encoder_Get(void)
{
	/*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/
	/*在这里,也可以直接返回Encoder_Count
	  但这样就不是获取增量值的操作方法了
	  也可以实现功能,只是思路不一样*/
	int16_t Temp;
	Temp = Encoder_Count;
	Encoder_Count = 0;
	return Temp;
}


void EXTI0_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line0) == SET)		//判断是否是外部中断0号线触发的中断
	{
		
  if (GPIO_ReadInputDataBit(Encoder_GPIO_Port, Encoder_GPIO_Pin_CLK) == 1)  // 再次判断引脚电平,以避免抖动,其实没必要
		{
			if (GPIO_ReadInputDataBit(Encoder_GPIO_Port, Encoder_GPIO_Pin_DT) == 0)		
			{
				Encoder_Count++;
			}
    }
		EXTI_ClearITPendingBit(EXTI_Line0);			//清除中断标志位
		
	}


}

void EXTI1_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line1) == SET)		 //判断是否是外部中断0号线触发的中断
	{
    if (GPIO_ReadInputDataBit(Encoder_GPIO_Port, Encoder_GPIO_Pin_DT) == 1)
		{
			if (GPIO_ReadInputDataBit(Encoder_GPIO_Port, Encoder_GPIO_Pin_CLK) == 0)
			{
				Encoder_Count--;			//此方向定义为反转,计数变量自减  
      }
    }
    
		EXTI_ClearITPendingBit(EXTI_Line1);			//清除中断标志位
	}

}

Encoder.h:

#ifndef __ENCODER_H
#define __ENCODER_H

#define Encoder_GPIO_CLK      RCC_APB2Periph_GPIOD
#define Encoder_GPIO_Port     GPIOD
#define Encoder_GPIO_Pin_CLK  GPIO_Pin_0
#define Encoder_GPIO_Pin_DT   GPIO_Pin_1
#define Encoder_GPIO_Pin_SW   GPIO_Pin_2
#define Encoder_GPIO_Port_Source   GPIO_PortSourceGPIOD

void Encoder_Init(void);
int16_t Encoder_Get(void);

#endif

PWM.c:

#include "stm32f10x.h"              
#include "PWM.h" 


void PWM_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
  
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

  GPIO_PinRemapConfig(GPIO_FullRemap_TIM3,ENABLE);
	
	TIM_InternalClockConfig(TIM3);
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInitStruct.TIM_Period = 100-1;
	TIM_TimeBaseInitStruct.TIM_Prescaler = 720-1;
	TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
	
	TIM_OCInitTypeDef TIM_OCInitStruct;
	TIM_OCStructInit(&TIM_OCInitStruct);
	TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
	TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
	TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
	TIM_OCInitStruct.TIM_Pulse = 0;
	
	TIM_OC1Init(TIM3, &TIM_OCInitStruct);
	TIM_OC2Init(TIM3, &TIM_OCInitStruct);

	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &GPIO_InitStruct);
	
	TIM_Cmd(TIM3, ENABLE);
	
}
 


void TIM3_SetCompare1(uint16_t Compare)
{	
	TIM_SetCompare1(TIM3, Compare);		//设置CCR1的值
}


void TIM3_SetCompare2(uint16_t Compare)
{	
	TIM_SetCompare2(TIM3, Compare);		//设置CCR1的值
}

PWM.h:

#ifndef __PWM_H
#define __PWM_H


void PWM_Init(void);
void TIM3_SetCompare1(uint16_t Compare);
void TIM3_SetCompare2(uint16_t Compare);


#endif

main.c: 部分代码,因为作者主函数代码太多,删减了一部分:OLED显示、串口显示......

#include "stm32f10x.h"                  // Device header
#include "LED.h"
#include "Encoder.h"
#include "PWM.h"


uint8_t i;			          //定义for循环的变量
	
int16_t Num;			//定义待被旋转编码器调节的变量
int16_t Speed;			


int main(void)
{
  /*模块初始化*/
  LED_Init();
  PWM_Init();
  Encoder_Init();		//旋转编码器初始化

	while (1)
	{
      
		Num += Encoder_Get();				//获取自上此调用此函数后,旋转编码器的增量值,并将增量值加到Num上
        Speed = Num*10;
        if(Speed>100)
    {
      Speed=100;
    }
    else if(Speed<0)
    {
      Speed=0;
    }
    
    if (GPIO_ReadInputDataBit(Encoder_GPIO_Port,Encoder_GPIO_Pin_SW) == 0)			//读PB1输入寄存器的状态,如果为1,则代表按键1按下
    {
      Delay_ms(10);											//延时消抖
      while (GPIO_ReadInputDataBit(Encoder_GPIO_Port,Encoder_GPIO_Pin_SW) == 0);	//等待按键松手
      Delay_ms(10);											//延时消抖
      LED1_Turn();
      Speed=00;
      Num=0;
    }
    
    if((GPIO_ReadOutputDataBit(LED1_GPIO_Port, LED1_GPIO_Pin) == 1))
    {
      TIM3_SetCompare1(Speed);
      TIM3_SetCompare2(0);
    }
    else
    {
      TIM3_SetCompare1(0);
      TIM3_SetCompare2(Speed);
    }
    
  }
}

整体代码,需要根据自己的实际情况修改,比如不用重映像、不用LED等。


http://www.kler.cn/news/350662.html

相关文章:

  • qt QNetworkProxy详解
  • 使用js-enumerate报错Cannot set properties of undefined
  • Linux可分配内存和空闲内存
  • 虚拟机安装麒麟v10、配置网络、安装docker
  • 深度学习:SGD的缺点
  • 关系型数据库(1)----MySQL(初阶)
  • Luminar Neo v1.21.0.13934 图像编辑软件绿色便携版
  • Angular 19 的改进功能
  • torch.fft 出现 ComplexHalf 或 Half 不支持
  • java互联网医院智能导诊系统源码,Uniapp前端开发框架,支持一次编写,多端运行
  • 分享一个图片RGB以及16进制颜色提取的在线网站
  • Lobby——网络游戏大厅设计与参考建议!!!
  • FreeRTOS:事件标志组
  • SpringMVC源码-接口请求执行流程,包含九大内置组件的实例化初始化,拦截器调用,页面渲染等源码讲解
  • K8s中pod 间通信的两种情况
  • Oracle中解决select into值集为空的报错情况
  • 洛谷 P1106:删数问题 ← 贪心算法
  • 常见的微控制器(ESP8266、ESP32,、51单片机、stm32)区别
  • 自感式压力传感器结构设计
  • 私有聊天平台的数据管理策略与分析实践
  • 什么是 SQL 注入
  • 深入剖析 Java Spring 中的 @Autowired、@Resource、@Qualifier、@Inject 注解:使用详解与注意事项
  • Vue的响应式原理
  • Linux查看处理器信息
  • Adobe Illustrator如何在图片插入latex公式
  • VS code连接远程服务器