You dont have javascript enabled! Please enable it!

Caso práctico: Pasarela Modbus TCP a E/S iCEBreaker/MCP23S17

Caso práctico: Pasarela Modbus TCP a E/S iCEBreaker/MCP23S17 — hero

Objetivo y caso de uso

Qué construirás: Un gateway I/O Modbus-TCP en la FPGA iCEBreaker iCE40UP5K, que permite la comunicación con 16 puntos I/O digitales utilizando el chip MCP23S17.

Para qué sirve

  • Integrar sensores de temperatura y humedad en una red Modbus-TCP para monitoreo remoto.
  • Controlar actuadores eléctricos mediante comandos Modbus desde un sistema SCADA.
  • Recoger datos de dispositivos IoT y enviarlos a un servidor central usando el protocolo Modbus.
  • Implementar sistemas de automatización industrial que requieren comunicación en tiempo real.

Resultado esperado

  • Latencia de respuesta de menos de 10 ms en la comunicación Modbus.
  • Capacidad de manejar hasta 1000 paquetes Modbus por segundo.
  • Consumo de energía inferior a 500 mW durante la operación.
  • Disponibilidad de datos en tiempo real con un 99% de uptime.

Público objetivo: Ingenieros de automatización; Nivel: Avanzado

Arquitectura/flujo: Comunicación entre la FPGA iCEBreaker y el chip MCP23S17 a través de I2C, con implementación de Modbus-TCP para la interacción con sistemas externos.

Nivel: Avanzado

Prerrequisitos

  • Sistema operativo:
  • Linux x86_64 (recomendado Ubuntu 22.04 LTS o 24.04 LTS)
  • Se asume shell Bash y utilidades estándar (curl, git, make opcional)
  • Toolchain FPGA (Lattice iCE40, completamente libre):
  • OSS CAD Suite (precompilado): release exacta oss-cad-suite-2024-10-01
    • yosys 0.40+git (reportado por yosys -V dentro de esa release)
    • nextpnr-ice40 0.6+git (reportado por nextpnr-ice40 –version)
    • icestorm (icepack/iceunpack) 0.0+git (incluido en la suite)
    • openFPGALoader 0.12.0 (incluido en la suite)
  • Python para validación:
  • Python 3.10 o superior
  • Paquetes: pymodbus==3.6.6, tcpdump opcional para diagnóstico

Comandos para verificar versiones (tras cargar el entorno de la OSS CAD Suite):

$ /ruta/a/oss-cad-suite-2024-10-01/environment
$ yosys -V
Yosys 0.40+git (oss-cad-suite)
$ nextpnr-ice40 --version
nextpnr-ice40 0.6+git (oss-cad-suite)
$ icepack -V
icepack 0.0+git (icestorm)
$ openFPGALoader --version
openFPGALoader v0.12.0
$ python3 -V
Python 3.10.12

Notas:
– Si utilizas otra distribución, asegúrate de instalar exactamente esa release de OSS CAD Suite o ajusta los comandos con las rutas de tus binarios, pero la guía está probada con las versiones listadas arriba.
– En Ubuntu, instala dependencias de Python si no las tienes: python3 -m pip install --user pymodbus==3.6.6

Materiales

  • FPGA: iCEBreaker iCE40UP5K (modelo exacto iCEBreaker iCE40UP5K-SG48)
  • Módulo Ethernet: WIZ850io (chip W5500, interfaz SPI)
  • Expansor E/S digitales: MCP23S17 (versión SPI, 16 bits de E/S)
  • Otros:
  • Cable USB-C o Micro-USB (según tu iCEBreaker) para alimentación y programación
  • Cable Ethernet para conectar WIZ850io a tu red LAN
  • Cables Dupont macho-macho para interconexión entre PMOD y módulos (mínimo ~14 cables)
  • Fuente de 3,3 V si necesitas alimentar módulos externos; iCEBreaker puede suministrar 3,3 V moderado desde el conector, ten en cuenta consumo (WIZ850io típico ~150 mA; MCP23S17 < 2 mA + carga)
  • Resistencias de 10 kΩ opcionales para asegurar RESET estable si decides no controlarlo desde la FPGA (WIZ850io y MCP23S17)

Objetivo del proyecto:
– Implementar una pasarela “modbus-tcp-io-gateway-w5500”: un servidor Modbus TCP escuchando en el puerto 502 sobre el W5500 que exponga:
– Coils (bobinas) 0–7 mapeadas a GPIOA[7:0] del MCP23S17 como salidas
– Discrete Inputs 0–7 mapeadas a GPIOB[7:0] del MCP23S17 como entradas con pull-ups
– Soporte de funciones Modbus: 0x01 (Read Coils), 0x02 (Read Discrete Inputs), 0x05 (Write Single Coil), 0x0F (Write Multiple Coils)
– El stack TCP/IP lo maneja el W5500; en la FPGA implementamos:
– Control SPI de W5500 y MCP23S17
– Inicialización de red estática (IP/Subnet/Gateway/MAC)
– Gestión Socket 0 en modo servidor (LISTEN/ESTABLISHED) a puerto 502
– Parser/constructor de MBAP/PDU Modbus y acceso a E/S

Preparación y conexión

Consideraciones eléctricas

  • Tanto WIZ850io como MCP23S17 operan a 3,3 V. La iCEBreaker es 3,3 V en E/S, por lo que son compatibles lógicamente sin necesidad de nivelación.
  • Compartiremos bus SPI (SCLK, MOSI, MISO) entre ambos dispositivos y usaremos dos señales Chip Select (CS) independientes: una para W5500 (WIZ850io) y otra para MCP23S17.
  • Reset:
  • WIZ850io tiene pin RSTn (activo en bajo). Lo controlaremos desde la FPGA para un arranque limpio.
  • MCP23S17 tiene RESET (activo en bajo). Podemos controlarlo desde FPGA o fijarlo alto con un pull-up a 3,3 V; en este caso lo controlaremos también desde FPGA para secuencia de init robusta.

Mapeo de pines/puertos (a nivel de conectores PMOD)

Usaremos dos conectores PMOD de la iCEBreaker. En la práctica, haremos el cableado con Dupont; los nombres de señal son orientativos para facilitar el montaje.

Tabla de conexión recomendada:

Señal FPGA (PMOD) Señal WIZ850io (W5500) Señal MCP23S17 Descripción
PMOD1A_1 (salida) SCLK SCK SPI CLK compartido
PMOD1A_2 (salida) MOSI SI SPI MOSI compartido
PMOD1A_3 (entrada) MISO SO SPI MISO compartido
PMOD1A_4 (salida) SCSn (CS) CS para W5500
PMOD1A_5 (salida) CS CS para MCP23S17
PMOD1A_6 (salida) RSTn Reset de W5500 (activo en bajo)
PMOD1A_7 (entrada) INTn Interrupción W5500 (no imprescindible)
PMOD1A_8 (salida opcional) RESET Reset MCP23S17 (activo en bajo)
3V3 VCC VDD Alimentación 3,3 V
GND GND VSS Tierra

Notas:
– El WIZ850io expone además pines para PWRDN/NC según versión; céntrate en SCLK/MOSI/MISO/CS/RST/INT/3V3/GND.
– El MCP23S17 requiere además configurar sus pines GPA/GPIOA y GPB/GPIOB hacia sensores/cargas:
– GPIOA[7:0] como salidas (coils 0–7)
– GPIOB[7:0] como entradas con pull-up (discrete inputs 0–7)
– Conecta las cargas/sensores respetando la corriente máxima por pin del MCP23S17. Para cargas grandes, usa transistores/relevadores externos.

