← Índice del curso

Microcontroladores · Tema 37

Taller: sensor I2C y medidor de distancia

Dos sensores, dos maneras de leer. El BME280 entrega temperatura, presión y humedad por I2C, pero esos números salen «crudos» y hay que compensarlos con fórmulas de enteros. El HC-SR04 mide distancias con ultrasonido: la información está en el tiempo que dura un pulso, y el temporizador del microcontrolador lo mide con precisión de microsegundos. Y la temperatura del primero corrige al segundo.

01 · Punto de partida

Qué vamos a construir

Una pequeña estación de medición que cada medio segundo lee la temperatura, la presión y la humedad del ambiente y la distancia a un objeto, y las envía por el puerto serie en una línea como T=25,08 C P=1006 hPa H=45 % D=1001 mm. Reúne casi todo lo del curso: I2C (tema 33), UART (tema 32), un temporizador con captura de entrada (tema 24), aritmética entera y calibración (tema 36).

Los dos sensores enseñan lecciones opuestas:

  • El BME280 (de Bosch) es un sensor digital: el microcontrolador no mide una tensión, lee registros. Pero lo que guarda en ellos son cuentas crudas de 20 bits, y cada chip trae de fábrica 18 coeficientes de calibración con los que se las convierte en unidades. El trabajo del programa es leerlos y aplicar la fórmula.
  • El HC-SR04 es un sensor de tiempo de vuelo: emite una ráfaga de ultrasonido de 40 kHz y avisa con un pulso en el pin ECHO cuyo ancho es el tiempo que tardó el eco en volver. La distancia sale de d = t · v / 2, con v la velocidad del sonido, que depende de la temperatura.

02 · Dos técnicas

El diseño: un bus y un temporizador

Resumen del taller.
SensorInterfazQué entregaTécnica del microcontrolador
BME280I2C (PC4, PC5), dirección 0x76 o 0x77Cuentas crudas de temperatura, presión y humedadLectura de registros y compensación con enteros de 32 bits
HC-SR04TRIG (PD2) y ECHO (PB0 = ICP1)Un pulso cuyo ancho es el tiempo de vueloCaptura de entrada del Timer1: el hardware guarda el instante de cada flanco

La captura de entrada (tema 24) es la herramienta natural para medir el ancho de un pulso. Cuando llega un flanco al pin ICP1, el hardware copia el contador del Timer1 en ICR1 sin intervención del programa: la latencia de la interrupción no altera la medición. Se restan dos capturas (flanco de subida y flanco de bajada) y queda el ancho en cuentas. Con el divisor 64, la cuenta es de 4 µs: eso son 0,69 mm de recorrido del eco, mucho menos que la resolución real del módulo (unos 3 mm).

El flujo de cada ciclo es:

  1. Pedir una medición al BME280 en modo forzado y esperar unos 10 ms.
  2. Leer 8 bytes y compensar. Sale la temperatura.
  3. Calcular la velocidad del sonido con esa temperatura.
  4. Disparar el HC-SR04 con un pulso de 10 µs y esperar el eco (hasta 25 ms).
  5. Convertir el ancho del eco en distancia con la velocidad calculada.
  6. Imprimir la línea.

03 · Paso 1

El cableado

Conexiones.
MóduloPin del móduloArduino Uno
BME280VCC (o VIN)5 V (si el módulo tiene regulador; si no, 3,3 V)
BME280GNDGND
BME280SDAA4 (PC4)
BME280SCLA5 (PC5)
BME280SDOGND → dirección 0x76; VCC → 0x77
BME280CSBVCC (modo I2C; en GND usaría SPI)
HC-SR04VCC5 V
HC-SR04GNDGND
HC-SR04TRIG2 (PD2)
HC-SR04ECHO8 (PB0 = ICP1)

Cuatro precauciones:

  • La tensión del BME280. El chip es de 3,3 V. Muchos módulos traen regulador y conversión de nivel y admiten 5 V; otros son solo de 3,3 V y se dañan con 5 V. Leé la serigrafía o la descripción del vendedor antes de alimentarlo.
  • Pull-ups del I2C. Los módulos suelen traer los suyos (10 kΩ); con dos o tres módulos en paralelo, la resistencia equivalente baja (tema 33).
  • El pin ECHO debe ir a PB0. Es el único pin con captura de entrada del Timer1; en el Uno es el digital 8.
  • El HC-SR04 consume unos 15 mA durante la medición: alimentarlo desde el pin de 5 V de la placa está bien, pero no desde un pin digital. Si el microcontrolador es de 3,3 V, el ECHO (5 V) necesita un divisor o un conversor de nivel.

04 · Paso 2

Hablar con el BME280

