本文目录
IIC(Inter-Integrated Circuit)是一种常用的串行通信协议,通常用于连接微控制器和各种外部设备(例如传感器、存储器、显示器等)。
●IIC、UART、SPI的比较:
通信协议 | UART | IIC | SPI |
---|---|---|---|
通信特征 | 异步串行全双工 | 同步串行半双工 | 同步串行全双工 |
接口 | TX、RX | SCL、SDA | MOSI、MISO、SCL、CS/NSS |
速度 | 多种波特率 | 100Khz、400Khz、3.4Mhz | |
数据帧格式 | 起始位+数据位+校验位+停止位 | 起始条件+位传输+应答+停止条件 | 四种模式:MODE0~MODE3 |
主从设备通信 | 没有主从 | 有主从 | 有主从 |
总线结构 | 一对一 | 一对多 | 一对多 |
一、知识点
1. 12C总线两线制包括: 串行数据SDA (Serial Data)、串行时钟SCL(Serial Clock)。
2. I2C总线上有主机和从机之分,可以有多个主机和多个从机。IIC总线上的电容不超过400pf,只要不超过这个电容量,任意挂多少个从机都可以。但是在通信的时刻,只能有一个作为主机,其他的都为从机。
3. 器件发送数据到总线上,则定义为发送器,器件接收数据则定义为接收器。主器件和从器件都可以工作于接收和发送状态(都可以作为发送器和接收器)。从机永远不会主动给主机发送数据。
4. 时钟线SCL必须由主机控制!主机控制时钟线SCL产生跳变沿, 谁控制SCL时钟线,谁就是主机!
5. 每个IIC设备都有一个唯一的地址。在通信开始之前,你需要知道要与主机进行通信的设备的地址。每个设备都有一个唯一的7位地址(4位固定地址+3位可编程地址),用于在总线上区分不同的设备。注:有些从设备的7位地址商家已经给固定好了,所以就不需要自行设计,查看商家给的手册就可以知道该地址。
如:AT24C02芯片中的A0、A1、A2就是可编程地址位,可以自行设计该位。下图中A0、A1、A2接地,则都为0。
6. 数据的有效性: 时钟线产生下降沿,发送方准备/发送数据。时钟线产生上升沿,接收方采集数据。
SDA数据线在 SCL 的每个时钟周期传输一位数据。传输时:
(1)SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据 “1”,为低电平时表示数据“ 0”。
(2)当 SCL为低电平时,SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。
- 主机与从机通信
无论主机是向从机写数据还是读取数据,主机发送给从机的第一个字节必须是从机设备地址和写方向位。主机跟哪个从机通讯,把从机的地址发出去。发送数据是8个bit,这8个bit位中前7个bit位是从机的地址,最后1个bit位是用来表示读或者写的。1表示读,0表示写;这个过程相当于主机往SDA上发了8个bit的数据。
主机发送的第一个字节主要是为了寻找与其通信的从机。
●如果你已知从机的七位地址(0111 100)。
要进行写操作:Write_IIC_Byte (0x78); // (0111 1000)
要进行读操作:Write_IIC_Byte (0x79); // (0111 1001)
二、硬件IIC内部
- 为了搞清楚IIC的内部结构图,我们需要首先明白一个知识点:器件芯片是如何输出高低电平的?
答:芯片内部有两个mos管,一个接高电平,一个接低电平。当上面的mos管导通时,输出高电平。下面的mos管导通时输出低电平。
- IIC内部有多个从机,如果一个从机发出高电平,一个从机发出低电平。那么这个数据线上到底是高电平还是低电平呢?
答:如果一个从机发出高电平,一个从机发出低电平,那么这两个从机的芯片内部会被导通,那么必定有一个元器件会被烧毁。所以IIC通信对从机设备的IO进行了阉割,那就是去掉了上面的mos管,这样就不会造成短路烧毁元器件了,并且从机只能输出低电平。这样数据线上肯定为低电平了。但是这样会有另一个问题:即从机只能输出低电平,如何输出高电平呢?
- 如何解决从机只能输出低电平的问题呢?
答:即在IIC总线上加上上拉电阻。这样当总线空闲时,默认为高电平,从机设备想输出低电平,则只需要把下面的mos管打开,则总线变为了低电平。如果想输出高电平,则关闭mos管即可。可以使用GPIO的开漏输出功能,来拉低端口电平。
三、软件模拟IIC基础
当我们使用端口来模拟IIC时序从而控制从机设备时,我们可以选择任意两个IO口作为SCL和SDA端,只要这两个IO口可以正常输入输出即可!并不是固定的IIC端口。
1. 端口模式选择
我认为这部分是一个比较重要的地方!我在查看其他博客时发现模式的选择多种多样。主要为以下几种。
(1)SDA、SCL:都为开漏输出模式。
这种模式适用于开发板设计的电路原理图中,一条IIC总线上挂载了多个从机元器件,为了防止烧毁器件,所以设置为开漏输出模式。至于为什么端口设置了开漏输出模式还是可以读取值,请查看这篇文章:端口模式问题详情。
(2)SCL:推挽输出模式,SDA:主机读–输入,主机写–输出
这种模式适用于一个IIC总线上只有一个从机设备,不需要担心被烧毁的问题。
这里我们选择第二种模式进行编写IIC时序。因为我们只是任意用两个IO口作为SCL和SDA来进行连接设备通信,且只能连接一个设备。
//使用寄存器来配置模式
#define SDA_IN() {GPIOA->CRH&=0XFFF0FFFF; GPIOA->CRH|=8<<16;} //输入模式
#define SDA_OUT() {GPIOA->CRH&=0XFFF0FFFF; GPIOA->CRH|=3<<16;} //输出模式
#define IIC_SCL PAout(11) //SCL
#define IIC_SDA PAout(12) //SDA
#define READ_SDA PAin(12) //读取SDA值
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
2. 选择目标设备地址(制造商预先分配,查看手册获取)
每个I2C设备都有一个唯一的地址。在通信开始之前,你需要知道你要与之通信的设备的地址。每个设备都有一个唯一的7位地址
,用于在总线上区分不同的设备。
3. IIC起始信号
主机发送一个启动信号来开始I2C通信(唤醒所有的从机设备)。起始信号的生成标志着I2C通信的开始,接着就可以发送目标设备的地址和读写位来开始通信。
步骤:初始SDA、SCL线都为高电平,维持一小段时间。然后SCL为高电平时,SDA由高变为低电平(起始信号),再维持一小段时间。然后SCL再变为低电平,这就是启动信号的完整步骤。
int IIC_Start(void)
{
SDA_OUT(); //SDA配置为输出模式
IIC_SDA=1;
IIC_SCL=1;
delay_us(1);
IIC_SDA=0; //START:when CLK is high,DATA change form high to low
delay_us(1);
IIC_SCL=0; //发送方准备数据
return 1;
}
4. IIC停止信号
发送停止信号以结束I2C通信。
步骤:初始都先为低电平,稳定一段时间后SCL先拉高,SDA再拉高,再稳定一小段时间。这就是IIC的停止信号。即SCL为高电平时,SDA线由低电平变为高电平。
void IIC_Stop(void)
{
SDA_OUT(); // 将SDA配置为输出模式
IIC_SCL = 0; // 将SCL线拉低,准备结束通信
IIC_SDA = 0; // 将SDA线拉低,准备停止条件
delay_us(1); // 延迟1微秒,确保SDA和SCL稳定
IIC_SCL = 1; // 将SCL线拉高,准备停止条件
IIC_SDA = 1; // 在SCL为高电平时,将SDA线拉高,形成停止条件
delay_us(1); // 延迟1微秒,确保停止条件稳定
}
5. 主机发送 应答和非应答信号
void IIC_Ack(void) //应答信号
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(1);
IIC_SCL=1;
delay_us(1);
IIC_SCL=0;
}
void IIC_NAck(void) //非应答信号
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(1);
IIC_SCL=1;
delay_us(1);
IIC_SCL=0;
}
6. 主机 等待从机的应答信号
从机的应答信号不需要我们来编写,因为从机接收到数据后会自己进行应答,我们只需要等待接收从机的应答即可。
从上图中可以看出,在保持SCL高电平的状态下,通过读取SDA线的电平状态来判断从机是否应答,由于SDA默认是为高电平(即非应答),所以当从机应答时会操作SDA线,将SDA线拉低,而不动则视为非应答。
int IIC_Wait_Ack(void)
{
u8 ucErrTime=0; //超时时间
SDA_IN();
IIC_SDA=1; //默认为非应答
delay_us(1);
IIC_SCL=1;
delay_us(1);
while(READ_SDA) //读取SDA线
{
ucErrTime++;
if(ucErrTime>50)
{
IIC_Stop();
return 0;
}
delay_us(1);
}
IIC_SCL=0;
return 1;
}
7. 主机向从机某个寄存器写数据
①时钟线SCL产生下降沿,发送方准备/发送数据。时钟线SCL产生上升沿,接收方采集数据。
②每次8bit的数据传输完成,都要有个应答信号,谁接收数据,谁来应答。
通信流程如下:
void IIC_Send_Byte(u8 txd)
{
u8 i;
SDA_OUT();
IIC_SCL=0; //发送方准备发送数据
for(i=0; i<8; i++)
{
IIC_SDA=(txd &0x80 )>>7; //SDA数据线发送数据,从数据的最高位开始发送。
txd<<=1;
delay_us(1);
IIC_SCL=1; //接受方采集数据
delay_us(1);
IIC_SCL=0; //发送方发送数据
delay_us(1);
}
}
//写数据
int i2cWrite(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *data)
{
int i;
if (!IIC_Start()) return 1;
IIC_Send_Byte(addr << 1 ); //将从机地址左移一位,包含上了读写位。写:0,读:1
if (!IIC_Wait_Ack()) {
IIC_Stop();
return 1;
}
IIC_Send_Byte(reg);
IIC_Wait_Ack();
for (i = 0; i < len; i++) {
IIC_Send_Byte(data[i]);
if (!IIC_Wait_Ack()) {
IIC_Stop();
return 0;
}
}
IIC_Stop();
return 0;
}
8. 主机读取从机某个寄存器的数据
①时钟线SCL产生下降沿,发送方准备/发送数据。时钟线SCL产生上升沿,接收方采集数据。
②每次8bit的数据传输完成,都要有个应答信号,谁接收数据,谁来应答。
通信流程如下:
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i, receive = 0;
SDA_IN(); // SDA配置为输入模式
for (i = 0; i < 8; i++)
{
IIC_SCL = 0;
delay_us(2);
IIC_SCL = 1;
receive <<= 1;
if (READ_SDA) receive++;
delay_us(2);
}
if (ack)
IIC_Ack(); // 发送ACK信号
else
IIC_NAck(); // 发送NACK信号
return receive;
}
//读取从机地址,寄存器上的数据。
int i2cRead(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *buf)
{
if (!IIC_Start())
return 1;
IIC_Send_Byte(addr << 1); //将从机地址和写方向
if (!IIC_Wait_Ack()) {
IIC_Stop();
return 1;
}
IIC_Send_Byte(reg);
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte((addr << 1) +1);
IIC_Wait_Ack();
while (len) {
if (len == 1)
*buf = IIC_Read_Byte(0); //最后一个数据发送非应答信号。
else
*buf = IIC_Read_Byte(1);
buf++;
len--;
}
IIC_Stop();
return 0;
}
四、通用IIC文件
如需要更改IIC引脚,请查看文章附录部分:寄存器下IIC引脚如何更换。
iic.c
#include "iic.h"
#include "sys.h"
#include "delay.h"
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
//开始信号
int IIC_Start(void)
{
SDA_OUT();
IIC_SDA=1;
if(!READ_SDA)return 0;
IIC_SCL=1;
delay_us(1);
IIC_SDA=0;
if(READ_SDA)return 0;
delay_us(1);
IIC_SCL=0;
return 1;
}
//停止信号
void IIC_Stop(void)
{
SDA_OUT();
IIC_SCL=0;
IIC_SDA=0;
delay_us(1);
IIC_SCL=1;
IIC_SDA=1;
delay_us(1);
}
//主机等待应答
int IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN();
IIC_SDA=1;
delay_us(1);
IIC_SCL=1;
delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>50)
{
IIC_Stop();
return 0;
}
delay_us(1);
}
IIC_SCL=0;
return 1;
}
//应答信号
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(1);
IIC_SCL=1;
delay_us(1);
IIC_SCL=0;
}
//非应答信号
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(1);
IIC_SCL=1;
delay_us(1);
IIC_SCL=0;
}
//IIC 写
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7;
txd<<=1;
delay_us(1);
IIC_SCL=1;
delay_us(1);
IIC_SCL=0;
delay_us(1);
}
}
int i2cWrite(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *data)
{
int i;
if (!IIC_Start())
return 1;
IIC_Send_Byte(addr << 1 );
if (!IIC_Wait_Ack()) {
IIC_Stop();
return 1;
}
IIC_Send_Byte(reg);
IIC_Wait_Ack();
for (i = 0; i < len; i++) {
IIC_Send_Byte(data[i]);
if (!IIC_Wait_Ack()) {
IIC_Stop();
return 0;
}
}
IIC_Stop();
return 0;
}
// IIC 读
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA?????????
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(2);
}
if (ack)
IIC_Ack();
else
IIC_NAck();
return receive;
}
int i2cRead(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *buf)
{
if (!IIC_Start())
return 1;
IIC_Send_Byte(addr << 1);
if (!IIC_Wait_Ack()) {
IIC_Stop();
return 1;
}
IIC_Send_Byte(reg);
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte((addr << 1)+1);
IIC_Wait_Ack();
while (len) {
if (len == 1)
*buf = IIC_Read_Byte(0);
else
*buf = IIC_Read_Byte(1);
buf++;
len--;
}
IIC_Stop();
return 0;
}
iic.h
#ifndef __IOI2C_H
#define __IOI2C_H
#include "stm32f10x.h"
#define SDA_IN() {GPIOA->CRH&=0XFFF0FFFF;GPIOA->CRH|=8<<16;}
#define SDA_OUT() {GPIOA->CRH&=0XFFF0FFFF;GPIOA->CRH|=3<<16;}
#define IIC_SCL PAout(11) //SCL
#define IIC_SDA PAout(12) //SDA
#define READ_SDA PAin(12) //????SDA
void IIC_Init(void);
int IIC_Start(void);
void IIC_Stop(void);
int IIC_Wait_Ack(void);
void IIC_Ack(void);
void IIC_NAck(void);
void IIC_Send_Byte(u8 txd);
int i2cWrite(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *data);
u8 IIC_Read_Byte(unsigned char ack);
int i2cRead(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *buf);
#endif
五、实验(获取BMP180温度/气压)
从机设备地址:0x77。具体操作流程请查看文章:BMO180通信软件编写流程。
bmp180.c
#include "bmp180.h"
#include "iic.h"
#include "delay.h"
int32_t b5; // 在全局范围内声明b5
extern BMP180_Calibration bmp180_calibration;
//读取校准参数
int BMP180_ReadCalibrationData(BMP180_Calibration *calibration) {
uint8_t buffer[22];
if (i2cRead(0x77, BMP180_CAL_AC1, 22, buffer)) {
return 1;
}
calibration->ac1 = (buffer[0] << 8) | buffer[1];
calibration->ac2 = (buffer[2] << 8) | buffer[3];
calibration->ac3 = (buffer[4] << 8) | buffer[5];
calibration->ac4 = (buffer[6] << 8) | buffer[7];
calibration->ac5 = (buffer[8] << 8) | buffer[9];
calibration->ac6 = (buffer[10] << 8) | buffer[11];
calibration->b1 = (buffer[12] << 8) | buffer[13];
calibration->b2 = (buffer[14] << 8) | buffer[15];
calibration->mb = (buffer[16] << 8) | buffer[17];
calibration->mc = (buffer[18] << 8) | buffer[19];
calibration->md = (buffer[20] << 8) | buffer[21];
return 0;
}
读取未校准的温度值
int BMP180_ReadTemperature(int32_t *temperature) {
int32_t ut;
uint8_t buffer[2];
uint8_t cmd = BMP180_READ_TEMP_CMD; // 使用中间变量存储命令
if (i2cWrite(0x77, BMP180_CONTROL, 1, &cmd)) {
return 1;
}
delay_ms(5);
if (i2cRead(0x77, BMP180_TEMP_DATA, 2, buffer)) {
return 1;
}
ut = (buffer[0] << 8) | buffer[1];
// 根据校准数据计算真实温度
int32_t x1 = ((ut - bmp180_calibration.ac6) * bmp180_calibration.ac5) >> 15;
int32_t x2 = (bmp180_calibration.mc << 11) / (x1 + bmp180_calibration.md);
b5 = x1 + x2; // 计算b5并存储在全局变量中
*temperature = (b5 + 8) >> 4;
return 0;
}
//读取未校准的气压值
int BMP180_ReadPressure(int32_t *pressure, uint8_t oss) {
int32_t up;
uint8_t buffer[3];
uint8_t cmd = BMP180_READ_PRESSURE_CMD + (oss << 6); // 使用中间变量存储命令
if (i2cWrite(0x77, BMP180_CONTROL, 1, &cmd)) {
return 1;
}
delay_ms(2 + (3 << oss));
if (i2cRead(0x77, BMP180_PRESSURE_DATA, 3, buffer)) {
return 1;
}
up = ((buffer[0] << 16) | (buffer[1] << 8) | buffer[2]) >> (8 - oss);
// 根据校准数据计算真实压力
int32_t b6 = b5 - 4000; // 使用之前计算的b5
int32_t x1 = (bmp180_calibration.b2 * (b6 * b6 >> 12)) >> 11;
int32_t x2 = (bmp180_calibration.ac2 * b6) >> 11;
int32_t x3 = x1 + x2;
int32_t b3 = (((bmp180_calibration.ac1 * 4 + x3) << oss) + 2) >> 2;
x1 = (bmp180_calibration.ac3 * b6) >> 13;
x2 = (bmp180_calibration.b1 * (b6 * b6 >> 12)) >> 16;
x3 = ((x1 + x2) + 2) >> 2;
uint32_t b4 = (bmp180_calibration.ac4 * (uint32_t)(x3 + 32768)) >> 15;
uint32_t b7 = ((uint32_t)up - b3) * (50000 >> oss);
if (b7 < 0x80000000) {
*pressure = (b7 * 2) / b4;
} else {
*pressure = (b7 / b4) * 2;
}
x1 = (*pressure >> 8) * (*pressure >> 8);
x1 = (x1 * 3038) >> 16;
x2 = (-7357 * *pressure) >> 16;
*pressure += (x1 + x2 + 3791) >> 4;
return 0;
}
bmp180.h
#ifndef __BMP180_H
#define __BMP180_H
#include "stm32f10x.h"
// BMP180寄存器地址
#define BMP180_CAL_AC1 0xAA
#define BMP180_CAL_AC2 0xAC
#define BMP180_CAL_AC3 0xAE
#define BMP180_CAL_AC4 0xB0
#define BMP180_CAL_AC5 0xB2
#define BMP180_CAL_AC6 0xB4
#define BMP180_CAL_B1 0xB6
#define BMP180_CAL_B2 0xB8
#define BMP180_CAL_MB 0xBA
#define BMP180_CAL_MC 0xBC
#define BMP180_CAL_MD 0xBE
#define BMP180_CONTROL 0xF4
#define BMP180_TEMP_DATA 0xF6
#define BMP180_PRESSURE_DATA 0xF6
#define BMP180_READ_TEMP_CMD 0x2E
#define BMP180_READ_PRESSURE_CMD 0x34
// 校准系数结构体
typedef struct {
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;
} BMP180_Calibration;
// 函数声明
void BMP180_Init(void);
int BMP180_ReadCalibrationData(BMP180_Calibration *calibration);
int BMP180_ReadTemperature(int32_t *temperature);
int BMP180_ReadPressure(int32_t *pressure, uint8_t oss);
#endif
main.c
#include "stm32f10x.h"
#include "iic.h"
#include "bmp180.h"
#include "stdio.h"
int32_t temperature, pressure;
BMP180_Calibration bmp180_calibration;
int main(void)
{
delay_init(); //必须要有这个
IIC_Init();
BMP180_ReadCalibrationData(&bmp180_calibration);
while(1)
{
BMP180_ReadTemperature(&temperature); // 读取温度
BMP180_ReadPressure(&pressure, 0); // 读取压力
temperature=temperature/10;
pressure=pressure/100;
delay(1000);
}
}