IO模拟IIC通讯
1.STM32使用软件IIC原因
- 由于STM32的硬件IIC会出现一定概率卡死等问题,为了系统更加稳定,少出BUG 。STM32的绝大多数开发者选择使用软件IIC 。软件IIC能让初学者更加容易理解IIC的工作原理,并且相对硬件IIC,软件IIC对不同平台的移植性更好。
2.IIC设备调试注意事项
- 在调试IIC通讯设备时,尽量不要用杜邦线连接IIC设备,杜邦线接头不稳定,很多时候会出现设备卡死不应答。假如必须用杜邦线连接,请用较短等长杜邦线连接,必要时焊死杜邦线。如果出现设备有时不应答情况,且多次检查IIC通讯程序没问题,那就很大可能是接线不稳造成。经常移动设备,请考虑这点。
3.模拟IIC的基本函数
- 起始信号:SCL在高电平时,SDA从高电平拉低。
- 结束信号:SCL在高电平时,SDA从低电平拉高。
首先对GPIO的初始化,模拟IIC就用推挽模式就好
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIO_Initstruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_2;
GPIO_Initstruct.GPIO_Mode = GPIO_Mode_Out_PP; //推挽模式
GPIO_Initstruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC,&GPIO_Initstruct);
GPIO_ResetBits(GPIOC,GPIO_Pin_0);
GPIO_ResetBits(GPIOC,GPIO_Pin_2);
后面函数要用到的宏定义在这给出,用宏定义可简化代码,增强可读性与可移植性。
#define SCL_H GPIO_SetBits(GPIOC,GPIO_Pin_0)
#define SCL_L GPIO_ResetBits(GPIOC,GPIO_Pin_0)
#define SDA_H GPIO_SetBits(GPIOC,GPIO_Pin_2)
#define SDA_L GPIO_ResetBits(GPIOC,GPIO_Pin_2)
#define MPU6050_SDA_Check_H() {while(((GPIOC->ODR) & (1<<2))!=(1<<2));}//检测ODR寄存器查看IO输出状态
#define MPU6050_SDA_Check_L() {while(((GPIOC->ODR) & (0<<2))!=(0<<2));}
#define MPU6050_SCL_Check_H() {while(((GPIOC->ODR) & (1<<0))!=(1<<0));}
#define MPU6050_SCL_Check_L() {while(((GPIOC->ODR) & (0<<0))!=(0<<0));}
#define SDA_IN() {GPIO_InitTypeDef GPIO_Initstruct;\
GPIO_Initstruct.GPIO_Pin = GPIO_Pin_2;\
GPIO_Initstruct.GPIO_Mode = GPIO_Mode_IPU;\
GPIO_Init(GPIOC,&GPIO_Initstruct);}
#define SDA_OUT() {GPIO_InitTypeDef GPIO_Initstruct;\
GPIO_Initstruct.GPIO_Pin = GPIO_Pin_2;\
GPIO_Initstruct.GPIO_Mode = GPIO_Mode_Out_PP;\
GPIO_Initstruct.GPIO_Speed = GPIO_Speed_50MHz;\
GPIO_Init(GPIOC,&GPIO_Initstruct);}
#define IIC_PORT GPIOC
#define IIC_SDA_Pin GPIO_Pin_2
用宏定义封装后代码是不是更加通俗易懂,而且放在函数里时不冗杂。每个信号线都有相应的检测函数,保证在确定的电平后再做其他事。
起始信号函数
void IIC_START()
{
SDA_H; //SDA先拉高准备后面的下降沿
SDA_Check_H(); //检查SDA是否拉高
Systick_Delay_us(1); //延时1us
//先拉高SDA再拉高SCL避免信号影响!!!!!
SCL_H; //SCL拉高
SCL_Check_H(); //检查SCL是否拉高
Systick_Delay_us(1);
SDA_L;
SDA_Check_L();
Systick_Delay_us(1);
SCL_L; //SCL拉低
SCL_Check_L(); //SCL拉低检查
Systick_Delay_us(1);
}
结束信号函数
void IIC_STOP()
{
SDA_L; //先拉低SDA为上升沿做准备,SCL此时要为低,防止误信号
SDA_Check_L();
Systick_Delay_us(1);
SCL_H;
SCL_Check_H();
Systick_Delay_us(1);
SDA_H;
SDA_Check_H();
Systick_Delay_us(1);
SCL_L; //获得想要信号后SCL先下降,避免产生误信号
SCL_Check_L();
Systick_Delay_us(1);
SDA_L;
SDA_Check_L();
Systick_Delay_us(1);
}
以上为IIC的起始信号与结束信号注意每个信号的SDA与SCL位置,防止产生误信号。导致通讯失败!!!在通讯时只有在SCL为高电平时才为有效电平,注意每个函数后面都要把SCL拉低,以防误信号。
写一个Byte函数
void IIC_Write_Byte(uint8_t Byte)
{
uint8_t num;
for(num = 0;num < 8;num++)
{
if(Byte&0x80)//最高位是否为1
{
SDA_H;
SDA_Check_H();
Systick_Delay_us(1);
SCL_H;
SCL_Check_H();
Systick_Delay_us(1);
SCL_L;
SCL_Check_L();
Systick_Delay_us(1);
}
else
{
SDA_L;
SDA_Check_L();
Systick_Delay_us(1);
SCL_H;
SCL_Check_H();
Systick_Delay_us(1);
SCL_L;
SCL_Check_L();
Systick_Delay_us(1);
}
Byte <<= 1; //数据是高位先行所以左移,注意左移符号后面要有“=”不然通讯失败,编译器还不报错
}
}
读一个Byte函数
uint8_t IIC_Read_Byte()
{
unsigned char num=0,temp=0,change=0;
SDA_IN(); //SDA为上拉输入模式
for(num = 0;num < 8;num++)
{
change=0; //每次清0变量防止数据错误
SCL_H;
SCL_Check_H();
if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_2) == 1)
change = 1; //不要习惯用++
temp =(temp<<1)+change; //更新数据
SCL_L; //结束关闭SCL防止误数据
SCL_Check_L();
Systick_Delay_us(1);
}
return temp;
}
等待从设备响应函数
uint8_t Wait_ASK()
{
uint16_t time = 1000; //为了防止程序卡死
SDA_IN(); //SDA为上拉输入
SCL_H;
SCL_Check_H();
while(GPIO_ReadInputDataBit(IIC_PORT,IIC_SDA_Pin) != 0)//当低电平时设备响应,响应跳过下面条件
{
time --;
if(time == 0)
{
SDA_OUT(); //设备未响应,设置SDA为输出,return退出,防止卡死。调试时建议死循环
return 0;
}
}
SCL_L;
SCL_Check_L();
SDA_OUT(); //SDA为输出模式
return 1;
}
主动发送响应信号让从机继续发送数据
主动发送响应函数(从机继续发数据)
void IIC_Send_ASC()
{
IIC_SDA_OUT();
IIC_SDA_L; //SDA拉低表示应答
IIC_SDA_Check_L();
IIC_SCL_H;
IIC_SCL_Check_H();
IIC_SCL_L;
IIC_SCL_Check_L();
}
主动发送非应答信号终止从机的数据传输
非应答信号函数
void Send_NASC()
{
SDA_OUT();
SDA_H; //SDA拉高表示非应答,结束数据传输
SDA_Check_H();
SCL_H;
SCL_Check_H();
SCL_L;
SCL_Check_L();
SDA_L;
SDA_Check_L();
}
3.写寄存器与读寄存器
前面做好了几个基本函数,后面就可以根据前面的基本函数来组成IIC的数据基本格式,来写寄存器与读寄存器。
写寄存器函数
void IIC_Write_Reg(uint8_t addr,uint8_t Byte)
{
IIC_START(); //起始信号
IIC_Write_Byte(DEV_ADDR); //发送设备地址
Wait_ASK(); //等待从机响应
IIC_Write_Byte(addr); //写入寄存器地址
Wait_ASK(); //等待从机响应
IIC_Write_Byte(Byte); //写入要写数据
Wait_ASK(); //等待从机响应
IIC_STOP(); //发送停止信号
}
读寄存器函数
uint8_t IIC_Read_Reg(uint8_t addr)
{
uint8_t temp;
IIC_START(); //起始信号
IIC_Write_Byte(DEV_ADDR); //发送设备地址
Wait_ASK(); //等待响应
IIC_Write_Byte(addr); //写入寄存器地址
Wait_ASK(); //等待响应
IIC_START(); //再次起始信号
IIC_Write_Byte(DEV_ADDR+1); //发送设备地址,最后一位为1表示读。
Wait_ASK(); //等待响应
temp = IIC_Read_Byte(); //读数据
Send_NASC(); //发送非应答信号
IIC_STOP(); //发送停止信号
return temp; //返回数据
}
这里的设备地址默认为8位,最后一位默认为0也就是写,列如,厂商给你7位设备地址为0X68则在这的DEV_ADDR为左移1位,也就是0XD0.所以要定义DEV_ADDR为0XD0;
读寄存器和写寄存器的函数写好后建议各位测试一下,列如:
uint8_t temp;
IIC_Write_Reg(0XE0,0XB6); //向0xE0写0XB6
temp = IIC_Read_Reg(0XE0); //读取0XE0寄存器值
通过仿真看看读出值是否正确,不正确就检查IIC函数,不要纠结硬件问题。设备地址也是很容易出错的,多检查几次,设备地址若是7位要左移一位才是DEV_ADDR的真实地址。
结语
- 学习通讯协议要用IO口模拟后印象才能深刻,简单用硬件通讯协议对整个通讯过程的理解不深,而且移植性较差。移植中要注意IIC的设备地址,SDA与SCL线的顺序,不然产生误信号会导致不能通讯,并且难以查找错误。