Registros del BME280 que usamos (datos de la hoja de Bosch).
DirecciónNombreContenido
0xD0idIdentificador: 0x60 para el BME280 (0x58 si es un BMP280)
0xE0resetEscribir 0xB6 reinicia el chip
0xF2ctrl_humSobremuestreo de la humedad (bits 2:0); se aplica al escribir ctrl_meas
0xF3statusBit 3: medición en curso; bit 0: copiando datos de calibración
0xF4ctrl_measSobremuestreo de temperatura (7:5) y presión (4:2); modo (1:0): 00 reposo, 01 forzado, 11 normal
0xF5configTiempo de espera en modo normal y filtro
0xF7 a 0xFEdatosPresión (20 bits), temperatura (20 bits) y humedad (16 bits), en 8 bytes seguidos
0x88 a 0xA1, 0xE1 a 0xE7calibraciónCoeficientes de fábrica de temperatura, presión y humedad

Se trabaja en modo forzado: el programa ordena una medición, el chip la hace y vuelve a reposo (consume casi nada). La secuencia es: escribir ctrl_hum, escribir ctrl_meas con el modo forzado, esperar a que termine y leer los 8 bytes de datos en una sola transacción (lectura de ráfaga: el chip incrementa la dirección solo). Con sobremuestreo ×1 en las tres magnitudes, la conversión dura como máximo 9,3 ms.

#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <util/twi.h>
/* i2c_init, i2c_start, i2c_enviar, i2c_recibir, i2c_stop: tema 33
   uart_init, uart_texto, uart_u16, uart_fijo: temas 30 y 32 */

#define BME_DIR 0x76

static int8_t bme_leer(uint8_t reg, uint8_t *dst, uint8_t n) {
    int8_t error = 0;
    if (i2c_start() != TW_START)                                         { error = -1; goto fin; }
    if (i2c_enviar((BME_DIR << 1) | TW_WRITE, TW_MT_SLA_ACK))             { error = -2; goto fin; }
    if (i2c_enviar(reg, TW_MT_DATA_ACK))                                 { error = -3; goto fin; }
    if (i2c_start() != TW_REP_START)                                     { error = -4; goto fin; }
    if (i2c_enviar((BME_DIR << 1) | TW_READ, TW_MR_SLA_ACK))              { error = -5; goto fin; }
    while (n--) *dst++ = i2c_recibir(n != 0);                            // ACK salvo en el último byte
fin:
    i2c_stop();
    return error;
}

static int8_t bme_escribir(uint8_t reg, uint8_t valor) {
    int8_t error = 0;
    if (i2c_start() != TW_START)                                         { error = -1; goto fin; }
    if (i2c_enviar((BME_DIR << 1) | TW_WRITE, TW_MT_SLA_ACK))             { error = -2; goto fin; }
    if (i2c_enviar(reg, TW_MT_DATA_ACK))                                 { error = -3; goto fin; }
    if (i2c_enviar(valor, TW_MT_DATA_ACK))                               { error = -4; goto fin; }
fin:
    i2c_stop();
    return error;
}

static int8_t bme_medir(int32_t *adc_T, int32_t *adc_P, int32_t *adc_H) {
    uint8_t d[8];
    if (bme_escribir(0xF2, 0x01)) return -1;                             // humedad ×1
    if (bme_escribir(0xF4, (1 << 5) | (1 << 2) | 0x01)) return -1;      // temperatura ×1, presión ×1, modo forzado
    _delay_ms(10);                                                       // conversión: 9,3 ms como máximo
    if (bme_leer(0xF7, d, 8)) return -2;
    *adc_P = ((int32_t)d[0] << 12) | ((int32_t)d[1] << 4) | (d[2] >> 4);
    *adc_T = ((int32_t)d[3] << 12) | ((int32_t)d[4] << 4) | (d[5] >> 4);
    *adc_H = ((int32_t)d[6] << 8) | d[7];
    return 0;
}

Antes de cualquier otra cosa, un programa prudente lee el registro 0xD0 y comprueba que vale 0x60. Si no, el sensor no está conectado, la dirección es la otra (0x77) o el chip no es un BME280. Es el equivalente a la lectura de identificación que recomendamos en SPI (tema 34).

05 · Paso 3

Compensar las lecturas

Cada BME280 sale de fábrica con 18 coeficientes medidos en ese chip: 3 de temperatura, 9 de presión y 6 de humedad. Se leen una sola vez al arrancar. Con ellos, las fórmulas que publica Bosch convierten las cuentas crudas en centésimas de grado, pascales y %HR × 1024, usando solo enteros de 32 bits. La temperatura se calcula primero porque genera una variable intermedia, t_fine, que necesitan la presión y la humedad.

static struct {
    uint16_t T1; int16_t T2, T3;
    uint16_t P1; int16_t P2, P3, P4, P5, P6, P7, P8, P9;
    uint8_t  H1; int16_t H2; uint8_t H3; int16_t H4, H5; int8_t H6;
} cal;

#define LE16(b, i) ((uint16_t)((b)[i] | ((uint16_t)(b)[(i) + 1] << 8)))

