I2C系列(三):软件模拟I2C读写24C02

一.目标

PC 端的串口调试软件通过 RS-485 与单片机通信,控制单片机利用软件模拟 I2C 总线对 EEPROM(24C02) 进行任意读写。

二.硬件简述

2.1 24C02硬件参数

24C02器件地址为0x50,存储容量为256字节,存储单元地址位数为8。地址发送方法如下图所示。

2.2 RS-485简述

在工业控制领域,传输距离越长,要求抗干扰能力也越强。由于 RS-232 无法消除共模干扰,且传输距离只有 15m 左右,无法满足要求。

工业标准组织提出了 RS-485 接口标准。 RS-485 标准采用差分信号传输方式,因此具有很强的抗共模干扰能力。 RS-485 接口芯片 SP485E 的封装及引脚说明如下图 1。

RS-485 的逻辑电平为:

①当 A 的电位比 B 高 200mV 以上时, 为逻辑 1;

②当 B 的电位比 A 电位高200mA 以上时为逻辑 0,传输距离可达 1200m。由于是差分传输,因此无需公共地,在 RS-485 总线上仅需连接两根线 A 和 B。

单片机与 RS-485 接口芯片的电路连接图如下图 2。

三.控制命令定义

定义如下命令:

①c------串口接收数据函数初始化

②s------单片机将接收到的数据发送到串口调试终端显示,以确认单片机是否已正确接收数据

③w------将接收缓冲区 wbuf 中的数据写入 EEPROM 中

④r------将刚才写入 EEPROM 中的数据读出到缓冲区 rbuf 中,并发送到串口调试终端显示

四.C代码

本代码注重功能实现,以期达到理解I2C协议和24C02读写方法的目的。实际项目还须考虑代码质量,如可读性、可维护性等。

4.1 I2C基础时序模拟

4.1.1 引脚初始化

cpp 复制代码
void i2cinit(void)
{
	sdaout;/*引脚输出模式*/
	sclout;
	sda = 1; /*释放总线*/
	scl = 1;
}

4.1.2 延时函数

1.SCL 时序控制延时函数

78K0指令的最短时钟周期为2个,一条NOP指令即为2个时钟周期,若使用内部8MHz时钟,则执行一条NOP指令需0.25us。

cpp 复制代码
void delay(void)
{
	UCHAR i;
	for(i = 0;i < NOP_num;i++)
		NOP();
}

2.长延时函数

cpp 复制代码
void delay_long(UINT a)
{
	UINT i,j;
	for(i = 0;i < a;i++)
	    for(j = 0;j < 100;j++);
}

4.1.3 起始信号模拟

SCL 线为高电平期间, SDA 线由高电平向低电平的变化表示起始信号。

cpp 复制代码
void i2cstart(void)
{
	sdaout;	
	sclout;
	/*1.初始化SDA为高电平:在SCL低电平期间拉高SDA*/
	scl = 0;
	delay();
	sda = 1;
	delay();
	
	/*2.模拟一个起始信号*/
	scl = 1;
	delay();
	sda = 0;
	delay();
}

4.1.4 停止信号模拟

SCL 线为高电平期间, SDA 线由低电平向高电平的变化表示终止信号。

cpp 复制代码
void i2cstop(void)
{
	sdaout;
	sclout;
	
	/*1.初始化SDA为低电平:在SCL低电平期间拉低SDA*/
	scl=0;
	delay();
	sda=0; 
	delay();
	
	/*2.模拟一个停止信号*/
	scl=1;
	delay();
	sda=1;
	delay();
} 

4.1.5 应答信号模拟

每一个字节必须保证是 8 位长度。数据传送时,先传送最高位(MSB),每一个被传送的

字节后面都必须跟随一位应答位(即一帧共有 9 位)。

在第 9 个时钟信号的高电平期间:若 SDA为 0,则为应答;若 SDA 为 1,则为非应答。

1.主机发送应答信号

