Self Balancing Car. (Post Largo)

Día 0: Bases.

  • intentar replicar algún proyecto existente.
  • Avanzar rápido y documentar todo rápido tambien.
  • Fotos y vídeo más que texto.

Primera idea: que la base sea una tarjeta PVC: Una idea tonta que limita el tamaño del desarrollo innecesariamente.

La idea de pegar cosas a la tarjeta PVC no es muy buena, porque de hecho la goma se despegaba.

Día 1

Idea dos: Copia y pega:

https://github.com/keyestudio/KS0193-Self-balancing-Car-Kit/blob/master/KS0193.md:

Ir soldando todos los componentes.

Esto puede ser otra idea: https://wiki.keyestudio.com/Ks0193_keyestudio_Self-balancing_Car construir el hardware, e ir haciendo pequeños proyectos y librerías para ir probando las funciones, y documentando.

Noche 2

Comparar el hardware de KSO193 y ver qué tengo que se asemeje:

La tarjeta principal es un clon de Arduino uno. La tarjeta segundaria tiene conexiones para el módulo Xbee y drivers para los motores:

  • 6612FNG chip driver
  • MPU-6050 unidad inercial

Cuáles son mis componentes?

  • Tengo motores amarillos,
  • Xiao ESP32-S3 (240 MHz, cámara, micrófono, microSD, 2 gpio adicionales en la expansión, 2.4GHz Wi-Fi y BLE, bluetooth mesh, 8 MB RAM y Flash, pines touch)
  • Xiao nrf52840 sense (64 MHz, BLE, NFC, 6 DOF IMU, micrófono, rgb)
  • Como el NRF52840 es el que tiene IMU LSM6DS3TR, ese es el que voy a usar.
  • Tengo unos L293DD en sop20.

Creo que esto es todo lo que necesito… El motor sí necesito comprar uno. este es el link del motor y tiene unos links abajo de lecturas interesantes que no voy a leer nunca XD

Día 3

Compré headers, y drivers de corriente (los primeros que vi no dan la corriente de los motores con encoders). Tuve pesadillas donde soñé que los servos y los motores de paso no necesitan realimentar, y parece que sí es así (son los de paso sí, con los servos no estoy seguro). Empecé a soldar y me he tardado.

Día 4.

Desistí de soldar porque la punta del soldador es muy gruesa. Compré un soldador en aliexpress, pero empecé a montar lo mínimo en un protoboard mínimo. La intención es hacer una base para el carrito, y poner el circuito ahí. Comencé con movimientos básicos del motor. y empezando a leer del giroscopio:

Despues de un ratoo peleando con el IMU…

Vídeo acá

Proximo paso? PID!

Noche 5:Filtros de Kalman.

Filtros de kalman

Encontré este vídeo que me gustó la explicación

Hice este prompt en ChatGPT: Estoy haciendo un carro de dos ruedas que se balancea. Estoy midiendo mi aceleración y cantidad de giro con una IMU LSM6DS3. ¿Porqué me proponen usar un filtro kalman para encontrar el ángulo respecto a la vertical, y el giro en el eje X?

ChatGPT: El filtro de Kalman es una herramienta muy útil para fusionar datos de múltiples fuentes, como en tu caso, donde estás midiendo la aceleración y la cantidad de giro con una IMU (unidad de medición inercial). Aquí hay algunas razones por las que se propone el uso de un filtro de Kalman para estimar el ángulo respecto a la vertical (inclinación) y el giro en el eje X:

  1. Precisión mejorada: Los datos brutos de una IMU pueden ser ruidosos y estar sujetos a errores, especialmente debido a factores como la vibración y la deriva. Un filtro de Kalman puede ayudar a filtrar este ruido y proporcionar una estimación más precisa y suavizada del ángulo y la velocidad angular.
  2. Fusión de datos: La IMU proporciona mediciones de aceleración lineal y velocidad angular en diferentes ejes. Utilizando un filtro de Kalman, puedes fusionar estas mediciones para obtener una estimación más completa y confiable del estado del sistema, que en tu caso sería el ángulo de inclinación y la velocidad angular en el eje X.
  3. Compensación de errores: Los sensores inerciales como la IMU pueden verse afectados por errores sistemáticos y de sesgo a lo largo del tiempo. El filtro de Kalman puede ayudar a compensar estos errores y proporcionar estimaciones más estables y consistentes a lo largo del tiempo.
  4. Adaptabilidad: El filtro de Kalman es un método adaptable que puede ajustarse dinámicamente según la confiabilidad relativa de las mediciones de la IMU en diferentes condiciones. Esto significa que puede adaptarse a cambios en la dinámica del sistema, como aceleraciones repentinas o movimientos bruscos del vehículo.