static void bme_calibracion(void) {
    uint8_t b[24], h[7];
    bme_leer(0x88, b, 24);                                   // 0x88 a 0x9F: temperatura y presión
    cal.T1 = LE16(b, 0);  cal.T2 = (int16_t)LE16(b, 2);  cal.T3 = (int16_t)LE16(b, 4);
    cal.P1 = LE16(b, 6);  cal.P2 = (int16_t)LE16(b, 8);  cal.P3 = (int16_t)LE16(b, 10);
    cal.P4 = (int16_t)LE16(b, 12); cal.P5 = (int16_t)LE16(b, 14); cal.P6 = (int16_t)LE16(b, 16);
    cal.P7 = (int16_t)LE16(b, 18); cal.P8 = (int16_t)LE16(b, 20); cal.P9 = (int16_t)LE16(b, 22);
    bme_leer(0xA1, &cal.H1, 1);
    bme_leer(0xE1, h, 7);                                    // humedad
    cal.H2 = (int16_t)LE16(h, 0);  cal.H3 = h[2];
    cal.H4 = (int16_t)(((int16_t)(int8_t)h[3] << 4) | (h[4] & 0x0F));
    cal.H5 = (int16_t)(((int16_t)(int8_t)h[5] << 4) | (h[4] >> 4));
    cal.H6 = (int8_t)h[6];
}

static int32_t bme_temperatura(int32_t adc_T, int32_t *t_fine) {            // centésimas de °C
    int32_t var1 = (((adc_T >> 3) - ((int32_t)cal.T1 << 1)) * (int32_t)cal.T2) >> 11;
    int32_t var2 = (((((adc_T >> 4) - (int32_t)cal.T1) * ((adc_T >> 4) - (int32_t)cal.T1)) >> 12)
                    * (int32_t)cal.T3) >> 14;
    *t_fine = var1 + var2;
    return (*t_fine * 5 + 128) >> 8;
}

