I2C
简介(from AI):
这篇博客以ESP32的I2C通信为核心,深入讲解了I2C协议基础知识、ESP32硬件支持及其API使用方法,适合嵌入式开发者快速上手I2C编程。文章逻辑清晰,从理论到实践层层展开,涵盖了I2C通信原理、总线特性、上拉电阻的选择等基础内容,并结合ESP-IDF的API对I2C主从模式的安装、卸载、数据读写进行了详尽说明。通过表格和代码示例,详细展示了API参数和返回值的用法,代码部分以ADXL345加速度传感器为案例,提供了完整的初始化和数据读取流程,注释详尽,便于实操应用。博文不仅技术内容全面,还注重实际工程需求,如线程安全、功耗优化等问题的讨论,贴合嵌入式开发场景。
前言:本文档是本人在依照乐鑫科技编写的ESP32 API文档进行学习时所做的学习笔记,可能存在疏漏和错误,如有发现,敬请指正。
文章目录
I2C(Inter-Integrated Circuit)是一种串行同步半双工通信协议,I2C总线上可以同时挂载多个主机和从机。I2C 使用两条双向开漏线:串行数据线 (SDA) 和串行时钟线 (SCL),通过电阻上拉。通常,I2C 从机设备具有 7 位地址或 10 位地址
总线是连接计算机或嵌入式系统中各个组件之间的通信通道,用于在设备之间传输数据、地址和控制信号。它是一种共享资源,可以同时连接多个设备,按一定的协议进行数据交换。根据功能和用途,总线可以分为 数据总线(传输数据)、地址总线(指定存储位置)和 控制总线(传递控制信号)。常见的总线类型包括并行总线(如 PCI)和串行总线(如 I²C、SPI、UART),它们广泛用于芯片间通信、外设控制和数据传输
SCL(串行时钟线)
由主设备生成时钟信号
SDA(串行数据线)
用于传输数据,支持双向通信
通信角色
- 支持一个主设备(Master)控制多个从设备(Slave)
- 也支持多个主设备,但需要处理总线仲裁
总线仲裁是指在多主设备的通信系统中,当多个主设备同时尝试控制总线时,通过某种机制决定哪一个设备可以优先使用总线的过程
特性 | 主设备(Master) | 从设备(Slave) |
---|---|---|
角色 | 发起通信并控制总线 | 响应主设备并传输数据 |
时钟生成 | 生成 SCL 信号 | 使用主设备的时钟 |
通信发起 | 发送起始条件,选择从设备 | 被动等待主设备选择 |
地址管理 | 指定从设备的地址 | 每个从设备有唯一地址 |
多设备支持 | 支持多个主设备(多主模式) | 支持多个从设备 |
通信过程
- 主设备启动通信: 主设备拉低 SDA,并开始时钟信号(SCL)
- 发送从设备地址: 主设备发送 7 位或 10 位地址,并附加读/写位
- 从设备应答: 目标从设备检测到地址匹配后,发送 ACK(低电平)
- 数据传输:
- 主设备发送或接收数据字节
- 主/从设备在每个字节后发送 ACK
- 停止通信: 主设备释放 SDA 和 SCL,总线恢复空闲状态
在 I2C协议中,地址是用来标识从机设备的唯一标识符。当主机(Master)与从机(Slave)通信时,需要通过这个地址选择要与之通信的从机设备
ESP32 有 2 个 I2C 控制器(也被称为端口),负责处理 I2C 总线上的通信。单个 I2C 控制器既可以是主机也可以是从机
ESP32 支持 I2C 标准模式 (Sm) 和快速模式 (Fm),可分别达到 100 kHz 和 400 kHz(主机模式的 SCL 时钟频率不应大于 400 kHz)
SCL 的频率受上拉电阻及导线电容的影响。因此,强烈建议选择适当的上拉电阻,确保频率准确。推荐的上拉电阻值通常在 1 kΩ 到 10 kΩ 之间
SCL 的频率越高,上拉电阻应该越小(但不能小于 1 kΩ)。较大的电阻会降低电流,增加时钟切换时间并降低频率。通常推荐 2 kΩ 到 5 kΩ 左右的电阻,也可根据电流需求进行一定调整
上拉电阻是一种连接在信号线和电源正极之间的电阻,用于将信号线默认保持在逻辑高电平状态,防止信号悬空导致不稳定。它广泛应用于数字电路和通信总线(如 I²C),起到稳定信号、电平匹配和设定默认逻辑状态的作用。阻值通常在 1 kΩ 至 10 kΩ 之间,选择时需权衡功耗和信号响应速度
当有低电平输入时,信号线会从高电平被拉低到低电平,直到低电平输入解除,信号线才会恢复到高电平。上拉电阻在这个过程中不会干扰低电平输入的有效性,只在信号线空闲时提供默认高电平状态
I2C 驱动程序提供以下服务:
资源分配
包括如何使用正确的配置来分配 I2C 总线,以及如何在完成工作后回收资源I2C 主机控制器
包括 I2C 主机控制器的行为,介绍了数据发送、数据接收和数据的双向传输I2C 从机控制器
包括 I2C 从机控制器的行为,涉及数据发送和数据接收电源管理
描述了不同时钟源对功耗的影响IRAM 安全
描述了如何在 cache 被禁用时正常运行 I2C 中断线程安全
列出了驱动程序中线程安全的 APIKconfig 选项
列出了支持的 Kconfig 选项,这些选项可以对驱动程序产生不同影响
在嵌入式开发中,IRAM 安全(IRAM-Safe) 通常是指某些函数或代码段被设计为在 IRAM(Internal RAM, 内部 RAM) 中运行,以确保在系统关键时刻(如中断或低功耗模式)仍然可用
线程安全(Thread-Safe) 是指某些代码或函数在多线程环境下能够正确运行,不会因并发访问共享资源而导致数据错误或异常行为。在 I²C 通信中,线程安全是非常重要的,尤其是在多任务操作系统(如 FreeRTOS)中,多个线程可能同时尝试访问 I²C 总线
Kconfig选项 是一种用于配置嵌入式系统(例如 Linux 内核、Zephyr、ESP-IDF 等)的配置机制。它允许开发者通过层级化的菜单选项为系统功能和模块进行配置。Kconfig 文件定义了这些配置选项的规则、依赖关系和默认值
Headers that need to be included in the I2C application
i2c_master.h
用于使用主机模式的新驱动程序的应用i2c_slave.h
用于使从机模式的新驱动程序的应用
上述头文件中包含的公共头文件
i2c_types_legacy.h
仅在旧驱动程序中使用的旧公共类型i2c_types.h
提供公共类型的头文件
Install/Uninstall master bus and device
Install I2C master bus and device
I2C 主机总线是基于总线-设备模型设计的,因此需要分别使用 i2c_master_bus_config_t
和 i2c_device_config_t
来分配 I2C 主机总线实例和 I2C 设备实例
i2c_new_master_bus()
分配和初始化 I2C 总线
esp_err_t i2c_new_master_bus(const i2c_master_bus_config_t *bus_config,
i2c_master_bus_handle_t *ret_bus_handle)
参数
bus_config
I2C 主机总线配置
ret_bus_handle
I2C 总线句柄(传入地址,由函数写入)
bus_config 定义
// 示例
i2c_master_bus_config_t i2c_mst_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = TEST_I2C_PORT,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_io_num = I2C_MASTER_SDA_IO,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
clk_source
I2C主总线的时钟源
i2c_port
I2C端口号,-1
表示自动选择(不包括低功耗I2C实例)
scl_io_num
I2C SCL信号的GPIO编号,内部支持上拉
sda_io_num
I2C SDA信号的GPIO编号,内部支持上拉
glitch_ignore_cnt
设定线上的干扰周期过滤值,小于该值的干扰将被忽略,典型值为7
(单位:I2C模块时钟周期)
flags.enable_internal_pullup
启用内部上拉(内部上拉强度不足以支持高频传输,推荐使用外部合适的上拉电阻)
intr_priority
I2C中断优先级,若设置为0
,则驱动程序使用默认优先级(通常为1、2或3)
trans_queue_depth
内部传输队列深度。值越大,支持的后台挂起传输越多,仅适用于异步传输,建议设置为 max_device_num per_transaction
allow_pd
配置驱动程序是否允许系统在睡眠模式下关闭外设电源。在进入睡眠之前,系统将备份 I2C 寄存器上下文,当系统退出睡眠模式时,这些上下文将被恢复。关闭外设可以节省更多功耗,但代价是消耗更多内存来保存寄存器上下文。你需要在功耗和内存消耗之间做权衡。此配置选项依赖于特定的硬件功能,如果在不支持的芯片上启用它,你将看到类似 not able to power down in light sleep
的错误消息
端口宏定义
#define I2C_NUM_0 (0) // I2C 端口 0
#define I2C_NUM_1 (1) // I2C 端口 1
时钟配置
i2c_clock_source_t::I2C_CLK_SRC_DEFAULT
默认的 I2C 时钟源
i2c_clock_source_t::I2C_CLK_SRC_APB
以 APB 时钟作为 I2C 时钟源
I2C_CLK_SRC_DEFAULT 是一种由硬件或驱动自动选择的默认时钟源模式,其具体时钟来源因平台而异,通常为内部模块的时钟。该模式适合对时钟精度和性能要求不高的应用场景,开发者无需关注具体的时钟配置,适用于低速或普通速率的通信(如 100kHz 或 400kHz)。这种模式具有灵活性,并能在大多数情况下提供足够的性能,同时优化功耗
I2C_CLK_SRC_APB 模式明确使用 APB(高级外设总线)时钟作为 I2C 的时钟源,提供高频率和稳定的时钟信号,通常与 CPU 的主频相关。这种模式适合对通信速率和时钟精度有较高要求的应用,例如高速 I2C(1MHz 或更高)或需要稳定传输的场景。尽管功耗相对较高,但它在高性能和时间敏感的任务中表现优异
返回值
ESP_OK
I2C 主机总线初始化成功
ESP_ERR_INVALID_ARG
I2C 总线初始化失败,因为参数无效
ESP_ERR_NO_MEM
由于内存不足,创建 I2C 总线失败
ESP_ERR_NOT_FOUND
没有可用的 I2C 端口
i2c_master_bus_add_device()
分配 I2C 设备实例,并将设备挂载到主机总线上
分成bus和device能够更好的管理总线。比如一个总线上挂在的设备有不同的地址和传输速率,在这种框架下只要对device操作就可以了。如果没有device的话,要频繁去改变bus的参数,很麻烦
esp_err_t i2c_master_bus_add_device(i2c_master_bus_handle_t bus_handle,
const i2c_device_config_t *dev_config,
i2c_master_dev_handle_t *ret_handle)
参数
bus_handle
I2C 总线句柄
dev_config
设备配置
ret_handle
设备句柄(传入地址,由函数写入)
dev_config 定义
// 示例
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = 0x58,
.scl_speed_hz = 100000,
};
dev_addr_length
从设备的地址长度
device_address
I2C 设备原始地址
scl_speed_hz
SCL 线路频率
scl_wait_us
超时时间(单位:微秒),该值不能过小,否则可能无法正确处理时钟拉伸/干扰,设为0表示使用默认值(默认值为 0,可不自己设置)
disable_ack_check
禁用ACK检查。如果设置为 false
,表示启用ACK检查,当检测到NACK时,传输将停止并返回错误(默认值为 false
)
返回值
ESP_OK
成功创建 I2C 主设备
ESP_ERR_INVALID_ARG
I2C 总线初始化失败,因为参数无效
ESP_ERR_NO_MEM
由于内存不足,创建 I2C 总线失败
i2c_master_get_bus_handle()
该函数用于获取指定 I2C 端口号对应的主机总线句柄。前提是该端口的句柄已经初始化,函数仅返回已存在的句柄,且该句柄不能并发使用(适用于另一个模块中不方便获取该句柄时)
esp_err_t i2c_master_get_bus_handle(i2c_port_num_t port_num,
i2c_master_bus_handle_t *ret_handle)
参数
port_num
I2C端口号,用于指定需要获取句柄的I2C端口
ret_handle
指向一个变量的指针,用于存储检索到的句柄(传入地址,由函数写入)
返回值
ESP_OK
成功,句柄已成功检索
ESP_ERR_INVALID_ARG
参数无效,例如提供了非法的端口号
ESP_ERR_INVALID_STATE
状态无效,例如I2C端口尚未初始化
Uninstall I2C master bus and device
如果不再需要之前安装的 I2C 总线或设备,调用 i2c_master_bus_rm_device()
或 i2c_del_master_bus()
来回收资源,以释放底层硬件
i2c_master_bus_rm_device()
从 I2C 主总线上移除指定的设备
esp_err_t i2c_master_bus_rm_device(i2c_master_dev_handle_t handle)
参数
handle
I2C设备句柄,需要删除的目标设备句柄
返回值
ESP_OK
设备成功被删除
i2c_del_master_bus()
反初始化 I2C 主总线,并删除对应的总线句柄
esp_err_t i2c_del_master_bus(i2c_master_bus_handle_t bus_handle)
参数
bus_handle
I2C 总线句柄,需要删除的目标总线句柄
返回值
ESP_OK
总线成功被删除
其它
表示某些模块删除失败
Install I2C slave device
i2c_new_slave_device()
初始化一个 I2C 从设备,并返回对应的设备句柄
esp_err_t i2c_new_slave_device(const i2c_slave_config_t *slave_config,
i2c_slave_dev_handle_t *ret_handle)
参数
slave_config
设备配置
ret_handle
设备句柄(传入地址,由函数写入)
slave_config 定义
// 示例
i2c_slave_config_t i2c_slv_config = {
.i2c_port = I2C_SLAVE_NUM,
.clk_source = I2C_CLK_SRC_DEFAULT,
.scl_io_num = I2C_SLAVE_SCL_IO,
.sda_io_num = I2C_SLAVE_SDA_IO,
.slave_addr = ESP_SLAVE_ADDR,
.send_buf_depth = 100,
.receive_buf_depth = 100,
};
i2c_port
端口号,-1
表示自动选择端口
clk_source
总线的时钟源
slc_io_num
总线 SCL 信号的 GPIO 引脚编号
sda_io_num
总线 SDA 信号的 GPIO 引脚编号
slave_addr
从设备地址
send_buf_depth
从设备地址的位长度(通常为7位或10位)
receive_buf_depth
内部接收环形缓冲区的深度
这个参数有点奇怪,示例里面给了,但是文档中 i2c_slave_config_t 的解析里面和 “i2c_slave.h” 的定义又没有,从https://github.com/espressif/esp-idf/blob/master/components/esp_driver_i2c/include/driver/i2c_slave.h#L146 可以看出是文档疏漏
addr_bit_len
从设备地址的位长度(通常为7位或10位)
intr_priority
内部传输环形缓冲区的深度,值越大可支持更多后台挂起的传输
allow_pd
如果设置为 1
,在进入/退出睡眠模式前后,驱动会备份/恢复I2C寄存器,从而允许系统关闭I2C的电源域以节省功耗,但会增加RAM消耗
返回值
ESP_OK
成功创建 I2C 从设备
ESP_ERR_INVALID_ARG
参数无效
ESP_ERR_NO_MEM
由于内存不足,创建从设备失败
Uninstall I2C slave device
如果不再需要之前安装的 I2C 总线,建议调用 i2c_del_slave_device()
来回收资源,以释放底层硬件
i2c_del_slave_device()
反初始化 I2C 从设备并释放对应的资源
esp_err_t i2c_del_slave_device(i2c_slave_dev_handle_t i2c_slave)
参数
i2c_slave
I2C 设备句柄,需要删除的目标设备句柄
返回值
ESP_OK
设备成功被删除
ESP_ERR_INVALID_ARG
参数无效,例如传入的设备句柄为空或无效
I2C Master Controller
通过调用 i2c_new_master_bus()
安装好 I2C 主机控制器驱动程序后,ESP32 就可以与其他 I2C 设备进行通信了
I2C Master Write
在成功安装 I2C 主机总线之后,可以通过调用 i2c_master_transmit()
来向从机设备写入数据,通信过程如下
Master : START | Device address W Data STOP
Slave : ACK ACK
i2c_master_transmit()
在 I2C 总线上执行一次写入事务
- 如果未注册回调函数,函数将阻塞直到传输完成或超时
- 如果注册了回调函数,则事务为异步,函数立即返回,传输完成信息通过回调函数获取
esp_err_t i2c_master_transmit(i2c_master_dev_handle_t i2c_dev,
const uint8_t *write_buffer,
size_t write_size,
int xfer_timeout_ms)
参数
i2c_dev
由 i2c_master_bus_add_device
创建的 I2C 主设备句柄
write_buffer
待发送的数据缓冲区
write_size
write_buffer
的大小(字节数)
xfer_timeout_ms
输入参数,等待超时时间(单位:毫秒),-1
表示永不超时
返回值
ESP_OK
成功,数据已通过 I2C 总线发送
ESP_ERR_INVALID_ARG
参数无效,例如设备句柄为空、缓冲区无效或缓冲区大小为 0
ESP_ERR_TIMEOUT
超时,可能是总线忙或硬件故障,传输时间超过 xfer_timeout_ms
I2C Master Read
在成功安装 I2C 主机总线后,可以通过调用 i2c_master_receive()
从从机设备读取数据
通信过程如下
Master : START | Device address W ACK NACK STOP
Slave : ACK Data ACK Data
i2c_master_receive()
在 I2C 总线上执行一次读取,从从设备接收数据
esp_err_t i2c_master_receive(i2c_master_dev_handle_t i2c_dev,
uint8_t *read_buffer,
size_t read_size,
int xfer_timeout_ms)
参数
i2c_dev
I2C 主设备句柄
read_buffer
用于存储从 I2C 总线接收到的数据
read_size
读取缓冲区的大小
xfer_timeout_ms
输入参数,设置等待超时时间(单位:毫秒),-1
表示无限等待直到操作完成。
返回值
ESP_OK
成功
ESP_ERR_INVALID_ARG
参数无效
ESP_ERR_TIMEOUT
超时
I2C Master Write and Read Permalink to this headline
从一些 I2C 设备中读取数据之前需要进行写入配置(如 ADXL345),可通过 i2c_master_transmit_receive()
接口进行配置
通信过程如下
Master : START | Device address W Data START Device address R NACK STOP
Slave : ACK Data ACK Data
i2c_master_transmit_receive()
在 I2C 总线上执行一次 写入-读取(Write-Read)
- 主设备先向从设备发送数据(写操作),然后从从设备接收数据(读操作)
esp_err_t i2c_master_transmit_receive(i2c_master_dev_handle_t i2c_dev,
const uint8_t *write_buffer,
size_t write_size,
uint8_t *read_buffer,
size_t read_size,
int xfer_timeout_ms)
参数
i2c_dev
I2C 主设备句柄
write_buffer
待发送的数据缓冲区
write_size
写缓冲区的大小(字节数)
read_buffer
用于存储从设备返回的数据
read_size
读缓冲区的大小(字节数)
xfer_timeout_ms
输入参数,设置等待超时时间(单位:毫秒),-1
表示无限等待直到操作完成
返回值
ESP_OK
成功
ESP_ERR_INVALID_ARG
参数无效
ESP_ERR_TIMEOUT
超时
Example Code:ESP32 I2C Communication with ADXL345 Accelerometer
/*
* Author: Lamonce
* Description: i2c communication test
* Date: 2024-12-5
* esp-idf Version: 5.3.1
*/
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#define ADXL345_I2C_ADDRESS 0x53 // ADXL345 地址
#define I2C_MASTER_SDA_IO 21 // SDA 引脚
#define I2C_MASTER_SCL_IO 22 // SCL 引脚
#define I2C_MASTER_FREQ_HZ 400000 // 通信速率
#define GRAVITY_ACCELERATION 32 / 8196.0 // 重力加速度
i2c_master_bus_handle_t i2c_master_bus_handle; // I2C 总线句柄
i2c_master_dev_handle_t i2c_master_ADXL345_handle; // ADXL345 设备句柄
// 在此处添加函数声明
void i2cMasterInit(void); // I2C 总线初始化
void i2cADXL345Init(void); // ADXL345 设备初始化
void ADXL345ReadData(void); // 读取 ADXL345 数据
void app_main(void)
{
vTaskDelay(5000 / portTICK_PERIOD_MS); // 延时 5 秒,方便观察LOG
i2cMasterInit(); // I2C 总线初始化
i2cADXL345Init(); // ADXL345 设备初始化
for (int i = 0; i < 10; i++)
{
ADXL345ReadData();
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
i2c_del_master_bus(i2c_master_bus_handle); // 删除 I2C 总线
ESP_LOGI("I2C", "I2C master has been deleted.");
}
void i2cMasterInit(void)
{
i2c_master_bus_config_t i2c_master_config = {
.clk_source = I2C_CLK_SRC_DEFAULT, // 使用默认时钟源
.i2c_port = I2C_NUM_0, // I2C 端口
.sda_io_num = I2C_MASTER_SDA_IO, // SDA 引脚
.scl_io_num = I2C_MASTER_SCL_IO, // SCL 引脚
.glitch_ignore_cnt = 7, // 设置主机总线的毛刺周期
.flags.enable_internal_pullup = true, // 启用内部上拉
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_master_config, &i2c_master_bus_handle)); // 创建 I2C 总线
ESP_LOGI("I2C", "I2C master has been initialized.");
}
void i2cADXL345Init(void)
{
i2c_device_config_t i2c_device_config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7, // 设备地址长度
.device_address = ADXL345_I2C_ADDRESS, // 设备地址
.scl_speed_hz = I2C_MASTER_FREQ_HZ, // 通信速率
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_master_bus_handle, &i2c_device_config, &i2c_master_ADXL345_handle)); // 添加设备,注意传参顺序
ESP_LOGI("I2C", "ADXL345 has been added to the bus.");
uint8_t adxl345_config_data[] = {
0x31, 0x0f, // 设置数据左对齐
0x2c, 0x08, // 设置速率
0x2d, 0x08, // 设置电源模式
0x2e, 0x80, // 使能 DATA_READY 中断
0x1e, 0x00, // x偏移量
0x1f, 0x00, // y偏移量
0x20, 0x00, // z偏移量
};
for (int i = 0; i < sizeof(adxl345_config_data); i += 2) // 写入配置数据
{
ESP_ERROR_CHECK(i2c_master_transmit(i2c_master_ADXL345_handle, &adxl345_config_data[i], 2, -1));
}
vTaskDelay(100 / portTICK_PERIOD_MS); // 延时 100ms,等待配置生效
uint8_t init_check = 0;
uint8_t reg_address = 0x00;
ESP_ERROR_CHECK(i2c_master_transmit_receive(i2c_master_ADXL345_handle, ®_address, 1, &init_check, 1, -1)); // 读取初始化检查寄存器
ESP_LOGI("ADXL345", "init_check: %x", init_check);
if (init_check == 0xE5) // 读出的数据为0XE5,表示正确
{
ESP_LOGI("ADXL345", "ADXL345 has been initialized successfully.");
}
else
{
ESP_LOGE("ADXL345", "ADXL345 initialization failed.");
}
}
void ADXL345ReadData(void)
{
uint8_t reg_address = 0x32;
uint8_t adxl345_data[6];
ESP_ERROR_CHECK(i2c_master_transmit_receive(i2c_master_ADXL345_handle, ®_address, 1, adxl345_data, sizeof(adxl345_data), -1)); // 读取数据
// ADXL345 数据为补码,需要转换为原码
for (int i = 0; i < sizeof(adxl345_data); i++) // 将补码转换为原码
{
adxl345_data[i] = ~adxl345_data[i];
adxl345_data[i] += 1;
}
int16_t x_data = (adxl345_data[1] << 8) | adxl345_data[0];
int16_t y_data = (adxl345_data[3] << 8) | adxl345_data[2];
int16_t z_data = (adxl345_data[5] << 8) | adxl345_data[4];
// 格式化数据
x_data >>= 3;
y_data >>= 3;
z_data >>= 3;
if (x_data > 4096)
{
x_data -= 8192;
}
if (y_data > 4096)
{
y_data -= 8192;
}
if (z_data > 4096)
{
z_data -= 8192;
}
// 转换为重力加速度
double x_data_g = x_data * GRAVITY_ACCELERATION;
double y_data_g = y_data * GRAVITY_ACCELERATION;
double z_data_g = z_data * GRAVITY_ACCELERATION;
ESP_LOGI("ADXL345", "x_data: %fg, y_data: %fg, z_data: %fg", x_data_g, y_data_g, z_data_g);
}