Переглянути джерело

replace FPGA project with PWM control CI demo

botao 1 місяць тому
батько
коміт
f69a5f50b4

+ 2 - 0
.gitignore

@@ -10,3 +10,5 @@ output/
 *.bit
 *.bid
 *.tsm
+*.vvp
+*.vcd

+ 0 - 11
fpga/constraints/ef2l45_motor_hil_trace.adc

@@ -1,11 +0,0 @@
-# EF2L45BG256B MINI V2.0 - single CP2102 motor HIL + Trace
-# KEY_1 is N6 and is active low.
-set_pin_assignment {sys_clk}         {LOCATION=J1;  IOSTANDARD=LVCMOS33; PULLTYPE=NONE;}
-set_pin_assignment {reset_n}         {LOCATION=N6;  IOSTANDARD=LVCMOS33; PULLTYPE=PULLUP;}
-set_pin_assignment {uart_tx}         {LOCATION=C4;  IOSTANDARD=LVCMOS33; PULLTYPE=NONE;}
-set_pin_assignment {uart_rx}         {LOCATION=E6;  IOSTANDARD=LVCMOS33; PULLTYPE=PULLUP;}
-set_pin_assignment {led_heartbeat_n} {LOCATION=H16; IOSTANDARD=LVCMOS33; PULLTYPE=NONE;}
-set_pin_assignment {led_reset_n}     {LOCATION=H15; IOSTANDARD=LVCMOS33; PULLTYPE=NONE;}
-set_pin_assignment {led_activity_n}  {LOCATION=J16; IOSTANDARD=LVCMOS33; PULLTYPE=NONE;}
-set_pin_assignment {led_tx_n}        {LOCATION=J15; IOSTANDARD=LVCMOS33; PULLTYPE=NONE;}
-set_pin_assignment {led_error_n}     {LOCATION=K16; IOSTANDARD=LVCMOS33; PULLTYPE=NONE;}

+ 0 - 1
fpga/constraints/ef2l45_motor_hil_trace.sdc

@@ -1 +0,0 @@
-create_clock -name sys_clk -period 40 -waveform {0 20} [get_ports sys_clk]

+ 0 - 212
fpga/rtl/motor_controller_hil.v

