Tue. Aug 16th, 2022

Este artículo repasa el diseño de firmware para un proyecto de PCB personalizado: un subsistema de inclinómetro de precisión.

Recientemente diseñé una PCB personalizada con un inclinómetro IC muRata SCA103T-D04. Mi objetivo era construir un subsistema con una precisión realmente ridícula, capaz de detectar con precisión hasta una milésima de grado de inclinación.

Este artículo cubrirá algunos aspectos destacados de cómo diseñé el firmware para el microcontrolador MSP430 de la placa para recopilar y procesar datos del IC.

Para ponerse al día con el proyecto en general, consulte los artículos a continuación:

Empezando

Es difícil crear un circuito electrónico en estos días sin tener que hacer una cierta cantidad de programación. Desafortunadamente, el conocimiento que obtiene al aprender a crear firmware para un fabricante no es necesariamente aplicable a otro.

Por esta razón, generalmente es una buena idea comprar un Tablero de evaluación para que pueda aprender a programar su microcontrolador en un tablero que seguramente funcionará antes de arrancarse el cabello con su propio diseño. También es una buena idea familiarizarse con los recursos que el fabricante ha puesto a su disposición: foros, hojas de datos, guías de usuario, videos, etc.

En este caso, utilicé el MSP430FR2633, así que practiqué con el kit de desarrollo MCU CapTIvate MCU y me dirigí a la comunidad E2E de TI para pedirle consejo.

Este artículo no cubre cada línea del código fuente; más bien, proporciona información general sobre la programación del firmware utilizando el código fuente como ejemplo.

Agregando un Diagrama de Pinout ASCII

Finalmente, quiero agregar alguna referencia extra para mí. Dado que este es un panel personalizado, quiero proporcionarme tanta información en un lugar como sea posible. Existe una gran posibilidad de que no recuerde cuáles son mis conexiones de pin dentro de una hora, y ciertamente no recordaré dentro de una semana. Si realizo algún cambio en la programación, sería bueno no tener que desenterrar los esquemas.

Por esa razón, incluí un diagrama de conexión en el código fuente. Hay una variedad de Los generadores de diagramas ASCII disponibles en la web hacen de esta una operación relativamente rápida.

                    // CP2102N ┌──────┬──────┐ ┌────────────┐
// ┌────────────┐ │ │ P1.0 │ → UCB0STE → │ EN (NC)
// │ USB │ → RX → │ P2.5 │ P1.1 │ → UCB0CLK → │ CLK │
// │ TO │ → TX ← │ P2.6 │ P1.2 │ → UCBSIMO → │ SIMO (NC)
// │ UART │ │ │ P1.3 │ ← UCBSOMI ← │ SOMI │
// └────────────┘ ├──────┼──────┤ │ │
// ST_IN1 ← │ P2.0 │ P2.4 │ ← BUSY ← │ BUSY │
// ST_IN2 → │ P2.1 │ P3.0 │ → RDL → │ RDL │
// │ │ P3.1 │ → CNV → │ CNV │
// └──────┴──────┘ └────────────┘
// MSP430FR2633 ADC

                  

Lo primero que tendremos que hacer es lidiar con nuestros pines para que el compilador sepa qué dispositivos están conectados a qué. Para hacer eso, necesitarás entender el concepto de registros. Si ya está familiarizado con estos conceptos, siéntase libre de saltar a la sección "Definir accesos directos de descripción de registro" a continuación.

¿Qué son los registros?

Los registros son ubicaciones en la memoria identificadas por números hexadecimales, y los bits pertenecen al registro. Los bits en cada registro controlan un aspecto específico de la funcionalidad del microcontrolador.

Cada registro controla un byte (8 bits) o una palabra (16 bits). Para mantener la discusión simple, sin embargo, los siguientes diagramas ilustran los registros que controlan un solo byte.

Definición de accesos directos de descripción de registro

Ahora que tenemos un breve resumen de los registros, vamos a usarlo en contexto.

Las descripciones de registro en el La Guía del usuario de la familia MSP430FR2xx indica qué bits controlan qué funciones. Cuando un registro no permite el acceso directo a bits individuales, puede usar operaciones de bytes junto con una máscara de bits apropiada; los bits individuales se pueden configurar con el operador "| =", se pueden borrar con el operador "& =" o se pueden alternar con el operador "^ =".

El diagrama anterior muestra los datos en los registros imaginarios W, X, Y y Z antes y después de cuatro tipos de operaciones de bytes.

