jueves, 10 de septiembre de 2020

ADC Lectura de Celda con HX711

Medición de Peso con PIC16F


 
Este sitio fue creado para compartir el mutuo interés en la electrónica y en especial la programación de microcontroladores PIC.

Ahora veremos como realizar una medición de peso utilizando un modulo muy comercial que utiliza el chip HX711. Nuestro objetivo es poder medir con el PIC16F887 la magnitud en gramos o kilogramos haciendo uso de una celda de carga y el modulo amplificador HX711. El valor final del peso, se enviara como mensaje mediante una comunicación serial con el ordenador PC.

La programación se realizara utilizando el compilador XC8 y el entorno de desarrollo integrado MPLABX disponibles en la pagina de microchip de forma gratuita, por lo que sera necesario contar con conocimientos previos de microcontrolador y lenguaje C.



1. Celda de carga

Una celda de carga eléctrica permite traducir la fuerza aplicada sobre una estructura, en una señal de voltaje lineal que un microcontrolador puede medir utilizando un amplificador y convertidor ADC.

Fig1. Tipos de celdas

La celda esta compuesta por un metal y al menos una galga extensométrica adherida en su interior de tal manera que cuando aplicamos una fuerza en ella, el metal se deforma cambiando la resistencia de la galga y como esta se conecta utilizando un arreglo conocido como puente wheatstone, su salida entrega una señal de voltaje proporcional a la fuerza aplicada. El metal de la celda se calcula para soportar un rango de fuerza ya sea de tensión, compresión o ambos limitando la deformación que sufre a una capacidad máxima de carga. Si se excede este limite el metal sufre una deformación permanente que imposibilita su regreso al punto inicial.

Fig2. Puente Wheatstone 1, 2 y 4 galgas

El voltaje de salida de una celda es lineal con amplitudes que se encuentran en el orden de los milivoltios, y el fabricante denota este valor proporcionando la variación mV por cada Voltio de alimentacion. Por ejemplo si la hoja de datos de una celda indica que la salida es 1mV/V, esto nos dice que si alimentamos la celda con 10V el valor final a su máxima capacidad de carga sera 10mV.

En caso de conectar mas de una celda en paralelo, la capacidad de carga total se suma y la señal de salida permanece igual siempre que utilicemos un circuito sumador conocido como combinador, caso contrario se divide por la cantidad de celdas conectadas.

Fig3. Conexión en paralelo

La conexión en paralelo a menudo requiere utilizar resistencias adicionales de bajo valor que se calculan para compensar la variación nominal y distribución de carga entre las celdas, los detalles respecto a este punto no se consideran en este articulo del blog, pero la siguiente imagen ilustra un circuito con todos lo elementos necesarios para este fin.

Fig4. Circuito de compensación

Uno de los factores externos que afecta a una celda eléctrica es la temperatura, y por esta razón es necesario registrar el voltaje de salida sin peso, para luego considerar la variación que tiene y compensar la diferencia utilizando un peso conocido. De esta manera sera posible calcular la magnitud en gramos o kg.

La celda de carga que utilizare presenta las siguientes características.
  • Capacidad de carga = 5000gr
  • Precisión = 0.05%
  • Voltaje de excitación = 3 - 14V  
  • Relación de Salida =  1.0± 0.15mV/V
  • Efecto de la temperatura = 0.05% F.S./10°C
Fig5. Celda de un solo punto


2. Modulo Amplificador HX711
Como la salida de una celda de carga es un voltaje analógico es de muy baja amplitud, sera necesario amplificar la magnitud para su tratamiento. En un sistema digital esta señal deberá convertirse a un valor digital utilizando un convertidor analógico a digital ADC.

El circuito integrado HX711 incorpora un amplificador de ganancia programable PGA y un convertidor ADC de 24 bits de dos canales, y lo interesante es que su diseño permite conectar directamente a la celda de carga, reduciendo gran parte del circuito necesario para que un microcontrolador pueda medir el peso. El circuito esta disponible únicamente en formato SOP de 16 pines.


