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
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.



