You dont have javascript enabled! Please enable it!

Caso práctico: logger de velocidad UGV con Raspberry Pi

Caso práctico: logger de velocidad UGV con Raspberry Pi — hero

Objetivo y caso de uso

Qué construirás: Una herramienta de registro de velocidad basada en Python para un UGV con Raspberry Pi 5 que realiza un barrido sistemático de la potencia del motor a través de un HAT PWM PCA9685 y un controlador TB6612FNG. Registra los pulsos (ticks) del codificador de alta frecuencia para caracterizar la respuesta del motor en bucle abierto y exporta los datos de rendimiento a un CSV.

Por qué es importante / Casos de uso

  • Caracterización de la banda muerta del motor: Identificar el ciclo de trabajo PWM mínimo exacto (por ejemplo, 12-15%) necesario para superar la fricción interna del motor e iniciar el movimiento físico.
  • Línea base del controlador PID: Establecer la curva de respuesta en bucle abierto y el límite máximo de RPM para ajustar con precisión los bucles proporcional-integral-derivativo (PID) para el control de trayectorias en línea recta.
  • Detección de deslizamiento de ruedas: Comparar la velocidad teórica con la retroalimentación real del codificador (pulsos/seg) para identificar la pérdida de tracción o la latencia en diferentes superficies.

Resultado esperado

  • Un archivo CSV generado que mapea los ciclos de trabajo PWM del 0 al 100% con las velocidades reales de las ruedas a una tasa de sondeo de 50-100 Hz.
  • Identificación del umbral de inicio PWM preciso y las RPM máximas para tu chasis UGV específico.
  • Una secuencia de calibración segura y automatizada diseñada para ejecutarse en un bloque de prueba suspendido para evitar una aceleración descontrolada.

Audiencia: Ingenieros en robótica y desarrolladores de Python que construyen vehículos autónomos; Nivel: Intermedio

Arquitectura/flujo: Script de Python → I2C (400kHz) a HAT PCA9685 → Controlador TB6612FNG → Motores DC → Interrupciones GPIO de codificadores de ruedas → Registro de datos CSV

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. El validador comprobó los bloques de código, la estructura del artículo, los comandos copiables y la coherencia con el catálogo de dispositivos soportados.

Evidencia de validación publicada

  • Resultado automático: PASS.
  • Estructura parseada: 3 apartados, 3 tablas y 6 bloques de código detectados antes de publicar.
  • Código comprobado: 2 Python/py_compile, 2 Bash/copy-paste checks.
  • Catálogo soportado: el texto se contrastó contra los perfiles de dispositivo validables de Prometeo y los stacks no soportados bloquean la publicación.
  • Hallazgos del informe: sin hallazgos bloqueantes.

Esta validación confirma compatibilidad sintáctica y de herramientas para el material publicado, pero no sustituye la prueba física sobre tu hardware, cableado y entorno exactos.

Nota educativa de seguridad

Este proyecto es un prototipo educativo, no un producto certificado. Antes de encender la configuración, verifica el esquema de pines de tu revisión exacta de la placa ULX3S, mantén las señales de E/S de la FPGA a 3.3 V, nunca conectes 5 V directamente a los pines de E/S, desconecta la alimentación antes de cambiar el cableado y utiliza fuentes externas adecuadas para cargas, motores o servos mientras compartes la tierra (GND) solo cuando el cableado lo requiera.

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.

Requisitos previos

  • Hardware: Un ordenador para escribir código y acceder por SSH a la Raspberry Pi.
  • SO: Raspberry Pi OS Bookworm (64 bits) instalado en la Raspberry Pi 5.
  • Software: Python 3.11.
  • Configuración: I2C habilitado en la Raspberry Pi (sudo raspi-config -> Interfacing Options -> I2C -> Enable).
  • Dependencias: Instala las bibliotecas de comunicación de hardware usando sudo apt-get install python3-smbus2 python3-rpi.gpio.

Materiales

  • Dispositivo objetivo: Raspberry Pi 5.
  • Control de motores: HAT PWM I2C PCA9685 y controlador de motor dual TB6612FNG.
  • Fuente de alimentación: Fuente de alimentación de 5V/5A para la Raspberry Pi 5, y un paquete de baterías apropiado (por ejemplo, LiPo 2S o 4xAA) para el controlador del motor (VMOT).
  • Sensores: Dos codificadores de ruedas estándar, ópticos o magnéticos, que emitan pulsos digitales.

Configuración y conexiones

Debido a que este tutorial se enfoca en un chasis UGV modular, las conexiones enlazan la Raspberry Pi 5, el HAT PWM I2C, el controlador del motor y los codificadores. Asegúrate de tener una tierra común en todos los componentes.

1. Control I2C y PWM (PCA9685)

