/************ Introduction to PI Controllers * The continuous form of a PI loop is * * A(t) = P e(t) + I∫ e(t')dt' * where e(t) is the error (setpoint - measured), and * the integral goes from 0 to the current time 't'. * * In digital systems the integral must be approximated. * The normal way of doing this is a first-order approximation * of the derivative of A(t). * * dA(t)/dt = P de(t)/dt + Ie(t) * A(t_n) - A(t_{n-1}) ≅ P (e(t_n) - e(t_{n-1})) + Ie(t_n)Δt * A(t_n) ≅ A(t_{n-1}) + e(t_n)(P + IΔt) - Pe(t_{n-1}) * * Using α = P + IΔt, and denoting A(t_{n-1}) as A_p, * * A ≅ A_p + αe - Pe_p. * * The formula above is what this module implements. This way, * the controller only has to store two values between each * run of the loop: the previous error and the previous output. * This also reduces the amount of (redundant) computations * the loop must execute each iteration. * * Calculating α requires knowing the precise timing of each * control loop cycle, which in turn requires knowing the * ADC and DAC timings. This is done outside the Verilog code. * and can be calculated from simulating one iteration of the * control loop. * *************** Fixed Point Integers ************* * A regular number is stored in decimal: 123056. * This is equal to * 6*10^0 + 5*10^1 + 0*10^2 + 3*10^3 + 2*10^4 + 1*10^5. * A whole binary number is only ones and zeros: 1101, and is * equal to * 1*2^0 + 0*2^1 + 1*2^2 + 1*2^3. * * Fixed-point integers shift the exponent of each number by a * fixed amount. For instance, 123.056 is * 6*10^-3 + 5*10^-2 + 0*10^-1 + 3*10^0 + 2*10^1 + 1*10^2. * Similarly, the fixed point binary integer 11.01 is * 1*2^-2 + 0*2^-1 + 1*2^0 + 1*2^1. * * To a computer, a whole binary number and a fixed point binary * number are stored in exactly the same way: no decimal point * is stored. It is only the *interpretation* of the data that * changes. * * Fixed point numbers are denoted WHOLE.FRAC or [WHOLE].[FRAC], * where WHOLE is the amount of whole number bits (including sign) * and FRAC is the amount of fractional bits (2^-1, 2^-2, etc.). * * The rules for how many digits the output has given an input * is the same for fixed point binary and regular decimals. * * Addition: W1.F1 + W2.F2 = [max(W1,W2)+1].[max(F1,F2)] * Multiplication: W1.F1 * W2.F2 = [W1+W2].[F1+F2] * *************** Precision ************** * The control loop is designed around these values, but generally * does not hardcode them. * * Since α and P are precalculated outside of the loop, their * conversion to numbers the loop understands is done outside of * the loop and in the kernel. * * The 18-bit ADC is twos-compliment, -10.24V to 10.24V, * with 78μV per increment. * The 20-bit DAC is twos-compliment, -10V to 10V. * * The `P` constant has a minimum value of 1e-7 with a precision * of 1e-9, and a maxmimum value of 1. * * The `I` constant has a minimum value of 1e-4 with a precision * of 1e-6 and a maximum value of 100. * * Δt is cycles/100MHz. This makes Δt at least 10 ns, with a * maximum of 1 ms. * * Intermediate values are 48-bit fixed-point integers multiplied * by the step size of the ADC. The first 18 bits are the whole * number and sign bits. This means intermediate values correspond * exactly to values as understood by the ADC, with extra precision. * * To get the normal fixed-point value of an intermediate value, * multiply it by 78e-6. To convert a normal fixed-point integer * to an intermediate value, multiply it by 1/78e-6. In both * cases, the conversion constant is a normal fixed-point integer. * * For instance, to encode the value 78e-6 as an intermediate * value, multiply it by 1/78e-6 to obtain 1. Thus the value * should be stored as 1 (whole bit) followed by zeros (fractional * bits). */ /* * 0x3214*78e-6 = 0.9996 + lower order (storable as 15 whole bits) */ `define ERR_WID (ADC_WID + 1) module control_loop #( parameter ADC_WID = 18, /* Code assumes DAC_WID > ADC_WID. If/when this is not the * case, truncation code must be changed. */ parameter DAC_WID = 24, /* Analog Devices DACs have a register code in the upper 4 bits. * The data follows it. */ parameter DAC_DATA_WID = 20, parameter CONSTS_WID = 48, // larger than ADC_WID parameter CONSTS_FRAC_WID = CONSTS_WID-15, parameter DELAY_WID = 16, /* [ERR_WID_SIZ-1:0] must be able to store * ERR_WID (ADC_WID + 1). */ parameter ERR_WID_SIZ = 6, parameter ADC_POLARITY = 1, parameter ADC_PHASE = 0, parameter DAC_POLARITY = 0, parameter DAC_PHASE = 1 ) ( input clk, input arm, output adc_sck, input adc_in, output adc_conv, output dac_sck, output dac_ss, output dac_out, /* Informational output. * These registers are also used for storing information while * the loop is running. */ output signed [ERR_WID-1:0] err_cur, output signed [CONSTS_WID-1:0] adj, input signed [ADC_WID-1:0] setpt_in, input signed [CONSTS_WID-1:0] cl_alpha_in, input signed [CONSTS_WID-1:0] cl_p_in, input [DELAY_WID-1:0] delay_in ); /* Registers used to lock in values at the start of each iteration */ reg signed [ADC_WID-1:0] setpt = 0; reg signed [CONSTS_WID-1:0] cl_alpha_reg = 0; reg signed [CONSTS_WID-1:0] cl_p_reg = 0; reg [DELAY_WID-1:0] saved_delay = 0; /* Registers for PI calculations */ reg signed [ERR_WID-1:0] err_prev = 0; /****** State machine * * -> WAIT_ON_ARM -> WAIT_ON_ADC -> WAIT_ON_MUL -\ * \------\------------ WAIT_ON_DAC discard fractional bits: CONSTS_WHOLE+ADC_WID+1.(DAC_DATA_WID - ADC_WID) * --> Saturate-truncate: ADC_WID.(DAC_DATA_WID-ADC_WID) * --> reinterpret and write into DAC: DAC_DATA_WID.0 */ /******* Get measured value ********/ reg signed [ADC_WID-1:0] measured = 0; reg adc_arm = 0; wire adc_finished; spi_master_no_write #( .WID(ADC_WID), .POLARITY(ADC_POLARITY), .PHASE(ADC_PHASE), .CYCLE_HALF_WAIT(1), .TIMER_LEN(3) ) adc ( .clk(clk), .from_slave(adcbuf), .miso(adc_in), .sck_wire(adc_sck), .arm(adc_arm), .finished(adc_finished) ); /**** Calculate Error ****/ assign err_cur = measured - setpoint; /****** Multiplication ******* * Truncation of a fixed-point integer to a smaller buffer requires * 1) truncating higher order bits * 2) removing lower order bits * * The ADC number has no fractional digits, so the fixed point output * is [CONSTS_WHOLE + ERR_WID].CONSTS_FRAC_WID * with total width CONSTS_WID + ERR_WID * * Both multipliers are armed at the same time. * Their output wires are ANDed together so the state machine * progresses when both are finished. */ localparam MUL_WHOLE_WID = CONSTS_WHOLE + ERR_WID; localparam MUL_FRAC_WID = CONSTS_FRAC; localparam MUL_WID = MUL_WHOLE_WID + MUL_FRAC_WID; reg arm_mul = 0; wire alpha_err_fin; wire signed [MUL_WID-1:0] alpha_err; wire p_err_prev_fin; wire signed [MUL_WID-1:0] p_err_prev; wire mul_finished = alpha_err_prev_fin & p_err_fin; /* αe */ boothmul #( .A1_LEN(CONSTS_WID), .A2_LEN(ERR_WID), .A2LEN_SIZ(ERR_WID_SIZ) ) boothmul_alpha_err_mul ( .clk(clk), .arm(arm_mul), .a1(cl_alpha_reg), .a2(err), .outn(alpha_err), .fin(alpha_err_fin) ); /* Pe_p */ boothmul #( .A1_LEN(CONSTS_WID), .A2_LEN(ERR_WID), .A2LEN_SIZ(ERR_WID_SIZ) ) booth_mul_P_err_mul ( .clk(clk), .arm(arm_mul), .a1(cl_p_reg), .a2(err_prev), .outn(p_err_prev), .fin(p_err_prev_fin) ); /**** Subtraction after multiplication ****/ localparam SUB_WHOLE_WID = MUL_WHOLE_WID + 1; localparam SUB_FRAC_WID = MUL_FRAC_WID; localparam SUB_WID = SUB_WHOLE_WID + SUB_FRAC_WID; reg signed [SUB_WID-1:0] adj_old; wire signed [SUB_WID-1:0] newadj = adj_old + alpha_err - p_err_prev; /**** Discard fractional bits **** * The truncation of the subtraction result first takes off the lower * order bits: * [ SUB_WHOLE_WID ].[ SUB_FRAC_WID ] * [ SUB_WHOLE_WID ].[RTRUNC_FRAC_WID]############ * (SUB_FRAC_WID - RTRUNC_FRAC_WID)^ */ localparam RTRUNC_FRAC_WID = DAC_DATA_WID - ADC_WID; localparam RTRUNC_WHOLE_WID = SUB_WHOLE_WID; localparam RTRUNC_WID = RTRUNC_WHOLE_WID + RTRUNC_FRAC_WID; wire signed rtrunc[RTRUNC_WID-1:0] = newadj[SUB_WID-1:SUB_FRAC_WID-RTRUNC_FRAC_WID]; /**** Truncate-Saturate **** * Truncate the result into a value acceptable to the DAC. * [ SUB_WHOLE_WID ].[RTRUNC_FRAC_WID]############ * [ADC_WID].[DAC_DATA_WID - ADC_WID] * reinterpreted as * [DAC_DATA_WID].0 */ wire signed dac_adj_val[DAC_DATA_WID-1:0]; intsat #( .IN_LEN(RTRUNC_WID), .LTRUNC(DAC_DATA_WID) ) ( .inp(newadj_rtrunc), .outp(dac_adj_val) ); /**** Write to DAC ****/ reg dac_arm = 0; spi_master_no_read #( .WID(DAC_WID), .WID_LEN(5), .CYCLE_HALF_WAIT(3), .TIMER_LEN(3), .POLARITY(DAC_POLARITY), .PHASE(DAC_PHASE) ) dac ( .clk(clk), .to_slave({4'b0010,dac_adj_val}), .mosi(dac_out), .sck_wire(dac_sck), .arm(dac_arm) ); reg [DELAY_WID-1:0] timer = 0; always @ (posedge clk) begin case (state) WAIT_ON_ARM: begin if (!arm) begin adj_prev <= 0; err_prev <= 0; timer <= 0; end else if (timer == 0) begin saved_delay <= delay_in; timer <= 1; end else if (timer < saved_delay) begin timer <= timer + 1; setpt <= setpt_in; cl_alpha_reg <= cl_alpha_in; cl_p_reg <= cl_p_in; end else begin state <= WAIT_ON_ADC; timer <= 0; adc_arm <= 1; end end WAIT_ON_ADC: if (adc_finished) begin adc_arm <= 0; arm_mul <= 1; state <= WAIT_ON_MUL; end WAIT_ON_MUL: if (mul_finished) begin arm_mul <= 0; dac_arm <= 1; state <= WAIT_ON_DAC; end WAIT_ON_DAC: if (dac_finished) begin state <= WAIT_ON_ARM; dac_arm <= 0; end end endmodule