本帖最后由 硕果磊磊 于 2022-1-14 16:42 编辑
STM32的GPIO模拟IIC时序
背景:使用单片机(master)GPIO模拟IIC时序,读取CP3000(slave)状态字,CP3000是一种一次电源,支持PMbus总线协议。
说明:使用GPIO模拟IIC,GPIO模式必须设置为开漏模式。原因是slave产生clock stretch时会拉低SCL,master此时要拉高SCL(一般用来当做SCL的GPIO都有上拉电阻,不会短路),但如果GPIO是推挽模式,会产生一个半高时钟,不会将SCL完全拉低;只有GPIO配置为开漏模式,输出高阻作为高电平,在slave拉低SCL时才会真正的变为低。
1、IIC简介
I2C,即Inter-Integrated Circuit,是一种常用的串行通信协议,用于在器件之间——特别是两个或两个以上不同电路之间建立通信。I2C总线由一条数据线(SDA)和一条时钟线(SCL)组成。设备分主从,主设备提供时钟,并发起操作。
● SDA是供主器件和节点发送和接收数据的线路。 ● SCL是承载时钟信号的线路。SCL总是由I2C主器件生成。规范对时钟信号的低相位和高相位有最短周期要求。 2、PMBus简介 PMBus是一种基于I2C而扩展出来的协议。它定义了电源控制和管理组件所需的一组命令和数据结构。有一些更为复杂的操作,但是原理都还是基于I2C的。
3、PMBus与IIC对比 PMBus IIC 最大传输速度 100kHz 最大传输速度400kHz 最小传输速度 10kHz 无最小传输速度 高为1.4V,低为0.6V 高为0.7Vdd,低为0.3Vdd 35ms时钟低超时 无时钟超时 固定的逻辑电平 逻辑电平由VDD决定 不同的地址类型(保留、动态等) 7位、10位和广播呼叫从地址类型 不同的总线协议(快速命令、处理呼叫等) 无总线协议
4、时序 (1)等待clock stretch slave在准备数据时会拉低SCL(时钟延伸clock stretch)。master检测clock stretch采用拉高SCL,同时读取SCL的值来判断是否结束了clock stretch。实际测试中发现,单独将检测clock strech操作拉出,会产生一个时钟,导致时序紊乱。在可能产生clock stretch的时序,如开始信号,读写操作都有将SCL拉高操作改为IIC_Wait_ClkStretch()。 - int IIC_Wait_ClkStretch(void)
- {
- int time=0;
-
- IIC_SCL=1;
- delay_us(2);//1
- while( 0 == READ_SCL )
- {
- time++;
- delay_us(5);//1
- IIC_SCL=1;
- if(time > 5000 )
- {
- return -1;
- }
- }
- return 0;
- }
复制代码
(2)开始信号 开始时先将SCL和SDA拉低,钳住总线(也可以不用),然后在SCL高电平期间产生一个SDA低跳变。其中我将拉高SCL改为IIC_Wait_ClkStretch()。因为在发送第二段开始信号之前可能slave产生clock stretch(时钟延时)。 - int IIC_Start(void)
- {
- int res = 0;
- SDA_OUT(); //sda线输出
- IIC_SDA=0;
- IIC_SCL=0;
- delay_us(2);
- IIC_SDA=1;
- res = IIC_Wait_ClkStretch();
- if(res < 0 )
- {
- return -1;
- }
- delay_us(5);
- IIC_SDA=0;//START:when CLK is high,DATA change form high to low
- delay_us(5);
- IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
- return 0;
- }
复制代码(3)停止信号 在SCL高电平期间产生一个SDA低跳变。 - void IIC_Stop(void)
- {
- //int res;
- SDA_OUT();//sda线输出
- IIC_SCL=0;
- IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
- delay_us(5);
- IIC_SCL=1;
- delay_us(1);
- IIC_SDA=1;//发送I2C总线结束信号
- delay_us(4);
- }
复制代码(4)等待ACK 在SCL高电平期间,读取SDA电平,SDA为高表示NACK,SDA为低表示ACK。此函数采用超时等待ACK,否则发送停止信号。 - int IIC_Wait_Ack(void)
- {
- u8 ucErrTime=0;
- SDA_IN(); //SDA设置为输入
- IIC_SDA=1;delay_us(4);
- IIC_SCL=1;delay_us(4);
- while(READ_SDA)
- {
- ucErrTime++;
- if(ucErrTime>250)
- {
- IIC_Stop();
- return -1;
- }
- }
- IIC_SCL=0;//时钟输出0
- delay_us(4);
- return 0;
- }
复制代码(5)应答信号 在SCL高电平期间,SDA为高表示NACK,SDA为低表示ACK。 - //产生ACK应答
- void IIC_Ack(void)
- {
- IIC_SCL=0;
- SDA_OUT();
- IIC_SDA=0;
- delay_us(4);
- IIC_SCL=1;
- delay_us(4);
- IIC_SCL=0;
- }
- //不产生ACK应答
- void IIC_NAck(void)
- {
- IIC_SCL=0;
- SDA_OUT();
- IIC_SDA=1;
- delay_us(4);
- IIC_SCL=1;
- delay_us(4);
- IIC_SCL=0;
- }
复制代码(6)读操作 读一个字节,每次读取时先将SCL拉低钳住总线,再拉高SCL,延时一段,在时钟中段读取SDA。 - //读1个字节,ack=1时,发送ACK,ack=0,发送nACK
- int IIC_Read_Byte(unsigned char ack)
- {
- int res;
- unsigned char i,receive=0;
- SDA_IN();//SDA设置为输入
- for(i=0;i<8;i++ )
- {
- IIC_SCL=0;
- delay_us(3);//2
- res = IIC_Wait_ClkStretch();
- if(res < 0 )
- {
- return -1;
- }
- delay_us(1);//2
- receive<<=1;
- if(READ_SDA)receive++;
- delay_us(1);//1
- }
- if (!ack)
- IIC_NAck();//发送nACK
- else
- IIC_Ack(); //发送ACK
- return receive;
- }
复制代码(7)写操作 写一个字节,每次先准备数据,准备好数据后,延时一段,再操控SCL产生一个时钟,保证SCL高电平期间,SDA电平达到稳定状态。- int IIC_Send_Byte(u8 txd)
- {
- int res;
- u8 t;
- SDA_OUT();
- IIC_SCL=0;//拉低时钟开始数据传输
- for(t=0;t<8;t++)
- {
- //IIC_SDA=(txd&0x80)>>7;
- if((txd&0x80)>>7)
- IIC_SDA=1;
- else
- IIC_SDA=0;
- txd<<=1;
- delay_us(2); //对TEA5767这三个延时都是必须的
- //IIC_SCL=1; //修改为等待clock stretch
- res = IIC_Wait_ClkStretch();
- if(res < 0 )
- {
- return -1;
- }
- IIC_SCL=0;
- delay_us(4);
- }
- return 0;
- }
复制代码5、IIC时序波形
GPIO模拟IIC波形图
总结: 此次操作难点在于clock stretch识别,最开始使用树莓派GPIO模拟IIC,由于树莓派GPIO无法设置开漏模式,导致电平无法输出高阻,形成半高时钟,最后被识别为高电平,导致时序紊乱;不过最后使用STM32单片机完成了这一“壮举”,虽说网上GPIO模拟IIC代码到处都是,但是亲自按照协议“撸”时序还是会遇到很多困难,本次模拟还是值得记录的。
|