You dont have javascript enabled! Please enable it!

Caso práctico: probador de servos SG90 con ULX3S

Caso práctico: probador de servos SG90 con ULX3S — hero

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

Botones ULX3S

Sincronizador/antirrebote

Selector de modo

Generador periodo 20 ms

Comparador ancho de pulso

Salida PWM 50 Hz

Servo SG90

Flujo conceptual de control: entrada de botones, selección de modo, temporización PWM y movimiento del servo.

Ruta de validación

Verilog fuente

Verilator lint/testbench

Yosys síntesis

nextpnr-ecp5

ecppack bitstream

ULX3S programada

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:
  • verilator
  • yosys
  • nextpnr-ecp5
  • ecppack
  • openFPGALoader

Materiales

ElementoModelo exactoCantidadNotas
Placa FPGARadiona ULX3S (Lattice ECP5-85F)1Placa objetivo
ServoMicro servo SG901Servo hobby de 3 hilos
Fuente del servoFuente externa de 5 V para servo1Debe soportar picos de corriente del servo
Cable USBCable USB compatible con ULX3S1Alimentación y programación de la placa
Cables jumperCables jumper adecuadosVariosCableado de señal y tierra
Osciloscopio o analizador lógicoCualquier modelo básicoOpcional pero recomendadoPara 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

  1. Alimenta la ULX3S desde USB.
  2. Alimenta el servo desde la fuente externa de 5 V.
  3. Conecta la tierra de la fuente externa de 5 V a una tierra de la ULX3S.
  4. Conecta el pin de salida de la FPGA servo_pwm al 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 central
  • btn_min: posición mínima
  • btn_max: posición máxima
  • btn_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 ...

🔒 Parte del código validado es premium. Con el pase de 7 días o la suscripción mensual podrás consultar el archivo 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 ...

🔒 Parte del código validado es premium. Con el pase de 7 días o la suscripción mensual podrás consultar el archivo 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_n
  • btn_min_n
  • btn_max_n
  • btn_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:

  1. apaga la fuente del servo
  2. conecta la señal del servo a servo_pwm
  3. conecta la tierra del servo a la tierra de la fuente
  4. conecta la tierra de la fuente a la tierra de la ULX3S
  5. enciende la ULX3S
  6. 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:

  • CLK25
  • BTN1
  • BTN2
  • BTN3
  • BTN4
  • GPIO0

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>

Quiz rápido

Pregunta 1: ¿Qué placa FPGA se utiliza en el proyecto descrito?




Pregunta 2: ¿Qué modelo de servo se menciona para ser controlado en este proyecto?




Pregunta 3: ¿Cuál es la duración nominal de la trama PWM generada por la FPGA?




Pregunta 4: ¿Con qué voltaje se alimenta el micro servo SG90 según el texto?




Pregunta 5: ¿Cuántos botones se utilizan para controlar las funciones del probador de servos?




Pregunta 6: ¿Cuál es el ancho de pulso aproximado para la posición central del servo?




Pregunta 7: ¿Qué función realiza el modo de barrido automático?




Pregunta 8: ¿Cuál es la frecuencia de actualización aproximada de la señal PWM entregada?




Pregunta 9: ¿Qué instrumento se sugiere para verificar los anchos de pulso de la señal PWM?




Pregunta 10: ¿Cuál es uno de los propósitos educativos de este proyecto?




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