Sensores

MPU6050 com BluePill e STM32CubeIDE

Eletrogate 19 de janeiro de 2023

Introdução

Muitas das bibliotecas voltadas ao Arduino IDE são portadas para a BluePill, o que é um grande pró à escolha deste como ambiente de trabalho com a placa. O STM32CubeIDE, por outro lado, apesar de não apresentar tantas bibliotecas voltadas a esta placa -nenhuma oficial, inclusive, visto que a placa foi desenvolvida pela comunidade e não integra o portfólio da empresa-, oferece recursos personalizados para seus microcontroladores, o que faz com que o desenvolvimento de projetos neste seja mais dinâmico e aprofundado do que no primeiro. No post BluePill com STM32CubeIDE, aprendemos a desenvolver e carregar programas utilizando este IDE. Neste, exploraremos, um pouco, suas funções voltadas às interfaces USART e IIC.


O MPU6050

Na falta de uma biblioteca dedicada a este módulo, precisaremos compreender, em um nível mais baixo, seu funcionamento. A operação do MPU6050, cujo uso junto ao Arduino IDE é abordado no post Acelerômetros MPU-6050, MMA8452 e MPU-9250, é regida por seus registradores, explicados em https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Register-Map1.pdf. Para nossa aplicação, precisamos nos atentar a poucos. Sobre estes, tratamos abaixo:

  • WHO_AM_I (0x75): identificação do sensor

Registrador para leitura, somente. Armazena o valor padrão 0x68. Pode ser utilizado para verificar se a comunicação foi estabelecida com sucesso.

  • PWR_MGMT_1 (0x6B): gerenciamento de energia
    • Seus bits [7:5] controlam os modos de consumo reduzido. Para operação normal, devem estar desativados.
    • O bit 4 é reservado.
    • Quando o bit 3 está acionado, o sensor de temperatura do módulo é desligado.
    • Os bits [2:0] selecionam a fonte de clock do sensor. Para nossa aplicação, são mantidos desativados.
  • CONFIG (0x1A): configurações gerais
    • Os bits [7:6] são reservados.
    • Os bits [5:3] determinam o registrador em que um sinal externo será armazenado. Em nossa aplicação, devem ser mantidos desativados.
    • Os bits [2:0] selecionam a faixa de corte do filtro passa-baixas do sensor. Optaremos pelo corte em 5 Hz, acionando os pinos [2:1].
  • ACCEL_CONFIG (0x1C): configuração do acelerômetro
    • Os bits [7:5] disparam o autoteste de cada eixo. Não usaremos este recurso.
    • Os bits[4:3] selecionam a escala do acelerômetro. Optaremos pela menor, ± 2 g, em que [g] é a aceleração da gravidade no local. Para isso, estes bits devem ser mantidos desativados.
    • Os bits [2:0] são reservados.
  • GYRO_CONFIG (0x1B): configuração do giroscópio
    • Os bits [7:5] disparam o autoteste de cada eixo. Não usaremos este recurso.
    • Os bits[4:3] selecionam a escala do giroscópio. Optaremos pela menor, ± 250 °/s. Para isso, estes bits devem ser mantidos desativados.
    • Os bits [2:0] são reservados.
  • ACCEL_XOUT_H (0x3B), ACCEL_XOUT_L (0x3C), ACCEL_YOUT_H (0x3D), ACCEL_YOUT_L (0x3E), ACCEL_ZOUT_H (0x3F) e ACCEL_ZOUT_L (0x40)

Cada par sucessivo de registradores compõe o resultado da leitura em um eixo. Assim, a concatenação dos registradores ACCEL_XOUT_H e ACCEL_XOUT_L corresponde à leitura da aceleração no eixo X, ACCEL_YOUT_H e ACCEL_YOUT_L no Y e ACCEL_ZOUT_H e ACCEL_ZOUT_L, no Z.

  • GYRO_XOUT_H (0x43), GYRO_XOUT_L (0x44), GYRO_YOUT_H (0x45), GYRO_YOUT_L (0x46), GYRO_ZOUT_H (0x47) e GYRO_ZOUT_L (0x48)

Cada par sucessivo de registradores compõe o resultado da leitura em um eixo. Assim, a concatenação dos registradores GYRO_XOUT_H e GYRO_XOUT_L corresponde à leitura da velocidade angular no eixo X, GYRO_YOUT_H e GYRO_YOUT_L no Y e GYRO_ZOUT_H e GYRO_ZOUT_L, no Z.


Comunicação