Fig6. Circuito HX711

Por fortuna este circuito circuito integrado es muy comercial y puede adquirirse como un modulo listo para ser integrado a nuestro sistema digital. 

Fig7. Modulo HX711

Alguno detalles adicionales del HX711 son:
  • El voltaje de alimentación de 2.6 a 5.5V
  • Rango de temperatura de -40 a 85°C 
  • Ganancia canal A ajustable de 64 o 128
  • Ganancia canal B fija de 32
  • Rango entrada diferencial ±0.5V/Ganancia
  • Salida serial sincronizada
Un dato importante con respecto a la lectura de datos, es que el pin RATE permite ajustar el tiempo a 10Hz (RATE = 0) o 80Hz (RATE = 1), por lo tanto sera necesario verificar en el modulo a que nivel esta conectado este pin.
 
La comunicación serial utiliza un pin de entrada SCK para recibir pulsos de sincronización del microcontrolador y un pin de salida DT para enviar los datos digitales al microcontrolador. Así mismo la salida DT cambia a nivel bajo cuando hay datos listos para enviar, esperando entre 25 y 27 pulsos de sincronización para enviar cada uno de los bits.

Durante una lectura el microcontrolador debe enviar mínimo 25 pulsos de reloj, y opcionalmente los pulsos 26 y 27 para determinar con que canal y ganancia se trabajara.

Fig8. Ajuste de Canal y PGA

Como el rango de voltaje a escala completa para la conversión es ±0.5V/Ganancia, el rango final sera de ±80mV si la ganancia es 32, ±40mV si la ganancia es 64 y ±20mV con una ganancia de 128. El valor digital en salida esta en formato complemento a dos en el rango de 800000h a 7FFFFFh.

Si alimentamos la celda con 5V y esta tiene una salida de 1.0mV/V, el voltaje final a máxima capacidad de carga sera 5mV, para trabajar esta variación conviene utilizar el canal A con ganancia 128 porque el rango de entrada para la conversión seria ±20mV, y la relación de voltaje por bit se calcula en Rv =0.040mV/2^24bits. Con esta configuración si aplicamos el peso máximo de 5kg a la celda, su salida llegara a medir 4.15mV y el resultado digital de la conversión puede calcularse dividiendo este valor con la relación V/bits, esto es +0.005mV / Rv = 2097152 o 200000h, tomar en cuenta que este resultado estará en formato complemento a dos y para obtener un valor absoluto debemos sumar 800000h.

Otro dato que podemos calcular es la relación de bits por unidad de peso, para eso utilizare el gramo como unidad de medida donde 5000gr representa 0.005mV en la salida, y el voltaje para 1gr se calcula en 0.005mV x 1gr / 5000gr = 0.000001mV. Entonces la relación de bits por unidad es 0.000001mV / Rv = 41.9 bits, es decir que por cada gramo de peso adicional el resultado digital se incrementa con 42 unidades.

Luego notara que en la practica el voltaje que aplica el modulo HX711 es 4.15V debido en parte a la caída de voltaje del transistor PNP que alimenta a la celda, por lo tanto el voltaje final a máxima capacidad sera solo 4.15mV y por esta razón las lecturas que recibe el PIC16F887 serán de magnitud inferior a los valores calculados, lo bueno es que este punto tampoco no afectara al calculo final del peso debido que un procedimiento de calibración ajustara el valor de referencia y luego se utilizara unicamente la variación, siendo el mayor requisito la linealidad del sensor.

3. Esquema del circuito PIC16F887
La siguiente imagen describe en forma resumida el esquema de conexión para la celda, el amplificador y el microcontrolador utilizado en el ejemplo. El PIC16F887 se alimentara con 5V y la programación se efectuara vía ICSP, un LED conectado al pin RE2 destellara para indicar que el programa esta funcionando, un pulsador BUT conectado al pin RB2 servirá para iniciar el procedimiento de calibración y el pin RC6 corresponde a la salida TXD del modulo USART. La información del peso se enviara en un mensaje al ordenador utilizando un convertidor serial TTL/USB.

