Design of PID controller based on FPGA

1 Knowledge background

PID control should be regarded as a very widely used control algorithm. Common ones include controlling the ambient temperature, controlling the flight height and speed of the drone, etc. We divide PID into three parameters, as follows:
P-proportional control, the basic function is to increase the control object in a linear manner, and dynamically output at a constant ratio. The disadvantage is that it will produce a steady-state error.
I-Integral control, its basic function is to eliminate the steady-state error, but its disadvantage is that it will produce overshoot.
D-Differential control, its basic function is to weaken the overshoot phenomenon and increase the inertial response speed.

PID control system principle block diagram
Insert image description here

PID formula
Insert image description here

In general, when the output of the system is obtained, the output is added to the input through three operation methods: proportion, integral, and differential, thus forming a closed-loop control system. In real practice, the most difficult thing is how to determine the coefficients of the three terms, which requires a lot of experiments and experience to determine. Through constant trying and thinking, you can select the appropriate parameters and make an excellent PID controller.

2 System framework

No matter how much theory is said, it is better to practice it yourself. I made a simple PID control model. Because PID is used to control the PWM duty cycle, the output PWM duty cycle and the collected duty cycle are basically different. There are errors, so there are still some differences from real PID closed-loop control. This is just an example to illustrate how to use FPGA to make a simple PID algorithm. Then you can observe the curve changes of adjusting the duty cycle through Modelsim simulation. Finally, verify it on the development board. Use an oscilloscope to test the output PWM duty cycle and compare it with our settings. The target duty cycle is consistent. If we want to control the motor speed, there is friction resistance and speed acquisition error. At this time, we need to adjust Kp, Ki, and Kd to achieve the best control effect. The model block diagram is as follows:
Insert image description here

target: target value
actual: actual value

3 Experimental requirements and purposes

Generates a PWM signal with a fixed frequency and a duty cycle that can be adjusted by pressing the button. When KEY1 is pressed, the duty cycle increases by 10%, and when KEY2 is pressed, the duty cycle decreases by 10%. A PID calculation is performed for each PWM cycle.

4 Required Hardware

  1. ALOGIC_V4 FPGA development board
  2. FPGA downloader
  3. Oscilloscope

5 Formula Analysis

We have listed the formulas of PID theory before, but it is still a bit confusing to implement it using Verilog language just by looking at the theoretical formulas, so we need to make slight changes in the company and convert it into a formula that is convenient for using FPGA to implement PID:
Insert image description here

Kp: proportional term parameter
Ki: integral term parameter
Kd: differential term parameter
error: error, target-actual
sum_error: sum of errors
error-last_error: current error minus the previous error

6 Programming

The program block diagram is as follows:

Insert image description here

key_xd: key debounce module, this module directly calls the debounce routine of our development board.
target_gen: Target value generation module, which generates the desired duty cycle value. Since the FPGA cannot handle decimals, the data is expanded 100 times to facilitate processing. If the set value is 980, the actual duty cycle is 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: Duty cycle detection module, calculates the actual output PWM duty cycle, that is, the target value. Since the FPGA cannot handle decimals, the data is expanded 100 times to facilitate processing. If the set value is 980, the actual duty cycle is 9.8%. This module requires division. We cannot use "/" directly in the code to perform calculations. Instead, we need to call the divider IPCORE to perform calculations.

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 calculation module, according to the above formula, addition and multiplication are needed. For addition, we can use "+" directly in the code to calculate. For multiplication, we need to call the IPCORE of the multiplier (because the error is divided into positive and negative , so our multiplier IPCORE also needs to be set to signed, this must be paid attention to, otherwise there will be problems in the calculation), and finally the duty cycle value is calculated. Since the FPGA cannot handle decimals, Kp, Ki, Kd are expanded 100 times. If Kp=10, the real value is 0.1. According to the formula, we know that the final PWM duty cycle value is expanded by 10,000 times.

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: According to the duty cycle calculated by the pid_ctrl module, the PWM signal is output. The PWM signal is divided into two. One channel is output through the FPGA pin. The waveform can be observed with an oscilloscope. The other channel is directly passed to the zkb_calc module, thus forming a Closed loop system. This module needs to use multiplication and division calculations and needs to call the corresponding 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<hcnt[15:0])
			pwm<=1;
		else
			pwm<=0;
	end
	MUL_UNSIGN_32X32 U_MUL(
	.clock	(clk		), // input clk
	.aclr	(~rst_n		),
	.dataa	(mul_a		), // input [15 : 0] a
	.datab	(mul_b		), // input [15 : 0] b
	.clken	(mul_ce	), // input ce
	.result	(mul_result) // output [31 : 0] p
	);
	DIV U_DIV(
	.denom	({8'b0,div_divisor}),
	.numer	({4'b0,div_dividend[35:0]}),
	.quotient(quotient),
	.remain  ()
	);
endmodule

7 Simulation (observe the adjustment effect through Modelsim simulation)

The duty cycle target value we set is 980, which is 9.8%.

7.1 The first set of PID parameters

Kp=0.1, Ki=0.03, Kd=0;
settings are as shown below:
Insert image description here
The simulation is as shown below:
Insert image description here

It can be seen through simulation that the duty cycle value (pwm_zkb) output by the pid_ctrl module is a smooth curve, slowly rising from 0 to 98160, and then stabilizing. The experimentally measured duty cycle (actual) is equal to 980, which is equal to the target value. During this process, the error also slowly decreases until the error is equal to 0. This is a closed-loop adjustment process.

7.2 The second set of PID parameters

Kp=0.1, Ki=0.15, Kd=0;
parameter settings are as follows:
Insert image description here

The simulation is shown in the figure below:
Insert image description here

We increased the Ki parameter and found that the rising slope of the signal became steeper. This has the advantage of shortening the adjustment time, so that the signal can reach our target value faster, but the signal has oscillation (overshoot) phenomenon, that is, It first exceeded the target value (980), and then slowly stabilized. The larger the Kp parameter, the more serious the overshoot phenomenon is. In actual use, we do not allow serious oscillation phenomena to occur, because this will cause problems in our control system. For example, when we control a drone, if we set a height of 1,000 meters, if the oscillation is severe, the drone will suddenly rise to a height of more than 1,000 meters, and then drop to 1,000 meters. During the oscillation process, if there is a drone at 1,200 meters Obstacle, then the drone will hit the obstacle, causing serious consequences, so we must avoid serious overshoot. To solve the overshoot phenomenon, two parameters, Ki and Kd, need to be adjusted.

7.3 The third set of PID parameters

Kp=0.1, Ki=0.1, Kd=0.15;
set as shown below:
Insert image description here

The simulation is shown in the figure below:
Insert image description here

It can be clearly observed that the overshoot phenomenon is reduced a lot.

8 Board verification

Use our ALOGIC_V4 development board to verify that the duty cycle can reach our target value. As shown below:
Insert image description here

Insert image description here

Guess you like

Origin blog.csdn.net/Moon_3181961725/article/details/127139612