Pin Raspberry Pi 5Pin PCA9685Descripción
Pin 1 (3.3V)VCCAlimentación lógica para el chip PWM
Pin 6 (GND)GNDTierra común
Pin 3 (GPIO 2 / SDA)SDADatos I2C
Pin 5 (GPIO 3 / SCL)SCLReloj I2C

2. Lógica del controlador de motor (TB6612FNG)

Componente de origenPin de origenPin TB6612FNGDescripción
PCA9685Canal 0PWMAVelocidad PWM del motor izquierdo
PCA9685Canal 1PWMBVelocidad PWM del motor derecho
RPi 5Pin 29 (GPIO 5)AIN1Avance del motor izquierdo
RPi 5Pin 31 (GPIO 6)AIN2Retroceso del motor izquierdo
RPi 5Pin 33 (GPIO 13)BIN1Avance del motor derecho
RPi 5Pin 35 (GPIO 19)BIN2Retroceso del motor derecho
RPi 5Pin 37 (GPIO 26)STBYEn espera / Habilitar (HIGH para funcionar)
Paquete de bateríasPositivoVMOTAlimentación del motor
Paquete de bateríasNegativoGNDTierra común (conectar a GND de RPi)

3. Codificadores de ruedas

Nota: Para el registro básico de velocidad, solo necesitamos la Fase A para contar pulsos. La Fase B se utiliza para la decodificación de cuadratura direccional, la cual se omite aquí.

Pin Raspberry Pi 5Pin del codificadorDescripción
Pin 17 (3.3V)VCC (Ambos)Alimentación lógica del codificador
Pin 39 (GND)GND (Ambos)Tierra común
Pin 11 (GPIO 17)Fase A izquierdaSalida de pulso de la rueda izquierda
Pin 13 (GPIO 27)Fase A derechaSalida de pulso de la rueda derecha

Implementación

La solución se divide en dos archivos. El primero es el script de registro principal que interactúa con el hardware (o lo simula). El segundo es un script de análisis para procesar los datos del CSV.

1. Script de registro principal

Guarda el siguiente código como ugv_speed_logger.py. Este script utiliza clases adaptadoras para permitir la ejecución en un PC estándar sin hardware pasando la bandera --dry-run.

#!/usr/bin/env python3
"""
UGV Wheel Encoder Speed Logger
Drives a dual-motor chassis through a PWM sweep and logs encoder RPM to a CSV.
Supports --dry-run for offline validation.
"""

import argparse
import csv
import time
import sys

# ---------------------------------------------------------
# Mock Adapters for Offline Validation
# ---------------------------------------------------------

class MockSMBus:
    def __init__(self, bus_number):
        self.bus_number = bus_number
        self.registers = {}
        print(f"[MOCK] Initialized SMBus {bus_number}")

    def write_byte_data(self, address, register, value):
        self.registers[(address, register)] = value

class MockGPIO:
    BCM = "BCM"
    IN = "IN"
    OUT = "OUT"
    RISING = "RISING"
    HIGH = 1
    LOW = 0

    def __init__(self):
        self.callbacks = {}
        self.pins = {}
        print("[MOCK] Initialized GPIO")

    def setmode(self, mode):
        pass

    def setup(self, pin, mode):
        self.pins[pin] = mode

    def output(self, pin, state):
        pass

    def add_event_detect(self, pin, edge, callback):
        self.callbacks[pin] = callback

    def cleanup(self):
        print("[MOCK] GPIO cleaned up")

    def simulate_tick(self, pin):
        if pin in self.callbacks:
            self.callbacks[pin](pin)

# ---------------------------------------------------------
# Device Drivers
# ---------------------------------------------------------

class PCA9685:
    def __init__(self, bus, address=0x40):
        self.bus = bus
        self.address = address
        # Basic PCA9685 initialization (50Hz)
        self.bus.write_byte_data(self.address, 0x00, 0x10) # Sleep
        self.bus.write_byte_data(self.address, 0xFE, 121)  # Prescale for ~50Hz
        self.bus.write_byte_data(self.address, 0x00, 0x00) # Wake
        time.sleep(0.005)
        self.bus.write_byte_data(self.address, 0x00, 0xA1) # Auto-increment

    def set_pwm(self, channel, duty_percent):
        duty_percent = max(0, min(100, duty_percent))
        off_val = int((duty_percent / 100.0) * 4095)
        reg = 0x06 + (channel * 4)
        self.bus.write_byte_data(self.address, reg, 0)
        self.bus.write_byte_data(self.address, reg+1, 0)
        self.bus.write_byte_data(self.address, reg+2, off_val & 0xFF)
        self.bus.write_byte_data(self.address, reg+3, off_val >> 8)