cpp 复制代码
void i2c_ack_write(UCHAR ack)
{
	sdaout;
	sclout;
	
	/*1.初始化SDA为应答信号/非应答电平:在SLC低电平期间改变SDA*/
	scl=0; /*sda 变化前,先将 scl 置 0,一个时钟周期的开始*/
	delay();
	if(ack == 1)
		sda = 1;/*1,不应答从机,通知从机释放 sda*/
	else 
		sda = 0;/*0,应答从机*/
	delay();
	
	/*2.scl 置高,通知从机读取SDA*/
	scl= 1;
	delay();
} 

2.读取从机的应答信号

cpp 复制代码
UCHAR i2c_ack_read(void)
{
	UCHAR sack;
	sdaout;
	sclout;
	/*1.释放SDA,让从机控制:在SLC低电平期间拉高SDA*/
	scl=0;
	delay();
	sda=1;
	delay();
	sdain; /*sda 设置为输入模式,以检测从机的应答信号*/
	
	/*2.scl 置高,读取从机的应答信号*/
	scl=1
	delay();
	if(sda==1)
		sack=1;/*从机无应答*/
	else 
		sack=0;/*从机应答*/
	return sack; 
} 

4.1.6 写一个字节数据

只是数据段传送的操作,即开始与应答之间的操作,但不包括开始和应答。

cpp 复制代码
void writebyte(UCHAR dat)
{
	UCHAR temp=0;
	UCHAR i;
	sdaout;
	sclout;
	for(i = 0;i < 8;i++)
	{
		/*1.在SCL低电平时,准备好SDA*/
		temp = dat&0x80;
		scl = 0; 
		delay();
		if(temp == 0)
			sda = 0;
		else 
			sda = 1; 
		delay();
		
		/*2.拉高SCL,通知从机读SDA*/
		scl = 1;
		delay();
		
		dat = dat << 1;
	}
	
	/*3.释放SDA总线*/
	scl = 0;
	delay();
	sda = 1;
	delay();
}

4.1.7 读一个字节数据

只是数据段传送的操作,即开始与应答之间的操作,但不包括开始和应答。

cpp 复制代码
UCHAR readbyte(void)
{
	UCHAR i;
	UCHAR temp = 0;
	sdaout;
	sclout;
	/*1.主机释放SDA,并将SDA设为输入模式*/
	scl = 0;
	delay();
	sda = 1;
	delay();
	sdain; 
	
	for(i = 0;i < 8;i++)
	{
		/*1.拉低SCL,通知从机发送数据*/
		scl=0;
		delay();
		
		/*2.拉高SCL,读取SDA*/
		scl=1;
		delay();
		if(sda == 1)
			temp = (temp << 1) | 0x01;
		else
			temp = (temp << 1) | 0x00;
	}
	return temp;
}

4.2 I2C读写数据

4.2.1 向任意地址写单字节数据

包括数据传送、应答信号(从机),但不包括开始和停止信号。

cpp 复制代码
UCHAR writebyte_to_anyaddr(UCHAR dev_addr,UCHAR mem_addr,UCHAR dat)
{
	UCHAR sack;
	
	/*1.写器件地址*/
	writebyte((dev_addr << 1) | 0);	
	sack = i2c_ack_read(); 
	if(sack	==	1)
		return 1;
	
	/*2.写存储单元地址*/
	writebyte(mem_addr);
	sack = i2c_ack_read(); 
	if(sack == 1)
		return 1;
	
	/*3.发送数据*/
	writebyte(dat);	
	sack = i2c_ack_read();
	if(sack == 1)
		return 1;
	else
		return 0;
} 

4.2.2 从任意地址读单字节数据

包括数据传送、应答信号,但不包括开始和停止信号。

cpp 复制代码
UCHAR readbyte_from_anyaddr(UCHAR dev_addr,UCHAR mem_addr,UCHAR *data)
{
	UCHAR temp = 0;
	UCHAR sack = 0;
	
	/*1.写器件地址*/
	writebyte((dev_addr << 1) | 0);
	sack = i2c_ack_read();
	if(sack == 1)
		return 1;
	
	/*2.写存储单元地址*/
	writebyte(mem_addr);
	sack = i2c_ack_read();
	if(sack == 1)
		return 1;
	
	/*3.重置开始,改为读方向*/
	i2cstart();	
	writebyte((dev_addr << 1) | 1);
	sack = i2c_ack_read();
	if(sack==1)
		return 1;
	
	/*4.读数据*/
	readbyte(&temp);
	
	/*5.读完后,主机不应答,通知从机释放 sda*/
	i2c_ack_write(M_NACK); 
	
	*data = temp;
	return 0; 
} 

