GPIO模拟IIC过程中对IIC的理解

谈谈个人在用GPIO模拟IIC过程中对IIC的理解
前两天在配置一款数字陀螺仪的IIC时序,分别实现了使用芯片自带IIC模块配置和使用GPIO模拟,下面谈谈个人对IIC的一些理解。

概述

  1. IIC是一个总线协议,和spi类似的一个串行总线协议,它的特点是只用的信号线特别少,只需要两根,分别是SCL和SDA,但是与此同时也降低了通信速度。
  2. IIC总线信号线少就造成它的通信协议稍微复杂,理解IIC的时序是理解IIC总线的关键。
  3. .IIC总线通过上拉电阻接到VDD,当总线空闲时,两根信号线均为高电平。连接总线的任意一个器件输出低电平,将使总线信号变低,即各器件的信号线是“线与”关系。
  4. 每个IIC上连接的器件都有唯一的地址(7bit),当某个器件发出广播(地址信号)时,该器件即为主机,其它器件为从机,读写行为都由主机发起,SCL信号总由主机产生。
  5. .当有多个器件企图发起广播传送数据时,需要总线仲裁模块进行仲裁。
  6. .在SCL为高电平期间,数据线上的数据必须保持稳定(0或者1),数据接收者会在这个期间接受数据,在SCL为低电平期间可以改变数据(改变SDA的电平)。
  7. 读时序:主机发起通信start(7bit从器件ID+1bit读写位0)——从机收到广播并回应(ack)——主机发送地址信号(8bit)——主机重新发起通信start(7bit从器件ID+1bit读写位0)——再次发送从器件寄存器地址address(8bit)——接收数据(SCL高电平期间从器件不会改变SDA,在此期间读SDA,将SCL拉低后从器件会将下一位数据送到SDA上)——每8bit主机需要发送一个应答信号表示继续接收,否则认为结束接收——接收结束发送无应答信号通知从器件完成数据接收——发送停止信号stop——一次通信结束写时序:主机发起通信start(7bit器件ID+1bit读写位1)——从机接收到广播并回应ack——主机写从机寄存器地址(8bit)——从机应答——写数据(8bit)——从机应答——主机发送停止信号stop——一次通信结束
  8. 两次通信之间应该有足够的时间间隔,不同器件的时间间隔不一样,一般为us数量级
  9. 当SCL=1期间,SDA从1变为0表示start信号当SCL=1期间,SDA从0变为1表示stop信号应答期间输出0表示有应答,输出1表示无应答接受应答期间接收0表示有应答,接收1表示无应答
  10. K60上使用LPLD底层库实现GPIO模拟IIC的代码如下,该代码是用于对GY_50数字陀螺仪的读写

    Created with Raphaël 2.1.0 master master salver salver start信号 发送data ack()应答 stop信号 数据线空闲时候才能启动

    start信号

    SCL为1期间SDA从1变为0表示开始信号

//开始信号
void GY_50_Start(void)
{                                                         
    LPLD_GPIO_Output_b(GY_50_SDA_PORT_ID,GY_50_SDA_PORT_NUM,1);
    LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID,GY_50_SCL_PORT_NUM,1);

    GY_50_Delay(1);

    LPLD_GPIO_Output_b(GY_50_SDA_PORT_ID,GY_50_SDA_PORT_NUM,0);

    GY_50_Delay(1);
    LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID,GY_50_SCL_PORT_NUM,0);
}

停止信号

SCL为1期间SDA从1变为0表示结束信号

//停止信号
void GY_50_Stop(void)
{
  LPLD_GPIO_Output_b(GY_50_SDA_PORT_ID, GY_50_SDA_PORT_NUM,0);
  LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,1);
  GY_50_Delay(1);
  LPLD_GPIO_Output_b(GY_50_SDA_PORT_ID, GY_50_SDA_PORT_NUM,1); 
  GY_50_Delay(1);
}

发送应答信号

向数据线上传送一位数据表示有应答或者无应答