O MPU6050 se comunica por uma interface IIC. Por isso, utilizaremos este periférico da BluePill. O STM32CubeIDE, por meio de sua HAL, sigla para “Hardware Abstraction Layer”, “Camada de Abstração de Hardware”, disponibiliza duas funções voltadas à comunicação, por meio da IIC, com dispositivos regidos por memória:

  • HAL_I2C_Mem_Write(I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t * pData, uint16_t Size, uint32_t Timeout);
    • I2C_HandleTypeDef * hi2c: ponteiro para a estrutura que armazena as informações sobre a interface;
    • uint16_t DevAddress: endereço do dispositivo com o qual a comunicação será realizada;
    • uint16_t MemAddress: endereço, na memória do dispositivo, em que o primeiro dado será escrito;
    • uint16_t MemAddSize: tamanho, em bytes, de cada célula da memória do dispositivo;
    • uint8_t * pData: ponteiro para, na memória do microcontrolador, os dados a serem enviados para o dispositivo;
    • uint16_t Size: quantidade de dados a serem enviados;
    • uint32_t Timeout: máximo de tempo a ser aguardado pelos sinais de confirmação do dispositivo;

Esta função permite a escrita de dados na memória do dispositivo.

  • HAL_I2C_Mem_Read(I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t * pData, uint16_t Size, uint32_t Timeout);
    • I2C_HandleTypeDef * hi2c: ponteiro para a estrutura que armazena as informações sobre a interface;
    • uint16_t DevAddress: endereço do dispositivo com o qual a comunicação será realizada;
    • uint16_t MemAddress: endereço, na memória do dispositivo, de onde o primeiro dado será lido;
    • uint16_t MemAddSize: tamanho, em bytes, de cada célula da memória do dispositivo;
    • uint8_t * pData: ponteiro para, na memória do microcontrolador, o endereço em que os dados lidos serão armazenados;
    • uint16_t Size: quantidade de dados a ser lida;
    • uint32_t Timeout: máximo de tempo a ser aguardado pelos sinais de confirmação do dispositivo;

Esta função permite a leitura de dados da memória do dispositivo.

Com estas, será possível realizar toda a comunicação necessária para a configuração e leitura das medições do sensor.


O Código

No código, encontrado em BluePillMPU6050, primeiro, declaramos uma constante com o endereço do sensor, neste módulo, 0xD0: const uint8_t mpu6050Addr = 0xD0;. Abaixo, são definidas as estruturas a armazenar os dados lidos do sensor:

typedef struct leituraAcel {
    int16_t accelX;
    int16_t accelY;
    int16_t accelZ;
} leituraAcel;

typedef struct leituraGyro {
    int16_t gyroX;
    int16_t gyroY;
    int16_t gyroZ;
} leituraGyro;

Cada estrutura armazena, separadamente, o valor correspondente a cada eixo. Então, definimos a primeira função personalizada, void mpu6050Init(void). Esta verifica se a comunicação com o sensor está adequada e, em caso positivo, prossegue com sua configuração. Para configurar o módulo, serão escritos, nos registradores previamente abordados, os valores correspondentes às configurações escolhidas. Como o acesso à memória do dispositivo não pode ser feito bit-a-bit, mas byte-a-byte, devem ser enviados valores que correspondam ao acionamento, somente, dos bits corretos. A tabela abaixo relaciona cada registrador ao estado desejado para seus bits e ao correspondente valor hexadecimal.

Registrador Estado dos Bits Valor Hexadecimal
WHO_AM_I 01101000 68
PWR_MGMT_1 00001000 08
CONFIG 00000110 06
ACCEL_CONFIG 00000000 0
GYRO_CONFIG 00000000 0

O trecho abaixo contém a definição da função.

void mpu6050Init(void) {
    static const uint8_t whoAmIReg = 0x75;
    static const uint8_t pwrMgmt1Reg = 0x6B;
    static const uint8_t configReg = 0x1A;
    static const uint8_t accelConfigReg = 0x1C;
    static const uint8_t gyroConfigReg = 0x1B;
    static unsigned char msgErro[] = "Erro na inicializacao do MPU6050";

    uint8_t check;
    uint8_t data;

    HAL_I2C_Mem_Read(&hi2c1, mpu6050Addr, whoAmIReg, 1, &check, 1, 1000);

    if(check == 0x68) {

        data = 0x08;
        HAL_I2C_Mem_Write(&hi2c1, mpu6050Addr, pwrMgmt1Reg, 1, &data, 1, 1000);

        data = 0x06;
        HAL_I2C_Mem_Write(&hi2c1, mpu6050Addr, configReg, 1, &data, 1, 1000);

        data = 0x00;
        HAL_I2C_Mem_Write(&hi2c1, mpu6050Addr, accelConfigReg, 1, &data, 1, 1000);

        data = 0x00;
        HAL_I2C_Mem_Write(&hi2c1, mpu6050Addr, gyroConfigReg, 1, &data, 1, 1000);
    } else {
        HAL_UART_Transmit(&huart1, msgErro, sizeof(msgErro), 100);
    }
}

