有关I2C
通信协议我们在《通信协议-I2C
》已经进行了详细的介绍,因此这一节不再重复介绍。
一、软件/硬件I2C
想要控制STM32
产生I2C
方式的通讯,可以采用软件模拟或硬件I2C
这两种方式。
1.1 软件模拟
所谓软件模拟,即直接使用CPU
内核按照I2C
协议的要求控制GPIO
输出高低电平。
如控制产生I2C
的起始信号时, 先控制作为SCL
线的GPIO
引脚输出高电平, 然后控制作为SDA
线的GPIO
引脚在此期间完成由高电平至低电平的切换,最后再控制SCL
线切换为低电平,这样就输出了一个标准的I2C
起始信号。
1.2 硬件I2C
硬件I2C
是指直接利用STM32
芯片中的硬件I2C
外设,该硬件I2C
外设跟USART
串口外设类似,只要配置好对应的寄存器, 外设就会产生标准串口协议的时序。
使用它的I2C
外设则可以方便地通过外设寄存器产生I2C
协议方式的通讯,如初始化好I2C
外设后, 只需要把某寄存器位置1
,那么外设就会控制对应的SCL
及SDA
线自动产生I2C
起始信号,而不需要内核直接控制引脚的电平。
相对来说,硬件I2C
直接使用外设来控制引脚,可以减轻CPU
的负担。不过使用硬件I2C
时必须使用某些固定的引脚作为SCL
和SDA
, 软件模拟I2C
则可以使用任意GPIO
引脚,相对比较灵活。
在本开发板中,由于STM32F103RCT6
芯片引脚较少,资源比较紧张, 在设计硬件时不方便使用硬件I2C
指定的引脚连接外部设备,所以在控制程序上使用软件模拟I2C
的方式。
二、AT24C02
2.1 回顾
有关AT24C02)
可以参考《通信协议-I2C
》小节中的介绍,我们介绍了AT24C0x
的读写命令。
后续我们又在《linux
驱动移植-I2C
驱动移植(OLED SSD1306
)》中介绍了I2C
设备驱动的编写,并以OLED SSD1306
作为实现设备。
2.2 硬件接线
我所使用的STM32F103RTC6
开发板上有一块数据存储器,采用的是AT24C02
芯片;
AT24C02
是一个2K
位串行可擦可编程只读存储器(EEPRAM
),内部含有256
个8
位字节,AT24C02
有一个16
字节页写缓冲区,该器件通过I2C
总线接口进行操作,有一个专门的写保护功能。
其中I2C_SCL
连接到处理器的PC12
引脚,I2C_SDA
连接到处理器的PC11
引脚。
三、AT24C02
源码实现
3.1 I2C
软件模拟
3.1.1 I2C
初始化
配置PC11~PC12
引脚,均配置为通用推挽输出,最大速度50MHZ
,输出高电平;
//宏定义I2C端口***********************************************
#define I2C_SCL PCout(12)
#define I2C_SDA PCout(11)
//宏定义SDA数据方向********************************************
#define SDA_OUT() gpio_init(PC11,GPO_PUSH_PULL_50,HIGH)
#define SCL_OUT() gpio_init(PC12,GPO_PUSH_PULL_50,HIGH)
//宏定义读取SDA数据*******************************************
#define Read_SDA PCin(11)
void SDA_IN(void)
{
gpio_init(PC11, GPI_DOWN,HIGH); // 输入下拉
PCout(11)=0;
}
/**********************************************************************************
*
* Description:I2C初始化
*
**********************************************************************************/
void I2C_Init(void) //I2C初始化
{
SDA_OUT(); //SDA设置为输出
SCL_OUT(); //SCL设置为输出
I2C_SCL=1;
I2C_SDA=1;
}
3.1.2 起始信号
I2C
通信的起始信号由主设备发起,SCL
保持高电平,SDA
由高电平变为低电平;只有在起始信号发送之后,其它数据才有效;
/**********************************************************************************
*
* Description:I2C起始信号
* SCL=1; SDA:1->0
*
**********************************************************************************/
void I2C_Start(void) //I2C起始信号
{
SDA_OUT();
I2C_SDA =1;
delay_us(5);
I2C_SCL =1;
delay_us(5);
I2C_SDA =0;
delay_us(5);
I2C_SCL =0;
}
3.1.3 终止信号
I2C
通信的终止信号由主设备发起,SCL
保持高电平,SDA
由低电平变为高低电平;随着终止信号的出现,所有外部操作就结束;
/**********************************************************************************
*
* Description:I2C终止信号
* SCL=1; SDA:0->1
*
**********************************************************************************/
void I2C_Stop(void) //I2C终止信号
{
SDA_OUT();
I2C_SDA =0;
delay_us(5);
I2C_SCL =1;
delay_us(5);
I2C_SDA =1;
delay_us(5);
I2C_SCL =0;
}
3.1.4 应答信号
I2C
总线在进行数据传送时,传送的字节数没有限制,但是每个字节长度必须为8
位。
数据传送过程中,先传送最高位(MSB
),接收端在收到有效数据后向对方相应的信号,发送端每发送一个字节数据(8
位),在第9
个始终周期释放数据线去接收对方的应答;因此一帧数据共有9
位;
- 当
SDA
位低电平位有效应答(ACK
),表示接收端已经接收到数据; - 当
SDA
是高电平位无效应答(NAK
),表示接收端没有接收成功;
主设备发送完数据需要等待从设备的应答,GPIO
模拟:
/**********************************************************************************
*
* Description:I2C(主机)等待来自从机的应答信号
* parameter:返回0:接受失败 1:接收成功
*
**********************************************************************************/
u8 I2C_WaitAck(void) //等待来自从机的应答信号
{
u16 time;
SDA_IN();
I2C_SCL =0;
delay_us(5);
I2C_SDA =1;
delay_us(5);
I2C_SCL =1;
delay_us(5);
while(Read_SDA)
{
time++;
if(time>=2500)
{
I2C_Stop();
return 0;
}
}
I2C_SCL =0;
return 1;
}
主设备接收到从设备发送的数据后,需要向从设备发送方发送应答,GPIO
模拟:
/**********************************************************************************
*
* Description:I2C(主机)产生应答信号
* parameter: ack:0不应答 1:应答
*
**********************************************************************************/
void I2C_Ack(u8 ack) //产生应答信号
{
SDA_OUT();
I2C_SCL =0;
delay_us(5);
if(ack)
I2C_SDA =0;
else
I2C_SDA =1;
I2C_SCL =1;
delay_us(5);
I2C_SCL =0;
delay_us(5);
}
3.1.5 有效数据
I2C
总线进行数据传送时,时钟信号SCL
为高电平期间,数据线SDA
上的数据必须稳定;只有在SCL
上的信号为低电平时,SDA
上的高电平或低电平状态才允许变化。
因为当SCL
是高电平时,数据线SDA
的变化被规定为控制命令(也就是前面的起始信号和终止信号)。
主设备向从设备发送发送一个字节数据,GPIO
模型模拟:
/**********************************************************************************
*
* Description:I2C写一个字节数据
* byte:一个字节的数据
*
**********************************************************************************/
void I2C_WriteData(u8 byte) //I2C写一个字节数据
{
u8 i=0;
SDA_OUT();
I2C_SCL =0;
for(i=0;i<8;i++)
{
I2C_SDA =(byte&0x80)>>7;
byte <<=1;
delay_us(5);
I2C_SCL =1;
delay_us(5);
I2C_SCL =0;
delay_us(5);
}
}
主设备从从设备读取一个字节数据,GPIO
模拟:
/**********************************************************************************
*
* Description:I2C(主机)读取一个数据
* parameter:返回读取的数据
*
**********************************************************************************/
u8 I2C_ReadData(void) //I2C读取一个数据
{
u8 i=0;
u8 data=0;
SDA_IN();
I2C_SCL =0;
for(i=0;i<8;i++)
{
I2C_SCL =1;
data <<=1;
data |=(u8)Read_SDA;
delay_us(5);
I2C_SCL =0;
delay_us(5);
}
return data;
}
3.2 AT24C02
3.2.1 按字节写
/**********************************************************************************************
*
* function:向24c02的address地址中写入一字节数据
* address:0~0xFF
*
**********************************************************************************************/
void Write24c02(u8 address,u8 byte)
{
I2C_Start();
I2C_WriteData(0xA0);
I2C_WaitAck();
I2C_WriteData(address);
I2C_WaitAck();
I2C_WriteData(byte);
I2C_WaitAck();
I2C_Stop();
delay_ms(10); //这个延时一定要足够长,否则会出错。因为24c02在从sda上取得数据后,还需要一定时间的烧录过程。
}
3.2.2 随机读
/**********************************************************************************************
*
* function:从24c02的地址address中读取一个字节数据
* address:0~0xFF
*
***********************************************************************************/
u8 Read24c02(u8 address)
{
u8 byte;
I2C_Start();
I2C_WriteData(0xA0);
I2C_WaitAck();
I2C_WriteData(address);
I2C_WaitAck();
I2C_Start();
I2C_WriteData(0xA1);
I2C_WaitAck();
byte=I2C_ReadData();
//I2C_Ack(0);
I2C_Stop();
delay_ms(10);
return byte;
}
3.2.3 连续写
/**********************************************************************************************
*
* function:向24c02的address地址中写入字符串
* address:24C02的起始地址 0~0xFF
* str:字符串的指针
*
**********************************************************************************************/
void Write_24c02Buffer(u8 address,const u8 *str)
{
u8 i=0;
u8 length=0;
length= strlen((char *)str);
for(i=0;i<length;i++)
{
Write24c02(address++,str[i]);
}
}
3.2.3 连续读
/**********************************************************************************************
*
* function:从24c02的address地址中读取字符串
* address:24c02的起始地址 0~0xFF
* str: 写入的字符串指针
* length: 写入的数据长度不包含'\0'
*
**********************************************************************************************/
void Read_24c02Buffer(u8 address,u8 *str,u8 length)
{
u8 i=0;
for(i=0;i<length;i++)
{
str[i]=Read24c02(address++);
}
str[i]='\0';
}
3.3 实现功能
这里我们通过向AT24C02
写入并读取来测试I2C
功能。
3.3.1 main
函数实现
int main()
{
u8 *time;
char temp[256];
STM32_Clock_Init(9); //系统时钟初始化
STM32_NVIC_Init(2,USART1_IRQn,0,1); //串口中断优先级初始化,其中包括中断使能
usart_init(USART_1,115200); //串口1初始化,波特率115200 映射到PA9 PA10
STM32_NVIC_Init(2,RTC_IRQn,0,1); //RTC中断优先级初始化,其中包括中断使能
while(RTC_Init()); //RTC初始化
OLED12864_GPIO_Init(); //GPIO初始化
OLED12864_Init(); //OLED初始化
OLED_P16x8Str(45,0,"OLED"); //调用LCD_P8x16Str字符串显示函数,在第0页即第一行的第45列开始,显示字符串“OLED"
I2C_Init(); //I2C初始化
OLED_P8x16Chi(16,6,"安徽理工大学");
Write_24c02Buffer(10,"郑洋是好人"); //连续写入
while(1)
{
Read_24c02Buffer(10,temp,10);
printf("打印-----%s\n",temp);
time = RTCTime();
OLED_P8x6Str(8,4,time); //显示当前时间
delay_ms(1000);
}
}
3.3.2 测试
编译程序并下载测试,打开串口查看;
下图是使用逻辑分析器捕捉到的模拟I2C
信号;
参考文章
[1] I2C
—读写EEPROM