FPGA

Using the Colorlight 5A-75B board with ECP5 FPGA

last updated: 2025-01-07

Quick links

Intro

In the French Mag Hackable Magazine nr 35 I found in 2020 a cool article from Fabien Marteau to use cheap LED control boards named Colorlight 5A-75B with an ECP5 FPGA as development board. I have two of this boards.

I started with FPGAs using a Tang Nano 9k board. Now I'm trying to use my newly acquired knowledge to programme the Colorlight FPGAs.

Hardware

Colorlight 5A-75B with ECP5

The Colorlight board uses an FPGA from Lattice called ECP5 LFE5U-25. It has 24k LUTs and 56 sysMEM Blocks with 18 kbits. An embedded Memory of 1008 kbits and 194k distributed RAM Bits. Also 28 18x18 Multipliers and 2 PLLs. More info and data sheet: https://www.latticesemi.com/Products/FPGAandCPLD/ECP5.

The board itself is clocked with 25 MHz. The 56 outputs are buffered (12x 74HC245T) and deliver 5 V. There is only one input connected to a button of the board. The button is pulled up to 3.3 V. We have also a user LED of the board connected to P11.

The board is powered with 5 V. It also has 32 Mbits SPI flash, 2x Gigabit Ethernet PHYs and an 200MHz SDRAM (organized as 4 x 512k x 32bit). Thanks to reverse engineering and the documentation by chubby75 we are able to progress fast: https://github.com/q3k/chubby75/blob/master/5a-75b/hardware_V8.0.md.

I will use the buffered outputs to drive LEDs directly without resistor. This is nice for testing, but in a real project, this is not a good idea, because the current is quite high and can make the LED ageing faster.

We get 8 header with 16 pins. Two of them are GND, so there are 14 pins per header. The pins 8 to 15 are shared, so we get 6 pins x 8 headers + 8 shared pins = 56 outputs.

Programmer and schematic

The board has a 4 pin JTAG header to load the code to the FPGA. I use an Raspberry pico board with dirtyJtag as programmer. The code from phdussud can be found here: https://github.com/phdussud/pico-dirtyJtag.

Schematic

schematic

Toolchain

I will use my Raspberry Pi 5 with it's installed toolchain to configure the ECP5 FPGA (<(http://www.weigu.lu/other_projects/fpga/fpga_tang_nano_9k/index.html)>).

New in the Toolchain is the project Trellis, replacing Apicula used for the Tang Nano 9k. The following packages are needed, but normally already installed:

    sudo apt install python3-dev cmake libboost-python-dev libboost-filesystem-dev
    sudo apt install libboost-program-options-dev

Now we download and build the project Trellis:

    cd ~/fpga/git
    git clone --recursive https://github.com/YosysHQ/prjtrellis.git
    cd ~/fpga/git/prjtrellis/libtrellis
    cmake -DCMAKE_INSTALL_PREFIX=/usr/local .
    make
    sudo make install

We also need nextpnr-ecp5.

    sudo apt install libboost-iostream-dev
    cd ~/fpga/git/nextpnr
    cmake . -DARCH=ecp5 -DTRELLIS_INSTALL_PREFIX=/usr/local
    make -j$(nproc)
    sudo make install

I wanted to use the GUI of nextpnr. For this Qt5 is needed (sudo apt install qtcreator qtbase5-dev qt5-qmake). We have to change the cmake command and add -DBUILD_GUI=ON (cmake . -DARCH=ecp5 -DTRELLIS_INSTALL_PREFIX=/usr/local -DBUILD_GUI=ON). But I was not able to get this running with Wayland on the Raspberry Pi 5.

The toolchain consist now of:

A button and an LED

For the first test we use the only input of the 5A-75B connected the button of the board. The button is pulled up to 3.3 V. We also use the user LED of the board connected to P11.

    cd fpga
    mkdir ecp5_5a75b
    cd ecp5_5a75b
    mkdir button_2_led
    cd button_2_led
    nano button_2_led.v

The Verilog code button_2_led.v