Los bits de configuración se proporcionan en las hojas de datos en notación hexadecimal o binaria. Sin embargo, es bastante inconveniente escribir cosas como 0x3F2A & = 0xC9 y hacer un seguimiento de lo que está sucediendo en un programa. Por lo tanto, los nombres de registro y pin se definen en los archivos de encabezado de referencia o al principio del programa. Y los nombres del registro y los pines asociados con ese registro se utilizan en lugar de los números hexadecimales y binarios sin procesar. Por ejemplo, “WDTCTL = WDTHOLD” (registro de control del temporizador de WatchDog = valor de retención del temporizador de WatchDog), en otras palabras, detenga el temporizador de vigilancia.

Por lo tanto, en lugar de utilizar direcciones hexadecimales, la programación se convierte en una cuestión de especificar registros y bits utilizando estos accesos directos con nombre y modificando bits directamente o mediante operaciones de bytes (como las mencionadas anteriormente).

                    vacío principal
{
    WDTCTL = (WDTPW | WDTHOLD); // Detener el temporizador de vigilancia
    ...
    CSCTL3 | = SELREF__REFOCLK; // Establecer REFO como fuente de referencia FLL
    CSCTL0 = 0; // borrar los registros DCO y MOD
    CSCTL1 & = ~ (DCORSEL_7); // Borrar los bits de selección de frecuencia DCO primero
    CSCTL1 | = DCORSEL_5; // Establecer DCO = 16MHz
    ...

                  

Nombrar variables: posición de bits de los pines

Para el MSP430, cada puerto controla una colección de pines: los GPIO suelen ser de ocho bits, de modo que un puerto corresponde a un byte. Si bien no puede definir el puerto inmediatamente, los registros que controlan los pines del puerto no son direccionables por bits y, por lo tanto, no es posible crear un nombre que corresponda a un pin individual (por ejemplo, "#define somePinVar 1.5").

En su lugar, adjuntamos un nombre a la posición de bit del pin y usamos este nombre junto con el registro de puerto para controlarlo.

Comience por nombrar los pines (el puerto al que están conectados se indicará más adelante en el programa). Esto se hace al principio del programa principal o en un archivo de encabezado separado. Los nombres de las variables y las constantes se eligen para que sean comprensibles más adelante y se basan en los nombres de red correspondientes en la PCB.

                    #define UART_RX_PIN BIT5 // P2.5 eUSCI_A1
#define UART_TX_PIN BIT6 // P2.6 eUSCI_A1

#define SPI_EN_PIN BIT0 // P1.0 eUSCI_B0 - no utilizado
#define SPI_CLK_PIN BIT1 // P1.1 eUSCI_B0
#define SPI_MOSI_PIN BIT2 // P1.2 eUSCI_B0 - todavía no se utiliza
#define SPI_MISO_PIN BIT3 // P1.3 eUSCI_B0

#define ADC24_RDL BIT0 // P3.0 establecido bajo siempre
#define ADC24_CNV BIT1 // P3.1 L-> H-> L (20 ns) para convertir
#define ADC24_BUSY BIT4 // P2.4 baja después de la conversión

#define ST_IN1 BIT0 // 3PST switch input 0
#define ST_IN2 BIT1 // 3PST switch input 1
                  

La siguiente parte del programa define las variables utilizadas, así como los prototipos de funciones. Utilicé variables globales y dejé la matriz como volátil para evitar cualquier problema potencial con la rutina del servicio de interrupción de SPI y UART. Todas las variables definidas en esta sección están disponibles para todas las funciones.

                    // Declaraciones de variables
...
uint16_t numTrials; // número de veces para repetir lecturas
uint16_t numReads; // número de conversiones por lectura
...
uint8_t byteCounter; // necesita 24 bits + 16 bits (5 bytes)
uint8_t dataBuffer volátil[5]; // Titular de todos los datos SPI

// Prototipos de funciones
void readADC (uint16_t); // Ejecuta conversiones
uint8_t spix (uint8_t); // comunicación de la interfaz SPI
voart uartx (uint8_t); // UART interfaz de comunicación unidireccional

                  

Asignar funciones de pin

El MSP430FR2633 tiene pines que pueden soportar múltiples funciones. Por lo tanto, se debe informar a la MCU si se usará un pin para la entrada / salida genérica o para un periférico integrado como UART o yo2DO.

Las siguientes líneas de código configuran el puerto 2 para su uso seleccionando primero la función alternativa (es decir, el periférico integrado) para los pines del puerto 2 correspondientes a las conexiones UART Tx / Rx, luego configurando el pin UART_TX y los dos interruptores de autoprueba pines como salidas.

                    // Seleccionar la función del módulo para UART (página 63)
    P2SEL0 | = (UART_TX_PIN | UART_RX_PIN);
    // Establecer los pines del puerto como salida para UART y SP3T Switch
    P2DIR | = (UART_TX_PIN | ST_IN1 | ST_IN2);

                  

Funciones personalizadas

Las funciones personalizadas se usan para leer el ADC, esencialmente solo para cambiar el indicador de inicio de conversión, esperar a que el indicador de ocupado se apague, y luego volver a activar el indicador de inicio de conversión, tantas veces como lo indique una constante establecida al principio de el programa.

                    void readADC (uint16_t numReads)
{
// Iniciar la conversión, esperar a que la bandera de ocupado se apague,
// y comienza la siguiente conversión.
    para (contador = 1; contador 20 ns para comenzar
        __delay_cycles (1); // medida. 1 / 16MHz * 1 ciclos = 62.5 ns
        P3OUT & = ~ ADC24_CNV; // Establecer la bandera de convertir de nuevo bajo
// Espere a que termine la conversión - nunca podría entrar en la función
        while ((P2IN & ADC24_BUSY))
        {
            __delay_cycles (1); // 1 / 16Mhz = 62.5 ns
        }
    }

                  

Reuniendo datos

Los datos saldrán de mi microcontrolador y se transmitirán a una PC a través de USB, con mucha prisa. Si convertí estos datos a un equivalente decimal o grados / minutos / segundos antes de enviarlos a través del convertidor de UART a USB, sería difícil ver qué tan estables están los datos, ya que se desplazan a aproximadamente 100 líneas por segundo. En su lugar, convertiremos los datos a binarios.

Al convertir los datos a binarios, puedo ver rápidamente en mi monitor de serie cuántos bits sin modificar hay simplemente contando desde la izquierda. Una vez que los bits comienzan a cambiar, he encontrado ruido (suponiendo, por supuesto, que el sensor está perfectamente parado). Los datos salen en bloques que separé con el carácter de tabulación en el programa completo: tres bytes para los datos de ángulo, dos bytes para el número de muestras por lectura (luego agregué dos bytes más para crear un contador).

                    para (byteCounter = 0; byteCounter> (7 - bitCounter)) & 0b1) == 0)
            {uartx ('0'); // ASCII 0}
            Else {uartx ('1'); // Ascii 1}
        }

                  

Te darás cuenta de que tuve que cambiar elendianness ”de cada byte para que llegue primero a la UART MSB.

Los datos de muestra muestran una columna para la lectura de ADC, el número de lecturas tomadas desde la última transferencia de datos y un contador.

Procesando datos con Mathematica

En el programa final, finalmente agregué un contador de prueba y procesé los datos con Mathematica (gratis en el sistema operativo Rasbian). Un artículo futuro explicará los datos y el procesamiento de datos con mayor detalle.

Los datos de un ensayo preliminar se muestran arriba. Las escalas verticales con los triángulos multicolores muestran los valores máximo, + 1σ, media, -1σ y mínimo en tres niveles de zoom. También se incluye un diagrama de dispersión, así como un histograma y la distribución normal ideal que lo acompaña.


Este artículo explica una pequeña parte de la programación del firmware para este proyecto. El siguiente artículo caracterizará el ruido en el dispositivo y los artículos futuros analizarán los datos.

A continuación encontrará un archivo descargable para el código fuente completo del programa en la MCU. ¡Avíseme en los comentarios a continuación si también estaría interesado en acceder a los archivos de código fuente de Mathematica!

Inclinómetro de precisión de firmware

                    // _ _ _ _ _____ _ _ _
// /  | | | /  | | | | / ____ (_) (_) |
// /  | | | /  | | __ ___ _ _ | | _ | | _ _ __ ___ _ _ _ | | _ ___
// / /   | | | / /   | '_  / _  | | | | __ | | | | '__ / __ | | | | | __ / __ |
/// ____  | | | / ____  | | _) | (_) | | _ | | | _ | | ____ | | | | (__ | | _ | | | | _  __ 
// _ /  _  _ | _ / _ /  _  _.__ /  ___ /  __, _ |  __ |  _____ | _ | _ |  ___ |  __, _ | _ |  __ | ___ /
// _____ _ __ _____ _
// | | ___ ___ | | _ __ | | | | | _ _ ___ | | _ ___ ___
// | | | | . '| _ | '_ | | | | | | | | . | | -_ | _ - |
// | _ | _ | _ | __, | _ | | _, _ | | _____ | | __ | __ | ___ | _ | _ | _ | ___ | ___ |
// con el apoyo de Bruce McKenney | ___ | 2018/11/09
// _ __
// ')) _ / _
// ______. ./--'__. / __.
// / / / /
#incluir 
#incluir 

/ ******************************* Pin Definitions **************** ************** /
#define UART_RX_PIN BIT5 // P2.5 eUSCI_A1
#define UART_TX_PIN BIT6 // P2.6 eUSCI_A1

#define SPI_EN_PIN BIT0 // P1.0 eUSCI_B0 - no utilizado
#define SPI_CLK_PIN BIT1 // P1.1 eUSCI_B0
#define SPI_MOSI_PIN BIT2 // P1.2 eUSCI_B0 - todavía no se utiliza
#define SPI_MISO_PIN BIT3 // P1.3 eUSCI_B0

#define ADC24_RDL BIT0 // P3.0 establecido bajo siempre
#define ADC24_CNV BIT1 // P3.1 L-> H-> L (20 ns) para convertir
#define ADC24_BUSY BIT4 // P2.4 baja después de la conversión

#define ST_IN1 BIT0 // 3PST switch input 0
#define ST_IN2 BIT1 // 3PST switch input 1

/ **************************** Declaraciones de variables ******************* ********
// Se puede consolidar y revisar más tarde con las llamadas de función adecuadas y
// los datos vuelven más tarde.
uint8_t bitCounter; // 0-7 contador de bits en un byte.
uint8_t byteCounterSPI; // Contador 0-4 para bytes de datos SPI.
uint8_t byteCounter; // necesita 24 bits + 16 bits (5 bytes)
contador uint16_t; // Contador temporal
uint16_t numTrials; // Contador temporal
uint16_t numTrialsMax = 1024; // Número de veces para repetir la medición.
uint16_t numReads = 4; // número de conversiones por lectura.

// Puede reemplazar el buffer volátil con un puntero más adelante.
uint8_t dataBuffer volátil[5]; // Titular de todos los datos SPI

/ ****************************** Función Prototipos ***************** ********** /
// Prototipos de funciones
void readADC (uint16_t); // Decide el número de conversiones
uint8_t spix (uint8_t); // interfaz SPI
voart uartx (uint8_t); // interfaz UART

/******************************** Programa principal *************** ******************
vacío principal
{
    // ********************* Comenzar la configuración ************************* *******

    WDTCTL = (WDTPW | WDTHOLD); // Detener el temporizador de vigilancia
    FRCTL0 = FRCTLPW | NWAITS_1; // Configuración de FRAM para> 8 MHz
    __bis_SR_register (SCG0); // Desactivar bucle de frecuencia bloqueada (FLL)
    CSCTL3 | = SELREF__REFOCLK; // Establecer REFO como fuente de referencia FLL
    CSCTL0 = 0; // Borrar registros DCO y MOD
    CSCTL1 & = ~ (DCORSEL_7); // Borrar los bits de selección de frecuencia DCO primero
    CSCTL1 | = DCORSEL_5; // Establecer DCO = 16MHz
    CSCTL2 = FLLD_0 + 487; // DCOCLKDIV = 16MHz
    __delay_cycles (3); // Esperar para permitir la estabilización del reloj.
    __bic_SR_register (SCG0); // Reenable FLL
    while (CSCTL7 & (FLLUNLOCK0 | FLLUNLOCK1))
        /*VACÍO*/; // FLL bloqueado
    // DCOCLKDIV predeterminado como fuente MCLK y SMCLK
    CSCTL4 = SELMS__DCOCLKDIV | SELA__REFOCLK;
    // Desactivar el modo de alta impedancia predeterminado de encendido GPIO
    PM5CTL0 & = ~ LOCKLPM5; // Desactivar el modo de alta impedancia predeterminado de encendido GPIO
    // PxDIR: 0 (In) 1 (Out)
    // PxSEL: Registro de selección de función (ver hoja de datos)
    // PxOUT: 0 (L) 1 (H): Registro de salida
    // PxREN: 0 (L) 1 (En): habilitación de resistencia (solo en la entrada)

    // Seleccionar la función del módulo SPI para SPI (página 60)
    P1SEL0 | = (SPI_MISO_PIN | SPI_MOSI_PIN | SPI_CLK_PIN);
    // Establecer MOSI y CLK como salidas.
    P1DIR | = (SPI_MOSI_PIN | SPI_CLK_PIN);

    // Seleccionar la función del módulo para UART (página 63)
    P2SEL0 | = (UART_TX_PIN | UART_RX_PIN);
    // Establecer los pines del puerto para UART y SP3T Switch
    P2DIR | = (UART_TX_PIN | ST_IN1 | ST_IN2);
    // IN1 / IN2 inicialmente bajo para abrir el interruptor SPST.
    P2OUT & = ~ (ST_IN1 | ST_IN2);

    // Establecer pines del puerto para ADC
    P3SEL0 & = ~ (ADC24_RDL | ADC24_CNV);
    // Establecer dirección para RDL y CNV
    P3DIR | = (ADC24_RDL | ADC24_CNV);
    // Establecer salida de puerto baja para RDL y CNV
    P3OUT & = ~ (ADC24_RDL | ADC24_CNV);

    // Configurar SPI en UCB0 Control Word 0
    // Coloque el UCB en estado de reinicio antes de modificar la configuración

    UCB0CTLW0 | = UCSWRST;
    // Modo maestro, reloj síncrono, estado inactivo alto, MSB primero.
    UCB0CTLW0 | = (UCMST | UCMSB | UCCKPH | UCSYNC | UCMSB | UCSSEL__SMCLK);
    // Bit reloj preescalador
    UCB0BRW = 0x0002; // Reloj de velocidad de bits = SMCLK / 2 = 8 MHz
    // Liberar restablecer e inicializar la máquina de estado
    UCB0CTLW0 & = ~ UCSWRST;

    // Poner la máquina de estado UCA en modo reinicio, selección y reloj.
    UCA1CTLW0 = UCMODE_0 | UCSSEL__SMCLK | UCSWRST; // UART, SMCLK, Reset
    // 16M / (UCOS) 16/9600 UCA1BRW = 104
    // UCA1BRW = 104;
    // UCBRS = 0xD6, 16x sobreexposición, UCBRF = 2
    // UCA1MCTLW = (0xD6 20 ns para iniciar
        __delay_cycles (1); // medida. 1 / 16MHz * 1 ciclos = 62.5 ns
        P3OUT & = ~ ADC24_CNV; // Establecer la bandera de convertir de nuevo bajo

        // Espere a que termine la conversión - nunca podría entrar en la función
        while ((P2IN & ADC24_BUSY))
        {
            __delay_cycles (1); // 1 / 16Mhz = 62.5 ns
        }
    }

    // Cambie los bytes ficticios para permitir que los datos de ADC se conviertan en dataBuffer[]
    para (byteCounterSPI = 0; byteCounterSPI> (7 - bitCounter)) & 0b1) == 0)
            {
                uartx ('0'); // ASCII 0
            }
            más
            {
                uartx ('1'); // Ascii 1
            }
        }

        // Formato de datos
        if (byteCounter == 2)
        {
            // Después de los tres bytes de datos, agregue una pestaña para separar el número de lecturas
            uartx (' t'); // pestaña Ascii
        }

        // Una vez que el último byte está fuera, envíe un retorno de carro y un avance de línea.
        if (byteCounter == 4)
        {
            uartx (' t');

            para (bitCounter = 0; bitCounter> (15 - bitCounter)) & 0b1) == 0)
                {
                    uartx ('0'); // ASCII 0
                }
                más
                {
                    uartx ('1'); // Ascii 1
                }
            }

            uartx (' r'); // Regreso
            uartx (' n'); // Linea de alimentación
        }
    }
    regreso;
}

uint8_t spix (uint8_t c)
{
    while (! (UCB0IFG & UCTXIFG))
        /*VACÍO*/;
    UCB0TXBUF = c;
    while (! (UCB0IFG & UCRXIFG))
        /*VACÍO*/;
    c = UCB0RXBUF;
    retorno (c);
}

uartx vacío (uint8_t c)
{
    while (! (UCA1IFG & UCTXIFG))
        /*VACÍO*/;
    UCA1TXBUF = c;
    regreso;

}

                  

By Maria Montero

Me apasiona la fotografía y la tecnología que nos permite hacer todo lo que siempre soñamos. Soñadora y luchadora. Actualmente residiendo en Madrid.