一.目标
PC 端的串口调试软件通过 RS-485 与单片机通信,控制单片机利用软件模拟 I2C 总线对 EEPROM(24C04) 进行任意读写。
二.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协议和24C04读写方法的目的。实际项目还须考虑代码质量,如可读性、可维护性等。
4.1 I2C基础时序模拟
4.1.1 引脚初始化
void i2cinit(void)
{
sdaout;/*引脚输出模式*/
sclout;
sda = 1; /*释放总线*/
scl = 1;
}
4.1.2 延时函数
1.SCL 时序控制延时函数
78K0指令的最短时钟周期为2个,一条NOP指令即为2个时钟周期,若使用内部8MHz时钟,则执行一条NOP指令需0.25us。
void delay(void)
{
UCHAR i;
for(i = 0;i < NOP_num;i++)
NOP();
}
2.长延时函数
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 线由高电平向低电平的变化表示起始信号。
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 线由低电平向高电平的变化表示终止信号。
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.主机发送应答信号
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.读取从机的应答信号
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 写一个字节数据
只是数据段传送的操作,即开始与应答之间的操作,但不包括开始和应答。
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 读一个字节数据
只是数据段传送的操作,即开始与应答之间的操作,但不包括开始和应答。
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 向任意地址写单字节数据
包括数据传送、应答信号(从机),但不包括开始和停止信号。
UCHAR writebyte_to_anyaddr(UCHAR addr,UCHAR dat)
{
UCHAR sack;
/*1.写器件地址*/
writebyte(deviceaddr0);
sack = i2c_ack_read();
if(sack == 1)
return 1;
/*2.写存储单元地址*/
writebyte(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 从任意地址读单字节数据
包括数据传送、应答信号,但不包括开始和停止信号。
UCHAR readbyte_from_anyaddr(UCHAR addr)
{
UCHAR temp = 0;
UCHAR sack = 0;
/*1.写器件地址*/
writebyte(deviceaddr0);
sack = i2c_ack_read();
if(sack == 1)
return 1;
/*2.写存储单元地址*/
writebyte(addr);
sack = i2c_ack_read();
if(sack == 1)
return 1;
/*3.重置开始,改为读方向*/
i2cstart();
writebyte(deviceaddr1);
sack = i2c_ack_read();
if(sack==1)
return 1;
/*4.读数据*/
temp=readbyte();
return temp;
}
4.2.3 写n字节数据到任意地址
UCHAR writenbytes_to_anyaddr(UCHAR addr,UCHAR* buf,UCHAR buflen)
{
UCHAR i = 0;
UCHAR sack = 0;
i2cstart();
writebyte_to_anyaddr(addr,buf[i]);/*向指定地址写一个数据*/
for(i = 1;i < buflen;i++)
{
if(BYTES_PER_PAGE == 8)/*页边界处理:页内字节数为 8*/
{
if((addr & 0x07) == 0x07)/*地址的低 3 位为“111”时,主机须送下一页的起始地址*/
{
i2cstop(); /*到页边界时,主机须发停止信号,通知从机结束当前页的传送*/
delay_long(1);/*结束信号与开始信号之间须延时*/
i2cstart();/*开始*/
writebyte(deviceaddr0); /*送器件地址,写*/
sack = i2c_ack_read(); /*检测从机应答*/
if(sack==1)
return 1;/*无应答,返回 1*/
writebyte(addr + 1);/*写数据地址,地址值加 1*/
sack=i2c_ack_read();/*检测从机应答*/
if(sack == 1)
return 1;/*无应答,返回 1*/
}
}
if(BYTES_PER_PAGE == 16)/*页边界处理:页内字节数为 16*/
{
if((addr&0x0f)==0x0f)/*地址的低 4 位为“1111”时,主机须送下一页的起始地址*/
{
i2cstop();
delay_long(1);
i2cstart();
writebyte(deviceaddr0);
i2c_ack_read();
writebyte(addr+1);
sack=i2c_ack_read();
if(sack==1)
return 1;
}
}
writebyte(buf[i]); /*页内写字节,地址自动加 1*/
sack=i2c_ack_read();
if(sack==1)
return 1;
addr++;/*addr 始终等于当前写入数据的地址,以便进行页边界判断*/
}
return 0; /*返回 0,写成功*/
}
4.2.4 从任意地址读n字节数据
UCHAR readnbytes_from_anyaddr(UCHAR addr,UCHAR* buf,UCHAR buflen)
{
UCHAR i=0;
UCHAR sack;
i2cstart();/*开始*/
writebyte(deviceaddr0);/*送器件地址,写*/
sack=i2c_ack_read();/*检测从机应答*/
if(sack==1)
return 1;
writebyte(addr);/*送数据地址*/
sack=i2c_ack_read();/*检测从机应答*/
if(sack==1)
return 1;
i2cstart();/*开始*/
writebyte(deviceaddr1);/*送器件地址,读*/
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 接收中断处理函数
__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 发送中断处理函数
__interrupt void MD_INTST0(void)/*发送中断处理函数*/
{
if( gUart0TxCnt > 0 )
{
TXS0 = *gpUart0TxAddress;
gpUart0TxAddress++;
gUart0TxCnt--;
}
else /*发送完毕*/
{
P1.2=0;/*将 485 设置为接收模式*/
SRMK0=0;/*开接收中断*/
}
}
4.4 宏定义和声明
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 deviceaddr0 0xa0 /*器件地址宏定义,输出*/
#define deviceaddr1 0xa1 /*器件地址宏定义,输入*/
#define M_NACK 1 /*主机无应答常量定义*/
#define M_ACK 0 /*主机应答常量定义*/
#define BYTES_PER_PAGE 16 /*EEPROM 页内字节数宏定义*/
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 addr,UCHAR dat);/*向任意地址写一个数,包括开始信号、数据传送、应答信号(从机),但不包括停止信号*/
UCHAR readbyte_from_anyaddr(UCHAR addr);/*从任意地址中读一个数,包括开始信号和数据传送,但不包括应答信号(主机)和停止信号*/
UCHAR writenbytes_to_anyaddr(UCHAR addr,UCHAR* buf,UCHAR buflen);/*向任一地址开始的连续多个储存单元写一串数据*/
UCHAR readnbytes_from_anyaddr(UCHAR addr,UCHAR* buf,UCHAR buflen);/*从任一地址开始的连续多个储存单元读出一串数据*/
4.5 主处理函数main
void main( void )
{
UCHAR wbuf[256]={0};/*待写到 EEPROM 中的数据缓冲区*/
UCHAR rbuf[256]={0};/*从 EEPROM 中读出的数据缓冲区*/
UCHAR wend[10]={'w','r','i','t','e',' ','e','n','d','!'};/*写完成提示*/
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(0,wbuf,temp);
i2cstop();
delay_long(2);
i2cstart();
writebyte_to_anyaddr(250, temp);
i2cstop();
EI();/*开中断*/
SRMK0=1;/*屏蔽接收中断*/
P1.2=1; /*设置为发送模式*/
UART0_SendData( wend,10);/*发送写结束字符串到串口调试终端显示*/
}
if(flag==5)/*r 命令,将刚才写入到 EEPROM 中的数据读出到 rbuf 中,并发送到串口调试终端显示*/
{
flag=0;
DI();/*读过程中,禁止中断*/
i2cstart();
wnum=readbyte_from_anyaddr(250);
i2cstop();
delay_long(1);
readnbytes_from_anyaddr(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)进行页边界处理后,结果如下图 :