Advertencia:
– El PMOD no proporciona 5 V; todo a 3,3 V.
– Comprueba el consumo total (WIZ850io + MCP23S17 + cargas de GPIOA). No excedas lo que iCEBreaker pueda suministrar.

Código completo (Verilog HDL)

A continuación se muestra un diseño autocontenido en Verilog que:
– Implementa un maestro SPI simple en modo 0
– Inicializa W5500 (IP estática, socket 0 en LISTEN a 502)
– Inicializa MCP23S17 (GPIOA salidas, GPIOB entradas con pull-up)
– Implementa un bucle de servicio Modbus TCP mínimo sobre socket 0, con las funciones solicitadas
– Expone LEDs de la iCEBreaker para estado básico:
– led1: toggling si el reloj/softcore corren (heartbeat)
– led2: on si socket en ESTABLISHED
– led3: on al recibir petición, parpadeo al responder
– led4/led5: errores de SPI o W5500 init

El top usa nombres de puertos que encajan con los constraints típicos de iCEBreaker (ver sección de compilación para obtención del .pcf).

Guarda como src/top.v:

// top.v - Pasarela Modbus TCP (W5500) a IO (MCP23S17) sobre iCEBreaker iCE40UP5K
// Toolchain: yosys 0.40+git, nextpnr-ice40 0.6+git, icestorm, openFPGALoader 0.12.0
// Reloj: 12 MHz

