motor_controller_hil.v 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. `timescale 1ns/1ps
  2. // -----------------------------------------------------------------------------
  3. // Motor controller used for the single-UART HIL teaching demo.
  4. //
  5. // The FPGA is real. The motor is simulated on the PC. Each control exchange is
  6. // one simulated 10 ms step:
  7. // FPGA -> PC : PWM/enable/state command (packet type 0x90)
  8. // PC -> FPGA: RPM/current/fault feedback (packet type 0x91)
  9. //
  10. // The controller intentionally uses only integer arithmetic so that the RTL is
  11. // easy to synthesize and easy to understand.
  12. // -----------------------------------------------------------------------------
  13. module motor_controller_hil (
  14. input wire clk,
  15. input wire reset_n,
  16. input wire start,
  17. input wire feedback_valid,
  18. input wire [15:0] feedback_rpm,
  19. input wire [15:0] feedback_current_ma,
  20. input wire feedback_fault,
  21. input wire feedback_encoder_ok,
  22. input wire [15:0] feedback_time_ms,
  23. output wire busy,
  24. output wire done,
  25. output reg [6:0] pwm_duty,
  26. output reg motor_enable,
  27. output reg direction,
  28. output wire [15:0] state_id,
  29. output reg [7:0] fault_code,
  30. output wire trace_valid,
  31. output wire [15:0] trace_id,
  32. output wire [15:0] trace_timestamp,
  33. output wire [31:0] trace_data
  34. );
  35. // Stable IDs. Python scans these @TRACE annotations to build the map.
  36. localparam [15:0] M_IDLE = 16'h4000; // @TRACE IDLE | Waiting for PC start command
  37. localparam [15:0] M_START = 16'h4001; // @TRACE START | First motor command after start
  38. localparam [15:0] M_ACCEL = 16'h4002; // @TRACE ACCEL | Increase PWM until measured speed reaches target band
  39. localparam [15:0] M_RUN = 16'h4003; // @TRACE RUN | Closed-loop speed trim around 1000 RPM
  40. localparam [15:0] M_STOP = 16'h4004; // @TRACE STOP | Disable drive and wait for simulated motor to coast down
  41. localparam [15:0] M_FAULT = 16'h4005; // @TRACE FAULT | Protection state before finishing the test
  42. localparam [15:0] M_DONE = 16'h4006; // @TRACE DONE | Test finished; top-level will dump the Trace RAM
  43. reg [15:0] state;
  44. reg [15:0] step_count;
  45. reg [7:0] run_hold_count;
  46. reg [7:0] stop_hold_count;
  47. assign busy = (state != M_IDLE) && (state != M_DONE);
  48. assign done = (state == M_DONE);
  49. assign state_id = state;
  50. // One Trace record per HIL feedback/control step, not one record per 25 MHz
  51. // FPGA clock. This makes TRACE_DEPTH=1024 useful for a slow mechanical plant.
  52. assign trace_valid = feedback_valid && (state != M_IDLE) && (state != M_DONE);
  53. assign trace_id = state;
  54. assign trace_timestamp = feedback_time_ms;
  55. // 32-bit motor snapshot, no division/multiplier required:
  56. // [31:20] measured RPM, saturated to 12 bits by truncation
  57. // [19:13] PWM duty 0..100
  58. // [12:1] current in mA, low 12 bits (demo stays < 4096 mA)
  59. // [0] feedback fault flag
  60. assign trace_data = {
  61. feedback_rpm[11:0],
  62. pwm_duty[6:0],
  63. feedback_current_ma[11:0],
  64. feedback_fault
  65. };
  66. always @(posedge clk or negedge reset_n) begin
  67. if (!reset_n) begin
  68. state <= M_IDLE;
  69. pwm_duty <= 7'd0;
  70. motor_enable <= 1'b0;
  71. direction <= 1'b1;
  72. fault_code <= 8'd0;
  73. step_count <= 16'd0;
  74. run_hold_count <= 8'd0;
  75. stop_hold_count <= 8'd0;
  76. end else begin
  77. if (start && (state == M_IDLE || state == M_DONE)) begin
  78. state <= M_START;
  79. pwm_duty <= 7'd25;
  80. motor_enable <= 1'b1;
  81. direction <= 1'b1;
  82. fault_code <= 8'd0;
  83. step_count <= 16'd0;
  84. run_hold_count <= 8'd0;
  85. stop_hold_count <= 8'd0;
  86. end else if (feedback_valid && (state != M_IDLE) && (state != M_DONE)) begin
  87. step_count <= step_count + 1'b1;
  88. // Once FAULT is entered, keep one explicit FAULT exchange in the
  89. // Trace and then finish, even if the external fault remains asserted.
  90. if (state == M_FAULT) begin
  91. pwm_duty <= 7'd0;
  92. motor_enable <= 1'b0;
  93. state <= M_DONE;
  94. end else if (feedback_fault) begin
  95. state <= M_FAULT;
  96. pwm_duty <= 7'd0;
  97. motor_enable <= 1'b0;
  98. fault_code <= 8'd1; // simulator/driver fault input
  99. end else if (!feedback_encoder_ok && (step_count >= 16'd5)) begin
  100. state <= M_FAULT;
  101. pwm_duty <= 7'd0;
  102. motor_enable <= 1'b0;
  103. fault_code <= 8'd2; // encoder feedback lost
  104. end else if (feedback_current_ma > 16'd2200) begin
  105. state <= M_FAULT;
  106. pwm_duty <= 7'd0;
  107. motor_enable <= 1'b0;
  108. fault_code <= 8'd3; // over-current
  109. end else begin
  110. case (state)
  111. M_IDLE: begin // @TRACE_EXEC
  112. state <= M_IDLE;
  113. pwm_duty <= 7'd0;
  114. motor_enable <= 1'b0;
  115. end
  116. M_START: begin // @TRACE_EXEC
  117. state <= M_ACCEL;
  118. pwm_duty <= 7'd35;
  119. motor_enable <= 1'b1;
  120. end
  121. M_ACCEL: begin // @TRACE_EXEC
  122. if (feedback_rpm >= 16'd950) begin
  123. state <= M_RUN;
  124. run_hold_count <= 8'd0;
  125. end else if (step_count >= 16'd80) begin
  126. state <= M_FAULT;
  127. pwm_duty <= 7'd0;
  128. motor_enable <= 1'b0;
  129. fault_code <= 8'd4; // startup timeout
  130. end else begin
  131. // Three-stage acceleration command. The virtual motor
  132. // is modeled as about 1800 RPM at 100% PWM, so 55..58%
  133. // is close to the 1000 RPM target and avoids large overshoot.
  134. if (feedback_rpm < 16'd300)
  135. pwm_duty <= 7'd45;
  136. else if (feedback_rpm < 16'd700)
  137. pwm_duty <= 7'd55;
  138. else
  139. pwm_duty <= 7'd58;
  140. end
  141. end
  142. M_RUN: begin // @TRACE_EXEC
  143. // Very small integral-like trim around the 1000 RPM target.
  144. if (feedback_rpm < 16'd980) begin
  145. if (pwm_duty < 7'd90)
  146. pwm_duty <= pwm_duty + 1'b1;
  147. end else if (feedback_rpm > 16'd1020) begin
  148. if (pwm_duty > 7'd5)
  149. pwm_duty <= pwm_duty - 1'b1;
  150. end
  151. if (run_hold_count >= 8'd49) begin
  152. state <= M_STOP;
  153. pwm_duty <= 7'd0;
  154. motor_enable <= 1'b0;
  155. stop_hold_count <= 8'd0;
  156. end else begin
  157. run_hold_count <= run_hold_count + 1'b1;
  158. end
  159. end
  160. M_STOP: begin // @TRACE_EXEC
  161. pwm_duty <= 7'd0;
  162. motor_enable <= 1'b0;
  163. if (feedback_rpm <= 16'd20) begin
  164. state <= M_DONE;
  165. end else if (stop_hold_count >= 8'd80) begin
  166. state <= M_FAULT;
  167. fault_code <= 8'd5; // failed to stop in time
  168. end else begin
  169. stop_hold_count <= stop_hold_count + 1'b1;
  170. end
  171. end
  172. M_FAULT: begin // @TRACE_EXEC
  173. // Keep one explicit FAULT exchange in the Trace, then finish.
  174. pwm_duty <= 7'd0;
  175. motor_enable <= 1'b0;
  176. state <= M_DONE;
  177. end
  178. M_DONE: begin // @TRACE_EXEC
  179. pwm_duty <= 7'd0;
  180. motor_enable <= 1'b0;
  181. state <= M_DONE;
  182. end
  183. default: begin
  184. state <= M_FAULT;
  185. pwm_duty <= 7'd0;
  186. motor_enable <= 1'b0;
  187. fault_code <= 8'hFF;
  188. end
  189. endcase
  190. end
  191. end
  192. end
  193. end
  194. endmodule