Copy the following code into the file and save (Ctrl+s, Ctrl+x)

    module button_2_led (
        input      button_i,
        output     led_o
    );

    assign led_o = button_i;

    endmodule

The constraints file button_2_led.lpf

Now we create the constraints file with the following content:

    nano button_2_led.v
    LOCATE COMP "led_o" SITE "T6";
    IOBUF PORT "led_o" IO_TYPE=LVCMOS33;
    LOCATE COMP "button_i" SITE "R7";    

Copy the content to the file, then save and exit with Ctrl+s, Ctrl+x. In the documentation to the board we find the corresponding pins for version 8 of the board (https://github.com/q3k/chubby75/blob/master/5a-75b/hardware_V8.0.md).

Step by step

Normally we use a makefile to do the following steps. But let's look whats happening step by step. First we use Yosys for Verilog RTL synthesis.

Wikipedia: "In computer engineering, logic synthesis is a process by which an abstract specification of desired circuit behaviour, typically at register transfer level (RTL), is turned into a design implementation in terms of logic gates, typically by a computer program called a synthesis tool" .

So the behaviour is in the Verilog file. Yosys will synthesise this behaviour to a json file with the a netlist needed by .

Yosys has a command line that we access with yosys:

    cd ~/fpga/ecp5_5a75b/button_2_led
    yosys

Then we ask for the json file with the following commands:

    read_verilog button_2_led.v
    synth_ecp5 -json button_2_led.json

The output helps us to see how many resources of the FPGA are used by our code. We exit yosys with Ctrl+c. We get a new file called button_2_led.json.

Both commands can be regrouped in one line for our Makefile:

    yosys -p "synth_ecp5 -json button_2_led.json" button_2_led.v

Next we use nextpnr-ecp5 as place and route tool with the following command:

    nextpnr-ecp5 --25k --package CABGA256 --speed 6 --json button_2_led.json --textcfg button_2_led_out.config --lpf button_2_led.lpf --freq 65

The use the Gui we would need to add --gui to this command, but as stated the GUI is not working.

The output of the command can contain important information and should not be ignored. Our example does not use a clock and is asynchronous. That's ok here but in general it is a bad idea to use asynchronous timing in an FPGA.

We get the bitstream with everything needed for our project in a .config file (button_2_led_out.config). We need to pack this ASCII file to a bitstream that the FPGA recognises. This is done with the command ecppack. Then we can upload the bitstream with openFPGALoader. The cable is dirtyJTAG:

    ecppack button_2_led_out.config button_2_led.bit
    openFPGALoader -c dirtyJtag button_2_led.bit

The LED should light when pressing the button. The code resides in SRAM, so it disappears when we cut the power. To write the code to the EEPROM we need to add the FLASH parameter (-f) to the command. We get an error: "can't unlock without --unprotect-flash", so we add that parameter too:

    openFPGALoader -c dirtyJtag button_2_led.bit -f --unprotect-flash

The Makefile

Let's automatize the steps above with a Makefile.

    cd ~/fpga/ecp5_5a75b/button_2_led
    nano Makefile

Use the following code:

    # Target and top module names
    TARGET = button_2_led
    TOP = button_2_led

    # Source files
    OBJS = button_2_led.v

    # Path to Trellis
    TRELLIS = /usr/local/share/trellis

    # Default target
    all: ${TARGET}.bit

    # Synthesis step
    $(TARGET).json: $(OBJS)
            yosys -p "synth_ecp5 -top $(TOP) -json $@" $(OBJS)

    # Place and route step
    $(TARGET)_out.config: $(TARGET).json
            nextpnr-ecp5 --25k --package CABGA256 --speed 6 --json $< --textcfg $@ --lpf $>

    # Bitstream generation
    $(TARGET).bit: $(TARGET)_out.config
            ecppack --svf ${TARGET}.svf $< $@

    # SVF file generation (optional)
    ${TARGET}.svf: ${TARGET}.bit

    # Programming target
    prog: ${TARGET}.svf
            openFPGALoader -c dirtyJtag $(TARGET).bit

    # Clean target to remove generated files
    clean:
            rm -f ${TARGET}.json ${TARGET}_out.config ${TARGET}.bit ${TARGET}.svf

    .PHONY: all prog clean

Copy the content to the makefile. Pay attention to use only tabs because makefiles are sensitive :). Save and exit. Now everything is in place, and we can use make:

    make clean && make && make prog