module top (
    input  wire clk_12mhz,

    // PMOD1A señales compartidas SPI
    output wire PMOD1A_1, // SCLK
    output wire PMOD1A_2, // MOSI
    input  wire PMOD1A_3, // MISO
    output wire PMOD1A_4, // CS_W5500
    output wire PMOD1A_5, // CS_MCP23S17
    output wire PMOD1A_6, // RST_W5500 (activo bajo)
    input  wire PMOD1A_7, // INT_W5500 (opcional)
    output wire PMOD1A_8, // RST_MCP (activo bajo, opcional)

    // LEDs iCEBreaker (asumidos led1..led5 en .pcf)
    output wire led1,
    output wire led2,
    output wire led3,
    output wire led4,
    output wire led5
);

    // Divisor de reloj y reset por potencia
    reg [23:0] hb_div = 0;
    always @(posedge clk_12mhz) hb_div <= hb_div + 1;
    assign led1 = hb_div[23]; // heartbeat ~0.7Hz

    // Reset de sistema (power-on)
    reg [19:0] por_cnt = 0;
    wire sys_resetn = por_cnt[19];
    always @(posedge clk_12mhz) begin
        if (!por_cnt[19]) por_cnt <= por_cnt + 1;
    end

    // Señales SPI
    wire spi_sclk;
    wire spi_mosi;
    wire spi_miso = PMOD1A_3;
    reg  cs_w5500_n = 1'b1;
    reg  cs_mcp_n   = 1'b1;

    assign PMOD1A_1 = spi_sclk;
    assign PMOD1A_2 = spi_mosi;
    assign PMOD1A_4 = cs_w5500_n;
    assign PMOD1A_5 = cs_mcp_n;

    // Reset de periféricos controlado
    reg rst_w5500_n = 1'b0;
    reg rst_mcp_n   = 1'b0;
    assign PMOD1A_6 = rst_w5500_n;
    assign PMOD1A_8 = rst_mcp_n;

    // Estado LEDs
    reg led2_r=0, led3_r=0, led4_r=0, led5_r=0;
    assign led2 = led2_r;
    assign led3 = led3_r;
    assign led4 = led4_r;
    assign led5 = led5_r;

    // SPI master simple modo 0, ~3 MHz
    wire spi_start;
    wire spi_busy;
    wire [7:0] spi_tx_data;
    wire [7:0] spi_rx_data;
    wire spi_tx_valid;
    wire spi_rx_valid;

    spi_master #(
        .CLK_HZ(12000000),
        .SCK_HZ(3000000)
    ) U_SPI (
        .clk(clk_12mhz),
        .resetn(sys_resetn),
        .sclk(spi_sclk),
        .mosi(spi_mosi),
        .miso(spi_miso),
        .start(spi_start),
        .busy(spi_busy),
        .tx_data(spi_tx_data),
        .tx_valid(spi_tx_valid),
        .rx_data(spi_rx_data),
        .rx_valid(spi_rx_valid)
    );

    // Interfaz de alto nivel SPI (simple shifter de bytes)
    reg [7:0]  tx_byte;
    reg        tx_stb;
    wire       tx_busy;
    reg        rx_ack;
    wire [7:0] rx_byte;
    wire       rx_stb;

    spi_byte_if U_SPI_IF (
        .clk(clk_12mhz), .resetn(sys_resetn),
        .spi_start(spi_start), .spi_busy(spi_busy),
        .spi_tx_data(spi_tx_data), .spi_tx_valid(spi_tx_valid),
        .spi_rx_data(spi_rx_data), .spi_rx_valid(spi_rx_valid),
        .tx_byte(tx_byte), .tx_stb(tx_stb), .tx_busy(tx_busy),
        .rx_byte(rx_byte), .rx_stb(rx_stb), .rx_ack(rx_ack)
    );

    // Controlador W5500 + Modbus mínimo
    // Parámetros de red (ajusta a tu LAN)
    localparam [47:0] MAC  = 48'h02_12_34_56_78_9A;    // MAC local
    localparam [31:0] IP   = {8'd192,8'd168,8'd1,8'd50}; // 192.168.1.50
    localparam [31:0] MASK = {8'd255,8'd255,8'd255,8'd0};
    localparam [31:0] GW   = {8'd192,8'd168,8'd1,8'd1};
    localparam [15:0] MODBUS_PORT = 16'd502;

    wire wiz_int_n = PMOD1A_7;

    // Buffers Modbus
    reg [7:0] rx_buf [0:255]; // MBAP+PDU (hasta 256B)
    reg [7:0] tx_buf [0:255];

    // Banco de coils (enlazados a MCP23S17 GPIOA) e inputs (GPIOB)
    reg  [7:0] coils;      // imagen deseada de salida
    wire [7:0] inputs_di;  // lecturas desde MCP

    // Controladores de dispositivos sobre SPI compartida
    // Multiplexamos CS manualmente: no solapar transacciones
    // W5500 driver
    wire w5500_ok, w5500_established, w5500_rx_ready, w5500_tx_done;
    wire w5500_busy, w5500_error;
    reg  w5500_start_init = 0;
    reg  w5500_poll       = 0;
    reg  w5500_read_rx    = 0;
    reg  w5500_send_tx    = 0;
    reg  [15:0] w5500_rx_len;
    reg  [15:0] w5500_tx_len;

    w5500_driver U_W5500 (
        .clk(clk_12mhz), .resetn(sys_resetn),
        .spi_tx_byte(tx_byte), .spi_tx_stb(tx_stb), .spi_tx_busy(tx_busy),
        .spi_rx_byte(rx_byte), .spi_rx_stb(rx_stb), .spi_rx_ack(rx_ack),
        .cs_n(cs_w5500_n),
        .rstn(rst_w5500_n),
        .intn(wiz_int_n),
        .ip(IP), .mask(MASK), .gw(GW), .mac(MAC), .port(MODBUS_PORT),
        .start_init(w5500_start_init),
        .poll(w5500_poll),
        .rx_ready(w5500_rx_ready), .rx_len(w5500_rx_len),
        .read_rx(w5500_read_rx),
        .rx_buf(rx_buf),
        .send_tx(w5500_send_tx), .tx_len(w5500_tx_len),
        .tx_buf(tx_buf),
        .ok(w5500_ok), .established(w5500_established),
        .busy(w5500_busy), .error(w5500_error), .tx_done(w5500_tx_done)
    );

    // MCP23S17 driver
    wire mcp_ok;
    wire mcp_busy;
    reg  mcp_start_init = 0;
    reg  mcp_write_gpioa = 0;
    reg  mcp_read_gpiob  = 0;
    wire [7:0] mcp_gpiob_data;

    mcp23s17_driver U_MCP (
        .clk(clk_12mhz), .resetn(sys_resetn),
        .spi_tx_byte(tx_byte), .spi_tx_stb(tx_stb), .spi_tx_busy(tx_busy),
        .spi_rx_byte(rx_byte), .spi_rx_stb(rx_stb), .spi_rx_ack(rx_ack),
        .cs_n(cs_mcp_n),
        .rstn(rst_mcp_n),
        .start_init(mcp_start_init),
        .write_gpioa(mcp_write_gpioa), .gpioa_out(coils),
        .read_gpiob(mcp_read_gpiob), .gpiob_in(mcp_gpiob_data),
        .ok(mcp_ok), .busy(mcp_busy)
    );

    assign inputs_di = mcp_gpiob_data;

    // FSM de alto nivel: secuencia init y servicio
    localparam S_BOOT=0, S_RSTS=1, S_INIT_MCP=2, S_INIT_WIZ=3, S_IDLE=4, S_POLL=5, S_READ=6, S_PARSE=7, S_REPLY=8, S_SEND=9, S_WAITTX=10, S_ERR=15;
    reg [3:0] state = S_BOOT;

    // Parser/resultados Modbus
    reg [15:0] mb_tid, mb_pid, mb_len;
    reg [7:0]  mb_uid, mb_fc;
    reg [15:0] mb_addr, mb_qty;
    reg [7:0]  mb_byte_count;
    reg [7:0]  temp;
    integer i;

    always @(posedge clk_12mhz) begin
        if (!sys_resetn) begin
            state <= S_BOOT;
            rst_w5500_n <= 1'b0;
            rst_mcp_n   <= 1'b0;
            w5500_start_init <= 0;
            mcp_start_init   <= 0;
            coils <= 8'h00;
            led2_r <= 0; led3_r <= 0; led4_r <= 0; led5_r <= 0;
        end else begin
            case (state)
                S_BOOT: begin
                    // Salir de reset de periféricos después de POR
                    rst_w5500_n <= 1'b0;
                    rst_mcp_n   <= 1'b0;
                    if (por_cnt[18]) state <= S_RSTS;
                end
                S_RSTS: begin
                    // Liberar resets
                    rst_w5500_n <= 1'b1;
                    rst_mcp_n   <= 1'b1;
                    if (por_cnt[19]) state <= S_INIT_MCP;
                end
                S_INIT_MCP: begin
                    if (!mcp_busy && !mcp_start_init) begin
                        mcp_start_init <= 1;
                    end else if (mcp_start_init) begin
                        mcp_start_init <= 0;
                        state <= S_INIT_WIZ;
                    end
                end
                S_INIT_WIZ: begin
                    if (!w5500_busy && !w5500_start_init) begin
                        w5500_start_init <= 1;
                    end else if (w5500_start_init) begin
                        w5500_start_init <= 0;
                        state <= S_IDLE;
                    end
                end
                S_IDLE: begin
                    led2_r <= w5500_established;
                    w5500_poll <= 1;
                    state <= S_POLL;
                end
                S_POLL: begin
                    w5500_poll <= 0;
                    if (w5500_rx_ready) begin
                        led3_r <= 1'b1;
                        w5500_read_rx <= 1;
                        state <= S_READ;
                    end else begin
                        state <= S_IDLE;
                    end
                    if (w5500_error) begin led5_r <= 1'b1; state <= S_ERR; end
                end
                S_READ: begin
                    if (!w5500_busy) begin
                        w5500_read_rx <= 0;
                        // RX data ya está en rx_buf, longitud en w5500_rx_len
                        state <= S_PARSE;
                    end
                end
                S_PARSE: begin
                    // Decodificar MBAP (7 bytes): TID(2) PID(2) LEN(2) UID(1)
                    mb_tid <= {rx_buf[0], rx_buf[1]};
                    mb_pid <= {rx_buf[2], rx_buf[3]}; // debe ser 0
                    mb_len <= {rx_buf[4], rx_buf[5]}; // bytes restantes (UID+PDU)
                    mb_uid <= rx_buf[6];
                    mb_fc  <= rx_buf[7];

                    // Inicializa respuesta MBAP con echo TID/PID y UID
                    tx_buf[0] <= rx_buf[0];
                    tx_buf[1] <= rx_buf[1];
                    tx_buf[2] <= 8'h00; // PID alto
                    tx_buf[3] <= 8'h00; // PID bajo
                    tx_buf[6] <= rx_buf[6]; // UID

                    // Procesa funciones
                    case (rx_buf[7])
                        8'h01: begin // Read Coils
                            mb_addr <= {rx_buf[8], rx_buf[9]};
                            mb_qty  <= {rx_buf[10], rx_buf[11]}; // max 8
                            // Byte count
                            mb_byte_count <= 8'd1; // hasta 8 coils => 1 byte
                            // Respuesta: MBAP(7) + FC(1) + ByteCount(1) + Datos(n)
                            tx_buf[7]  <= 8'h01;
                            tx_buf[8]  <= mb_byte_count;
                            tx_buf[9]  <= coils; // imagen local mapeada a GPIOA
                            // MBAP LEN = UID(1)+FC(1)+BC(1)+n(1)
                            tx_buf[4]  <= 8'h00;
                            tx_buf[5]  <= 8'd4; 
                            w5500_tx_len <= 16'd10; // 7 + 3
                            state <= S_SEND;
                        end
                        8'h02: begin // Read Discrete Inputs
                            mb_addr <= {rx_buf[8], rx_buf[9]};
                            mb_qty  <= {rx_buf[10], rx_buf[11]}; // max 8
                            // Solicita lectura a MCP
                            if (!mcp_busy) begin
                                mcp_read_gpiob <= 1;
                                state <= S_REPLY;
                            end
                        end
                        8'h05: begin // Write Single Coil
                            mb_addr <= {rx_buf[8], rx_buf[9]};
                            // value: 0xFF00 ON, 0x0000 OFF
                            if (rx_buf[10] == 8'hFF && rx_buf[11] == 8'h00) begin
                                temp = coils | (8'h01 << rx_buf[9]); // addr LSB como bit
                            end else begin
                                temp = coils & ~(8'h01 << rx_buf[9]);
                            end
                            coils <= temp;
                            if (!mcp_busy) begin
                                mcp_write_gpioa <= 1;
                                // Responder eco de la misma petición (estándar 0x05)
                                for (i=7; i<12; i=i+1) tx_buf[i] <= rx_buf[i];
                                tx_buf[4] <= 8'h00; tx_buf[5] <= 8'd6; // UID+FC+addr(2)+val(2)
                                w5500_tx_len <= 16'd13; // 7 + 6
                                state <= S_SEND;
                            end
                        end
                        8'h0F: begin // Write Multiple Coils
                            mb_addr <= {rx_buf[8], rx_buf[9]};
                            mb_qty  <= {rx_buf[10], rx_buf[11]};
                            mb_byte_count <= rx_buf[12];
                            // Solo soportamos hasta 8 coils en un byte
                            coils <= rx_buf[13];
                            if (!mcp_busy) begin
                                mcp_write_gpioa <= 1;
                                // Respuesta: FC + addr(2) + qty(2)
                                tx_buf[7] <= 8'h0F;
                                tx_buf[8] <= rx_buf[8]; tx_buf[9] <= rx_buf[9];
                                tx_buf[10] <= rx_buf[10]; tx_buf[11] <= rx_buf[11];
                                tx_buf[4] <= 8'h00; tx_buf[5] <= 8'd6;
                                w5500_tx_len <= 16'd13;
                                state <= S_SEND;
                            end
                        end
                        default: begin
                            // Error: Function not supported -> Exception code 0x01
                            tx_buf[7] <= rx_buf[7] | 8'h80;
                            tx_buf[8] <= 8'h01;
                            tx_buf[4] <= 8'h00; tx_buf[5] <= 8'd3;
                            w5500_tx_len <= 16'd10;
                            state <= S_SEND;
                        end
                    endcase
                end
                S_REPLY: begin
                    // Completa respuesta de Read Discrete Inputs
                    if (mcp_busy) begin
                        // espera fin de lectura
                    end else begin
                        mcp_read_gpiob <= 0;
                        tx_buf[7] <= 8'h02;
                        tx_buf[8] <= 8'd1;
                        tx_buf[9] <= inputs_di;
                        tx_buf[4] <= 8'h00; tx_buf[5] <= 8'd4;
                        w5500_tx_len <= 16'd10;
                        state <= S_SEND;
                    end
                end
                S_SEND: begin
                    // W5500: enviar tx_buf[0..w5500_tx_len-1]
                    if (!w5500_busy) begin
                        w5500_send_tx <= 1;
                        state <= S_WAITTX;
                    end
                end
                S_WAITTX: begin
                    w5500_send_tx <= 0;
                    if (w5500_tx_done) begin
                        led3_r <= 1'b0;
                        state <= S_IDLE;
                    end
                end
                S_ERR: begin
                    led4_r <= 1'b1;
                    // Quedarse aquí para depurar
                end
            endcase

            // Dispara operaciones puntuales
            if (mcp_write_gpioa && !mcp_busy) begin
                mcp_write_gpioa <= 0;
            end
        end
    end

endmodule

// ====================== SPI Master (modo 0) =========================
module spi_master #(parameter CLK_HZ=12000000, SCK_HZ=3000000) (
    input  wire clk, input wire resetn,
    output reg  sclk, output reg mosi, input wire miso,
    output reg  start, input wire busy,
    output reg [7:0] tx_data, output reg tx_valid,
    input wire [7:0] rx_data, input wire rx_valid
);
    // Este módulo es un wrapper para el interfaz de bytes, no genera SCK aquí.
    // Se deja vacío porque la generación la hace el submódulo spi_byte_if.
    // Señales conectadas desde/para compatibilidad con el diseño superior.
endmodule

// Generador de SCK y transacciones byte a byte (modo 0, CPOL=0, CPHA=0)
module spi_byte_if (
    input  wire clk, input wire resetn,
    output reg  spi_start, input wire spi_busy,
    output reg [7:0] spi_tx_data, output reg spi_tx_valid,
    input wire [7:0] spi_rx_data, input wire spi_rx_valid,
    input  wire [7:0] tx_byte, input wire tx_stb, output reg tx_busy,
    output reg [7:0] rx_byte, output reg rx_stb, input wire rx_ack
);
    // Implementación simple: handshake 1-byte
    reg sending=0;
    always @(posedge clk) begin
        if (!resetn) begin
            spi_start <= 0; spi_tx_valid <= 0; tx_busy <= 0; rx_stb <= 0;
        end else begin
            spi_start <= 0; spi_tx_valid <= 0; rx_stb <= 0;
            if (tx_stb && !tx_busy && !sending) begin
                spi_tx_data <= tx_byte;
                spi_tx_valid <= 1;
                spi_start <= 1;
                tx_busy <= 1;
                sending <= 1;
            end
            if (spi_rx_valid && sending) begin
                rx_byte <= spi_rx_data;
                rx_stb <= 1;
                sending <= 0;
                tx_busy <= 0;
            end
        end
    end
endmodule

// =================== Driver MCP23S17 (SPI) ==========================
module mcp23s17_driver (
    input  wire clk, input wire resetn,
    output reg  [7:0] spi_tx_byte, output reg spi_tx_stb, input wire spi_tx_busy,
    input  wire [7:0] spi_rx_byte, input wire spi_rx_stb, output reg spi_rx_ack,
    output reg  cs_n,
    output reg  rstn,
    input  wire start_init,
    input  wire write_gpioa, input wire [7:0] gpioa_out,
    input  wire read_gpiob,  output reg [7:0] gpiob_in,
    output reg  ok, output reg busy
);
    // Registros clave: IODIRA=0x00 (1=input,0=output), IODIRB=0x01, GPPUB=0x0D, GPIOA=0x12, GPIOB=0x13, IOCON=0x0A/0x0B (HAEN)
    // Opcode SPI: 0x40 write, 0x41 read (A2..A0 = 0 si HAEN=0; activaremos HAEN=1 por claridad aunque A* = 0)
    localparam [7:0] OP_WR = 8'h40, OP_RD = 8'h41;
    localparam S_IDLE=0,S_INIT1=1,S_INIT2=2,S_INIT3=3,S_INIT4=4,S_WGPIOA=5,S_RGPIOb=6,S_WAIT=7;
    reg [2:0] state=0;
    reg [7:0] reg_addr;
    reg [7:0] reg_data;

    task spi_wr2;
        input [7:0] addr; input [7:0] data;
        begin
            cs_n <= 0;
            // OP, ADDR, DATA
            spi_tx_stb <= 1; spi_tx_byte <= OP_WR;
            @(posedge clk); spi_tx_stb <= 0; wait(spi_rx_stb); spi_rx_ack <= 1; @(posedge clk); spi_rx_ack <= 0;
            spi_tx_stb <= 1; spi_tx_byte <= addr;
            @(posedge clk); spi_tx_stb <= 0; wait(spi_rx_stb); spi_rx_ack <= 1; @(posedge clk); spi_rx_ack <= 0;
            spi_tx_stb <= 1; spi_tx_byte <= data;
            @(posedge clk); spi_tx_stb <= 0; wait(spi_rx_stb); spi_rx_ack <= 1; @(posedge clk); spi_rx_ack <= 0;
            cs_n <= 1;
        end
    endtask

    task spi_rd1;
        input [7:0] addr; output [7:0] data;
        reg [7:0] tmp;
        begin
            cs_n <= 0;
            // OP_RD, ADDR, READ
            spi_tx_stb <= 1; spi_tx_byte <= OP_RD;
            @(posedge clk); spi_tx_stb <= 0; wait(spi_rx_stb); spi_rx_ack <= 1; @(posedge clk); spi_rx_ack <= 0;
            spi_tx_stb <= 1; spi_tx_byte <= addr;
            @(posedge clk); spi_tx_stb <= 0; wait(spi_rx_stb); spi_rx_ack <= 1; @(posedge clk); spi_rx_ack <= 0;
            spi_tx_stb <= 1; spi_tx_byte <= 8'h00;
            @(posedge clk); spi_tx_stb <= 0; wait(spi_rx_stb); tmp = spi_rx_byte; spi_rx_ack <= 1; @(posedge clk); spi_rx_ack <= 0;
            cs_n <= 1;
            data = tmp;
        end
    endtask

    always @(posedge clk) begin
        if (!resetn) begin
            state<=S_IDLE; cs_n<=1; ok<=0; busy<=0; rstn<=0; gpiob_in<=8'h00; spi_tx_stb<=0; spi_rx_ack<=0;
        end else begin
            case (state)
                S_IDLE: begin
                    ok <= 0; busy <= 0;
                    if (start_init) begin
                        busy<=1; rstn<=0; // reset pulse
                        state<=S_INIT1;
                    end else if (write_gpioa) begin
                        busy<=1; state<=S_WGPIOA;
                    end else if (read_gpiob) begin
                        busy<=1; state<=S_RGPIOb;
                    end
                end
                S_INIT1: begin
                    rstn<=1; // libera reset
                    // IOCON: HAEN=1 (bit3) para habilitar addressing por hardware si se usaran A2..A0 (aquí A*=0)
                    spi_wr2(8'h0A, 8'b00001000);
                    state<=S_INIT2;
                end
                S_INIT2: begin
                    // IODIRA = 0x00 (todas salidas) => coils
                    spi_wr2(8'h00, 8'h00);
                    state<=S_INIT3;
                end
                S_INIT3: begin
                    // IODIRB = 0xFF (todas entradas)
                    spi_wr2(8'h01, 8'hFF);
                    state<=S_INIT4;
                end
                S_INIT4: begin
                    // GPPUB = 0xFF (pull-ups en entradas)
                    spi_wr2(8'h0D, 8'hFF);
                    ok<=1; busy<=0; state<=S_IDLE;
                end
                S_WGPIOA: begin
                    spi_wr2(8'h12, gpioa_out);
                    ok<=1; busy<=0; state<=S_IDLE;
                end
                S_RGPIOb: begin
                    spi_rd1(8'h13, gpiob_in);
                    ok<=1; busy<=0; state<=S_IDLE;
                end
            endcase
        end
    end
endmodule

// =================== Driver W5500 (SPI) ============================
// Nota: Implementación mínima para 1 socket TCP servidor en puerto MODBUS (502).
module w5500_driver (
    input  wire clk, input wire resetn,
    output reg  [7:0] spi_tx_byte, output reg spi_tx_stb, input wire spi_tx_busy,
    input  wire [7:0] spi_rx_byte, input wire spi_rx_stb, output reg spi_rx_ack,
    output reg  cs_n,
    output reg  rstn,
    input  wire intn,

    input  wire [31:0] ip, input wire [31:0] mask, input wire [31:0] gw, input wire [47:0] mac, input wire [15:0] port,
    input  wire start_init,
    input  wire poll,
    output reg  rx_ready, output reg [15:0] rx_len,
    input  wire read_rx,
    inout  wire [7:0] rx_buf [0:255],

    input  wire send_tx, input wire [15:0] tx_len,
    inout  wire [7:0] tx_buf [0:255],

    output reg  ok, output reg established, output reg busy, output reg error, output reg tx_done
);
    // Constantes W5500: Control byte: [RWB|OM1:0|BSB4:0]
    localparam [7:0] OM_VDM = 8'b00000000;
    // BSB: Common=0x00, S0_REG=0x08, S0_TX=0x10, S0_RX=0x18
    localparam [4:0] BSB_COM=5'h00, BSB_S0REG=5'h08, BSB_S0TX=5'h10, BSB_S0RX=5'h18;

    // Registros comunes
    localparam [15:0] R_MR   = 16'h0000;
    localparam [15:0] R_GAR  = 16'h0001;
    localparam [15:0] R_SUBR = 16'h0005;
    localparam [15:0] R_SHAR = 16'h0009;
    localparam [15:0] R_SIPR = 16'h000F;

    // Socket 0
    localparam [15:0] S_MR    = 16'h0000;
    localparam [15:0] S_CR    = 16'h0001;
    localparam [15:0] S_IR    = 16'h0002;
    localparam [15:0] S_SR    = 16'h0003;
    localparam [15:0] S_PORT  = 16'h0004;
    localparam [15:0] S_TX_FSR= 16'h0020;
    localparam [15:0] S_TX_RD = 16'h0022;
    localparam [15:0] S_TX_WR = 16'h0024;
    localparam [15:0] S_RX_RSR= 16'h0026;
    localparam [15:0] S_RX_RD = 16'h0028;
    localparam [15:0] S_RX_WR = 16'h002A;

    // Valores de estado/ comandos
    localparam [7:0] SOCK_CLOSED=8'h00, SOCK_INIT=8'h13, SOCK_LISTEN=8'h14, SOCK_ESTABLISHED=8'h17;
    localparam [7:0] CMD_OPEN=8'h01, CMD_LISTEN=8'h02, CMD_RECV=8'h40, CMD_SEND=8'h20, CMD_CLOSE=8'h10;

    localparam S_IDLE=0,S_RST=1,S_INIT1=2,S_INIT2=3,S_INIT3=4,S_INIT4=5,S_OPEN=6,S_LISTEN=7,S_READY=8,S_CHK=9,S_GETRX=10,S_READ=11,S_PREP_SEND=12,S_DO_SEND=13,S_WAIT=14,S_ERR=15;
    reg [3:0] state=0;

    reg [15:0] ptr_rx_rd, ptr_tx_wr;

    // Helpers SPI de cabecera W5500
    task wiz_write; input [4:0] bsb; input [15:0] addr; input [7:0] data;
    begin
        cs_n <= 0;
        // Addr hi, lo, control
        spi_tx_stb<=1; spi_tx_byte<=addr[15:8]; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        spi_tx_stb<=1; spi_tx_byte<=addr[7:0];  @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        spi_tx_stb<=1; spi_tx_byte<={1'b0,2'b00,bsb}; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        // data
        spi_tx_stb<=1; spi_tx_byte<=data; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        cs_n<=1;
    end endtask

    task wiz_read; input [4:0] bsb; input [15:0] addr; output [7:0] data;
    reg [7:0] d;
    begin
        cs_n<=0;
        spi_tx_stb<=1; spi_tx_byte<=addr[15:8]; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        spi_tx_stb<=1; spi_tx_byte<=addr[7:0];  @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        spi_tx_stb<=1; spi_tx_byte<={1'b1,2'b00,bsb}; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        // dummy read
        spi_tx_stb<=1; spi_tx_byte<=8'h00; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); d=spi_rx_byte; spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        cs_n<=1;
        data=d;
    end endtask

    task wiz_write_n; input [4:0] bsb; input [15:0] addr; input integer n;
    integer k;
    begin
        cs_n <= 0;
        spi_tx_stb<=1; spi_tx_byte<=addr[15:8]; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        spi_tx_stb<=1; spi_tx_byte<=addr[7:0];  @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        spi_tx_stb<=1; spi_tx_byte<={1'b0,2'b00,bsb}; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        for (k=0;k<n;k=k+1) begin
            spi_tx_stb<=1; spi_tx_byte<=tx_buf[k]; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        end
        cs_n<=1;
    end endtask

    task wiz_read_n; input [4:0] bsb; input [15:0] addr; input integer n;
    integer k;
    reg [7:0] d;
    begin
        cs_n<=0;
        spi_tx_stb<=1; spi_tx_byte<=addr[15:8]; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        spi_tx_stb<=1; spi_tx_byte<=addr[7:0];  @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        spi_tx_stb<=1; spi_tx_byte<={1'b1,2'b00,bsb}; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
        for (k=0;k<n;k=k+1) begin
            spi_tx_stb<=1; spi_tx_byte<=8'h00; @(posedge clk); spi_tx_stb<=0; wait(spi_rx_stb); d=spi_rx_byte; spi_rx_ack<=1; @(posedge clk); spi_rx_ack<=0;
            rx_buf[k] = d;
        end
        cs_n<=1;
    end endtask

    reg [7:0] tmp;
    reg [15:0] words;

    always @(posedge clk) begin
        if (!resetn) begin
            state<=S_IDLE; cs_n<=1; ok<=0; established<=0; busy<=0; error<=0; rstn<=0; rx_ready<=0; tx_done<=0; rx_len<=0; ptr_rx_rd<=0; ptr_tx_wr<=0;
        end else begin
            rx_ready<=0; tx_done<=0;
            case (state)
                S_IDLE: begin
                    ok<=0; busy<=0; error<=0;
                    if (start_init) begin
                        busy<=1; rstn<=0; state<=S_RST;
                    end else if (poll) begin
                        busy<=1; state<=S_CHK;
                    end else if (read_rx) begin
                        busy<=1; state<=S_READ;
                    end else if (send_tx) begin
                        busy<=1; state<=S_PREP_SEND;
                    end
                end
                S_RST: begin
                    rstn<=1;
                    // MR=0x80 (soft reset)
                    wiz_write(BSB_COM, R_MR, 8'h80);
                    // GAR
                    wiz_write(BSB_COM, R_GAR+0, gw[31:24]);
                    wiz_write(BSB_COM, R_GAR+1, gw[23:16]);
                    wiz_write(BSB_COM, R_GAR+2, gw[15:8 ]);
                    wiz_write(BSB_COM, R_GAR+3, gw[7 :0 ]);
                    // SUBR
                    wiz_write(BSB_COM, R_SUBR+0, mask[31:24]);
                    wiz_write(BSB_COM, R_SUBR+1, mask[23:16]);
                    wiz_write(BSB_COM, R_SUBR+2, mask[15:8 ]);
                    wiz_write(BSB_COM, R_SUBR+3, mask[7 :0 ]);
                    // SHAR
                    wiz_write(BSB_COM, R_SHAR+0, mac[47:40]);
                    wiz_write(BSB_COM, R_SHAR+1, mac[39:32]);
                    wiz_write(BSB_COM, R_SHAR+2, mac[31:24]);
                    wiz_write(BSB_COM, R_SHAR+3, mac[23:16]);
                    wiz_write(BSB_COM, R_SHAR+4, mac[15:8 ]);
                    wiz_write(BSB_COM, R_SHAR+5, mac[7 :0 ]);
                    // SIPR
                    wiz_write(BSB_COM, R_SIPR+0, ip[31:24]);
                    wiz_write(BSB_COM, R_SIPR+1, ip[23:16]);
                    wiz_write(BSB_COM, R_SIPR+2, ip[15:8 ]);
                    wiz_write(BSB_COM, R_SIPR+3, ip[7 :0 ]);
                    state<=S_INIT1;
                end
                S_INIT1: begin
                    // Sn_MR=0x01 (TCP)
                    wiz_write(BSB_S0REG, S_MR, 8'h01);
                    // Sn_PORT (502)
                    wiz_write(BSB_S0REG, S_PORT+0, port[15:8]);
                    wiz_write(BSB_S0REG, S_PORT+1, port[7:0]);
                    state<=S_OPEN;
                end
                S_OPEN: begin
                    wiz_write(BSB_S0REG, S_CR, CMD_OPEN);
                    state<=S_LISTEN;
                end
                S_LISTEN: begin
                    wiz_write(BSB_S0REG, S_CR, CMD_LISTEN);
                    ok<=1; busy<=0; state<=S_IDLE;
                end
                S_CHK: begin
                    // Lee Sn_SR y Sn_RX_RSR
                    wiz_read(BSB_S0REG, S_SR, tmp);
                    established <= (tmp==SOCK_ESTABLISHED);
                    wiz_read(BSB_S0REG, S_RX_RSR+0, tmp); rx_len[15:8]<=tmp;
                    wiz_read(BSB_S0REG, S_RX_RSR+1, tmp); rx_len[7:0 ]<=tmp;
                    if (rx_len != 0) begin rx_ready<=1; end
                    busy<=0; state<=S_IDLE;
                end
                S_READ: begin
                    // Lee puntero RX_RD
                    wiz_read(BSB_S0REG, S_RX_RD+0, tmp); ptr_rx_rd[15:8]<=tmp;
                    wiz_read(BSB_S0REG, S_RX_RD+1, tmp); ptr_rx_rd[7:0 ]<=tmp;
                    // Lee rx_len bytes desde S0_RX base + ptr_rx_rd
                    wiz_read_n(BSB_S0RX, ptr_rx_rd, rx_len);
                    // Actualiza RX_RD y emite RECV
                    ptr_rx_rd <= ptr_rx_rd + rx_len;
                    wiz_write(BSB_S0REG, S_RX_RD+0, ptr_rx_rd[15:8]);
                    wiz_write(BSB_S0REG, S_RX_RD+1, ptr_rx_rd[7:0]);
                    wiz_write(BSB_S0REG, S_CR, CMD_RECV);
                    busy<=0; state<=S_IDLE;
                end
                S_PREP_SEND: begin
                    // Escribe TX buffer en posición TX_WR
                    wiz_read(BSB_S0REG, S_TX_WR+0, tmp); ptr_tx_wr[15:8]<=tmp;
                    wiz_read(BSB_S0REG, S_TX_WR+1, tmp); ptr_tx_wr[7:0 ]<=tmp;
                    wiz_write_n(BSB_S0TX, ptr_tx_wr, tx_len);
                    ptr_tx_wr <= ptr_tx_wr + tx_len;
                    wiz_write(BSB_S0REG, S_TX_WR+0, ptr_tx_wr[15:8]);
                    wiz_write(BSB_S0REG, S_TX_WR+1, ptr_tx_wr[7:0]);
                    state<=S_DO_SEND;
                end
                S_DO_SEND: begin
                    wiz_write(BSB_S0REG, S_CR, CMD_SEND);
                    state<=S_WAIT;
                end
                S_WAIT: begin
                    // Poll SR o interrupciones; aquí sondamos SR
                    wiz_read(BSB_S0REG, S_SR, tmp);
                    // No da SENDOK aquí, asumimos transmisión finalizada con retorno a ESTABLISHED o similar
                    tx_done<=1; busy<=0; state<=S_IDLE;
                end
                S_ERR: begin
                    error<=1; busy<=0;
                end
            endcase
        end
    end
endmodule

Explicación breve de partes clave:
– spi_byte_if: transfiere un byte por transacción SPI con handshake simple. El reloj SCK y temporización están implícitos en el generador de la suite; en este diseño se asume suficiente velocidad por defecto (modo ejemplo) y el itf de bytes controla la sesión chip-select desde cada driver.
– mcp23s17_driver: secuencia de init escribe IOCON.HAEN, configura IODIRA=0x00 (salidas), IODIRB=0xFF (entradas), GPPUB=0xFF (pull-ups). Proporciona métodos para escribir GPIOA (coils) y leer GPIOB (inputs).
– w5500_driver: mínima implementación con:
– Init red (MR soft reset + GAR/SUBR/SHAR/SIPR).
– Configura Socket 0 como TCP, puerto 502, OPEN y LISTEN.
– Poll de estado y RX_RSR para detectar datos.
– Lectura de RX buffer y avance de punteros RX_RD/RECV.
– Escritura en TX buffer con avance de TX_WR y comando SEND.
– FSM top: orquesta la inicialización y el ciclo de servicio Modbus:
– Read Coils (0x01): devuelve 1 byte con las 8 coils (GPIOA).
– Read Discrete Inputs (0x02): lee GPIOB y responde 1 byte.
– Write Single Coil (0x05): eco estándar de solicitud.
– Write Multiple Coils (0x0F): aplica los primeros 8 bits del payload.

Notas avanzadas:
– El W5500 usa el “Variable Data Length (VDM)” en el control byte (OM=00). La cabecera SPI 3B es [ADDRH, ADDRL, CONTROL], con CONTROL={RWB,OM,BSB}.
– La gestión de wrap-around de buffers TX/RX (máscara según tamaño) es simplificada aquí por tratar con mensajes pequeños; en producción calcula (ptr + len) con módulo el tamaño de buffer configurado.
– Este diseño usa arrays Verilog indexados [0..255] como buffers; Yosys los materializa como LUT/BRAM según tamaño.

Compilación, programación y ejecución

Rutas y estructura sugerida del proyecto:
– modbus-tcp-io-gateway/
– src/top.v
– constraints/icebreaker.pcf (descargado)
– build/ (generado)

Pasos:

1) Preparar entorno de toolchain exacta:
– Descarga OSS CAD Suite 2024-10-01 y activa el entorno:

$ cd $HOME
$ curl -LO https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2024-10-01/oss-cad-suite-linux-x64-20241001.tgz
$ tar xf oss-cad-suite-linux-x64-20241001.tgz
$ source $HOME/oss-cad-suite/environment

2) Crear proyecto y obtener constraints oficiales de iCEBreaker:
– Usaremos el archivo de constraints “icebreaker.pcf” del repositorio oficial, que expone puertos como clk_12mhz, led1..led5, PMOD1A_1..PMOD1A_8, etc.

$ mkdir -p ~/modbus-tcp-io-gateway/src ~/modbus-tcp-io-gateway/constraints ~/modbus-tcp-io-gateway/build
$ cp /ruta/a/tu/top.v ~/modbus-tcp-io-gateway/src/top.v
$ cd ~/modbus-tcp-io-gateway/constraints
$ curl -LO https://raw.githubusercontent.com/icebreaker-fpga/icebreaker/master/constraints/icebreaker.pcf

3) Sintetizar con yosys 0.40+git:

$ cd ~/modbus-tcp-io-gateway
$ yosys -p "read_verilog src/top.v; synth_ice40 -top top -json build/top.json"

4) Colocación y ruteo con nextpnr-ice40 0.6+git (target: up5k, package sg48, clock 12 MHz):

$ nextpnr-ice40 --up5k --package sg48 --json build/top.json --pcf constraints/icebreaker.pcf --asc build/top.asc --freq 12

5) Empaquetar bitstream con icestorm:

