目录
直流有刷电机电流环控制实现
硬件设计
直流电机电流环控制-位置式PID实现
编程要点
配置ADC可读取电流值
配置基本定时器6产生定时中断读取当前电路中驱动电机的电流值并执行PID运算
配置定时器1输出PWM控制电机
ADC数据处理
编写位置式PID算法
直流电机电流环控制-增量式PID实现
编程要点
配置ADC可读取电流值
配置基本定时器6产生定时中断读取当前电路中驱动电机的电流值并执行PID运算
配置定时器1输出PWM控制电机
ADC数据处理
编写增量式PID算法
直流有刷电机电流环控制实现
利用直流有刷驱动板来完成对电流的采集,最终实现电流环的闭环控制。
在一些场景中想让电机吊起超出电机能力的重物(即超载),但电机的能力有限,电机长期超载工作会严重损坏电机。如果想合理利用电机的性能,就需要控制电流的输出,所以需要电流环的控制。
硬件设计
可选:L298N电机驱动板、野火MOS搭建的驱动板。
直流电机电流环控制-位置式PID实现
编程要点
配置ADC可读取电流值
配置基本定时器产生定时中断读取当前电路中驱动电机的电流值并执行PID运算
配置定时器输出PWM控制电机
编写位置式PID算法
编写电流控制函数
增加上位机曲线观察相关代码
编写按键控制代码
配置ADC可读取电流值
#define VBUS_MAX 14 // 电压最大值
#define VBUS_MIN 10 // 电压最小值#define VBUS_HEX_MAX ((VBUS_MAX/37.0+1.24)/VREF*65536) // 电压最大值(测量电压是电源电压的1/37)
#define VBUS_HEX_MIN ((VBUS_MIN/37.0+1.24)/VREF*65536) // 电压最小值(测量电压是电源电压的1/37)__IO uint16_t ADC_ConvertedValue;
DMA_HandleTypeDef DMA_Init_Handle;
ADC_HandleTypeDef ADC_Handle;static uint16_t adc_buff[1024];
static uint16_t vbus_adc_mean = 0; // 电源电压 ADC 采样结果平均值
static uint32_t adc_mean_sum = 0; // 平均值累加
static uint32_t adc_mean_count = 0; // 累加计数/*** @brief 电流采集初始化* @param 无* @retval 无*/
void ADC_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;__GPIOB_CLK_ENABLE();__DMA2_CLK_ENABLE();__ADC1_CLK_ENABLE();// PB1--电流GPIO_InitStructure.Pin = GPIO_PIN_1;GPIO_InitStructure.Mode = GPIO_MODE_ANALOG;GPIO_InitStructure.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);// PB0--电压GPIO_InitStructure.Pin = GPIO_PIN_0;HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);// ADC1使用DMA2,数据流0,通道0,这个是手册固定死的DMA_Init_Handle.Instance = DMA2_Stream0;DMA_Init_Handle.Init.Direction = DMA_PERIPH_TO_MEMORY;DMA_Init_Handle.Init.PeriphInc = DMA_PINC_DISABLE;DMA_Init_Handle.Init.MemInc = DMA_MINC_ENABLE;DMA_Init_Handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;DMA_Init_Handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;DMA_Init_Handle.Init.Mode = DMA_CIRCULAR;DMA_Init_Handle.Init.Priority = DMA_PRIORITY_HIGH;DMA_Init_Handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE;DMA_Init_Handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_HALFFULL;DMA_Init_Handle.Init.MemBurst = DMA_MBURST_SINGLE;DMA_Init_Handle.Init.PeriphBurst = DMA_PBURST_SINGLE;// 选择 DMA 通道,通道存在于流中DMA_Init_Handle.Init.Channel = DMA_CHANNEL_0;//初始化DMA流,流相当于一个大的管道,管道里面有很多通道HAL_DMA_Init(&DMA_Init_Handle);__HAL_LINKDMA(&ADC_Handle, DMA_Handle, DMA_Init_Handle);ADC_Handle.Instance = ADC1;ADC_Handle.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV4;ADC_Handle.Init.Resolution = ADC_RESOLUTION_12B;ADC_Handle.Init.ScanConvMode = ENABLE;ADC_Handle.Init.ContinuousConvMode = ENABLE;ADC_Handle.Init.DiscontinuousConvMode = DISABLE;ADC_Handle.Init.NbrOfDiscConversion = 0;ADC_Handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;ADC_Handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;ADC_Handle.Init.DataAlign = ADC_DATAALIGN_LEFT;ADC_Handle.Init.NbrOfConversion = 2;ADC_Handle.Init.DMAContinuousRequests = ENABLE;ADC_Handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV;HAL_ADC_Init(&ADC_Handle);ADC_ChannelConfTypeDef ADC_Config;ADC_Config.Channel = ADC_CHANNEL_9;ADC_Config.Rank = 1;ADC_Config.SamplingTime = ADC_SAMPLETIME_3CYCLES;ADC_Config.Offset = 0;HAL_ADC_ConfigChannel(&ADC_Handle, &ADC_Config);/** Configure the analog watchdog*/ADC_AnalogWDGConfTypeDef AnalogWDGConfig = {0};AnalogWDGConfig.WatchdogMode = ADC_ANALOGWATCHDOG_SINGLE_REG;AnalogWDGConfig.HighThreshold = VBUS_HEX_MAX;AnalogWDGConfig.LowThreshold = VBUS_HEX_MIN;AnalogWDGConfig.Channel = ADC_CHANNEL_8;AnalogWDGConfig.ITMode = ENABLE;if (HAL_ADC_AnalogWDGConfig(&ADC_Handle, &AnalogWDGConfig) != HAL_OK){while (1);}/** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.*/ADC_Config.Channel = ADC_CHANNEL_8;ADC_Config.Rank = 2;ADC_Config.SamplingTime = ADC_SAMPLETIME_3CYCLES;ADC_Config.Offset = 0;if (HAL_ADC_ConfigChannel(&ADC_Handle, &ADC_Config) != HAL_OK){while (1);}// 外设中断优先级配置和使能中断配置HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 4, 0);HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);HAL_NVIC_SetPriority(ADC_IRQn, 3, 0);HAL_NVIC_EnableIRQ(ADC_IRQn);HAL_ADC_Start_DMA(&ADC_Handle, (uint32_t *)&adc_buff, 1024);
}
配置基本定时器6产生定时中断读取当前电路中驱动电机的电流值并执行PID运算
TIM_HandleTypeDef TIM_TimeBaseStructure;/*** @brief 初始化基本定时器定时,默认50ms产生一次中断* @param 无* @retval 无*/
void TIMx_Configuration(void)
{HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 1, 3);HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);__TIM6_CLK_ENABLE();TIM_TimeBaseStructure.Instance = TIM6;TIM_TimeBaseStructure.Init.Period = 50 * 50 - 1;TIM_TimeBaseStructure.Init.Prescaler = 1680 - 1;TIM_TimeBaseStructure.Init.CounterMode = TIM_COUNTERMODE_UP;TIM_TimeBaseStructure.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;HAL_TIM_Base_Init(&TIM_TimeBaseStructure);// 开启定时器更新中断HAL_TIM_Base_Start_IT(&TIM_TimeBaseStructure);uint32_t temp = (__HAL_TIM_GET_AUTORELOAD(&TIM_TimeBaseStructure) + 1) / 50.0; // 计算周期,单位msset_computer_value(SEND_PERIOD_CMD, CURVES_CH1, &temp, 1); // 给通道 1 发送目标值
}/*** @brief 定时器更新事件回调函数* @param 无* @retval 无*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim == (&TIM_TimeBaseStructure)){motor_pid_control(); // 每50ms执行一次PID运算}
}
配置定时器1输出PWM控制电机
TIM_HandleTypeDef DCM_TimeBaseStructure;/*** @brief 初始化控制通用定时器* @param 无* @retval 无*/
void Motor_TIMx_Configuration(void)
{GPIO_InitTypeDef GPIO_InitStruct;TIM_OC_InitTypeDef TIM_OCInitStructure;__HAL_RCC_GPIOA_CLK_ENABLE();__TIM1_CLK_ENABLE();// PA8--PWM_TIM_CH1GPIO_InitStruct.Pin = GPIO_PIN_8;GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;GPIO_InitStruct.Alternate = PWM_TIM_GPIO_AF;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);// PA9--PWM_TIM_CH2GPIO_InitStruct.Pin = GPIO_PIN_9;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);// TIM1 66.7us一次周期DCM_TimeBaseStructure.Instance = TIM1;DCM_TimeBaseStructure.Init.Period = 5600 - 1;DCM_TimeBaseStructure.Init.Prescaler = 1 - 1;DCM_TimeBaseStructure.Init.CounterMode = TIM_COUNTERMODE_UP;DCM_TimeBaseStructure.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;HAL_TIM_PWM_Init(&DCM_TimeBaseStructure);/*PWM模式配置*/TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM1;TIM_OCInitStructure.Pulse = 0;TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;TIM_OCInitStructure.OCNPolarity = TIM_OCNPOLARITY_HIGH;TIM_OCInitStructure.OCIdleState = TIM_OCIDLESTATE_SET;TIM_OCInitStructure.OCNIdleState = TIM_OCNIDLESTATE_RESET;/*配置PWM通道*/HAL_TIM_PWM_ConfigChannel(&DCM_TimeBaseStructure, &TIM_OCInitStructure, TIM_CHANNEL_1);/*开始输出PWM*/HAL_TIM_PWM_Start(&DCM_TimeBaseStructure, TIM_CHANNEL_1);/*配置PWM通道*/HAL_TIM_PWM_ConfigChannel(&DCM_TimeBaseStructure, &TIM_OCInitStructure, TIM_CHANNEL_2);/*开始输出PWM*/HAL_TIM_PWM_Start(&DCM_TimeBaseStructure, TIM_CHANNEL_2);
}/*** @brief 设置TIM通道的占空比* @param channel 通道 (1,2,3,4)* @param compare 占空比* @note 无* @retval 无*/
void TIM1_SetPWM_pulse(uint32_t channel, int compare)
{switch (channel){case TIM_CHANNEL_1:__HAL_TIM_SET_COMPARE(&DCM_TimeBaseStructure, TIM_CHANNEL_1, compare);break;case TIM_CHANNEL_2:__HAL_TIM_SET_COMPARE(&DCM_TimeBaseStructure, TIM_CHANNEL_2, compare);break;case TIM_CHANNEL_3:__HAL_TIM_SET_COMPARE(&DCM_TimeBaseStructure, TIM_CHANNEL_3, compare);break;case TIM_CHANNEL_4:__HAL_TIM_SET_COMPARE(&DCM_TimeBaseStructure, TIM_CHANNEL_4, compare);break;}
}
ADC数据处理
static uint16_t flag_num = 0;
/*** @brief 常规转换在非阻塞模式下完成回调* @param hadc: ADC 句柄.* @retval 无*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{uint32_t adc_mean = 0;HAL_ADC_Stop_DMA(hadc); // 停止 ADC 采样,处理完一次数据在继续采样/* 计算电流通道采样的平均值 */for (uint32_t count = 0; count < 1024; count += 2){adc_mean += (uint32_t)adc_buff[count];}adc_mean_sum += adc_mean / (1024 / 2); // 保存平均值adc_mean_count++;adc_mean = 0;/* 计算电压通道采样的平均值 */for (uint32_t count = 1; count < 1024; count += 2){adc_mean += (uint32_t)adc_buff[count];}vbus_adc_mean = adc_mean / (1024 / 2); // 保存平均值HAL_ADC_Start_DMA(&ADC_Handle, (uint32_t *)&adc_buff, 1024); // 开始 ADC 采样
}/*** @brief 在非阻塞模式模拟看门狗回调* @param hadc: ADC 句柄.* @retval 无*/
void HAL_ADC_LevelOutOfWindowCallback(ADC_HandleTypeDef *hadc)
{float temp_adc;flag_num++; // 电源电压超过阈值电压temp_adc = get_vbus_val();if (temp_adc > VBUS_MIN && temp_adc < VBUS_MAX){flag_num = 0;}if (flag_num > 10) // 电源电压超过阈值电压10次{set_motor_disable();flag_num = 0;printf("电源电压超过限制!请检查原因,复位开发板在试!\r\n");while (1);}
}/*** @brief 获取电流值(应定时调用)* @param 无* @retval 转换得到的电流值*/
int32_t get_curr_val(void)
{static uint8_t flag = 0;static uint32_t adc_offset = 0; // 偏置电压int16_t curr_adc_mean = 0; // 电流 ACD 采样结果平均值curr_adc_mean = adc_mean_sum / adc_mean_count; // 保存平均值adc_mean_count = 0;adc_mean_sum = 0;if (flag < 17 && is_motor_en == 0) // 仅在电机未启动时记录{adc_offset = curr_adc_mean; // 多次记录偏置电压,待系统稳定偏置电压才为有效值flag += 1;}if (curr_adc_mean >= adc_offset){curr_adc_mean -= adc_offset; // 减去偏置电压}else{curr_adc_mean = 0;}float vdc = (float)curr_adc_mean/(float)65536 * 3.3f; // 获取电压值return (float)vdc / 8.0f / 0.02f * 1000.0f;
}/*** @brief 获取电源电压值* @param 无* @retval 转换得到的电流值*/
float get_vbus_val(void)
{float vdc = (float)vbus_adc_mean/(float)65536 * 3.3f; // 获取电压值return ((float)vdc - (float)1.24) * (float)37.0; // 电源电压值(测量电压是电源电压的1/37)
}
编写位置式PID算法
typedef struct
{float target_val; // 目标值float actual_val; // 实际值float err; // 定义偏差值float err_last; // 定义上一个偏差值float Kp,Ki,Kd; // 定义比例、积分、微分系数float integral; // 定义积分值
}_pid;
_pid pid;/*** @brief PID参数初始化* @note 无* @retval 无*/
void PID_param_init()
{/* 初始化参数 */pid.target_val = 40.0;pid.actual_val = 0.0;pid.err = 0.0;pid.err_last = 0.0;pid.integral = 0.0;pid.Kp = 0;pid.Ki = 3.5;pid.Kd = 0;float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3); // 给通道 1 发送 P I D 值
}/*** @brief 设置目标值* @param val 目标值* @note 无* @retval 无*/
void set_pid_target(float temp_val)
{pid.target_val = temp_val; // 设置当前的目标值
}/*** @brief 获取目标值* @param 无* @note 无* @retval 目标值*/
float get_pid_target(void)
{return pid.target_val; // 设置当前的目标值
}/*** @brief 设置比例、积分、微分系数* @param p:比例系数 P* @param i:积分系数 i* @param d:微分系数 d* @note 无* @retval 无*/
void set_p_i_d(float p, float i, float d)
{pid.Kp = p; // 设置比例系数 Ppid.Ki = i; // 设置积分系数 Ipid.Kd = d; // 设置微分系数 D
}/*** @brief PID算法实现* @param actual_val:实际值* @note 无* @retval 通过PID计算后的输出*/
float PID_realize(float actual_val)
{/* 限制电流幅值,野火电机空载时最大电流在100ma左右,如果过大容易积分饱和 */if (pid.target_val >= 120){pid.target_val = 120.0;}else if (pid.target_val <= 5){pid.target_val = 0.0;}/*计算目标值与实际值的误差*/pid.err = pid.target_val - actual_val;/*误差累积*/pid.integral += pid.err;/*PID算法实现*/pid.actual_val = pid.Kp * pid.err + pid.Ki * pid.integral + pid.Kd * (pid.err - pid.err_last);/*误差传递*/pid.err_last = pid.err;/*返回当前实际值*/return pid.actual_val;
}/*** @brief 定时器每50ms产生一次中断回调函数* @param htim:定时器句柄* @retval 无*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim == (&TIM_TimeBaseStructure)){motor_pid_control();}
}/*** @brief 电机位置式 PID 控制实现(定时调用)* @param 无* @retval 无*/
void motor_pid_control(void)
{int32_t actual_current = get_curr_val(); // 读取当前电流值if (is_motor_en == 1) // 电机在使能状态下才进行控制处理{float cont_val = 0; // 当前控制值cont_val = PID_realize(actual_current); // 进行 PID 计算if (cont_val < 0){cont_val = 0; // 下限处理}else if (cont_val > 5500){cont_val = 5600; // 速度上限处理}set_motor_speed(cont_val); // 设置 PWM 占空比set_computer_value(SEND_FACT_CMD, CURVES_CH1, &actual_current, 1); // 给通道 1 发送实际值}
}
直流电机电流环控制-增量式PID实现
编程要点
配置ADC可读取电流值
配置基本定时器产生定时中断读取当前电路中驱动电机的电流值并执行PID运算
配置定时器输出PWM控制电机
编写增量式PID算法
编写电流控制函数
增加上位机曲线观察相关代码
编写按键控制代码
配置ADC可读取电流值
同上。
配置基本定时器6产生定时中断读取当前电路中驱动电机的电流值并执行PID运算
同上。
配置定时器1输出PWM控制电机
同上。
ADC数据处理
同上。
编写增量式PID算法
typedef struct
{float target_val; //目标值float actual_val; //实际值float err; //定义当前偏差值float err_next; //定义下一个偏差值float err_last; //定义最后一个偏差值float Kp, Ki, Kd; //定义比例、积分、微分系数
}_pid;
_pid pid;/*** @brief PID参数初始化* @note 无* @retval 无*/
void PID_param_init()
{/* 初始化参数 */pid.target_val = 80;pid.actual_val = 0.0;pid.err = 0.0;pid.err_last = 0.0;pid.err_next = 0.0;pid.Kp = 0;pid.Ki = 2.8;pid.Kd = 0;float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3); // 给通道 1 发送 P I D 值
}/*** @brief 设置目标值* @param val 目标值* @note 无* @retval 无*/
void set_pid_target(float temp_val)
{pid.target_val = temp_val; // 设置当前的目标值
}/*** @brief 获取目标值* @param 无* @note 无* @retval 目标值*/
float get_pid_target(void)
{return pid.target_val; // 设置当前的目标值
}/*** @brief 设置比例、积分、微分系数* @param p:比例系数 P* @param i:积分系数 i* @param d:微分系数 d* @note 无* @retval 无*/
void set_p_i_d(float p, float i, float d)
{pid.Kp = p; // 设置比例系数 Ppid.Ki = i; // 设置积分系数 Ipid.Kd = d; // 设置微分系数 D
}/*** @brief PID算法实现* @param actual_val:实际值* @note 无* @retval 通过PID计算后的输出*/
float PID_realize(float actual_val)
{/*计算目标值与实际值的误差*/pid.err = pid.target_val - actual_val;/*PID算法实现*/pid.actual_val += pid.Kp * (pid.err - pid.err_next) + pid.Ki * pid.err + pid.Kd * (pid.err - 2 * pid.err_next + pid.err_last);/* 限幅输出,防止深度饱和 */if (pid.actual_val > 5600){pid.actual_val = 5600;}else if (pid.actual_val <= 0){pid.actual_val = 0;}/*传递误差*/pid.err_last = pid.err_next;pid.err_next = pid.err;/*返回当前实际值*/return pid.actual_val;
}/*** @brief 定时器每50ms产生一次中断回调函数* @param htim:定时器句柄* @retval 无*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim == (&TIM_TimeBaseStructure)){motor_pid_control();}
}/*** @brief 电机增量式 PID 控制实现(定时调用)* @param 无* @retval 无*/
void motor_pid_control(void)
{int32_t actual_current = get_curr_val(); // 读取当前电流值if (is_motor_en == 1) // 电机在使能状态下才进行控制处理{float cont_val = 0; // 当前控制值static __IO int32_t Capture_Count = 0; // 当前时刻总计数值static __IO int32_t Last_Count = 0; // 上一时刻总计数值cont_val = PID_realize(actual_current); // 进行 PID 计算if (cont_val > 0) // 判断电机方向{//set_motor_direction(MOTOR_FWD);}else{cont_val = 0;// cont_val = -cont_val;//set_motor_direction(MOTOR_REV);}cont_val = (cont_val > PWM_MAX_PERIOD_COUNT) ? PWM_MAX_PERIOD_COUNT : cont_val; // 速度上限处理set_motor_speed(cont_val); // 设置 PWM 占空比set_computer_value(SEND_FACT_CMD, CURVES_CH1, &actual_current, 1); // 给通道 1 发送实际值}
}