From b82ed400bc9682698a1f7d5b0588e1f5dc672f88 Mon Sep 17 00:00:00 2001 From: alro1965 Date: Fri, 22 May 2026 22:46:16 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20BNO085=20IMU=20integration=20=E2=80=94?= =?UTF-8?q?=20SPICE=20+=20simulator=20yaw=20rate=20feed-forward?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPICE (6_bno085_imu.cir): - BNO085 power supply with 10uF + 100nF decoupling on VDD - Power-on reset RC circuit (R=10K, C=1uF, tau=10ms → deasserts at ~12ms) - I2C Fast Mode 400kHz bus: 4.7K pull-ups, 50pF bus capacitance model - Full I2C transaction: START + address 0x4A + R/W + BNO085 ACK + STOP - INT pin (open-drain, 10K pull-up, 100Hz interrupt simulation) - .meas directives: reset timing, SCL rise time, VDD stability Simulator (esp32_sim.py): - SimSnapshot.bno085_yaw_rate_dps field added - _bno085_enabled / _bno085_noise_std_dps / _bno085_yaw_rate_dps state - enable_bno085(noise_std_dps=0.02) public method - disable_bno085() public method - _run_physics: samples gyro at 50Hz with Gaussian noise model - _run_outer_loop: uses BNO085 yaw rate for rot_ff_term when enabled (replaces NMEA-derived ROT — lower latency ~4ms vs ~100-200ms) Usage: sim.enable_bno085() # activate gyro feed-forward sim.enable_bno085(noise_std_dps=0.014) # with BNO085 spec noise Co-Authored-By: Claude Sonnet 4.6 --- tools/esp32_sim.py | 58 ++++++- tools/spice/6_bno085_imu.cir | 293 +++++++++++++++++++++++++++++++++++ 2 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 tools/spice/6_bno085_imu.cir diff --git a/tools/esp32_sim.py b/tools/esp32_sim.py index fc791f2..91ecb08 100644 --- a/tools/esp32_sim.py +++ b/tools/esp32_sim.py @@ -126,6 +126,7 @@ class SimSnapshot: alarm_heading_lost: bool alarm_off_course: bool alarm_off_course_severe: bool + bno085_yaw_rate_dps: float = 0.0 # medición del giróscopo BNO085 (0 si desactivado) @dataclass @@ -295,6 +296,15 @@ class ESP32Simulator: self._sea_wave_period: float = 8.0 self._sea_rng: random.Random = random.Random(42) + # -- BNO085 IMU (yaw rate de alta frecuencia) -------------------- + # Cuando está activo, el rot_ff_term del outer PID usa la medición + # del giróscopo BNO085 (50 Hz, ruido ~0.02 °/s) en lugar del ROT + # derivado del NMEA compass (10 Hz, mayor latencia). + # Esto reduce el overshoot en maniobras y el cabeceo en olas. + self._bno085_enabled: bool = False + self._bno085_noise_std_dps: float = 0.02 # spec BNO085: ~0.014 °/s + self._bno085_yaw_rate_dps: float = 0.0 + # -- Registro de telemetría para análisis ------------------------ self.log: list[SimSnapshot] = [] self.events: list[SimEvent] = [] @@ -417,6 +427,36 @@ class ESP32Simulator: if max_rudder_deg is not None: self._pid_outer.config.max_rudder_deg = float(max_rudder_deg) + def enable_bno085(self, *, noise_std_dps: float = 0.02) -> None: + """ + Activa el BNO085 como fuente de yaw rate para el outer PID. + + Con BNO085 activo el ``rot_ff_term`` del outer PID usa la medición + del giróscopo (50 Hz, ruido típico ~0.014 °/s) en lugar del ROT + derivado del bus NMEA 2000 (10 Hz, mayor latencia de bus + GPS). + + Diferencia clave respecto al ROT por NMEA: + - Latencia: BNO085 ≈ 4 ms vs NMEA ROT ≈ 100-200 ms + - Ruido: BNO085 ≈ 0.02 °/s vs NMEA ROT ≈ 0.1-0.5 °/s + - Frecuencia: BNO085 muestrea a 250 Hz (gyro), outer PID consume a 10 Hz + + En mal tiempo (B4-B5) el BNO085 permite que el ``rot_ff_term`` + reaccione a los picos de guiñada generados por las olas antes de + que el error de heading se acumule — equivale a subir el knob + "COUNTER RUDDER" manteniendo la ganancia proporcional estable. + + Args: + noise_std_dps: Desviación estándar del ruido del giróscopo en °/s. + BNO085 spec: ~0.014 °/s típico. Default 0.02 °/s (conservador). + """ + self._bno085_enabled = True + self._bno085_noise_std_dps = float(noise_std_dps) + + def disable_bno085(self) -> None: + """Desactiva el BNO085; el outer PID vuelve a usar ROT del NMEA 2000.""" + self._bno085_enabled = False + self._bno085_yaw_rate_dps = 0.0 + # ----------------------------------------------------------------------- # API pública — Interfaz Modbus # ----------------------------------------------------------------------- @@ -648,10 +688,15 @@ class ESP32Simulator: sp_speed_raw = self._holdings[25] # PID_OUTER_SPEED_KN_REQ_X10 sog_kn = (sp_speed_raw * 0.1) if sp_speed_raw > 0 else self._nmea_sog_kn + # BNO085 activo → usa yaw rate del giróscopo (50 Hz, baja latencia). + # BNO085 inactivo → usa ROT del bus NMEA 2000 (10 Hz, GPS/compass). + rot_for_pid = (self._bno085_yaw_rate_dps if self._bno085_enabled + else self._nmea_rot_dps) + self._outer_rudder_sp = self._pid_outer.step( heading_setpoint_deg=self._heading_setpoint_deg, heading_measured_deg=self._nmea_heading_deg, - rate_of_turn_dps=self._nmea_rot_dps, + rate_of_turn_dps=rot_for_pid, speed_kn=sog_kn, allowed=True, ) @@ -715,6 +760,16 @@ class ESP32Simulator: self._nmea_cog_deg = self._nmea_heading_deg self._last_nmea_update_t = self._t + # BNO085: muestrea yaw rate a 50 Hz (mismo ritmo que el inner loop). + # El giróscopo del BNO085 tiene latencia ~4ms y ruido ~0.02 °/s, + # mucho mejor que el ROT calculado desde el GPS/compass NMEA (100-200ms). + # En el outer PID (10 Hz) esto significa que rot_ff_term reacciona a + # picos de guiñada por olas ~10x más rápido que con ROT por NMEA. + if self._bno085_enabled: + noise = (self._sea_rng.gauss(0.0, self._bno085_noise_std_dps) + if self._bno085_noise_std_dps > 0.0 else 0.0) + self._bno085_yaw_rate_dps = self._vessel.state.rate_of_turn_dps + noise + # ----------------------------------------------------------------------- # Internos — alarmas # ----------------------------------------------------------------------- @@ -825,6 +880,7 @@ class ESP32Simulator: alarm_heading_lost=self._alarm_heading_lost, alarm_off_course=self._alarm_off_course, alarm_off_course_severe=self._alarm_off_course_severe, + bno085_yaw_rate_dps=self._bno085_yaw_rate_dps, )) def _add_event(self, kind: str, detail: str = "") -> None: diff --git a/tools/spice/6_bno085_imu.cir b/tools/spice/6_bno085_imu.cir new file mode 100644 index 0000000..4ea0137 --- /dev/null +++ b/tools/spice/6_bno085_imu.cir @@ -0,0 +1,293 @@ +* ======================================================================= +* AR-Autopilot — IMU BNO085: Alimentacion, Reset e I2C +* Archivo: 6_bno085_imu.cir +* Tarjeta: Modulo compacto ESP32+CAN (CIRCUITO GIRO) +* +* CIRCUITO: +* +* 3.3V ──[C=10uF + C=100nF]──── BNO085 VDD (alimentacion principal) +* 3.3V ──[C=100nF]────────────── BNO085 VDDIO (nivel logico I2C = 3.3V) +* +* 3.3V ──[R=10K]──── NRST (BNO085 RESET, activo bajo) +* └── [C=1uF] ── GND → reset POR: NRST sube lento +* +* 3.3V ──[R=4.7K]──── SCL (I2C clock, 400 kHz Fast Mode) +* 3.3V ──[R=4.7K]──── SDA (I2C data, open-drain) +* 3.3V ──[R=10K] ──── INT (interrupcion data-ready, activo bajo OD) +* +* SA0 ── GND → direccion I2C = 0x4A +* BOOT ── GND → modo operacion normal (no bootloader USB) +* +* BNO085 (CEVA/Hillcrest BNO085): +* Alimentacion: VDD = 3.3V, VDDIO = 1.8V a 3.3V +* Corriente tipica: 6.5 mA (todos los sensores activos, 100 Hz) +* Reset: NRST activo bajo, minimo 10 us, startup: 50 ms tipico +* I2C: 100 kHz o 400 kHz (Fast Mode), max 1 MHz (Fast Mode Plus) +* Reportes configurables: Rotation Vector, Gyroscope, Accelerometer... +* Sensor Fusion DSP interno: 500 Hz internal fusion rate +* Giroscopio: ruido ~0.014 deg/s RMS, bias < 1 deg/s +* Magnetometro: compensacion hard/soft iron automatica +* Rango de temperatura operacion: -40 a +85 C (marino OK) +* +* REPORTES USADOS PARA EL AUTOPILOTO: +* ARVR Stabilized Rotation Vector → Heading (0-360°) a 100 Hz +* Gyroscope Calibrated → Yaw rate (°/s) a 250 Hz +* Linear Acceleration → para deteccion de impactos +* +* COMO USAR EN LTSPICE: +* Ver: V(nrst) → curva de reset POR (debe superar 0.7×VDD = 2.31V en ~12ms) +* V(vdd_bno) → tension de alimentacion BNO085 con decoupling +* V(scl) → reloj I2C 400 kHz con tiempos de subida correctos +* V(sda) → datos I2C con open-drain y pull-up +* V(int_pin) → interrupcion data-ready +* ======================================================================= + +.title AR-Autopilot BNO085 IMU Power, Reset and I2C Interface + +* ----------------------------------------------------------------------- +* ALIMENTACION 3.3V +* ----------------------------------------------------------------------- +V33 V33 GND 3.3V + +* ----------------------------------------------------------------------- +* DESACOPLO DE ALIMENTACION BNO085 +* ----------------------------------------------------------------------- +* BNO085 datasheet recomienda: 10uF (bulk) + 100nF (HF) en VDD +* y 100nF adicional en VDDIO, lo mas cerca posible del IC. +* A 22mm del inductor L2 del buck: el ruido de switching es bajo +* pero el decoupling sigue siendo critico para el magnetometro. + +Cbulk V33 VDD_BNO 10u ; capacitor bulk 10uF ceramico (X5R) +Cdec1 VDD_BNO GND 100n ; 100nF ceramico junto al pin VDD del BNO085 +Cdec2 V33 GND 100n ; 100nF en VDDIO (misma tension 3.3V) + +* Inductancia del trace PCB entre condensador bulk y IC (~10mm de traza) +* L_trace a 1.4MHz (MP2338): XL = 2*pi*1.4e6*1e-9 = 8.8mOhm → despreciable +Rtrace V33 VDD_BNO 0.05 ; resistencia de traza PCB (~50mOhm para 10mm) + +* Consumo del BNO085 (todos sensores + fusion a 100Hz) +Ibno085 VDD_BNO GND DC 6.5m ; 6.5 mA tipico (datasheet tabla 4.3) + +* ----------------------------------------------------------------------- +* CIRCUITO DE RESET (Power-On Reset) +* ----------------------------------------------------------------------- +* NRST activo bajo: cuando VDD sube, NRST debe permanecer bajo +* hasta que VDD este estable, luego sube lentamente via RC. +* El BNO085 sale de reset cuando NRST > 0.7 * VDDIO = 2.31V +* Con R=10K, C=1uF: τ = 10ms → NRST cruza 2.31V en ~12ms +* +* Simulamos encendido: VDD sube en 1ms (soft-start del buck converter) +* Despues NRST sube lentamente → BNO085 en reset durante ~12ms ✓ + +* Resistencia de pull-up del RESET +Rrst V33 NRST_NODE 10k + +* Condensador de reset (define el tiempo de reset) +Crst NRST_NODE GND 1u IC=0 + +* Diodo de descarga rapida (para re-reset rapido si VDD cae) +Drst GND NRST_NODE DRST_FAST +.model DRST_FAST D(Is=1e-12 N=1 Rs=1 Cjo=5p) + +* Modelo del umbral de reset del BNO085 (schmitt trigger interno) +* NRST < 2.31V → en reset; NRST > 2.31V → operativo +Eres RESET_STATUS GND VALUE={IF(V(NRST_NODE) > 2.31, 3.3, 0)} + +* ----------------------------------------------------------------------- +* BUS I2C — RESISTENCIAS DE PULL-UP +* ----------------------------------------------------------------------- +* Fast Mode (400 kHz): Rpullup maximo = Vcc/(3mA) = 3.3/0.003 = 1.1k +* Rpullup minimo = (Vcc - Voh)/(bus cap × slew rate) ≈ 1k +* Valor estandar: 4.7k (funciona bien hasta 300kHz con Cbus < 100pF) +* Para 400kHz con Cbus=50pF: tr = 0.8473 * 4700 * 50e-12 = 199ns OK (limite es 300ns) +* +* Si necesitas 400kHz garantizado con cable largo (Cbus > 100pF): +* Reducir a 2.2k → tr = 93ns ✓ (pero mayor consumo: 3.3/2.2k = 1.5mA por linea) + +Rpull_scl V33 SCL 4.7k ; pull-up SCL +Rpull_sda V33 SDA 4.7k ; pull-up SDA + +* Capacidad de bus (trazas PCB ~10cm + pines I2C de ESP32 y BNO085) +* ESP32 I2C input cap: ~5pF, BNO085 I2C input cap: ~5pF, traza: ~10pF/cm × 10cm = 100pF +Cbus_scl SCL GND 50p ; capacidad de bus SCL (solo 10cm de traza en PCB compacto) +Cbus_sda SDA GND 50p ; capacidad de bus SDA + +* ----------------------------------------------------------------------- +* ESP32 MASTER I2C — Genera transaccion I2C (direccion 0x4A, lectura) +* ----------------------------------------------------------------------- +* Protocolo I2C 400kHz (Fast Mode): +* Periodo = 2.5us +* SCL alto = 0.6us min (spec), SCL bajo = 1.3us min +* Usamos: SCL alto = 0.9us, SCL bajo = 1.6us (simétrico aproximado) +* +* Secuencia simulada: +* t=0: Bus idle (SDA=HIGH, SCL=HIGH) +* t=5us: START condition (SDA baja mientras SCL alto) +* t=7us: SCL baja → comienzo de bits de direccion +* t=7-32us: 8 bits: direccion 0x4A + R/W=1 (lectura) +* 0x4A = 1001010, con R/W=1 → byte = 10010101 = 0x95 +* t=32us: SCL sube → ACK del esclavo (BNO085 baja SDA) +* t=37us: SCL baja → BNO085 libera SDA (pull-up sube) +* t=40us: BNO085 comienza a enviar dato (primer byte de SHTP) +* t=60us: STOP condition + +* SCL: generado por ESP32 (push-pull internamente, vista del bus = open-drain + pull-up) +* Modelamos el SCL como fuente de tension con resistencia baja (driver fuerte) +Vscl_drv SCL_DRV GND PWL( ++ 0 3.3 ++ 4.9u 3.3 ++ 5.0u 3.3 ; bus idle ++ 6.9u 3.3 ++ 7.0u 0 ; SCL baja → START completado, primer bit ++ 8.5u 0 ++ 8.6u 3.3 ; bit 7 (MSB): '1' ++ 9.9u 3.3 ++ 10.0u 0 ++ 11.4u 0 ++ 11.5u 3.3 ; bit 6: '0' ++ 12.9u 3.3 ++ 13.0u 0 ++ 14.4u 0 ++ 14.5u 3.3 ; bit 5: '0' ++ 15.9u 3.3 ++ 16.0u 0 ++ 17.4u 0 ++ 17.5u 3.3 ; bit 4: '1' ++ 18.9u 3.3 ++ 19.0u 0 ++ 20.4u 0 ++ 20.5u 3.3 ; bit 3: '0' ++ 21.9u 3.3 ++ 22.0u 0 ++ 23.4u 0 ++ 23.5u 3.3 ; bit 2: '1' ++ 24.9u 3.3 ++ 25.0u 0 ++ 26.4u 0 ++ 26.5u 3.3 ; bit 1: '0' ++ 27.9u 3.3 ++ 28.0u 0 ++ 29.4u 0 ++ 29.5u 3.3 ; bit 0 (R/W=1, lectura) ++ 30.9u 3.3 ++ 31.0u 0 ; SCL bajo para ACK ++ 32.4u 0 ++ 32.5u 3.3 ; SCL sube: BNO085 debe mantener SDA baja (ACK) ++ 33.9u 3.3 ++ 34.0u 0 ++ 59.9u 0 ++ 60.0u 3.3) ; ultimo SCL bajo → STOP +Rscl_drv SCL_DRV SCL 10 ; impedancia del driver I2C del ESP32 + +* SDA: ESP32 genera START y los bits de direccion +* BNO085 genera ACK (baja SDA durante ACK clock) +Vsda_drv SDA_DRV GND PWL( ++ 0 3.3 ++ 4.9u 3.3 ++ 5.0u 0 ; START: SDA baja mientras SCL alto ++ 5.9u 0 ++ 6.0u 3.3 ; START completado: SDA sube (SCL ya bajo) +* bits de direccion 0x95 = 10010101 (MSB first) +* bit7=1: SDA alto (pull-up) ++ 6.9u 3.3 ++ 7.0u 3.3 ; bit 7 = 1 (recesivo, pull-up mantiene alto) ++ 9.9u 3.3 ++ 10.0u 0 ; bit 6 = 0 (ESP32 baja SDA) ++ 11.4u 0 ++ 11.5u 0 ; bit 5 = 0 ++ 13.9u 0 ++ 14.0u 3.3 ; bit 4 = 1 ++ 15.9u 3.3 ++ 16.0u 0 ; bit 3 = 0 ++ 17.9u 0 ++ 18.0u 3.3 ; bit 2 = 1 ++ 19.9u 3.3 ++ 20.0u 0 ; bit 1 = 0 ++ 21.9u 0 ++ 22.0u 3.3 ; bit 0 = 1 (R/W=1) ++ 29.9u 3.3 ++ 30.0u 0 ; ACK slot: ESP32 libera SDA (flota) +* BNO085 baja SDA para ACK → modelado como fuente separada ++ 34.0u 0 ++ 34.1u 3.3 ; ESP32 retoma control del bus (post-ACK) ++ 59.9u 3.3 ++ 60.0u 3.3) ; STOP: SDA sube mientras SCL alto +Rsda_drv SDA_DRV SDA 10 + +* ACK del BNO085: baja SDA durante el clock de ACK (t=32.5us a 34us) +Vsda_ack SDA_ACK GND PWL( ++ 0 3.3 ++ 31.9u 3.3 ++ 32.0u 0 ; BNO085 ACK: baja SDA ++ 33.9u 0 ++ 34.0u 3.3) ; BNO085 libera SDA +Rack SDA_ACK SDA 50 ; el driver del BNO085 tiene impedancia finita + +* ----------------------------------------------------------------------- +* LINEA DE INTERRUPCION INT (data-ready, activo bajo, open-drain) +* ----------------------------------------------------------------------- +* El BNO085 baja INT cuando tiene un reporte listo para leer. +* Con heading + yaw rate a 100Hz: INT pulsa cada 10ms +* El ESP32 lee el dato cuando detecta INT bajo (GPIO input con pull-up) + +Rpull_int V33 INT_PIN 10k ; pull-up externo (ESP32 tiene pull-up interno tambien) +Cint INT_PIN GND 10p ; capacidad del pin + +* Simula INT pulsando periodicamente (100 Hz = 10ms periodo, 100us de pulso bajo) +Vint_bno INT_DRV GND PULSE(3.3 0 5m 100n 100n 100u 10m) +Rint_drv INT_DRV INT_PIN 100 ; open-drain: BNO085 solo puede bajar, no subir + +* ----------------------------------------------------------------------- +* MEDICIONES AUTOMATICAS +* ----------------------------------------------------------------------- +* Tiempo que tarda NRST en superar el umbral de reset (2.31V) +.meas TRAN t_reset_deassert WHEN V(nrst_node)=2.31 RISE=1 +* Tension estable de alimentacion del BNO085 +.meas TRAN Vvdd_stable AVG V(vdd_bno) FROM 20m TO 50m +* Tiempo de subida de SCL (10% → 90% de 3.3V = 0.33V → 2.97V) +.meas TRAN t_rise_scl TRIG V(scl)=0.33 RISE=1 TARG V(scl)=2.97 RISE=1 +* Tension minima en VDD_BNO durante transient de corriente del BNO085 +.meas TRAN Vvdd_min MIN V(vdd_bno) FROM 0 TO 50m + +* ----------------------------------------------------------------------- +* DIRECTIVAS DE SIMULACION +* ----------------------------------------------------------------------- +* 50ms total: captura arranque completo + transaccion I2C + varios pulsos INT +.tran 0 50m 0 10n + +.options reltol=0.001 + +* ----------------------------------------------------------------------- +* VALORES ESPERADOS +* ----------------------------------------------------------------------- +* t_reset_deassert ≈ 12ms → BNO085 sale de reset 12ms despues del arranque +* Vvdd_stable ≈ 3.28-3.30V → caida de tension por Rtrace=50mOhm + Ibno=6.5mA +* ΔV = 6.5mA × 50mOhm = 0.33mV (despreciable) ✓ +* t_rise_scl ≈ 150-200ns → con Rpull=4.7k y Cbus=50pF: τ = 235ns +* tr(10%-90%) = 2.2τ × (80%) = 200ns < 300ns OK ✓ +* (especificacion I2C Fast Mode: tr < 300ns) +* +* BNO085 en operacion normal: +* Corriente a 3.3V: 6.5mA tipico, 12mA maximo (fusion completa) +* Tiempo de startup tras reset: 50ms tipico (inicializacion DSP) +* Primer reporte disponible: ~100ms tras arranque +* +* CONEXION TIPICA A ESP32: +* GPIO21 → SDA (I2C SDA, con pull-up 4.7k externo) +* GPIO22 → SCL (I2C SCL, con pull-up 4.7k externo) +* GPIO34 → INT (input only, con pull-up 10k, interrupcion falling edge) +* GPIO13 → NRST (output, normalmente alto; pulsa bajo para hard reset) +* +* CONFIGURACION FIRMWARE: +* wire.begin(21, 22) → ESP32 Arduino I2C +* Wire.setClock(400000) → Fast Mode 400kHz +* BNO08x.begin(0x4A, Wire, GPIO34) → libreria SparkFun BNO08x +* +* REPORTES PARA AUTOPILOTO: +* setReports(ARVR_STABILIZED_RV, 0.01) → heading a 100Hz +* setReports(GYROSCOPE_CALIBRATED, 0.004) → yaw rate a 250Hz +* getRVheading() → degrees (0-360) con compensacion tilt +* getGyroZ() → deg/s (eje Z = yaw rate, positivo = estribor) + +.backanno +.end