Blinking LED

Next we use the clock of the board to blink an LED on pin 2 of HUB75 header J1.

    cd fpga/ecp5_5a75b
    mkdir blink_led
    cd blink_led
    nano blink_led.v

I asked copilot for a Verilog file. I think to get the counter width by calculating is a bit overkill and adds complexity, but it's ok. For the rest it's self explaining.

    module blink_led (
        input      clk_i,  // 25MHz clock input on P6
        output reg led_o   // LED output (P11)
    );

    // Parameters for counter
    localparam MAX_COUNT = 25_000_000 / 10;  // 0.1 second count for 25MHz clock
    localparam WIDTH = $clog2(MAX_COUNT);    // Width of the counter

    // Counter register
    reg [WIDTH-1:0] counter;

    // Always block for counter and LED control
    always @(posedge clk_i) begin
        if (counter == MAX_COUNT - 1) begin
            counter <= 0;       // Reset counter
            led_o <= ~led_o;    // Toggle LED
        end else begin
            counter <= counter + 1;  // Increment counter
        end
    end

    endmodule

The constraint file needs the input of the clock:

    LOCATE COMP "led_o" SITE "C4";    # LED on Pin1 J1
    IOBUF PORT "led_o" IO_TYPE=LVCMOS33;
    LOCATE COMP "clk_i" SITE "P6";
    IOBUF PORT "clk_i" IO_TYPE=LVCMOS33;
    FREQUENCY PORT "clk_i" 25 MHz;    

In the Makefile we change only the filenames.

Ok that's fun, so let's test the running light code from our Tang Nano board.

Running light

I have some old RGB LEDs and want to use all pins of the header J1 and the button for the reset. So I can have a running light with 14 LEDs :).

    module running_light (
        input sys_clk,          // in system clock
        input sys_rst_n,        // in reset active low
        output reg [13:0] led   // out 14 LEDS pin
    );

    localparam MAX_COUNT = 25_000_000 / 10;  // 0.1 second count for 25MHz clock

      reg [23:0] counter;

      always @(posedge sys_clk or negedge sys_rst_n)
      begin
        if (!sys_rst_n)
          counter <= 24'd0;
        else if (counter == MAX_COUNT)
          counter <= 24'd0;
        else
          counter <= counter + 1'b1;
      end

      always @(posedge sys_clk or negedge sys_rst_n)
      begin
        if (!sys_rst_n)
          led <= 14'b00000000000001;
        else if (counter == MAX_COUNT)
          led <= {led[12:0], led[13]};
      end
    endmodule

And the constraints file:

    LOCATE COMP "led[0]" SITE "C4";    # LED1  C4 pin1  J1
    LOCATE COMP "led[1]" SITE "D4";    # LED2  D4 pin2  J1
    LOCATE COMP "led[2]" SITE "E4";    # LED3  E3 pin3  J1
    LOCATE COMP "led[3]" SITE "D3";    # LED4  D3 pin5  J1
    LOCATE COMP "led[4]" SITE "F5";    # LED5  F5 pin6  J1
    LOCATE COMP "led[5]" SITE "E3";    # LED6  E3 pin7  J1
    LOCATE COMP "led[6]" SITE "N4";    # LED7  N4 pin8  J1-J8
    LOCATE COMP "led[7]" SITE "N5";    # LED8  N5 pin9  J1-J8
    LOCATE COMP "led[8]" SITE "N3";    # LED9  N3 pin10 J1-J8
    LOCATE COMP "led[9]" SITE "P3";    # LED10 P3 pin11 J1-J8
    LOCATE COMP "led[10]" SITE "P4";   # LED11 P4 pin12 J1-J8
    LOCATE COMP "led[11]" SITE "M3";   # LED12 M3 pin13 J1-J8
    LOCATE COMP "led[12]" SITE "N1";   # LED13 N1 pin14 J1-J8
    LOCATE COMP "led[13]" SITE "M4";   # LED14 M4 pin15 J1-J8
    IOBUF PORT "led" IO_TYPE=LVCMOS33;
    LOCATE COMP "sys_rst_n" SITE "R7"; # Reset input
    LOCATE COMP "sys_clk" SITE "P6";   # Clock input
    IOBUF PORT "sys_clk" IO_TYPE=LVCMOS33;
    FREQUENCY PORT "sys_clk" 25 MHz;    