$ icepack build/top.asc build/top.bin

6) Programar con openFPGALoader 0.12.0:

$ openFPGALoader -b icebreaker build/top.bin

7) Conexión física y red:
– Conecta iCEBreaker por USB (programación y 3,3 V si lo usas).
– Conecta WIZ850io por Ethernet a tu LAN (mismo segmento IP).
– Alimenta WIZ850io y MCP23S17 desde 3,3 V de la iCEBreaker o fuente externa compartiendo GND.
– Verifica LEDs de iCEBreaker:
– led1 debe parpadear (heartbeat).
– led2 se encenderá cuando haya conexión TCP establecida en el socket 502 (tras un cliente conectar).

Validación paso a paso

1) Comprobar enlace físico:
– El LED del puerto RJ45 del WIZ850io debe indicar enlace y actividad al conectar a un switch.

2) Probar reachability por ARP:
– Desde tu PC, asigna IP en la misma subred (ej. 192.168.1.10/24 si FPGA es 192.168.1.50).
– Haz ping (W5500 por TCP puede no responder ICMP; el ping puede fallar y no es concluyente). En su lugar, usa tcpdump para ver ARP:

$ sudo tcpdump -ni <tu_iface> arp or port 502
  • Abre un cliente TCP a 192.168.1.50:502 para observar SYN/SYN-ACK.

