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
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.
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 5 | Pin PCA9685 | Descripción |
|---|---|---|
| Pin 1 (3.3V) | VCC | Alimentación lógica para el chip PWM |
| Pin 6 (GND) | GND | Tierra común |
| Pin 3 (GPIO 2 / SDA) | SDA | Datos I2C |
| Pin 5 (GPIO 3 / SCL) | SCL | Reloj I2C |
2. Lógica del controlador de motor (TB6612FNG)
| Componente de origen | Pin de origen | Pin TB6612FNG | Descripción |
|---|---|---|---|
| PCA9685 | Canal 0 | PWMA | Velocidad PWM del motor izquierdo |
| PCA9685 | Canal 1 | PWMB | Velocidad PWM del motor derecho |
| RPi 5 | Pin 29 (GPIO 5) | AIN1 | Avance del motor izquierdo |
| RPi 5 | Pin 31 (GPIO 6) | AIN2 | Retroceso del motor izquierdo |
| RPi 5 | Pin 33 (GPIO 13) | BIN1 | Avance del motor derecho |
| RPi 5 | Pin 35 (GPIO 19) | BIN2 | Retroceso del motor derecho |
| RPi 5 | Pin 37 (GPIO 26) | STBY | En espera / Habilitar (HIGH para funcionar) |
| Paquete de baterías | Positivo | VMOT | Alimentación del motor |
| Paquete de baterías | Negativo | GND | Tierra 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 5 | Pin del codificador | Descripció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 izquierda | Salida de pulso de la rueda izquierda |
| Pin 13 (GPIO 27) | Fase A derecha | Salida 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.
- 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
- 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
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.




