Objetivo y caso de uso
Qué construirás: Un probador de servos de banco con salida PWM en la Radiona ULX3S (Lattice ECP5-85F) para controlar un micro servo SG90 alimentado con una fuente externa de 5 V. La FPGA generará una trama nominal de 20 ms y, mediante cuatro botones, podrás seleccionar posición central, mínima, máxima o un barrido automático entre pulsos de 1.0 ms a 2.0 ms.
Para qué sirve
- Validar rápidamente servos hobby SG90 sin necesidad de un microcontrolador adicional.
- Comprobar en banco señales PWM de control con período cercano a 20 ms y latencia de respuesta de 1 trama (~20 ms).
- Practicar diseño digital en FPGA con una carga real, de bajo consumo y uso de GPU del 0%.
- Verificar con osciloscopio o analizador lógico que los anchos de pulso cambian entre ~1.0 ms, ~1.5 ms y ~2.0 ms según el botón pulsado.
Resultado esperado
- La ULX3S entrega una señal PWM estable para servo con frecuencia de actualización aproximada de 50 Hz.
- Los botones integrados seleccionan centro, mínimo, máximo y modo de barrido de forma reproducible.
- El barrido modifica el ancho de pulso progresivamente a lo largo de tramas repetidas, observándose movimiento continuo del servo.
- El flujo completo pasa lint con Verilator, síntesis con Yosys, place-and-route con nextpnr-ecp5, bitstream con ecppack y programación con openFPGALoader.
Público objetivo: estudiantes, makers y perfiles de electrónica digital/FPGA que quieran probar servos hobby; Nivel: inicial–intermedio
Arquitectura/flujo: botones integrados → lógica de selección de modo y temporización en FPGA → generador PWM de 20 ms con pulsos de ~1.0/1.5/2.0 ms o barrido → pin de salida hacia señal de control del SG90; alimentación del servo desde 5 V externa con masa común; validación en hardware midiendo período, latencia de 20 ms y ancho de pulso.
Diagrama de bloques conceptual
Vista de alto nivel: qué entra, qué procesa cada bloque y qué sale del sistema.
Arquitectura funcional
Flujo conceptual de control: entrada de botones, selección de modo, temporización PWM y movimiento del servo.
Ruta de validación
La validación automática comprueba sintaxis, simulación/lint y compatibilidad con la toolchain ULX3S/ECP5.
Prerrequisitos
Nota educativa de validación
Antes de publicar este caso, el contenido pasó la puerta automática de validación de Prometeo con estado PASS. Para este perfil FPGA/ULX3S, los bloques de Verilog sintetizable se comprobaron con Yosys (read_verilog) y el conjunto Verilog de diseño/test se revisó con Verilator. El validador también comprobó la estructura de los bloques de código, que los comandos usen opciones copiables con guiones ASCII, que no aparezcan stacks no soportados y que esté disponible la toolchain ULX3S/ECP5 (yosys, nextpnr-ecp5, ecppack, openFPGALoader).
Esta validación confirma compatibilidad sintáctica y de herramientas para el código publicado, pero no sustituye la prueba física sobre tu revisión exacta de ULX3S, tu archivo de restricciones de pines y tu cableado real.
Necesitas:
- una Radiona ULX3S (Lattice ECP5-85F)
- un micro servo SG90
- una fuente externa de 5 V para el servo
- conexión USB para alimentación y programación de la ULX3S
- herramientas instaladas:
verilatoryosysnextpnr-ecp5ecppackopenFPGALoader
Materiales
| Elemento | Modelo exacto | Cantidad | Notas |
|---|---|---|---|
| Placa FPGA | Radiona ULX3S (Lattice ECP5-85F) | 1 | Placa objetivo |
| Servo | Micro servo SG90 | 1 | Servo hobby de 3 hilos |
| Fuente del servo | Fuente externa de 5 V para servo | 1 | Debe soportar picos de corriente del servo |
| Cable USB | Cable USB compatible con ULX3S | 1 | Alimentación y programación de la placa |
| Cables jumper | Cables jumper adecuados | Varios | Cableado de señal y tierra |
| Osciloscopio o analizador lógico | Cualquier modelo básico | Opcional pero recomendado | Para validación de la forma de onda |
Cableado
Cables del servo
Colores típicos de los cables del SG90:
- marrón/negro: GND
- rojo: +5 V
- naranja/amarillo/blanco: señal de control
Conexiones
- Alimenta la ULX3S desde USB.
- Alimenta el servo desde la fuente externa de 5 V.
- Conecta la tierra de la fuente externa de 5 V a una tierra de la ULX3S.
- Conecta el pin de salida de la FPGA
servo_pwmal cable de señal del servo.
Nota de seguridad visible
Nota de seguridad educativa
Este proyecto acciona un actuador en movimiento desde una fuente de alimentación externa.
- Mantén los dedos y los cables sueltos alejados del brazo del servo mientras esté alimentado.
- No alimentes el servo desde un pin de E/S de la FPGA.
- No conectes 5 V directamente a ninguna E/S de la ULX3S.
- Conecta siempre las tierras en común para que la señal tenga una referencia válida.
- Si el servo se bloquea, vibra ruidosamente o se calienta, apágalo e inspecciona el mecanismo.
Asignación de botones
Este tutorial usa cuatro entradas de botón:
btn_center: posición centralbtn_min: posición mínimabtn_max: posición máximabtn_sweep: modo de barrido
Si no se presiona ningún botón, el diseño usa por defecto la posición central.
Código fuente
Archivo: src/servo_tester.v
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
`timescale 1ns/1ps
module servo_tester #(
parameter integer CLK_HZ = 25000000,
parameter integer FRAME_HZ = 50,
parameter integer PULSE_MIN_US = 1000,
parameter integer PULSE_CENTER_US = 1500,
parameter integer PULSE_MAX_US = 2000,
parameter integer SWEEP_STEP_US = 10,
parameter integer SWEEP_UPDATE_MS = 20
) (
input wire clk,
input wire btn_center,
input wire btn_min,
input wire btn_max,
input wire btn_sweep,
output reg servo_pwm
);
localparam integer US_TICKS = CLK_HZ / 1000000;
localparam integer FRAME_TICKS = CLK_HZ / FRAME_HZ;
localparam integer PULSE_MIN_TICKS = PULSE_MIN_US * US_TICKS;
localparam integer PULSE_CENTER_TICKS = PULSE_CENTER_US * US_TICKS;
localparam integer PULSE_MAX_TICKS = PULSE_MAX_US * US_TICKS;
localparam integer SWEEP_STEP_TICKS = SWEEP_STEP_US * US_TICKS;
localparam integer SWEEP_UPDATE_TICKS = (CLK_HZ / 1000) * SWEEP_UPDATE_MS;
reg [31:0] frame_counter = 32'd0;
reg [31:0] pulse_ticks = PULSE_CENTER_TICKS;
reg [31:0] sweep_counter = 32'd0;
reg [31:0] sweep_ticks = PULSE_CENTER_TICKS;
reg sweep_dir_up = 1'b1;
always @(posedge clk) begin
if (btn_min) begin
pulse_ticks <= PULSE_MIN_TICKS;
sweep_counter <= 32'd0;
sweep_ticks <= PULSE_CENTER_TICKS;
sweep_dir_up <= 1'b1;
end else if (btn_center) begin
pulse_ticks <= PULSE_CENTER_TICKS;
sweep_counter <= 32'd0;
sweep_ticks <= PULSE_CENTER_TICKS;
sweep_dir_up <= 1'b1;
end else if (btn_max) begin
pulse_ticks <= PULSE_MAX_TICKS;
sweep_counter <= 32'd0;
// ... continúa para miembros en el código completo validado ...`timescale 1ns/1ps
module servo_tester #(
parameter integer CLK_HZ = 25000000,
parameter integer FRAME_HZ = 50,
parameter integer PULSE_MIN_US = 1000,
parameter integer PULSE_CENTER_US = 1500,
parameter integer PULSE_MAX_US = 2000,
parameter integer SWEEP_STEP_US = 10,
parameter integer SWEEP_UPDATE_MS = 20
) (
input wire clk,
input wire btn_center,
input wire btn_min,
input wire btn_max,
input wire btn_sweep,
output reg servo_pwm
);
localparam integer US_TICKS = CLK_HZ / 1000000;
localparam integer FRAME_TICKS = CLK_HZ / FRAME_HZ;
localparam integer PULSE_MIN_TICKS = PULSE_MIN_US * US_TICKS;
localparam integer PULSE_CENTER_TICKS = PULSE_CENTER_US * US_TICKS;
localparam integer PULSE_MAX_TICKS = PULSE_MAX_US * US_TICKS;
localparam integer SWEEP_STEP_TICKS = SWEEP_STEP_US * US_TICKS;
localparam integer SWEEP_UPDATE_TICKS = (CLK_HZ / 1000) * SWEEP_UPDATE_MS;
reg [31:0] frame_counter = 32'd0;
reg [31:0] pulse_ticks = PULSE_CENTER_TICKS;
reg [31:0] sweep_counter = 32'd0;
reg [31:0] sweep_ticks = PULSE_CENTER_TICKS;
reg sweep_dir_up = 1'b1;
always @(posedge clk) begin
if (btn_min) begin
pulse_ticks <= PULSE_MIN_TICKS;
sweep_counter <= 32'd0;
sweep_ticks <= PULSE_CENTER_TICKS;
sweep_dir_up <= 1'b1;
end else if (btn_center) begin
pulse_ticks <= PULSE_CENTER_TICKS;
sweep_counter <= 32'd0;
sweep_ticks <= PULSE_CENTER_TICKS;
sweep_dir_up <= 1'b1;
end else if (btn_max) begin
pulse_ticks <= PULSE_MAX_TICKS;
sweep_counter <= 32'd0;
sweep_ticks <= PULSE_CENTER_TICKS;
sweep_dir_up <= 1'b1;
end else if (btn_sweep) begin
pulse_ticks <= sweep_ticks;
if (sweep_counter >= (SWEEP_UPDATE_TICKS - 1)) begin
sweep_counter <= 32'd0;
if (sweep_dir_up) begin
if (sweep_ticks >= (PULSE_MAX_TICKS - SWEEP_STEP_TICKS)) begin
sweep_ticks <= PULSE_MAX_TICKS;
sweep_dir_up <= 1'b0;
end else begin
sweep_ticks <= sweep_ticks + SWEEP_STEP_TICKS;
end
end else begin
if (sweep_ticks <= (PULSE_MIN_TICKS + SWEEP_STEP_TICKS)) begin
sweep_ticks <= PULSE_MIN_TICKS;
sweep_dir_up <= 1'b1;
end else begin
sweep_ticks <= sweep_ticks - SWEEP_STEP_TICKS;
end
end
end else begin
sweep_counter <= sweep_counter + 32'd1;
end
end else begin
pulse_ticks <= PULSE_CENTER_TICKS;
sweep_counter <= 32'd0;
sweep_ticks <= PULSE_CENTER_TICKS;
sweep_dir_up <= 1'b1;
end
if (frame_counter >= (FRAME_TICKS - 1)) begin
frame_counter <= 32'd0;
end else begin
frame_counter <= frame_counter + 32'd1;
end
if (frame_counter < pulse_ticks) begin
servo_pwm <= 1'b1;
end else begin
servo_pwm <= 1'b0;
end
end
endmodule
Archivo: tb/servo_tester_tb.v
Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.
`timescale 1ns/1ps
module servo_tester_tb;
reg clk = 1'b0;
reg btn_center = 1'b0;
reg btn_min = 1'b0;
reg btn_max = 1'b0;
reg btn_sweep = 1'b0;
wire servo_pwm;
integer high_count;
integer i;
servo_tester #(
.CLK_HZ(1000000),
.FRAME_HZ(50),
.PULSE_MIN_US(1000),
.PULSE_CENTER_US(1500),
.PULSE_MAX_US(2000),
.SWEEP_STEP_US(100),
.SWEEP_UPDATE_MS(20)
) dut (
.clk(clk),
.btn_center(btn_center),
.btn_min(btn_min),
.btn_max(btn_max),
.btn_sweep(btn_sweep),
.servo_pwm(servo_pwm)
);
always #500 clk = ~clk;
task automatic measure_one_frame;
begin
while (servo_pwm !== 1'b1) begin
@(posedge clk);
end
high_count = 0;
// ... continúa para miembros en el código completo validado ...`timescale 1ns/1ps
module servo_tester_tb;
reg clk = 1'b0;
reg btn_center = 1'b0;
reg btn_min = 1'b0;
reg btn_max = 1'b0;
reg btn_sweep = 1'b0;
wire servo_pwm;
integer high_count;
integer i;
servo_tester #(
.CLK_HZ(1000000),
.FRAME_HZ(50),
.PULSE_MIN_US(1000),
.PULSE_CENTER_US(1500),
.PULSE_MAX_US(2000),
.SWEEP_STEP_US(100),
.SWEEP_UPDATE_MS(20)
) dut (
.clk(clk),
.btn_center(btn_center),
.btn_min(btn_min),
.btn_max(btn_max),
.btn_sweep(btn_sweep),
.servo_pwm(servo_pwm)
);
always #500 clk = ~clk;
task automatic measure_one_frame;
begin
while (servo_pwm !== 1'b1) begin
@(posedge clk);
end
high_count = 0;
while (servo_pwm === 1'b1) begin
@(posedge clk);
high_count = high_count + 1;
end
$display("Measured high ticks: %0d", high_count);
end
endtask
initial begin
$display("Starting servo_tester_tb");
btn_center = 1'b1;
repeat (3) begin
measure_one_frame();
end
btn_center = 1'b0;
btn_min = 1'b1;
repeat (3) begin
measure_one_frame();
end
btn_min = 1'b0;
btn_max = 1'b1;
repeat (3) begin
measure_one_frame();
end
btn_max = 1'b0;
btn_sweep = 1'b1;
for (i = 0; i < 8; i = i + 1) begin
measure_one_frame();
end
btn_sweep = 1'b0;
$display("Testbench complete");
$finish;
end
endmodule
Archivo: constraints/ulx3s_servo.lpf
Usa nombres de sitio ULX3S válidos para la revisión exacta de tu placa.
BLOCK RESETPATHS;
BLOCK ASYNCPATHS;
FREQUENCY PORT "clk" 25.0 MHz;
LOCATE COMP "clk" SITE "CLK25";
IOBUF PORT "clk" IO_TYPE=LVCMOS33;
LOCATE COMP "btn_center" SITE "BTN1";
IOBUF PORT "btn_center" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "btn_min" SITE "BTN2";
IOBUF PORT "btn_min" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "btn_max" SITE "BTN3";
IOBUF PORT "btn_max" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "btn_sweep" SITE "BTN4";
IOBUF PORT "btn_sweep" IO_TYPE=LVCMOS33 PULLMODE=UP;
LOCATE COMP "servo_pwm" SITE "GPIO0";
IOBUF PORT "servo_pwm" IO_TYPE=LVCMOS33 DRIVE=4;
Envoltorio para botones activos en bajo
Muchas entradas de botón de la ULX3S son activas en bajo. Si el cableado de tu placa requiere inversión, usa un envoltorio de nivel superior separado para la síntesis.
Archivo: src/servo_tester_active_low.v
`timescale 1ns/1ps
module servo_tester_active_low (
input wire clk,
input wire btn_center_n,
input wire btn_min_n,
input wire btn_max_n,
input wire btn_sweep_n,
output wire servo_pwm
);
wire btn_center = ~btn_center_n;
wire btn_min = ~btn_min_n;
wire btn_max = ~btn_max_n;
wire btn_sweep = ~btn_sweep_n;
servo_tester u_servo_tester (
.clk(clk),
.btn_center(btn_center),
.btn_min(btn_min),
.btn_max(btn_max),
.btn_sweep(btn_sweep),
.servo_pwm(servo_pwm)
);
endmodule
Si usas este envoltorio, actualiza el nombre del módulo superior en el comando de síntesis y renombra los puertos del LPF para que coincidan:
btn_center_nbtn_min_nbtn_max_nbtn_sweep_n
Estructura del proyecto
project/
├── build/
├── constraints/
│ └── ulx3s_servo.lpf
├── src/
│ ├── servo_tester.v
│ └── servo_tester_active_low.v
└── tb/
└── servo_tester_tb.v
Compilar y programar
1. Crear el directorio de compilación
mkdir -p build
2. Ejecutar lint de Verilator
verilator --lint-only -Wall -Wno-DECLFILENAME src/servo_tester.v tb/servo_tester_tb.v
Si sintetizas el envoltorio activo en bajo, puedes ejecutar lint sobre ambos archivos fuente:
verilator --lint-only -Wall -Wno-DECLFILENAME src/servo_tester.v src/servo_tester_active_low.v tb/servo_tester_tb.v
3. Sintetizar con Yosys
Para el módulo superior directo:
yosys -p "read_verilog src/servo_tester.v; synth_ecp5 -top servo_tester -json build/servo_tester.json"
Para el módulo superior con envoltorio activo en bajo:
yosys -p "read_verilog src/servo_tester.v src/servo_tester_active_low.v; synth_ecp5 -top servo_tester_active_low -json build/servo_tester.json"
4. Place and route
nextpnr-ecp5 --85k --package CABGA381 --json build/servo_tester.json --lpf constraints/ulx3s_servo.lpf --textcfg build/servo_tester.config
5. Empaquetar el bitstream
ecppack build/servo_tester.config build/servo_tester.bit
6. Detectar el programador
openFPGALoader --detect
7. Programar la ULX3S
openFPGALoader -b ulx3s build/servo_tester.bit
Método de validación
Este proyecto hace afirmaciones de temporización medibles, así que valídalas directamente.
1. Evidencia de la cadena de herramientas
Evidencia esperada:
- Verilator termina sin errores fatales
- Yosys escribe
build/servo_tester.json - nextpnr-ecp5 termina correctamente
- ecppack escribe
build/servo_tester.bit - openFPGALoader programa la placa
2. Evidencia de simulación
El testbench se ejecuta a 1 MHz, así que cada tick en alto equivale a 1 us.
Evidencia esperada de la salida de $display:
- modo centro: aproximadamente 1500 ticks
- modo mínimo: aproximadamente 1000 ticks
- modo máximo: aproximadamente 2000 ticks
- modo de barrido: valores que cambian entre mediciones repetidas
3. Evidencia de forma de onda en hardware
Antes de conectar el servo, mide servo_pwm con un osciloscopio o analizador lógico.
Evidencia esperada:
- período de trama cercano a 20 ms
- ancho de pulso cercano a:
- 1.0 ms para mínimo
- 1.5 ms para centro
- 2.0 ms para máximo
- en modo de barrido, el ancho de pulso cambia con el tiempo
4. Evidencia funcional del servo
Después de validar la forma de onda:
- apaga la fuente del servo
- conecta la señal del servo a
servo_pwm - conecta la tierra del servo a la tierra de la fuente
- conecta la tierra de la fuente a la tierra de la ULX3S
- enciende la ULX3S
- enciende la fuente externa de 5 V del servo
Evidencia esperada:
- el botón de centro mueve el servo a una posición media repetible
- mínimo y máximo mueven el servo hacia extremos opuestos
- el modo de barrido mueve el servo de un lado a otro
Solución de problemas
El servo no se mueve
Verifica:
- la fuente externa de 5 V está encendida
- el cable rojo del servo va a +5 V
- la tierra del servo está conectada
- la tierra de la ULX3S y la tierra de la fuente del servo están conectadas entre sí
- el pin de salida correcto de la FPGA está asignado en el LPF
- el bitstream realmente fue programado
El servo tiembla pero no sigue los comandos
Causas comunes:
- falta de tierra común
- mapeo de pines incorrecto en el LPF
- fuente de 5 V del servo débil o inestable
- discrepancia en la polaridad de los botones
nextpnr informa errores de sitio LPF
Los nombres de sitio del LPF deben coincidir con la revisión exacta de tu placa ULX3S. Actualiza:
CLK25BTN1BTN2BTN3BTN4GPIO0
a los nombres válidos según la documentación de tu placa.
Los modos parecen invertidos o bloqueados
Es probable que tus botones sean activos en bajo. Usa el envoltorio servo_tester_active_low y sintetiza ese módulo superior en su lugar.
Lista de verificación final
- [ ] Usé la Radiona ULX3S (Lattice ECP5-85F)
- [ ] Usé un micro servo SG90
- [ ] Alimenté el servo desde una fuente externa de 5 V
- [ ] Conecté la tierra de la fuente del servo a la tierra de la ULX3S
- [ ] Verifiqué la forma de onda PWM antes de conectar el servo
- [ ] El lint de Verilator pasó
- [ ] La síntesis con Yosys pasó
- [ ] El place-and-route con nextpnr-ecp5 pasó
- [ ] ecppack generó un bitstream
- [ ] openFPGALoader programó la placa
- [ ] Medí anchos de pulso de aproximadamente 1.0 ms, 1.5 ms y 2.0 ms para los modos esperados
Esto produce un probador de servos práctico basado en FPGA en la ULX3S ECP5-85F para validación rápida en banco de un servo SG90.
<div class="amazon-affiliate">
<p><strong>Encuentra este producto y/o libros sobre este tema en Amazon</strong></p>
<p><a class="amazon-affiliate-btn" href="https://amzn.to/4mt8r4C" target="_blank" rel="nofollow sponsored noopener">Ir a Amazon</a></p>
<p class="amazon-affiliate-disclaimer">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.</p>
</div>