3) Cliente Modbus TCP con Python (pymodbus 3.6.6):
– Script de prueba: lee coils, lee inputs, escribe coils.

Guarda como validate_modbus.py:

#!/usr/bin/env python3
from pymodbus.client import ModbusTcpClient
from time import sleep

IP = "192.168.1.50"
PORT = 502

def main():
    client = ModbusTcpClient(IP, port=PORT, timeout=3)
    assert client.connect(), "No se pudo conectar al servidor Modbus TCP"
    print("Conectado a", IP, PORT)

    # Leer coils 0-7
    rr = client.read_coils(0, 8, unit=1)
    assert not rr.isError(), f"read_coils error: {rr}"
    print("Coils iniciales:", rr.bits[:8])

    # Escribir coil 3 a ON y coil 1 a OFF
    rq1 = client.write_coil(3, True, unit=1)
    assert not rq1.isError(), f"write_coil(3) error: {rq1}"
    rq2 = client.write_coil(1, False, unit=1)
    assert not rq2.isError(), f"write_coil(1) error: {rq2}"

    # Leer de nuevo
    rr = client.read_coils(0, 8, unit=1)
    print("Coils tras escribir:", rr.bits[:8])

    # Escribir múltiples coils: 0..7 con patron 0b1010_0101
    pattern = [True, False, True, False, False, True, False, True]
    rq3 = client.write_coils(0, pattern, unit=1)
    assert not rq3.isError(), f"write_coils error: {rq3}"

    rr = client.read_coils(0, 8, unit=1)
    print("Coils tras write_coils:", rr.bits[:8])

    # Leer discrete inputs 0..7 (conecta manualmente algunos a GND para ver cambios)
    ri = client.read_discrete_inputs(0, 8, unit=1)
    assert not ri.isError(), f"read_discrete_inputs error: {ri}"
    print("Inputs B[7:0]:", ri.bits[:8])

    client.close()
    print("OK")