Fig9. Circuito para Medición de Peso


4. Programa de Lectura

Ahora crearemos el programa de lectura para el PIC16F887, y para esto necesitamos revisar la señalización de salida que tiene el amplificador HX711 que es relativamente simple de acuerdo a los siguientes pasos:
  • El HX711 coloca el pin DOUT en nivel bajo para indicar que hay datos listos para enviar.
  • El PIC16 detecta si hay disponibilidad de datos verificando la linea DOUT.
  • Para iniciar la lectura el PIC16 enviá mínimo 25 pulsos de reloj, y entre cada pulso realiza la lectura de nivel de la linea DOUT, almacenando estos valores en un registro de 24bits.
  • Opcionalmente el PIC puede enviar un o dos pulsos para cambiar de canal o ganancia en la siguiente lectura.


Fig10. Señalización serial

El programa descrito a continuación muestra el uso de la función de lectura del canal A con ganancia 128 para el modulo HX711, esta función llamada HXReadvalue devuelve un valor absoluto.

#pragma config FOSC = INTRC_NOCLKOUT
#pragma config WDTE = OFF, LVP = OFF
#include <stdio.h>
#include <xc.h>
#define _XTAL_FREQ 8000000 //Frecuencia 8MHz
#define HX_DATpin PORTBbits.RB1 //Pin de salida DOUT
#define HX_SCKpin PORTBbits.RB0  //Pin de pulsos CLK
#define LEDpin    PORTEbits.RE2 //Pin de salida LED
unsigned long adcval = 0; //Registro de 32 bits
unsigned long HXReadvalue(); //Prototipo de función
void main()
{
    OSCCONbits.IRCF = 0b111; //Oscilador a 8MHz Tcy 0.5u
    while(!OSCCONbits.HTS); //Espera hasta estabilizar INTOSC
    ANSEL = 0;    //Deshabilita los pines Analógicos ANS0-ANS7
    ANSELH = 0; //Deshabilita los pines Analógicos ANS8-ANS13
    TRISEbits.TRISE2 = 0; //Salida pin LED
    TRISBbits.TRISB0 = 0; //Pin CLK de salida
    TRISBbits.TRISB1 = 1; //Pin DOUT como entrada
    //OPTION_REGbits.nRBPU = 0; //Activa las Pullup del PORTB
    TXSTAbits.BRGH = 1;    //USART en modo de alta velocidad
    BAUDCTLbits.BRG16 = 0; //Generador de Baudios de 8-bit
    SPBRG = 52;    //Fosc/(16x(9600+1))
    TXSTAbits.TXEN = 1; //Habilita el transmisor
    RCSTAbits.SPEN = 1; //Habilita el modulo USART
    while(1)
    {
        adcval = HXReadvalue();
        adcval &= 0x00FFFFFF; //Enmascara los 24bits
        printf("VAL:%lu\r\n", adcval); //Muestra el valor
        __delay_ms(1000);
        LEDpin = !LEDpin; //Destello LED
    }
}

unsigned long HXReadvalue()
{
    unsigned long value = 0;
    char i = 0;
    HX_SCKpin = 0;
    while(HX_DATpin); //Espera disponibilidad
    for(i = 0; i < 24; i ++) //Solo 25 pulsos Canal A
    {
        HX_SCKpin = 1;
        value <<= 1; //1uS retardo
        HX_SCKpin = 0;
        if(HX_DATpin) //Lectura del pin DOUT
            value ++; //Coloca 1 al bit lsb
    }
    HX_SCKpin = 1;
    value ^= 0x800000; //Convierte a un valor absoluto
    HX_SCKpin = 0;
    return value;
}