En resumen, el uso de un filtro de Kalman te permitirá obtener estimaciones más precisas y confiables del ángulo de inclinación y la velocidad angular de tu carro de dos ruedas, al fusionar de manera óptima las mediciones de la IMU y compensar los errores inherentes a los sensores inerciales.

Recordar que en este link hay un código propuesto. Que me tocó adaptar un poco a mi micro, y a mi acelerómetro.

#include <variant.h> //modifiqué este archivo para incluir STBY en el pin P0.19
// ver C:\Users\TU_USER_ACA_!!!\AppData\Local\Arduino15\packages\Seeeduino\hardware\nrf52\1.1.8\variants\Seeed_XIAO_nRF52840_Sense

//////////////////// MOTORES ////////////////////
#include <SparkFun_TB6612.h>
#define PWMA 0
#define AIN2 1
#define AIN1 2
#define STBY 33 //D33 está mapeado a P0.19 que no está conectado a nada. Lo hice porque la librería pide un STBY, y no se me ocurrió modificar la librería XD
#define BIN1 4
#define BIN2 5
#define PWMB 6

const int offsetA = 1;
const int offsetB = 1;

Motor motor0 = Motor(AIN1, AIN2, PWMA, offsetA, STBY);
Motor motor1 = Motor(BIN1, BIN2, PWMB, offsetB, STBY);
////////////////// FIN MOTORES //////////////////

// For the XIAO BLE  and XIAO BLE Sense boards, pins_arduino.h
// defines LEDR, LEDG, and LEDB
#include <pins_arduino.h>
#define LED_BLUE_PIN LED_BLUE
#define LED_RED_PIN  LED_RED /* Same as LED_BUILTIN */

//////////////////// ACELEROMETROS ////////////////////
//Create a instance of class LSM6DS3
#include "LSM6DS3.h"
LSM6DS3 myIMU(I2C_MODE, 0x6A);    //I2C device address 0x6A
//////////////////// FIN ACELEROMETROS ////////////////////

///////////////////////Kalman_Filter////////////////////////////
float Q_angle = 0.001;  //Covariance of gyroscope noise
float Q_gyro = 0.003;    //Covariance of gyroscope drift noise
float R_angle = 0.5;    //Covariance of accelerometer
char C_0 = 1;
float dt = 0.005; //The value of dt is the filter sampling time.
float K1 = 0.05; // a function containing the Kalman gain is used to calculate the deviation of the optimal estimate.
float K_0,K_1,t_0,t_1;
float angle_err;
float q_bias;    //gyroscope drift 

float accelz = 0;
float angle;
float angle_speed;
float angleY_one;

float Pdot[4] = { 0, 0, 0, 0};
float P[2][2] = {{ 1, 0 }, { 0, 1 }};
float  PCt_0, PCt_1, E;
//////////////////////Kalman_Filter/////////////////////////

void setup()
{
  Serial.begin(115200);

// IO GENERALES //
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_BLUE, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
// FIN IO GENERALES //

// ACELEROMETROS //
    while (!Serial);
  //Call .begin() to configure the IMUs
  if (myIMU.begin() != 0) {
      Serial.println("Device error");
  } else {
      Serial.println("Device OK!");
  }
  myIMU.settings.accelRange = 4;//Max G force readable.  Can be: 2, 4, 8, 16
  myIMU.settings.accelBandWidth = 50;  //Hz.  Can be: 50, 100, 200, 400;
  myIMU.settings.accelSampleRate = 208;//Hz.  Can be: 13, 26, 52, 104, 208, 416, 833, 1666, 3332, 6664, 13330
// FIN ACELEROMETROS //

  Serial.flush(); // Waits for the transmission of outgoing serial data to complete.
}