//发送应答信号(0:ACK 1:NAK)
void GY_50_SendAck(uint8 ack)
{
  ack= ack?1:0;
  LPLD_GPIO_Output_b(GY_50_SDA_PORT_ID, GY_50_SDA_PORT_NUM,ack);
  LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,1);
  GY_50_Delay(1);
  LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,0);
  GY_50_Delay(1);
}

接收应答信号

从数据线上读取一位数据查看是否应答

//接收应答信号(0:ACK 1:NAK)
uint8 GY_50_ReceiveAck(void)
{
  uint8 data_temp=0;
  LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,1);
  GY_50_Delay(1);
  SetGpioPortIO(GY_50_SDA_PORT_ID,GY_50_SDA_PORT_PIN,DIR_INPUT);
  data_temp = LPLD_GPIO_Input_b(GY_50_SDA_PORT_ID, GY_50_SDA_PORT_NUM);
  SetGpioPortIO(GY_50_SDA_PORT_ID,GY_50_SDA_PORT_PIN,DIR_OUTPUT);
  LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,0);
  GY_50_Delay(1);
  return data_temp;
}

向IIC总线发送一个字节的数据

按照协议要求,在SCL为低期间改变数据,在SCL为高期间保持数据,并保持一定时间,保证对方读取完毕

//向IIC发送一个字节的数据
void GY_50_SendByte(uint8 data)
{
  static uint8 i,data_temp;
  for(i=0;i<8;i++)
  {
    data_temp = data&0x80;//取最高位
    data <<= 1;
    data_temp = data_temp?1:0;

    LPLD_GPIO_Output_b(GY_50_SDA_PORT_ID, GY_50_SDA_PORT_NUM,data_temp);
    LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,1);
    GY_50_Delay(1);
    LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,0);
    GY_50_Delay(1);
  }
  GY_50_ReceiveAck();
}

从IIC接收一个字节数据

根据协议要求,在SCL为高期间读取数据,在SCL为低期间让对方发送数据,SCL为低要保持一定时间,保证数据线上的数据稳定

//从IIC接收一个数据
uint8 GY_50_ReceiveByte(void)
{
  static uint8 i,data_temp=0;
  LPLD_GPIO_Output_b(GY_50_SDA_PORT_ID, GY_50_SDA_PORT_NUM,1);
  SetGpioPortIO(GY_50_SDA_PORT_ID,GY_50_SDA_PORT_PIN,DIR_INPUT);
  for(i=0;i<8;i++)
  {
    data_temp <<= 1;
    LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,1);
    GY_50_Delay(1);
    data_temp |= LPLD_GPIO_Input_b(GY_50_SDA_PORT_ID, GY_50_SDA_PORT_NUM);
    LPLD_GPIO_Output_b(GY_50_SCL_PORT_ID, GY_50_SCL_PORT_NUM,0);
    GY_50_Delay(1);
  }
  SetGpioPortIO(GY_50_SDA_PORT_ID,GY_50_SDA_PORT_PIN,DIR_OUTPUT);
  //GY_50_Delay(1);
  return data_temp;
}

写一个字节

根据协议要求,串行传送信号

//单字节写入
void GY_50_WriteByte(uint8 add, uint8 data)
{
  GY_50_Start();
  GY_50_SendByte(GY_50_ADDRESS);
  GY_50_SendByte(add);
  GY_50_SendByte(data);
  GY_50_Stop();
}

读单个字节

根据协议要求,串行传送信号和指令

//单字节读取
uint8 GY_50_ReadByte(uint8 add)
{
  uint8 data_temp;
  GY_50_Start();
  GY_50_SendByte(GY_50_ADDRESS);
  GY_50_SendByte(add);
  GY_50_Start();
  GY_50_SendByte(GY_50_ADDRESS|0x01);
  data_temp = GY_50_ReceiveByte();
  GY_50_SendAck(1);
  GY_50_Stop();

  return data_temp;
}

延迟函数

该函数在总线周期为50M的K60芯片上大约延迟4us(k=1)

//延时函数
void GY_50_Delay(uint8 k)
{
  uint16 i,j;
  for(i=0;i<k;i++)
  {
    for(j=0;j<200;j++)
    {
      asm("nop");
    }
  }
}