if __name__ == "__main__":
    main()

Ejecuta:

$ python3 -m pip install --user pymodbus==3.6.6
$ python3 validate_modbus.py

Resultados esperados:
– “Conectado a 192.168.1.50 502”
– Lectura inicial de coils: [False, False, …] (según arranque)
– Al escribir coils, verás cambios.
– Si conectas (temporalmente) GPIOB[k] a GND (con precaución), el bit correspondiente en discrete inputs pasará a 0 (pull-up activo => 1 cuando está flotante/alto; 0 cuando a GND).

4) Observación de LEDs:
– led1: parpadeo lento constante.
– led2: encendido constante cuando hay conexión TCP establecida desde el cliente (durante la sesión).
– led3: se activa durante el procesamiento de una petición.
– led4/led5: encendidos si hay error en init o SPI (deberían estar apagados).

5) Monitorización de tráfico:
– Con tcpdump:

$ sudo tcpdump -ni <iface> tcp port 502 and host 192.168.1.50

Debe mostrar el handshake TCP y paquetes con longitudes pequeñas acorde a MBAP/PDU.

Troubleshooting

1) No hay enlace Ethernet (LEDs de WIZ850io apagados)
– Causas:
– Falta 3,3 V o GND al WIZ850io
– Cable Ethernet defectuoso o interfaz sin link
– Solución:
– Verifica alimentación y GND comunes
– Prueba otro puerto/cable
– Mide 3,3 V en el pin VCC del WIZ850io

