QMA6100P:读取三轴加速度并识别姿态

2026-09-10 00:00    #ESP32-S3   #ESP-IDF   #外设   #阶段3  

本课对应《DNESP32S3 使用指南(IDF 版)》第 32 章。配套完整工程:examples/27-qma6100p。

环境与验收边界: DNESP32S3 V1.2,ESP-IDF v5.5.5;本机 macOS 构建通过。Ubuntu 26、外设接线、屏幕效果和实际测量仍待实板验收。下文输出均为预期格式,不能当作已采集的硬件日志。

本课完成后能做什么

硬件连接与资源

信号或资源对应关系
I²C0SDA GPIO41,SCL GPIO42
QMA6100P地址 0x12,芯片 ID 0x90
配置±8g,约 100 Hz 输出,1024 LSB/g
INTXL9555 IO0_1,本例轮询不用

以下为正点原子 DNESP32S3 V1.2 原理图的相关页,保留原厂标识;表格用于快速定位,遇到板卡版本差异以实际原理图为准。点击图片可查看引脚与网络名。

DNESP32S3 原理图第 2 页

DNESP32S3 原理图第 3 页

本课信号与数据处理流程

静止为什么仍然能读到约 1g

加速度计测量的是包含重力影响的比力。平放且传感器 +Z 朝上时,X、Y 应接近 0g,Z 接近 +1g。如果把三个轴都校正为 0,就会同时去掉计算静态姿态所需的重力方向。

本例先检查芯片 ID,再执行教材复位序列,设置 ±8g 和 100 Hz。输出数据是左对齐的有符号 14 位数;先把高低字节组合为 int16_t,再换算到 g,避免负加速度被误当成巨大的正数。

零偏校准有前提

启动提示后三秒内让板卡静止,确认传感器 +Z 向上,收集 100 个样本取平均。X、Y 的均值作为偏置,Z 的均值减去 1g 作为偏置。

这种校准要求指定姿态,不能用于任意姿态,也不等同于六面标定。如果板倒放、倾斜或运动,校准就会把重力和运动错记成偏差。测试前核对轴向;需要覆盖未知姿态的产品应另做标定流程及稳定性检测。

从三轴到倾斜角

roll 使用 atan2(y,z),pitch 使用 atan2(-x,sqrt(y*y+z*z)),弧度再转换成角度。atan2 能保留象限信息,比简单相除后 atan 更方便。

本例在任一角度的绝对值超过 20° 时输出 TILTED,否则输出 LEVEL。这个阈值适合观察,不是精密姿态控制器;边界处可加入滞回,运动时的加速度也会污染仅靠重力推算的姿态。加速度计无法单独给出可靠的航向 yaw。

工程如何组织

驱动放在 components/BSP/,应用放在 main/main.c。本工程包含 BUS、QMA6100P 模块。ESP-IDF 会自动发现项目根目录下的 components/BSP。main 通过 PRIV_REQUIRES BSP 声明组件依赖,INCLUDE_DIRS 则导出编译时使用的头文件搜索路径,不涉及运行时文件查找。

应用代码:入口与处理流程

下面代码直接取自配套工程的 main/main.c,头文件和 BSP 实现在仓库中一并提供。初学时先看 app_main(),再追踪它调用的模块。

 1#include <stdio.h>
 2#include <stdint.h>
 3#include <stdbool.h>
 4#include <string.h>
 5#include <stdlib.h>
 6#include <assert.h>
 7#include "esp_err.h"
 8#include "esp_log.h"
 9#include "esp_timer.h"
10#include "freertos/FreeRTOS.h"
11#include "freertos/task.h"
12#include "qma6100p.h"
13#include <math.h>
14void app_main(void)
15{
16    i2c_master_bus_handle_t bus;
17    ESP_ERROR_CHECK(bus_init(0,CONFIG_LESSON_I2C_SDA,CONFIG_LESSON_I2C_SCL,&bus));
18    ESP_ERROR_CHECK(motion_open(bus));
19    ESP_ERROR_CHECK(motion_reset());
20    ESP_LOGI("qma","Keep board STILL, +Z UP for calibration in 3 seconds");
21    vTaskDelay(pdMS_TO_TICKS(3000));
22    acceleration_t bias={0},s;
23    for(int i=0;i<100;i++) {
24        ESP_ERROR_CHECK(motion_read(&s));
25        bias.x+=s.x/100; bias.y+=s.y/100; bias.z+=s.z/100;
26        vTaskDelay(pdMS_TO_TICKS(10));
27    }
28    bias.z-=1; // 保留 +Z 方向的 1g 重力,不能把三个轴的均值都减到零。
29    while(1) {
30        esp_err_t err=motion_read(&s);
31        if(err==ESP_OK) {
32            s.x-=bias.x; s.y-=bias.y; s.z-=bias.z;
33            float roll=atan2f(s.y,s.z)*57.29578f;
34            float pitch=atan2f(-s.x,sqrtf(s.y*s.y+s.z*s.z))*57.29578f;
35            ESP_LOGI("qma","g=(%.3f,%.3f,%.3f) roll=%.1f pitch=%.1f %s",
36                s.x,s.y,s.z,roll,pitch,fabsf(roll)>20 || fabsf(pitch)>20 ? "TILTED":"LEVEL");
37        } else ESP_LOGW("qma","%s",esp_err_to_name(err));
38        vTaskDelay(pdMS_TO_TICKS(200));
39    }
40}

