05. I2C通信
一、I2C简介
1.1、I2C协议简介
I2C(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接微控制器以及其外围设备。它是由 数据线 SDA 和 时钟线 SCL构成的串行总线,可发送和接收数据,在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送。
I2C 总线的基本参数如下:
- 速率:I2C 总线有标准模式(100kbit/s)和快速模式(400kbit/s)扩展模式和高速模式。
- 器件地址:每个设备都有唯一的 7 位或 10 位地址,可以通过地址选择来确定与谁进行通信。
- 总线状态:I2C 总线有五种状态,分别是空闲状态、起始信号、结束信号、响应信号、数据传输。
- 数据格式:I2C 总线有两种数据格式,标准格式和快速格式。标准格式是 8 位数据字节加上 1 位 ack/nack(应答/非应答)位,快速格式允许两个字节同时传输。
I2C 总线有如下特点:
- 总线由 数据线 SDA 和 时钟线 SCL 构成的串行总线,数据线用来传输数据,时钟线用来同步数据收发。
- 总线上每一个器件都有一个 唯一的地址识别,所以我们只需要知道器件的地址,根据时序就可以实现微控制器与器件之间的通信。
- 数据线 SDA 和 时钟线 SCL 都是 双向线路,都通过一个电流源或上拉电阻连接到正的电压,所以当总线空闲的时候,这两条线路都是 高电平。
- 总线上数据的传输速率在标准模式下可达 100kbit/s,在快速模式下可达 400kbit/s,在高速模式下可达 3.4Mbit/s。
- 总线支持设备连接。在使用 I2C 通信总线时,可以有多个具备 I2C 通信能力的设备挂载在上面,同时支持多个主机和多个从机,连接到总线的接口数量只由总线电容 400pF 的限制决定。
I2C 总线挂载多个器件的示意图,如下图所示:
由于 SCL 和 SDA 线是双向的,它们也可能会由于外部原因(比如线路中的电容等)出现电平误差,而从而导致通信出错。因此,在 I2C 总线中,通常使用上拉电阻来保证信号线在空闲状态下的电平为高电平。
1.2、I2C总线时序图
#define I2C_SCL_GPIO_NUM GPIO_NUM_1
#define I2C_SDA_GPIO_NUM GPIO_NUM_2
#define I2C_SCL(x) gpio_set_level(I2C_SCL_GPIO_NUM, x)
#define I2C_SDA(x) gpio_set_level(I2C_SDA_GPIO_NUM, x)
#define I2C_READ_SDA() gpio_get_level(I2C_SDA_GPIO_NUM)
#define I2C_DELAY() vTaskDelay(pdMS_TO_TICKS(1))
通过宏定义标识符的方式去定义 SCL 和 SDA 两个引脚,同时通过宏定义的方式定义了 I2C_SCL()
和 I2C_SDA()
设置这两个管脚可以输出 0 或者 1,主要还是通过 IDF 库的 GPIO 操作函数实现的。另外方便在 I2C 操作函数中调用读取 SDA 管脚的数据,这里直接宏定义 I2C_READ_SDA()
实现。
/**
* @brief 模拟I2C初始化函数
*
*/
void bsp_simulate_i2c_init(void)
{
gpio_config_t gpio_config_struct = {0};
gpio_config_struct.pin_bit_mask = (1ULL << I2C_SCL_GPIO_NUM) | (1ULL << I2C_SDA_GPIO_NUM); // 设置引脚
gpio_config_struct.intr_type = GPIO_INTR_DISABLE; // 不使用中断
gpio_config_struct.mode = GPIO_MODE_INPUT_OUTPUT_OD; // 开漏输入输出模式
gpio_config_struct.pull_up_en = GPIO_PULLUP_ENABLE; // 使用上拉
gpio_config_struct.pull_down_en = GPIO_PULLUP_DISABLE; // 不使用下拉
gpio_config(&gpio_config_struct); // 配置GPIO
// 空闲时,I2C总线SCL为高电平,I2C SDA为高电平
I2C_SCL(1);
I2C_SDA(1);
}
【1】、起始信号
当 SCL 为高电平期间,SDA 由高到低的跳变。起始信号是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传输。
/**
* @brief I2C产生起始信号函数
*
* @note SCL为高电平期间,SDA从高电平往低电平跳变
*/
void bsp_simulate_i2c_start(void)
{
// 1、释放SDA和SCL,并延迟,空闲状态
I2C_SDA(1);
I2C_SCL(1);
I2C_DELAY();
// 2、拉低SDA,SDA产生下降沿,并延迟
I2C_SDA(0);
I2C_DELAY();
// 3、钳住SCL总线,准备发送数据/接收数据,并延时
I2C_SCL(0);
I2C_DELAY();
}
【2】、停止信号
当 SCL 为高电平期间,SDA 由低到高的跳变。停止信号也是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
/**
* @brief I2C产生结束信号函数
*
* @note SCL为高电平期间,SDA从低电平往高电平跳变
*/
void bsp_simulate_i2c_stop(void)
{
// 1、SDA拉低,SCL拉高,并延迟
I2C_SDA(0);
I2C_SCL(1);
I2C_DELAY();
// 2、拉高SDA,产生上升沿,并延迟
I2C_SDA(1);
I2C_DELAY();
}
【3】、应答信号
发送器每发送一个字节,就在时钟脉冲 9 期间释放数据线,由接收器反馈一个应答信号。应答信号 为 低电平 时,规定为 有效应答位(ACK 简称应答位),表示接收器已经成功地接收了该字节;应答信号 为 高电平 时,规定为 非应答位(NACK),一般表示接收器接收该字节没有成功。
有效应答的要求是从机在第 9 个时钟脉冲之前的低电平期间将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它收到最后一个字节后,发送一个 NACK信号,以通知被控发送器结束数据发送,并释放 SDA线,以便主机接收器发送一个停止信号。
/**
* @brief 主机检测应答信号
*
* @return uint8_t 0,接收应答成功;1,接收应答失败
*/
uint8_t bsp_simulate_i2c_wait_ack(void)
{
uint8_t wait_time = 0;
uint8_t ack = 0;
// 1、主机释放SDA数据线并延迟,此时外部器件可以拉低SDA线
I2C_SDA(1);
I2C_DELAY();
// 2、主机拉高SCL,此时从机可以返回ACK
I2C_SCL(1);
I2C_DELAY();
// 3、SCL高电平期间主机读取SDA状态,等待应答
while (I2C_READ_SDA())
{
// 4、如果超时的话,就直接产生结束信号,非应答
wait_time++;
if (wait_time > 250)
{
bsp_simulate_i2c_stop();
ack = 1;
break;
}
}
// 5、SCL=0,结束ACK检查
I2C_SCL(0);
I2C_DELAY();
// 6、返回是否接收到应答信号
return ack;
}
bsp_i2c_simulate_wait_ack()
函数主要用在写时序中,当启动起始信号,发送完 8bit 数据到从机时,我们就需要等待以及处理接收从机发送过来的响应信号或者非响应信号,一般就是在 bsp_i2c_simulate_send_one_byte()
函数后面调用。
首先先释放 SDA,把电平拉高,延时等待从机操作 SDA 线,然后主机拉高时钟线并延时,确保有充足的时间让主机接收到从机发出的 SDA 信号,这里使用的是 I2C_READ_SDA()
宏定义去读取,根据 I2C 协议,主机读取 SDA 的值为低电平,就表示 “应答信号”;读到 SDA 的值为高电平,就表示 “非应答信号”。在这个等待读取的过程中加入了超时判断,假如超过这个时间没有接收到数据,那么主机直接发出停止信号,跳出循环,返回等于 1 的变量。在正常等待到应答信号后,主机会把 SCL 时钟线拉低并延时,返回是否接收到应答信号。
SDA 为低电平即应答信号,高电平即非应答信号,首先先根据返回 “应答” 或者 “非应答” 两种情况拉低或者拉高 SDA,并延时等待 SDA 电平稳定,然后主机拉高 SCL 线并延时,确保从机能有足够时间去接收 SDA 线上的电平信号。然后主机拉低时钟线并延时,完成这一位数据的传送。最后把 SDA 拉高,呈高阻态,方便后续通信用到。
/**
* @brief 发送应答信号或非应答信号
*
* @param ack 0,发送应答信号;1,发送非应答信号
*/
void bsp_simulate_i2c_send_ack(uint8_t ack)
{
// 1、拉低SDA,表示应答,拉高SDA,表示非应答,并延迟
I2C_SDA(ack);
I2C_DELAY();
// 2、主机拉高SCL线,并延迟,确保从机能有足够时间去接收SDA线上的电平信号
I2C_SCL(1);
I2C_DELAY();
// 3、主机拉低时钟线并延时,完成这一位数据的传送
I2C_SCL(0);
I2C_DELAY();
// 4、释放SDA线,并延迟
I2C_SDA(1);
I2C_DELAY();
}
【4】、数据有效性
I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在 SCL 的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
在 SCL 低电平期间,从机将数据位依次放到 SDA 总线上(高位先行),然后释放 SCL,主机将在 SCL 高电平期间读取数据位,所以 SCL 高电平期间不允许有数据变化,依次循环上过过程 8 次,即可接收一个字节(主机在接收之前,需要释放 SDA)。
/**
* @brief I2C读取一个字节函数
*
* @param ack 0,发送应答信号,1,发送非应答信号
* @return uint8_t
*/
uint8_t bsp_simulate_i2c_read_one_byte(uint8_t ack)
{
uint8_t receive = 0;
// 1、主机释放SDA
I2C_SDA(1);
for (uint8_t i = 0; i < 8; i++)
{
// 2、释放SCL,主机将在SCL高电平期间读取数据位
I2C_SCL(1);
I2C_DELAY();
// 3、读取SDA
if (I2C_READ_SDA())
{
receive |= 0x80 >> i;
}
// 4、拉低SCL,从机切换SDA线输出数据
I2C_SCL(0);
I2C_DELAY();
}
// 5、发送应答信号或非应答信号
bsp_simulate_i2c_send_ack(ack);
// 6、返回读取的数据
return receive;
}
首先可以明确的是时钟信号是通过主机发出的,而且接收到的数据大小为 1 字节,但是 I2C 传输的单位是 bit,所以就需要执行 8 次循环,才能把一字节数据接收完整。
首先需要一个变量 receive 存放接收到的数据。在每次循环的开始的时候,在 SCL 高电平期间加入延时,确保有足够的时间能让数据发送并进行处理,使用宏定义 I2C_READ_SDA()
就可以判断读取到的高低电平,假如 SDA 为高电平,会对 0x80 这个数右移当前的循环数,然后这个结果会与 receive 进行或运算,将指定位置 1,如果 SDA 是低电平,则不进行处理,当前位仍为 0。当 SCL 线拉低后,需要加入延时,便于从机切换 SDA 线输出数据。在 8 次循环结束后,我们就获得了 8bit 数据,把它作为返回值返回,然而按照时序图,作为主机就需要发送应答或者非应答信号,去回复从机。
【5】、数据传输
在 I2C 总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 SCL 串行时钟的配合下,在 SDA 上逐位地串行传送每一位数据。数据位的传输是边沿触发。
数据传输时序图如下:
SCL 低电平期间,主机将数据位依次放到 SDA 总线上(高位先行),然后释放 SCL,从机将在 SCL 高电平期间读取数据位,所以 SCL 高电平期间 SDA 不允许有数据变化,依次循环上述过程 8 次,即可发送一个字节。
/**
* @brief I2C发送一个字节函数
*
* @param data 待发送的数据
*/
void bsp_simulate_i2c_send_one_byte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// 1、发送数据位的高位
I2C_SDA((data & 0x80) >> 7);
I2C_DELAY();
// 2、释放SCL,从机将在SCL高电平期间读取数据位
I2C_SCL(1);
I2C_DELAY();
// 3、拉低SCL
I2C_SCL(0);
// 4、数据左移一位,用于下次发送
data <<= 1;
}
// 5、发送完成,主机释放SDA线
I2C_SDA(1);
}
【6】、空闲状态
I2C 总线的 SDA 和 SCL 两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
在 I2C 的发送函数 bsp_i2c_simulate_send_one_byte()
中,我们把需要发送的数据作为形参,形参大小为 1 个字节。在 I2C 总线传输中,一个时钟信号就发送一个 bit,所以该函数需要循环八次,模拟八个时钟信号,才能把形参的 8 个位数据都发送出去。这里使用的是形参 data 和 0x80 与运算的方式,判断其最高位的逻辑值,假如为 1 即需要控制 SDA 输出高电平,否则为 0 控制 SDA 输出低电平。
经过第一步的 SDA 高低电平的确定后,接着需要延时,确保 SDA 输出的电平稳定,在 SCL 保持高电平期间,SDA 线上的数据是有效的,此过程也是需要延时,使得从设备能够采集到有效的电平。然后准备下一位的数据,所以这里需要的是把 data 左移一位,等待下一个时钟的到来,从设备进行读取。把上述的操作重复 8 次就可以把 data 的 8 个位数据发送完毕,循环结束后,把 SDA 线拉高,等待接收从设备发送过来的应答信号。
1.3、I2C读写操作过程
1.3.1、I2C写操作过程
主机 首先在 I2C 总线上 发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送 从机地址 + 0(写操作)组成的 8bit 数据,所有从机接收到该 8bit 数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么 从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。
IIC 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
1.3.2、I2C读操作过程
主机向从机读取数据的操作由 主机发出起始信号,接着发送 从机地址 + 1(读操作)组成的 8bit 数据,从机接收到数据验证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返回 8bit 数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,直到主机发出非应答信号,从机才会停止发送数据。
二、ESP32的I2C控制器
ESP32 S3有两个 I2C 总线接口,根据用户的配置,总线接口可以用作 I2C 主机或从机模式。I2C 接口特点:
- 可支持标准模式(100Kbit/s)、快速模式(400Kbit/s),速度最高可达 800Kbit/s,但受限于 SCL 和 SDA 上拉强度。
- 可支持 7 位寻址模式和 10 位寻址模式。
- 可支持双地址(从机地址和从机寄存器地址)寻址模式。
在 ESP32 S3 硬件 I2C 控制器中,都有相对应的空间存放相对应的内容。在 cmd 内存区中存放的是就是命令序列,就比如前面提及到的起始信号、写过程、读过程、停止信号;在 RAM 内存区中存放的就是某些命令序列携带的内容。
当主机在软件配置好命令序列和 RAM 数据后,操作寄存器启动数据传输时。控制器的行为可分为以下四步:
- 等待 SCL 线位高电平,以避免 SCL 线被其它主机或者从机占用。
- 执行 RSTART 命令发送 START 位。即发送起始信号。
- 执行 WRITE 命令从 RAM 的首地址开始取出 N+1个字节并一次发送给从机,其中第一个字节为地址。这个过程中会产生对应的时序,携带数据进行发送。
- 发送 STOP 命令,即发送停止信号。
三、I2C常用函数
ESP IDF 提供了一套 API 来配置 I2C。要使用此功能,需要导入必要的头文件:driver/i2c_master.h
。
#include "driver/i2c_master.h"
3.1、I2C主总线初始化
我们可以使用 i2c_new_master_bus()
函数 分配 I2C 主总线,函数原型如下:
/**
* @brief 分配I2C主总线
*
* @param bus_config 指向I2C总线配置结构体的指针
* @param ret_bus_handle I2C总线句柄
* @return esp_err_t ESP_OK分配成功,其它分配失败
*/
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 总线配置结构体的指针,它的定义如下:
typedef struct
{
i2c_port_num_t i2c_port; // I2C总线端口号,如果-1,则自动选择
gpio_num_t sda_io_num; // I2C总线SDA引脚
gpio_num_t scl_io_num; // I2C总线SCL引脚
union
{
i2c_clock_source_t clk_source; // I2C总线时钟源选择
#if SOC_LP_I2C_SUPPORTED
lp_i2c_clock_source_t lp_source_clk; // 低功耗I2C时钟源选择
#endif
};
uint8_t glitch_ignore_cnt; // 如果线路上的故障周期小于此值,则可以过滤掉,通常值为7(单位:I2C模块时钟周期)
int intr_priority; // I2C中断优先级,如果设置为0,驱动程序将选择默认优先级(1,2,3)
size_t trans_queue_depth; // 内部传输队列的深度,只在异步事务中有效
struct
{
uint32_t enable_internal_pullup: 1; // 启用内部拉起。注意:这不足以在高速频率上拉。如果可能的话,建议使用外部上拉
uint32_t allow_pd: 1; // 如果设置,驱动程序将在进入/存在睡眠模式之前/之后备份/恢复I2C寄存器
} flags; // I2C主配置标志
} i2c_master_bus_config_t;
形参 bus_config
的成员 i2c_port
用来 指定 I2C 总线端口号,它的可选值如下:
typedef enum
{
I2C_NUM_0 = 0, // I2C port 0
#if SOC_HP_I2C_NUM >= 2
I2C_NUM_1, // I2C port 1
#endif // SOC_HP_I2C_NUM >= 2
#if SOC_LP_I2C_NUM >= 1
LP_I2C_NUM_0, // LP_I2C port 0
#endif // SOC_LP_I2C_NUM >= 1
I2C_NUM_MAX, // I2C port max
} i2c_port_t;
typedef int i2c_port_num_t;
形参 bus_config
的成员 clk_source
用来 指定 I2C 总线时钟源选择,它的可选值如下:
typedef enum
{
I2C_CLK_SRC_APB = SOC_MOD_CLK_APB,
I2C_CLK_SRC_DEFAULT = SOC_MOD_CLK_APB,
} soc_periph_i2c_clk_src_t;
typedef soc_periph_i2c_clk_src_t i2c_clock_source_t;
3.2、I2C主线添加设备
我们可以使用 i2c_master_bus_add_device()
函数 向 I2C 主线添加设备,它的函数原型如下:
/**
* @brief I2C主线添加设备
*
* @param bus_handle I2C总线句柄
* @param dev_config 指向I2C设备配置结构体的指针
* @param ret_handle I2C设备句柄
* @return esp_err_t ESP_OK添加成功,其它添加失败
*/
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);
形参 dev_config
是 指向 I2C 设备配置结构体的指针,它的定义如下:
typedef struct
{
i2c_addr_bit_len_t dev_addr_length; // 设备地址长度
uint16_t device_address; // 设备地址
uint32_t scl_speed_hz; // SCL时钟频率
uint32_t scl_wait_us; // SCL时钟等待时间,单位us,如果设置为0,则意味着使用默认的reg值
struct
{
uint32_t disable_ack_check: 1; // 关闭ACK检查
} flags; // I2C设备配置标志
} i2c_device_config_t;
形参 dev_config
的成员 dev_addr_length
用来 指定设备地址长度,它的可选值如下:
typedef enum
{
I2C_ADDR_BIT_LEN_7 = 0, // i2c address bit length 7
#if SOC_I2C_SUPPORT_10BIT_ADDR
I2C_ADDR_BIT_LEN_10 = 1, // i2c address bit length 10
#endif
} i2c_addr_bit_len_t;
3.3、I2C设备发送数据
我们可以使用 函数 发送数据,它的函数原型如下:
/**
* @brief I2C设备发送数据
*
* @param i2c_dev I2C设备句柄
* @param write_buffer 发送数据的缓冲区
* @param write_size 发送数据的长度
* @param xfer_timeout_ms 超时时间
* @return esp_err_t ESP_OK发送成功,其它发送失败
*/
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);
3.4、I2C设备读取数据
我们可以使用 i2c_master_receive()
函数 读取数据,它的函数原型如下:
/**
* @brief I2C设备读取数据
*
* @param i2c_dev I2C设备句柄
* @param read_buffer 保存接收数据缓冲区
* @param read_size 读取的数据长度
* @param xfer_timeout_ms 超时时间
* @return esp_err_t ESP_OK读取成功,其它读取失败
*/
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);
3.5、I2C设备发送读取数据
我们可以使用 i2c_master_receive()
函数 发送读取数据,它的函数原型如下:
/**
* @brief I2C设备发送读取数据
*
* @param i2c_dev I2C设备句柄
* @param write_buffer 发送数据的缓冲区
* @param write_size 发送数据的长度
* @param read_buffer 保存接收数据缓冲区
* @param read_size 读取的数据长度
* @param xfer_timeout_ms 超时时间
* @return esp_err_t ESP_OK成功,其它失败
*/
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);
四、SHT30简介
SHT30 是一种常见的温湿度传感器,是一款完全校准的线性化的温湿度数字传感器,增强了数字信号。当 ADDR 引脚接入 GND 时,SHT30 的设备地址为 0x44,当 ADDR 引脚接入 VCC 时,SHT30 的设备地址为 0x45。这里使用 SHT30 模块通过下拉电阻接地。不过需要注意的是,实际地址为 0X44 左移一位,因需要空出最低位给读写位,所以实际的地址是 0X44 << 1。
SHT30 有两种测量模式,分别是 单次测量模式 和 周期测量模式。
在 单次测量模式 下,发出一个测量命令就触发一次数据采集。每个数据都由一个16位的温度值和一个16位的湿度值(按此顺序)组成。在传输过程中,每个数据值后面总是跟着一个 CRC 校验和。但是在该模式下又分有时钟拉伸模式和时钟不拉伸模式,具体情况见下图。
并且在单次测量模式下,可以选择不同的测量命令。它们在可重复性(低、中、高)和时钟拉伸(启用或禁用)方面有所不同。这里的可重复性设置影响测量持续时间,从而影响传感器的总体能耗。
在周期测量模式下,时钟拉伸模式禁用,但是可以分为高中低的可重复性测量,测量周期为 0.5、1、2、4、10(单位 次/秒)(这种模式下最快的测量速度是 1 秒 10 次)如果传感器在一种工作模式下正在测量数据,此时要发送其他命令(推荐先发送一次中断命令),让传感器停止当前的测量,进入单次测量模式,然后再发送命令。这里需要注意:如果测量频率过高,会导致传感器自热 。
设置好周期测量模式的测量周期和可重复性强度后,随时可以进行测量读取数据,需要发送一个读取命令(0xE000)。一旦读取时序结束之后,寄存器中的数值就会清零,如果这时再一次读取数据将得到 0。下一次测量结束后,寄存器的值就会重新写入。
/**
* @brief SHT30设置周期读取模式
*
* @param read_mode SHT30读取模式
*/
void sht30_set_periodic_read_mode(uint16_t read_cmd)
{
bsp_simulate_i2c_start(); // 发送开始信号
bsp_simulate_i2c_send_one_byte(SHT30_I2C_DEVICE_ADDRESS << 1); // 发送设备地址+写操作位
bsp_simulate_i2c_wait_ack(); // 等待ACK信号
// 发送16位命令
bsp_simulate_i2c_send_one_byte(read_cmd >> 8); // 发送命令高8位
bsp_simulate_i2c_wait_ack(); // 等待ACK信号
bsp_simulate_i2c_send_one_byte(read_cmd & 0xFF); // 发送命令低8位
bsp_simulate_i2c_wait_ack(); // 等待ACK信号
bsp_simulate_i2c_stop(); // 发送结束信号
}
/**
* @brief SHT30读取数据
*
* @param read_cmd SHT30读取命令,如果是周期读取模式,则发送0xE000命令
* @param temperature 温度值
* @param humidity 湿度值
*/
void sht30_read_data(uint16_t read_cmd, float *temperature, float *humidity)
{
uint8_t buff[6] = {0};
uint16_t data = 0;
bsp_simulate_i2c_start(); // 发送开始信号
bsp_simulate_i2c_send_one_byte(SHT30_I2C_DEVICE_ADDRESS << 1); // 发送设备地址+写操作位
bsp_simulate_i2c_wait_ack(); // 等待ACK信号
// 发送16位命令
bsp_simulate_i2c_send_one_byte(read_cmd >> 8); // 发送命令高8位
bsp_simulate_i2c_wait_ack(); // 等待ACK信号
bsp_simulate_i2c_send_one_byte(read_cmd & 0xFF); // 发送命令低8位
bsp_simulate_i2c_wait_ack(); // 等待ACK信号
bsp_simulate_i2c_start(); // 再次发送开始信号
bsp_simulate_i2c_send_one_byte(SHT30_I2C_DEVICE_ADDRESS << 1 | 0x01); // 发送设备地址+读操作位
bsp_simulate_i2c_wait_ack(); // 等待ACK信号
for (uint8_t i = 0; i < 6; i++)
{
buff[i] = bsp_simulate_i2c_read_one_byte(i == 5 ? 1 : 0); // 接收数据
}
bsp_simulate_i2c_stop(); // 发送结束信号
//计算温度值
data = buff[0];
data = (data << 8) | buff[1];
*temperature = (data / 65535.0) * 175.0 - 45;
//计算湿度值
data = buff[3];
data = (data << 8) | buff[4];
*humidity = (data / 65535.0) * 100.0;
}
五、实验例程
5.1、I2C相关的函数
这里,我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_i2c.h
文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_i2c.c
文件。
#ifndef _BSP_I2C_H_
#define _BSP_I2C_H_
#include "driver/i2c_master.h"
void bsp_i2c_init(i2c_master_bus_handle_t *handle, i2c_port_t i2c_num, gpio_num_t scl_gpio_num, gpio_num_t sda_gpio_num);
void bsp_i2c_bus_add_device(i2c_master_dev_handle_t *device_handle, i2c_master_bus_handle_t bus_handle, uint16_t device_address, i2c_addr_bit_len_t device_address_length, int clock_speed, uint32_t scl_wait_us);
void bsp_i2c_send_one_byte(i2c_master_dev_handle_t device_handle, uint8_t data);
void bsp_i2c_send_bytes(i2c_master_dev_handle_t device_handle, const uint8_t *data, uint16_t length);
uint8_t bsp_i2c_receive_one_byte(i2c_master_dev_handle_t device_handle);
void bsp_i2c_receive_bytes(i2c_master_dev_handle_t device_handle, uint8_t *data, uint16_t length);
void bsp_i2c_transmit_receive(i2c_master_dev_handle_t device_handle, uint8_t *write_buffer, size_t write_size, uint8_t *read_buffer, size_t read_size);
#endif // !_BSP_I2C_H_
#include "bsp_i2c.h"
i2c_master_bus_handle_t g_i2c_master_bus_handle;
/**
* @brief I2C初始化
*
* @param handle I2C控制器句柄
* @param i2c_num I2C控制器编号
* @param scl_gpio_num SCL引脚编号
* @param sda_gpio_num SDA引脚编号
*/
void bsp_i2c_init(i2c_master_bus_handle_t *bus_handle, i2c_port_t i2c_num, gpio_num_t scl_gpio_num, gpio_num_t sda_gpio_num)
{
i2c_master_bus_config_t i2c_master_config = {0};
i2c_master_config.i2c_port = i2c_num; // I2C控制器编号
i2c_master_config.scl_io_num = scl_gpio_num; // SCL引脚编号
i2c_master_config.sda_io_num = sda_gpio_num; // SDA引脚编号
i2c_master_config.clk_source = I2C_CLK_SRC_DEFAULT; // 时钟源
i2c_master_config.glitch_ignore_cnt = 7; // 滤波器宽度
i2c_master_config.flags.enable_internal_pullup = true; // 启用内部上拉
i2c_new_master_bus(&i2c_master_config, bus_handle);
}
/**
* @brief I2C总线添加设备
*
* @param device_handle I2C设备句柄
* @param bus_handle I2C总线句柄
* @param device_address I2C设备地址
* @param device_address_length I2C设备地址长度
* @param mode I2C设备通信模式
* @param clock_speed I2C设备通信时钟频率
* @param scl_wait_us I2C设备通信时,SCL线等待的时间
*/
void bsp_i2c_bus_add_device(i2c_master_dev_handle_t *device_handle,
i2c_master_bus_handle_t bus_handle,
uint16_t device_address,
i2c_addr_bit_len_t device_address_length,
int clock_speed,
uint32_t scl_wait_us)
{
i2c_device_config_t i2c_device_config = {0};
i2c_device_config.device_address = device_address; // I2C设备的设备地址
i2c_device_config.dev_addr_length = device_address_length; // I2C设备的设备长度
i2c_device_config.scl_speed_hz = clock_speed; // I2C设备通信的时钟频率
i2c_device_config.scl_wait_us = scl_wait_us; // I2C设备通信时,SCL线等待的时间
i2c_master_bus_add_device(bus_handle, &i2c_device_config, device_handle); // 添加I2C设备
}
/**
* @brief I2C发送一个字节数据
*
* @param device_handle I2C设备句柄
* @param data 要发送的数据
*/
void bsp_i2c_send_one_byte(i2c_master_dev_handle_t device_handle, uint8_t data)
{
i2c_master_transmit(device_handle, &data, 1, 1000);
}
/**
* @brief I2C发送多个字节数据
*
* @param device_handle I2C设备句柄
* @param data 要发送的多个字节的数据的缓冲区
*/
void bsp_i2c_send_bytes(i2c_master_dev_handle_t device_handle, const uint8_t *data, uint16_t length)
{
i2c_master_transmit(device_handle, data, length, 1000);
}
/**
* @brief I2C读取一个字节数据
*
* @param device_handle I2C设备句柄
* @return uint8_t 读取的数据
*/
uint8_t bsp_i2c_receive_one_byte(i2c_master_dev_handle_t device_handle)
{
uint8_t data = {0};
i2c_master_receive(device_handle, &data, 1, 1000);
return data;
}
/**
* @brief I2C读取多个字节数据
*
* @param device_handle I2C设备句柄
* @param data 要发送的多个字节的数据的缓冲区
*/
void bsp_i2c_receive_bytes(i2c_master_dev_handle_t device_handle, uint8_t *data, uint16_t length)
{
i2c_master_receive(device_handle, data, length, 1000);
}
/**
* @brief I2C发送和接收多个字节数据
*
* @param device_handle I2C设备句柄
* @param write_buffer 要发送的多个字节的数据的缓冲区
* @param write_size 要发送的多个字节的数据长度
* @param read_buffer 要接收的多个字节的数据的缓冲区
* @param read_size 要接收的多个字节的数据长度
*/
void bsp_i2c_transmit_receive(i2c_master_dev_handle_t device_handle, uint8_t *write_buffer, size_t write_size, uint8_t *read_buffer, size_t read_size)
{
i2c_master_transmit_receive(device_handle, write_buffer, write_size, read_buffer, read_size, 1000);
}
5.2、SHT30相关的函数
我们在【components】文件夹下的【device】文件夹中新增了一个 【sht30】 文件夹,用于存放 sht30.c 和 sht30.h 这两个文件。
#ifndef __SHT30_H__
#define __SHT30_H__
#include "driver/i2c.h"
#include "bsp_i2c.h"
#define SHT30_I2C_DEVICE_ADDRESS 0x44
extern i2c_master_dev_handle_t g_sht30_i2c_device_handle;
void sht30_set_periodic_read_mode(uint16_t read_cmd);
void sht30_read_data(uint16_t read_cmd, float *temperature, float *humidity);
#endif // !__SHT30_H__
#include "sht30.h"
i2c_master_dev_handle_t g_sht30_i2c_device_handle;
/**
* @brief SHT30设置周期读取模式
*
* @param read_cmd SHT30读取模式
*/
void sht30_set_periodic_read_mode(uint16_t read_cmd)
{
uint8_t cmd[2] = {0};
cmd[0] = read_cmd >> 8;
cmd[1] = read_cmd & 0xFF;
bsp_i2c_send_bytes(g_sht30_i2c_device_handle, cmd, 2);
}
/**
* @brief SHT30读取数据
*
* @param i2c_num I2C端口号
* @param device_address SHT30设备地址
* @param read_cmd SHT30读取命令
* @param temperature 温度值
* @param humidity 湿度值
*/
void sht30_read_data(uint16_t read_cmd, float *temperature, float *humidity)
{
uint8_t cmd[2] = {0};
uint8_t buff[6] = {0};
uint16_t data = 0;
cmd[0] = read_cmd >> 8;
cmd[1] = read_cmd & 0xFF;
bsp_i2c_transmit_receive(g_sht30_i2c_device_handle, cmd, 2, buff, 6);
//计算温度值
data = buff[0];
data = (data << 8) | buff[1];
*temperature = (data / 65535.0) * 175.0 - 45;
//计算湿度值
data = buff[3];
data = (data << 8) | buff[4];
*humidity = (data / 65535.0) * 100.0;
}
然后,我们修改【components】文件夹下的【device】文件夹下的 CMakeLists.txt
文件。
# 源文件路径
set(src_dirs
sht30
)
# 头文件路径
set(include_dirs
sht30
# ${CMAKE_SOURCE_DIR}表示顶级 CMakeLists.txt 文件所在的目录的绝对路径
${CMAKE_SOURCE_DIR}/components/peripheral/inc
)
# 设置依赖库
set(requires
driver
)
# 注册组件到构建系统的函数
idf_component_register(
# 源文件路径
SRC_DIRS ${src_dirs}
# 自定义头文件的路径
INCLUDE_DIRS ${include_dirs}
# 依赖库的路径
REQUIRES ${requires}
)
# 设置特定组件编译选项的函数
# -ffast-math: 允许编译器进行某些可能减少数学运算精度的优化,以提高性能。
# -O3: 这是一个优化级别选项,指示编译器尽可能地进行高级优化以生成更高效的代码。
# -Wno-error=format: 这将编译器关于格式字符串不匹配的警告从错误降级为警告。
# -Wno-format: 这将完全禁用关于格式字符串的警告。
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
5.3、main()函数
修改【main】文件夹下的 main.c
文件。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "sht30.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
float temperature = 0, humidity = 0;
bsp_i2c_init(&g_i2c_master_bus_handle, I2C_NUM_0, GPIO_NUM_1, GPIO_NUM_2);
bsp_i2c_bus_add_device(&g_sht30_i2c_device_handle, g_i2c_master_bus_handle, SHT30_I2C_DEVICE_ADDRESS, I2C_ADDR_BIT_LEN_7, 100000, 10);
sht30_set_periodic_read_mode(0x2130);
while (1)
{
sht30_read_data(0xE000, &temperature, &humidity);
printf("temperature: %.2f, humidity: %.2f\n", temperature, humidity);
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(1000));
}
}