Al correr este programa y conectado a una terminal podemos observar que los datos recibidos presentan fluctuaciones en los ultimo dígitos, y por ello es preferible trabajar con promedios por intervalo de tiempo.
Para no efectuar cambios mayores a este programa y debido que el modulo que utilizo tiene el pin RATE conectado a tierra, lo cual me dice que los datos estarán disponibles cada 0.1 segundo(10Hz), siendo necesario al menos 10 segundos para efectuar un promedio de 100 lecturas, adicionare una linea al programa principal para descartar los últimos 4 bits, y así obtener un valor mas estable. Además considerando que la relación de bits por gramo calculado es de 41.9, el máximo error provocado si la diferencia fuera 15(4-bits), sera 0.5 gramos. 

Fig11. Resultado de lectura de celda

Con la linea adicional el bucle indefinido del programa principal queda de la siguiente manera.

    while(1)
    {
        adcval = HXReadvalue();
        adcval &= 0x00FFFFFF; //Enmascara los 24bits
        adcval = adcval >> 4; //Descarta los últimos 4 bits
        printf("VAL:%lu\r\n", adcval); //Muestra el valor
        __delay_ms(1000);
        LEDpin = !LEDpin; //Destello LED
    }

Fig12. Resultado de celda sin los últimos 4 bits

En este punto lo que necesitamos para que el PIC pueda determinar y mostrar el peso en gramos es agregar un algoritmo que determine la diferencia entre una lectura sin peso y otra con un peso conocido, esta diferencia servirá para efectuar un calculo final aplicando una regla de tres. Un ejemplo para las lecturas de la figura anterior, tomando en cuenta los valores que mas se repiten sera: El valor de referencia sin peso Dref = 531356, y la diferencia cuando se utiliza un peso de 350 gramos es Dif = 539550 - Dref = 8194, entonces para determinar un nuevo peso aplico la siguiente regla Peso = (Lectura - Dref) * 350 / Dif.

Ahora modificamos el programa con estos valores de referencia calculados para obtener las siguientes lecturas de la imagen inferior.

while(1)
{
    adcval = HXReadvalue();
    adcval &= 0x00FFFFFF; //Enmascara los 24bits
    adcval = adcval >> 4; //Descarta los últimos 4 bits
    if(adcval > 531356) //No debe ser menor a la Dref
    adcval = (adcval - 531356) * 350 / 8194; //Formula
    else adcval = 0;
    printf("VAL:%lu\r\n", adcval); //Muestra el valor
    __delay_ms(1000);
    LEDpin = !LEDpin; //Destello LED
}

Fig13. Lecturas de peso en gramos

Se observa que los cálculos realizados por el programa ya ajustado a las referencias medidas, muestran una variación con los pesos reales indicados en la imagen. Esto se debe a que utilize unos envases de conserva que no tienen una medida exacta, incluso ensayando dos de ellos al azar, uno indica 141 gramos y el otro 152 gramos, por lo tanto para efectuar un ajuste real debes utilizar pesos de referencia exactos. 

Fig14. Balanza de prueba


5. Programa de Calibración

Es necesario crear un procedimiento de calibración que permita registrar el valor de salida que tiene la celda sin peso alguno y luego registrar el valor de salida con un peso conocido para así determinar la variación resultante, luego esta información debería almacenarse en una memoria no volátil para evitar repetir el proceso cada vez que el microcontrolador se reinicie, la calibracion solo es necesario cuando la celda es reemplazada o cuando la variación de temperatura sea considerable, para darnos una idea sobre este punto, revisando la característica de la celda que utilizo me indica que el efecto de temperatura es 0.05% F.S./10°C, lo que significa que la variación por cada 10°C sera de 2.5 gramos(0.05/100) * 5000).
 
Si el peso de referencia es 140g, la ecuación de calculo analizado anteriormente resulta en Peso (gramos) = (Lectura - Dref) * 140 / Dif.

El código de ejemplo para este procedimiento realiza la calibracion utilizando el pulsador BUT en tres pasos:

paso 1. Cuando se presiona el pulsador por al menos un segundo,  se inicia el procedimiento de calibracion y se mantiene en modo espera indicando por mensaje que se libere de peso la balanza
paso 2. Con la balanza libre de peso, se presiona nuevamente el pulsador para que el procedimiento registre el valor de la celda Dref y luego envié otro mensaje solicitando colocar un peso conocido, en nuestro caso 140 gramos.
paso 3. Ya con el peso de referencia sobre la balanza, se presiona nuevamente el pulsador para que el procedimiento registre el valor de la celda y calcula la diferencia Diff = Valor con peso - Dref.
 
Con la finalidad de que el código escrito hasta el momento no sufra cambios en su lógica, es que las lineas de código necesarias para este procedimiento de ajuste se adicionan al bucle principal utilizando retardos indefinidos, con el único fin de mostrar su funcionamiento.  Para una implementación real es recomendable apoyar este procedimiento con interrupciones o técnicas de programación mas robustas como ser una maquina estados finitos. 

Entonces al bucle del programa principal adicionaremos la detección del pulsador, que en caso de ser presionado llamara al procedimiento de Ajuste


while(1)
{
    adcval = HXReadvalue();
    adcval &= 0x00FFFFFF; //Enmascara los 24bits
    adcval = adcval >> 4; //Descarta los últimos 4 bits
    if(adcval > Dref) //No debe ser menor a la Dref
    adcval = (adcval - Dref) * 140 / Diff; //Formula
    else adcval = 0;
    printf("VAL:%lu\r\n", adcval); //Muestra el valor
    __delay_ms(1000);
    LEDpin = !LEDpin; //Destello LED
    if(BUTpin == 0) //Primer pulso de boton
    {
        delay_ms(50);
        if(BUTpin == 0)
        {
            while(BUTpin);
            Ajuste(); //llama al procedimiento de ajuste
        }
    }
}



El procedimiento de ajuste bloqueara la secuencia del bucle principal con su propio bucle el cual finalizara solo cuando el pulsador es presionado por tercera vez.


void Ajuste() //Procedimiento para mostrar
{
    char paso = 0, loop = 1;
    printf("Retire Peso y presione BUT\r\n"); //Muestra el valor
    while(loop)
    {
        adcval = HXReadvalue();
        adcval &= 0x00FFFFFF; //Max 8000000h set MSB to 0
        adcval >>= 4;
        printf("ADC:%lu\r\n", adcval);
        __delay_ms(1000);
        if(BUTpin == 0) //Segundo pulso de botón
        {
            __delay_ms(50);
            if(BUTpin == 0) //Tercer pulso de botón.
            {
                while(BUTpin);
                if(paso == 1)
                {
                    Diff = adcval - Dref;
                    printf("Ajuste final %06u", Diff);
                    loop = 0; //Finaliza el bucle del procedimiento
                }
                if(paso == 0)
                {
                    Dref = adcval;
                    paso ++;
                    printf("Coloque Peso y presione BUT\r\n");
                }
            }
        }
    }
}

Compilamos y cargamos nuevamente el código al microcontrolador para efectuar una prueba de funcionamiento, logrando el resultado que se ilustra en la siguiente imagen.
Fig15. Resultados de una calibracion.



6. Interfaz visual con QT

Esta parte es opcional solo para demostrar como crear una aplicación de escritorio que visualice la información del peso en gramos con un formato mas legible y elegante. Se da por entendido que ya cuentas con el conocimiento y experiencia en la programación orientada a objetos con C++. Para crear la esta aplicación de Escritorio utilizare la plataforma de código abierto  QT(Community), con el IDE QTCreator instalado en el sistema operativo Linux-Debían 11.

El primer paso una vez iniciado QTCreator, es crear un proyecto para escritorio(Qt Widgets Applicaction), en el ejemplo se coloca el nombre HX711_PICMonitor. Ya con el proyecto creado y configurado se adicionara al formulario visual mainwindow.ui los objetos visuales que formaran parte de la interfaz. La siguiente figura muestra los elementos utilizados en la aplicación.
Fig16. Aplicación de Lectura MonitorHX711