4.2.3 写n字节数据到任意地址

cpp 复制代码
UCHAR writenbytes_to_anyaddr(UCHAR dev_addr,UCHAR mem_addr,UCHAR* buf,UCHAR buflen)
{
	UCHAR i = 0;
	UCHAR sack = 0;
	i2cstart();
	writebyte_to_anyaddr(dev_addr,mem_addr,buf[i]);/*向指定地址写一个数据*/
	for(i = 1;i < buflen;i++)
	{
		/*页边界处理*/
		if((mem_addr & 0x07) == 0x07)/*地址的低 3 位为"111"时,主机须送下一页的起始地址*/
		{
			i2cstop(); /*到页边界时,主机须发停止信号,通知从机结束当前页的传送*/
			delay_long(1);/*结束信号与开始信号之间须延时*/
			i2cstart();/*开始*/
			writebyte((dev_addr << 1) | 0); /*送器件地址,写*/
			sack = i2c_ack_read(); /*检测从机应答*/
			if(sack==1)
				return 1;/*无应答,返回 1*/
		
			writebyte(mem_addr + 1);/*写数据地址,地址值加 1*/
			sack=i2c_ack_read();/*检测从机应答*/
			if(sack == 1)
				return 1;/*无应答,返回 1*/
		}
		
		 /*页内写字节,地址自动加 1*/
		writebyte(buf[i]);
		sack=i2c_ack_read();
		if(sack==1)
			return 1;
	
		mem_addr++;/*mem_addr 始终等于当前写入数据的地址,以便进行页边界判断*/
	}
	return 0; /*返回 0,写成功*/
} 

4.2.4 从任意地址读n字节数据

cpp 复制代码
UCHAR readnbytes_from_anyaddr(UCHAR dev_addr,UCHAR mem_addr,UCHAR* buf,UCHAR buflen)
{
	UCHAR i=0;
	UCHAR sack;
	i2cstart();/*开始*/
	writebyte((dev_addr << 1) | 0);/*送器件地址,写*/
	sack=i2c_ack_read();/*检测从机应答*/
	if(sack==1)
		return 1;
	
	writebyte(mem_addr);/*送数据地址*/
	sack=i2c_ack_read();/*检测从机应答*/
	if(sack==1)
		return 1;
	
	i2cstart();/*开始*/
	writebyte((dev_addr << 1) | 1);/*送器件地址,读*/
	sack=i2c_ack_read();/*检测从机应答*/
	if(sack==1)
		return 1;
	
	for(i=0;i<buflen;i++)
	{
		buf[i]=readbyte(); /*读一字节数据到指定的缓冲区中*/
		if(i==(buflen-1))
			i2c_ack_write(M_NACK); /*读完后,主机不应答,通知从机释放 sda*/
		else
			i2c_ack_write(M_ACK); /*若未读完,主机应答,继续读*/
	}
	return 0;/*返回 0,读成功*/
}

4.3 串口中断服务函数

4.3.1 接收中断处理函数

cpp 复制代码
__interrupt void MD_INTSR0(void)
{
	UCHAR err_type;
	UCHAR rx_data;
	err_type = ASIS0;
	rx_data = RXB0;
	P7=rx_data;
	if( err_type & 0x07 )
	{
		CALL_UART0_Error( err_type );
		return;
	}
	if(rx_data=='c') /*接收到 c 命令, flag 置 1*/
	{
		flag=1;
		return;
	}
	if(rx_data=='s') /*接收到 s 命令, flag 置 2*/
	{
		flag=2;
		return;
	}
	if(rx_data=='w') /*接收到 w 命令, flag 置 4*/
	{
		flag=4;
		return;
	}
	if(rx_data=='r')/*接收到 r 命令, flag 置 5*/
	{
		flag=5;
		return;
	}
	if(gUart0RxLen > gUart0RxCnt)/*正常接收数据,非命令*/
	{
		*gpUart0RxAddress = rx_data;
		gpUart0RxAddress++;
		gUart0RxCnt++;
	}
	else
		flag=3;/*接收缓冲区满, flag 置 3*/
} 

