1. 模拟IIC
2. 硬件IIC
IIC官方介绍:
•只要求两条总线线路 一条串行数据线 SDA 一条串行时钟线 SCL
• 每个连接到总线的器件都可以通过唯一的地址和一直存在的简单的主机 从机关系软件设定地
址 主机可以作为主机发送器或主机接收器
• 它是一个真正的多主机总线 如果两个或更多主机同时初始化数据传输可以通过冲突检测和仲裁
防止数据被破坏
• 串行的 8 位双向数据传输位速率在标准模式下可达 100kbit/s 快速模式下可达 400kbit/s 高速模式下可达 3.4Mbit/s
• 片上的滤波器可以滤去总线数据线上的毛刺波 保证数据完整
• 连接到相同总线的 IC 数量只受到总线的最大电容 400pF 限制
一、模拟IIC
一直听说STMF1系列的硬件IIC有问题,而且配置用起来有点麻烦。作为一个刚入门的小白,也没能力验证。所以先从模拟IIC学起。模拟IIC,基本所有引脚都可作为SDA、SCL线来用,而且配置简单。所以代码移植起来非常方便。但是任何事情都有利又有弊,模拟IIC给MCU带来了更大的负荷。增加了功耗。
我的理解模拟IIC主要分为四部分起始信号、停止信号、ACK应答信号和数据的发送、读取方法。
1)起始信号、停止信号
SDA、SCL都是高电平为空闲电平。起始信号为都是高电平是,SDA线信号拉低,然后SCL滞后拉低
停止信号为当都为低电平时,SCL先拉高,SDA滞后拉高。
为防止以后写错没地方参照,堆上我那不成熟的代码
#define IIC_OUT_MODE() {GPIOB->CRL &= 0x0fffffff;GPIOB->CRL |= 0x30000000;} //模式定义,SDA为输出模式,发送数据
#define IIC_IN_MODE() {GPIOB->CRL &= 0x0fffffff;GPIOB->CRL |= 0x80000000;} //输入模式,读取数据
#define IIC_SDA_OUT pbout(7)//拉高拉低电平
#define IIC_SDA_IN pbin(7)//读取SDA电平
#define IIC_SCL pbout(6)//控制SCL线电平高低
/*IIC开始信号*/
void IIC_Start(void)
{
IIC_OUT_MODE();
IIC_SDA_OUT=1;
IIC_SCL=1;
DelayUs(2);
IIC_SDA_OUT=0;
DelayUs(2);
IIC_SCL=0;
}
/*IIC停止信号*/
void IIC_Stop(void)
{
IIC_OUT_MODE();
IIC_SCL=0;
IIC_SDA_OUT=0;
DelayUs(3);
IIC_SCL=1;
DelayUs(3);
IIC_SDA_OUT=1;
}
2)信号传输
要值得注意的是当发起起始信号后,发送的第一个字节数据必须是器件地址,要告诉MCU寻找的器件地址是什么,数据格式为八位,高七位为地址位,最低位为读写位。
如读写EEPROM:
地址寻址,高七位为地址位,最低位为传输方向位,0为写,1为读。原子哥A2,A1,A0在板子上都接地,所以为0.,p为野地址(p=0表示低256字节,1表示高256字节),a为器件地址,在读写时候,首先是起始条件(1010)+器件地址。
数据格式:八位,从高到低发送。保证数据的有效性必须保证当SCL为高电平时读取到的SDA电平状态。当SCL为低时可以改变SDA的电平状态。当SCL为高时不允许SDA改变,如图:
代码:
/*
读取一个字节数据
返回值:读取到的数据
ack:读取数据后还继续读取否,读取就有应答,为1.结束就为零。
*/
u8 IIC_ReadOneByte(void)
{
u8 data=0,i=0;
IIC_IN_MODE();
IIC_SCL = 0;
IIC_SDA_OUT=0;
DelayUs(2);
for(i=0;i<8;i++)
{
IIC_SCL=0;
DelayUs(2);
IIC_SCL=1;
data<<=1;
if(IIC_SDA_IN)data |= 1;
DelayUs(2);
}
IIC_SCL=0;
return data;
}
/*
发送一个字节数据
data:数据
*/
void IIC_WriteOneByte(u8 data)
{
u8 i=0;
IIC_OUT_MODE();
IIC_SCL=0;
IIC_SDA_OUT=0;
DelayUs(2);
for(i=0;i<8;i++)
{
if(data&0x80)IIC_SDA_OUT=1;
else IIC_SDA_OUT=0;
data<<=1;
DelayUs(2);
IIC_SCL=1;
DelayUs(2);
IIC_SCL=0;
IIC_SDA_OUT=0;
DelayUs(2);
}
IIC_SCL=0;
}
3)应答信号
当传输数据结束后,总有一个由从机发送来的应答信号。发生在发送完最低位数据后,也就是第8个数据后,主机读取到从机的数据线信号变化。下面是原子哥的专业解释:
发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。 应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
/*
检测是否有应答
ack
返回值:1有应答,0无应答
*/
u8 IIC_ACKChick(void)
{
u8 i=0;
IIC_IN_MODE();
IIC_SDA_OUT=1;DelayUs(1);
IIC_SCL=1;DelayUs(1);
while(IIC_SDA_IN)
{
i++;
if(i>0xfe)
{
IIC_Stop();//无应答传输结束
return 0;
}
}
IIC_SCL=0;
return 1;
}
/*产生一个应答,应答就是当SCL为高时读到的SDA为低*/
void IIC_ACK(void)
{
IIC_SCL=0;
IIC_OUT_MODE();
IIC_SDA_OUT=0;
DelayUs(3);
IIC_SCL=1;
DelayUs(3);
IIC_SCL=0;
}
/*产生一个非应答*/
void IIC_NACK(void)
{
IIC_SCL=0;
IIC_OUT_MODE();
IIC_SDA_OUT=1;
DelayUs(3);
IIC_SCL=1;
DelayUs(3);
IIC_SCL=0;
}
产生ACK和NACK的函数在编写IIC设备驱动时调用
二、硬件IIC
在参考了无数前辈们的资料后,今天,终于把硬件IIC配置出来了。硬是啃了整整两天。今天终于发现,原来读、写步骤没错,是我太自信觉得初始化肯定没错,然后今天突然想试一下某前辈的代码试一下,结果真的是我配置错了。。。然后其它小毛病就不算大问题。
1)初始化:
/*
硬件IIC配置
HardWare:硬件
IIC1_SDA:PB7
IIC1_SCL:PB6
*/
void HWIIC_Init(void)
{
RCC->APB1ENR |= 1<<21;//IIC1时钟
RCC->APB1RSTR |= 1<<21;
RCC->APB1RSTR &= ~(1<<21);
RCC->APB2ENR |= 1<<3;
GPIOB->CRL &= 0x00ffffff;
GPIOB->CRL |= 0xff000000;//设置为复用开漏
I2C1->CR1 |= 1<<15;//复位
I2C1->CR1 &= ~(1<<15);//关闭IIC1复位
I2C1->CR1 |= 1<<0;//启用IIC模块
//三行-原来的错误配置。
// I2C1->CR2 |= 4<<0;//配置时钟频率为4Mhz,Tpclk1=1/4=0.25us
// I2C1->CCR |= 5<<0; //产生400Khz的SCL时钟频率。250ns*ccr*2=2500ns,2.5us->400Khz
// I2C1->TRISE |= 1<<0;//1000/250=4+1,1000为SCL最大上升时间
I2C1->CR2 |= 30 << 0;//I2C2输入时钟频率设置为30MHz
I2C1->CCR |= 1 << 15;//设置成快速模式
I2C1->CCR |= 1 << 14;//占空比设置为16/9
I2C1->CCR |= 3 << 0;//时钟控制分频系数设置为3(400KHz = 2.5us = (16+9)*CCR*Tpclk1)
I2C1->TRISE |= 10 << 0;//设置主模式时的最大上升时间(标准模式为1000ns,快速为300ns,超快为120ns)
I2C1->CR1 |= 1<<10;//应答使能
}
2)写数据:
/*
写数据
QJaddr:器件地址
txaddr:写入的起始地址
*data:数据,发送的数据
*/
void HWIIC_WriteData(u8 QJaddr,u8 txaddr,u8 *data,u8 len)
{
u8 i=0;
u16 j=0;
//起始信号、器件地址、写入地址
I2C1->CR1 |= 1<<10; //打开应答
while(I2C1->SR2 & 1<<1){} //等待总线空闲
I2C1->CR1 |= 1<<8; //产生起始条件
while(!(I2C1->SR1 & 1<<0)){} //等待起始条件发送
j=I2C1->SR1; //读寄存器
I2C1->DR = QJaddr; //写入器件地址
while(!(I2C1->SR1 & 1<<1)){}; //等待地址发送完成,当收到地址的ACK后该位被置’1’(ADDR)
j=I2C1->SR1;
j=I2C1->SR2; //读取SR1,然后读取SR2,清除ADDR位
I2C1->DR=txaddr;
//开始写入数据
while(!(I2C1->SR1 & 1<<2)){} //等待一个新数据发送完成且没有新数据填入时
for(i=0;i<len;i++)
{
while(!(I2C1->SR1 & (1 << 7))); //发送数据寄存器为空
I2C1->DR = data[i];
}
while(!(I2C1->SR1 & 1<<2)){} //等待一个新数据发送完成且没有新数据填入时,BTF=1
I2C1->CR1 |= 1<<9; //产生一个停止条件
}
3)读数据:
/*
读取数据
QJaddr:器件地址
rxaddr:读取数据的起始地址
rxdata:读取到的存放的变量
len:读取的长度
*/
void HWIIC_ReadData(u8 QJaddr,u8 rxaddr,u8 *rxdata,u8 len)
{
u8 i=0;
u8 j=0;
I2C1->CR1 |= 1<<10; //打开应答
while(I2C1->SR2 & 1<<1){} //等待总线空闲
I2C1->CR1 |= 1<<8; //起始信号
while(!(I2C1->SR1 & 1<<0)){} //等待起始条件发送完成
j = I2C1->SR1;
I2C1->DR = QJaddr;
while(!(I2C1->SR1 & 1<<1)){} //等待器件地址发送结束
j = I2C1->SR1;
j = I2C1->SR2; //读取SR1,然后读取SR2,清除ADDR位
I2C1->DR = rxaddr; //发送读取的起始地址
while(!(I2C1->SR1 & 1<<2)){} //等待地址发送结束(一个新数据发送完成且没有新数据填入时)
//开始准备读取
I2C1->CR1 |= 1<<8; //起始信号
while(!(I2C1->SR1 & 1<<0)){} //等待起始条件发送完成
I2C1->DR = QJaddr+1; //读
while(!(I2C1->SR1 & 1<<1)){} //等待器件地址发送结束
j = I2C1->SR1;
j = I2C1->SR2; //读取SR1,然后读取SR2,清除ADDR位
//读取数据
for(i=0;i<len-1;i++)
{
while(!(I2C1->SR1 & 1<<6)){} //等待接收数据寄存器非空
rxdata[i]=I2C1->DR;
}
I2C1->CR1 &= ~(1<<10); //关闭应答
I2C1->CR1 |= 1<<9; //产生停止条件
}
其中写和读要延时2ms左右,不然读不出来数据,会在读数据函数-发送器件地址时收不到从机的应答。直接卡死。本来也是怎么也找不到错误,然后想起另一位前辈的话,试了一下,没有一丝毛病~
我板子上是AT24WC08,有一个16位的页缓冲器,代码没有再编写EEPROM的页写代码,所以一次写入不要超过16个字节。否则超过的会重新从头写入,造成写入数据错乱。
//main里面的测试的几行代码
HWIIC_SendData(0xa0,0,(u8 *)"CarlyRaeJepsen\n",15);//写数据
DelayMs(5);//必须加延时,否则读不出来数据,会在发送器件地址时收不到从机的应答。
HWIIC_ReadData(0xa0,0,C08_Buff,15);//读数据
printf("HWIIC_Data:%s\n",C08_Buff);//打印数据
堆上老师的代码。。。老师都发过好几天了,我竟然没看到。。。难受。。。
/*
函数功能: IIC总线初始化
硬件连接:
SCL-PB6
SDA-PB7
*/
void IIC_Init(void)
{
//配置GPIO模式
RCC->APB2ENR|=1<<3; //PB
GPIOB->CRL&=0x00FFFFFF;
GPIOB->CRL|=0xFF000000;
GPIOB->ODR|=0x3<<6;
//I2C1
RCC->APB1ENR|=1<<21;//I2C1
RCC->APB1RSTR|=1<<21;//I2C1复位使能
RCC->APB1RSTR&=~(1<<21);//I2C1复位失能
//配置I2C1模式
I2C1->OAR1|=1<<14;//由软件位保持为一
I2C1->OAR1|=0xa;//设置七位地址为000 1010
I2C1->CCR|=1<<15;//选择I2C1主模式为快速模式
I2C1->CCR|=0x1e<<0;//配置I2C1 CCR大小
I2C1->CR1|=1<<10;//使能应答
I2C1->CR1|=1<<0;//使能I2C1
I2C1->CR2=0x24<<0;//选择时钟频率为36MHZ
}
/*
函数功能:IIC总线起始信号
*/
void IIC_Start(void)
{
I2C1->CR1|=1<<8; //产生起始信号
while(!(I2C1->SR1&1<<0)){} //检查起始条件是否发送成功
I2C1->SR1=0;
}
/*
函数功能:IIC总线停止信号
*/
void IIC_Stop(void)
{
I2C1->CR1|=1<<9; //产生停止信号
}
/*
函数功能: 发送7位地址
函数参数: flag 表示读写标志 0表示写 1表示读
*/
void I2C_Send7bitAddress(u8 addr,u8 flag)
{
I2C1->DR = addr;
if(flag==0) //写
{
while(1)
{
u16 state1,state2;
state1=I2C1->SR1;
state2=I2C1->SR2;
if((state1&1<<1)&&(state1&1<<7)&&(state2&0x7))
{
break;
}
}
}
else //读
{
while(1)
{
u16 state1,state2;
state1=I2C1->SR1;
state2=I2C1->SR2;
if((state1&1<<1)&&(state2&1<<1))
{
break;
}
}
}
}
/*
函数功能: 发送数据
*/
void I2C_SendData(u8 data)
{
I2C1->DR = data;
while(1)
{
u16 state1,state2;
state1=I2C1->SR1;
state2=I2C1->SR2;
if((state1&1<<2)&&(state1&1<<7)&&(state2&0x7))
{
break;
}
}
}
/*
函数功能: 读取总线数据
*/
u8 I2C_ReadData(void)
{
while(!(I2C1->SR1&1<<6)){} //等待数据到来
return I2C1->DR;
}
菜鸟一枚,只是试出来简单配置就暂时放下,没有大量测试和深入配置DMA等强大的功能。将来再去调试啦~