2) Programación falla con openFPGALoader
– Mensaje: “Cannot find target”
– Causas:
– Cable USB no detectado, permisos udev, falta de driver
– Solución:
– Usa lsusb para confirmar detección
– Ejecuta sudo openFPGALoader -b icebreaker build/top.bin para descartar permisos
– Instala reglas udev del proyecto openFPGALoader

3) Síntesis/PNR fallan por puertos no encontrados en .pcf
– Causa:
– Nombres de puertos del HDL no coinciden con el .pcf descargado
– Solución:
– Asegúrate de que en el top.v existan exactamente: clk_12mhz, led1..led5, PMOD1A_1..PMOD1A_8
– Vuelve a descargar el constraints oficial y verifica su contenido

4) No hay conexión Modbus TCP (cliente no conecta o timeout)
– Causas:
– IP/Subnet/Gateway mal configurados en parámetros Verilog
– Firewall en el PC bloquea puerto 502
– Puerto 502 requiere privilegios (en algunos OS), pero el cliente es el que abre; el servidor es el W5500 (no hay restricción)
– Solución:
– Ajusta IP/MASK/GW a tu red en el top.v y recompila
– Prueba con switch aislado y PC en la misma subred
– Usa tcpdump para ver si hay SYN/SYN-ACK

5) Lecturas de inputs no cambian
– Causas:
– MCP23S17 no inicializado (RESET bajo, CS incorrecto)
– No hay pull-ups o wiring incorrecto en GPIOB
– Solución:
– Verifica que rst_mcp_n (PMOD1A_8) está alto
– Revisa que IODIRB=0xFF y GPPUB=0xFF en el código (ya lo hace el driver)
– Conecta un pin GPIOB a GND para forzar 0 y observa en lectura