4.3.2 发送中断处理函数

cpp 复制代码
__interrupt void MD_INTST0(void)/*发送中断处理函数*/
{
	if( gUart0TxCnt > 0 )
	{
		TXS0 = *gpUart0TxAddress;
		gpUart0TxAddress++;
		gUart0TxCnt--;
	}
	else /*发送完毕*/
	{
		P1.2=0;/*将 485 设置为接收模式*/
		SRMK0=0;/*开接收中断*/
	}
}

4.4 宏定义和声明

cpp 复制代码
extern volatile USHORT gUart0RxCnt; /*接收数据统计*/
extern UCHAR flag;/*串口调试软件终端发送的命令标识*/

#define NOP_num 60/*延时函数中 NOP()指令的执行次数*/
#define scl P6.0 /*开漏输出引脚 P6.0 作为时钟引脚*/
#define sda P6.1 /*开漏输出引脚 P6.1 作为数据引脚*/
#define sclout PM6.0=0 /*时钟引脚输出模式*/
#define sdaout PM6.1=0 /*数据引脚输出模式*/
#define sclin PM6.0=1 /*时钟引脚输入模式*/
#define sdain PM6.1=1 /*数据引脚输入模式*/

#define DEV_ADDR 0x50 /*器件地址宏定义,输出*/

#define M_NACK 1 /*主机无应答常量定义*/
#define M_ACK 0 /*主机应答常量定义*/

void delay(void); /*SCL 时序控制延时函数*/
void delay_long(UINT a);/*长延时函数*/

void i2cinit(void);/*IIC 引脚初始化函数*/

void i2cstart(void);/*启动函数*/
void i2cstop(void);/*停止函数*/
void i2c_ack_write(UCHAR);/*主机应答处理函数*/
UCHAR i2c_ack_read(void);/*从机应答处理函数*/

void writebyte(UCHAR dat);/*写一个字节函数,只是数据段传送的操作,即开始与应答之间的操作,但不包括开始和应答*/
UCHAR readbyte(void);/*读一个字节函数,只是数据段传送的操作,即开始与应答之间的操作,但不包括开始和应答*/

UCHAR writebyte_to_anyaddr(UCHAR dev_addr,UCHAR mem_addr,UCHAR dat);/*向任意地址写一个数,包括开始信号、数据传送、应答信号(从机),但不包括停止信号*/
UCHAR readbyte_from_anyaddr(UCHAR dev_addr,UCHAR mem_addr);/*从任意地址中读一个数,包括开始信号和数据传送,但不包括应答信号(主机)和停止信号*/

UCHAR writenbytes_to_anyaddr(UCHAR dev_addr,UCHAR mem_addr,UCHAR* buf,UCHAR buflen);/*向任一地址开始的连续多个储存单元写一串数据*/
UCHAR readnbytes_from_anyaddr(UCHAR dev_addr,UCHAR mem_addr,UCHAR* buf,UCHAR buflen);/*从任一地址开始的连续多个储存单元读出一串数据*/

4.5 主处理函数main