BSP 关键实现:QMA6100P

以下是本课对应驱动的完整源文件。对外声明见 qma6100p.h。公共 BUS 模块统一使用新 I²C master API,将单次事务等待限制在 50 ms;设备 API 使用 7 位地址。

 1#include "qma6100p.h"
 2#include "freertos/FreeRTOS.h"
 3#include "freertos/task.h"
 4static i2c_master_dev_handle_t s_dev;
 5esp_err_t motion_open(i2c_master_bus_handle_t bus) { return bus_device(bus, 0x12, &s_dev); }
 6esp_err_t motion_reset(void)
 7{
 8    uint8_t id;
 9    ESP_RETURN_ON_ERROR(reg_read(s_dev, 0, &id, 1), "qma", "id");
10    if (id != 0x90) return ESP_ERR_NOT_SUPPORTED;
11    ESP_RETURN_ON_ERROR(reg_write(s_dev, 0x36, 0xb6), "qma", "reset");
12    vTaskDelay(pdMS_TO_TICKS(5));
13    ESP_RETURN_ON_ERROR(reg_write(s_dev, 0x36, 0), "qma", "reset release");
14    vTaskDelay(pdMS_TO_TICKS(10));
15    // 教材的芯片上电序列。每一步都检查 I2C 错误。
16    const uint8_t sequence[][2] = {{0x11,0x80},{0x11,0x84},{0x4a,0x20},
17        {0x56,0x01},{0x5f,0x80}};
18    for (size_t i=0; i<sizeof(sequence)/sizeof(sequence[0]); ++i)
19        ESP_RETURN_ON_ERROR(reg_write(s_dev, sequence[i][0], sequence[i][1]), "qma", "setup");
20    vTaskDelay(pdMS_TO_TICKS(1));
21    ESP_RETURN_ON_ERROR(reg_write(s_dev, 0x5f, 0), "qma", "commit");
22    vTaskDelay(pdMS_TO_TICKS(10));
23    ESP_RETURN_ON_ERROR(reg_write(s_dev, 0x0f, 4), "qma", "range"); // ±8 g,1024 LSB/g。
24    ESP_RETURN_ON_ERROR(reg_write(s_dev, 0x10, 0), "qma", "ODR"); // 100 Hz。
25    return reg_write(s_dev, 0x11, 0x84);
26}
27esp_err_t motion_read(acceleration_t *out)
28{
29    uint8_t b[6];
30    ESP_RETURN_ON_ERROR(reg_read(s_dev, 1, b, 6), "qma", "read");
31    // 有符号 14 位数据左对齐;先转 int16_t,避免负值被解释成大正数。
32    out->x = ((int16_t)((b[1]<<8)|b[0]) >> 2) / 1024.0f;
33    out->y = ((int16_t)((b[3]<<8)|b[2]) >> 2) / 1024.0f;
34    out->z = ((int16_t)((b[5]<<8)|b[4]) >> 2) / 1024.0f;
35    return ESP_OK;
36}

CMake 与中文注释

1# 私有依赖只供本组件源码使用。
2idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
3    PRIV_REQUIRES esp_timer BSP )

每个工程有自己的 project() 名称和 sdkconfig.defaults。VS Code 已关闭普通 CMake 自动配置,C/C++ 语言服务从 build/compile_commands.json 读取编译信息。首次构建前,这个文件还不存在,需要先激活 IDF 并完成构建,无需手工拼接头文件路径。

编译、烧录与观察

 1# 激活 EIM 安装的 IDF;每个新终端都需要激活。
 2source ~/.espressif/tools/activate_idf_v5.5.5.sh
 3# 进入单个示例根目录,不是在 examples 总目录运行。
 4cd ~/mycode/esp32-blog/examples/27-qma6100p
 5# 首次配置目标;已有正确配置时不用每次重复 set-target。
 6idf.py set-target esp32s3
 7# 编译会生成 build/compile_commands.json,供 VS Code 使用。
 8idf.py build
 9# 把这个值换成实际连接开发板的端口;macOS 通常为 /dev/cu.*。
10PORT=/dev/ttyUSB0
11idf.py -p "$PORT" flash monitor
12# 串口监视退出:Ctrl+]。

预期输出或屏幕内容(示意;数值、日志前缀和启动时间依现场而变):

1qma: Keep board STILL, +Z UP for calibration in 3 seconds
2qma: g=(0.000,0.000,1.000) roll=0.0 pitch=0.0 LEVEL

故障定位与逐项验收

先确认目标为 esp32s3,再检查实际端口、供电,并按本文引脚表核对跳线。编译成功只能说明源码与工具链兼容,模块型号、物理线路和时序仍需实板确认。

遇到通信错误,先检查错误码和对应模块,不要用“错误时返回 0”掩盖异常。文中的预期日志只供阅读程序时参考;实板验收时,应记录实际现象,保存日志和接线照片。

与教材和官方实现对照

对照教材第 32 章正文:保留芯片初始化寄存器序列,检查每次通信返回值;增加带姿态前提的零偏处理和重力保留,不无条件返回 ESP_OK。

本地验证记录与参考

返回课程目录