Para establecer la comunicación y recibir los datos serie del PIC, QT cuenta con la clase QSerial y debemos incluirlo en el proyecto adicionando a la linea QT += serialport en el archivo de proyecto HX711_PICMonitor.pro, Luego de esto solo resta trabajar con los archivos del programa principal que son: mainwindows.cpp y mainwindows.h
 
 //Archivo mainwindows.cpp
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    port = new QSerialPort();
    connect(port, SIGNAL(readyRead()), this, SLOT(readSerialPort()));
    foreach(const QSerialPortInfo &portinfo, QSerialPortInfo::availablePorts())
    {
        ui->comboBox->addItem(portinfo.systemLocation());
    }
}

MainWindow::~MainWindow()
{
    delete ui;
}
void MainWindow::readSerialPort()
{
    QByteArray databytes;
    if(port->canReadLine()) //Espera el siguiente Texto VAL:347\r\n
    {
        databytes = port->readLine();
        QString msg(databytes);
        msg.remove(0,4); //Quita el texto "VAL:"
        ui->lcdNumber->display(msg.toInt()); //Muestra el valor
    }
}

void MainWindow::on_checkBox_stateChanged(int arg1)
{
    if(ui->checkBox->isChecked())
    {
        port->setPortName(ui->comboBox->currentText());
        port->setBaudRate(port->Baud9600, port->Input);
        port->setFlowControl(port->NoFlowControl);
        if(port->open(QIODevice::ReadOnly))
            ui->statusbar->showMessage("Puerto Conectado", 0);
        else
            ui->statusbar->showMessage("Error de conexion", 0)
    }
    else
    {
        if(port->isOpen())
       {
            port->close();
            ui->statusbar->showMessage("Puerto Desconectado", 5000);
        }
    }
}
 
    //Archivo mainwindows.h
    #include <QMainWindow>
    #include <QSerialPort>
    #include <QSerialPortInfo>
    QT_BEGIN_NAMESPACE
    namespace Ui { class MainWindow; }
    QT_END_NAMESPACE
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
        public:
        MainWindow(QWidget *parent = nullptr);
        ~MainWindow();
        private slots:
        void readSerialPort();
        void on_checkBox_stateChanged(int arg1);
        private:
        Ui::MainWindow *ui;
        QSerialPort *port;
    };
 
En la siguiente imagen se muestra como la aplicacion Qt informa del peso en gramos que envia el microcontrolador.
Fig17. Aplicación de Lectura MonitorHX711
 
 
7. Recomendación y Conclusión

Es recomendable utilizar una celda que tenga un capacidad superior a los limites que utilizo en el diseño. por ejemplo si considero que el limite de carga es 100Kg, utilice una celda de 150Kg. Caso contrario reduzca el limite de carga máxima. 

También mencionar que si bien el valor referencia Dref medido cada dia, cada hora o cuando se apaga y enciende el circuito puede variar considerablemente, la relación Diff se mantendra, una opcion interesante para evitar estas variaciones es hacer que el proceso de lectura inicie siempre con la celda libre de carga, para registrar ese valor como la referencia inicial Dref y luego colocar la carga a medir, este lapso no demoria mas de un segundo.

Aquí dejo los enlaces para que puedas descargar los proyectos creados con MPLAB y QT para este blog, como usuario de linux comentar que ambos proyectos están comprimidos en gzip y es probable que necesites modificar algunas referencias si quieres compilar en Windows.
Proyecto MPLABX hx711p16f
Proyecto QT5 HX711_PICMonitor

Sin mas que mencionar agradezco tu visita al blog y espero que el ejemplo visto pueda ser útil en tu formación y el proyecto que desarrollas.
Atte. Pablo Zárate Arancibia
email: pablinza@me.com / pablinzte@gmail.com
@pablinzar
Santa Cruz de la Sierra - Bolivia

Referencias Adicionales.