cpp 复制代码
void main( void )
{
	UCHAR wbuf[256]={0};/*待写到 EEPROM 中的数据缓冲区*/
	UCHAR rbuf[256]={0};/*从 EEPROM 中读出的数据缓冲区*/
	UCHAR wend[] = "write end!";/*写完成提示*/
	UCHAR temp=0;
	UCHAR i;
	UCHAR wnum=0;
	i2cinit();/*IIC 引脚初始化*/
	UART0_ReceiveData( wbuf,256);/*串口接收数据函数初始化*/
	P1.2=0;/*RS-485 使能引脚,设置为数据接收模式*/
	UART0_Start(); /*启动串口*/
	P7.0=0; /*程序运行 LED 指示*/
	while (1)
	{
		if(flag == 1)/*c 命令,串口接收数据函数初始化*/
		{
			UART0_ReceiveData( wbuf,256);
			flag=0;
		}
		if(flag == 2)/*s 命令,单片机将接收到的数据发送到串口调试终端显示*/
		{
			SRMK0=1;/*屏蔽接收中断*/
			P1.2=1; /*单片机设置为数据发送模式*/
			delay_long(1);
			flag=0;
			temp=(UCHAR)gUart0RxCnt;/*强制类型转换*/
			UART0_SendData(wbuf,temp);/*将接收的数据发送到串口调试终端*/
		}
		if(flag==3)/*接收缓冲区满,初始化串口接收函数,覆盖原来的数据*/
		{
			flag=0;
			UART0_ReceiveData( wbuf,256);
		}
		if(flag==4)/*w 命令,将 wbuf 中的数据写入 EEPROM 中*/
		{
			flag=0;
			DI();/*写过程,禁止中断*/
			temp=(UCHAR)gUart0RxCnt;
			writenbytes_to_anyaddr(DEV_ADDR,0,wbuf,temp);
			i2cstop();
			delay_long(2);
			i2cstart();
			writebyte_to_anyaddr(DEV_ADDR,250, temp);
			i2cstop();
			EI();/*开中断*/
			SRMK0=1;/*屏蔽接收中断*/
			P1.2=1; /*设置为发送模式*/
			UART0_SendData( wend,sizeof(wend));/*发送写结束字符串到串口调试终端显示*/
		}
		if(flag==5)/*r 命令,将刚才写入到 EEPROM 中的数据读出到 rbuf 中,并发送到串口调试终端显示*/
		{
			flag=0;
			DI();/*读过程中,禁止中断*/
			i2cstart();
			readbyte_from_anyaddr(DEV_ADDR,250,&wnum);
			i2cstop();
			delay_long(1);
			readnbytes_from_anyaddr(DEV_ADDR,0,rbuf,wnum); /*读数据*/
			i2cstop();
			EI();/*开中断*/
			SRMK0=1;
			P1.2=1;
			delay_long(1);
			UART0_SendData(rbuf,wnum);
		}
	}
}

五.测试结果

发送 "1,2,3....18" 共 18 个数给单片机,让单片机以页写的方式写入 24C04 中。

结果如下:

1) 若不进行页边界处理,则 17 和 18 两个数覆盖 01 和 02,即为页上卷,且说明页内字节数为

16字节。(如下图 )

2)进行页边界处理后,结果如下图 :

相关推荐
清风6666661 小时前
基于单片机的双档输出数字直流电压源设计
单片机·mongodb·毕业设计·nosql·课程设计
牛马大师兄1 小时前
STM32独立看门狗IWDG与窗口看门狗WWDG知识梳理笔记
笔记·stm32·单片机·嵌入式硬件·嵌入式·看门狗
夜月yeyue1 小时前
STM32 Flash 访问加速器详解(ART Accelerator)
linux·单片机·嵌入式硬件·uboot·bootloard
A9better2 小时前
嵌入式开发学习日志37——stm32之USART
stm32·嵌入式硬件·学习
国科安芯6 小时前
ASP4644芯片低功耗设计思路解析
网络·单片机·嵌入式硬件·安全
充哥单片机设计6 小时前
【STM32项目开源】基于STM32的智能厨房火灾燃气监控
stm32·单片机·嵌入式硬件
CiLerLinux13 小时前
第四十九章 ESP32S3 WiFi 路由实验
网络·人工智能·单片机·嵌入式硬件
时光の尘13 小时前
【PCB电路设计】常见元器件简介(电阻、电容、电感、二极管、三极管以及场效应管)
单片机·嵌入式硬件·pcb·二极管·电感·三极管·场效应管
Lu Zelin13 小时前
单片机为什么不能跑Linux
linux·单片机·嵌入式硬件
宁静致远202114 小时前
stm32 freertos下基于hal库的模拟I2C驱动实现
stm32·嵌入式硬件·freertos