I2C也叫IIC,集成线路总线。由菲利普设计,主要用来连接整体线路,是一种多向控制总线。
I2C传输速率:100kbps~1Mbps
标准速率:100kbps,400kbps,1Mbps。
I2C串行总线一般有两根信号线,一个双向数据线SDA,另一根时钟线SCL。所有接到 I2C 总线的设备,SDA都连接到总线SDA上,SCL都连接到SCL上。
总线运行由主机(发出启动信号,发出时钟信号的设备,通常是微处理器)控制,被主机寻访的设备称为从机。
每个连接到I2C总线的从机都有一个唯一的地址(7bit地址),主机为寻找通信目标,所以有一个寻址的动作。
可以主机发送数据,也可以从机发送数据。凡是发数据到主机的叫发送器,接收主机数据的叫接收器。
类似手机连接到路由器,每个手机都有独立的IP地址,路由器要与指定手机通信,根据IP地址来分辨,手机和路由器可以互相收发数据。
从机地址:7位地址加一位读写控制位(1读,0写)。主要由芯片手册(地址输入引脚)、硬件设计决定。7位地址:由固定地址+硬件设计(查看开发板原理图,A0A1A2对应的引脚电平)
查看时序图,看图编程。
主机:起始信号触发通信开始 从机:
寻设备地址
等应答 应答
发送数据存储地址
等应答 应答
发送数据
等应答 应答
继续起始信号/停止信号
代码示例(PB8,PB9)
/*以下代码参考前面STM32的文章,主要实现延时和串口通信*/
#include "stm32f4xx.h"
#include "sys.h"
#include <string.h>
#include <stdio.h>
static GPIO_InitTypeDef GPIO_InitStructure; //GPIO初始化的结构体
static NVIC_InitTypeDef NVIC_InitStructure; //中断优先级初始化的结构体
static USART_InitTypeDef USART_InitStructure; //串口初始化的结构体
void delay_us(uint32_t n)
{
while(n--)
{
SysTick->CTRL =0; //关闭系统定时器
SysTick->LOAD = (21); //设置定时时间为1us
SysTick->VAL = 0; //清空标志位
SysTick->CTRL = 1; //使能系统定时器工作,开始计数,同时使用的时钟源为系统时钟(21MHz)
while((SysTick->CTRL & 0x00010000)==0); //等待系统定时器计数完毕
SysTick->CTRL =0; //关闭系统定时器
}
}
void delay_ms(uint32_t n)
{
while(n--)
{
SysTick->CTRL =0; //关闭系统定时器
SysTick->LOAD = (21000); //设置定时时间为1ms
SysTick->VAL = 0; //清空标志位
SysTick->CTRL = 1; //使能系统定时器工作,开始计数,同时使用的时钟源为系统时钟(21MHz)
while((SysTick->CTRL & 0x00010000)==0); //等待系统定时器计数完毕
SysTick->CTRL =0; //关闭系统定时器
}
}
int fputc(int ch,FILE *f)
{
USART_SendData(USART1,ch);
//检测当前的串口是否发送数据完成
while(RESET==USART_GetFlagStatus(USART1,USART_FLAG_TXE));
return ch;
}
void usart1_init(uint32_t baud)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
//使能串口1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
/* 多功能引脚的配置,将PA9和PA10连接到串口1 */
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9|GPIO_Pin_10; //第9 10号引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //多功能模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出,增强驱动能力,引脚的输出电流更大
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //引脚的速度最大为100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //没有使用内部上拉电阻
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = baud; //波特率的配置
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //8位的数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1; //1个停止位
USART_InitStructure.USART_Parity = USART_Parity_No; //无校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //支持数据的发送与接收
USART_Init(USART1, &USART_InitStructure);
/* 使能串口1中断并且设置其优先级 */
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 使能串口1工作 */
USART_Cmd(USART1, ENABLE);
//设置串口接收到数据后,就触发中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}
/*****************开始I2C的配置**************/
//一、引脚配置,宏定义引脚名,方便使用
#define SCL_W PBout(8); //宏定义时钟线
#define SDA_W PBout(9); //宏定义数据线,写权限
#define SDA_R PBin(9); //宏定义数据线,读权限
void at24c02_Init(void)
{
//1、使能时钟,相当于打开对应端口的开关,让其开始工作
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
//2、给GPIO初始化结构体内成员赋值,并使配置生效
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9; //第8 9号引脚,相同端口引脚可以使用‘|’一起初始化
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //输出模式,另外有输入模式,多功能模式,模拟模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出,增强驱动能力,引脚的输出电流更大,另外有开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //引脚的速度最大为100MHz,根据实际调节
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //没有使用内部上拉电阻
GPIO_Init(GPIOB, &GPIO_InitStructure); //使初始化好的结构体生效
//3、设定时钟线和数据线的初始状态
SCL_W = 1; //根据芯片时序图,设定时钟线初始状态为高电平
SDA_W = 1; //根据芯片时序图,设定数据线初始状态为高电平
}
/*引脚输入输出模式的切换,需要重新配置引脚。SDA引脚发送数据时为输出模式,等待应答信号时为输入模式,时钟引脚不变*/
void i2c_sda_mode(GPIOMode_TypeDef mode)
{
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //第 9号引脚
GPIO_InitStructure.GPIO_Mode = mode; //输出/输入模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出,增强驱动能力,引脚的输出电流更大
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //引脚的速度最大为100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //没有使用内部上拉电阻
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**************以下为数据通信功能***********/
/*起始信号,根据时序图编写*/
void i2c_start(void)
{
//保证SDA引脚为输出模式
i2c_sda_mode(GPIO_Mode_OUT);
SCL_W=1;
SDA_W=1;
//查看芯片手册,得到芯片的最大运行频率
//延时5us(一般100KHz通信,10us就1Hz,一个时钟周期由一半高电平,一半低电平组成,所以高电平为5us)
delay_us(5);
SDA_W=0;
//延时5us
delay_us(5);
//拉低时钟线,占用时钟线,告诉其他从机已经被占用了(可以查看http://blog.sina.com.cn/s/blog_6582c5f30102v9ic.html)
SCL_W=0;
//延时5us
delay_us(5);
}
/*结束信号*/
void i2c_stop(void)
{
//保证SDA引脚为输出模式
i2c_sda_mode(GPIO_Mode_OUT);
SCL_W=0;
SDA_W=0;
//延时5us
delay_us(5);
SCL_W=1;
//延时5us
delay_us(5);
SDA_W=1;
//延时5us
delay_us(5);
}
/*发送1个字节,一个字节有8位的数据*/
void i2c_send_byte(uint8_t byte)
{
uint32_t i=0;
//保证SDA引脚为输出模式
i2c_sda_mode(GPIO_Mode_OUT);
SCL_W=0;
SDA_W=0;
//延时5us
delay_us(5);
for(i=0; i<8; i++)
{
//MSB,最高有效位优先数据发送
if(byte & (1<<(7-i)))
SDA_W=1;
else
SDA_W=0;
//延时5us
delay_us(5);
//设置时钟线为高电平,这个时候,从机会读取该电平的状态
SCL_W=1;
delay_us(5);
//设置时钟线为低电平,这个时候,从机不需要读取该电平的状态,主机要更改该引脚电平
SCL_W=0;
delay_us(5);
}
}
/*等待从机应答*/
uint8_t i2c_wait_ack(void)
{
uint8_t ack=0;
//保证SDA引脚为输入模式
i2c_sda_mode(GPIO_Mode_IN);
SCL_W=1;
//延时5us
delay_us(5);
//判断SDA引脚的电平
if(SDA_R)
ack=1;
else
ack=0;
SCL_W=0;
//延时5us
delay_us(5);
return ack;
}
/*写数据*/
int32_t at24c02_write(uint8_t addr,uint8_t *pbuf,uint32_t len)
{
uint8_t ack=0;
//发送起始信号
i2c_start();
//发送设备地址,进行寻址,前7位为A,最后一位为写访问操作
i2c_send_byte(0xA0);
//等待从机应答
ack=i2c_wait_ack();
if(ack)
{
printf("i2c ack device address fail\r\n");
return -1;
}
printf("i2c ack device address ok\r\n");
//发送数据存储的地址,写访问操作
i2c_send_byte(addr);
//等待从机应答
ack=i2c_wait_ack();
if(ack)
{
printf("i2c ack word address fail\r\n");
return -2;
}
printf("i2c ack word address ok\r\n");
//发送数据内容
while(len--)
{
//发送数据内容,写访问操作
i2c_send_byte(*pbuf++);
//等待从机应答
ack=i2c_wait_ack();
if(ack)
{
printf("i2c ack data fail\r\n");
return -3;
}
}
printf("i2c write data ok\r\n");
//发送停止信号,结束这个I2C的通信
i2c_stop();
return 0;
}
/*读数据*/
int32_t at24c02_read(uint8_t addr,uint8_t *pbuf,uint32_t len) //设备地址(读写权限),内容,长度
{
uint8_t ack=0;
//发送起始信号
i2c_start();
//发送设备地址,进行寻址,写访问操作
i2c_send_byte(0xA0);
//等待从机应答
ack=i2c_wait_ack();
if(ack)
{
printf("i2c ack device address fail \r\n");
return -1;
}
printf("i2c ack device address 1 ok \r\n");
//发送数据存储的地址,写访问操作
i2c_send_byte(addr);
//等待从机应答
ack=i2c_wait_ack();
if(ack)
{
printf("i2c ack word address fail\r\n");
return -2;
}
printf("i2c ack word address ok\r\n");
//发送起始信号
i2c_start();
//发送设备地址,进行寻址,读访问操作
i2c_send_byte(0xA1);
//等待从机应答
ack=i2c_wait_ack();
if(ack)
{
printf("i2c ack device address fail \r\n");
return -3;
}
printf("i2c ack device address 1 ok \r\n");
//读取数据内容
len=len-1;
while(len--)
{
//接收数据内容
*pbuf++=i2c_recv_byte();
//向从机发送应答信号
i2c_ack(0);
}
*pbuf = i2c_recv_byte();
//向从机发送无应答信号
i2c_ack(1);
//printf("i2c read data ok\r\n");
//发送停止信号,结束这个I2C的通信
i2c_stop();
return 0;
}
/*主函数,逻辑实现*/
int main(void)
{
uint8_t buf[32]={0};
uint8_t i=0;
//配置中断优先级分组选择第二组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//串口1初始化波特率为115200bps,参考(https://mp.csdn.net/postedit/86110869)
usart1_init(115200);
//at24c02初始化
at24c02_init();
//向0地址发送8个字节,全都是0xAC
memset(buf,0xAC,8);
at24c02_write(0,buf,8);
printf("at24c02 write data all is 0xAC\r\n");
//24c02这个芯片,写完之后必须要一定的延时,至少2ms左右,完成从机内部的数据刷新,才能进行其他操作
delay_ms(2);
memset(buf,0,8);
at24c02_read(0,buf,16);
printf("at24c02 read data at addr 0:");
for(i=0; i<16; i++)
{
printf("%02X ",buf[i]);
}
printf("\r\n");
while(1)
{
}
}
void USART1_IRQHandler(void)
{
uint8_t d=0;
//检测是否有接收到数据
if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
{
d= USART_ReceiveData(USART1);
//清空标志位,告诉CPU,我已经处理完毕,可以接收新的一次中断请求
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}