class ChassisController:
    def __init__(self, gpio, i2c_bus, is_dry_run=False):
        self.gpio = gpio
        self.pca = PCA9685(i2c_bus)
        self.is_dry_run = is_dry_run

        # TB6612FNG Pins
        self.AIN1 = 5
        self.AIN2 = 6
        self.BIN1 = 13
        self.BIN2 = 19
        self.STBY = 26

        self.gpio.setmode(self.gpio.BCM)
        for pin in [self.AIN1, self.AIN2, self.BIN1, self.BIN2, self.STBY]:
            self.gpio.setup(pin, self.gpio.OUT)
            self.gpio.output(pin, self.gpio.LOW)

        # Enable motor driver and set forward direction
        self.gpio.output(self.STBY, self.gpio.HIGH)
        self.gpio.output(self.AIN1, self.gpio.HIGH)
        self.gpio.output(self.AIN2, self.gpio.LOW)
        self.gpio.output(self.BIN1, self.gpio.HIGH)
        self.gpio.output(self.BIN2, self.gpio.LOW)

    def set_speed(self, duty_percent):
        self.pca.set_pwm(0, duty_percent) # Left
        self.pca.set_pwm(1, duty_percent) # Right

    def stop(self):
        self.set_speed(0)
        self.gpio.output(self.STBY, self.gpio.LOW)

class EncoderLogger:
    def __init__(self, gpio, left_pin=17, right_pin=27, ticks_per_rev=20):
        self.gpio = gpio
        self.left_pin = left_pin
        self.right_pin = right_pin
        self.ticks_per_rev = ticks_per_rev

        self.left_ticks = 0
        self.right_ticks = 0

        self.gpio.setup(self.left_pin, self.gpio.IN)
        self.gpio.setup(self.right_pin, self.gpio.IN)

        self.gpio.add_event_detect(self.left_pin, self.gpio.RISING, callback=self._left_tick)
        self.gpio.add_event_detect(self.right_pin, self.gpio.RISING, callback=self._right_tick)

    def _left_tick(self, channel):
        self.left_ticks += 1

    def _right_tick(self, channel):
        self.right_ticks += 1

    def get_and_clear_ticks(self):
        lt = self.left_ticks
        rt = self.right_ticks
        self.left_ticks = 0
        self.right_ticks = 0
        return lt, rt

# ---------------------------------------------------------
# Main Execution
# ---------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(description="UGV Speed Logger")
    parser.add_argument("--dry-run", action="store_true", help="Run without hardware")
    parser.add_argument("--output", type=str, default="speed_log.csv", help="CSV output file")
    parser.add_argument("--duration", type=int, default=10, help="Test duration in seconds")
    args = parser.parse_args()

    if args.dry_run:
        gpio = MockGPIO()
        i2c_bus = MockSMBus(1)
    else:
        try:
            import RPi.GPIO as rpi_gpio
            from smbus2 import SMBus
            gpio = rpi_gpio
            i2c_bus = SMBus(1)
        except ImportError:
            print("Error: Hardware libraries not found. Run with --dry-run or install them.")
            sys.exit(1)

    chassis = ChassisController(gpio, i2c_bus, args.dry_run)
    encoders = EncoderLogger(gpio)

    print(f"Starting sweep test. Logging to {args.output}")
    start_time = time.time()
    last_time = start_time

    try:
        with open(args.output, mode='w', newline='') as csv_file:
            writer = csv.writer(csv_file)
            writer.writerow(["Time_s", "PWM_Percent", "Left_Ticks", "Right_Ticks", "Left_RPM", "Right_RPM"])

            while True:
                current_time = time.time()
                elapsed = current_time - start_time

                if elapsed > args.duration:
                    break

                # Ramp PWM from 0 to 100
                pwm_target = (elapsed / args.duration) * 100.0
                chassis.set_speed(pwm_target)

                time.sleep(0.2)
                now = time.time()
                dt = now - last_time
                last_time = now

                # Simulate ticks for dry-run
                if args.dry_run:
                    if pwm_target > 20.0: # Mock deadband at 20%
                        sim_rpm = (pwm_target - 20.0) * 2.5
                        sim_ticks_per_sec = (sim_rpm * encoders.ticks_per_rev) / 60.0
                        ticks_to_add = int(sim_ticks_per_sec * dt)
                        for _ in range(ticks_to_add):
                            gpio.simulate_tick(encoders.left_pin)
                            gpio.simulate_tick(encoders.right_pin)

                left_t, right_t = encoders.get_and_clear_ticks()

                dt_min = dt / 60.0
                left_rpm = (left_t / encoders.ticks_per_rev) / dt_min if dt_min > 0 else 0
                right_rpm = (right_t / encoders.ticks_per_rev) / dt_min if dt_min > 0 else 0

                writer.writerow([round(elapsed, 2), round(pwm_target, 2), left_t, right_t, round(left_rpm, 2), round(right_rpm, 2)])
                print(f"Time: {elapsed:05.2f}s | PWM: {pwm_target:05.1f}% | L_RPM: {left_rpm:06.1f} | R_RPM: {right_rpm:06.1f}")

    except KeyboardInterrupt:
        print("\nTest interrupted by user.")
    finally:
        chassis.stop()
        gpio.cleanup()
        print("Test complete. Motors stopped.")

