PID控制应该算是应用非常广泛的控制算法了。常见的比如控制环境温度,控制无人机飞行高度速度等。PID我们将其分成三个参数,如下:
P-比例控制,基本作用就是控制对象以线性的方式增加,在一个常量比例下,动态输出,缺点是会产生一个稳态误差。
I-积分控制,基本作用是用来消除稳态误差,缺点是会产生超调现象
D-微分控制,基本作用是减弱超调现象,加大惯性响应速度。
总的来说,当得到系统的输出后,将输出经过比例,积分,微分三种运算方式再叠加到输入中,从而形成一个闭环控制系统。在真正的实践中,最难的是如何确定三个项的系数,这就需要大量的实验以及经验来确定了,通过不断的尝试和思考,就能选取合适的参数,从而做出一个优良的PID控制器。
理论说得再多,不如亲自动手实践一下,我做了一个简单的PID控制模型,因为采用的是用PID控制PWM占空比,输出的PWM占空比和采集到的占空比基本不会存在误差,所以和真正的PID闭环控制还有一些差别。仅以此例说明如何用FPGA做一个简易的PID算法,然后可通过Modelsim仿真观察到调节占空比的曲线变化,最后在开发板上验证,用示波器测试输出的PWM占空比,与我们设置的目标占空比一致。假如我们要控制电机速度,有摩擦阻力以及速度采集误差,此时就需要我们对Kp,Ki,Kd进行调节,以达到最佳的控制效果。模型框图如下所示:
targe:目标值
actual:实际值
产生频率固定,占空比可通过按键进行调节的PWM信号。按下KEY1占空比加10%,按下KEY2占空比减10%,每个PWM周期进行一次PID计算
前面我们列出了PID理论的公式,但是光看理论公式我们要用Verilog语言将其实现出来还是有点摸不着头脑,所以我们需要将公司稍做变化,转成方便用FPGA实现PID的公式:
Kp:比例项参数
Ki:积分项参数
Kd:微分项参数
error:误差,targe-actual
sum_error:error的总和
error-last_error:当前error减去上一次error
程序框图如下:
key_xd:按键消抖模块,该模块直接调用我们开发板的消抖例程。
targe_gen:目标值生成模块,即生成想要的占空比数值。由于FPGA无法处理小数,所以为了方便处理将此数据扩大了100倍,假如设置的数值是980,那实际占空比就是9.8%。
module targe_gen(
input clk,
input rst_n,
input key_add,
input key_sub,
output reg rst_n_out,
output reg [15:0] targe
);
reg [7:0] rst_cnt;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
targe<=3500;
rst_n_out<=0;
end else if(key_add)begin
targe<=targe+1000;
rst_n_out<=0;
end else if(key_sub)begin
targe<=targe-1000;
rst_n_out<=0;
end else if(rst_cnt==100)begin
rst_n_out <=1;
end
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
rst_cnt<=0;
else if(key_add||key_sub)
rst_cnt<=0;
else if(rst_n_out==0)
rst_cnt<=rst_cnt+1;
else
rst_cnt<=0;
end
endmodule
zkb_calc:占空比检测模块,计算实际输出的PWM占空比,即targe值。由于FPGA无法处理小数,所以为了方便处理将此数据扩大了100倍,假如设置的数值是980,那实际占空比就是9.8%。该模块需要用到除法,我们不能直接在代码里面用"/"来进行计算,而是需要调用除法器IPCORE来进行计算。
module zkb_calc(
input clk ,
input rst_n ,
input pwm_in ,//反馈信号
output reg [15:0] pwm_zkb ,//计算的PWM占空比
output reg pwm_zkb_vld //PWM占空比有效标志
);
parameter ST0 =4'd0;
parameter ST1 =4'd1;
parameter CALC_ST =4'd2;
parameter RESULT_ST =4'd3;
parameter time_out_num=500;//采样时间
reg [3:0] curr_st;
reg [31:0] pwm_hcnt;
reg [31:0] pwm_hlcnt;
reg [31:0] div_dividend;
reg [31:0] div_divisor;
reg div_ce;
reg [31:0] time_out_cnt;
reg pwm_in_ff1,pwm_in_ff2,pwm_in_ff3;
wire[39:0] quotient;
reg rdy;
assign pwm_in_rise=pwm_in_ff2&&(pwm_in_ff3==0);
always@(posedge clk)pwm_in_ff1<=pwm_in;
always@(posedge clk)pwm_in_ff2<=pwm_in_ff1;
always@(posedge clk)pwm_in_ff3<=pwm_in_ff2;
always@(posedge clk)rdy<=div_ce;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
pwm_zkb <=0;
pwm_zkb_vld <=0;
end else if(rdy)begin
pwm_zkb <=quotient[15:0];
pwm_zkb_vld <=1;
end else
pwm_zkb_vld<=0;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
curr_st <=ST0;
div_ce <=0;
div_dividend<=0;
div_divisor<=0;
end else case(curr_st)
ST0:begin
if(time_out_cnt==1)
curr_st<=ST1;
else;
end
ST1:begin
if(time_out_cnt==1)
curr_st<=CALC_ST;
else;
end
CALC_ST:begin
curr_st<=RESULT_ST;
div_dividend<={pwm_hcnt,13'h0}+{pwm_hcnt,10'h0}+{pwm_hcnt,9'h0}+{pwm_hcnt,8'h0}+{pwm_hcnt,4'h0};//x10000
div_divisor<=pwm_hlcnt;
div_ce<=1;
end
RESULT_ST:begin
div_ce<=0;
curr_st<=ST0;
end
default:;
endcase
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
time_out_cnt<=0;
else if(time_out_cnt==time_out_num-1)
time_out_cnt<=0;
else
time_out_cnt<=time_out_cnt+1;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
pwm_hcnt<=0;
else if(curr_st==RESULT_ST)
pwm_hcnt<=0;
else if(curr_st==ST1&&pwm_in_ff3)
pwm_hcnt<=pwm_hcnt+1;
else;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
pwm_hlcnt<=0;
else if(curr_st==ST0)
pwm_hlcnt<=0;
else if(curr_st==ST1)
pwm_hlcnt<=pwm_hlcnt+1;
else;
end
DIV U_DIV(
.denom ({8'b0,div_divisor}),//被除数
.numer ({8'b0,div_dividend}),//除数
.quotient(quotient),
.remain ()
);
endmodule
pid_ctrl:pid计算模块,按照上面的公式,需要用到加法和乘法,加法我们可直接在代码里面用"+"来进行计算,乘法就需要调用乘法器的IPCORE(由于误差是有正负之分,所以我们的乘法器IPCORE也需要设置成有符号的,这一点一定要注意,否则计算会出问题),最终计算出占空比数值,由于FPGA无法处理小数,所以Kp,Ki,Kd都扩大了100倍,假如Kp=10,真实值即为0.1。根据公式我们知道最终的PWM占空比数值扩大了10000倍。
module pid_ctrl(
input clk ,
input rst_n ,
input [15:0] targe ,//x100
input [15:0] actual ,//x100
input actual_vld ,
output reg [31:0] pwm_zkb , //x10000,因为targe,actual乘以100,Kp,Ki,Kd乘以100,所以结果放大了10000
output reg pwm_zkb_vld
);
parameter IDLE =8'd0;
parameter STEP1 =8'd1;
parameter STEP2 =8'd2;
parameter STEP3 =8'd3;
parameter STEP4 =8'd4;
parameter STEP5 =8'd5;
parameter STEP6 =8'd6;
parameter Kp =10;//x100;
parameter Ki =10;//x100;
parameter Kd =15;//x100;
reg [7:0] curr_st;
reg [31:0] sum_error;
reg [31:0] last_error;
reg [31:0] error ;
reg [31:0] mul1_a,mul2_a,mul3_a;
reg [31:0] mul1_b,mul2_b,mul3_b;
reg mul1_ce,mul2_ce,mul3_ce;
wire[31:0] mul1_result,mul2_result,mul3_result;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
curr_st <=IDLE;
sum_error <=0;
last_error <=0;
error <=0;
pwm_zkb <=0;
mul1_a <=0;
mul1_b <=0;
mul1_ce <=0;
mul2_a <=0;
mul2_b <=0;
mul2_ce <=0;
mul3_a <=0;
mul3_b <=0;
mul3_ce <=0;
pwm_zkb_vld <=0;
end else case(curr_st)
IDLE:begin
pwm_zkb_vld<=0;
if(actual_vld)begin
curr_st<=STEP1;
end else;
end
STEP1:begin
last_error<=error;
curr_st<=STEP2;
end
STEP2:begin
error<=targe-actual;
curr_st<=STEP3;
end
STEP3:begin
sum_error<=sum_error+error;
curr_st<=STEP4;
end
STEP4:begin
mul1_a<=Kp;
mul1_b<=error;
mul1_ce<=1;
mul2_a<=Ki;
mul2_b<=sum_error;
mul2_ce<=1;
mul3_a<=Kd;
mul3_b<=error-last_error;
mul3_ce<=1;
curr_st<=STEP5;
end
STEP5:curr_st<=STEP6;
STEP6:begin
mul1_ce<=0;
mul2_ce<=0;
mul3_ce<=0;
pwm_zkb<=mul1_result+mul2_result+mul3_result;
pwm_zkb_vld<=1;
curr_st<=IDLE;
end
default;
endcase
end
MUL_SIGN_32X32 U_MUL1(
.clock (clk ), // input clk
.aclr (~rst_n ),
.dataa (mul1_a ), // input [15 : 0] a
.datab (mul1_b ), // input [15 : 0] b
.clken (mul1_ce ), // input ce
.result (mul1_result) // output [31 : 0] p
);
MUL_SIGN_32X32 U_MUL2(
.clock (clk ), // input clk
.aclr (~rst_n ),
.dataa (mul2_a ), // input [15 : 0] a
.datab (mul2_b ), // input [15 : 0] b
.clken (mul2_ce ), // input ce
.result (mul2_result) // output [31 : 0] p
);
MUL_SIGN_32X32 U_MUL3(
.clock (clk ), // input clk
.aclr (~rst_n ),
.dataa (mul3_a ), // input [15 : 0] a
.datab (mul3_b ), // input [15 : 0] b
.clken (mul3_ce ), // input ce
.result (mul3_result) // output [31 : 0] p
);
endmodule
pwm_drv:根据pid_ctrl模块计算出的占空比,输出PWM信号,PWM信号一分为二,一路通过FPGA管脚输出,可以用示波器观测到波形,另一路直接传给zkb_calc模块,这样就构成了一个闭环系统。该模块需要用到乘法和除法计算,需要调用相应的IPCORE。
module pwm_drv(
input clk,
input rst_n,
input [31:0] pwm_zkb,//x10000
input pwm_zkb_vld,
output reg pwm
);
parameter period_num=500;//FREQ 10K,频率越大,可调占空比精度越低,实测如果频率是100K,占空比只能精确到个位,频率是10K,可确到小数点后1位。
reg [19:0] period_cnt;
reg [31:0] hcnt;
reg [35:0] div_dividend;
reg [31:0] div_divisor;
reg div_ce;
wire[31:0] quotient;
reg rdy;
reg [31:0] mul_a,mul_b;
reg mul_ce;
wire[63:0] mul_result;
reg [3:0] curr_st;
always@(posedge clk)rdy<=div_ce;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
hcnt<=0;
else if(rdy)
hcnt<=quotient[31:0];
else;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
period_cnt<=0;
else if(period_cnt==period_num-1)
period_cnt<=0;
else
period_cnt<=period_cnt+1;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
curr_st<=0;
mul_a<=0;
mul_b<=0;
mul_ce<=0;
div_ce<=0;
div_dividend<=0;
div_divisor<=0;
end else case(curr_st)
0:begin
div_ce<=0;
if(pwm_zkb_vld)
curr_st<=1;
else
;
end
1:begin
mul_a<=period_num;
mul_b<=pwm_zkb;
mul_ce<=1;
curr_st<=2;
end
2:begin
mul_ce<=0;
curr_st<=3;
end
3:begin
div_dividend<=mul_result[63:0];
div_divisor<=1000000;//占空比扩大100倍,360就是3.6%,0.036,此处引起误差
div_ce<=1;
curr_st<=0;
end
default:;
endcase
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
pwm<=0;
else if(period_cnt
我们设置的占空比目标值是980,即9.8%。
Kp=0.1,Ki=0.03,Kd=0;
设置如下图:
仿真如下图所示:
通过仿真可以看到,pid_ctrl模块输出的占空比值(pwm_zkb)是一条平滑的曲线,从0缓慢上升到98160,然后稳定下来。实验测出的占空比(actual)等于980,与目标值相等。在这个过程中误差(error)也慢慢减小,直到误差等于0,这就是一个闭环调节过程。
我们将Ki参数调大了,发现信号上升的坡度变陡了,这样的好处是缩短了调节时间,便信号能更快的达到我们的目标值,但是信号出现了振荡(超调)现象,即先是超过了目标值(980),然后才慢慢趋于稳定。Kp参数越大,超调现象越严重,在实际使用中我们是不允许有严重振荡现象出现,因为这样会造成我们的控制系统出现问题。比如我们控制无人机,比如我们设置1000米的高度,如果振荡严重,那么无人机会突然一下上升到1000多米的高度,然后再降到1000米,在振荡过程中,如果1200米处有一个障碍物,那无人机就撞上障碍物了,导致严重的后果,所以我们要避免出现严重的超调现象。解决超调现象需要Ki和Kd两个参数来调节。
可明显观察到超调现象减弱了很多。