Running light with reset generator

In the article mentioned, I found some code for a reset generator rst_gen.v. So why this code? Let's ask Copilot:

This code is a reset generator module. It generates a reset signal (rst_o) based on the input clock (clk_i) and an external reset signal (rst_i). The purpose of this module is to ensure that the reset signal (rst_o) is asserted for a specific number of clock cycles after the external reset signal (rst_i) is deasserted. This can be useful in ensuring that all parts of the system are properly reset before normal operation begins.

The reset generator module (rst_gen) ensures that the system starts in a known state. Here'™s why this is important:

  1. Initialization: When the FPGA or system powers up, all registers and flip-flops may have undefined values. The reset signal ensures that all parts of the system start from a known state.

  2. Synchronization: The reset signal helps synchronize the start of the system. This is crucial for ensuring that all components begin operation simultaneously and correctly.

  3. Stability: By holding the reset signal for a few clock cycles, the reset generator ensures that the system has enough time to stabilize before starting normal operation.

The reset generator ensures that the LED blinking starts correctly and predictably every time the system is reset or powered on.

Ok that's helpfull. I used also Copilot to add comments to the code and make minor changes. So here is the AI code :):

rst_gen.v
    module rst_gen (
      input     sys_clk,    // Clock input
      input     sys_rst_n,  // External reset input
      output reg    rst_gen // Generated reset output
    );

    /* try to generate a reset */
    reg [2:0]   rst_cpt;      // 3-bit counter for reset duration

    initial begin
        rst_cpt <= 3'b000;  // Initialize counter to 0
        rst_gen <= 1'b0;    // Initialize reset output to 0
    end

    always @(posedge sys_clk) begin
      if (sys_rst_n) begin
        rst_cpt <= 3'b000;  // Reset counter to 0 if external reset is active
        rst_gen <= 1'b1;    // Assert reset output
      end else begin
        if (rst_cpt == 3'b100) begin
          rst_cpt <= rst_cpt;  // Hold counter value when it reaches 4
          rst_gen <= 1'b0;     // Deassert reset output
        end else begin
          rst_cpt <= rst_cpt + 3'b001;  // Increment counter
          rst_gen <= 1'b1;              // Keep reset output asserted
        end
      end
    end

    endmodule

Now let's add this to our running light. For this we copy our files in a new directory (e.g. running_light_w_rst_gen) and add the rst_gen.v file. So now we have modules in two different Verilog files.

In the running light Verilog file we need to instantiate the reset generator module and change the reset variable:

running_light_w_rst_gen.v
    module running_light_w_rst_gen (
        input sys_clk,          // System clock input
        input sys_rst_n,        // Active low reset input
        output reg [13:0] led   // 14 LED outputs
    );

    localparam MAX_COUNT = 25_000_000 / 10;  // Adjust this value based on your clock frequency

    reg [23:0] counter;
    wire rst_gen;

    // Instantiate the reset generator
    rst_gen rst_inst (
        .sys_clk(sys_clk),
        .sys_rst_n(sys_rst_n),
        .rst_gen(rst_gen)
    );

    always @(posedge sys_clk or negedge rst_gen) begin
        if (!rst_gen)
            counter <= 24'd0;
        else if (counter == MAX_COUNT)
            counter <= 24'd0;
        else
            counter <= counter + 1'b1;
    end

    always @(posedge sys_clk or negedge rst_gen) begin
        if (!rst_gen)
            led <= 14'b00000000000001;
        else if (counter == MAX_COUNT)
            led <= {led[12:0], led[13]};
    end

    endmodule

In the make file we add the generator file:

Makefile
    # Target and top module names
    TARGET = running_light_w_rst_gen
    TOP = running_light_w_rst_gen

    # Source files
    OBJS = running_light_w_rst_gen.v rst_gen.v
    ...

RGB WS2812b driver

I have a flexible RGB matrix with 16x16 LED. The LEDs are addressable and use the WS2812b protocol. For this protocol it is important to have an accurate timing. This is easier to get with an FPGA as with a microcontroller. The code was generated with AI in the book of Jörg Rippel for the Ice40. Let's try it here.

    cd fpga/ecp5_5a75b
    mkdir rgb_matrix
    cd rgb_ws2812b_driver
    nano rgb_ws2812b_driver.v
rgb_ws2812b_driver.v
module WS2812B_Driver(
    input wire sys_clk,         // System clock at 25 MHz
    output reg data_out = 0     // Data output for WS2812B, initialised to 0
);

// Timing constants
parameter integer HIGH_1 = 90;        // 0.8µs High for "1"
parameter integer HIGH_0 = 45;        // 0.4µs High for "0"
parameter integer TOTAL_CYCLES = 125; // Total duration 1.25µs for "1" or "0"
parameter integer RESET_CYCLES = 8000; // 50µs for Reset

// Configuration parameters
parameter integer NUM_LEDS = 16*16; // Number of LEDs in the chain
parameter integer ANIMATION_SPEED = 30000000; // Speed of the animation

// Colour parameters
parameter [23:0] RED = 24'hFF0000;
parameter [23:0] GREEN = 24'h00FF00;
parameter [23:0] BLUE = 24'h0000FF;

// Colour data for LEDs
reg [23:0] color_data [0:NUM_LEDS-1]; // Array for colour data
reg [31:0] animation_counter = 0; // Counter for the animation speed

// Counting and control variables
reg [4:0] bit_index = 0; // Current bit
reg [6:0] cycle_count = 0; // Counts the clock cycles per bit
reg bit_value; // Current bit value
reg [13:0] reset_counter = 0; // Counter for reset interval
integer led_index = 0; // Current index in the LED chain
integer j; // Loop variable

always @(posedge sys_clk) begin
    if (animation_counter >= ANIMATION_SPEED) begin // 1 part state machine
        animation_counter <= 0;
        // Change LED colour of every LED
        for (j = 0; j < NUM_LEDS; j = j + 1) begin
            case (color_data[j])
                RED: color_data[j] <= GREEN; // red to green
                GREEN: color_data[j] <= BLUE; // green to blue
                BLUE: color_data[j] <= RED; // blue to red
                default: color_data[j] <= RED; // back to red
            endcase
        end
    end else begin
        animation_counter <= animation_counter + 1;
    end

    if (reset_counter >= RESET_CYCLES) begin // 2 part state machine
        // Reset interval waited, send colour data
        if (cycle_count < TOTAL_CYCLES) begin
            if (cycle_count == 0) bit_value = color_data[led_index][23 - bit_index];
            data_out <= (bit_value ? (cycle_count < HIGH_1) : (cycle_count < HIGH_0));
            cycle_count <= cycle_count + 1;
        end else begin
            cycle_count <= 0;
            bit_index <= bit_index + 1;
            if (bit_index == 23) begin
                bit_index <= 0;
                led_index <= led_index + 1;
                if (led_index == NUM_LEDS - 1) begin
                    led_index <= 0;
                    reset_counter <= 0; // Begin reset interval
                end
            end
        end
    end else begin
        // In reset interval
        data_out <= 0;
        reset_counter <= reset_counter + 1;
    end
end

endmodule

Interesting links