static uint32_t bme_presion(int32_t adc_P, int32_t t_fine) {                // pascales
    int32_t var1 = (t_fine >> 1) - 64000;
    int32_t var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * (int32_t)cal.P6;
    var2 += (var1 * (int32_t)cal.P5) << 1;
    var2 = (var2 >> 2) + ((int32_t)cal.P4 << 16);
    var1 = (((cal.P3 * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) + (((int32_t)cal.P2 * var1) >> 1)) >> 18;
    var1 = ((32768 + var1) * (int32_t)cal.P1) >> 15;
    if (var1 == 0) return 0;                                                // evita dividir por cero
    uint32_t p = (((uint32_t)(1048576 - adc_P) - (uint32_t)(var2 >> 12))) * 3125;
    p = (p < 0x80000000UL) ? (p << 1) / (uint32_t)var1 : (p / (uint32_t)var1) * 2;
    var1 = ((int32_t)cal.P9 * (int32_t)(((p >> 3) * (p >> 3)) >> 13)) >> 12;
    var2 = ((int32_t)(p >> 2) * (int32_t)cal.P8) >> 13;
    return (uint32_t)((int32_t)p + ((var1 + var2 + cal.P7) >> 4));
}

static uint32_t bme_humedad(int32_t adc_H, int32_t t_fine) {                // %HR × 1024
    int32_t v = t_fine - 76800;
    v = (((((adc_H << 14) - ((int32_t)cal.H4 << 20) - ((int32_t)cal.H5 * v)) + 16384) >> 15) *
         (((((((v * (int32_t)cal.H6) >> 10) * (((v * (int32_t)cal.H3) >> 11) + 32768)) >> 10) + 2097152)
           * (int32_t)cal.H2 + 8192) >> 14));
    v -= (((((v >> 15) * (v >> 15)) >> 7) * (int32_t)cal.H1) >> 4);
    if (v < 0) v = 0;
    if (v > 419430400) v = 419430400;
    return (uint32_t)(v >> 12);
}

Estas fórmulas no se «entienden»: se copian del datasheet, y la prueba de que están bien copiadas es reproducir su ejemplo numérico. Con los coeficientes del ejemplo de Bosch, las cuentas 519 888 (temperatura) y 415 148 (presión) deben dar 25,08 °C y algo más de 100 650 Pa. La primera herramienta del laboratorio lo hace. El código C reproduce las mismas operaciones de 32 bits.

Con la presión se puede calcular la altitud: h = 44 330 · (1 − (p / p0)0,1903) metros, con p0 la presión al nivel del mar (1013,25 hPa en la atmósfera estándar). El clima mueve p0 hasta unos ±30 hPa (más de 200 m), por eso un barómetro no da altura absoluta sin referencia, aunque sí diferencias de altura (un piso son 0,4 hPa).

06 · Paso 4

El HC-SR04 con captura

El módulo se dispara con un pulso alto de al menos 10 µs en TRIG. Poco después emite 8 pulsos de 40 kHz y sube ECHO; ECHO baja cuando recibe el eco. Si no hay obstáculo, el módulo baja ECHO solo después de unos 38 ms. Se recomienda esperar al menos 60 ms entre mediciones, para que el eco anterior se extinga.

static volatile uint16_t t_subida;
static volatile uint16_t eco_cuentas;             // ancho del eco en cuentas de 4 µs
static volatile uint8_t  eco_listo;

static void sonar_init(void) {
    DDRD |=  (1 << PD2);                          // TRIG: salida
    DDRB &= ~(1 << PB0);                         // ICP1: entrada
    TCCR1A = 0;
    TCCR1B = (1 << ICNC1) | (1 << ICES1) | (1 << CS11) | (1 << CS10);   // filtro de ruido, flanco de subida, divisor 64
    TIMSK1 = (1 << ICIE1);                        // interrupción de captura
}

ISR(TIMER1_CAPT_vect) {
    if (TCCR1B & (1 << ICES1)) {                // flanco de subida: empieza el eco
        t_subida = ICR1;
        TCCR1B &= ~(1 << ICES1);                // ahora se espera el de bajada
    } else {                                      // flanco de bajada: termina el eco
        eco_cuentas = (uint16_t)(ICR1 - t_subida);   // resta modular de 16 bits
        TCCR1B |= (1 << ICES1);
        eco_listo = 1;
    }
}

static void sonar_disparar(void) {
    eco_listo = 0;
    TCCR1B |= (1 << ICES1);                       // siempre se empieza esperando la subida
    PORTD |=  (1 << PD2);
    _delay_us(10);
    PORTD &= ~(1 << PD2);
}

// Distancia en milímetros; temp_centi en centésimas de °C
static uint16_t distancia_mm(uint16_t cuentas, int32_t temp_centi) {
    uint32_t v10 = (uint32_t)(3313L + temp_centi * 606L / 10000L);   // velocidad en décimas de m/s
    uint32_t t_us = (uint32_t)cuentas * 4UL;
    return (uint16_t)(t_us * v10 / 20000UL);                          // d = t · v / 2
}

Detalles que importan. El divisor 64 da una cuenta de 4 µs y un desborde del contador a los 262 ms: sobra para el eco máximo de 38 ms. Con el divisor 8 (0,5 µs por cuenta), el contador de 16 bits da la vuelta a los 32,8 ms y un eco «sin obstáculo» de 38 ms se mediría como 5 ms, es decir, un objeto falso a 90 cm. La resta modular ICR1 − t_subida sobre enteros sin signo de 16 bits da el ancho correcto aunque el contador pase por cero entre los dos flancos (tema 24). El filtro de ruido (ICNC1) exige cuatro muestras iguales antes de aceptar un flanco: elimina falsos disparos a costa de 4 ciclos de retardo, irrelevante acá. Y la velocidad del sonido se calcula con una aproximación lineal, 331,3 + 0,606 · T m/s, que es válida en el rango ambiente y que en décimas de m/s se escribe 3313 + 0,0606 · Tcenti.

07 · Paso 5

El programa completo

int main(void) {
    uart_init();
    i2c_init();
    sonar_init();
    sei();

    uint8_t id = 0;
    bme_leer(0xD0, &id, 1);
    if (id != 0x60) { uart_texto("BME280 no encontrado\r\n"); for (;;) { } }
    bme_escribir(0xE0, 0xB6);  _delay_ms(5);          // reinicio por software
    bme_calibracion();

    for (;;) {
        int32_t aT, aP, aH, t_fine;
        if (bme_medir(&aT, &aP, &aH) != 0) { uart_texto("error I2C\r\n"); _delay_ms(500); continue; }
        int32_t  centi = bme_temperatura(aT, &t_fine);
        uint32_t pa    = bme_presion(aP, t_fine);
        uint32_t hum   = bme_humedad(aH, t_fine);

        sonar_disparar();
        for (uint8_t i = 0; i < 40 && !eco_listo; i++) _delay_ms(1);   // hasta 40 ms

        uart_texto("T=");  uart_fijo((int16_t)centi, 2);  uart_texto(" C  ");
        uart_texto("P=");  uart_u16((uint16_t)(pa / 100));       uart_texto(" hPa  ");
        uart_texto("H=");  uart_u16((uint16_t)(hum / 1024));     uart_texto(" %  ");
        uart_texto("D=");
        if (eco_listo) uart_u16(distancia_mm(eco_cuentas, centi)); else uart_texto("--");
        uart_texto(" mm\r\n");
        _delay_ms(500);
    }
}

El orden tiene un motivo: la lectura del BME280 va primero porque su temperatura se usa para corregir la distancia. El _delay_ms y la impresión por sondeo bloquean la CPU durante decenas de milisegundos, algo aceptable en un registrador lento. En un programa que además deba atender otras cosas, la medición del eco, la conversión del BME280 y la impresión se reparten con una máquina de estados (tema 42): la captura de entrada ya trabaja sola, y el BME280 mide en segundo plano.

08 · Paso 6

Verificar

  1. El identificador. El registro 0xD0 debe leerse como 0x60. Si da 0x58, es un BMP280 (sin humedad); si no hay respuesta, revisá el cableado y la dirección (0x76 o 0x77).
  2. La temperatura. Compará con otro termómetro o con el LM35 del taller del tema 30. El BME280 tiene una exactitud de ±1 °C, y se calienta un poco con su propio consumo y con la cercanía de la placa: separalo del microcontrolador unos centímetros.
  3. La presión. Debe estar entre 950 y 1050 hPa a nivel del mar. Compará con la presión informada por un servicio meteorológico cercano (hay que corregir por la altitud: unos 12 hPa cada 100 m). Subir el sensor a un piso más alto debe bajar la lectura unos 0,4 hPa.
  4. La humedad. Exhalar sobre el sensor debe subirla más de 20 puntos en unos segundos y volver a su valor en medio minuto.
  5. La distancia. Con una regla y una pared plana, medí a 20, 50, 100 y 200 cm. El error debe ser de unos pocos milímetros. Probá también un objeto inclinado (el eco se desvía y no vuelve) y uno blando como una tela (absorbe el sonido): son los fallos típicos del ultrasonido.
  6. El efecto de la temperatura. Calentá o enfriá el aire (con un secador, o saliendo al exterior en invierno): sin la corrección de temperatura, la distancia cambiaría un 0,17 % por cada grado.
  7. Los fallos. Tapá el HC-SR04 con la mano pegada (distancia mínima, menor de 2 cm: lectura inválida) y apuntalo al cielo (sin eco: D=--).

09 · Analogía con programación

Compensación, eco y tiempos

Este programa implementa la compensación entera y la de coma flotante del fabricante y las compara con el ejemplo de la hoja de datos, calcula la velocidad del sonido y el error de suponer 343 m/s, simula el desborde del contador de 16 bits con dos divisores y arma el presupuesto de tiempo de un ciclo de la estación.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

import math

# --- coeficientes de compensacion (valores del ejemplo del datasheet del BMP280) ---
CAL = dict(T1=27504, T2=26435, T3=-1000, P1=36477, P2=-10685, P3=3024, P4=2855, P5=140,
           P6=-7, P7=15500, P8=-14600, P9=6000)
# coeficientes de humedad: valores de ejemplo, no pertenecen a un chip real
CAL_H = dict(H1=75, H2=362, H3=0, H4=316, H5=50, H6=30)

def i32(x):
    x &= 0xFFFFFFFF
    return x - (1 << 32) if x & 0x80000000 else x

def u32(x):
    return x & 0xFFFFFFFF

def temperatura_entera(adc_T, c):
    var1 = (((adc_T >> 3) - (c["T1"] << 1)) * c["T2"]) >> 11
    var2 = ((((((adc_T >> 4) - c["T1"]) * ((adc_T >> 4) - c["T1"])) >> 12) * c["T3"]) >> 14)
    t_fine = var1 + var2
    return (t_fine * 5 + 128) >> 8, t_fine            # centesimas de grado

def presion_entera(adc_P, t_fine, c):
    var1 = (t_fine >> 1) - 64000
    var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * c["P6"]
    var2 = var2 + ((var1 * c["P5"]) << 1)
    var2 = (var2 >> 2) + (c["P4"] << 16)
    var1 = (((c["P3"] * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) + ((c["P2"] * var1) >> 1)) >> 18
    var1 = ((32768 + var1) * c["P1"]) >> 15
    if var1 == 0:
        return 0
    p = u32((1048576 - adc_P) - (var2 >> 12)) * 3125
    p = u32(p)
    if p < 0x80000000:
        p = u32(p << 1) // var1
    else:
        p = (p // var1) * 2
    var1 = (c["P9"] * (((p >> 3) * (p >> 3)) >> 13)) >> 12
    var2 = ((p >> 2) * c["P8"]) >> 13
    return p + ((var1 + var2 + c["P7"]) >> 4)            # pascales

def temperatura_float(adc_T, c):
    v1 = (adc_T / 16384.0 - c["T1"] / 1024.0) * c["T2"]
    v2 = (adc_T / 131072.0 - c["T1"] / 8192.0) ** 2 * c["T3"]
    t_fine = v1 + v2
    return t_fine / 5120.0, t_fine

def presion_float(adc_P, t_fine, c):
    v1 = t_fine / 2.0 - 64000.0
    v2 = v1 * v1 * c["P6"] / 32768.0
    v2 = v2 + v1 * c["P5"] * 2.0
    v2 = v2 / 4.0 + c["P4"] * 65536.0
    v1 = (c["P3"] * v1 * v1 / 524288.0 + c["P2"] * v1) / 524288.0
    v1 = (1.0 + v1 / 32768.0) * c["P1"]
    p = 1048576.0 - adc_P
    p = (p - v2 / 4096.0) * 6250.0 / v1
    v1 = c["P9"] * p * p / 2147483648.0
    v2 = p * c["P8"] / 32768.0
    return p + (v1 + v2 + c["P7"]) / 16.0

def humedad_entera(adc_H, t_fine, h):
    v = t_fine - 76800
    v = (((((adc_H << 14) - (h["H4"] << 20) - (h["H5"] * v)) + 16384) >> 15) *
         (((((((v * h["H6"]) >> 10) * (((v * h["H3"]) >> 11) + 32768)) >> 10) + 2097152) * h["H2"] + 8192) >> 14))
    v = v - (((((v >> 15) * (v >> 15)) >> 7) * h["H1"]) >> 4)
    v = max(0, min(419430400, v))
    return (v >> 12) / 1024.0                                # %HR

def humedad_float(adc_H, t_fine, h):
    v = t_fine - 76800.0
    v = (adc_H - (h["H4"] * 64.0 + h["H5"] / 16384.0 * v)) * (h["H2"] / 65536.0 * (1.0 + h["H6"] / 67108864.0 * v * (1.0 + h["H3"] / 67108864.0 * v)))
    v = v * (1.0 - h["H1"] * v / 524288.0)
    return max(0.0, min(100.0, v))

adc_T, adc_P, adc_H = 519888, 415148, 28500
t_c, t_fine = temperatura_entera(adc_T, CAL)
t_f, tf_f = temperatura_float(adc_T, CAL)
print(f"temperatura: entera {t_c / 100:.2f} C, coma flotante {t_f:.3f} C   (datasheet: 25,08 C)")
p_i = presion_entera(adc_P, t_fine, CAL)
p_f = presion_float(adc_P, tf_f, CAL)
print(f"presion: entera {p_i} Pa, coma flotante {p_f:.2f} Pa   (datasheet: 100653 Pa)")
h_i, h_f = humedad_entera(adc_H, t_fine, CAL_H), humedad_float(adc_H, tf_f, CAL_H)
print(f"humedad: entera {h_i:.2f} %, coma flotante {h_f:.2f} %")
altura = 44330 * (1 - (p_i / 101325) ** 0.1903)
print(f"altitud respecto de 1013,25 hPa: {altura:.1f} m")

peor_t = max(abs(temperatura_entera(a, CAL)[0] / 100 - temperatura_float(a, CAL)[0]) for a in range(480000, 560000, 997))
print(f"diferencia maxima de temperatura entera frente a flotante: {peor_t:.3f} C")

# --- HC-SR04: del tiempo del eco a la distancia ---
def velocidad_sonido(temp_c):
    return 331.3 + 0.606 * temp_c                             # m/s

print()
print("temperatura   velocidad    us por cm (ida y vuelta)")
for temp in (-10, 0, 20, 35):
    v = velocidad_sonido(temp)
    print(f"{temp:8d} C   {v:7.1f} m/s   {2e4 / v:6.2f}")

print()
print("error de usar 343 m/s fijos a 1 metro:")
for temp in (0, 20, 35):
    t_us = 2 * 1.0 / velocidad_sonido(temp) * 1e6
    medido = t_us * 1e-6 * 343 / 2
    print(f"  a {temp:2d} C el eco dura {t_us:6.0f} us y se calcula {medido * 100:6.1f} cm (error {(medido - 1) * 100:+.1f} cm)")

# --- captura con un temporizador de 16 bits: el eco "sin obstaculo" dura unos 38 ms ---
def medir(ancho_us, prescaler, f_cpu=16e6):
    tick = prescaler / f_cpu
    ticks = int(ancho_us * 1e-6 / tick)
    inicio = 12345
    fin = (inicio + ticks) & 0xFFFF                          # el contador de 16 bits se desborda
    medido = (fin - inicio) & 0xFFFF                         # resta modular
    return medido * tick * 1e6

print()
print("eco de 38 ms (sin obstaculo) y de 5,83 ms (1 metro), medidos con el temporizador de 16 bits:")
for prescaler in (8, 64):
    sin_obs, un_metro = medir(38000, prescaler), medir(5830, prescaler)
    print(f"  divisor {prescaler:2d}: 38 ms -> {sin_obs / 1000:5.2f} ms (distancia {sin_obs * 1e-6 * 343 / 2 * 100:6.1f} cm); 5,83 ms -> {un_metro / 1000:4.2f} ms")

# --- presupuesto de tiempo de un ciclo de la estacion ---
def ciclo(osrs=1, baud=9600, chars=48, i2c=100e3, dist_max=4.0, temp=25):
    conversion = 1.25 + 2.3 * osrs + (2.3 * osrs + 0.575) + (2.3 * osrs + 0.575)   # ms
    lectura_i2c = (9 * (1 + 1 + 1 + 8) + 3) / i2c * 1000                            # ms: escribir registro y leer 8 bytes
    eco = 2 * dist_max / velocidad_sonido(temp) * 1000
    imprimir = chars * 10 / baud * 1000
    return conversion, lectura_i2c, eco, imprimir

c, l, e, p = ciclo()
print()
print(f"ciclo: conversion BME280 {c:.1f} ms, lectura I2C {l:.2f} ms, eco maximo {e:.1f} ms, imprimir 48 caracteres a 9600 {p:.1f} ms")
print(f"secuencial: {c + l + e + p:.1f} ms; con la conversion y el eco solapados: {max(c, e) + l + p:.1f} ms")

Resultado: con los coeficientes del ejemplo del datasheet, la temperatura entera da 25,08 °C (25,082 con coma flotante) y la presión entera 100 656 Pa contra 100 653,27 de la versión de coma flotante: 3 Pa de diferencia, el costo de los 32 bits. La humedad con coeficientes de ejemplo coincide en 0,01 %HR. A 1 metro, suponer 343 m/s fijos da +3,5 cm a 0 °C y −2,7 cm a 35 °C. Con el divisor 8 un eco de 38 ms se mide como 5,23 ms (89,7 cm falsos); con el divisor 64 se mide bien. Un ciclo completo tarda 83 ms en secuencia y 74 ms si se solapan la conversión y el eco; imprimir 48 caracteres a 9600 baudios (50 ms) es lo que más pesa.

Los coeficientes de humedad de este programa son de ejemplo, no de un chip real; solo sirven para comprobar que la versión entera concuerda con la de coma flotante.

10 · Exploración interactiva

Eco, compensación y tiempos

La primera herramienta convierte el ancho del eco en distancia (y al revés) con la corrección por temperatura. La segunda compensa las cuentas crudas del BME280 con la fórmula entera de 32 bits. La tercera estima el tiempo de un ciclo de la estación.

Banco de la estación

1. HC-SR04: tiempo de vuelo y distancia

Velocidad del sonido—
Resultado—
Cuentas del temporizador—
Límite del contador—
Error de ignorar la temperatura—

2. BME280: compensación entera

Registros 0xF7 a 0xFE—
Temperatura—
Presión—
Humedad—
Altitud—

3. Tiempo de un ciclo

BME280—
Etapas—
Ciclo total—

Recorrido guiado

  1. Con 5830 µs y 20 °C: 100,1 cm. La velocidad es 343,4 m/s, es decir 58,24 µs por centímetro de distancia. Con el divisor 64, el eco ocupa 1458 cuentas de 4 µs (0,69 mm por cuenta).
  2. Pasá a 0 °C: ignorar la temperatura da +3,4 cm (3,5 %). Con el divisor 8, la nota avisa que el contador da la vuelta antes de los 38 ms.
  3. En la segunda herramienta, 519 888, 415 148 y 28 500 dan 25,08 °C, 100 656 Pa y 45,57 %HR. Los registros son 65 5A C0 7E ED 00 6F 54: presión, temperatura y humedad empaquetadas.
  4. Subí adc_T a 540 000: 31,38 °C y la presión sube a 101 625 Pa con las mismas cuentas de presión (la compensación depende de la temperatura).
  5. En la tercera: conversión de 9,3 ms, lectura I2C de 1,0 ms, eco máximo de 23,3 ms e impresión de 50 ms a 9600 baudios. El ciclo dura 83,6 ms en secuencia y 74,3 ms solapando. Pasá a 115 200 baudios: imprimir cae a 4,2 ms y el ciclo a unos 38 ms.
  6. Subí el sobremuestreo a ×16: la conversión pasa a 112,8 ms y ya domina el ciclo.
Desafío: la altura de un edificio con un barómetro

Con la presión de la planta baja (1006,56 hPa) y la del último piso (1002,3 hPa) se puede estimar la altura del edificio. La diferencia es 4,26 hPa, y como cerca del nivel del mar cada hPa son unos 8,4 m, el edificio mide unos 36 m. No hace falta conocer p0, porque el clima afecta ambas lecturas casi por igual. El ruido de la presión con sobremuestreo ×1 es de algunos pascales (decenas de centímetros de altura), y con más sobremuestreo y filtro baja a centímetros; promediar varias lecturas también ayuda.

11 · Siguiente nivel

Del prototipo al producto

  • Filtrar la distancia. El HC-SR04 produce lecturas atípicas (ecos perdidos, rebotes múltiples). Una mediana de 5 lecturas (tema 36) elimina los picos sin retardar el escalón.
  • No esperar parado. Reemplazar los _delay_ms por una base de tiempo y una máquina de estados (tema 42): disparar, esperar el eco, leer el BME280, imprimir.
  • Reducir el consumo. El BME280 en modo forzado consume microamperios entre mediciones; el microcontrolador puede dormir entre ciclos (tema 40). El HC-SR04 gasta 15 mA y se puede apagar con un transistor.
  • Calibrar. Comparar la distancia con una regla y la temperatura con un patrón, y corregir con un offset (tema 36). Para la altitud, ajustar p0 con la presión local del servicio meteorológico.
  • Ahorrar tiempo de bus. Leer la calibración una sola vez (ya se hace) y todos los datos de una vez en una lectura de ráfaga.
  • Detectar fallas. Un código crudo de 0x80000 en la presión (valor «omitido» tras un error) o un eco de 0 o de 38 ms exacto son lecturas que no corresponden a una medida.

12 · Comprensión

Confusiones frecuentes

«El BME280 me da la temperatura directamente»

Da cuentas de 20 bits. Hay que leer los coeficientes de fábrica de ese chip y aplicar la fórmula de compensación; sin ella el valor no significa nada.

«La distancia es el tiempo del eco por la velocidad del sonido»

Falta dividir por 2: el eco recorre el camino de ida y de vuelta. Es el error más común al implementar el HC-SR04.

«Con un contador de 16 bits siempre me alcanza»

Depende del divisor. Con divisor 8 el contador da la vuelta a los 32,8 ms y un eco sin obstáculo de 38 ms se mide mal. Hay que comparar el máximo del sensor con el alcance del contador.

«La velocidad del sonido es 343 m/s»

Lo es a 20 °C. Cambia unos 0,6 m/s por grado: a 0 °C son 331 m/s y a 35 °C, 352 m/s. A un metro de distancia son más de 3 cm de diferencia.

«La altitud sale directamente de la presión»

Solo con una referencia: la presión al nivel del mar cambia con el clima hasta ±30 hPa. Las diferencias de altura entre dos puntos cercanos sí son confiables.

13 · Práctica guiada

Predecí antes de comprobar

1. De eco a distancia

El eco dura 11,6 ms a 20 °C. ¿A qué distancia está el objeto y cuántas cuentas del Timer1 (divisor 64) son?

Ver solución

d = 11 600 µs × 343,4 m/s / 2 = 199,2 cm. Cuentas = 11 600 / 4 = 2900.

2. El error de la temperatura

Un objeto está a 2,00 m y el aire está a 35 °C, pero el programa supone 343 m/s. ¿Qué lee?

Ver solución

La velocidad real es 331,3 + 0,606 × 35 = 352,5 m/s. El eco dura 2 × 2 / 352,5 = 11,35 ms. Con 343 m/s se calcula 11,35 ms × 343 / 2 = 194,6 cm: 5,4 cm de menos.

3. Empaquetar y desempaquetar

Los bytes de temperatura son 0x7E, 0xED, 0x00. ¿Qué valor de adc_T forman?

Ver solución

adc_T = (0x7E << 12) | (0xED << 4) | (0x00 >> 4) = 516 096 + 3792 + 0 = 519 888.

4. El desborde del contador

Con el divisor 8 a 16 MHz, ¿hasta qué eco puede medir el Timer1 sin dar la vuelta y a qué distancia equivale?

Ver solución

65 536 × 0,5 µs = 32,8 ms, que equivalen a 5,6 m a 20 °C. Un eco de 38 ms excede ese límite y se mediría como 38 − 32,8 = 5,2 ms.

5. Una altitud

La presión medida es 954,6 hPa y la del nivel del mar, 1013,25 hPa. ¿A qué altitud estás?

Ver solución

h = 44 330 × (1 − (954,6 / 1013,25)0,1903) = 44 330 × (1 − 0,9887) = unos 500 m.

14 · Síntesis

Ideas para recordar

  • Un sensor digital entrega cuentas crudas más coeficientes de fábrica: el programa los lee una vez y aplica la fórmula de compensación del fabricante, con enteros de 32 bits.
  • El BME280 se maneja en modo forzado: se ordena una medición, se espera su duración (9,3 ms con ×1) y se leen los 8 bytes de datos en una ráfaga; el registro 0xD0 identifica el chip.
  • La temperatura se calcula primero porque produce t_fine, que necesitan la presión y la humedad.
  • El HC-SR04 convierte la distancia en el ancho de un pulso: d = t · v / 2. El temporizador con captura de entrada mide ese ancho sin que la latencia de la interrupción lo afecte.
  • La velocidad del sonido (331,3 + 0,606 · T m/s) depende de la temperatura; ignorarla cuesta más de 3 cm por metro entre 0 y 35 °C.
  • Hay que comparar el alcance del contador con el eco máximo (38 ms): el divisor 64 sirve, el 8 no.
  • El tiempo de un ciclo lo dominan las esperas y la impresión por sondeo; solaparlas con interrupciones y máquinas de estados lo acelera.

¿Listo para el tema 38?

Antes de avanzar, explicá cómo se mide el ancho de un pulso con la captura de entrada, por qué se calcula primero la temperatura del BME280 y qué pasa si el eco excede el alcance del contador. Si podés justificar las tres respuestas, tenés dominados los sensores digitales y de tiempo.

En el próximo tema pasamos del lado de los actuadores: control de actuadores: potencia desde un pin. Un pin entrega 20 mA; un motor necesita amperios. Veremos transistores, MOSFET, relés, el diodo de rueda libre y los drivers.