I2C软件模拟时序
- 引脚模式的配置
- SDA引脚配置
- 如果外部有接上拉电阻,那么直接开漏输出就可以了
- 如果没有上拉电阻,那在编程时就需要考虑在不同的阶段,设置不同的输入输出模式
- SDA引脚配置
数据有效性
起始信号
- 时序图:
-
SCL 线是高电平时 ,SDA 线从高电平向低电平切换表示起始信号
-
程序如下:
void A_I2C_Start(void) { SCL_High; //先拉高时钟线 delay_us(2); //延时函数很有必要,延时是微秒级,因为使用的是stm32f4,可以使用SysTick来实现,后面给出 SDA_High; //拉高数据线 delay_us(2); SDA_LOW; delay_us(2); SCL_LOW; //拉低时钟线,使SDA可以进行数据的切换 delay_us(2); } void delay_us(uint32_t nus) { uint32_t ticks; uint32_t told,tnow,reload,tcnt=0; reload = SysTick->LOAD; ticks = nus * (SystemCoreClock / 1000000); told=SysTick->VAL; while(1) { tnow=SysTick->VAL; if(tnow!=told) { if(tnow<told) tcnt+=told-tnow; else tcnt+=reload-tnow+told; told=tnow; if(tcnt>=ticks)break; } } }
停止信号
-
时序图见上图
-
当 SCL 是高电平时 ,SDA 线由低电平向高电平切换表示停止信号
-
程序如下:
void A_I2C_Stop(void) { SDA_LOW; SCL_High; delay_us(2); SDA_High; delay_us(2); }
应答信号
-
应答信号时序图:
-
作为数据接收端时,当接收到一个字节也就是8位数据后,在第九位需要发送应答或者非应答信号,如果想继续接收则发送应答信号,否则发送非应答信号;
-
此时的数据发送端会释放SDA线,将SDA线拉成高电平;
-
由应答信号的时序图可知,高电平为非应答,低电平为应答。
-
程序如下:
void A_I2C_Ack(void) { SCL_LOW;//先拉低SCL线,使SDA进行数据的变换 SDA_LOW; //拉低SDA,产生应答信号 delay_us(2); SCL_High; //拉高SCL使SDA数据有效 delay_us(2); SCL_LOW; //先拉低SCL线,使SDA进行数据的变换 SDA_High; //释放SDA }
非应答信号
-
非应答信号的原理和应答信号的差不多,代码如下:
void A_I2C_NAck(void) { SCL_LOW;//先拉低SCL线,使SDA进行数据的变换 SDA_High;//拉高SDA,产生非应答信号 delay_us(2); SCL_High; delay_us(2); SCL_LOW; SDA_High; }
等待应答信号
-
等待应答信号是发送端发送完数据后,等待接收端给出的应答信号,如果在一定的时间内没有等到应答信号,则通信失败
-
代码如下:
uint8_t I2C_Wait_Ack(void) { uint8_t times = 0; SCL_LOW;//先拉低SCL线,使SDA进行数据的变换 delay_us(1); SCL_High;//拉高SCL使SDA数据有效 delay_us(1); while (HAL_GPIO_ReadPin(OLED_SDA_GPIO_Port,OLED_SDA_Pin)) { if (++times > 250) { A_I2C_Stop(); return 1; } } SCL_LOW;//先拉低SCL线,使SDA进行数据的变换 delay_us(2); return 0; }
写一个字节
-
SCL的每一个高电平,SDA发送一个比特的数据,所以一个字节需要循环发送8次
void I2C_write(uint8_t date) { uint8_t i, temp; temp = date; SCL_LOW; delay_us(2); for(i = 0; i < 8; i++) { /*移位发送*/ if (((temp << i) & 0x80) == 0 ) SDA_LOW; else SDA_High; SCL_High; //拉高SCL使SDA数据有效 delay_us(2); SCL_LOW; //先拉低SCL线,使SDA进行数据的变换 delay_us(2); } }
读一个字节
-
原理和发送字节差不多,需要在定义一个变量来存取读来的数据
uint8_t I2C_read(void) { uint8_t i, temp = 0; for(i = 0; i < 8; i++) { SCL_High; delay_us(2); temp <<= 1; /*stm32在输出模式下是可以正确读取引脚的数值的,这和GPIO的内部寄存器结构有关*/ if(HAL_GPIO_ReadPin(OLED_SDA_GPIO_Port,OLED_SDA_Pin) == 1) { temp |= 0x01; } SCL_LOW; delay_us(5); } return temp; }
向寄存器写命令
-
实质就是发送寄存器的地址,再发送命令,本质还是发送数据
void A_I2C_WriteByte(uint8_t slaveaddr,uint8_t registeraddr,uint8_t data) { A_I2C_Start(); I2C_write(slaveaddr); I2C_Wait_Ack(); I2C_write(registeraddr); I2C_Wait_Ack(); I2C_write(data); I2C_Wait_Ack(); }
向其他设备发送一串字节
-
里面会涉及到I2C的报文格式,可以看看这一篇I2C的协议层
void A_I2C_WriteByteS(uint8_t slaveaddr,uint8_t registeraddr,uint8_t *pbuffer,uint16_t num) { uint16_t t; A_I2C_Start(); I2C_write(slaveaddr); I2C_Wait_Ack(); I2C_write(registeraddr); I2C_Wait_Ack(); for(t=0;t<num;t++) { I2C_write(*(pbuffer)+t); I2C_Wait_Ack(); } A_I2C_Stop(); }
从其他设备读取一串字节
-
原理和上面差不多
void A_I2C_ReadBytes(uint8_t slaveaddr,uint8_t registeraddr,uint8_t *pbuffer,uint16_t num ) { uint16_t t; A_I2C_Start(); I2C_write(slaveaddr); I2C_Wait_Ack(); I2C_write(registeraddr); I2C_Wait_Ack(); A_I2C_Start(); I2C_write(slaveaddr+1); I2C_Wait_Ack(); for(t=0;t<num;t++) { *(pbuffer+t)=I2C_read(); if (t== num-1) { A_I2C_Ack(); } else { A_I2C_NAck(); } } A_I2C_Stop(); }
如有错误的地方,还望大家多多批评指正!