@@ -1,212 +0,0 @@
-`timescale 1ns/1ps
-
-// -----------------------------------------------------------------------------
-// Motor controller used for the single-UART HIL teaching demo.
-//
-// The FPGA is real. The motor is simulated on the PC. Each control exchange is
-// one simulated 10 ms step:
-//   FPGA -> PC : PWM/enable/state command (packet type 0x90)
-//   PC   -> FPGA: RPM/current/fault feedback (packet type 0x91)
-//
-// The controller intentionally uses only integer arithmetic so that the RTL is
-// easy to synthesize and easy to understand.
-// -----------------------------------------------------------------------------
-module motor_controller_hil (
-    input  wire        clk,
-    input  wire        reset_n,
-    input  wire        start,
-
-    input  wire        feedback_valid,
-    input  wire [15:0] feedback_rpm,
-    input  wire [15:0] feedback_current_ma,
-    input  wire        feedback_fault,
-    input  wire        feedback_encoder_ok,
-    input  wire [15:0] feedback_time_ms,
-
-    output wire        busy,
-    output wire        done,
-    output reg  [6:0]  pwm_duty,
-    output reg         motor_enable,
-    output reg         direction,
-    output wire [15:0] state_id,
-    output reg  [7:0]  fault_code,
-
-    output wire        trace_valid,
-    output wire [15:0] trace_id,
-    output wire [15:0] trace_timestamp,
-    output wire [31:0] trace_data
-);
-    // Stable IDs. Python scans these @TRACE annotations to build the map.
-    localparam [15:0] M_IDLE  = 16'h4000; // @TRACE IDLE  | Waiting for PC start command
-    localparam [15:0] M_START = 16'h4001; // @TRACE START | First motor command after start
-    localparam [15:0] M_ACCEL = 16'h4002; // @TRACE ACCEL | Increase PWM until measured speed reaches target band
-    localparam [15:0] M_RUN   = 16'h4003; // @TRACE RUN   | Closed-loop speed trim around 1000 RPM
-    localparam [15:0] M_STOP  = 16'h4004; // @TRACE STOP  | Disable drive and wait for simulated motor to coast down
-    localparam [15:0] M_FAULT = 16'h4005; // @TRACE FAULT | Protection state before finishing the test
-    localparam [15:0] M_DONE  = 16'h4006; // @TRACE DONE  | Test finished; top-level will dump the Trace RAM
-
-    reg [15:0] state;
-    reg [15:0] step_count;
-    reg [7:0]  run_hold_count;
-    reg [7:0]  stop_hold_count;
-
-    assign busy     = (state != M_IDLE) && (state != M_DONE);
-    assign done     = (state == M_DONE);
-    assign state_id = state;
-
-    // One Trace record per HIL feedback/control step, not one record per 25 MHz
-    // FPGA clock. This makes TRACE_DEPTH=1024 useful for a slow mechanical plant.
-    assign trace_valid     = feedback_valid && (state != M_IDLE) && (state != M_DONE);
-    assign trace_id        = state;
-    assign trace_timestamp = feedback_time_ms;
-
-    // 32-bit motor snapshot, no division/multiplier required:
-    //   [31:20] measured RPM, saturated to 12 bits by truncation
-    //   [19:13] PWM duty 0..100
-    //   [12:1]  current in mA, low 12 bits (demo stays < 4096 mA)
-    //   [0]     feedback fault flag
-    assign trace_data = {
-        feedback_rpm[11:0],
-        pwm_duty[6:0],
-        feedback_current_ma[11:0],
-        feedback_fault
-    };
-
-    always @(posedge clk or negedge reset_n) begin
-        if (!reset_n) begin
-            state           <= M_IDLE;
-            pwm_duty        <= 7'd0;
-            motor_enable    <= 1'b0;
-            direction       <= 1'b1;
-            fault_code      <= 8'd0;
-            step_count      <= 16'd0;
-            run_hold_count  <= 8'd0;
-            stop_hold_count <= 8'd0;
-        end else begin
-            if (start && (state == M_IDLE || state == M_DONE)) begin
-                state           <= M_START;
-                pwm_duty        <= 7'd25;
-                motor_enable    <= 1'b1;
-                direction       <= 1'b1;
-                fault_code      <= 8'd0;
-                step_count      <= 16'd0;
-                run_hold_count  <= 8'd0;
-                stop_hold_count <= 8'd0;
-            end else if (feedback_valid && (state != M_IDLE) && (state != M_DONE)) begin
-                step_count <= step_count + 1'b1;
-
-                // Once FAULT is entered, keep one explicit FAULT exchange in the
-                // Trace and then finish, even if the external fault remains asserted.
-                if (state == M_FAULT) begin
-                    pwm_duty     <= 7'd0;
-                    motor_enable <= 1'b0;
-                    state        <= M_DONE;
-                end else if (feedback_fault) begin
-                    state        <= M_FAULT;
-                    pwm_duty     <= 7'd0;
-                    motor_enable <= 1'b0;
-                    fault_code   <= 8'd1;  // simulator/driver fault input
-                end else if (!feedback_encoder_ok && (step_count >= 16'd5)) begin
-                    state        <= M_FAULT;
-                    pwm_duty     <= 7'd0;
-                    motor_enable <= 1'b0;
-                    fault_code   <= 8'd2;  // encoder feedback lost
-                end else if (feedback_current_ma > 16'd2200) begin
-                    state        <= M_FAULT;
-                    pwm_duty     <= 7'd0;
-                    motor_enable <= 1'b0;
-                    fault_code   <= 8'd3;  // over-current
-                end else begin
-                    case (state)
-                        M_IDLE: begin // @TRACE_EXEC
-                            state        <= M_IDLE;
-                            pwm_duty     <= 7'd0;
-                            motor_enable <= 1'b0;
-                        end
-
-                        M_START: begin // @TRACE_EXEC
-                            state        <= M_ACCEL;
-                            pwm_duty     <= 7'd35;
-                            motor_enable <= 1'b1;
-                        end
-
-                        M_ACCEL: begin // @TRACE_EXEC
-                            if (feedback_rpm >= 16'd950) begin
-                                state          <= M_RUN;
-                                run_hold_count <= 8'd0;
-                            end else if (step_count >= 16'd80) begin
-                                state        <= M_FAULT;
-                                pwm_duty     <= 7'd0;
-                                motor_enable <= 1'b0;
-                                fault_code   <= 8'd4; // startup timeout
-                            end else begin
-                                // Three-stage acceleration command. The virtual motor
-                                // is modeled as about 1800 RPM at 100% PWM, so 55..58%
-                                // is close to the 1000 RPM target and avoids large overshoot.
-                                if (feedback_rpm < 16'd300)
-                                    pwm_duty <= 7'd45;
-                                else if (feedback_rpm < 16'd700)
-                                    pwm_duty <= 7'd55;
-                                else
-                                    pwm_duty <= 7'd58;
-                            end
-                        end
-
-                        M_RUN: begin // @TRACE_EXEC
-                            // Very small integral-like trim around the 1000 RPM target.
-                            if (feedback_rpm < 16'd980) begin
-                                if (pwm_duty < 7'd90)
-                                    pwm_duty <= pwm_duty + 1'b1;
-                            end else if (feedback_rpm > 16'd1020) begin
-                                if (pwm_duty > 7'd5)
-                                    pwm_duty <= pwm_duty - 1'b1;
-                            end
-
-                            if (run_hold_count >= 8'd49) begin
-                                state           <= M_STOP;
-                                pwm_duty        <= 7'd0;
-                                motor_enable    <= 1'b0;
-                                stop_hold_count <= 8'd0;
-                            end else begin
-                                run_hold_count <= run_hold_count + 1'b1;
-                            end
-                        end
-
-                        M_STOP: begin // @TRACE_EXEC
-                            pwm_duty     <= 7'd0;
-                            motor_enable <= 1'b0;
-                            if (feedback_rpm <= 16'd20) begin
-                                state <= M_DONE;
-                            end else if (stop_hold_count >= 8'd80) begin
-                                state      <= M_FAULT;
-                                fault_code <= 8'd5; // failed to stop in time
-                            end else begin
-                                stop_hold_count <= stop_hold_count + 1'b1;
-                            end
-                        end
-
-                        M_FAULT: begin // @TRACE_EXEC
-                            // Keep one explicit FAULT exchange in the Trace, then finish.
-                            pwm_duty     <= 7'd0;
-                            motor_enable <= 1'b0;
-                            state        <= M_DONE;
-                        end
-
-                        M_DONE: begin // @TRACE_EXEC
-                            pwm_duty     <= 7'd0;
-                            motor_enable <= 1'b0;
-                            state        <= M_DONE;
-                        end
-
-                        default: begin
-                            state        <= M_FAULT;
-                            pwm_duty     <= 7'd0;
-                            motor_enable <= 1'b0;
-                            fault_code   <= 8'hFF;
-                        end
-                    endcase
-                end
-            end
-        end
-    end
-endmodule

+ 0 - 134
fpga/rtl/motor_feedback_rx.v

@@ -1,134 +0,0 @@
-`timescale 1ns/1ps
-
-// Parse the same 16-byte A5 5A framed protocol used by trace_packet_tx.
-// Only packet type 0x91 (PC -> FPGA MOTOR_FEEDBACK) is accepted.
-module motor_feedback_rx (
-    input  wire        clk,
-    input  wire        reset_n,
-    input  wire [7:0]  rx_data,
-    input  wire        rx_valid,
-
-    output reg         feedback_valid,
-    output reg  [15:0] feedback_seq,
-    output reg  [15:0] feedback_flags,
-    output reg  [31:0] feedback_timestamp,
-    output reg  [15:0] feedback_rpm,
-    output reg  [15:0] feedback_current_ma,
-    output reg         crc_error
-);
-    localparam [7:0] PKT_MOTOR_FB = 8'h91;
-    localparam [1:0] R_SYNC_A5 = 2'd0;
-    localparam [1:0] R_SYNC_5A = 2'd1;
-    localparam [1:0] R_BODY    = 2'd2;
-
-    reg [1:0] state;
-    reg [4:0] index;
-    reg [7:0] crc;
-    reg [7:0] type_l;
-    reg [15:0] seq_l;
-    reg [15:0] flags_l;
-    reg [31:0] time_l;
-    reg [31:0] data_l;
-
-    function [7:0] crc8_update;
-        input [7:0] crc_in;
-        input [7:0] data_in;
-        integer i;
-        reg [7:0] c;
-        begin
-            c = crc_in ^ data_in;
-            for (i = 0; i < 8; i = i + 1) begin
-                if (c[7])
-                    c = (c << 1) ^ 8'h07;
-                else
-                    c = (c << 1);
-            end
-            crc8_update = c;
-        end
-    endfunction
-
-    always @(posedge clk or negedge reset_n) begin
-        if (!reset_n) begin
-            state              <= R_SYNC_A5;
-            index              <= 5'd0;
-            crc                <= 8'd0;
-            type_l             <= 8'd0;
-            seq_l              <= 16'd0;
-            flags_l            <= 16'd0;
-            time_l             <= 32'd0;
-            data_l             <= 32'd0;
-            feedback_valid     <= 1'b0;
-            feedback_seq       <= 16'd0;
-            feedback_flags     <= 16'd0;
-            feedback_timestamp <= 32'd0;
-            feedback_rpm       <= 16'd0;
-            feedback_current_ma<= 16'd0;
-            crc_error          <= 1'b0;
-        end else begin
-            feedback_valid <= 1'b0;
-            crc_error      <= 1'b0;
-
-            if (rx_valid) begin
-                case (state)
-                    R_SYNC_A5: begin
-                        if (rx_data == 8'hA5)
-                            state <= R_SYNC_5A;
-                    end
-
-                    R_SYNC_5A: begin
-                        if (rx_data == 8'h5A) begin
-                            state <= R_BODY;
-                            index <= 5'd2;
-                            crc   <= 8'h00;
-                        end else if (rx_data != 8'hA5) begin
-                            state <= R_SYNC_A5;
-                        end
-                    end
-
-                    R_BODY: begin
-                        if (index <= 5'd14)
-                            crc <= crc8_update(crc, rx_data);
-
-                        case (index)
-                            5'd2:  type_l       <= rx_data;
-                            5'd3:  seq_l[15:8]  <= rx_data;
-                            5'd4:  seq_l[7:0]   <= rx_data;
-                            5'd5:  flags_l[15:8]<= rx_data;
-                            5'd6:  flags_l[7:0] <= rx_data;
-                            5'd7:  time_l[31:24]<= rx_data;
-                            5'd8:  time_l[23:16]<= rx_data;
-                            5'd9:  time_l[15:8] <= rx_data;
-                            5'd10: time_l[7:0]  <= rx_data;
-                            5'd11: data_l[31:24]<= rx_data;
-                            5'd12: data_l[23:16]<= rx_data;
-                            5'd13: data_l[15:8] <= rx_data;
-                            5'd14: data_l[7:0]  <= rx_data;
-                            default: begin end
-                        endcase
-
-                        if (index == 5'd15) begin
-                            if (rx_data == crc) begin
-                                if (type_l == PKT_MOTOR_FB) begin
-                                    feedback_seq        <= seq_l;
-                                    feedback_flags      <= flags_l;
-                                    feedback_timestamp  <= time_l;
-                                    feedback_rpm        <= data_l[31:16];
-                                    feedback_current_ma <= data_l[15:0];
-                                    feedback_valid      <= 1'b1;
-                                end
-                            end else begin
-                                crc_error <= 1'b1;
-                            end
-                            state <= R_SYNC_A5;
-                            index <= 5'd0;
-                        end else begin
-                            index <= index + 1'b1;
-                        end
-                    end
-
-                    default: state <= R_SYNC_A5;
-                endcase
-            end
-        end
-    end
-endmodule

+ 0 - 20
fpga/rtl/reset_sync.v

@@ -1,20 +0,0 @@
-`timescale 1ns/1ps
-
-// Asynchronous assert, synchronous deassert reset conditioner.
-// External KEY_1 is active-low.
-module reset_sync(
-    input  wire clk,
-    input  wire reset_n_ext,
-    output wire reset_n
-);
-    reg [1:0] sync_ff;
-
-    always @(posedge clk or negedge reset_n_ext) begin
-        if (!reset_n_ext)
-            sync_ff <= 2'b00;
-        else
-            sync_ff <= {sync_ff[0], 1'b1};
-    end
-
-    assign reset_n = sync_ff[1];
-endmodule

+ 0 - 39
fpga/rtl/rtl.al

@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Project>
-    <Project_Created_Time>2026-08-19 14:45:50</Project_Created_Time>
-    <TD_Version>4.6.96021</TD_Version>
-    <UCode>11001001</UCode>
-    <Name>rtl</Name>
-    <HardWare>
-        <Family>EF2</Family>
-        <Device>EF2L45BG256B</Device>
-    </HardWare>
-    <Source_Files>
-        <Header>
-            <File>trace_build_id.vh</File>
-        </Header>
-        <Verilog>
-            <File>motor_controller_hil.v</File>
-            <File>motor_feedback_rx.v</File>
-            <File>reset_sync.v</File>
-            <File>top_ef2l45_motor_hil_trace.v</File>
-            <File>trace_capture_ram.v</File>
-            <File>trace_packet_tx.v</File>
-            <File>uart_rx.v</File>
-            <File>uart_tx.v</File>
-        </Verilog>
-        <ADC_FILE>../constraints/ef2l45_motor_hil_trace.adc</ADC_FILE>
-        <SDC_FILE>../constraints/ef2l45_motor_hil_trace.sdc</SDC_FILE>
-        <CWC_FILE/>
-    </Source_Files>
-    <TOP_MODULE>
-        <LABEL/>
-        <MODULE>top_ef2l45_motor_hil_trace</MODULE>
-        <CREATEINDEX>auto</CREATEINDEX>
-    </TOP_MODULE>
-    <Project_Settings>
-        <Step_Last_Change>2026-09-02 18:01:14</Step_Last_Change>
-        <Current_Step>60</Current_Step>
-        <Step_Status>true</Step_Status>
-    </Project_Settings>
-</Project>

+ 0 - 5
fpga/rtl/test.v

@@ -1,5 +0,0 @@
-module test( );
-
-
-
-endmodule

+ 0 - 472
fpga/rtl/top_ef2l45_motor_hil_trace.v

@@ -1,472 +0,0 @@
-`timescale 1ns/1ps
-`include "trace_build_id.vh"
-
-// -----------------------------------------------------------------------------
-// Single-CP2102 motor HIL + Trace top-level.
-//
-// One UART carries BOTH:
-//   * control/HIL packets while the virtual motor is running
-//   * Trace packets after the motor test finishes
-//
-// The PC program is the virtual motor and Trace receiver at the same time.
-// -----------------------------------------------------------------------------
-module top_ef2l45_motor_hil_trace #(
-    parameter integer CLK_HZ                  = 25000000,
-    parameter integer BAUD                    = 115200,
-    parameter integer TRACE_DEPTH             = 1024,
-    parameter integer TRACE_ADDR_W            = 10,
-    parameter integer STEP_MS                 = 10,
-    parameter integer FEEDBACK_TIMEOUT_CYCLES = 50000000,
-    parameter integer LED_HOLD_CYCLES         = 6250000
-)(
-    input  wire sys_clk,
-    input  wire reset_n,
-    output wire uart_tx,
-    input  wire uart_rx,
-
-    output wire led_heartbeat_n,
-    output wire led_reset_n,
-    output wire led_activity_n,
-    output wire led_tx_n,
-    output wire led_error_n
-);
-    localparam [7:0] PKT_STATUS    = 8'h81;
-    localparam [7:0] PKT_TRACE     = 8'h82;
-    localparam [7:0] PKT_SUMMARY   = 8'h83;
-    localparam [7:0] PKT_MOTOR_CMD = 8'h90;
-    localparam [15:0] STATUS_EVENT_ID = 16'hB17D;
-
-    localparam [3:0] T_IDLE         = 4'd0;
-    localparam [3:0] T_SEND_STATUS  = 4'd1;
-    localparam [3:0] T_WAIT_STATUS  = 4'd2;
-    localparam [3:0] T_SEND_MOTOR   = 4'd3;
-    localparam [3:0] T_WAIT_MOTOR   = 4'd4;
-    localparam [3:0] T_WAIT_FB      = 4'd5;
-    localparam [3:0] T_APPLY_FB     = 4'd6;
-    localparam [3:0] T_CHECK        = 4'd7;
-    localparam [3:0] T_PREP         = 4'd8;
-    localparam [3:0] T_READ         = 4'd9;
-    localparam [3:0] T_SEND_TRACE   = 4'd10;
-    localparam [3:0] T_WAIT_TRACE   = 4'd11;
-    localparam [3:0] T_SEND_SUMMARY = 4'd12;
-    localparam [3:0] T_WAIT_SUMMARY = 4'd13;
-
-    wire rst_n;
-    wire [7:0] rx_data;
-    wire rx_valid;
-    wire rx_framing_error;
-
-    wire fb_packet_valid;
-    wire [15:0] fb_packet_seq;
-    wire [15:0] fb_packet_flags;
-    wire [31:0] fb_packet_timestamp;
-    wire [15:0] fb_packet_rpm;
-    wire [15:0] fb_packet_current_ma;
-    wire fb_crc_error;
-
-    reg motor_start;
-    reg motor_feedback_valid;
-    reg [15:0] motor_feedback_rpm;
-    reg [15:0] motor_feedback_current_ma;
-    reg motor_feedback_fault;
-    reg motor_feedback_encoder_ok;
-    reg [15:0] motor_feedback_time_ms;
-
-    wire motor_busy;
-    wire motor_done;
-    wire [6:0] motor_pwm;
-    wire motor_enable;
-    wire motor_direction;
-    wire [15:0] motor_state_id;
-    wire [7:0] motor_fault_code;
-    wire motor_trace_valid;
-    wire [15:0] motor_trace_id;
-    wire [15:0] motor_trace_timestamp;
-    wire [31:0] motor_trace_data;
-
-    reg capture_reset;
-    wire [15:0] captured_count;
-    wire [31:0] drop_count;
-    wire overflow;
-    reg rd_en;
-    reg [TRACE_ADDR_W-1:0] rd_addr;
-    wire [63:0] rd_data;
-
-    reg pkt_send;
-    reg [7:0] pkt_type;
-    reg [15:0] pkt_seq;
-    reg [15:0] pkt_event_id;
-    reg [31:0] pkt_timestamp;
-    reg [31:0] pkt_data;
-    wire pkt_busy;
-    wire pkt_done;
-
-    reg [3:0] top_state;
-    reg [15:0] hil_seq;
-    reg [31:0] sim_time_ms;
-    reg [31:0] feedback_timeout_counter;
-    reg [15:0] total_to_dump;
-    reg [15:0] dump_index;
-    reg [31:0] sim_time_latched;
-    reg [31:0] drop_count_latched;
-    reg hil_link_error;
-
-    // Latch one validated feedback packet before applying it to the controller.
-    reg [15:0] fb_seq_l;
-    reg [15:0] fb_flags_l;
-    reg [31:0] fb_time_l;
-    reg [15:0] fb_rpm_l;
-    reg [15:0] fb_current_l;
-
-    reg [31:0] heartbeat_counter;
-    reg heartbeat_phase;
-    reg [31:0] activity_hold_counter;
-    reg [31:0] tx_hold_counter;
-    reg error_latched;
-
-    wire start_cmd = rx_valid && (rx_data == 8'h53) && (top_state == T_IDLE); // 'S'
-    wire ping_cmd  = rx_valid && (rx_data == 8'h50) && (top_state == T_IDLE); // 'P'
-
-    wire transaction_busy = (top_state != T_IDLE) &&
-                            (top_state != T_SEND_STATUS) &&
-                            (top_state != T_WAIT_STATUS);
-
-    reset_sync u_reset_sync (
-        .clk(sys_clk),
-        .reset_n_ext(reset_n),
-        .reset_n(rst_n)
-    );
-
-    uart_rx #(
-        .CLK_HZ(CLK_HZ),
-        .BAUD(BAUD)
-    ) u_uart_rx (
-        .clk(sys_clk),
-        .reset_n(rst_n),
-        .rx(uart_rx),
-        .data(rx_data),
-        .valid(rx_valid),
-        .framing_error(rx_framing_error)
-    );
-
-    motor_feedback_rx u_feedback_rx (
-        .clk(sys_clk),
-        .reset_n(rst_n),
-        .rx_data(rx_data),
-        .rx_valid(rx_valid),
-        .feedback_valid(fb_packet_valid),
-        .feedback_seq(fb_packet_seq),
-        .feedback_flags(fb_packet_flags),
-        .feedback_timestamp(fb_packet_timestamp),
-        .feedback_rpm(fb_packet_rpm),
-        .feedback_current_ma(fb_packet_current_ma),
-        .crc_error(fb_crc_error)
-    );
-
-    motor_controller_hil u_motor_controller (
-        .clk(sys_clk),
-        .reset_n(rst_n),
-        .start(motor_start),
-        .feedback_valid(motor_feedback_valid),
-        .feedback_rpm(motor_feedback_rpm),
-        .feedback_current_ma(motor_feedback_current_ma),
-        .feedback_fault(motor_feedback_fault),
-        .feedback_encoder_ok(motor_feedback_encoder_ok),
-        .feedback_time_ms(motor_feedback_time_ms),
-        .busy(motor_busy),
-        .done(motor_done),
-        .pwm_duty(motor_pwm),
-        .motor_enable(motor_enable),
-        .direction(motor_direction),
-        .state_id(motor_state_id),
-        .fault_code(motor_fault_code),
-        .trace_valid(motor_trace_valid),
-        .trace_id(motor_trace_id),
-        .trace_timestamp(motor_trace_timestamp),
-        .trace_data(motor_trace_data)
-    );
-
-    trace_capture_ram #(
-        .TRACE_DEPTH(TRACE_DEPTH),
-        .TRACE_ADDR_W(TRACE_ADDR_W)
-    ) u_trace_ram (
-        .clk(sys_clk),
-        .reset_n(rst_n),
-        .capture_reset(capture_reset),
-        .trace_valid(motor_trace_valid && (top_state == T_APPLY_FB)),
-        .trace_id(motor_trace_id),
-        .trace_timestamp(motor_trace_timestamp),
-        .trace_data(motor_trace_data),
-        .captured_count(captured_count),
-        .drop_count(drop_count),
-        .overflow(overflow),
-        .rd_en(rd_en),
-        .rd_addr(rd_addr),
-        .rd_data(rd_data)
-    );
-
-    trace_packet_tx #(
-        .CLK_HZ(CLK_HZ),
-        .BAUD(BAUD)
-    ) u_packet_tx (
-        .clk(sys_clk),
-        .reset_n(rst_n),
-        .send(pkt_send),
-        .pkt_type(pkt_type),
-        .seq(pkt_seq),
-        .event_id(pkt_event_id),
-        .timestamp(pkt_timestamp),
-        .data(pkt_data),
-        .uart_tx_o(uart_tx),
-        .busy(pkt_busy),
-        .done(pkt_done)
-    );
-
-    assign led_heartbeat_n = heartbeat_phase ? 1'b0 : 1'b1;
-    assign led_reset_n     = reset_n;
-    assign led_activity_n  = (activity_hold_counter != 0) ? 1'b0 : 1'b1;
-    assign led_tx_n        = (tx_hold_counter != 0) ? 1'b0 : 1'b1;
-    assign led_error_n     = error_latched ? 1'b0 : 1'b1;
-
-    always @(posedge sys_clk or negedge rst_n) begin
-        if (!rst_n) begin
-            heartbeat_counter     <= 32'd0;
-            heartbeat_phase       <= 1'b0;
-            activity_hold_counter <= 32'd0;
-            tx_hold_counter       <= 32'd0;
-            error_latched         <= 1'b0;
-        end else begin
-            if (heartbeat_counter >= (CLK_HZ / 2) - 1) begin
-                heartbeat_counter <= 32'd0;
-                heartbeat_phase   <= ~heartbeat_phase;
-            end else begin
-                heartbeat_counter <= heartbeat_counter + 1'b1;
-            end
-
-            if (start_cmd || transaction_busy)
-                activity_hold_counter <= LED_HOLD_CYCLES;
-            else if (activity_hold_counter != 0)
-                activity_hold_counter <= activity_hold_counter - 1'b1;
-
-            if (pkt_busy || pkt_send)
-                tx_hold_counter <= LED_HOLD_CYCLES;
-            else if (tx_hold_counter != 0)
-                tx_hold_counter <= tx_hold_counter - 1'b1;
-
-            if (start_cmd)
-                error_latched <= 1'b0;
-            if (rx_framing_error || fb_crc_error || overflow || hil_link_error || (motor_fault_code != 0))
-                error_latched <= 1'b1;
-        end
-    end
-
-    // Combinational control for one-cycle pulses and the shared packet transmitter.
-    always @(*) begin
-        motor_start               = 1'b0;
-        motor_feedback_valid      = 1'b0;
-        motor_feedback_rpm        = fb_rpm_l;
-        motor_feedback_current_ma = fb_current_l;
-        motor_feedback_fault      = fb_flags_l[0];
-        motor_feedback_encoder_ok = fb_flags_l[1];
-        motor_feedback_time_ms    = fb_time_l[15:0];
-
-        capture_reset = 1'b0;
-        rd_en         = 1'b0;
-        rd_addr       = dump_index[TRACE_ADDR_W-1:0];
-
-        pkt_send      = 1'b0;
-        pkt_type      = PKT_TRACE;
-        pkt_seq       = dump_index;
-        pkt_event_id  = rd_data[47:32];
-        pkt_timestamp = {16'd0, rd_data[63:48]};
-        pkt_data      = rd_data[31:0];
-
-        case (top_state)
-            T_IDLE: begin
-                if (start_cmd) begin
-                    capture_reset = 1'b1;
-                    motor_start   = 1'b1;
-                end
-            end
-
-            T_SEND_STATUS: begin
-                pkt_type      = PKT_STATUS;
-                pkt_seq       = 16'd0;
-                pkt_event_id  = STATUS_EVENT_ID;
-                pkt_timestamp = 32'd0;
-                pkt_data      = `TRACE_BUILD_ID;
-                if (!pkt_busy)
-                    pkt_send = 1'b1;
-            end
-
-            T_SEND_MOTOR: begin
-                pkt_type      = PKT_MOTOR_CMD;
-                pkt_seq       = hil_seq;
-                pkt_event_id  = motor_state_id;
-                pkt_timestamp = sim_time_ms;
-                pkt_data      = {1'b0, motor_pwm, motor_enable, motor_direction, 22'd0};
-                if (!pkt_busy)
-                    pkt_send = 1'b1;
-            end
-
-            T_APPLY_FB: begin
-                motor_feedback_valid = 1'b1;
-            end
-
-            T_READ: begin
-                rd_en   = 1'b1;
-                rd_addr = dump_index[TRACE_ADDR_W-1:0];
-            end
-
-            T_SEND_TRACE: begin
-                if (!pkt_busy)
-                    pkt_send = 1'b1;
-            end
-
-            T_SEND_SUMMARY: begin
-                pkt_type      = PKT_SUMMARY;
-                pkt_seq       = total_to_dump;
-                pkt_event_id  = hil_link_error ? 16'hFFFE : 16'hFFFF;
-                pkt_timestamp = sim_time_latched;
-                pkt_data      = drop_count_latched;
-                if (!pkt_busy)
-                    pkt_send = 1'b1;
-            end
-
-            default: begin end
-        endcase
-    end
-
-    always @(posedge sys_clk or negedge rst_n) begin
-        if (!rst_n) begin
-            top_state                <= T_IDLE;
-            hil_seq                  <= 16'd0;
-            sim_time_ms              <= 32'd0;
-            feedback_timeout_counter <= 32'd0;
-            total_to_dump            <= 16'd0;
-            dump_index               <= 16'd0;
-            sim_time_latched         <= 32'd0;
-            drop_count_latched       <= 32'd0;
-            hil_link_error           <= 1'b0;
-            fb_seq_l                 <= 16'd0;
-            fb_flags_l               <= 16'd0;
-            fb_time_l                <= 32'd0;
-            fb_rpm_l                 <= 16'd0;
-            fb_current_l             <= 16'd0;
-        end else begin
-            case (top_state)
-                T_IDLE: begin
-                    hil_seq                  <= 16'd0;
-                    sim_time_ms              <= 32'd0;
-                    feedback_timeout_counter <= 32'd0;
-                    dump_index               <= 16'd0;
-                    if (start_cmd) begin
-                        hil_link_error <= 1'b0;
-                        top_state      <= T_SEND_MOTOR;
-                    end else if (ping_cmd) begin
-                        top_state <= T_SEND_STATUS;
-                    end
-                end
-
-                T_SEND_STATUS: begin
-                    if (!pkt_busy)
-                        top_state <= T_WAIT_STATUS;
-                end
-
-                T_WAIT_STATUS: begin
-                    if (pkt_done)
-                        top_state <= T_IDLE;
-                end
-
-                T_SEND_MOTOR: begin
-                    if (!pkt_busy)
-                        top_state <= T_WAIT_MOTOR;
-                end
-
-                T_WAIT_MOTOR: begin
-                    if (pkt_done) begin
-                        feedback_timeout_counter <= 32'd0;
-                        top_state <= T_WAIT_FB;
-                    end
-                end
-
-                T_WAIT_FB: begin
-                    if (fb_packet_valid) begin
-                        if (fb_packet_seq == hil_seq) begin
-                            fb_seq_l     <= fb_packet_seq;
-                            fb_flags_l   <= fb_packet_flags;
-                            fb_time_l    <= fb_packet_timestamp;
-                            fb_rpm_l     <= fb_packet_rpm;
-                            fb_current_l <= fb_packet_current_ma;
-                            top_state    <= T_APPLY_FB;
-                        end else begin
-                            hil_link_error <= 1'b1;
-                            top_state      <= T_PREP;
-                        end
-                    end else if (feedback_timeout_counter >= FEEDBACK_TIMEOUT_CYCLES - 1) begin
-                        hil_link_error <= 1'b1;
-                        top_state      <= T_PREP;
-                    end else begin
-                        feedback_timeout_counter <= feedback_timeout_counter + 1'b1;
-                    end
-                end
-
-                T_APPLY_FB: begin
-                    top_state <= T_CHECK;
-                end
-
-                T_CHECK: begin
-                    hil_seq     <= hil_seq + 1'b1;
-                    sim_time_ms <= sim_time_ms + STEP_MS;
-                    if (motor_done)
-                        top_state <= T_PREP;
-                    else
-                        top_state <= T_SEND_MOTOR;
-                end
-
-                T_PREP: begin
-                    total_to_dump      <= captured_count;
-                    sim_time_latched   <= sim_time_ms;
-                    drop_count_latched <= drop_count;
-                    dump_index         <= 16'd0;
-                    if (captured_count == 0)
-                        top_state <= T_SEND_SUMMARY;
-                    else
-                        top_state <= T_READ;
-                end
-
-                T_READ: begin
-                    top_state <= T_SEND_TRACE;
-                end
-
-                T_SEND_TRACE: begin
-                    if (!pkt_busy)
-                        top_state <= T_WAIT_TRACE;
-                end
-
-                T_WAIT_TRACE: begin
-                    if (pkt_done) begin
-                        if ((dump_index + 1'b1) >= total_to_dump) begin
-                            top_state <= T_SEND_SUMMARY;
-                        end else begin
-                            dump_index <= dump_index + 1'b1;
-                            top_state  <= T_READ;
-                        end
-                    end
-                end
-
-                T_SEND_SUMMARY: begin
-                    if (!pkt_busy)
-                        top_state <= T_WAIT_SUMMARY;
-                end
-
-                T_WAIT_SUMMARY: begin
-                    if (pkt_done)
-                        top_state <= T_IDLE;
-                end
-
-                default: top_state <= T_IDLE;
-            endcase
-        end
-    end
-endmodule

+ 0 - 2
fpga/rtl/trace_build_id.vh

@@ -1,2 +0,0 @@
-// AUTO-GENERATED. Run prepare_build.bat before HDL2Bit.
-`define TRACE_BUILD_ID 32'h3949C09B

+ 0 - 65
fpga/rtl/trace_capture_ram.v

@@ -1,65 +0,0 @@
-`timescale 1ns/1ps
-
-// Capture-and-dump trace memory.
-//
-// V2.2 internal record width is deliberately reduced to 64 bits:
-//   timestamp[15:0] + event/state ID[15:0] + data[31:0]
-//
-// The UART protocol still sends a 32-bit timestamp; the top expands the stored
-// 16-bit timestamp with zeroes. With the default TRACE_DEPTH=1024 and the
-// full-cycle template (one trace per active algorithm cycle), 16 timestamp bits
-// are far more than required and save block-RAM resources versus the old 80-bit
-// record.
-//
-// Memory is NOT cleared between runs. Only count/pointers are reset.
-module trace_capture_ram #(
-    parameter integer TRACE_DEPTH  = 1024,
-    parameter integer TRACE_ADDR_W = 10
-)(
-    input  wire                    clk,
-    input  wire                    reset_n,
-    input  wire                    capture_reset,
-    input  wire                    trace_valid,
-    input  wire [15:0]             trace_id,
-    input  wire [15:0]             trace_timestamp,
-    input  wire [31:0]             trace_data,
-    output reg  [15:0]             captured_count,
-    output reg  [31:0]             drop_count,
-    output reg                     overflow,
-
-    input  wire                    rd_en,
-    input  wire [TRACE_ADDR_W-1:0] rd_addr,
-    output reg  [63:0]             rd_data
-);
-    reg [63:0] mem [0:TRACE_DEPTH-1];
-    reg [TRACE_ADDR_W-1:0] wr_addr;
-
-    always @(posedge clk or negedge reset_n) begin
-        if (!reset_n) begin
-            wr_addr        <= {TRACE_ADDR_W{1'b0}};
-            captured_count <= 16'd0;
-            drop_count     <= 32'd0;
-            overflow       <= 1'b0;
-        end else if (capture_reset) begin
-            wr_addr        <= {TRACE_ADDR_W{1'b0}};
-            captured_count <= 16'd0;
-            drop_count     <= 32'd0;
-            overflow       <= 1'b0;
-        end else if (trace_valid) begin
-            if (captured_count < TRACE_DEPTH) begin
-                mem[wr_addr]    <= {trace_timestamp, trace_id, trace_data};
-                wr_addr         <= wr_addr + 1'b1;
-                captured_count  <= captured_count + 1'b1;
-            end else begin
-                drop_count <= drop_count + 1'b1;
-                overflow   <= 1'b1;
-            end
-        end
-    end
-
-    // Synchronous read: friendlier to true block-RAM inference.
-    always @(posedge clk) begin
-        if (rd_en)
-            rd_data <= mem[rd_addr];
-    end
-endmodule

+ 0 - 151
fpga/rtl/trace_packet_tx.v

@@ -1,151 +0,0 @@
-`timescale 1ns/1ps
-
-// 16-byte binary frame over UART.
-// Byte 0      : 0xA5
-// Byte 1      : 0x5A
-// Byte 2      : type (0x81 STATUS/PONG, 0x82 TRACE, 0x83 SUMMARY)
-// Byte 3..4   : sequence/count, big-endian
-// Byte 5..6   : event_id, big-endian
-// Byte 7..10  : timestamp/run_cycles, big-endian
-// Byte 11..14 : data/drop_count, big-endian
-// Byte 15     : CRC-8, polynomial 0x07, init 0x00, over bytes 2..14
-module trace_packet_tx #(
-    parameter integer CLK_HZ = 25000000,
-    parameter integer BAUD   = 115200
-)(
-    input  wire        clk,
-    input  wire        reset_n,
-    input  wire        send,
-    input  wire [7:0]  pkt_type,
-    input  wire [15:0] seq,
-    input  wire [15:0] event_id,
-    input  wire [31:0] timestamp,
-    input  wire [31:0] data,
-    output wire        uart_tx_o,
-    output reg         busy,
-    output reg         done
-);
-    reg [7:0]  type_l;
-    reg [15:0] seq_l;
-    reg [15:0] event_l;
-    reg [31:0] time_l;
-    reg [31:0] data_l;
-
-    reg [4:0] byte_index;
-    reg [7:0] crc;
-    reg       uart_start;
-    reg [7:0] uart_data;
-    reg       waiting_uart;
-
-    wire uart_busy;
-    wire uart_done;
-
-    function [7:0] crc8_update;
-        input [7:0] crc_in;
-        input [7:0] data_in;
-        integer i;
-        reg [7:0] c;
-        begin
-            c = crc_in ^ data_in;
-            for (i = 0; i < 8; i = i + 1) begin
-                if (c[7])
-                    c = (c << 1) ^ 8'h07;
-                else
-                    c = (c << 1);
-            end
-            crc8_update = c;
-        end
-    endfunction
-
-    function [7:0] frame_byte;
-        input [4:0] idx;
-        begin
-            case (idx)
-                5'd0:  frame_byte = 8'hA5;
-                5'd1:  frame_byte = 8'h5A;
-                5'd2:  frame_byte = type_l;
-                5'd3:  frame_byte = seq_l[15:8];
-                5'd4:  frame_byte = seq_l[7:0];
-                5'd5:  frame_byte = event_l[15:8];
-                5'd6:  frame_byte = event_l[7:0];
-                5'd7:  frame_byte = time_l[31:24];
-                5'd8:  frame_byte = time_l[23:16];
-                5'd9:  frame_byte = time_l[15:8];
-                5'd10: frame_byte = time_l[7:0];
-                5'd11: frame_byte = data_l[31:24];
-                5'd12: frame_byte = data_l[23:16];
-                5'd13: frame_byte = data_l[15:8];
-                5'd14: frame_byte = data_l[7:0];
-                5'd15: frame_byte = crc;
-                default: frame_byte = 8'h00;
-            endcase
-        end
-    endfunction
-
-    uart_tx #(
-        .CLK_HZ(CLK_HZ),
-        .BAUD(BAUD)
-    ) u_uart_tx (
-        .clk(clk),
-        .reset_n(reset_n),
-        .start(uart_start),
-        .data(uart_data),
-        .tx(uart_tx_o),
-        .busy(uart_busy),
-        .done(uart_done)
-    );
-
-    always @(posedge clk or negedge reset_n) begin
-        if (!reset_n) begin
-            type_l       <= 8'd0;
-            seq_l        <= 16'd0;
-            event_l      <= 16'd0;
-            time_l       <= 32'd0;
-            data_l       <= 32'd0;
-            byte_index   <= 5'd0;
-            crc          <= 8'd0;
-            uart_start   <= 1'b0;
-            uart_data    <= 8'd0;
-            waiting_uart <= 1'b0;
-            busy         <= 1'b0;
-            done         <= 1'b0;
-        end else begin
-            uart_start <= 1'b0;
-            done       <= 1'b0;
-
-            if (!busy) begin
-                waiting_uart <= 1'b0;
-                if (send) begin
-                    type_l     <= pkt_type;
-                    seq_l      <= seq;
-                    event_l    <= event_id;
-                    time_l     <= timestamp;
-                    data_l     <= data;
-                    byte_index <= 5'd0;
-                    crc        <= 8'h00;
-                    busy       <= 1'b1;
-                end
-            end else begin
-                if (!waiting_uart && !uart_busy) begin
-                    uart_data    <= frame_byte(byte_index);
-                    uart_start   <= 1'b1;
-                    waiting_uart <= 1'b1;
-                end
-
-                if (waiting_uart && uart_done) begin
-                    if ((byte_index >= 5'd2) && (byte_index <= 5'd14))
-                        crc <= crc8_update(crc, frame_byte(byte_index));
-
-                    waiting_uart <= 1'b0;
-
-                    if (byte_index == 5'd15) begin
-                        busy <= 1'b0;
-                        done <= 1'b1;
-                    end else begin
-                        byte_index <= byte_index + 1'b1;
-                    end
-                end
-            end
-        end
-    end
-endmodule

+ 0 - 109
fpga/rtl/uart_rx.v

@@ -1,109 +0,0 @@
-`timescale 1ns/1ps
-
-// 8-N-1 UART receiver.
-// RX input is first synchronized into clk domain.
-module uart_rx #(
-    parameter integer CLK_HZ = 25000000,
-    parameter integer BAUD   = 115200
-)(
-    input  wire       clk,
-    input  wire       reset_n,
-    input  wire       rx,
-    output reg  [7:0] data,
-    output reg        valid,
-    output reg        framing_error
-);
-    localparam integer CLKS_PER_BIT = CLK_HZ / BAUD;
-    localparam integer HALF_BIT     = CLKS_PER_BIT / 2;
-
-    localparam [1:0] R_IDLE  = 2'd0;
-    localparam [1:0] R_START = 2'd1;
-    localparam [1:0] R_DATA  = 2'd2;
-    localparam [1:0] R_STOP  = 2'd3;
-
-    reg rx_meta;
-    reg rx_sync;
-    reg [1:0]  state;
-    reg [15:0] clk_count;
-    reg [2:0]  bit_index;
-    reg [7:0]  data_shift;
-
-    always @(posedge clk or negedge reset_n) begin
-        if (!reset_n) begin
-            rx_meta <= 1'b1;
-            rx_sync <= 1'b1;
-        end else begin
-            rx_meta <= rx;
-            rx_sync <= rx_meta;
-        end
-    end
-
-    always @(posedge clk or negedge reset_n) begin
-        if (!reset_n) begin
-            state         <= R_IDLE;
-            clk_count     <= 16'd0;
-            bit_index     <= 3'd0;
-            data_shift    <= 8'd0;
-            data          <= 8'd0;
-            valid         <= 1'b0;
-            framing_error <= 1'b0;
-        end else begin
-            valid         <= 1'b0;
-            framing_error <= 1'b0;
-
-            case (state)
-                R_IDLE: begin
-                    clk_count <= 16'd0;
-                    bit_index <= 3'd0;
-                    if (!rx_sync)
-                        state <= R_START;
-                end
-
-                R_START: begin
-                    // Sample around the middle of the start bit to reject a short glitch.
-                    if (clk_count == HALF_BIT - 1) begin
-                        clk_count <= 16'd0;
-                        if (!rx_sync)
-                            state <= R_DATA;
-                        else
-                            state <= R_IDLE;
-                    end else begin
-                        clk_count <= clk_count + 1'b1;
-                    end
-                end
-
-                R_DATA: begin
-                    if (clk_count == CLKS_PER_BIT - 1) begin
-                        clk_count             <= 16'd0;
-                        data_shift[bit_index] <= rx_sync;
-                        if (bit_index == 3'd7) begin
-                            bit_index <= 3'd0;
-                            state     <= R_STOP;
-                        end else begin
-                            bit_index <= bit_index + 1'b1;
-                        end
-                    end else begin
-                        clk_count <= clk_count + 1'b1;
-                    end
-                end
-
-                R_STOP: begin
-                    if (clk_count == CLKS_PER_BIT - 1) begin
-                        clk_count <= 16'd0;
-                        if (rx_sync) begin
-                            data  <= data_shift;
-                            valid <= 1'b1;
-                        end else begin
-                            framing_error <= 1'b1;
-                        end
-                        state <= R_IDLE;
-                    end else begin
-                        clk_count <= clk_count + 1'b1;
-                    end
-                end
-
-                default: state <= R_IDLE;
-            endcase
-        end
-    end
-endmodule

+ 0 - 100
fpga/rtl/uart_tx.v

@@ -1,100 +0,0 @@
-`timescale 1ns/1ps
-
-// 8-N-1 UART transmitter.
-module uart_tx #(
-    parameter integer CLK_HZ = 25000000,
-    parameter integer BAUD   = 115200
-)(
-    input  wire       clk,
-    input  wire       reset_n,
-    input  wire       start,
-    input  wire [7:0] data,
-    output reg        tx,
-    output reg        busy,
-    output reg        done
-);
-    localparam integer CLKS_PER_BIT = CLK_HZ / BAUD;
-
-    localparam [1:0] U_IDLE  = 2'd0;
-    localparam [1:0] U_START = 2'd1;
-    localparam [1:0] U_DATA  = 2'd2;
-    localparam [1:0] U_STOP  = 2'd3;
-
-    reg [1:0]  state;
-    reg [15:0] clk_count;
-    reg [2:0]  bit_index;
-    reg [7:0]  data_latched;
-
-    always @(posedge clk or negedge reset_n) begin
-        if (!reset_n) begin
-            state        <= U_IDLE;
-            clk_count    <= 16'd0;
-            bit_index    <= 3'd0;
-            data_latched <= 8'd0;
-            tx           <= 1'b1;
-            busy         <= 1'b0;
-            done         <= 1'b0;
-        end else begin
-            done <= 1'b0;
-
-            case (state)
-                U_IDLE: begin
-                    tx        <= 1'b1;
-                    busy      <= 1'b0;
-                    clk_count <= 16'd0;
-                    bit_index <= 3'd0;
-                    if (start) begin
-                        data_latched <= data;
-                        busy         <= 1'b1;
-                        tx           <= 1'b0;
-                        state        <= U_START;
-                    end
-                end
-
-                U_START: begin
-                    busy <= 1'b1;
-                    tx   <= 1'b0;
-                    if (clk_count == CLKS_PER_BIT - 1) begin
-                        clk_count <= 16'd0;
-                        bit_index <= 3'd0;
-                        tx        <= data_latched[0];
-                        state     <= U_DATA;
-                    end else begin
-                        clk_count <= clk_count + 1'b1;
-                    end
-                end
-
-                U_DATA: begin
-                    busy <= 1'b1;
-                    tx   <= data_latched[bit_index];
-                    if (clk_count == CLKS_PER_BIT - 1) begin
-                        clk_count <= 16'd0;
-                        if (bit_index == 3'd7) begin
-                            tx    <= 1'b1;
-                            state <= U_STOP;
-                        end else begin
-                            bit_index <= bit_index + 1'b1;
-                        end
-                    end else begin
-                        clk_count <= clk_count + 1'b1;
-                    end
-                end
-
-                U_STOP: begin
-                    busy <= 1'b1;
-                    tx   <= 1'b1;
-                    if (clk_count == CLKS_PER_BIT - 1) begin
-                        clk_count <= 16'd0;
-                        busy      <= 1'b0;
-                        done      <= 1'b1;
-                        state     <= U_IDLE;
-                    end else begin
-                        clk_count <= clk_count + 1'b1;
-                    end
-                end
-
-                default: state <= U_IDLE;
-            endcase
-        end
-    end
-endmodule

+ 73 - 0
rtl/pwm_control_demo.v

@@ -0,0 +1,73 @@
+`timescale 1ns/1ps
+
+module pwm_control_demo (
+    input  wire                    clk,
+    input  wire                    reset_n,
+
+    // Control command: -128 .. +127
+    input  wire signed [7:0]       cmd,
+
+    // Safety/control signals
+    input  wire                    enable,
+    input  wire                    fault,
+
+    // Observable internal results
+    output reg  signed [7:0]       limited_cmd,
+    output reg                     direction,
+    output reg  [6:0]              duty,
+
+    // Actual PWM waveform
+    output wire                    pwm_out
+);
+
+    // 0..99 PWM carrier counter
+    reg [6:0] pwm_counter;
+
+    // ---------------------------------------------------------
+    // 1. Saturation limiter: command limited to -90 .. +90
+    // 2. Sign -> direction
+    // 3. Absolute value -> PWM duty
+    // ---------------------------------------------------------
+    always @(*) begin
+        if (cmd > 8'sd90)
+            limited_cmd = 8'sd90;
+        else if (cmd < -8'sd90)
+            limited_cmd = -8'sd90;
+        else
+            limited_cmd = cmd;
+
+        if (limited_cmd < 0) begin
+            direction = 1'b1;          // reverse
+            duty = -limited_cmd;
+        end else begin
+            direction = 1'b0;          // forward
+            duty = limited_cmd;
+        end
+    end
+
+    // ---------------------------------------------------------
+    // PWM carrier: 0,1,2 ... 99,0,1...
+    // ---------------------------------------------------------
+    always @(posedge clk or negedge reset_n) begin
+        if (!reset_n)
+            pwm_counter <= 7'd0;
+        else if (pwm_counter == 7'd99)
+            pwm_counter <= 7'd0;
+        else
+            pwm_counter <= pwm_counter + 1'b1;
+    end
+
+    // ---------------------------------------------------------
+    // PWM output
+    //
+    // duty=25 -> 25 high clocks / 100 clocks = 25%
+    //
+    // enable=0 or fault=1 -> PWM forced OFF
+    // ---------------------------------------------------------
+    assign pwm_out =
+        reset_n &&
+        enable &&
+        !fault &&
+        (pwm_counter < duty);
+
+endmodule

+ 168 - 0
sim/pwm_control_demo_tb.v

@@ -0,0 +1,168 @@
+`timescale 1ns/1ps
+
+module pwm_control_demo_tb;
+
+    reg clk;
+    reg reset_n;
+    reg signed [7:0] cmd;
+    reg enable;
+    reg fault;
+
+    wire signed [7:0] limited_cmd;
+    wire direction;
+    wire [6:0] duty;
+    wire pwm_out;
+
+    integer errors;
+    integer high_count;
+    integer i;
+
+    pwm_control_demo dut (
+        .clk(clk),
+        .reset_n(reset_n),
+        .cmd(cmd),
+        .enable(enable),
+        .fault(fault),
+        .limited_cmd(limited_cmd),
+        .direction(direction),
+        .duty(duty),
+        .pwm_out(pwm_out)
+    );
+
+    // 100 MHz simulation clock.
+    // Frequency itself is not important for this demo;
+    // PWM duty ratio is what we verify.
+    initial clk = 0;
+    always #5 clk = ~clk;
+
+    task check_control;
+        input signed [7:0] test_cmd;
+        input signed [7:0] expected_limit;
+        input expected_direction;
+        input [6:0] expected_duty;
+        begin
+            cmd = test_cmd;
+            #1;
+
+            if (limited_cmd !== expected_limit) begin
+                $display("FAIL limiter: cmd=%0d expected=%0d actual=%0d",
+                         test_cmd, expected_limit, limited_cmd);
+                errors = errors + 1;
+            end else begin
+                $display("PASS limiter: cmd=%0d -> %0d",
+                         test_cmd, limited_cmd);
+            end
+
+            if (direction !== expected_direction) begin
+                $display("FAIL direction: cmd=%0d expected=%0d actual=%0d",
+                         test_cmd, expected_direction, direction);
+                errors = errors + 1;
+            end else begin
+                $display("PASS direction: cmd=%0d direction=%0d",
+                         test_cmd, direction);
+            end
+
+            if (duty !== expected_duty) begin
+                $display("FAIL duty: cmd=%0d expected=%0d actual=%0d",
+                         test_cmd, expected_duty, duty);
+                errors = errors + 1;
+            end else begin
+                $display("PASS duty: cmd=%0d duty=%0d%%",
+                         test_cmd, duty);
+            end
+        end
+    endtask
+
+    task check_pwm;
+        input [6:0] expected_percent;
+        begin
+            high_count = 0;
+
+            // Measure exactly one complete 100-clock PWM period.
+            for (i = 0; i < 100; i = i + 1) begin
+                @(negedge clk);
+                if (pwm_out)
+                    high_count = high_count + 1;
+            end
+
+            if (high_count !== expected_percent) begin
+                $display("FAIL PWM: expected=%0d%% measured=%0d%%",
+                         expected_percent, high_count);
+                errors = errors + 1;
+            end else begin
+                $display("PASS PWM: expected=%0d%% measured=%0d%%",
+                         expected_percent, high_count);
+            end
+        end
+    endtask
+
+    initial begin
+        errors = 0;
+        reset_n = 0;
+        cmd = 0;
+        enable = 0;
+        fault = 0;
+
+        repeat (3) @(posedge clk);
+        reset_n = 1;
+
+        $display("");
+        $display("========== PWM CONTROL TEST ==========");
+
+        // Normal forward command
+        enable = 1;
+        fault = 0;
+        check_control(25, 25, 0, 25);
+        check_pwm(25);
+
+        // Another normal command
+        check_control(50, 50, 0, 50);
+        check_pwm(50);
+
+        // Upper limiter
+        check_control(95, 90, 0, 90);
+        check_pwm(90);
+
+        // Reverse command
+        check_control(-50, -50, 1, 50);
+        check_pwm(50);
+
+        // Lower limiter
+        check_control(-120, -90, 1, 90);
+        check_pwm(90);
+
+        // Zero command
+        check_control(0, 0, 0, 0);
+        check_pwm(0);
+
+        // Disable interlock
+        cmd = 50;
+        enable = 0;
+        fault = 0;
+        #1;
+        check_pwm(0);
+        $display("PASS enable=0 forces PWM OFF");
+
+        // Fault interlock
+        enable = 1;
+        fault = 1;
+        #1;
+        check_pwm(0);
+        $display("PASS fault=1 forces PWM OFF");
+
+        $display("");
+
+        if (errors == 0) begin
+            $display("======================================");
+            $display("ALL TESTS PASSED");
+            $display("======================================");
+            $finish;
+        end else begin
+            $display("======================================");
+            $display("TEST FAILED: %0d error(s)", errors);
+            $display("======================================");
+            $fatal(1);
+        end
+    end
+
+endmodule