注:

  1. SetGpioPortIO(GPIO_Type *portx,uint32 pins,uint8 io)为一个改变GPIO引脚数据传送方向的函数。

希望以上对大家的学习有所帮助

注:个人才疏学浅,难免有疏漏之处,还望批评指正

在C语言GPIO (General Purpose Input Output) 模拟 IIC 可能是指通过 GPIO 端口来实现 I2C 总线通信的一种做法。通常,微控制器硬件上自带了专门用于 I2C 的模块,但在某些情况下,开发者可能会选择只利用 GPIO 来实现基本的 I2C 功能,这需要自定义驱动和协议处理。 ### C语言实现IIC的基本步骤 1. **初始化IIC**: - 配置GPIO作为I2C总线的SCL和SDA信号线。 - 设置定时器或断机制来处理数据的发送和接收时间。 2. **生成起始和停止条件**: - 发送起始条件(0x00)来开始传输。 - 发送设备地址加上读写标志(读写命令),并等待应答。 3. **发送和接收数据**: - 使用循环结构来轮流发送和接收数据字节。 - 对于每个数据字节,先发送或接收开始位(0),然后是8位数据位。 4. **终止交易**: - 发送结束条件(0xFF)来结束交易。 ### 示例代码结构 假设我们有一个简单的函数 `send_i2c_message` 来实现这个过程: ```c #include <avr/io.h> // 引入AVR寄存器操作库 void send_i2c_message(unsigned char device_address, unsigned char read_write_bit, unsigned char data[], int length) { unsigned char i; // 初始化SCL和SDA GPIO端口 DDRD |= (1 << PD6); // SCL为输出 PORTD &= ~(1 << PD6); DDRE |= (1 << PE2); // SDA为输出 PORTE &= ~(1 << PE2); // 生成起始条件 SCL = HIGH; // 将SCL设置高电平 delayMicroseconds(100); // 给器件一些初始化时间 SDA = LOW; // 将SDA拉低,发送起始条件 SCL = LOW; delayMicroseconds(1); // 停留片刻以便接收方准备好 SCL = HIGH; // 写入设备地址和读写标志 for(i=7; i>=0; i--) { if((device_address >> i) & 0x01) { // 判断地址的每一位是否为1 SDA ^= 1; // 如果为1,则将SDA拉高表示1;如果为0,则保持SDA的状态 delayMicroseconds(1); // 等待 } SCL = LOW; delayMicroseconds(1); SCL = HIGH; delayMicroseconds(1); } // 添加读写标志 if(read_write_bit == READ) { SDA ^= 1; // 读操作标志 } else { SDA = 0; // 写操作标志 } SCL = LOW; delayMicroseconds(1); SCL = HIGH; // 发送数据 for(i=0; i<length; i++) { while(!bit_is_set(SDA)); // 等待SDA为低电平 SCL = LOW; // 开始位 for(int j=7; j>=0; j--) { SDA ^= 1; // 根据data[i]的第j位值改变SDA状态 SCL = HIGH; delayMicroseconds(1); SCL = LOW; delayMicroseconds(1); } SCL = HIGH; // 数据发送完成,进入下一个位 while(bit_is_set(SDA)); // 等待SDA回到低电平 } // 释放资源 PORTD |= (1 << PD6); // SCL变为输入 PORTE |= (1 << PE2); // SDA变为输入 // 结束条件 SCL = HIGH; delayMicroseconds(100); SDA = HIGH; delayMicroseconds(500); } // 定义一个辅助函数来判断GPIO引脚状态 int bit_is_set(unsigned char pin) { return (PIND & (1 << pin)) != 0; // 返回非零值,说明引脚为高电平 } ``` ### 注意事项和潜在改进点 - 这是一个非常基础的示例,实际应用可能需要添加更多的错误检查、时序调整以及更复杂的控制逻辑。 - 对于不同的微控制器平台,GPIO配置和操作细节会有所不同。 - 实际编码时需考虑时钟同步、数据冲突等问题,并可能需要用到定时器断或其他高级功能来优化性能。 ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值