// PID PID PID PID PID PID PID PID PID PID PID PID PID PID PID PID PID //
///////////////////////angle parameters//////////////////////////////
float Angle;
float angle_X; //calculate the inclined angle variable of X-axis by accelerometer
float angle_Y; //calculate the inclined angle variable of Y-axis by accelerometer
float angle0 = 1; //Actual measured angle (ideally 0 degrees) 
float Gyro_x,Gyro_y,Gyro_z;  //Angular angular velocity for gyroscope calculation
///////////////////////angle parameters//////////////////////////////
int16_t xa, ya, za;
int16_t xg, yg, zg;

//////////////////////PID parameters///////////////////////////////
double kp = 24, ki = 0.3, kd = 0.32;                   //angle loop parameters
double kp_speed = 3.6, ki_speed = 0.080, kd_speed = 0;   // speed loop parameters
double setp0 = 0; //angle balance point
int PD_pwm;  //angle output
float pwm1=0,pwm2=0;

void DSzhongduan()
{
  xa = myIMU.readRawAccelX();
  ya = myIMU.readRawAccelY();
  za = myIMU.readRawAccelZ();
  xg = myIMU.readRawGyroX();
  yg = myIMU.readRawGyroY();
  zg = myIMU.readRawGyroZ();
  angle_calculate(xa, ya, za, xg, yg, zg, dt, Q_angle, Q_gyro, R_angle, C_0, K1);      //get angle and Kalman_Filter
  PD();         // angle loop of PD control
  anglePWM();
}

//////////////////angle PD////////////////////
void PD()
{
  PD_pwm = kp * (angle + angle0) + kd * angle_speed; //PD angle loop control
}

void anglePWM()
{
  pwm2=-PD_pwm;            //The final value assigned to the motor PWM
  pwm1=-PD_pwm;
  
  if(pwm1>255)             //limit PWM value not greater than 255
  {
    pwm1=255;
  }
  if(pwm1<-255) 
  {
    pwm1=-255;
  }
  if(pwm2>255)
  {
    pwm2=255;
  }
  if(pwm2<-255)
  {
    pwm2=-255;
  }

  if(angle>80 || angle<-80)      //When the self-balancing trolley’s tilt angle is greater than 45 degrees, the motor will stop.
  {
    pwm1=pwm2=0;
  }
  motor0.drive(pwm2);
  motor1.drive(pwm1);

}

// VARIABLES GLOBALES DEL LOOP.
int incremento = 0;
int velocidad = 0;

void loop()
{
  DSzhongduan();
  delay(10);
}

void angle_calculate(int16_t ax,int16_t ay,int16_t az,int16_t gx,int16_t gy,int16_t gz,float dt,float Q_angle,float Q_gyro,float R_angle,float C_0,float K1)
{
  Angle = -atan2(ay , az) * (180/ PI);           //Radial rotation angle calculation formula; negative sign is direction processing
  Gyro_x = -gx / 131;              //The X-axis angular velocity calculated by the gyroscope; the negative sign is the direction processing
  Kalman_Filter(Angle, Gyro_x);            //  Kalman Filter
  //Rotation Angle Z axis parameter
  Gyro_z = -gz / 131;                      //Z-axis angular velocity
  //accelz = az / 16.4;

  float angleAx = -atan2(ax, az) * (180 / PI); //Calculate the angle with the x-axis
  Gyro_y = -gy / 131.00; //Y-axis angular velocity
  Yiorderfilter(angleAx, Gyro_y); //first-order filter
}
////////////////////////////////////////////////////////////////

/////////////////////first-order Filter/////////////////
void Yiorderfilter(float angle_m, float gyro_m)
{
  angleY_one = K1 * angle_m + (1 - K1) * (angleY_one + gyro_m * dt);
}