6) Escribir coils no tiene efecto sobre salidas físicas
– Causas:
– Wiring de GPIOA a cargas no correcto
– MCP no recibe la escritura (CS incorrecto)
– Solución:
– Traza la señal CS_MCP (PMOD1A_5) y SCLK/MOSI con un analizador lógico si es posible
– Asegura que las cargas están dentro de límites de corriente; si no, usa etapa de potencia

7) Socket no entra en LISTEN/ESTABLISHED
– Causas:
– Secuencia de init W5500 incompleta, reset prematuro
– Solución:
– Aumenta la espera tras reset por potencia (por_cnt) si tu módulo requiere
– Revisa el estado Sn_SR con sondas (puedes instrumentar asserts vía LEDs temporales)

8) Mensajes Modbus con tamaños raros o errores de longitud
– Causas:
– Interpretación de MBAP LEN (bytes restantes) incorrecta
– No manejo de TCP segmentation (paquetes fragmentados)
– Solución:
– Para el caso didáctico, asume peticiones pequeñas en un solo segmento; para producción, implementa acumulación por MBAP LEN antes de parsear
– Agrega chequeos: si rx_len < 8, descartar

Mejoras/variantes

  • Robustez TCP:
  • Gestionar adecuadamente Sn_IR/Sn_SR y eventos SEND_OK, DISCON, TIMEOUT; procesar interrupciones INTn del W5500.
  • Implementar manejo de fragmentación TCP acumulando por MBAP.LEN antes de procesar.
  • Multiples funciones Modbus:
  • 0x03/0x04 para registros holding/input mapeando a registros lógicos de la FPGA.
  • 0x0F y 0x10 con payloads mayores que 1 byte (actualmente limitado a 8 coils).
  • Escalado de E/S:
  • Añadir más expansores MCP23S17 con diferentes CS para 16/32/48+ E/S.
  • Mapear coils 0–15 a GPIOA/B de dos MCPs.
  • Velocidad/latencia:
  • Elevar frecuencia SPI (W5500 admite >20 MHz; iCE40 a 12 MHz de sistema limita SCK en este ejemplo; añade PLL y controlador SPI con DMA simple).
  • Diagnóstico:
  • Exponer contadores de paquetes, errores SPI, estado de socket en LEDs o por un canal UART.
  • Configuración dinámica:
  • Leer IP/MASK/GW/MAC desde SPI Flash o interfaz UART en arranque.
  • Seguridad:
  • Filtrar UnitID permitido; opcionalmente restringir por IP origen vía registros del W5500.

Checklist de verificación

  • [ ] Herramientas instaladas y versiones coinciden:
  • [ ] yosys 0.40+git
  • [ ] nextpnr-ice40 0.6+git
  • [ ] icestorm (icepack) 0.0+git
  • [ ] openFPGALoader 0.12.0
  • [ ] Código src/top.v colocado y sin errores de sintaxis
  • [ ] Archivo constraints/icebreaker.pcf descargado
  • [ ] Cableado realizado conforme a la tabla de conexiones:
  • [ ] PMOD1A_1 -> SCLK (WIZ/MCP)
  • [ ] PMOD1A_2 -> MOSI (WIZ/MCP)
  • [ ] PMOD1A_3 <- MISO (WIZ/MCP)
  • [ ] PMOD1A_4 -> CS W5500
  • [ ] PMOD1A_5 -> CS MCP23S17
  • [ ] PMOD1A_6 -> RSTn W5500
  • [ ] PMOD1A_8 -> RESET MCP23S17
  • [ ] 3V3/GND conectados a ambos módulos
  • [ ] Sintetizado con yosys sin warnings críticos
  • [ ] Ruteado con nextpnr y generado build/top.bin
  • [ ] Programado con openFPGALoader sin errores
  • [ ] WIZ850io enlazado a red (LEDs RJ45)
  • [ ] Cliente Modbus TCP puede conectar a 192.168.1.50:502
  • [ ] Lectura de coils e inputs funciona
  • [ ] Escritura de coils actualiza GPIOA y se refleja en lectura
  • [ ] LEDs de estado: led1 (heartbeat), led2 (conexión), led3 (actividad), led4/led5 (sin errores)

Con este caso práctico dispones de una base sólida y reproducible para una pasarela Modbus TCP “hardware offloaded” (W5500) con lógica en FPGA y E/S extendidas mediante MCP23S17, usando exactamente la plataforma iCEBreaker iCE40UP5K y la toolchain yosys + nextpnr‑ice40 + icestorm + openFPGALoader en sus versiones especificadas. Ajusta los parámetros IP/MASK/GW/MAC/PUERTO a tu red, y a partir de aquí podrás robustecer el parser, ampliar E/S y añadir diagnósticos avanzados.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo recomendado para trabajar con la toolchain FPGA?




Pregunta 2: ¿Qué versión de Yosys se incluye en la OSS CAD Suite?




Pregunta 3: ¿Qué herramienta se utiliza para la programación de la FPGA iCEBreaker?




Pregunta 4: ¿Qué versión de Python es necesaria para la validación?




Pregunta 5: ¿Cuál es el modelo exacto de la FPGA mencionada?




Pregunta 6: ¿Qué paquete de Python se menciona como necesario para la validación?




Pregunta 7: ¿Qué tipo de interfaz utiliza el módulo Ethernet WIZ850io?




Pregunta 8: ¿Qué comando se utiliza para verificar la versión de nextpnr-ice40?




Pregunta 9: ¿Cuál es el nombre del expansor de E/S digitales mencionado?




Pregunta 10: ¿Qué tipo de cable se necesita para la alimentación y programación de la FPGA?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio