什么是自然大小调?1=C 4/4是什么意思?

本文详细解读了音乐中的音调系统,包括大调、小调的概念及区别,以及如何通过不同音符间的半音与全音关系来构建旋律。同时介绍了音乐节拍的基本概念,包括四分音符、八分音符等节奏单位及其在不同拍子中的应用。通过实例解释了如何在实际创作中灵活运用这些音乐元素。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

From: http://blog.sina.com.cn/s/blog_7595241d0100w7il.html



   我们都知道1、2、3、4、5、6、7(Do、Re、Mi、Fa、Sol、La、Si),在这七个音中,我们可以选择任意一个音作为主要的音,其他音围绕着它发展,写出来的旋律味道是很不一样的,一般接触到的大都是以1、6、5为主音的。以1(Do)为主音就是自然大调,以6为主音就是自然小调,它们的音阶排列顺序分别为:自然大调1(主音)、2(上主音)、3(上中音)、4(下属音)、5(属音)、6(下中音)、7(导音);自然小调6(主音)、7(上主音)、1(上中音)、2(下属音)、3(属音)、4(下中音)、5(导音)。自然大调和自然小调也就是我们平常说的大调和小调,大调色彩明朗,适合写欢快、抒情、温暖的歌;小调色彩黯淡,适合写忧伤抑郁的歌;但这并不是绝对的,大调也可以写出忧伤的旋律,小调则可以写出非常气势磅礴的歌,这就要看旋律的具体安排了,还有演唱者的理解发挥、以及编曲等等。有一点我们要记住,无论大调还是小调,3跟4,7跟1之间,是半音关系,在吉他上就是相邻的两个音,而其他音之间都是全音关系,在吉他上就是隔了一个品格。

    至于1=C4/4的意思,要分几层去理解:音乐上使用的音有七个基本音级——C、D、E、F、G、A、B,加入升降号还可以得到其他变化音级——#C(bD)等等,我们可以把其中任何一个音唱作Do(1),把C唱作Do就是C调,记作1=C,这样我们就知道了,C调的音阶就是:C、D、E、F、G、A、B(1、2、3、4、5、6、7),也就是说E跟F,B跟C是半音关系,不管放在哪都是这样。依此类推,把D唱作Do就是D调,记作1=D,根据大调音阶的关系(3跟4,7跟1之间,是半音关系,其他音之间都是全音关系),D大调音阶包括的音就是1(D)、2(E)、3(#F)、4(G)、5(A)、6(B)、7(#C),其他调也是这么推算的,大家可以自己试试,然后再结合自己知道的某调的和弦去理解,就知道为什么每个调和弦不一样了。4/4的分母是说以四分音符为一拍,我们常说拍子拍子,至于怎样才叫一拍,是人规定的,也就是说可以有多种情况,人们先设定我们常见的那种一小节四拍的,也也就是音符下面没有横线,后面没有附点、横线的,这样的四个音合起来叫做一个全音符,那么每一个音就是整体的四分之一,称作四分音符,每一个四分音符再分作两份,就相当于把一个全音符分成了八份,每一个音符就叫做一个八分音符,每个音符下面有一条短线,这样的一小节就可以有八个八分音,同样还可以推出16分音符(下面两条横线的),32分音符(下面三条横线的);分子是说在一个小节里一共有几拍,也就是说多少拍重复一个节奏循环;现在我们把一个四分音符作为一拍,每小节四拍,就是4/4拍了,同样还有3/4拍、2/4拍;而如果把八分音符作为一拍,每小节6拍,就是我们看到的6/8拍;每种节拍都有自己的节奏强弱规律,反复出现,我们才能感觉到节奏的存在,4/4拍的就是强、弱、次强、弱,3/4拍的是强、弱、弱,2/4的是强、弱,6/8拍的是强、弱、弱、次强、弱、弱,我们演奏的时候也要突出这点。