///////////////////////////////KalmanFilter/////////////////////
void Kalman_Filter(double angle_m, double gyro_m)
{
  angle += (gyro_m - q_bias) * dt;          //Prior estimate
  angle_err = angle_m - angle;
  
  Pdot[0] = Q_angle - P[0][1] - P[1][0];    //Differential of azimuth error covariance
  Pdot[1] = - P[1][1];
  Pdot[2] = - P[1][1];
  Pdot[3] = Q_gyro;
  
  P[0][0] += Pdot[0] * dt;    //The integral of the covariance differential of the prior estimate error
  P[0][1] += Pdot[1] * dt;
  P[1][0] += Pdot[2] * dt;
  P[1][1] += Pdot[3] * dt;
  
  //Intermediate variable of matrix multiplication
  PCt_0 = C_0 * P[0][0];
  PCt_1 = C_0 * P[1][0];
  //Denominator
  E = R_angle + C_0 * PCt_0;
  //Gain value
  K_0 = PCt_0 / E;
  K_1 = PCt_1 / E;
  
  t_0 = PCt_0;  //Intermediate variable of matrix multiplication
  t_1 = C_0 * P[0][1];
  
  P[0][0] -= K_0 * t_0;    //Posterior estimation error covariance 
  P[0][1] -= K_0 * t_1;
  P[1][0] -= K_1 * t_0;
  P[1][1] -= K_1 * t_1;
  
  q_bias += K_1 * angle_err;    //Posterior estimation
  angle_speed = gyro_m - q_bias;   //The differential value of the output value; work out the optimal angular velocity
  angle += K_0 * angle_err; ////Posterior estimation; work out the optimal angle
}

Luego de todo esto, imprimir algunas cosas con la impresora 3D y jugar un poco con las constantes PID, llegué a esto:

Por ahora voy a considerar cerrado este tema XD aunque todavía necesito implementar la parte de control en el micro, y en el celular. En particular el micro NRF52840 sense es útil porque incluye la conectividad bluetooth también y no es necesario un periférico adicional.

Actualizaciones

Acá hay unas imágenes de las bases que usé, y un poco de la lógica de porqué las usé así:

Mi intención es que fuese algo muy simple, y sujetar todas las piezas con alguna cinta. Quise que fuera algo simple porque en el proyecto anterior modelé esto:

Y para modelar/3D-imprimir esto me tomó bastante tiempo y bastantes iteraciones. Y sentí que mi tiempo iba a estar mejor invertido si simplemente modelaba una base muy sencilla y me dedicaba a resolver otros problemas más interesantes como la comunicación, o la lógica de control.

Espero que este post le ahorre la locura a alguien.

Activar refrigeración pasiva en Windows 11

Tengo una laptop de hace 5 años y mi única queja es que a veces se calienta mucho. Como es de metal entonces es un poco incómodo escribir.

Noté que cuando la uso sin el cargador conectado, entonces la laptop no se calienta, y recordé que cuando no está conectada a la corriente las laptops trabajan a menor velocidad. Entonces hay dos configuraciones que ajusté en mi pc:

Esto ha ayudado a mantener un poco más fría mi PC.

Windows 11 no te muestra la directiva de refrigeración?

A mi tampoco me lo enseñaba. Tuve que usar este comando en un cmd de administrador:

REG ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\PowerSettings\54533251-82be-4824-96c1-47b60b740d00\94D3A615-A899-4AC5-AE2B-E4D8F634367F /v Attributes /t REG_DWORD /d 2 /f

Y TODO TU BITCOIN SERÁ MIO!!!! Ejem… digo, y esto habilitará la directiva de refrigeración en windows 11.

NRF52840: mapear pines no usados

Problema: estoy usando una librería SparkFun_TB6612.h que llama a definir varias señales:

PWMA, AIN2 1, AIN1 2, STBY, BIN1, BIN2 y PWMB. Mi módulo XIAO tiene pocos pines. No quise modificar la librería (aunque creo que este es el paso lógico). Entonces modifiqué el archivo variants.cpp así:

const uint32_t g_ADigitalPinMap[] =
{...    // Dañando cosas 
    19 //D33 es P0.19 (STBY)
};

Y el variants.h así:

#define PINS_COUNT              (34)
#define NUM_DIGITAL_PINS        (34)

Esto me deja en mi código (en el demo de motorTestRun) hacer esto:

#define PWMA 0
#define AIN2 1
#define AIN1 2
#define STBY 33 //D33 está mapeado a P0.19 que no está conectado a nada. Lo hice porque la librería pide un STBY, y no se me ocurrió modificar la librería XD
#define BIN1 4
#define BIN2 5
#define PWMB 6

Y no necesitar usar un pin adicional para la señal STBY

SEEED nrf54840 + Bluetooth LE BLE (con arduino y mit app inventor)

Recientemente he hecho el curso de coursera+edgeImpulse de ML. Ahí usé un arduino nano 33 ble sense que cuesta 40USD. Durante el curso quemé el arduino, y buscando una alternativa, conseguí que el SEEED nrf54840 es muy parecido (tiene una IMU, un RGB y un microfono) y cuesta 15USD (y hay una versión con BLE sin el micrófono ni IMU que cuesta 10USD).

Para las primeras pruebas del arduino, quize conectarlo usando el BLE (porque nunca había hecho nada así) en la página de SEEED  dan algunos consejos. Y en este vídeo de youtube aprendí cómo codificar la app de en MIT AppInventor.

En el app inventor tuve un problema al enviar datos al NRF usando en método WriteIntegers. Esto nunca funcionó. Se solucionó usando el método WriteIntegersWithResponse.

Al final terminé con este bloque en AppInventor:

Y el código en arduino sería:

#include <ArduinoBLE.h>
// Acá puede ser cualquier número que siga el formato XXXXXXXX-XXXx-XXXx-XXXx-XXXXXxxxxx
BLEService ledService(«19b10000-e8f2-537e-4f6c-d104768a1214»);
BLEByteCharacteristic switchCharacteristic(«19b10001-e8f2-537e-4f6c-d104768a1214», BLERead | BLEWrite);
const int ledPin = LED_BUILTIN; // pin to use for the LED
void setup() {
  pinMode(ledPin, OUTPUT);
  if (!BLE.begin()) {
    while (1);
  }
  BLE.setLocalName(«LED»);
  BLE.setAdvertisedService(ledService);
  ledService.addCharacteristic(switchCharacteristic);
  BLE.addService(ledService);
  switchCharacteristic.writeValue(0);
  BLE.advertise();
}
void loop() {
  BLEDevice central = BLE.central();
  if (central) {
  while (central.connected()) {
        if (switchCharacteristic.written()) {
          if (switchCharacteristic.value()) {  
            digitalWrite(ledPin, LOW); // changed from HIGH to LOW      
          } else {                              
            digitalWrite(ledPin, HIGH); // changed from LOW to HIGH    
          }
        }
      }
  }
}
Suerte!
Postmortem1: el GPS tiene que estar activado para que el teléfono encuentre los dispositivos BLE
Postmortem2: debe estar seleccionado este dispositivo en el IDE: nrf52 mbed-enabled boards

Invalid texture used for cursor – check importer settings or texture creation

El error completo en Unity lee:
Invalid texture used for cursor – check importer settings or texture creation. Texture must be RGBA32, readable, have alphaIsTransparency enabled and have no mip chain.

Este warning se estaba generando porque al crear la textura del cursor:

Luego a temp le iba a asignar la suma de dos imágenes. Como al instanciar temp no incluí el textFormat ni el mipChain, entonces daba ese warning:

Prueba de manejo: Estacionamientos

La prueba de manejo consiste en tener en cuenta 3 factores:

Las consideraciones generales para los estacionamientos son las siguientes:

Las luces de emergencia hay que prenderlas siempre (SIEMPRE) que vayas de retroceso.

Si te equivocas alineandote en el paralelo (p.e. quedas muy fuera), no lo corrijas. Equivocarse es -30, correjirlo es -30 y estás fuera.

