本文目录
- 一、基础
- 1. 查看开发板手册,获取可用IIC总线
- 2. 挂载从机,查看从机地址。
- 3. 查看BMP180手册,使用命令读/写某寄存器值。
- 4. 查看BMP180手册通信流程。
- 二、IIC常用API
- 1. iic数据包/报
- 2. ioctl函数
- 三、数据包如何被处理
- 四、代码编写流程
- 1. IIC读取数据
- 2. IIC写入数据
- 3. 读取校验参数
- 4. 读取未校准的温度值
- 5. 读取未校准的压力值
- 6. 将未校准的测量值转为真实值
- 五、完整代码
在 Linux ARM 平台上使用 I2C 时,不需要手动编写 I2C 时序是因为 Linux 内核和硬件抽象层已经处理了这些复杂的细节,提供了高层次的接口供开发者使用。
无论是哪个IIC从机设备,我们只需要实现 IIC读数据和IIC写数据即可。然后根据不同设备的手册规则来向寄存器写或者读数据,从而实现某些特定的功能。
一、基础
1. 查看开发板手册,获取可用IIC总线
香橙派OrangepiAipor引脚只有两个IIC可以使用,分别是IIC6和IIC7。分别对应如下两个设备节点。
2. 挂载从机,查看从机地址。
我们将从机设备随便连接到IIC的其中一个上面,这里我们使用BMP180作为从机设备连接香橙派的IIC7总线,对应的设备节点为i2c-7。我们可以使用命令来查看IIC总线7上挂载的设备的地址。命令:i2cdetect -y -r 7
。这样我们就获得了从机的地址(当然可以查看BMP180手册获得)。
3. 查看BMP180手册,使用命令读/写某寄存器值。
BMP180手册中寄存器地址分布如下:
●读取 i2c-7
总线上,从机设备地址为0x77 ,寄存器地址为 0xF7上的值。
i2cget -y 7 0x77 0xF7
●写入i2c-7
总线上,从机设备地址为0x77 ,寄存器地址为 0xF4,值为0X2E。
i2cset 7 0x77 0xF4 0x2E
4. 查看BMP180手册通信流程。
我们可以发现,通信大致流程就是往寄存器里写值,然后读取寄存器的值。将获取的值通过公式转换为真实的温度/压力值。
二、IIC常用API
1. iic数据包/报
头文件:#include <linux/i2c.h>
●数据包:
在 Linux 内核中,struct i2c_msg 结构体用于描述 I2C 消息,这是在 I2C 总线上传输的数据块。它被用作 ioctl 调用的一部分,通过 I2C_RDWR 命令进行 I2C 读写操作。
struct i2c_msg {
__u16 addr; /* 从设备地址 */
__u16 flags; /* 消息标志,写:0, 读:1 */
__u8 *buf; /* 数据缓冲区。对于写操作,这里存储的是要发送的数据;对于读操作,这里存储的是接收到的数据。 */
__u16 len; /* 数据缓冲区长度 */
};
●数据报:包含多个数据包。
struct i2c_rdwr_ioctl_data {
struct i2c_msg *msgs; /* 指向 I2C 消息数组的指针 */
__u32 nmsgs; /* 消息的数量 */
};
2. ioctl函数
是一个系统调用,专门用来让程序与设备进行通信。它有点像是一个“万能”函数,通过它可以向设备发送各种控制命令或者配置设备的某些参数。具体使用看下面的内容理解。相当于将消息报传给设备。
int ioctl(int fd, unsigned long request, ...);
//int fd :设备的文件描述符。
//unsigned long request:请求,例如可读可写等。
//...(可变参数):根据 request 的不同,ioctl 可能需要一个或多个额外的参数。这些参数的类型和数量取决于具体的控制命令。
三、数据包如何被处理
当数据包传输到设备时,设备的硬件或驱动程序会根据I2C协议进行解析和处理。
-
设备地址识别: 首先,设备会检查数据包中的地址字段,以确定它是否是被设备所识别的地址。如果是,则设备将继续处理数据包;如果不是,则设备会忽略该数据包。
-
数据包解析: 设备会根据数据包的格式进行解析。对于写数据包,设备会读取数据包中的数据内容,并根据寄存器地址将这些数据写入到对应的寄存器中。对于读数据包,设备会从指定的寄存器中读取数据,并将这些数据放置在响应数据包的缓冲区中。
-
数据处理: 一旦数据被写入或读取,设备可能会执行相应的操作。这可能包括修改设备内部的状态、更新设备的寄存器值、执行特定的功能等。
四、代码编写流程
在IIC通信中,我们最主要的就是写出IIC读和IIC写的函数。无论是读还是写,我们在与设备通信时传输的第一个字节必须是要操作的寄存器的地址,因为数据的读写通常是通过向设备发送特定的寄存器地址来触发的。
1. IIC读取数据
传入打开的IIC设备文件描述符、要读取的从机设备地址、 要读取的寄存器地址、将数据读取到哪、读多少。
/*
uint8_t slave_addr :从机地址
uint8_t reg_addr :要读取的寄存器
uint8_t* buffer:读取的数据存在哪
int length:读取的长度
*/
int iic_read(int fd, uint8_t slave_addr, uint8_t reg_addr, uint8_t* buffer, int length)
{
struct i2c_msg msgs[2]; //读数据包以及写数据包
struct i2c_rdwr_ioctl_data pack; //数据报
int ret;
//第一个数据包:用于向目标设备发送要操作的寄存器地址。
msgs[0].addr = slave_addr;
msgs[0].flags = 0; // 写方向
msgs[0].buf = ®_addr;
msgs[0].len = sizeof(reg_addr);
//第二个数据包:表明是读取寄存器的内容。
msgs[1].addr = slave_addr;
msgs[1].flags = 1; // 读方向
msgs[1].buf = buffer; //从寄存器读取的内容存到buffer中。
msgs[1].len = length; //读取的长度
pack.msgs = msgs;
pack.nmsgs = 2;//
//ioctl函数将消息报 pack 发送给指定的设备。
ret = ioctl(fd, I2C_RDWR, &pack);
if (ret < 0) {
perror("ioctl I2C_RDWR failed");
return -1;
}
return 0;
}
2. IIC写入数据
传入打开的IIC设备文件描述符、要写入的从机设备地址、 要写入的寄存器地址、写什么数据、写多少。
注意:这里我们要将寄存器地址和写入的数据放到一个数据包中传输。即第一个字节为寄存器地址,后面为传输的值。具体为什么使用一个数据包而不是两个,原因不太清楚,我使用两个数据包传输时,有问题。
int iic_write(int fd, uint8_t slave_addr, uint8_t reg_addr, uint8_t* data, int length)
{
struct i2c_rdwr_ioctl_data pack;
struct i2c_msg msg;
uint8_t buffer[length + 1]; // 为了包含寄存器地址,需要额外的空间
int ret;
buffer[0] = reg_addr; // 将寄存器地址作为第一个字节
// 将要写入的数据拷贝到缓冲区中
memcpy(buffer + 1, data, length);
msg.addr = slave_addr;
msg.flags = 0; // 写方向
msg.len = length + 1; // 包含了寄存器地址
msg.buf = buffer;
pack.msgs = &msg;
pack.nmsgs = 1;
ret = ioctl(fd, I2C_RDWR, &pack);
if (ret < 0) {
perror("ioctl I2C_RDWR failed");
return -1;
}
return 0;
}
3. 读取校验参数
由于在Linux-arm下是大端序,则先读取高位,再读低位。因为每个数据占两个字节,所以要将这两个字节的数据进行位移操作来合并为一个数据。
#define Slave_Addr 0x77
void read_calibration_data(int fd, int16_t* AC1, int16_t* AC2, int16_t* AC3,
uint16_t* AC4, uint16_t* AC5, uint16_t* AC6, int16_t* B1,
int16_t* B2, int16_t* MB, int16_t* MC, int16_t* MD)
{
uint8_t buffer[22]; //每个数据占2个字节,共有11个数据。
iic_read(fd, Slave_Addr , 0xAA, buffer, 22);
*AC1 = (buffer[0] << 8) | buffer[1];
*AC2 = (buffer[2] << 8) | buffer[3];
*AC3 = (buffer[4] << 8) | buffer[5];
*AC4 = (buffer[6] << 8) | buffer[7];
*AC5 = (buffer[8] << 8) | buffer[9];
*AC6 = (buffer[10] << 8) | buffer[11];
*B1 = (buffer[12] << 8) | buffer[13];
*B2 = (buffer[14] << 8) | buffer[15];
*MB = (buffer[16] << 8) | buffer[17];
*MC = (buffer[18] << 8) | buffer[19];
*MD = (buffer[20] << 8) | buffer[21];
}
//下面内容只是为了演示如何使用而已。
int main()
{
int16_t AC1, AC2, AC3, B1, B2, MB, MC, MD;
uint16_t AC4, AC5, AC6;
// 读取校准数据
read_calibration_data(fd, &AC1, &AC2, &AC3, &AC4, &AC5, &AC6, &B1, &B2, &MB, &MC, &MD);
}
4. 读取未校准的温度值
由于在Linux-arm下是大端序,则先读取高位,再读低位。
#define Slave_Addr 0x77
#define Data_Out_MSB 0xF6
#define Data_Out_LSB 0xF7
#define Tempture_Pressure_reg 0xF4
// 启动温度测量
int main()
{
uint8_t send_data[1];
uint8_t receive_data[2];
int32_t raw_temp; //未校准的温度数据。
send_data[0] = 0x2e; //要写入的数据
if (iic_write(fd, Slave_Addr, Tempture_Pressure_reg , send_data,1) <0) { //开始测量温度
perror("iic_write error");
close(fd);
return -1;
}
usleep(4500); // 等待测量完成
// 读取未校准的温度数据
if (iic_read(fd, Slave_Addr, Data_Out_MSB , receive_data, 2) < 0) {
perror("iic_read error");
close(fd);
return -1;
}
raw_temp= receive_data[0]<<8|receive_data[1]; //未校准的温度值
}
5. 读取未校准的压力值
由于在Linux-arm下是大端序,则先读取高位,再读低位。
#define Slave_Addr 0x77
#define Data_Out_MSB 0xF6
#define Data_Out_LSB 0xF7
#define Data_Out_XLSB 0xF8
#define Tempture_Pressure_reg 0xF4
// 启动温度测量
/*
通常,"oss" 的取值范围在 0 到 3 之间,代表不同的过采样率。具体取值对应的过采样率取决于传感器型号和制造商的实现。在 BMP180 中,oss 的取值对应着以下过采样率:
oss = 0: 单次采样
oss = 1: 2 倍过采样
oss = 2: 4 倍过采样
oss = 3: 8 倍过采样
本文采用单次采样即可。
*/
int main()
{
uint8_t send_data[1];
uint8_t receive_data[3];
int32_t raw_pressure; //未校准的压力数据。
send_data[0] = 0x34; //要写入的数据
if (iic_write(fd, Slave_Addr, Tempture_Pressure_reg , send_data,1) <0) { //开始测量压力
perror("iic_write error");
close(fd);
return -1;
}
usleep(4500); // 等待测量完成
// 读取未校准的压力数据
if (iic_read(fd, Slave_Addr, Data_Out_MSB , receive_data, 3) < 0) {
perror("iic_read error");
close(fd);
return -1;
}
raw_pressure =(receive_data[0]<<16|receive_data[1]<<8|receive_data[0]) >>8; //未校准的压力值
}
6. 将未校准的测量值转为真实值
注意:代码中的右移多少位就相当于乘了2的几次方。左移相当于除。
//这里的参数很多都是校准参数。
void calculate_true_values(int32_t raw_temp, int32_t raw_pressure, int32_t* true_temp, int32_t* true_pressure, int16_t AC1, int16_t AC2, int16_t AC3, uint16_t AC4, uint16_t AC5, uint16_t AC6, int16_t B1, int16_t B2, int16_t MB, int16_t MC, int16_t MD) {
int32_t X1, X2, X3, B3, B5, B6, B7, p;
uint32_t B4;
// 温度计算,
X1 = (raw_temp - AC6) * AC5 >> 15;
X2 = (MC << 11) / (X1 + MD);
B5 = X1 + X2;
*true_temp = (B5 + 8) >> 4;
// 压力计算
B6 = B5 - 4000;
X1 = (B2 * (B6 * B6 >> 12)) >> 11;
X2 = AC2 * B6 >> 11;
X3 = X1 + X2;
B3 = (((AC1 * 4 + X3) << 1) + 2) >> 2;
X1 = AC3 * B6 >> 13;
X2 = (B1 * (B6 * B6 >> 12)) >> 16;
X3 = ((X1 + X2) + 2) >> 2;
B4 = AC4 * (uint32_t)(X3 + 32768) >> 15;
B7 = ((uint32_t)raw_pressure - B3) * (50000 >> 1);
if (B7 < 0x80000000) {
p = (B7 * 2) / B4;
} else {
p = (B7 / B4) * 2;
}
X1 = (p >> 8) * (p >> 8);
X1 = (X1 * 3038) >> 16;
X2 = (-7357 * p) >> 16;
*true_pressure = p + ((X1 + X2 + 3791) >> 4);
}
五、完整代码
iic.c
#include <stdint.h>
#include <string.h>
#include "iic.h"
int iic_init(const char *device)
{
return open(device,O_RDWR); //可读可写
}
int iic_close(int fd)
{
return close(fd);
}
/*
uint8_t slave_addr :从机地址
uint8_t reg_addr :要读取的寄存器
uint8_t* buffer:读取的数据存在哪
int length:读取的长度
*/
int iic_read(int fd, uint8_t slave_addr, uint8_t reg_addr, uint8_t* buffer, int length)
{
struct i2c_msg msgs[2]; //读数据包以及写数据包
struct i2c_rdwr_ioctl_data pack; //数据报
int ret;
//第一个数据包:用于向目标设备发送要操作的寄存器地址。
msgs[0].addr = slave_addr;
msgs[0].flags = 0; // 写方向
msgs[0].buf = ®_addr;
msgs[0].len = sizeof(reg_addr);
//第二个数据包:表明是读取寄存器的内容。
msgs[1].addr = slave_addr;
msgs[1].flags = 1; // 读方向
msgs[1].buf = buffer; //从寄存器读取的内容存到buffer中。
msgs[1].len = length; //读取的长度
pack.msgs = msgs;
pack.nmsgs = 2;//
//ioctl函数将消息报 pack 发送给指定的设备。
ret = ioctl(fd, I2C_RDWR, &pack);
if (ret < 0) {
perror("ioctl I2C_RDWR failed");
return -1;
}
return 0;
}
//IIC写数据
int iic_write(int fd, uint8_t slave_addr, uint8_t reg_addr, uint8_t* data, int length)
{
struct i2c_rdwr_ioctl_data pack;
struct i2c_msg msg;
uint8_t buffer[length + 1]; // 为了包含寄存器地址,需要额外的空间
int ret;
buffer[0] = reg_addr; // 将寄存器地址作为第一个字节
// 将要写入的数据拷贝到缓冲区中
memcpy(buffer + 1, data, length);
msg.addr = slave_addr;
msg.flags = 0; // 写方向
msg.len = length + 1; // 包含了寄存器地址
msg.buf = buffer;
pack.msgs = &msg;
pack.nmsgs = 1;
ret = ioctl(fd, I2C_RDWR, &pack);
if (ret < 0) {
perror("ioctl I2C_RDWR failed");
return -1;
}
return 0;
}
//读取校准参数
void read_calibration_data(int fd, int16_t* AC1, int16_t* AC2, int16_t* AC3, uint16_t* AC4, uint16_t* AC5, uint16_t* AC6, int16_t* B1, int16_t* B2, int16_t* MB, int16_t* MC, int16_t* MD) {
uint8_t buffer[22];
iic_read(fd, Slave_Addr, 0xAA, buffer, 22);
*AC1 = (buffer[0] << 8) | buffer[1];
*AC2 = (buffer[2] << 8) | buffer[3];
*AC3 = (buffer[4] << 8) | buffer[5];
*AC4 = (buffer[6] << 8) | buffer[7];
*AC5 = (buffer[8] << 8) | buffer[9];
*AC6 = (buffer[10] << 8) | buffer[11];
*B1 = (buffer[12] << 8) | buffer[13];
*B2 = (buffer[14] << 8) | buffer[15];
*MB = (buffer[16] << 8) | buffer[17];
*MC = (buffer[18] << 8) | buffer[19];
*MD = (buffer[20] << 8) | buffer[21];
}
//计算真实值
void calculate_true_values(int32_t raw_temp, int32_t raw_pressure, int32_t* true_temp, int32_t* true_pressure, int16_t AC1, int16_t AC2, int16_t AC3, uint16_t AC4, uint16_t AC5, uint16_t AC6, int16_t B1, int16_t B2, int16_t MB, int16_t MC, int16_t MD) {
int32_t X1, X2, X3, B3, B5, B6, B7, p;
uint32_t B4;
// 温度计算
X1 = (raw_temp - AC6) * AC5 >> 15;
X2 = (MC << 11) / (X1 + MD);
B5 = X1 + X2;
*true_temp = (B5 + 8) >> 4;
// 压力计算
B6 = B5 - 4000;
X1 = (B2 * (B6 * B6 >> 12)) >> 11;
X2 = AC2 * B6 >> 11;
X3 = X1 + X2;
B3 = (((AC1 * 4 + X3) << 1) + 2) >> 2;
X1 = AC3 * B6 >> 13;
X2 = (B1 * (B6 * B6 >> 12)) >> 16;
X3 = ((X1 + X2) + 2) >> 2;
B4 = AC4 * (uint32_t)(X3 + 32768) >> 15;
B7 = ((uint32_t)raw_pressure - B3) * (50000 >> 1);
if (B7 < 0x80000000) {
p = (B7 * 2) / B4;
} else {
p = (B7 / B4) * 2;
}
X1 = (p >> 8) * (p >> 8);
X1 = (X1 * 3038) >> 16;
X2 = (-7357 * p) >> 16;
*true_pressure = p + ((X1 + X2 + 3791) >> 4);
}
iic.h
#ifndef __IIC_H
#define __IIC_H
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#define Slave_Addr 0x77
#define Data_Out_MSB 0xF6
#define Data_Out_LSB 0xF7
#define Data_Out_XLSB 0xF8
#define Tempture_Pressure_reg 0xF4
int iic_init(const char *device);
int iic_close(int fd);
int iic_read(int fd, uint8_t slave_addr, uint8_t reg_addr, uint8_t* buffer, int length);
int iic_write(int fd, uint8_t slave_addr, uint8_t reg_addr, uint8_t* data, int length);
void read_calibration_data(int fd, int16_t* AC1, int16_t* AC2, int16_t* AC3, uint16_t* AC4, uint16_t* AC5, uint16_t* AC6, int16_t* B1, int16_t* B2, int16_t* MB, int16_t* MC, int16_t* MD);
void calculate_true_values(int32_t raw_temp, int32_t raw_pressure, int32_t* true_temp, int32_t* true_pressure, int16_t AC1, int16_t AC2, int16_t AC3, uint16_t AC4, uint16_t AC5, uint16_t AC6, int16_t B1, int16_t B2, int16_t MB, int16_t MC, int16_t MD) ;
#endif
main.c
#include <stdio.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdint.h>
#include <linux/i2c.h>
#include <string.h>
#include "iic.h"
int main() {
int fd;
int32_t raw_temp, raw_pressure, true_temp, true_pressure;
int16_t AC1, AC2, AC3, B1, B2, MB, MC, MD;
uint16_t AC4, AC5, AC6;
uint8_t send_data[1];
uint8_t receive_temp[2];
uint8_t receive_pressure[3];
// 打开I2C设备
fd = iic_init("/dev/i2c-7");
if (fd < 0) {
perror("iic_init error");
return -1;
}
// 读取校准数据
read_calibration_data(fd, &AC1, &AC2, &AC3, &AC4, &AC5, &AC6, &B1, &B2, &MB, &MC, &MD);
//开始测量温度
send_data[0] = 0x2e; //要写入的数据
if (iic_write(fd, Slave_Addr, Tempture_Pressure_reg , send_data, 1) <0) {
perror("iic_write error");
close(fd);
return -1;
}
usleep(4500); // 等待测量完成
// 读取未校准的温度数据
if (iic_read(fd, Slave_Addr, Data_Out_MSB , receive_temp, 2) < 0) {
perror("iic_read error");
close(fd);
return -1;
}
raw_temp= receive_temp[0]<<8|receive_temp[1]; //未校准的温度值
send_data[0] = 0x34; //要写入的数据
if (iic_write(fd, Slave_Addr, Tempture_Pressure_reg , send_data, 1) <0) { //开始测量压力
perror("iic_write error");
close(fd);
return -1;
}
usleep(4500); // 等待测量完成
// 读取未校准的压力数据
if (iic_read(fd, Slave_Addr, Data_Out_MSB , receive_pressure, 3) < 0) {
perror("iic_read error");
close(fd);
return -1;
}
raw_pressure =(receive_pressure[0]<<16|receive_pressure[1]<<8|receive_pressure[0]) >>8; //未校准的压力值
// 计算实际温度和压力
calculate_true_values(raw_temp, raw_pressure, &true_temp, &true_pressure, AC1, AC2, AC3, AC4, AC5, AC6, B1, B2, MB, MC, MD);
// 输出实际温度和压力
printf("True Temperature: %.2f C\n", true_temp / 10.0);
printf("True Pressure: %.2f hPa\n", true_pressure / 100.0);
// 关闭I2C设备
close(fd);
return 0;
}