写一个IIC通信协议
Brief Introduction
说明:本文是参考bilibili up 江协科技 STM32入门教程
本文仅作为个人学习和思考记录,可能有些地方写的不是很到位,还请评论指出不足。
IIC作为一个(SDA、SCL)双线通讯协议,特点同步,半双工,带有数据应答,且支持一主多从、多主多从。形象来说该协议像我们小时候玩的木头人游戏,游戏开始时,主机开始发言123木头人…,此时从机可以随意行动,当主机突然发出不许动这句话时,所有从机必须保持不变,只有主机说123木头人时才可以动。SCL低电平时,SDA可以变化,但是SCL高电平时,SDA就必须保持不变。
START
开始之前先定义端口
首先IIC初始化函数,这里开启GPIOB的时钟,又配置了Pin10和Pin11,最后把他们设置为高电平。加粗字体可以自行修改。
void IIC_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}
这里把SDA和SCL都封装成函数,函数里又包含延时函数,这样调用的时候,既方便改引脚又方便改延时时间。
void IIC_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
Delay_us(10);
}
void IIC_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
Delay_us(10);
}
(BitAction)BitValue
是一个类型转换,它将一个变量 BitValue 转换成 BitAction 类型。这里BitValue取0或1,然后转换成下面枚举类型里面内容。
文件:stm32f10x_gpio.c
typedef enum
{ Bit_RESET = 0,
Bit_SET
}BitAction;
这是读SDA引脚值函数,它还有一个返回值BitValue,用于接收字节和应答。
uint8_t IIC_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
Start & Stop
IIC开始先把SDA、SCL都释放,然后拉低SDA,再拉低SCL;
IIC_Start()
{
IIC_SDA(1);
IIC_SCL(1);
IIC_SDA(0);
IIC_SCL(0);
}
这里建议先释放SDA,再释放SCL,目的是为了兼容重复起始条件。
IIC结束先拉低SDA,再释放SCL,再释放SDA;
void IIC_Stop
{
IIC_SDA(0);
IIC_SCL(1);
IIC_SDA(1);
}
Send one Byte
SCL低电平,允许数据变换,SCL高电平,保持数据稳定;
在发送一个字节时,高位先行,因此变换数据的时候,先放最高位,再放次高位,…,最后最低位;
依次把一个字节每一位放在SDA线上,每放完一位,释放SCL,拉低SCL,驱动时钟运转;
首先SCL低电平期间,先把Byte的最高位放在SDA线上,然后判断该位是否为0,是就SDA低电平,否就是SDA高电平,然后SCL释放,SCL再拉低,接着判断次高位…循环8次就完成了一个字节的发送。
这里除了终止条件SCL以高电平结束,其他所有单元SCL以低电平结束,因此在后面的函数模块里不会在开始时先写IIC_SCL(0);
void IIC_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)
{
if((Byte & 0x80>>i) == 0){
IIC_SDA(0);
}
else{
IIC_SDA(1);
}
IIC_SCL(1);
IIC_SCL(0);
}
}
Receive one Byte
在接受一个字节开始时,SCL低电平,此时从机需要把数据放到SDA上,(为了防止主机干扰从机写入数据,主机需要先释放SDA,释放SDA相当于切换为输入模式),如果从机想发1,就释放SDA,如果从机想发0就拉低SDA,然后主机释放SCL,在SCL高电平期间读取SDA,再然后拉低SCL,从机把下一位数据放到SDA上,重复8次,主机就能读到一个字节了。
接受一个字节,进来之后SCL是低电平,主机释放SDA,从机把数据放到SDA,这时主机释放SCL,SCL高电平主机就能读取数据了,如果读到的数据为1,就把0x80右移一位,Byte和0x80或运算,再赋给Byte,如果读到数据为0,则进入下一次循环,默认这一位为0,循环8次返回Byte的值。
//整体函数内容不唯一可以用移位运算实现字节读取
uint8_t IIC_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
IIC_SDA(1); //主机释放SDA
for (i = 0; i < 8; i ++)
{
IIC_SCL(1); //主机在SCL高电平时,读取数据
if (IIC_R_SDA() == 1){Byte |= (0x80 >> i);}
IIC_SCL(0);
}
return Byte;
}
Send Ack & Receive Ack
发送应答
相当于发送一个字节简化版,函数进来时,SCL低电平,主机把AckBit放到SDA上,SCL高电平,从机读取应答,SCL低电平进入下一个时序单元。
void IIC_SendAck(uint8_t AckBit)
{
IIC_SDA(AckBit);
IIC_SCL(1);
IIC_SCL(0);
}
接收应答
uint8_t IIC_ReceiveAck(void)
{
uint8_t AckBit;
IIC_SDA(1);
IIC_SCL(1);
AckBit = IIC_R_SDA() ;
IIC_SCL(0);
return AckBit;
}
END
验证
需要有支持IIC通信的器件,还有引脚要插对位置;
需要OLED来显示结果;
这里使用从机地址为0x68的IIC器件(如:MPU6050)
MPU6050的引脚AD0为低电平时从机地址为 110 1000;高电平时为 110 1001;
因此可以通过配置AD0引脚来更改从机地址,来避免地址重名。
把110 1000分成高三位和低四位的七位二进制,转换成十六进制就是0x68;
在IIC通信中高七位是从机地址,最后一位是读写位,一种方式是如果把0x68当作从机地址,在发送第一个字节时,那么需要0x68左移一位再按位或上读写位,读1写0;
另一种方式是直接把0x68左移一位后当作从机地址,即0xD0,如果要写就直接把0xD0当作第一个字节,如果读就把0xD0和0x01或运算,0xD1当作第一个字节。此方式将读写位融入到从机地址中。
int main(void)
{
OLED_Init();
IIC_Init();
IIC_Start();
IIC_SendByte(0xD0);
uint8_t Ack = IIC_ReceiveAck();
IIC_Stop();
OLED_ShowNum(1,1,Ack,3);
}
显示为0表示MPU6050应答,否则会显示1
根据上面main函数,在该函数里面套一个for循环用于遍历从机地址,
//由于从机地址是一个7位二进制数,所有它的十六进制数范围0x00~0x7F
for(uint8_t address=0x00;address<0x7F;address++)
{
IIC_Start();
IIC_SendByte(address<<1); // I2C 地址的最低位为读写位,此处写入模式,读1写0
uint8_t Ack = IIC_ReceiveAck();
IIC_Stop();
if(Ack==0)
{
OLED_ShowString(1,1,"Ack=");
OLED_ShowNum(1,5,Ack,3);
OLED_ShowString(2,1,"address:"); //移位后的从机地址
OLED_ShowHexNum(2,9,address<<1,4); //没有移位的从机地址
OLED_ShowString(3,1,"ID:");
OLED_ShowHexNum(3,4,address,4);
Delay_s(1);
}
}
运行结果