Al salir del estacionamiento paralelo hay que encender la direccional izquierda.

Al entrar al estacionamiento diagonal hay que encender la direccional derecha.

Estacionamiento paralelo

el veedor te dará 3 instrucciones. Alinear, estacionar y salir.

Alinear:

Estacionar paso 1

Colocar el banderin (del número que nos desinaron) en la mitad de la luna pequeña. Girar todo hacia la derecha. Encender las luces de emergencia. Poner el retroceso.

Tip: en este punto la llanta trasera estará pisando la línea blanca.

Paso 2

El banderin posterior estará arriba de la línea roja del seguro de la puerta trasera:

Girar todo hacia la izquierda y seguir retrocediendo.

Paso 3

Detener cuándo el banderin delantero esté en la mitad del volante. Girar completo a la derecha y avanzar.

Paso 4:

Avanzar hasta que el auto esté paralelo con la línea horizontal. Verificar retrovisor.

Para salir, poner retroceso, encender la luces de emergencia, cruzar todo el volante a la derecha, retroceder hasta que el banderín esté al centro del volante.

Poner luces de cruce a la izquierda, poner D, sacar la cabeza por la ventana y verificar que tenemos preferencia. Salir.

Este vídeo está bueno, pero no explica las luces de emergencia porque son un requisito más nuevo

Estacionamiento Diagonal

Al esperar indicaciones poner intermitentes derechas.

Al recibir el número, avanzar acercandonos a la línea izquierda, hasta que la línea derecha de nuestro puesto esté a la altura del retrovisor:

Girar todo a la derecha. Avanzar hasta que el poste asignado esté en el centro del tablero:

Comenzar a devolver el timón. En este vídeo explican el proceso con pasos, pero la verdad es que si ya manejas es más fácil dejarse llevar por la memoria muscular que tengas. Avanzar hasta que las llantas toquen el tope. Poner parking

Esperar la indicación para salir. Al retroceder recordar encender las luces. Verificar la prioridad. Mirar que la línea blanca esté en el marco de la ventana pequeña trasera. Girar todo a la derecha. Seguir retrocediendo.

Mirar por la ventana trasera hasta que el semáforo esté entre las dos cabeceras. Frenar. Ponder D. Quitar las luces de emergencia. Seguir con la ruta.

Suerte en el exámen!!!

Prueba de manejo: Preferencias

La prueba de manejo consiste en tener en cuenta 3 factores:

Las consideraciones para las preferencias son las siguientes:

En los ovalos: si ya hay carros transitando, y si tienes que entrar, tienes que esperar hasta que puedas ver las luces traseras, y luego entrar

En los estacionamientos: Si tienes autos detrás de ti, tienes que esperar que ellos salgan primero. Hay que voltear a ver hacia atrás antes de salir.

Casos pendejos:

Un auto ruta a sale, un auto ruta b entra.

En la siguiente imágen el auto amarillo ruta B tiene prioridad para cambiar de canal antes que el rojo cambie de canal. El rojo se tiene que detener al terminar la jiba (el montículo).

Si dos autos se encuentran frente a frente para salir del circuito

El que sale de la ruta B tiene preferencia para salir.

Curva pendeja. Caso A (esperar siempre si eres amarillo)

Tú eres el carro amarillo. El coche azul entra al circuito B, El coche rojo va a salir del circuito A. Tú tienes que esperar que azul salga, luego va a pasar rojo, de último tú pasas. Si estás delante del rayado, no importa si el semaforo arriba de ti está en rojo o verde, eso es para los que están detrás del rayado.

En uno de mis intentos de prueba el coche rojo se quedó estacionado ahí, yo conté unos 10 segundos y como no se movió, yo seguí. En esos casos donde la gente tiene un colapso en la pista, tienes que contar hasta 10 y seguir.

Curva pendeja, caso B (esperar siempre si eres amarillo)

El auto naranja va a salir de la ruta B. Hay que dejarlo pasar en ambos casos

Suerte en el exámen!!!