10. 无刷电机速度环控制(BLDC)¶
前面我们学习了无刷电机简单的6步PWM控制。但是我们在实际使用中并不是只是简单的PWM控制就能满足应用要求, 通常我们还需要对速度进行控制控制,如前面章节中讲到的为什么使用PID一节中列举的小车控制一样, 如果不对速度进行控制可能系统运行效果会不如预期那么好, 本章节中我们就通过速度环的PID控制来实现直流无刷电机的速度控制。
本章通过我们前面学习的位置式PID和增量式PID两种控制方式分别来实现速度环的控制, 如果还不知道什么是位置式PID和增量式PID,请务必先学习前面PID算法的通俗解说这一章节。
10.1. 硬件设计¶
关于详细的硬件分析在直流无刷电机章节中已经讲解过,这里不再做分析, 如有不明白请参考前面章节,这里只给出接线表。
电机主控板与无刷电机驱动板连接见下表所示。
电机 |
无刷电机驱动板 |
---|---|
粗黄 |
U |
粗绿 |
V |
粗蓝 |
W |
细红 |
+(编码器电源) |
细黑 |
-(编码器电源) |
细黄 |
HIU |
细绿 |
HIV |
细蓝 |
HIW |
无刷电机驱动板与主控板连接见下表所示。
无刷电机驱动板 |
主控板 |
---|---|
5V_IN |
5V |
GND |
GND |
U+ |
PE10 |
U- |
PE13 |
V+ |
PE11 |
V- |
PE14 |
W+ |
PE12 |
W- |
PE15 |
HU |
PB06 |
HV |
PB07 |
HW |
PA10 |
SD |
PB15 |
推荐使用配套的牛角排线直接连接驱动板和主控板。连接开发板的那端,请连接在“无刷电机驱动接口1”上。
10.2. 直流无刷电机速度环控制-位置式PID实现¶
10.2.1. 软件设计1¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本章配套的工程。 本章代码在野火电机驱动例程中:\base_code\improve_part\6T2\直流无刷电机-速度环控制-位置式PID目录下。
10.2.1.1. 编程要点1¶
FSP库的定时器、外部中断配置
定时器、外部中断模块初始化
根据电机的换相表编写换相中断回调函数
根据定时器定义电机控制相关函数
配置基本定时器可以产生定时中断来执行PID运算
编写位置式PID算法
编写速度控制函数
增加上位机曲线观察相关代码
编写串口控制代码
10.2.2. 软件分析1¶
FSP库的配置,在编程要点中的1和2在前章节已经讲解过,这里就不在详细讲解, 如果不明白请先学习前面相关章节的内容。这里主要讲解速度的获取方法和分析PID算法的控制实现部分。
1 2 3 4 5 6 7 8 9 10 11 | void set_pwm_pulse(uint16_t pulse)
{
timer_callback_args_t *p_args;
p_args->event = TIMER_EVENT_CAPTURE_B;
/* 设置定时器通道输出 PWM 的占空比 */
bldcm_pulse = pulse;
if ((motor_drive.enable_flag == 1))
time1_callback(p_args); // 执行一次换相
}
|
该函数用于设置pwm输出的占空比,本函数只是将占空比存放在了bldcm_pulse变量中, 而真正设置正空比是在time1_callback()函数中, 所以在电机使能时需要调用time1_callback()设置pwm输出的占空比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void update_motor_speed(uint32_t time)
{
float speed_x;
static uint32_t now_time;
static uint32_t last_time;
now_time = time + (overflow_times * period);
data_time = now_time - last_time;
speed_x = (float)((data_time / 120000000.0) / 3.0);
speed_x = (float)((1.0f / 12.0f) / (speed_x / 60.0f));
motor_drive.speed = speed_x * dir;
last_time = time;
overflow_times = 0;
}
|
该函数用于更新电机的当前速度,其中形参time传入的是霍尔传感器有变化时定时器捕获到的值, 通过time就可以计算出一次换相的时间为(1.0/(120000000.0) * time)秒,电机旋转一圈共有12个变化信号, 所以电机的速度为:(1.0 / 12.0) / ((1.0 / (12000000.0 ) * time) / 60.0)RPM。 dir是电机的正反转方向,通过霍尔传感器去识别转子的正反转来获得 将计算得到的速度保存在motor_drive.speed中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | void update_speed_dir(uint8_t dir_in)
{
if(last_dir != 0 && (last_dir != dir_in)) //645132 623154
{
switch(dir_in)
{
case 1:
if(last_dir == 3)
{
dir = 1;
}
else
{
dir = -1;
}
break;
case 2:
if(last_dir == 6)
{
dir = 1;
}
else
{
dir = -1;
}
break;
case 3:
if(last_dir == 2)
{
dir = 1;
}
else
{
dir = -1;
}
break;
case 4:
if(last_dir == 5)
{
dir = 1;
}
else
{
dir = -1;
}
break;
case 5:
if(last_dir == 1)
{
dir = 1;
}
else
{
dir = -1;
}
break;
case 6:
if(last_dir == 4)
{
dir = 1;
}
else
{
dir = -1;
}
break;
}
}
last_dir = dir_in;
}
|
该函数用于更新电机的速度方向,使用当前读到的霍尔值,与上一次读到的霍尔值进行对比,来确定方向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | void time1_callback(timer_callback_args_t *p_args)
{
uint8_t step = 0;
if(p_args->event != TIMER_EVENT_CYCLE_END)
{
step = Get_Hall_State();
}
if(p_args->event == TIMER_EVENT_CYCLE_END)
{
overflow_times++;
if(bldcm_pulse != 0) //防止在占空比减到0时,还在进行堵转判断
{
if (update++ >= 5) // 有多次次在产生更新中断前霍尔传感器没有捕获到值
{
update = 0;
LED1_ON; // 点亮LED1表示堵转超时停止
/* 堵转超时停止 PWM 输出 */
hall_disable(); // 禁用霍尔传感器接口
stop_pwm_output(); // 停止 PWM 输出
while(1);
}
}
else //占空比为0时,手动设置速度为0 因为速度的计算是通过霍尔中断进行 一旦停止转动 就会停在最后一刻的速度 而不会清0
{
data_time = 0;
}
return;
}
if((p_args->event == TIMER_EVENT_CAPTURE_A) || (p_args->event == TIMER_EVENT_CAPTURE_B))
{
update_speed_dir(step);
update = 0;
if(get_bldcm_direction() == MOTOR_FWD)
{
switch(step)
{
case 1: /* U+ W- */
GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_u_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_HIGH);
break;
case 2: /* V+ U- */
GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_v_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_HIGH);
break;
case 3: /* V+ W- */
GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_v_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_HIGH);
break;
case 4: /* W+ V- */
GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_HIGH);
break;
case 5: /* U+ V- */
GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_u_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_HIGH);
break;
case 6: /* W+ U- */
GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_HIGH);
break;
}
}
else
{
switch(step)
{
case 1: /* W+ U- */
GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_HIGH);
break;
case 2: /* U+ V- */
GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_u_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_HIGH);
break;
case 3: /* W+ V- */
GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_HIGH);
break;
case 4: /* V+ W- */
GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_v_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_HIGH);
break;
case 5: /* V+ U- */
GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_v_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_HIGH);
break;
case 6: /* U+ W- */
GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);
GPT_PWM_SetDuty(&motor_u_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_HIGH);
break;
}
}
}
if(p_args->event == TIMER_EVENT_CAPTURE_A)
{
update_motor_speed(p_args->capture);
}
}
|
换相的实现在直流无刷电机章节已经讲过这个不在赘述,在上面第7行的**if**里面判断了触发的中断源,如果是超时, 就进行堵转检测,如果是通道中断,就进行换向、更新速度、方向,这里注意的是,速度的计算,是通过通道A 去进行的,计算的是每次转动到指定位置所需要的时间,而更新方向,是换向的顺序,每一次换向都可以驱动方向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void PID_param_init()
{
/* 初始化参数 */
pid.target_val=0;
pid.actual_val=0.0;
pid.err = 0.0;
pid.err_last = 0.0;
pid.err_next = 0.0;
pid.Kp = 0.10f;
pid.Ki = 0.03f;
pid.Kd = 0.00f;
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 值
}
|
PID_param_init()函数把结构体pid参数初始化,将目标、实际值、偏差值和积分项等初始化为0, 其中pid.Kp、pid.Ki和pid.Kd是我们配套电机运行效果相对比较好的参数,不同的电机该参数是不同的。 set_computer_value()函数用来同步上位机显示的PID值。
1 2 3 4 | void set_pid_target(float temp_val)
{
pid.target_val = temp_val; // 设置当前的目标值
}
|
设置目标值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | int PID_realize(void)
{
float speed_a;
speed_a = (float)get_motor_speed();; // 电机旋转的当前速度
/*计算目标值与实际值的误差*/
pid.err = pid.target_val - speed_a;
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;
}
|
这个函数主要实现了位置式PID算法,用传入的目标值减去实际值得到误差值得到比例项,在对误差值进行累加得到积分项, 用本次误差减去上次的误差得到微分项,然后通过前面章节介绍的位置式PID公式实现PID算法,并返回实际控制值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void bldcm_pid_control(void)
{
if (bldcm_data.is_enable)
{
cont_val = PID_realize();
if (cont_val < 0)
{
cont_val = -cont_val;
bldcm_data.direction = MOTOR_REV;
}
else
{
bldcm_data.direction = MOTOR_FWD;
}
cont_val = (cont_val > MAX_SPEED) ? MAX_SPEED : cont_val; // 上限处理
set_bldcm_speed((uint16_t)cont_val);
}
}
|
该函数在定时器0的中断里定时调用默认是50毫秒调用一次,如果改变了周期那么PID三个参数也需要做相应的调整, PID的控制周期与控制效果是息息相关的。 调用PID_realize()去进行计算,然后得出来速度。 根据运算结果的正负,设置电机的旋转方向。 最后对输出的结果做一个上限处理,最后用于PWM占空比的控制,最后将实际的速度值发送到上位机绘制变化的曲线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | /**
* @brief 接收的数据处理
* @param void
* @return -1:没有找到一个正确的命令.
*/
int8_t receiving_process(void)
{
uint8_t frame_data[128]; // 要能放下最长的帧
uint16_t frame_len = 0; // 帧长度
uint8_t cmd_type = CMD_NONE; // 命令类型
while(1)
{
cmd_type = protocol_frame_parse(frame_data, &frame_len);
switch (cmd_type)
{
case CMD_NONE:
{
return -1;
}
case SET_P_I_D_CMD:
{
uint32_t temp0 = COMPOUND_32BIT(&frame_data[13]);
uint32_t temp1 = COMPOUND_32BIT(&frame_data[17]);
uint32_t temp2 = COMPOUND_32BIT(&frame_data[21]);
float p_temp, i_temp, d_temp;
p_temp = *(float *)&temp0;
i_temp = *(float *)&temp1;
d_temp = *(float *)&temp2;
set_p_i_d(p_temp, i_temp, d_temp); // 设置 P I D
}
break;
case SET_TARGET_CMD:
{
actual_temp = COMPOUND_32BIT(&frame_data[13]); // 得到数据
set_pid_target(actual_temp); // 设置目标值
set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &actual_temp, 1);
}
break;
case START_CMD:
{
set_bldcm_enable();
}
break;
case STOP_CMD:
{
motor_drive.speed = 0; //占空比为0时,手动设置速度为0 因为速度的计算是通过霍尔中断进行 一旦停止转动 就会停在最后一刻的速度 而不会清0
set_bldcm_disable();
set_computer_value(SEND_STOP_CMD, CURVES_CH1, NULL, 0);
}
break;
case RESET_CMD:
{
Reset_Handler();
}
break;
default:
return -1;
}
}
}
|
这函数用于处理上位机发下的数据,在主函数中循环调用,可以使用上位机调整PID参数,使用上位机可以非常方便的调整PID参数, 这样可以不用每次修改PID参数时都要改代码、编译和下载代码;可以使用上位机设置目标速度;可以启动和停止电机; 可以使用上位机复位系统;可以使用上位机设置定时器的周期;具体功能的实现请参考配套工程代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | void hal_entry(void)
{
int target_speed = 500;
/* TODO: add your own code here */
/* LED初始化 */
LED_Init();
Key_IRQ_Init();
/* 串口初始化 */
Debug_UART9_Init();
/* 协议初始化 */
protocol_init();
PID_param_init();
/* 电机初始化 */
bldcm_init();
basic_gpt0_Init();
set_computer_value(SEND_STOP_CMD, CURVES_CH1, NULL, 0); // 同步上位机的启动按钮状态
set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &target_speed, 1); // 给通道 1 发送目标值
while(1)
{
/* 接收数据处理 */
receiving_process();
}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
|
在主函数里面首先做了一些外设的初始化,然后在while中去检测串口是否收到数据, 并通过去解析上位机发来的数据,去进行相应的控制。
10.2.3. 下载验证1¶
我们按前面介绍的硬件连接好电机和驱动板。
将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 在上位机中,按下启动按钮,然后可以去发送目标值,电机就会运行起来,可以通过上位机去看速度的曲线,也可以 给电机加上一些负载,看PID实际的效果。我们可以通过上位机来观察速度的变化情况,也可以通过上位机来控制电机。
10.3. 直流无刷电机速度环控制-增量式PID实现¶
10.3.1. 软件设计2¶
通过前面位置式PID控制的学习,大家应该对速度环PID控制有了更深刻的理解, 这里将只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本节配套的工程。 本章代码在野火电机驱动例程中:\base_code\improve_part\F407\直流无刷电机-速度环控制-增量式PID目录下。
10.3.1.1. 编程要点2¶
FSP库的定时器、外部中断配置
定时器、外部中断模块初始化
根据电机的换相表编写换相中断回调函数
根据定时器定义电机控制相关函数
配置基本定时器可以产生定时中断来执行PID运算
编写增量式PID算法
编写速度控制函数
增加上位机曲线观察相关代码
编写串口控制代码
10.3.2. 软件分析2¶
增量式PID实现的速度环控制和位置式PID现实的速度环控制其控制代码大部分都是一样的, 在上面的编程要点中只有第4项是不同的,其他代码均相同,所以这里将只讲解不一样的部分代码, 完整代码请参考本节配套工程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void PID_param_init()
{
/* 初始化参数 */
pid.target_val=0;
pid.actual_val=0.0;
pid.err = 0.0;
pid.err_last = 0.0;
pid.err_next = 0.0;
pid.Kp = 0.10f;
pid.Ki = 0.03f;
pid.Kd = 0.00f;
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 值
}
|
PID_param_init()函数把结构体pid参数初始化,将目标值、实际值、偏差值和上一次偏差值等初始化为0, 其中pid.err用来保存本次偏差值,pid.err_last用来保存上一次偏差值,pid.err_next用来保存上上次的偏差值; pid.Kp、pid.Ki和pid.Kd是我们配套电机运行效果相对比较好的参数,不同的电机该参数是不同的。 set_computer_value()函数用来同步上位机显示的PID值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | int PID_realize(void)
{
float speed_a;
speed_a = (float)get_motor_speed();; // 电机旋转的当前速度
/*计算目标值与实际值的误差*/
pid.err = pid.target_val - speed_a;
/*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);
/*传递误差*/
pid.err_last = pid.err_next;
pid.err_next = pid.err;
/*返回当前实际值*/
return (int)pid.actual_val;
}
|
这个函数主要实现了增量式PID算法,用传入的目标值减去实际值得到误差值得到当前偏差值, 在第7~9行中实现了下面公式中的增量式PID算法。
然后进行误差传递,将本次偏差和上次偏差保存下来,供下次计算时使用。 在第7行中将计算后的结果累加到pid.actual_val变量,最后返回该变量,用于控制电机的PWM占空比。
10.3.3. 下载验证2¶
我们按前面介绍的硬件连接好电机和驱动板。
将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 在上位机中,按下启动按钮,然后可以去发送目标值,电机就会运行起来,可以通过上位机去看速度的曲线,也可以 给电机加上一些负载,看PID实际的效果。我们可以通过上位机来观察速度的变化情况,也可以通过上位机来控制电机。
注意
注意:电机正在运行时应该先停止电机再复位,而不建议直接复位开发板,因为这属于非正常操作,复位的瞬间电机还在继续运动,产生的反电动势有损坏硬件的风险。