Objetivo y caso de uso
Qué construirás: Un sistema de control de un módulo e-Paper utilizando la placa Intel MAX 10 DE10-Lite a través de SPI.
Para qué sirve
- Visualización de datos en tiempo real en aplicaciones IoT.
- Despliegue de información estática en dispositivos portátiles.
- Actualización de contenido gráfico en sistemas de señalización digital.
- Implementación de interfaces de usuario en proyectos de automatización.
Resultado esperado
- Latencia de actualización de pantalla inferior a 1 segundo.
- Consumo de energía del módulo e-Paper menor a 10 mW en modo de espera.
- Capacidad de mostrar imágenes de hasta 128×296 píxeles con un tiempo de refresco de 2 segundos.
- Transmisión de datos a través de SPI a una velocidad de 1 Mbps.
Público objetivo: Ingenieros y desarrolladores de sistemas embebidos; Nivel: Avanzado
Arquitectura/flujo: Comunicación SPI entre la FPGA y el módulo e-Paper, con programación en Intel Quartus y uso del USB-Blaster II para la carga del diseño.
Nivel: Avanzado
Prerrequisitos
Sistema operativo y dependencias
- Ubuntu 22.04.4 LTS (64‑bit) con:
- Paquetes básicos: build-essential, git, udev, usbutils.
- Regla udev para USB-Blaster II (permite programar sin sudo).
- Alternativa (opcional): Windows 10/11 64‑bit con drivers de USB-Blaster II instalados (descargados desde Intel).
Toolchain exacta
- Intel Quartus Prime Lite Edition 22.1std.2 (Build 922 07/20/2023)
- Incluye Fitter y Programmer compatibles con MAX 10.
- Quartus Programmer 22.1std.2 (instalado con Quartus).
- ModelSim Intel FPGA Starter Edition 2020.1 (opcional, solo si deseas simular la lógica SPI).
Notas:
– Dispositivo objetivo: Intel MAX 10 10M50DAF484C7G (montado en la Terasic DE10‑Lite).
– Cable de programación: USB‑Blaster II (integrado en la DE10‑Lite).
Enlaces de referencia (opcional para ampliar)
- “DE10‑Lite System CD / Golden Top” de Terasic (versión reciente; se usará su archivo de asignación de pines).
- Datasheet del panel Waveshare 2.9-inch e‑Paper (SPI), variante monocroma (GDEH029A1 o similar; comandos compatibles).
Materiales
- 1 × Terasic DE10‑Lite (FPGA Intel MAX 10, modelo 10M50DAF484C7G).
- 1 × Módulo Waveshare 2.9-inch e‑Paper (SPI, monocromo; interfaz: SPI + pines DC, RST, BUSY).
- 8–10 cables Dupont macho‑macho.
- Alimentación desde la propia DE10‑Lite (3.3 V y GND del encabezado Arduino o GPIO).
- Opcional:
- Multímetro para verificar tensiones 3.3 V y continuidad.
- Osciloscopio o analizador lógico para verificar SCK/MOSI/CS/DC/RST.
Objetivo del proyecto: “spi-epaper-display” — inicializar y actualizar el panel e‑Paper por SPI desde el FPGA con un patrón verificable.
Preparación y conexión
Consideraciones eléctricas y de señal
- El e‑Paper Waveshare 2.9″ opera a 3.3 V lógicos, por lo que es compatible directamente con la DE10‑Lite (no se requieren adaptadores de nivel).
- Interfaz mínima a cablear:
- SPI: SCLK, MOSI, CS (MISO no se usa en esta demo).
- Control: DC (Data/Command), RST (reset), BUSY (estado).
- Alimentación: 3.3 V, GND.
Asignación lógica propuesta en la cabecera “Arduino” de la DE10‑Lite
Para evitar ambigüedades de pines físicos de encapsulado, usaremos la convención de la cabecera Arduino UNO‑R3 (presente en la DE10‑Lite) y los nombres de puertos que Terasic publica en su proyecto “Golden Top”. En dicho diseño de referencia, los puertos “ARDUINO_IO[15:0]” están mapeados a los pines Arduino D0..D13, A0..A5. Reutilizaremos esas asignaciones y nombraremos nuestro toplevel igual, de modo que la .qsf oficial nos sirva sin cambios.
Tabla de cableado (e‑Paper ↔ DE10‑Lite (Arduino header)):
| Señal e‑Paper | Función | Arduino UNO pin sugerido | ARDUINO_IO[n] | Dirección FPGA | Comentario |
|---|---|---|---|---|---|
| VCC | +3.3 V | 3V3 | — | — | Alimentación desde 3V3 de la placa |
| GND | GND | GND | — | — | Masa común |
| SCLK | SPI reloj | D13 | [13] | salida | SPI modo 0, ~1 MHz |
| MOSI (DIN) | SPI datos hacia panel | D11 | [11] | salida | Datos a panel |
| CS (CS#) | Chip Select (act. bajo) | D10 | [10] | salida | Selección del panel |
| DC | Data/Command selector | D9 | [9] | salida | 0=Comando, 1=Datos |
| RST (RST#) | Reset del panel (act. bajo) | D8 | [8] | salida | Mantener bajo para reset |
| BUSY | Estado ocupado del panel | D7 | [7] | entrada | 1=ocupado, 0=listo (según variante) |
Notas:
– El pin BUSY es crítico: la FSM debe esperar a que el panel termine operaciones (busy=0) antes de avanzar.
– El pin MISO del panel no se emplea en este caso práctico.
– Se recomiendan cables cortos (<15 cm) para señales SPI y control.
Verificación de la conexión antes de programar
- Comprueba continuidad 3V3 ↔ VCC y GND ↔ GND.
- Con la DE10‑Lite encendida sin programa, mide ~3.3 V entre VCC y GND del panel.
- Revisa polaridades: RST y CS son activos en bajo.
Código completo (Verilog) y explicación
Enfocamos el diseño a:
– Un master SPI de propósito general (modo 0).
– Un controlador FSM para e‑Paper 2.9″ (inicialización + escritura de patrón + actualización).
– Un módulo top que se ajusta a los puertos del Golden Top de la DE10‑Lite (para reutilizar asignaciones de pines).
Arquitectura y reloj
- Reloj de sistema: 50 MHz (MAX10_CLK1_50).
- Frecuencia SPI: ~1 MHz (configurable vía divisor).
- Temporizaciones e‑Paper: retardos en ms a partir de contador de 50 MHz.
- Señales de estado: LEDR[0] “DONE” (actualización completada), LEDR[1] “BUSY” reflejo del pin BUSY, LEDR[2] “SPI TX en curso”.
Bloque SPI Master (modo 0)
- CPOL=0, CPHA=0: el dato se muestrea en flanco de subida, se cambia en flanco de bajada.
- Interfaz sencilla: tx_valid + tx_byte → tx_ready/tx_done, señales sck/mosi/cs_n generadas por el propio master o por la FSM, según convenga.
- Aquí delegaremos CS al controlador EPD para mandar ráfagas completas por comando/datos.
Código: SPI + Controlador EPD + Top
// spi_master.v - SPI Mode 0 transmit-only (8 bits) con divisor de reloj parametrizable.
// Genera SCK y MOSI; CS_n lo gestiona el controlador superior (epd_driver).
module spi_master #(
parameter integer CLKS_PER_HALF_BIT = 25 // 50 MHz / (2*25) = 1 MHz
)(
input wire clk,
input wire rst, // sincrónico alto
input wire tx_valid, // pulso para iniciar transmisión de 'tx_data'
input wire [7:0] tx_data,
output reg tx_ready, // listo para aceptar un nuevo byte
output reg tx_done, // pulso 1 clk al finalizar un byte
output reg sck, // reloj SPI
output reg mosi
);
localparam IDLE = 2'd0, LOAD = 2'd1, SHIFT = 2'd2, DONE = 2'd3;
reg [1:0] state = IDLE;
reg [7:0] shreg;
reg [2:0] bit_cnt;
reg [15:0] divcnt;
always @(posedge clk) begin
if (rst) begin
state <= IDLE;
sck <= 1'b0;
mosi <= 1'b0;
tx_ready<= 1'b1;
tx_done <= 1'b0;
shreg <= 8'h00;
bit_cnt <= 3'd0;
divcnt <= 16'd0;
end else begin
tx_done <= 1'b0;
case (state)
IDLE: begin
sck <= 1'b0;
tx_ready <= 1'b1;
if (tx_valid) begin
state <= LOAD;
tx_ready<= 1'b0;
shreg <= tx_data;
end
end
LOAD: begin
// Output MSB first, SCK low
mosi <= shreg[7];
bit_cnt <= 3'd7;
divcnt <= 16'd0;
state <= SHIFT;
end
SHIFT: begin
// Togglear SCK cada CLKS_PER_HALF_BIT
if (divcnt == CLKS_PER_HALF_BIT-1) begin
divcnt <= 0;
sck <= ~sck;
if (sck == 1'b1) begin
// Flanco de bajada: shift out next bit
shreg <= {shreg[6:0], 1'b0};
mosi <= shreg[6];
if (bit_cnt == 0) begin
state <= DONE;
end else begin
bit_cnt <= bit_cnt - 1;
end
end
end else begin
divcnt <= divcnt + 1;
end
end
DONE: begin
// Asegura SCK=0 y anuncia fin
sck <= 1'b0;
tx_done <= 1'b1;
state <= IDLE;
end
endcase
end
end
endmodule
// epd_driver.v - Controlador simple para Waveshare 2.9" (GDEH029A1-like): init + patrón + update.
module epd_driver #(
parameter integer CLKS_PER_MS = 50000, // 50 MHz
parameter integer SPI_CLKS_PER_HALF_BIT = 25 // SPI ~1 MHz
)(
input wire clk,
input wire rst, // sincrónico alto
input wire epd_busy, // BUSY del panel (1=ocupado en muchas variantes; ajusta si es inverso)
output wire epd_sck, // SPI SCK
output wire epd_mosi, // SPI MOSI
output reg epd_cs_n, // Chip Select (activo en bajo)
output reg epd_dc, // Data/Command: 0=cmd, 1=data
output reg epd_rst_n, // Reset del panel (activo en bajo)
output reg done, // 1 cuando finaliza la primera actualización
output reg spi_active_led // LED de actividad SPI
);
// Instancia SPI
wire spi_tx_ready, spi_tx_done;
reg spi_tx_valid;
reg [7:0] spi_tx_data;
spi_master #(
.CLKS_PER_HALF_BIT(SPI_CLKS_PER_HALF_BIT)
) u_spi (
.clk(clk),
.rst(rst),
.tx_valid(spi_tx_valid),
.tx_data(spi_tx_data),
.tx_ready(spi_tx_ready),
.tx_done(spi_tx_done),
.sck(epd_sck),
.mosi(epd_mosi)
);
// Temporizador en ms
reg [31:0] ms_counter;
reg [31:0] ms_target;
wire ms_tick = (ms_counter == ms_target);
always @(posedge clk) begin
if (rst) begin
ms_counter <= 0;
end else begin
if (ms_counter < ms_target) ms_counter <= ms_counter + 1;
end
end
task ms_delay(input [15:0] ms);
begin
ms_target = ms * CLKS_PER_MS;
ms_counter = 0;
end
endtask
// Máquina de estados de alto nivel
localparam S_BOOT_RESET = 8'd0;
localparam S_BOOT_WAIT1 = 8'd1;
localparam S_BOOT_REL_RST = 8'd2;
localparam S_BOOT_WAIT2 = 8'd3;
localparam S_INIT_0 = 8'd10;
localparam S_INIT_1 = 8'd11;
localparam S_INIT_2 = 8'd12;
localparam S_INIT_3 = 8'd13;
localparam S_SET_RAM_XY = 8'd14;
localparam S_WRITE_RAM_CMD = 8'd15;
localparam S_WRITE_RAM = 8'd16;
localparam S_UPDATE_0 = 8'd20;
localparam S_UPDATE_1 = 8'd21;
localparam S_WAIT_BUSY = 8'd22;
localparam S_DONE = 8'd30;
reg [7:0] state = S_BOOT_RESET;
// Envío de bytes por SPI con DC y CS controlados aquí
reg launching;
reg [31:0] byte_count;
reg [31:0] total_bytes;
// Constantes del panel 296x128
localparam integer WIDTH = 128;
localparam integer HEIGHT = 296;
localparam integer BYTES_PER_LINE = WIDTH/8; // 16
localparam integer TOTAL_BYTES = (WIDTH*HEIGHT)/8; // 4736
// Helper para transmisión de un byte con DC = cmd/data
task send_cmd(input [7:0] b);
begin
epd_dc <= 1'b0;
spi_tx_data <= b;
spi_tx_valid<= 1'b1;
end
endtask
task send_dat(input [7:0] b);
begin
epd_dc <= 1'b1;
spi_tx_data <= b;
spi_tx_valid<= 1'b1;
end
endtask
// Generador de patrón: rayas verticales por bloques de 64 bytes alternos
function [7:0] pattern_byte(input [31:0] idx);
if (((idx/64) % 2) == 0) pattern_byte = 8'h00;
else pattern_byte = 8'hFF;
endfunction
always @(posedge clk) begin
if (rst) begin
state <= S_BOOT_RESET;
epd_rst_n <= 1'b1;
epd_cs_n <= 1'b1;
epd_dc <= 1'b0;
spi_tx_valid <= 1'b0;
done <= 1'b0;
spi_active_led<= 1'b0;
byte_count <= 0;
total_bytes <= 0;
end else begin
spi_tx_valid <= 1'b0; // pulso
spi_active_led<= 1'b0; // pulso cuando envíamos
case (state)
S_BOOT_RESET: begin
epd_cs_n <= 1'b1;
epd_rst_n <= 1'b0; // reset activo
ms_delay(10); // 10 ms
state <= S_BOOT_WAIT1;
end
S_BOOT_WAIT1: begin
if (ms_tick) state <= S_BOOT_REL_RST;
end
S_BOOT_REL_RST: begin
epd_rst_n <= 1'b1;
ms_delay(10);
state <= S_BOOT_WAIT2;
end
S_BOOT_WAIT2: begin
if (ms_tick) state <= S_INIT_0;
end
// Secuencia mínima de init para GDEH029A1 (ajusta si tu panel difiere)
// 0x01 DRIVER_OUTPUT_CONTROL: (HEIGHT-1) LSB, (HEIGHT-1) MSB, GD+SM=0x00
S_INIT_0: begin
epd_cs_n <= 1'b0;
if (spi_tx_ready) begin
send_cmd(8'h01);
spi_active_led <= 1'b1;
state <= S_INIT_1;
end
end
S_INIT_1: begin
if (spi_tx_done) begin
if (spi_tx_ready) begin
send_dat(8'h27); // 295 LSB
spi_active_led <= 1'b1;
state <= S_INIT_2;
end
end
end
S_INIT_2: begin
if (spi_tx_done) begin
if (spi_tx_ready) begin
send_dat(8'h01); // 295 MSB
spi_active_led <= 1'b1;
state <= S_INIT_3;
end
end
end
S_INIT_3: begin
if (spi_tx_done) begin
if (spi_tx_ready) begin
send_dat(8'h00); // GD=0, SM=0
spi_active_led <= 1'b1;
state <= S_SET_RAM_XY;
end
end
end
// Data entry mode, window y punteros
S_SET_RAM_XY: begin
if (spi_tx_done || spi_tx_ready) begin
case (byte_count)
0: begin send_cmd(8'h11); spi_active_led<=1'b1; end // Data Entry Mode
1: begin send_dat(8'h03); spi_active_led<=1'b1; end // X+ Y+
2: begin send_cmd(8'h44); spi_active_led<=1'b1; end // RAM X start/end
3: begin send_dat(8'h00); spi_active_led<=1'b1; end // X start = 0
4: begin send_dat((BYTES_PER_LINE-1)); spi_active_led<=1'b1; end // X end = 15
5: begin send_cmd(8'h45); spi_active_led<=1'b1; end // RAM Y start/end
6: begin send_dat(8'h00); spi_active_led<=1'b1; end // Y start LSB
7: begin send_dat(8'h00); spi_active_led<=1'b1; end // Y start MSB
8: begin send_dat(8'h27); spi_active_led<=1'b1; end // Y end LSB (295)
9: begin send_dat(8'h01); spi_active_led<=1'b1; end // Y end MSB
10: begin send_cmd(8'h4E); spi_active_led<=1'b1; end // Set X counter
11: begin send_dat(8'h00); spi_active_led<=1'b1; end
12: begin send_cmd(8'h4F); spi_active_led<=1'b1; end // Set Y counter
13: begin send_dat(8'h00); spi_active_led<=1'b1; end
14: begin send_dat(8'h00); spi_active_led<=1'b1; end
default: begin
byte_count <= 0;
state <= S_WRITE_RAM_CMD;
end
endcase
if (byte_count < 15) byte_count <= byte_count + 1;
end
end
// Escribir el comando de escritura de RAM 0x24
S_WRITE_RAM_CMD: begin
if (spi_tx_ready) begin
send_cmd(8'h24);
spi_active_led <= 1'b1;
byte_count <= 0;
total_bytes <= TOTAL_BYTES;
state <= S_WRITE_RAM;
end
end
// Escribir los 4736 bytes de imagen
S_WRITE_RAM: begin
if (spi_tx_done || (spi_tx_ready && byte_count == 0)) begin
if (byte_count < total_bytes) begin
if (spi_tx_ready) begin
send_dat(pattern_byte(byte_count));
spi_active_led <= 1'b1;
byte_count <= byte_count + 1;
end
end else begin
state <= S_UPDATE_0;
end
end
end
// Secuencia de actualización: 0x22 (Display Update Control 2) = 0xC7; 0x20 (Master Activate)
S_UPDATE_0: begin
if (spi_tx_ready) begin
send_cmd(8'h22);
spi_active_led <= 1'b1;
state <= S_UPDATE_1;
byte_count <= 0;
end
end
S_UPDATE_1: begin
if (spi_tx_done) begin
if (spi_tx_ready && byte_count == 0) begin
send_dat(8'hC7);
spi_active_led <= 1'b1;
byte_count <= 1;
end else if (spi_tx_ready && byte_count == 1) begin
send_cmd(8'h20);
spi_active_led <= 1'b1;
state <= S_WAIT_BUSY;
end
end
end
S_WAIT_BUSY: begin
// Espera a que el panel termine (BUSY=0, ajustar si es invertido)
if (epd_busy == 1'b0) begin
epd_cs_n <= 1'b1; // deselecciona
done <= 1'b1;
state <= S_DONE;
end
end
S_DONE: begin
// Estado final; aquí podrías entrar en deep sleep: cmd 0x10, data 0x01
// Ejemplo: enviar 0x10 (DEEP_SLEEP) si se desea
end
default: state <= S_BOOT_RESET;
endcase
end
end
endmodule
// top.v - Toplevel para DE10-Lite compatible con "Golden Top" (Terasic)
// Reutiliza nombres de puertos estándar para aprovechar el .qsf oficial.
module top (
input wire MAX10_CLK1_50,
input wire [1:0] KEY,
input wire [9:0] SW,
output wire [9:0] LEDR,
inout wire [15:0] ARDUINO_IO
);
wire clk = MAX10_CLK1_50;
wire rst = ~KEY[0]; // KEY[0] pulsador activo en bajo => reset activo en alto
// Señales EPD mapeadas a ARDUINO_IO
wire epd_sck, epd_mosi, epd_cs_n, epd_dc, epd_rst_n;
wire epd_busy;
// Mapas de pines hacia ARDUINO_IO (asumen Golden Top con ARDUINO_IO[13..7] en D13..D7)
assign ARDUINO_IO[13] = epd_sck; // D13 SCK
assign ARDUINO_IO[11] = epd_mosi; // D11 MOSI
assign ARDUINO_IO[10] = epd_cs_n; // D10 CS#
assign ARDUINO_IO[9] = epd_dc; // D9 D/C
assign ARDUINO_IO[8] = epd_rst_n; // D8 RST#
// BUSY como entrada
assign ARDUINO_IO[7] = 1'bz; // liberar línea como entrada
assign epd_busy = ARDUINO_IO[7];
// Resto de ARDUINO_IO en alta impedancia para evitar conflictos
assign ARDUINO_IO[12] = 1'bz;
assign ARDUINO_IO[6:0]= 7'h7F; // si es bus bidireccional, mejor dejarlos 'z'; aquí por simplicidad
// Nota: en síntesis real, conviene asignar 'z' a todos los no usados:
// assign ARDUINO_IO[6:0] = 7'bzzzzzzz;
// assign ARDUINO_IO[15:14] = 2'bzz;
wire done, spi_led;
wire [9:0] leds;
epd_driver #(
.CLKS_PER_MS(50000),
.SPI_CLKS_PER_HALF_BIT(25)
) u_epd (
.clk(clk),
.rst(rst),
.epd_busy(epd_busy),
.epd_sck(epd_sck),
.epd_mosi(epd_mosi),
.epd_cs_n(epd_cs_n),
.epd_dc(epd_dc),
.epd_rst_n(epd_rst_n),
.done(done),
.spi_active_led(spi_led)
);
// Indicadores en LEDs
// LEDR[0] = done; LEDR[1] = busy; LEDR[2] = spi actividad; Resto: estado de SW (para verificar IO)
assign LEDR[0] = done;
assign LEDR[1] = epd_busy;
assign LEDR[2] = spi_led;
assign LEDR[9:3] = SW[9:3];
endmodule
Breve explicación de partes clave:
– spi_master: genera SCK y MOSI con un divisor que produce ~1 MHz desde 50 MHz. Implementa un handshake simple (tx_valid/tx_ready/tx_done) por byte. CS es responsabilidad del controlador superior.
– epd_driver: aporta la FSM de alto nivel que:
1) Resetea el panel (RST# bajo 10 ms), libera reset, espera.
2) Envía comandos mínimos de inicialización para una pantalla 296×128 (GDEH029A1).
3) Define la ventana y los contadores RAM.
4) Carga un patrón de imagen sintetizado (4736 bytes) vía 0x24.
5) Lanza la actualización (0x22=0xC7; 0x20) y espera BUSY=0.
6) Señaliza done=1.
– top: utiliza nombres de puertos compatibles con el Golden Top de Terasic (“MAX10_CLK1_50”, “KEY”, “SW”, “LEDR”, “ARDUINO_IO”), lo que simplifica reaprovechar el archivo .qsf con mapeo de pines ya verificado. Mapea las señales a Arduino D13, D11, D10, D9, D8, D7 para e‑Paper.
Compilación, programación y ejecución
Estructura de proyecto y archivos
Propón un workspace con esta jerarquía:
- spi_epaper_de10lite/
- rtl/
- top.v
- spi_master.v
- epd_driver.v
- scripts/
- create_project.tcl
- constraints/
- de10_lite_golden_top.qsf (pinout oficial de Terasic; ver pasos)
- output_files/ (generado por Quartus)
Obtener y usar las asignaciones de pines del Golden Top
- Descarga desde Terasic el paquete “DE10‑Lite System CD” (versión reciente; por ejemplo, v1.3.x).
- Dentro encontrarás el proyecto “DE10_LITE_Golden_Top”, que contiene un .qsf con asignaciones para:
- MAX10_CLK1_50
- LEDR[9:0], KEY[1:0], SW[9:0]
- ARDUINO_IO[15:0]
- Copia ese .qsf a constraints/de10_lite_golden_top.qsf en tu proyecto.
Observación: Usamos exactamente los mismos nombres de puertos en top.v para que el .qsf del Golden Top sea aplicable sin cambios. Así, no necesitas deducir pines físicos (encapsulado F484) ni re-crear el pinout.
Script Tcl para crear y compilar el proyecto
Archivo: scripts/create_project.tcl
# create_project.tcl — Quartus Prime Lite 22.1std.2
set proj_name "spi_epaper_de10lite"
set top_name "top"
project_new $proj_name -overwrite
# Dispositivo: MAX 10 10M50DAF484C7G (DE10-Lite)
set_global_assignment -name FAMILY "MAX 10"
set_global_assignment -name DEVICE 10M50DAF484C7G
# Fuentes RTL
set_global_assignment -name TOP_LEVEL_ENTITY $top_name
set_global_assignment -name VERILOG_FILE rtl/top.v
set_global_assignment -name VERILOG_FILE rtl/spi_master.v
set_global_assignment -name VERILOG_FILE rtl/epd_driver.v
# Importa el .qsf de Terasic para pines (debe existir en constraints/)
# Opción 1: 'source' no es estándar en .qsf, así que incluimos su contenido.
# Opción 2 (recomendada): Añadir sus líneas aquí. Para automatizar, lo anexamos:
set pin_qsf [open "constraints/de10_lite_golden_top.qsf" r]
set pin_lines [read $pin_qsf]
close $pin_qsf
# Escribe líneas de pinout al .qsf del proyecto
set qsf_file "$proj_name.qsf"
set qsf [open $qsf_file a]
puts $qsf $pin_lines
close $qsf
# Opciones de compilación
set_global_assignment -name OPTIMIZATION_MODE "AGGRESSIVE PERFORMANCE"
set_global_assignment -name DEVICE_FILTER_PACKAGE F484
set_global_assignment -name DEVICE_FILTER_SPEED_GRADE 7
set_global_assignment -name NUM_PARALLEL_PROCESSORS ALL
# Compila
execute_flow -compile
Notas:
– Este Tcl concatena el contenido del .qsf de Terasic al .qsf del proyecto. Alternativamente, copia manualmente las líneas de pinout al .qsf principal y revisa que los nombres de puertos coincidan exactamente.
– Asegúrate de que el .qsf original esté actualizado para la versión de placa que tienes.
Comandos exactos (Ubuntu 22.04.4 LTS)
1) Instala reglas udev para USB-Blaster II (si no las tienes):
– Crea /etc/udev/rules.d/51-usbblaster.rules con contenido oficial de Intel (consulta documentación).
– Recarga udev:
– sudo udevadm control –reload-rules
– sudo udevadm trigger
2) Verifica que Quartus está en PATH (ajusta ruta según instalación):
– export QUARTUS_ROOTDIR=»/opt/intelFPGA_lite/22.1std/quartus»
– export PATH=»$QUARTUS_ROOTDIR/bin:$PATH»
3) Clona/crea el proyecto y archivos (o copia los .v y .tcl como se describió):
– mkdir -p spi_epaper_de10lite/{rtl,constraints,scripts}
– Copia top.v, spi_master.v, epd_driver.v en rtl/.
– Copia create_project.tcl en scripts/.
– Copia de10_lite_golden_top.qsf en constraints/.
4) Ejecuta la creación y compilación:
– cd spi_epaper_de10lite
– quartus_sh -t scripts/create_project.tcl
5) Programa la FPGA con el .sof generado:
– Conecta la DE10‑Lite por USB y enciéndela.
– Lista dispositivos JTAG:
– jtagconfig
– Programa:
– quartus_pgm -m jtag -o «p;output_files/spi_epaper_de10lite.sof»
6) (Opcional) Convertir a .pof para configurar en la memoria de configuración (MAX 10):
– quartus_cpf -c output_files/spi_epaper_de10lite.sof output_files/spi_epaper_de10lite.pof
– Programa la flash interna:
– quartus_pgm -m jtag -o «p;output_files/spi_epaper_de10lite.pof»
Observaciones:
– Para desarrollo iterativo, usa .sof por JTAG (rápido). Para “power‑up” autónomo, usa .pof.
Validación paso a paso
Verificación visual en la placa
- LEDR[1] debe reflejar el estado de BUSY (encendido durante la actualización y apagado al terminar).
- LEDR[2] parpadeará brevemente durante los envíos SPI (actividad).
- LEDR[0] se encenderá al completarse la primera actualización del panel (done=1).
Señales en el panel
- Al programar:
1) El panel debe hacer un “flash” de inicialización (típico de e‑Paper al reset/update).
2) Tras ~1–3 s, debe mostrarse un patrón de rayas verticales alternas (bloques de 64 bytes): zonas claras/oscura alternadas.
3) El panel quedará estático (e‑Paper retiene imagen sin energía).
Medidas con multímetro/oscopio
- VCC (3.3 V) del panel estable entre 3.25–3.35 V.
- Con osciloscopio o analizador:
- SCK en D13: frecuencia ≈ 1 MHz, duty ~50%.
- MOSI en D11: datos cambiando en flancos de bajada (CPHA=0).
- CS# en D10: bajo durante ráfagas de comandos/datos; alto en reposo.
- DC en D9: bajo en comandos (0x01,0x11,0x44,0x45,0x4E,0x4F,0x24,0x22,0x20) y alto durante la ráfaga de píxeles.
- RST# en D8: pulso bajo de ~10 ms al inicio.
- BUSY en D7: alto (ocupado) durante la actualización; transiciona a bajo al terminar (dependiendo de variante; invierte la lógica si observa lo contrario).
Confirmación funcional
- El patrón se ve sin “ghosting” severo. Si notas ghosting, repite la secuencia 0x22/0x20 (REFRESH) o añade clear pre‑fill (0x24 con 0xFF y luego con tu patrón).
- done=1 indica secuencia completada.
Troubleshooting (5–8 problemas típicos y soluciones)
1) No compilación por pines sin asignar
– Síntoma: Fitter error “Cannot place pin … no location assigned”.
– Causa: No se importó el .qsf del Golden Top o los nombres de puertos no coinciden.
– Solución:
– Asegúrate de que constraints/de10_lite_golden_top.qsf esté concatenado en el .qsf del proyecto.
– Verifica que tu top.v usa exactamente: MAX10_CLK1_50, KEY, SW, LEDR, ARDUINO_IO.
2) Programación JTAG falla (“No hardware detected”)
– Síntoma: jtagconfig no lista el dispositivo.
– Causa: Driver USB-Blaster II no cargado o cable no reconocido.
– Solución:
– En Linux: aplica reglas udev, reconecta el cable, ejecuta “sudo dmesg | tail”.
– En Windows: instala el driver desde el directorio drivers de Quartus.
3) El panel no actualiza (pantalla en blanco o fija)
– Síntoma: No hay cambios tras programar; LEDR[2] parpadea brevemente.
– Causas:
– Cableado incorrecto en DC/RST/CS o inversión de BUSY.
– Falta de 3.3 V o mala masa.
– Solución:
– Verifica tabla de cableado; mide 3.3 V entre VCC y GND.
– Invierte la lógica de BUSY si tu módulo usa la convención opuesta (si siempre ves ‘ocupado’, prueba epd_busy = ~ARDUINO_IO[7]).
– Aumenta retardos (ms_delay) a 100 ms en reset/pos‑reset.
4) SPI sin actividad en SCK/MOSI
– Síntoma: MOSI y SCK permanecen bajos en D11/D13.
– Causa: rst siempre activo (KEY[0] pulsado) o SPI divisor mal configurado.
– Solución:
– Libera KEY[0]; en la DE10‑Lite los KEY son pulsadores activos en bajo.
– Verifica SPI_CLKS_PER_HALF_BIT=25 (para 1 MHz con 50 MHz base).
5) Imagen invertida o patrón inesperado
– Síntoma: Patrón claro/oscuro invertido.
– Causa: Convención de bits del panel (1=blanco, 0=negro) vs. tu patrón.
– Solución:
– Cambia pattern_byte: usa ~pattern_byte o intercambia 0x00/0xFF.
6) Ghosting notable después de múltiples cargas
– Síntoma: Rastro de imágenes previas.
– Causa: e‑Paper requiere ciclos de borrado/refresh específicos.
– Solución:
– Antes de escribir tu imagen, llena RAM con 0xFF, ejecuta update, luego escribe tu imagen y actualiza de nuevo.
– Añade comando border waveform (0x3C) con 0x05 u otro según hoja de datos.
7) Bloqueo en S_WAIT_BUSY (nunca termina)
– Síntoma: LEDR[1] indica ocupado indefinidamente.
– Causas:
– RST o CS cableado mal.
– BUSY invertido.
– Solución:
– Revisa que CS# esté bajo durante envíos y suba al final.
– Prueba a invertir epd_busy (epd_busy = ~ARDUINO_IO[7]).
8) Ruido en líneas SPI (fallos intermitentes)
– Síntoma: Errores esporádicos, patrón corrupto.
– Causas:
– Cables largos o flojos, velocidad SPI excesiva.
– Solución:
– Reduce SPI a 500 kHz (CLKS_PER_HALF_BIT=50).
– Usa cables más cortos, mantén GND compartida cerca.
Mejoras y variantes
- Doble buffer y microcontrolador suave:
- Implementar un pequeño “microcódigo” ROM con secuencias de comandos/datos (compacta la FSM y facilita soporte de variantes v1/v2 del panel).
- Fuentes y texto:
- Integra una ROM de fuente monocroma y un rasterizador simple para escribir “Hello, FPGA!” en la pantalla.
- Imágenes desde SDRAM/Flash:
- Agrega un controlador para cargar bitmaps desde la memoria de configuración interna del MAX 10 o desde EPCQ.
- Modos de refresco parcial:
- Extiende la FSM con 0x24/0x26 (memoria negra/roja si el panel fuera tri‑color) y region-of-interest para actualizaciones parciales.
- Interfaz de alto nivel:
- Diseña un bus interno (p. ej., Avalon‑MM simplificado) para enviar comandos/bytes desde un soft‑core o desde un puerto UART.
- Timing adaptativo:
- Mide y ajusta los delays según temperatura (los e‑Papers varían con Tª).
Checklist de verificación
Marca cada punto al completarlo:
- [ ] Instalé Quartus Prime Lite 22.1std.2 y verifiqué “quartus_sh –version”.
- [ ] Tengo el archivo de pines del Golden Top de DE10‑Lite en constraints/de10_lite_golden_top.qsf.
- [ ] Creé la estructura del proyecto y copié rtl/top.v, rtl/spi_master.v, rtl/epd_driver.v.
- [ ] Revisé que los puertos toplevel coinciden exactamente con los del Golden Top: MAX10_CLK1_50, KEY, SW, LEDR, ARDUINO_IO.
- [ ] Cableé el e‑Paper a la cabecera Arduino: VCC=3V3, GND=GND, SCK=D13, MOSI=D11, CS=D10, DC=D9, RST=D8, BUSY=D7.
- [ ] Compilé con “quartus_sh -t scripts/create_project.tcl” sin errores de Fitter.
- [ ] Programé la FPGA con “quartus_pgm -m jtag -o ‘p;output_files/spi_epaper_de10lite.sof’”.
- [ ] Observé actividad en SCK/MOSI/CS/DC y RST con el osciloscopio/analizador.
- [ ] El panel mostró el patrón y LEDR[0] se encendió al terminar.
- [ ] Si hubo problemas, revisé Troubleshooting (BUSY invertido, delays, cables).
Comentarios finales avanzados
- Integridad de señal: Aunque 1 MHz es bajo para SPI, recuerda mantener retornos de GND cercanos y evitar lazos amplios. Si escalas a 8–10 MHz, cuida terminaciones y longitudes.
- Reutilización de .qsf: Adoptar los nombres de puerto del Golden Top es una práctica eficaz para acelerar prototipos con placas Terasic. Para producción, conviene consolidar un .qsf depurado solo con lo que tu diseño usa.
- Portabilidad: El driver EPD implementado aquí es deliberadamente sencillo (solo una imagen con patrón). Para soportar variantes de 2.9″ (V2 o tri‑color) deberás ajustar comandos (p. ej., 0x12 soft reset, 0x3C border, secuencias de power-on).
- Consumo: El e‑Paper consume durante las transiciones y casi nada en reposo. El MAX 10 puede entrar en modo de bajo consumo si reduces el reloj tras completar la actualización.
Con este caso práctico, has construido una ruta completa: diseño HDL → asignación de pines coherente con la placa → programación con toolchain exacta → validación física sobre un periférico SPI real (un e‑Paper). Esto sienta bases para controladores más complejos (pantallas mayores, buses de mayor velocidad, o agregados como memoria externa, microcontroladores suaves y pipelines de procesamiento de imagen).
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.



