diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/pins.pcf b/firmware/firmware-makerpnpcontrolcore/fpga/pins.pcf index f7c58aa..f40efe8 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/pins.pcf +++ b/firmware/firmware-makerpnpcontrolcore/fpga/pins.pcf @@ -87,3 +87,7 @@ set_io ENCODER_Y[0] F5 set_io ENCODER_Z[2] E2 set_io ENCODER_Z[1] R6 set_io ENCODER_Z[0] F4 + +### WS2812 outputs +set_io RGB_PORTS M11 +set_io RGB_UP_CAM D3 diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/core_top.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/core_top.v index 8e25155..4f317b5 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/core_top.v +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/core_top.v @@ -8,6 +8,8 @@ module core_top ( output wire FPGA_ACT, // LED 2 output wire BUZZER, // Buzzer + output wire RGB_PORTS, + output wire RGB_UP_CAM, output wire [15:0] LA_IO, @@ -68,6 +70,16 @@ module core_top ( wire [31:0] io_dout; wire io_we; + wire [5:0] ws0_addr; + wire [31:0] ws0_din; + wire [31:0] ws0_dout; + wire ws0_we; + + wire [5:0] ws1_addr; + wire [31:0] ws1_din; + wire [31:0] ws1_dout; + wire ws1_we; + wire [5:0] buzzer_addr; wire [31:0] buzzer_din; wire [31:0] buzzer_dout; @@ -191,6 +203,36 @@ module core_top ( .debug(encoder_debug) ); + // ---------------------- + // WS2812 - on-board LEDs + // ---------------------- + ws2812 ws2812_0_inst ( + .sys_clk(clk_100), + .reset(reset), + + .bus_we(ws0_we), + .bus_addr(ws0_addr), + .bus_din(ws0_din), + .bus_dout(ws0_dout), + + .ws_out(RGB_PORTS) + ); + + // ---------------------- + // WS2812 - Up-camera / Head / Work LEDs + // ---------------------- + ws2812 ws2812_1_inst ( + .sys_clk(clk_100), + .reset(reset), + + .bus_we(ws1_we), + .bus_addr(ws1_addr), + .bus_din(ws1_din), + .bus_dout(ws1_dout), + + .ws_out(RGB_UP_CAM) + ); + // ---------------------- // Instantiate Central Address Decoder // ---------------------- @@ -219,6 +261,16 @@ module core_top ( .io_din(io_din), .io_dout(io_dout), + .ws0_we(ws0_we), + .ws0_addr(ws0_addr), + .ws0_din(ws0_din), + .ws0_dout(ws0_dout), + + .ws1_we(ws1_we), + .ws1_addr(ws1_addr), + .ws1_din(ws1_din), + .ws1_dout(ws1_dout), + .buzzer_we(buzzer_we), .buzzer_addr(buzzer_addr), .buzzer_din(buzzer_din), diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/ws2812.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/ws2812.v new file mode 100644 index 0000000..b3d6bec --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/io/ws2812.v @@ -0,0 +1,326 @@ +module ws2812 #( + parameter MAX_LEDS = 256 +)( + input wire sys_clk, + input wire reset, + + // ========================= + // BUS INTERFACE + // ========================= + input wire bus_we, + input wire [5:0] bus_addr, + input wire [31:0] bus_din, + output reg [31:0] bus_dout, + + // ========================= + // WS OUTPUT + // ========================= + output reg ws_out +); + + // ============================================================ + // ADDRESS MAP + // ============================================================ + localparam WS_CTRL = 6'h00; + localparam WS_TX_CONFIG = 6'h04; + + localparam WS_DATA_1 = 6'h10; + + // ============================================================ + // CONTROL REGISTERS + // ============================================================ + reg enabled; + reg [1:0] mode; + reg [7:0] num_leds; + reg [5:0] bit_count; + + // ============================================================ + // STREAMING STATE + // ============================================================ + reg [7:0] write_ptr; + + reg streaming; + reg frame_ready; + + localparam ADDR_WIDTH = 8; // up to 256 entries + localparam DATA_WIDTH = 32; + + wire [15:0] ram_rdata_lo; + wire [15:0] ram_rdata_hi; + + reg write_en_r; + + reg [7:0] wr_addr; + reg [7:0] rd_addr; + + reg [31:0] wr_data; + wire [31:0] rd_data; + + assign rd_data = {ram_rdata_hi, ram_rdata_lo}; + + SB_RAM40_4K #( + .WRITE_MODE(0), + .READ_MODE(0) + ) ram_lo ( + .RCLK(sys_clk), + .RCLKE(1'b1), + .RE(1'b1), + .RADDR({3'b000, rd_addr}), + .RDATA(ram_rdata_lo), + + .WCLK(sys_clk), + .WCLKE(1'b1), + .WE(write_en_r), + .WADDR({3'b000, wr_addr}), + .WDATA(wr_data[15:0]), + .MASK(16'h0000) + ); + + SB_RAM40_4K #( + .WRITE_MODE(0), + .READ_MODE(0) + ) ram_hi ( + .RCLK(sys_clk), + .RCLKE(1'b1), + .RE(1'b1), + .RADDR({3'b000, rd_addr}), + .RDATA(ram_rdata_hi), + + .WCLK(sys_clk), + .WCLKE(1'b1), + .WE(write_en_r), + .WADDR({3'b000, wr_addr}), + .WDATA(wr_data[31:16]), + .MASK(16'h0000) + ); + + // ============================================================ + // BUS WRITE CAPTURE (encoder-style) + // ============================================================ + reg [31:0] sync_reg; + reg [5:0] sync_addr; + reg strobe_update; + + reg strobe_r1, strobe_r2; + + always @(posedge sys_clk) begin + if (reset) begin + strobe_update <= 0; + end else begin + strobe_update <= 0; + + if (bus_we) begin + sync_addr <= bus_addr; + sync_reg <= bus_din; + strobe_update <= 1; + end + end + end + + // ============================================================ + // BUS READ + // ============================================================ + always @(*) begin + case (bus_addr) + WS_CTRL: bus_dout = {29'b0, mode, enabled}; + WS_TX_CONFIG: bus_dout = {23'b0, num_leds}; + + default: bus_dout = 32'h00000000; + endcase + end + + // ============================================================ + // COLOR PACKING FUNCTION + // ============================================================ + + localparam MODE_RGB = 2'b00; + localparam MODE_RGBW = 2'b01; + localparam MODE_GRB = 2'b10; + localparam MODE_GRBW = 2'b11; + + // ============================================================ + // STREAMING WRITE LOGIC + // ============================================================ + integer i; + + wire data_write_region = + (sync_addr >= WS_DATA_1 && sync_addr <= 6'h2C); + + always @(posedge sys_clk) begin + write_en_r <= strobe_update && data_write_region; + end + + always @(posedge sys_clk) begin + if (reset) begin + write_ptr <= 0; + num_leds <= 0; + frame_ready <= 0; + + end else begin + + if (strobe_update && sync_addr == WS_CTRL) begin + mode <= sync_reg[2:1]; + enabled <= sync_reg[0]; + frame_ready <= 0; + case (sync_reg[2:1]) + MODE_RGB, MODE_GRB: bit_count <= 23; + MODE_RGBW, MODE_GRBW: bit_count <= 31; + endcase + + $display("enabled flag: %1d, mode: 0b%02b", sync_reg[0], sync_reg[2:1]); + end + + if (strobe_update && sync_addr == WS_TX_CONFIG) begin + num_leds <= sync_reg[7:0]; + write_ptr <= 0; + frame_ready <= 0; + end + + // DATA writes (all map to same behavior) + if (strobe_update && + (sync_addr >= WS_DATA_1 && sync_addr <= 6'h2C)) begin + + frame_ready <= 0; + + if (write_ptr < MAX_LEDS) begin + wr_addr <= write_ptr; + case (mode) + MODE_RGB: wr_data <= {8'd0, sync_reg[23:16], sync_reg[15:8], sync_reg[7:0]}; + MODE_RGBW: wr_data <= {sync_reg[23:16], sync_reg[15:8], sync_reg[7:0], sync_reg[31:24]}; + MODE_GRB: wr_data <= {8'd0, sync_reg[15:8], sync_reg[23:16], sync_reg[7:0]}; + MODE_GRBW: wr_data <= {sync_reg[15:8], sync_reg[23:16], sync_reg[7:0], sync_reg[31:24]}; + endcase + + write_ptr <= write_ptr + 1; + end + end + + // End of stream detection + if ( write_ptr >= num_leds) begin + frame_ready <= 1; + write_ptr <= 0; + end + end + end + + // ============================================================ + // WS2812 TRANSMITTER FSM + // ============================================================ + reg [7:0] led_index; + reg [5:0] bit_index; + + reg [31:0] shift_reg; + + localparam T0H = 40; // ~0.4us @ 100MHz (adjust as needed) + localparam T1H = 80; // ~0.8us + localparam T_TOTAL = 120; + localparam T_RESET = 8000; // 80us (50us min) + + // BRAM is two-cycle delayed, after which rd_data is correct + // NOTE: some documentation says one-cycle, but in sim it's two-cycle... + localparam T_FETCH = 1; // [0,1] = [first cycle, second cycle] + + + reg [7:0] tcount; + + localparam PHASE_RESET = 2'd0; + localparam PHASE_FETCH = 2'd1; + localparam PHASE_PREPARE = 2'd2; + localparam PHASE_TRANSMIT = 2'd3; + + reg [1:0] phase; + + reg [31:0] phase_counter; + + reg is_last_led; + + always @(posedge sys_clk) begin + if (reset) begin + is_last_led <= 1'b0; + end else begin + // Pre-evaluate the condition for the NEXT led_index update + is_last_led <= (led_index + 1'b1 == num_leds); + end + end + + always @(posedge sys_clk) begin + if (reset) begin + ws_out <= 0; + phase_counter <= 0; + phase <= PHASE_RESET; + end else if (enabled && frame_ready) begin + + case (phase) + PHASE_RESET: begin + phase_counter <= phase_counter + 1; + if (phase_counter == T_RESET) begin + led_index <= 0; + + phase_counter <= 0; + phase <= PHASE_FETCH; + end + end + PHASE_FETCH: begin + $display("fetch"); + rd_addr <= led_index; // request read + + phase_counter <= phase_counter + 1; + if (phase_counter == T_FETCH) begin + phase_counter <= 0; + phase <= PHASE_PREPARE; + end + end + PHASE_PREPARE: begin + $display("prepare"); + shift_reg <= rd_data[31:0]; + tcount <= 0; + bit_index <= bit_count; + ws_out <= 1; + phase <= PHASE_TRANSMIT; + end + PHASE_TRANSMIT: begin + // Loaded new LED + if (bit_index == bit_count && tcount == 0) begin + $display("transmit. index: %d, shift_reg: 0x%08h", led_index, shift_reg); + end + + // Timing engine + if (tcount < T_TOTAL) begin + tcount <= tcount + 1'b1; + + if (shift_reg[bit_index]) begin + ws_out <= (tcount < T1H); + end else begin + ws_out <= (tcount < T0H); + end + end else begin + ws_out <= 1'b0; + tcount <= 0; + + // next bit + if (bit_index == 0) begin + bit_index <= bit_count; + + // next LED + if (is_last_led) begin + $display("finished leds"); + phase <= PHASE_RESET; + end else begin + $display("next led. led_index: %d", led_index); + led_index <= led_index + 1'b1; + phase <= PHASE_FETCH; + end + end else begin + bit_index <= bit_index - 1'b1; + end + end + + end + endcase + + end else begin + ws_out <= 0; + end + end + +endmodule diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/memory.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/memory.v index a50d976..2048800 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/memory.v +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/main/registers/memory.v @@ -30,7 +30,19 @@ module memory ( output wire encoder_we, output wire [5:0] encoder_addr, output wire [31:0] encoder_din, - input wire [31:0] encoder_dout + input wire [31:0] encoder_dout, + + // Bus Interface to WS2812 Module 0 + output wire ws0_we, + output wire [5:0] ws0_addr, + output wire [31:0] ws0_din, + input wire [31:0] ws0_dout, + + // Bus Interface to WS2812 Module 0 + output wire ws1_we, + output wire [5:0] ws1_addr, + output wire [31:0] ws1_din, + input wire [31:0] ws1_dout ); localparam [31:0] IDENT = 32'hFA_CE_B0_0B; @@ -42,21 +54,29 @@ module memory ( localparam [2:0] TARGET_IO = 3'd2; localparam [2:0] TARGET_BUZZER = 3'd3; localparam [2:0] TARGET_ENCODER = 3'd4; + localparam [2:0] TARGET_WS0 = 3'd5; + localparam [2:0] TARGET_WS1 = 3'd6; reg led_we_r; reg io_we_r; reg buzzer_we_r; reg encoder_we_r; + reg ws0_we_r; + reg ws1_we_r; reg [5:0] led_addr_r; reg [5:0] io_addr_r; reg [5:0] buzzer_addr_r; reg [5:0] encoder_addr_r; + reg [5:0] ws0_addr_r; + reg [5:0] ws1_addr_r; reg [31:0] led_din_r; reg [31:0] io_din_r; reg [31:0] buzzer_din_r; reg [31:0] encoder_din_r; + reg [31:0] ws0_din_r; + reg [31:0] ws1_din_r; reg req_valid_r; reg req_we_r; @@ -73,23 +93,31 @@ module memory ( assign io_we = io_we_r; assign buzzer_we = buzzer_we_r; assign encoder_we = encoder_we_r; + assign ws0_we = ws0_we_r; + assign ws1_we = ws1_we_r; assign led_addr = led_addr_r; assign io_addr = io_addr_r; assign buzzer_addr = buzzer_addr_r; assign encoder_addr = encoder_addr_r; + assign ws0_addr = ws0_addr_r; + assign ws1_addr = ws1_addr_r; assign led_din = led_din_r; assign io_din = io_din_r; assign buzzer_din = buzzer_din_r; assign encoder_din = encoder_din_r; + assign ws0_din = ws0_din_r; + assign ws1_din = ws1_din_r; wire [2:0] target_a = (addr_a[15:6] == 10'h001) ? TARGET_LED : (addr_a[15:6] == 10'h002) ? TARGET_IO : (addr_a[15:6] == 10'h003) ? TARGET_BUZZER : (addr_a[15:6] == 10'h004) ? TARGET_ENCODER : - TARGET_NONE; + (addr_a[15:6] == 10'h005) ? TARGET_WS0 : + (addr_a[15:6] == 10'h006) ? TARGET_WS1 : + TARGET_NONE; always @(posedge clk_a) begin if (reset) begin @@ -111,16 +139,22 @@ module memory ( io_we_r <= 1'b0; buzzer_we_r <= 1'b0; encoder_we_r <= 1'b0; + ws0_we_r <= 1'b0; + ws1_we_r <= 1'b0; led_addr_r <= 6'd0; io_addr_r <= 6'd0; buzzer_addr_r <= 6'd0; encoder_addr_r <= 6'd0; + ws0_addr_r <= 6'd0; + ws1_addr_r <= 6'd0; led_din_r <= 32'd0; io_din_r <= 32'd0; buzzer_din_r <= 32'd0; encoder_din_r <= 32'd0; + ws0_din_r <= 32'd0; + ws1_din_r <= 32'd0; end else begin valid_a <= 1'b0; @@ -149,6 +183,8 @@ module memory ( io_we_r <= 1'b0; buzzer_we_r <= 1'b0; encoder_we_r <= 1'b0; + ws0_we_r <= 1'b0; + ws1_we_r <= 1'b0; // Stage 1: service the previously captured request. // For peripheral reads, this drives the peripheral address. @@ -181,6 +217,18 @@ module memory ( encoder_we_r <= req_we_r; end + TARGET_WS0: begin + ws0_addr_r <= req_addr_r[5:0]; + ws0_din_r <= req_din_r; + ws0_we_r <= req_we_r; + end + + TARGET_WS1: begin + ws1_addr_r <= req_addr_r[5:0]; + ws1_din_r <= req_din_r; + ws1_we_r <= req_we_r; + end + default: begin end endcase @@ -207,6 +255,14 @@ module memory ( dout_a <= encoder_dout; end + TARGET_WS0: begin + dout_a <= ws0_dout; + end + + TARGET_WS1: begin + dout_a <= ws1_dout; + end + default: begin dout_a <= global_dout_r; end diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/int_core_top_tb.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/int_core_top_tb.v index b02fe8b..75d7486 100644 --- a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/int_core_top_tb.v +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/int_core_top_tb.v @@ -31,6 +31,12 @@ module int_core_top_tb; // reg BUZZER; + // + // WS2812 RGB(W) LED outputs + // + reg RGB_PORTS; + reg RGB_UP_CAM; + // // Buttons // @@ -62,7 +68,9 @@ module int_core_top_tb; .ENCODER_C(ENCODER_C), .ENCODER_X(ENCODER_X), .ENCODER_Y(ENCODER_Y), - .ENCODER_Z(ENCODER_Z) + .ENCODER_Z(ENCODER_Z), + .RGB_PORTS(RGB_PORTS), + .RGB_UP_CAM(RGB_UP_CAM) ); // Clock generator helper - Starts from 1, pulls low, then drives high diff --git a/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/ws2812_tb.v b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/ws2812_tb.v new file mode 100644 index 0000000..114c266 --- /dev/null +++ b/firmware/firmware-makerpnpcontrolcore/fpga/src/test/io/ws2812_tb.v @@ -0,0 +1,334 @@ +`timescale 1ns/1ps + +`include "src/test/assertions.svh" + +module ws2812_tb; + + // ============================================================ + // CLOCK / RESET + // ============================================================ + reg RESET; + reg TCXO = 0; + + always #5 TCXO = ~TCXO; // 100 MHz + + // ============================================================ + // BUS SIGNALS + // ============================================================ + reg [5:0] addr; + reg [31:0] din; + wire [31:0] dout; + reg we; + + wire ws_out; + wire [15:0] debug; + + // ============================================================ + // DUT + // ============================================================ + ws2812 dut ( + .sys_clk(TCXO), + .reset(RESET), + + .bus_we(we), + .bus_addr(addr), + .bus_din(din), + .bus_dout(dout), + + .ws_out(ws_out) + ); + + // ============================================================ + // BUS WRITE TASK + // ============================================================ + task write(input [4:0] a, input [31:0] d); + begin + addr = a; + din = d; + we = 1; + #10; + we = 0; + #10; + end + endtask + + // ============================================================ + // BUS READ TASK + // ============================================================ + task read(input [4:0] a); + begin + addr = a; + we = 0; + #10; + end + endtask + + reg [31:0] expected [0:15]; + integer bit_count; + reg bitstream [0:2047]; // enough for 85 LEDs max (2048 bits safe) + + integer decoded_leds; + reg [23:0] led_data [0:255]; + + // ============================================================ + // TEST SEQUENCE + // ============================================================ + initial begin + $dumpfile("ws2812_tb.vcd"); + $dumpvars(0, ws2812_tb); + + // -------------------------------------------------------- + // RESET + // -------------------------------------------------------- + RESET = 1; + we = 0; + addr = 0; + din = 0; + + #50; + RESET = 0; + + #50; + + // ============================================================ + // TEST 1: + // Configure MODE + ENABLE + // Expect: output enabled BUT no LED transmission yet + // ============================================================ + begin : CONFIGURE_AND_ENABLE + $display("TEST 1: CTRL enable + mode set"); + + write(5'h00, 32'b0000_0000_0000_0000_0000_0000_0000_0001); + // mode = RGB (00), enable = 1 + + #200; + + `ASSERT_EQ(ws_out, 1'b0, "%d", "WS output should remain idle before data stream"); + + $display("PASS: CTRL sets state but no LED output yet"); + end + + // ============================================================ + // TEST 2: + // Configure NUM_LEDS = 16 + // ============================================================ + begin : SET_NUM_LEDS + $display("TEST 2: NUM_LEDS = 16"); + + write(5'h04, 32'd16); + + #200; + + // still no output activity yet + `ASSERT_EQ(ws_out, 1'b0, "%d", "WS output still idle before data"); + + $display("PASS: NUM_LEDS configured, still no transmission"); + end + + // ============================================================ + // TEST 3: + // STREAM RGB DATA (16 LEDs total) + // Each LED = 0xRRGGBB + // ============================================================ + + + begin : STREAM_DATA_1 + integer idx; + + $display("TEST 3: Streaming RGB data"); + + + for (idx = 0; idx < 16; idx = idx + 1) begin + expected[idx] = {8'h00, idx[7:0], 8'h10, 8'h20}; + $display("index: %d, value: 0x%08h", idx, expected[idx]); + write(5'h10 + idx, expected[idx]); + end + + + end + + // ============================================================ + // TEST 4: + // Decode WS2812 waveform is correct + // ============================================================ + begin : WS2812_PROTOCOL_TEST + + integer i; + integer high_count; + integer low_count; + integer bit_index; + + integer led_index; + integer b; + + integer cycle_count; + integer max_cycles_per_bit; + + reg bit_value; + + bit_count = 0; + led_index = 0; + + $display("WAITING FOR WS OUTPUT ACTIVITY..."); + + fork + begin + #100000; + end + + begin + wait (ws_out == 1'b1); + end + join_any + disable fork; + + `ASSERT_EQ(ws_out, 1'b1, "%d", + "WS2812 never started"); + + $display("WS OUTPUT START DETECTED"); + + // ============================================================ + // STEP 1: CAPTURE BITS + // ============================================================ + max_cycles_per_bit = 1500; // 1.5uS at 100Mhz + while (led_index < 15) begin + + high_count = 0; + low_count = 0; + cycle_count = 0; + + while (ws_out == 1'b1 && cycle_count < max_cycles_per_bit) begin + high_count = high_count + 1; + cycle_count = cycle_count + 1; + #1; + end + + while (ws_out == 1'b0 && cycle_count < max_cycles_per_bit) begin + low_count = low_count + 1; + cycle_count = cycle_count + 1; + #1; + end + $display("bit cycle_count: %d, high_count: %d, low_count: %d", cycle_count, high_count, low_count); + + // classify bit + // TODO improve this, since it doesn't check the actual timings. + if (high_count > low_count) + bit_value = 1; + else + bit_value = 0; + + bitstream[bit_count] = bit_value; + bit_count = bit_count + 1; + + // once we have 24 bits → form LED + if (bit_count % 24 == 0) begin + + led_index = bit_count / 24 - 1; + + led_data[led_index] = { + bitstream[bit_count-24], + bitstream[bit_count-23], + bitstream[bit_count-22], + bitstream[bit_count-21], + bitstream[bit_count-20], + bitstream[bit_count-19], + bitstream[bit_count-18], + bitstream[bit_count-17], + bitstream[bit_count-16], + bitstream[bit_count-15], + bitstream[bit_count-14], + bitstream[bit_count-13], + bitstream[bit_count-12], + bitstream[bit_count-11], + bitstream[bit_count-10], + bitstream[bit_count-9], + bitstream[bit_count-8], + bitstream[bit_count-7], + bitstream[bit_count-6], + bitstream[bit_count-5], + bitstream[bit_count-4], + bitstream[bit_count-3], + bitstream[bit_count-2], + bitstream[bit_count-1] + }; + + $display("LED %0d decoded: %h", + led_index, led_data[led_index]); + end + end + + // ============================================================ + // STEP 2: VERIFY AGAINST EXPECTED MODEL + // ============================================================ + $display("COMPARING DECODED LED DATA..."); + + for (i = 0; i < 16; i = i + 1) begin + + // NOTE: adjust ordering if your pack_pixel differs + `ASSERT_EQ(led_data[i][23:0], + expected[i][23:0], + "%h", + "LED mismatch at index"); + + end + + `ASSERT_EQ(ws_out, 0, "%d", + "WS2812 should be low after last LED"); + + end + + // ============================================================ + // TEST 4: + // Ensure reset pulse is present before next frame + // ============================================================ + + begin : RESET_PULSE_CHECK + + integer low_cycles; + integer max_cycles; + reg last_ws; + + low_cycles = 0; + max_cycles = 10000; // 100us margin at 100MHz + + $display("TEST: WS2812 RESET pulse validation"); + + low_cycles = 0; + + // ------------------------------------------------------------ + // Measure low time + // ------------------------------------------------------------ + while (ws_out == 1'b0) begin + low_cycles = low_cycles + 1; + @(posedge TCXO); + + if (low_cycles > max_cycles) begin + break; + end + end + + `ASSERT_LE(low_cycles, max_cycles, "%d", + "reset pulse exceeds expected window (or stuck low)"); + + // ------------------------------------------------------------ + // Convert cycles → time check + // ------------------------------------------------------------ + $display("RESET LOW duration = %0d cycles", low_cycles); + + `ASSERT_GE(low_cycles, 50, "%d", + "WS2812 reset pulse too short (<50us @ 100MHz)"); + + $display("PASS: WS2812 reset pulse valid"); + + end + + // TODO expand to include tests for different modes (GRB, GRBW, RGBW) + + // ============================================================ + // END + // ============================================================ + report(); + $finish; + end + +endmodule diff --git a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/firmware.rs b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/firmware.rs index 4cdd958..c34626d 100644 --- a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/firmware.rs +++ b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/firmware.rs @@ -130,8 +130,10 @@ async fn init_task(lp_spawner: Spawner, hp_spawner: SendSpawner, p: Peripherals) clock_mode: false, wrap_size: WrapSize::None, // TODO increase this speed as much as possible - //clock_prescaler: 5, // 133.33Mhz / (5+1) = 22.22Mhz - clock_prescaler: 132, // 133.33Mhz / (132+1) = 9.5Mhz + clock_prescaler: 5, // 133.33Mhz / (5+1) = 22.22Mhz + // clock_prescaler: 13, // 133.33Mhz / (13+1) = 9.5Mhz + // clock_prescaler: 132, // 133.33Mhz / (132+1) = 1.0Mhz + // clock_prescaler: 254, // 133.33Mhz / (254+1) = 0.522Mhz sample_shifting: true, delay_hold_quarter_cycle: false, chip_select_boundary: 0, @@ -220,6 +222,39 @@ async fn init_task(lp_spawner: Spawner, hp_spawner: SendSpawner, p: Peripherals) } } + if true { + for _ in 0..10 { + let mut ctrl_and_tx_config = [0x0000_0000; 2]; + + // 4x WS2812 leds with GRB color order. + fpga.write_u32(0x140, 0b00000000000000000000000000000101); + fpga.write_u32(0x144, 4); + // Red, green, blue, white + fpga.write_block_u32_chunked::<16>(0x150, &[0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF]); + fpga.read_block_u32(0x140, &mut ctrl_and_tx_config); + debug!("CTRL: 0x{:08x}, TX_CONFIG: 0x{:08x}", ctrl_and_tx_config[0], ctrl_and_tx_config[1]); + + // External LED strip - 32 leds + fpga.write_u32(0x180, 0b00000000000000000000000000000101); + fpga.write_u32(0x184, 32); + fpga.write_block_u32_chunked::<16>(0x190, &[ + 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF, + 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF, + + 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF, + 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF, + + 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF, + 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF, + + 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF, + 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF, + ]); + fpga.read_block_u32(0x140, &mut ctrl_and_tx_config); + debug!("CTRL: 0x{:08x}, TX_CONFIG: 0x{:08x}", ctrl_and_tx_config[0], ctrl_and_tx_config[1]); + } + } + if false { info!("Waiting for either button to be pressed."); let initial_buttons = fpga.read_buttons(); @@ -342,13 +377,19 @@ async fn fpga_task(mut fpga: FpgaInstance) -> ! { Timer::after(Duration::from_millis(500)).await; + let mut port_rgb_leds = [0x00FF0000, 0x0000FF00, 0x000000FF, 0x00FFFFFF]; + loop { fpga.led_1_disable(); fpga.led_2_enable(); - Timer::after(Duration::from_millis(1000)).await; + Timer::after(Duration::from_millis(500)).await; fpga.led_1_enable(); fpga.led_2_disable(); - Timer::after(Duration::from_millis(1000)).await; + Timer::after(Duration::from_millis(500)).await; + + port_rgb_leds.rotate_left(1); + + fpga.write_block_u32_chunked::<16>(0x150, &port_rgb_leds); } } diff --git a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/fpga/mod.rs b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/fpga/mod.rs index d3cf6cc..00188ee 100644 --- a/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/fpga/mod.rs +++ b/firmware/firmware-makerpnpcontrolcore/stm32h7/src/bin/fpga/mod.rs @@ -179,6 +179,9 @@ impl FpgaCore { BigEndian::write_u32(out, word); } + let buffer = &chunk_buf[..byte_len]; + trace!("FPGA block write chunked ({}). address: 0x{:04x}, length: 0x{:04x} data: \n{:02x}", CHUNK_SIZE, address, buffer.len(), buffer); + let transaction = TransferConfig { instruction: Some(CMD_WRITE_16 as u32), isize: AddressSize::_8Bit, @@ -194,7 +197,7 @@ impl FpgaCore { }; self.ospi - .blocking_write(&chunk_buf[..byte_len], transaction) + .blocking_write(buffer, transaction) .unwrap(); i += chunk_words;