#include "main.h" #include "i2c.h" #include "tim.h" #include "usart.h" #include "gpio.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "math.h" #include "oled.h" #include "stdio.h" #include <string.h> #define KEY1 HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3) #define KEY0 HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_4) #define PPR 13 #define ENCODER_MULTIPLY 4 #define GEAR_RATIO 20 // 计算每圈对应的计数 #define COUNTS_PER_REV (PPR * ENCODER_MULTIPLY * GEAR_RATIO) /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); /* USER CODE BEGIN PFP */ /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /* 速度环PID参数 */ float InnerTarget=0, InnerActual=0,Innerout=0;//目标值,实际值,输出值ֵ float InnerKp=1.5; float InnerKi=0.18; float InnerKd=0.1;//比例项积分项微分项的权重 float InnerError0=0,InnerError1=0,InnerErrorInt=0,InnerIntout=0;//本次误差,上次误差,误差积分 /* 位置环PID参数 */ float PositionTarget = 0, PositionActual = 0, Positionout = 0;//目标值,实际值,输出值ֵ float PositionKp = 0.4; float PositionKi = 0.0; float PositionKd = 0.03;//比例项积分项微分项的权重 float PositionError0 = 0, PositionError1 = 0, PositionErrorInt = 0, PositionIntOut = 0;//本次误差,上次误差,误差积分 extern unsigned char Hzk[][32]; uint32_t ccr_value=0; uint32_t arr_value; unsigned int x=0; int motor_direction; uint8_t rx_data; unsigned char* word0=(uint8_t *)"**<qianchen>**"; unsigned char* time=(uint8_t *)"*>2025-07-06<*"; unsigned char* word1=(uint8_t *)"Count:"; unsigned char* word2=(uint8_t *)"PWM:"; unsigned char* word3=(uint8_t *)"Speed:"; unsigned char* word4=(uint8_t *)"Target:"; unsigned char* word5=(uint8_t *)"Actual:"; unsigned char* word6=(uint8_t *)"Out:"; unsigned char* word7=(uint8_t *)"P_T:"; unsigned char* word8=(uint8_t *)"P_A:"; unsigned char* word9=(uint8_t *)"P_O:"; unsigned char* word10=(uint8_t *)"I_T:"; unsigned char* word11=(uint8_t *)"I_A:"; unsigned char* word12=(uint8_t *)"I_O:"; uint16_t count; float speed=0.0; uint8_t str_buff[64]; void delay(unsigned int x) { while(x--); } void Scan_Keys() { if(KEY0==GPIO_PIN_RESET) { delay(1000); if(KEY0==GPIO_PIN_RESET) { while(KEY0==GPIO_PIN_RESET); PositionTarget+=300; } } if(KEY1==GPIO_PIN_RESET) { delay(1000); if(KEY1==GPIO_PIN_RESET) { while(KEY1==GPIO_PIN_RESET); PositionTarget-=300; } } } void OLED_display_info() { OLED_Clear(); OLED_ShowString(8,0,word0,sizeof(word0)); OLED_ShowString(5,1,time,sizeof(time)); // OLED_ShowString(8,2,word1,sizeof(word1)); // OLED_ShowString(8,3,word2,sizeof(word2)); // OLED_ShowString(8,4,word3,sizeof(word3)); // OLED_ShowString(8,5,word4,sizeof(word4)); // OLED_ShowString(8,6,word5,sizeof(word5)); // OLED_ShowString(8,7,word6,sizeof(word6)); OLED_ShowString(8,2,word7,sizeof(word7)); OLED_ShowString(8,3,word8,sizeof(word8)); OLED_ShowString(8,4,word9,sizeof(word9)); OLED_ShowString(8,5,word10,sizeof(word10)); OLED_ShowString(8,6,word11,sizeof(word11)); OLED_ShowString(8,7,word12,sizeof(word12)); // OLED_ShowCHinese(0,3,0); // OLED_ShowCHinese(18,3,1); // OLED_ShowCHinese(36,3,2); // OLED_ShowCHinese(54,3,3); // OLED_ShowCHinese(0,6,4); // OLED_ShowCHinese(18,6,5); // OLED_ShowCHinese(36,6,6); // OLED_ShowCHinese(54,6,7); } void OLED_display_dat() { // sprintf((char*)str_buff,"%5d",count); // OLED_ShowString(64,2,str_buff,sizeof(str_buff)); // // arr_value = __HAL_TIM_GET_AUTORELOAD(&htim2); // 获取ARR值 // float duty_cycle = ((float)ccr_value / arr_value) * 100.0f; // 计算占空比 // // sprintf((char*)str_buff,"%+.0f",duty_cycle); // OLED_ShowString(64,3,str_buff,sizeof(str_buff)); // // sprintf((char*)str_buff,"%+.0f",speed); // OLED_ShowString(64,4,str_buff,sizeof(str_buff)); sprintf((char*)str_buff,"%+.0f",PositionTarget); OLED_ShowString(64,2,str_buff,sizeof(str_buff)); sprintf((char*)str_buff,"%+.0f",PositionActual); OLED_ShowString(64,3,str_buff,sizeof(str_buff)); sprintf((char*)str_buff,"%+.0f",Positionout); OLED_ShowString(64,4,str_buff,sizeof(str_buff)); sprintf((char*)str_buff,"%+.0f",InnerTarget); OLED_ShowString(64,5,str_buff,sizeof(str_buff)); sprintf((char*)str_buff,"%+.0f",InnerActual); OLED_ShowString(64,6,str_buff,sizeof(str_buff)); sprintf((char*)str_buff,"%+.0f",Innerout); OLED_ShowString(64,7,str_buff,sizeof(str_buff)); } uint16_t last_count = 0; uint16_t last_time_ms = 0; //void calculate_speed() //{ // count = __HAL_TIM_GET_COUNTER(&htim3); // uint16_t current_time_ms = HAL_GetTick(); // int16_t diff_count = count - last_count; // float delta_time_sec = (current_time_ms - last_time_ms) / 1000.0f; // // if (delta_time_sec > 0) // { // float rpm = ((float)diff_count / (float)COUNTS_PER_REV) * 60.0f / delta_time_sec; // // speed=rpm; // // InnerActual=speed/1220.0f*100; // // InnerError1 = InnerError0; // InnerError0 = InnerTarget - InnerActual; //// ErrorInt += Error0; // InnerIntOut += InnerKi * InnerError0; // // /*积分项输出限幅*/ // if(InnerIntOut>1000)InnerIntOut=1000; // if(InnerIntOut<0)InnerIntOut=0; // // Innerout = InnerKp * InnerError0 + InnerIntOut + InnerKd * (InnerError0 - InnerError1); // // if(Innerout > 1000)Innerout = 1000; // if(Innerout < 0)Innerout = 0; // // __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Innerout); // } // last_count = count; // last_time_ms = current_time_ms; //} void set_motor_direction_and_pwm(float pwm_value) { // 限制PWM范围 pwm_value = fmaxf(fminf(pwm_value, 1000.0f), 0); // 设置方向引脚(假设DIR_PIN控制方向) if (motor_direction > 0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // 正转 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // 反转 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); } // 设置PWM占空比 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pwm_value); } void calculate_Position() { count = __HAL_TIM_GET_COUNTER(&htim3); uint16_t current_time_ms = HAL_GetTick(); int16_t diff_count = count - last_count; float delta_time_sec = (current_time_ms - last_time_ms) / 1000.0f; if (delta_time_sec > 0) { // float rpm = ((float)diff_count / (float)COUNTS_PER_REV) * 60.0f / delta_time_sec; // // speed=rpm; // 更新位置实际值(编码器脉冲累计) PositionActual += diff_count; // 位置环控制:计算误差和方向 PositionError0 = PositionTarget - PositionActual; motor_direction = (PositionError0 >= 0) ? 1 : -1; // 目标>实际时正转,否则反转 // 位置环PID输出(取绝对值作为速度目标) PositionErrorInt += PositionError0; PositionIntOut = PositionKp * PositionError0 + PositionKi * PositionErrorInt; // 限制积分输出 PositionIntOut = fmaxf(fminf(PositionIntOut, 1000.0f), -1000.0f); // 速度环目标值(绝对值) InnerTarget = fabs(PositionIntOut); // 速度环目标值(绝对值) InnerTarget = fabs(PositionIntOut); // 速度环控制(InnerActual为编码器脉冲差值) InnerError0 = InnerTarget - fabs(diff_count); InnerIntout += InnerKi * InnerError0; InnerIntout = fmaxf(fminf(InnerIntout, 1000.0f), 0); // 速度环输出非负 Innerout = InnerKp * InnerError0 + InnerIntout; Innerout = fmaxf(fminf(Innerout, 1000.0f), 0); // 设置电机方向和PWM set_motor_direction_and_pwm(Innerout); // //速度环实际值 = 本周期编码器脉冲差值(或累计值) // InnerActual = PositionActual; // // //位置环控制 // PositionError1 = PositionError0; // PositionError0 = PositionTarget - PositionActual; // PositionErrorInt += PositionError0; // // /*积分项输出限幅*/ // if(PositionIntOut>1000)PositionIntOut=1000; // if(PositionIntOut<0)PositionIntOut=0; // Positionout = PositionKp * PositionError0 + PositionKi * PositionErrorInt + PositionKd * (PositionError0 - PositionError1); // // 确定方向和速度目标 // motor_direction = (Positionout >= 0) ? 1 : -1; // if(motor_direction==1) // { // HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1, GPIO_PIN_SET); // HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2, GPIO_PIN_RESET); // } // else if(motor_direction==-1) // { // HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1, GPIO_PIN_RESET); // HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2, GPIO_PIN_SET); // } // InnerTarget = fabs(Positionout); // InnerTarget = Positionout; // InnerError1 = InnerError0; // InnerError0 = InnerTarget - InnerActual; //// ErrorInt += Error0; // InnerIntout += InnerKi * InnerError0; // // /*积分项输出限幅*/ // if(InnerIntout>1000)InnerIntout=1000; // if(InnerIntout<0)InnerIntout=0; // // Innerout = InnerKp * InnerError0 + InnerIntout + InnerKd * (InnerError0 - InnerError1); // // if(Innerout > 1000)Innerout = 1000; // if(Innerout < 0)Innerout = 0; // // __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Innerout); } last_count = count; last_time_ms = current_time_ms; } //void dianjispeed() //{ // switch(x) // { // case 0: PositionTarget = 0; break; // case 1: PositionTarget = 300; break; // case 2: PositionTarget = 500; break; // case 3: PositionTarget = 800; break; // } //// __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,ccr_value); //} //void Send_PID_Data(float target, float actual, float output) //{ // char buffer[64]; // sprintf(buffer, "T:%.2f,A:%.2f,O:%.2f\r\n", target, actual, output); // HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY); //} /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_TIM2_Init(); // MX_TIM3_Init(); // MX_I2C1_Init(); // MX_TIM4_Init(); // MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ // OLED_Init(); // OLED_display_info(); // // 启动编码器 // HAL_TIM_Encoder_Start(&htim3,TIM_CHANNEL_ALL); // // 启动串口接收中断 // HAL_UART_Receive_IT(&huart1, &rx_data, 1); // 启动定时器并启用中断 HAL_TIM_Base_Start_IT(&htim4); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1, GPIO_PIN_SET); // HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2, GPIO_PIN_RESET); HAL_TIM_PWM_Start_IT(&htim2,TIM_CHANNEL_1); // PositionActual = 0; // 初始化位置实际值 while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ Scan_Keys(); // Send_PID_Data(PositionTarget, PositionActual, Positionout); // dianjispeed(); // __HAL_TIM_SET_COUNTER(&htim3,0); // HAL_Delay(500); } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } } /* USER CODE BEGIN 4 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim == &htim4) { calculate_Position(); // 计算速度 OLED_display_dat(); // 更新 OLED 显示 } } 以上是用位置环速度环双环PID算法控制MG310编码器电机的代码,如果改为控制步进电机转动指定角度该如何修改以上代码?
最新发布
07-14
<think>我们正在处理一个将双环PID控制从普通电机迁移到步进电机的任务。步进电机与普通直流电机的主要区别在于: 1. 步进电机通过脉冲控制,每个脉冲对应一个步进角度(如1.8度,即200步/转) 2. 步进电机通常开环控制,但也可以加入编码器实现闭环控制(即我们这里的情况) 3. 控制目标:转动到指定角度(位置环),同时可能需要控制速度(速度环) 原双环PID代码(假设为直流电机设计)通常包含: - 位置环PID:计算期望位置与实际位置的偏差,输出目标速度 - 速度环PID:计算目标速度与实际速度的偏差,输出控制量(如PWM占空比) 对于步进电机,控制输出不是PWM占空比,而是脉冲(包括脉冲频率和方向)。因此,我们需要将速度环的输出转换为脉冲频率,同时方向信号由位置偏差的符号决定(或速度的正负)。 修改思路: 1. 位置环:输入为设定角度(目标位置)和当前角度(编码器反馈),输出目标速度(单位:度/秒或步/秒) 2. 速度环:输入为目标速度和当前速度(通过编码器计算得到),输出为控制量。但步进电机的控制量是脉冲频率(即速度)和方向,因此速度环的输出实际上就是脉冲频率(频率与速度成正比)。 然而,步进电机通常通过改变脉冲频率来控制速度,所以速度环的输出可以直接作为脉冲频率。但注意,步进电机有一个最启动频率和最运行频率,需要限制。 另一种思路:由于步进电机是开环控制速度(通过脉冲频率),我们实际上可以省略速度环,只使用位置环。位置环的输出直接作为脉冲频率(但需要将位置环的输出转换为频率,同时方向由位置偏差的符号决定)。但是,如果要求精确的速度控制(如加减速过程平滑),则可能需要速度环。 考虑到步进电机在开环控制时通常使用梯形或S形加减速来控制脉冲频率的变化,以实现平滑运动。如果使用双环PID,我们可以将速度环的输出作为脉冲频率,但需要将位置环的输出(目标速度)进行限幅(防止超过步进电机的最运行频率),同时速度环的输出也要限制在合理范围内。 具体修改步骤: 1. 将位置环的输出作为目标速度(单位:步/秒,或脉冲频率Hz)。注意:步进电机的脉冲频率f(Hz)与转速v(步/秒)的关系是:v = f。因为一个脉冲走一步,所以频率就是每秒的步数。 因此,位置环的输出可以理解为目标脉冲频率(即目标速度)。 2. 速度环的输入为目标速度(位置环输出)和当前速度(通过编码器反馈计算得到:单位时间内步数的变化量)。速度环的输出通常是一个控制量,但在步进电机中,这个控制量可以直接作为脉冲频率(因为步进电机每一步都需要一个脉冲,所以脉冲频率就是速度)。但是,我们也可以将速度环的输出作为脉冲频率的增量(即改变频率的变化率),这样实现起来比较复杂。 3. 更直接的方法:将位置环的输出(目标速度)经过速度环的PID计算后,得到的是一个整量,然后加上当前速度?或者直接作为最终的目标脉冲频率?这需要根据控制策略来定。 实际上,在步进电机的双环控制中,位置环的输出作为速度环的设定值,速度环的输出直接作为控制脉冲的频率。即: 目标位置 -> 位置环PID -> 目标速度(v_target) 目标速度(v_target)和当前速度(v_current) -> 速度环PID -> 输出脉冲频率(f_output) 注意:脉冲频率f_output就是步进电机的运行速度(步/秒),所以速度环的输出就是我们要设置的脉冲频率。 4. 方向控制:根据位置偏差的符号(目标位置-当前位置)确定方向。如果位置偏差为正,则正方向;为负,则反方向。在位置环中,偏差的符号自然决定了速度环目标速度的符号(但速度环输出频率是绝对值,所以方向需要单独控制)。 5. 步进电机驱动器通常需要两个信号:方向(DIR)和脉冲(PUL)。因此,我们需要: 方向信号:由位置偏差(目标位置-当前位置)决定,如果偏差>0,则正方向;偏差<0,则反方向(偏差为0时,方向可保持上一个方向或停止)。 脉冲频率:由速度环输出(绝对值)决定,但需要取绝对值,因为频率为正。 6. 位置环和速度环的PID计算: 位置环:error_position = target_position - current_position output_velocity = Kp_pos * error_position + (积分项) + (微分项) [注意:输出是目标速度] 速度环:error_velocity = output_velocity - current_velocity [注意:这里output_velocity是位置环输出的目标速度] output_frequency = Kp_vel * error_velocity + (积分项) + (微分项) 7. 但是,这里有一个问题:位置环输出的目标速度可能很,我们需要对其进行限幅(不能超过步进电机的最运行频率)。同样,速度环的输出也要限幅。 8. 步进电机的控制频率:由于步进电机每一步都需要一个脉冲,所以控制周期应该尽可能短(通常使用定时器中断产生脉冲)。在控制代码中,我们需要根据计算出的脉冲频率设置定时器的重装值(ARR)和预分频(PSC)来产生相应频率的脉冲。 9. 编码器反馈:步进电机通常不使用编码器(开环),但闭环控制需要加装编码器。编码器的读数用于计算当前位置和当前速度。当前位置:通过编码器计数得到(注意编码器的分辨率)。当前速度:可以通过两次位置差除以时间得到(或使用M法测速)。 10. 代码结构修改: - 初始化:设置PID参数、编码器、定时器(用于脉冲输出)等。 - 主循环或中断中: 读取编码器得到当前位置current_position 计算位置环PID:得到目标速度v_target(注意限幅) 读取当前速度(或者通过编码器计算速度)current_velocity 计算速度环PID:得到输出频率f_output(取绝对值,并限幅) 根据位置偏差的符号设置方向信号 设置定时器产生频率为f_output的脉冲(通过改变ARR和PSC) 11. 注意:步进电机在停止时(位置偏差为0),应该停止发送脉冲(即f_output=0),否则会持续产生热量。 12. 步进电机的步距角:假设为1.8度,则每转200步。如果编码器是直接安装在电机轴上的,那么编码器每转的脉冲数(PPR)需要转换为角度(或步数)。例如,如果编码器是1000线,则每转4000个脉冲(4倍频)。那么,一个编码器脉冲对应0.09度(360/4000)。而步进电机一步是1.8度,所以需要将位置单位统一(建议统一为步数): 目标位置(步)= 目标角度(度) / 步距角(度/步) = 目标角度 / 1.8 当前位置(步)= 编码器累计值 / (编码器每转脉冲数) * (电机每转步数) = 编码器累计值 * (200) / (4000) [假设编码器每转4000脉冲] = 编码器累计值 / 20 这样,位置环的偏差单位是步数。 13. 速度单位:步/秒。编码器反馈的速度可以通过单位时间内的步数变化量得到。 14. 代码示例(伪代码): 假设我们使用定时器中断来更新PID计算(比如1ms中断一次) // 全局变量 float target_angle = 90.0; // 目标角度90度 float step_angle = 1.8; // 步距角 int steps_per_rev = 200; // 每转200步 int encoder_ppr = 4000; // 编码器每转4000脉冲(1000线4倍频) // PID参数 PID pos_pid, vel_pid; void TIM_Interrupt() // 假设1ms定时中断 { // 读取编码器 int32_t encoder_count = read_encoder(); // 计算当前位置(步) float current_position = (float)encoder_count * steps_per_rev / encoder_ppr; // 注意:这里encoder_count可能是多圈的,需要处理溢出 // 计算目标位置(步) float target_position = target_angle / step_angle; // 位置环PID float v_target = pos_pid.calculate(target_position, current_position); // 对v_target限幅,比如最速度限制在500步/秒 v_target = constrain(v_target, -500, 500); // 计算当前速度(步/秒):使用两次位置差除以时间(比如用上一次位置和当前时间差) static float last_position = 0; static uint32_t last_time = 0; uint32_t current_time = millis(); float dt = (current_time - last_time) / 1000.0; // 单位秒 float current_velocity = (current_position - last_position) / dt; last_position = current_position; last_time = current_time; // 速度环PID float f_output = vel_pid.calculate(v_target, current_velocity); // 取绝对值,因为频率为正,方向由位置偏差的符号决定 float abs_f_output = fabs(f_output); // 限幅,比如0到500Hz abs_f_output = constrain(abs_f_output, 0, 500); // 方向 if (target_position - current_position > 0) { set_direction(FORWARD); } else if (target_position - current_position < 0) { set_direction(BACKWARD); } // 设置脉冲频率(如果abs_f_output为0,则停止脉冲) if (abs_f_output > 0) { set_pulse_frequency(abs_f_output); // 设置定时器产生该频率的脉冲 enable_pulse(); } else { disable_pulse(); } } 15. 注意:上述代码中,速度环的目标速度v_target是由位置环输出的,它可能为正或负(表示方向),但速度环的反馈量current_velocity是实际速度(绝对值?还是带符号?)。这里我们应使用带符号的速度(即正负表示方向),因此速度环的误差计算是v_target(带符号)减去current_velocity(带符号)。这样,速度环的输出f_output可能是正的也可能是负的,但我们取绝对值作为脉冲频率(因为频率是正的),而方向由位置偏差决定(而不是速度偏差)。这样做的原因是:位置偏差决定了我们要往哪个方向走,而速度环只是控制电机以多快的速度往那个方向走(即使实际速度方向与目标速度方向相反,也可以通过位置偏差的方向来纠正)。 16. 另一种做法:将位置环的输出v_target的绝对值作为速度环的目标速度(绝对值),然后方向由v_target的符号决定。但这样速度环只控制速度的小,方向由位置环决定。这样速度环的输入都是正数(目标速度小和实际速度小),然后输出脉冲频率(正数)。这样可能更简单,但可能会在过零时(比如目标位置在当前位置附近来回变动)出现方向频繁切换的问题。 17. 建议采用带符号的速度控制:即位置环输出的v_target是带符号的(正表示正方向,负表示反方向),速度环的输入也是带符号的(目标速度v_target,实际速度current_velocity=带符号,根据编码器变化的方向决定正负)。然后速度环的输出f_output也是带符号的,但我们取绝对值作为脉冲频率,方向由v_target的符号决定(或者由位置偏差的符号决定,两者应该一致)。这样,速度环可以纠正由于负载等原因导致的速度偏差(包括方向)。 18. 但是,实际中,由于步进电机在开环控制时方向是单独控制的,所以速度环的输出符号并不直接控制方向,而是通过位置环的偏差符号来控制方向。因此,在速度环中,我们只关心速度的小,而方向由位置环决定。这样,速度环可以设计为只控制速度小(即目标速度为|v_target|,实际速度为|current_velocity|)。这样速度环的输入都是正数,输出也是正数(脉冲频率)。 19. 综合考虑,我们采用如下策略: 位置环:输入为位置偏差,输出为目标速度v_target(带符号,正表示正方向,负表示反方向) 方向信号:由v_target的符号决定(v_target>0为正方向,v_target<0为反方向) 速度环:输入为|v_target|(目标速度小)和|current_velocity|(实际速度小),输出为脉冲频率(正数) 20. 这样修改后,速度环只负责控制速度的小(即脉冲频率),而方向由位置环决定。这样避免了在速度环中处理方向问题。 21. 代码整: 在位置环计算后: float v_target = pos_pid.calculate(target_position, current_position); // 带符号 float abs_v_target = fabs(v_target); // 方向 if (v_target >= 0) { set_direction(FORWARD); } else { set_direction(BACKWARD); } // 速度环:输入目标速度小和实际速度小 float current_speed = fabs(current_velocity); // 实际速度小(绝对值) float f_output = vel_pid.calculate(abs_v_target, current_speed); // 输出脉冲频率(正数) 22. 注意:这样处理,速度环无法纠正方向错误(比如电机被外力推向反方向),但位置环会检测到位置偏差增,从而输出更的v_target(绝对值),然后速度环会增加频率,同时方向信号会试图纠正。另外,由于位置环的存在,最终会纠正位置。 23. 另一种情况:如果电机被外力推动,导致位置偏差减小,那么位置环输出的v_target会减小,速度环也会减小频率,最终会停止。但如果外力导致位置偏差反向,那么位置环的输出v_target会反向(符号改变),方向信号改变,速度环的目标速度小重新计算,从而反向加速。 24. 因此,这种结构是可行的。 25. 最后,关于PID的积分项,需要注意积分限幅和积分饱和问题,特别是位置环的输出限幅和速度环的输出限幅。 26. 步进电机在启动时,如果频率过高可能会失步,所以需要从较低频率开始启动。但在我们的控制中,由于位置环的输出目标速度是从0开始(如果初始偏差,位置环输出可能会很,所以需要位置环输出限幅,同时速度环的输出限幅也要设置一个合理的最值,比如300Hz)。另外,也可以使用梯形加减速算法,但在PID控制中,通过位置环和速度环的节,理论上可以实现加减速,但可能不如梯形加减速平滑。因此,如果要求快速且平滑,可以在位置环前设置一个轨迹生成器(生成平滑的位置指令和速度指令)。 27. 总结修改步骤: a. 统一位置单位(步数) b. 位置环:输入为位置偏差,输出为带符号的目标速度(步/秒) c. 方向由目标速度的符号决定 d. 速度环:输入为目标速度的绝对值(步/秒)和实际速度的绝对值(步/秒),输出为脉冲频率(Hz) e. 设置脉冲频率和方向信号 f. 注意PID的限幅和抗饱和处理 28. 代码示例(基于上述思路): 假设我们有一个PID类: class PID { public: PID(float kp, float ki, float kd, float min_output, float max_output) : kp_(kp), ki_(ki), kd_(kd), min_(min_output), max_(max_output), integral_(0), last_error_(0) {} float calculate(float setpoint, float measurement) { float error = setpoint - measurement; integral_ += error; // 积分限幅(抗饱和) if (integral_ > max_integral_) integral_ = max_integral_; else if (integral_ < min_integral_) integral_ = min_integral_; float derivative = error - last_error_; last_error_ = error; float output = kp_ * error + ki_ * integral_ + kd_ * derivative; // 输出限幅 if (output > max_) output = max_; else if (output < min_) output = min_; return output; } private: float kp_, ki_, kd_; float min_, max_; float integral_; float last_error_; // 可以添加积分限幅 float max_integral_ = 1000; // 根据需要设定 float min_integral_ = -1000; }; 在定时中断中: // 位置环PID对象 PID pos_pid(1.0, 0.0, 0.1, -500, 500); // 位置环输出限幅在±500步/秒 PID vel_pid(1.0, 0.0, 0.0, 0, 500); // 速度环输出限幅在0~500Hz // 中断处理函数 void control_interrupt() { // 1. 读取编码器,计算当前位置(步) int32_t encoder_count = read_encoder(); float current_position = (float)encoder_count * steps_per_rev / encoder_ppr; // 2. 目标位置(步) float target_position = target_angle / step_angle; // 3. 位置环计算 float v_target = pos_pid.calculate(target_position, current_position); // 4. 方向设置 if (v_target >= 0) { set_direction(FORWARD); } else { set_direction(BACKWARD); } // 5. 计算当前速度(带符号) static float last_position = current_position; static uint32_t last_time = millis(); uint32_t current_time = millis(); float dt = (current_time - last_time) / 1000.0; // 转换为秒 float current_velocity = (current_position - last_position) / dt; last_position = current_position; last_time = current_time; // 6. 速度环:输入为v_target的绝对值(目标速度小)和当前速度的绝对值 float abs_v_target = fabs(v_target); float abs_current_velocity = fabs(current_velocity); float f_output = vel_pid.calculate(abs_v_target, abs_current_velocity); // 7. 设置脉冲输出 if (f_output > 0) { set_pulse_frequency(f_output); // 设置脉冲频率(Hz) enable_pulse(); } else { disable_pulse(); } } 29. 注意:上述代码中,速度环的反馈量是当前速度的绝对值,因此速度环只负责控制速度的小,不关心方向。方向由位置环的输出符号决定。 30. 实际应用中,可能需要整PID参数,并且注意编码器的读取和速度计算(微分可能引入噪声,需要滤波)。 31. 另外,步进电机在停止时(位置偏差很小),位置环输出接近0,速度环输出接近0,脉冲停止,电机停止。但可能存在位置偏差不为0但很小的情况,此时位置环输出也很小,速度环输出可能不足以驱动电机(因为步进电机有最小启动频率),所以需要设置一个最小频率(死区补偿),或者当位置环输出小于某个阈值时,直接停止(避免电机在低频振荡)。 32. 可以添加一个死区处理:当位置偏差小于某个阈值(如0.1步)时,认为到达目标位置,将位置环和速度环重置,停止脉冲。 33. 最后,步进电机的驱动器需要方向信号在脉冲之前设置(通常提前几微秒),所以在改变方向后,需要延迟一点再发脉冲。 以上是修改双环PID控制代码以适应步进电机控制转动指定角度的思路和代码框架。具体实现需要根据硬件平台(如STM32、Arduino等)进行整。 相关问题: 1. 如何整步进电机双环PID的参数? 2. 步进电机闭环控制中,如何防止失步? 3. 步进电机的位置检测使用编码器时,如何解决多圈计数和零点问题? 4. 步进电机在低速时振动,如何优化? 5. 如何实现步进电机的S形加减速控制?</think>要将双环PID控制代码从普通电机修改为步进电机控制指定角度,需针对步进电机的特性(脉冲控制、开环特性、精确角度)进行整。以下是具体修改步骤和示例代码: --- ### **关键修改点** 1. **位置单位转换** 步进电机角度控制需将角度转换为步数(基于步距角): $$ \text{目标步数} = \frac{\text{目标角度}}{\text{步距角}} $$ 例如,步距角 $1.8^\circ$ 时,$90^\circ$ 目标角度 = $50$ 步。 2. **位置环输出整** 位置环PID输出目标速度(步/秒),需限制在步进电机最频率内(如 $500$ Hz)。 3. **速度环输出转换** 速度环输出直接作为脉冲频率(Hz),方向由位置偏差符号决定。 4. **脉冲控制逻辑** 使用定时器生成脉冲,方向信号提前设置(满足驱动器时序要求)。 --- ### **代码修改示例(基于STM32 HAL库)** ```c // 参数配置 #define STEP_ANGLE 1.8f // 步距角(度) #define STEPS_PER_REV 200 // 步数/转 (360/1.8) #define ENCODER_PPR 4000 // 编码器脉冲/转(1000线×4倍频) #define MAX_FREQ 500.0f // 最脉冲频率(Hz) // PID结构体(位置环+速度环) typedef struct { float kp, ki, kd; float integral, prev_error; float min_output, max_output; } PID; // 初始化PID void PID_Init(PID* pid, float kp, float ki, float kd, float min, float max) { pid->kp = kp; pid->ki = ki; pid->kd = kd; pid->integral = 0; pid->prev_error = 0; pid->min_output = min; pid->max_output = max; } // PID计算函数 float PID_Calculate(PID* pid, float setpoint, float measurement, float dt) { float error = setpoint - measurement; pid->integral += error * dt; float derivative = (error - pid->prev_error) / dt; pid->prev_error = error; float output = pid->kp * error + pid->ki * pid->integral + pid->kd * derivative; // 输出限幅 if (output > pid->max_output) output = pid->max_output; if (output < pid->min_output) output = pid->min_output; return output; } // 全局变量 PID pos_pid, vel_pid; // 位置环/速度环PID float target_angle = 90.0f; // 目标角度(度) float current_position = 0; // 当前位置(步) // 定时器中断(1ms周期) void TIM_Control_IRQHandler() { // 1. 读取编码器值并转换为步数 int32_t encoder_count = __HAL_TIM_GET_COUNTER(&htim_encoder); current_position = (float)encoder_count * STEPS_PER_REV / ENCODER_PPR; // 2. 位置环PID:计算目标速度(步/秒) float target_steps = target_angle / STEP_ANGLE; float v_target = PID_Calculate(&pos_pid, target_steps, current_position, 0.001f); // 3. 方向控制(位置偏差决定方向) if (v_target >= 0) HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_SET); // 正转 else HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_RESET); // 反转 // 4. 速度环PID:计算脉冲频率(Hz) float current_velocity = ...; // 通过编码器差分计算当前速度(步/秒) float abs_v_target = fabs(v_target); float pulse_freq = PID_Calculate(&vel_pid, abs_v_target, fabs(current_velocity), 0.001f); // 5. 设置脉冲频率(若到位则停止) if (fabs(target_steps - current_position) < 0.5f) { // 死区阈值0.5步 __HAL_TIM_SET_COMPARE(&htim_pulse, TIM_CHANNEL_1, 0); // 停止脉冲 } else { uint32_t arr = SystemCoreClock / (2 * pulse_freq) - 1; // 定时器重载值 __HAL_TIM_SET_AUTORELOAD(&htim_pulse, arr); __HAL_TIM_SET_COMPARE(&htim_pulse, TIM_CHANNEL_1, arr / 2); // 50%占空比 } } ``` --- ### **关键优化措施** 1. **方向信号时序** 方向信号需在脉冲前 $5\mu s$ 设置,防止驱动器响应错误[^1]。 2. **加减速控制** 在PID前端添加梯形/S曲线生成器,避免失步: ```c // 梯形加减速示例 void set_trapezoidal_speed(float target_speed) { static float current_speed = 0; float accel = 100.0f; // 加速度(步/秒²) if (target_speed > current_speed) current_speed += accel * 0.001f; // 1ms中断加速 else if (target_speed < current_speed) current_speed -= accel * 0.001f; // 减速 v_target = current_speed; // 更新位置环输入 } ``` 3. **抗饱和处理** 位置环积分分离:当误差过时暂停积分,避免超4. **低通滤波** 速度测量使用移动平均滤波: ```c #define FILTER_WINDOW 5 float velocity_buffer[FILTER_WINDOW]; float filtered_velocity = moving_average(current_velocity, velocity_buffer); ``` --- ### **试建议** 1. **参数整定顺序** **速度环** → **位置环**:先速度环KP确保电机稳定转动,再位置环KP实现快速响应。 2. **典型参数范围** - 速度环:$K_p \in [0.1, 1.0]$, $K_i = 0$(步进电机惯性小) - 位置环:$K_p \in [10, 100]$, $K_d \in [0.1, 1.0]$(抑制超) 3. **失步检测** 比较编码器位移与脉冲计数,误差超过阈值时触发复位: ```c if (fabs((pulse_count - encoder_steps) > 10) { emergency_stop(); // 紧急停止并报警 } ``` --- ### **常见问题解决** - **问题1:电机振动** **方案**:降低位置环KP,或增加速度环微分项[^1]。 - **问题2:到位后振荡** **方案**:设置位置死区(如±0.5步),到位后关闭PID积分。 - **问题3:高速丢步** **方案**:检查驱动器电流设置,确保脉冲频率不超过电机最响应频率。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值