A próxima função, void mpu6050ReadAccel(leituraAcel *leitura), lê e armazena as medições do acelerômetro. Para isso, são lidos 6 bytes a partir de accelXoutHReg. Cada par é, então, concatenado e armazenado na componente da estrutura correspondente ao respectivo eixo.

void mpu6050ReadAccel(leituraAcel *leitura) {
    static const uint8_t accelXoutHReg = 0x3B;
    uint8_t recData[6];

    HAL_I2C_Mem_Read(&hi2c1, mpu6050Addr, accelXoutHReg, 1, recData, 6, 1000);

    leitura->accelX = (int16_t) (recData[0] << 8 | recData [1]);
    leitura->accelY = (int16_t) (recData[2] << 8 | recData [3]);
    leitura->accelZ = (int16_t) (recData[4] << 8 | recData [5]);
}

Muito similar, void mpu6050ReadGyro(leituraGyro *leitura) lê e armazena as medições do giroscópio.

void mpu6050ReadGyro(leituraGyro *leitura) {
    static const uint8_t gyroXoutHReg = 0x43;
    uint8_t recData[6];

    HAL_I2C_Mem_Read(&hi2c1, mpu6050Addr, gyroXoutHReg, 1, recData, 6, 1000);

    leitura->gyroX = (int16_t) (recData[0] << 8 | recData [1]);
    leitura->gyroY = (int16_t) (recData[2] << 8 | recData [3]);
    leitura->gyroZ = (int16_t) (recData[4] << 8 | recData [5]);
}

Já na função principal, primeiro, são declarados o buffer para os caracteres da mensagem de saída, que é, também, inicializado como vazio, e as estruturas que receberão as medições.

uint8_t mensagem[128] = {0};
leituraAcel leituraA;
leituraGyro leituraG;

Após, o sensor é inicializado e configurado pela chamada mpu6050Init();. No loop infinito, os dados são lidos e armazenados nas estruturas, convertidos para as unidades de medidas correspondentes e formatados para, por fim, serem enviados pela interface USART.

while(1) {
    mpu6050ReadAccel(&leituraA);
    mpu6050ReadGyro(&leituraG);

    float accelG[3] = {leituraA.accelX / 16384.0, leituraA.accelY / 16384.0, leituraA.accelZ / 16384.0};
    float gyroGpS[3] = {leituraG.gyroX / 131.0, leituraG.gyroY / 131.0, leituraG.gyroZ / 131.0};

  sprintf((char *) mensagem, "a (x, y, z) [g]: %.2f %.2f %.2f | v (x, y, z) [°/s]: %.2f %.2f %.2f\r\n", accelG[0], accelG[1], accelG[2], gyroGpS[0], gyroGpS[1], gyroGpS[2]);
  HAL_UART_Transmit(&huart1, mensagem, sizeof(mensagem), 100);
  HAL_Delay(100);
}

Como o microcontrolador utilizado não possui hardware para aritmética de ponto flutuante, alguns recursos que a usam são, por padrão, desabilitados. Por isso, para formatar a mensagem como no trecho acima, é necessário habilitar a passagem de números em ponto flutuante como parâmetro para a função sprintf. Para isso, acesse Project → Properties → C/C++ Build → Settings → MCU Settings e habilite a caixa de seleção “Use float with printf from newlib-nano (-u _printf_float)”, como mostra a imagem abaixo.

Acesse Project → Properties → C/C++ Build → Settings → MCU Settings e habilite a caixa de seleção "Use float with printf from newlib-nano (-u _printf_float)".

Caminho para habilitar a formatação de números de ponto flutuante


Execução

A demonstração utiliza o terminal serial do próprio IDE e pode ser vista abaixo.


Conclusão

Em linhas de raciocínio similares, outros sensores podem ser utilizados juntos a esta placa e ao IDE. Além disso, para o MPU6050, as funções deste post podem ser reutilizadas.


Referências

  • https://controllerstech.com/how-to-interface-mpu6050-gy-521-with-stm32/;
  • https://microcontrollerslab.com/stm32-blue-pill-uart-tutorial-polling-method/

Sobre o Autor


Eduardo Henrique

Formado técnico em mecatrônica no CEFET-MG, atualmente estuda Engenharia de Controle e Automação na UFMG.


Eletrogate

19 de janeiro de 2023

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!