STM32F103 I2C软件模拟(AT24C02)
有关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信号;

四、源码下载
源码下载路径:stm32f103。
参考文章
[1] I2C—读写EEPROM

浙公网安备 33010602011771号