if __name__ == "__main__":
    main()

2. Script de análisis

Guarda el siguiente código como analyze_speed_log.py. Este script lee el CSV generado para extraer las métricas de rendimiento, calculando explícitamente la banda muerta y las RPM máximas.

#!/usr/bin/env python3
"""
Analyzes the UGV Speed Log CSV to determine motor deadband and max RPM.
"""

import csv
import argparse

def main():
    parser = argparse.ArgumentParser(description="Analyze UGV Speed Log")
    parser.add_argument("--input", type=str, default="speed_log.csv", help="Input CSV file")
    args = parser.parse_args()

    max_rpm = 0.0
    deadband_pwm = None

    try:
        with open(args.input, mode='r') as f:
            reader = csv.DictReader(f)
            for row in reader:
                pwm = float(row["PWM_Percent"])
                l_rpm = float(row["Left_RPM"])
                r_rpm = float(row["Right_RPM"])

                avg_rpm = (l_rpm + r_rpm) / 2.0

                if avg_rpm > max_rpm:
                    max_rpm = avg_rpm

                # The first time we observe sustained movement, record the PWM
                if deadband_pwm is None and avg_rpm > 5.0:
                    deadband_pwm = pwm

        print(f"--- Analysis Report for {args.input} ---")
        if deadband_pwm is not None:
            print(f"Estimated Motor Deadband Threshold: ~{deadband_pwm:.2f}% PWM")
        else:
            print("Estimated Motor Deadband Threshold: Not found (no movement logged).")
        print(f"Maximum Observed Speed: {max_rpm:.2f} RPM")

    except FileNotFoundError:
        print(f"Error: Could not find {args.input}. Run the logger script first.")

if __name__ == "__main__":
    main()

Validación y resultado esperado

Para validar la lógica del código sin hardware, o para verificar tu configuración física, ejecuta el script principal y luego ejecuta el análisis.

  1. Ejecutar el registrador:

bash
python3 ugv_speed_logger.py --dry-run --duration 5

Resultado esperado:

text
[MOCK] Initialized GPIO
[MOCK] Initialized SMBus 1
Starting sweep test. Logging to speed_log.csv
Time: 00.00s | PWM: 00.0% | L_RPM: 0000.0 | R_RPM: 0000.0
Time: 00.20s | PWM: 04.0% | L_RPM: 0000.0 | R_RPM: 0000.0
...
Time: 01.21s | PWM: 24.2% | L_RPM: 0014.9 | R_RPM: 0014.9
...
Time: 05.01s | PWM: 100.0% | L_RPM: 0198.8 | R_RPM: 0198.8
Test complete. Motors stopped.
[MOCK] GPIO cleaned up

  1. Ejecutar el analizador para validar las métricas:

bash
python3 analyze_speed_log.py --input speed_log.csv

Resultado esperado:

text
--- Analysis Report for speed_log.csv ---
Estimated Motor Deadband Threshold: ~22.18% PWM
Maximum Observed Speed: 198.81 RPM

Nota: En el modo --dry-run, el adaptador simulado imita una banda muerta al 20% de PWM y un máximo de RPM de ~200. Al ejecutarse en hardware real, el analizador revelará las restricciones físicas reales de tus motores específicos y el voltaje de la batería.

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

Ir a Amazon

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

Quiz rápido

Pregunta 1: ¿Qué hardware principal se utiliza en la herramienta de registro de velocidad descrita?




Pregunta 2: ¿Cuál es el objetivo principal de la herramienta que se va a construir?




Pregunta 3: ¿Qué tipo de datos registra la herramienta para caracterizar la respuesta del motor?




Pregunta 4: ¿En qué formato se exportan los datos de rendimiento generados por la herramienta?




Pregunta 5: ¿Qué es la 'banda muerta del motor' según el contexto?




Pregunta 6: ¿Cuál es un ejemplo del ciclo de trabajo PWM mínimo mencionado para superar la fricción interna?




Pregunta 7: ¿Para qué sirve establecer la curva de respuesta en bucle abierto y el límite máximo de RPM?




Pregunta 8: ¿Cómo ayuda la herramienta en la detección de deslizamiento de ruedas?




Pregunta 9: ¿Qué tipo de barrido realiza la herramienta sobre la potencia del motor?




Pregunta 10: ¿Qué tipo de respuesta del motor se busca caracterizar al registrar los pulsos del codificador?




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