Arduino 入门学习笔记(二十二):IIC_QMA6100P 实验
Arduino IIC_QMA6100P实验,开发板:正点原子ESP32S3,外设:QMA6100P,记录学习笔记。
Arduino 入门学习笔记(二十二):IIC_QMA6100P 实验开发板
正点原子ESP32S3
没有LCD屏可以用串口打印进行测试
例程源码在文章顶部可免费下载
1 QMA6100P 介绍
QMA6100P 是一款三轴加速度传感器,具有高集成、小尺寸封装的特点。它集成了信号调节 ASIC 的加速度传感器,可以感知倾斜、运动、冲击和振动。 QMA6100P 基于先进的高分辨率单晶硅 MEMS 技术,配合定制设计的 14 位 ADC 专用集成电路,具有低噪声、高精度、低功耗、偏置微调等优点。它支持数字接口 I2C 和 SPI,内置硬件计步器,支持多种不同中断模式。QMA6100P 的最大可支持 64 级 FIFO,待机电流为 5μ A,计步器工作电流为 44μ A。主要应用市场与优势是手机,手环,手表,各类低功耗 IOT 设备,集成各类应用算法,计步器,抬手亮屏,垂手亮屏,久坐提醒,跌倒报警,跌落报警,睡眠检测,平衡检测,倾斜检测,睡眠唤醒等超过 20 种不同应用。 QMA6100P 还具有低成本和与市场主流传感器兼容的优点,以及超低功耗、可靠性高的特点。
下图是 QMA6100P 内部框图:
根据上文, QMA6100P 三轴加速度计支持 SPI 和 IIC 两种通信接口。接口的实现流程可参考《QMA6100P Datasheet Rev. D.pdf》 数据手册。本实验以 ESP32-S3 开发板电路为基准,该开发板使用 IIC 通信接口来获取 QMA6100P 三轴加速度计的相关参数。
QMA6100P 的引脚说明如下表所示:
1.1 QMA6100P 寻址
从器件数据手册的所示, QMA6100P 在 IIC 通信下,具有两种设备地址设置,它们分别为 0x12 和 0x13(7 位器件地址)。这两个设备地址的选择是根据 QMA6100P 的第 1 号管脚和 0x20 寄存器第 6bit 确定,如下图所示:
从上图可知,当 pin1(AD0)拉低时, 无论 0x20 寄存器第 6 位数据怎么样, QMA6100P 设备地址都是被设置为 0x12, 否则,该设备地址有可能为 0x13 或者 0x12。开发板是把 AD0 管脚拉低,所以 QMA6100P 设备地址为 0x12,写操作地址为 0x24(设备地址 0x12 << 1 | 读写位 0),读操作地址为 0x25(设备地址 0x12 << 1 | 读写位 1)。
1.2 QMA6100P 寄存器介绍
QMA6100P 有一些寄存器,由这些寄存器来控制 QMA6100P 的工作模式,以及中断配置和数据输出等。这里我们仅介绍我们在本章需要用到的一些寄存器,其他寄存器的描述和说明,请大家参考 QMA6100P 的数据手册。
本章需要用到 QMA6100P 的寄存器如下表所示:
其余的寄存器可在数据手册下找到相关描述和配置信息。
1.3 QMA6100P 时序介绍
ESP32-S3 是通过 IIC 总线跟 QMA6100P 进行通信的,对 QMA6100P 相关寄存器进行写入配置,后面就是对相关数据寄存器进行读取。这里的时序主要就是写寄存器时序和读寄存器时序,我们一一介绍。
写寄存器时序
QMA6100P 的写寄存器时序如下图所示:
图中,先发送 QMA6100P 的写操作地址 0x24(器件地址 0X12 << 1 | 读写位 0),随后发送 8 位寄存器地址,最后发送 8 位寄存器值。其中: START,表示 IIC 起始信号; R/W,表示读/写标志位(R/W =0 表示写, R/W =1 表示读); SACK,表示应答信号; STOP,表示 IIC 停止信号。
读寄存器时序
QMA6100P 的读寄存器时序如下图所示:
图中,同样是先发送 QMA6100P 的写操作地址 0x24, 然后再发送寄存器地址,随后,重新发送起始信号(Sr),发送 QMA6100P 的读操作地址 0x25(器件地址 0X12 << 1 | 读写位 1) ,然后读取寄存器值。若主机一直提供应答信号, QMA6100P 会一直把寄存器数据返回,一旦主机发出非应答信号, QMA6100P 就停止数据发送。 其中: MACK,表示主机应答; NACK,表示设备不应答; 其他简写同上。
2. 硬件设计
2.1 例程功能
在 LCD 屏上显示由 QMA6100P 加速度数据运算出的 pitch 俯仰角和 roll 翻滚角。当我们转动开发板时, 这两个角度数据会重新计算。
2.2 硬件资源
- LED 灯
LED-IO1 - USART0
U0TXD-IO43
U0RXD-IO44 - XL9555
IIC_SDA-IO41
IIC_SCL-IO42 - SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555) - QMA6100P
IIC_SDA-IO41
IIC_SCL-IO42
QMA_INT-IO0_1(XL9555)
2.3 原理图
QMA6100P 原理图,如下图所示:
这里说明一下, QMA6100P 的 QMA_INT 脚是连接在 XL9555 器件的 IO0_1 脚上,如果大家要使用 QMA6100P 的中断输出功能,必须先初始化 XL9555 器件并配置 IO0_1 为输入功能,监测 XL9555 中断引脚是否有中断产生。若发现有中断产生,则判断是否是 IO0_1 导致的,从而检测到 QMA6100P 的中断。在本章中,并没有用到 QMA6100P 中断功能,所以没有对 XL9555器件的 IO0_1 做设置
3. 软件设计
3.1 程序流程图
下面看看本实验的程序流程图:
3.2 程序解析
QMA6100P 驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。 QMA6100P 驱动源码包括两个文件: qma6100p.cpp、 qma6100p.h、 imu.cpp 和 imu.h。
下面我们先解析 qma6100p.h 的程序。对 QMA6100P 的 IIC 引脚和器件地址做了相关定义。
#define IIC_SCL 42
#define IIC_SDA 41
#define IMU_DEV_ADDR 0x12 /* 7 位器件地址 */
我们选择使用 IO42 作为 IIC 时钟线, IO41 作为 IIC 数据线, QMA6100P 器件地址为 0x12。在 qma6100p.h 中,还定义了很多的宏,包括 QMA6100P 寄存器、寄存器相关位,大家自行查看源码即可。
下面我们再解析 qma6100p.cpp 的程序,首先先来看一下初始化函数 qma6100p_init,代码如下:
/**
* @brief qma6100p 初始化函数
* @param 无
* @retval 0:成功,非 0:失败
*/
uint8_t qma6100p_init(void)
{
uint8_t id = 0;
Wire.begin(IIC_SDA, IIC_SCL, 400000); /* 初始化 IIC 总线 */
qma6100p_read_reg(QMA6100P_CHIP_ID, &id, 1); /* 读取 QMA6100P 器件 ID */
if (id != QMA6100P_DEVICE_ID)
{
Serial.printf("ID:%#x \r\n", id);
return 0xFF;
}
/* 软件复位 */
qma6100p_write_reg(QMA6100P_REG_RESET, 0xb6);
delay(5);
qma6100p_write_reg(QMA6100P_REG_RESET, 0x00);
delay(10);
/* 厂家推荐的初始化序列 */
qma6100p_write_reg(0x11, 0x80); /* 从 standby 到 active 状态(0x11 寄存器<7>=1) */
qma6100p_write_reg(0x11, 0x84);
qma6100p_write_reg(0x4a, 0x20);
qma6100p_write_reg(0x56, 0x01);
qma6100p_write_reg(0x5f, 0x80);
delay(2);
qma6100p_write_reg(0x5f, 0x00);
delay(10);
/* 设置 QMA6100P 的配置参数(量程、输出频率、工作模式) */
qma6100p_write_reg(QMA6100P_REG_RANGE, QMA6100P_RANGE_8G);
/* 设置加速度计满量程范围(8G) */
qma6100p_write_reg(QMA6100P_REG_BW_ODR, (uint8_t)(QMA6100P_BW_100 |
QMA6100P_LPF_OFF)); /* 设置输出数据速率(100khz) */
qma6100p_write_reg(QMA6100P_REG_POWER_MANAGE, (uint8_t)QMA6100P_MCLK_51_2K |
0x80); /* 设置加速度的工作模式 */
return 0;
}
在 QMA6100P 初始化函数中, 首先初始化 IIC 接口,读取器件 ID 以此检测器件通信是否正常,然后对寄存器进行操作实现软件复位,随后用器件手册中推荐的初始化序列进行配置,最后设置 QMA6100P 的配置参数(量程、输出频率、工作模式)。
接下来,看一下前面提及到的向 QMA6100P 寄存器写入数据的函数 qma6100p_write_reg,代码如下:
/**
* @brief QMA6100P 对寄存器配置函数
* @param reg : 寄存器地址
* @param data : 写入到寄存器数据
* @retval 无
*/
void qma6100p_write_reg(uint8_t reg, uint8_t data)
{
Wire.beginTransmission(IMU_DEV_ADDR); /* 发送从机的 7 位器件地址到发送队列 */
Wire.write(reg); /* 发送要写入从机寄存器的地址到发送队列 */
Wire.write(data); /* 发送要写入从机寄存器的数据到发送队列 */
Wire.endTransmission();
/* IIC 发送 发送队列的数据(不带参数,表示发送 stop 信号,结束传输) */
}
这里的写操作流程跟前面内容中 QMA6100P 写寄存器时序图描述的过程是一致的。首先调用 Wire.beginTransmission 函数将从机地址 0x12 加入到发送数据队列,然后调用Wire.write 函数将要写入数据的寄存器地址加入到发送数据队列,继续调用 Wire.write 函数将要写入寄存器地址的数据加入到发送数据队列,最后调用 Wire.endTransmission 函数将数据队列的数据发送到 QMA6100P,最终实现对寄存器数据的写入。
继续看一下读取 QMA6100P 寄存器数据的函数 qma6100p_read_reg,代码如下:
/**
* @brief 从 QMA6100P 读取 N 字节数据
* @param reg : 寄存器地址
* @param buf : 数据存储 buf
* @param num : 读取长度
* @retval 无
*/
void qma6100p_read_reg(uint8_t reg, uint8_t *buf, uint16_t num)
{
uint8_t i = 0;
Wire.beginTransmission(IMU_DEV_ADDR); /* 发送从机的 7 位器件地址到发送队列 */
Wire.write(reg); /* 发送要读取从机的寄存器地址到发送队列 */
Wire.endTransmission(0);
/* IIC 发送 发送队列的数据(传参为 0,表示重新发送一个 start 信号,保持 IIC 总线有效连接) */
Wire.requestFrom(IMU_DEV_ADDR, num); /* 主机向从机发送数据请求,并获取到数据 */
while (Wire.available()) /* 得到已经接收到的数据字节数 */
{
buf[i++] = Wire.read(); /* 到数据缓冲区读取数据 */
}
}
这里的读操作流程跟前面中读寄存器时序图描述的过程是一致的。首先调用Wire.beginTransmission 函数将从机地址 0x12 加入到发送数据队列,然后调用 Wire.write 函数将要要读取数据的寄存器地址加入到发送数据队列,继续调用 Wire.endTransmission 函数将数据队列的数据发送到 QMA6100P。注意: Wire.endTransmission 函数带参数 0 表明会重新发送一个起始信号,保持 IIC 总线的连接。后面就通过调用 Wire.requestFrom 函数向 QMA6100P 指定寄存器读取 num 字节数据并保存到接收缓冲区, Wire.available 函数用于查询缓冲区是否有可读数据,而 Wire.read 函数就是用来读取缓冲区一字节数据,通过函数参数 buf 缓冲区将 num 字节数据存储。
初始化好 QMA6100P 传感器之后,就可以对其采集到的数据进行获取,实现这个读取过程的函数为 qma6100p_read_raw_xyz,代码如下:
/**
* @brief 从 QMA6100P 寄存器中读取原始 x,y,z 轴数据
* @param data : 3 轴数据存储数组
* @retval 无
*/
void qma6100p_read_raw_xyz(int16_t data[3])
{
uint8_t databuf[6] = {0};
int16_t raw_data[3];
qma6100p_read_reg(QMA6100P_XOUTL, databuf, 6);
raw_data[0] = (int16_t)(((databuf[1] << 8)) | (databuf[0]));
raw_data[1] = (int16_t)(((databuf[3] << 8)) | (databuf[2]));
raw_data[2] = (int16_t)(((databuf[5] << 8)) | (databuf[4]));
data[0] = raw_data[0] >> 2;
data[1] = raw_data[1] >> 2;
data[2] = raw_data[2] >> 2;
}
该函数直接读取 QMA6100P 数据寄存器的三轴 14 位 ADC 原始数据, 然后进行整合。
得到 ADC 原始数据后,还要进行换算, 才能获得加速度计的 x, y, z 轴数据, 代码如:
/**
* @brief 计算得到加速度计的 x,y,z 轴数据
* @param accdata : 3 轴数据存储数组
* @retval 无
*/
void qma6100p_read_acc_xyz(float accdata[3])
{
int16_t rawdata[3];
qma6100p_read_raw_xyz(rawdata);
accdata[0] = (float)(rawdata[0] * M_G) / 1024;
accdata[1] = (float)(rawdata[1] * M_G) / 1024;
accdata[2] = (float)(rawdata[2] * M_G) / 1024;
}
加速度计轴数据 = ADC 原始数据 * 重力加速度 / 加速度计灵敏度ADC 原始数据直接通过读取寄存器获得,而重力加速度是一个常数 9.80665,而加速度计灵敏度为 1024 LSB/g。通过计算即可得到加速度计的 X 轴、 Y 轴和 Z 轴数据。
在 imu.cpp 中,有一个得到欧拉角的函数,即从加速度计数据换算出欧拉角,代码如下:
#define M_PI (3.14159265358979323846f)
#define RAD_TO_DEG (180.0f / M_PI)
/**
* @brief 得到姿态解算后的欧拉角
* @param accl_in : 3 轴加速度数据
* @param angle : 俯仰角、横滚角
* @retval 返回值 : 欧拉角
*/
void acc_get_angle(float accl_in[3], float angle[2])
{
float accl_data[3];
float acc_normal, pitch, roll;
acc_normal = sqrtf(accl_in[0] * accl_in[0] + accl_in[1] * accl_in[1] +
accl_in[2] * accl_in[2]);
accl_data[0] = accl_in[0] / acc_normal;
accl_data[1] = accl_in[1] / acc_normal;
accl_data[2] = accl_in[2] / acc_normal;
pitch = -atan2f(accl_in[0], accl_in[2]);
angle[0] = pitch * RAD_TO_DEG;
acc_normal = sqrtf(accl_data[0] * accl_data[0] + accl_data[1] * accl_data[1]
+ accl_data[2] * accl_data[2]);
roll = asinf((accl_data[1] / acc_normal));
angle[1] = roll * RAD_TO_DEG;
}
19_iic_qma6100p.ino 代码
在 19_iic_qma6100p.ino 里面编写如下代码:
#include "led.h"
#include "uart.h"
#include "xl9555.h"
#include "spilcd.h"
#include "qma6100p.h"
#include "imu.h"
float g_acc_data[3];
float g_angle_data[2];
/**
* @brief 显示角度
* @param x, y : 坐标
* @param title: 标题
* @param angle: 角度
* @retval 无
*/
void user_show_angle(uint16_t x, uint16_t y, char *title, float angle)
{
char buf[20];
sprintf(buf,"%s%3.1f", title, angle); /* 格式化输出 */
lcd_fill(x, y, x + 160, y + 16, WHITE); /* 清除上次数据(最多显示 20 个字符) */
lcd_show_string(x, y, 160, 16, LCD_FONT_16, buf, BLUE); /* 显示字符串 */
}
/**
* @brief 当程序开始执行时,将调用 setup()函数,通常用来初始化变量、函数等
* @param 无
* @retval 无
*/
void setup()
{
led_init(); /* LED 初始化 */
uart_init(0, 115200); /* 串口 0 初始化 */
xl9555_init(); /* IO 扩展芯片初始化 */
lcd_init(); /* LCD 初始化 */
qma6100p_init(); /* 三轴加速度计初始化 */
lcd_show_string(30, 50, 200, 16, LCD_FONT_16, "ESP32-S3", RED);
lcd_show_string(30, 70, 200, 16, LCD_FONT_16, "IMU TEST", RED);
lcd_show_string(30, 90, 200, 16, LCD_FONT_16, "ATOM@ALIENTEK", RED);
}
/**
* @brief 循环函数,通常放程序的主体或者需要不断刷新的语句
* @param 无
* @retval 无
*/
void loop()
{
qma6100p_read_acc_xyz(g_acc_data);
acc_get_angle(g_acc_data, g_angle_data);
user_show_angle(30, 130, "Pitch :", g_angle_data[0]);
user_show_angle(30, 150, " Roll :", g_angle_data[1]);
LED_TOGGLE();
delay(500);
}
在 setup 函数中,调用 led_init 函数完成 LED 初始化,调用 uart_init 函数完成串口初始化,调用 xl9555_init 函数完成 XL9555 初始化,调用 lcd_init 函数完成 LCD 屏初始化,调用qma6100p_init 函数完成 QMA6100P 初始化, LCD 显示实验信息。
在 loop 函数中,间隔 500 毫秒调用 qma6100p_read_acc_xyz 函数获取加速度计原始数据,通过 acc_get_angle 函数得到俯仰角和横滚角, 然后在 LCD 进行显示。 LED 灯每隔 500 毫秒状态翻转,实现闪烁效果。
4. 下载验证
程序下载到开发板后, LCD 不断更新 pitch 俯仰角和 roll 翻滚角数据,如下图所示:
更多推荐



所有评论(0)