IIC软件模拟---从机的实现

一、前言

  • 在网上找了半天,都没怎么看到很多关于IIC软件模拟,写从机的,一咬牙就自己尝试写了。在这个过程中学到了许多新东西(都是踩坑踩出来的),我记忆力差防止后面忘记了趁着梳理知识点,做个记录总结。

  • IIC通信中,从机与主机之间的通信,无非在于2点,接收数据和发送数据,而这就在时序上有着严密的要求。这篇文章主要以从机的视角出发,如何根据SCL上高低电平的变化来正确的实现接收和发送数据。

二、正文

  • 关于SCL的控制,永远由主机来控制SCL电平的变换。
  • 外部中断用途介绍:

          SCL的上升沿---接收数据

          SDA的上升沿---检测停止信号

          SDA的下降沿 ---检测启动信号

  • 关于枚举根据自己需求定义:
typedef enum{
    IDLE = x1,
    ADDR = x2,
    RECEIVING = x3,
    SENDING = x4
}Slave_State;
Slave_State state;
  • 基础函数:
void IIC_W_SDA(uint8_t value)   
{
  GPIO_WriteBit(SDA_GPIO,SDA_PIN,(BitAction)value);   
}

uint8_t IIC_R_SDA()
{
   return GPIO_ReadInputDataBit(SDA_GPIO,SDA_PIN);  
}

void IIC_W_SCL(uint8_t value)
{
  GPIO_WriteBit(SCL_GPIO,SCL_PIN,(BitAction)value);   
}

uint8_t IIC_R_SCL()
{ 
   return GPIO_ReadInputDataBit(SCL_GPIO,SCL_PIN);     
}
  • 配置IO口模式
//配置SDA为输出
 void SDA_Out(void)
 {
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = SDA_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;  // 开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SDA_GPIO, &GPIO_InitStructure); 
 }
//配置SDA为输入
 void SDA_In(void)
 {
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = SDA_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(SDA_GPIO, &GPIO_InitStructure);
 }

2.1 数据的接收:

  • 读操作:当SCL:1,读取SDA上电平,这就需要从机捕捉SCL上的电平变化,那么如何捕捉电平的变换呢?
    这就需要用到IO口的外部中断来实现:捕捉SCL的上升沿
    //SCL    
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource8); //IO口与中断线的映射(选择)    
    EXTI_InitStruct8.EXTI_Line=EXTI_Line8; //配置中断触发模式
    EXTI_InitStruct8.EXTI_Mode=EXTI_Mode_Interrupt;
    EXTI_InitStruct8.EXTI_Trigger=EXTI_Trigger_Rising;
    EXTI_InitStruct8.EXTI_LineCmd=ENABLE;    
    EXTI_Init(&EXTI_InitStruct8);
    EXTI_ClearITPendingBit(EXTI_Line8);//清除初始化造成的标志位
  • 每次上升沿中断,能获取到的就是SDA线上的高低电平,也就相当于1bit,而一个字节8bit组成,因此需要中断8次才能获取一个完成的字节数据。
  • 这就需要1个静态变量(recv_byte )来存储每一次的高低电平,1个静态变量(bit_count)记录接收的位数。
     uint8_t sda = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9);
     recv_byte <<= 1;
     recv_byte |= sda;
     bit_count++;
     if(bit_count == 8){   //接收完成1个字节数据
        ......
     }
  • 在此说明:关于起始、停止信号的检测及数据的接收都是在外部中断函数中进行的,为了更加清楚的讲述,下面将其进行分开讲解。
void EXTI9_5_IRQHandler(void)
{   
    if(EXTI_GetITStatus(EXTI_Line8) != RESET){//数据接收
        EXTI_ClearITPendingBit(EXTI_Line8);
        if(state == ADDR || state == RECEIVING){ //state状态机
           //此处进行数据1bit的接收
            uint8_t sda = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9);
            Rx_byte <<= 1;
            Rx_byte |= sda;
            bit_count++;
            if(bit_count == 8){   //接收完成1个字节数据
              if(state == ADDR){
                 //判断地址是否正确。
                 //判断是读还是写:
                // ....详情看下面....
              }else if(state == RECEIVING){ //存储数据
                  buffer[i++] = Rx_byte;
              }     
            }
        }
    }
    if(EXTI_GetITStatus(EXTI_Line9) != RESET) {   //判断起始及停止信号
        EXTI_ClearITPendingBit(EXTI_Line9); 
       // ....详情看下面....
    }
}
  • 在IIC主从通信之间,每一次通话的发起都会由起始信号发起,停止信号结束,当SCL为高电平时,SDA由1-->0,为起始信号,SDA由0-->1为停止信号。

  • 所以从机还需要捕捉SDA的上下降沿,这也就要用到IO口的外部中断。

    //SDA
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource9); //IO口与中断线的映射(选择)      
    EXTI_InitStruct9.EXTI_Line=EXTI_Line9; //配置中断触发模式
    EXTI_InitStruct9.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStruct9.EXTI_Trigger=EXTI_Trigger_Rising_Falling;
    EXTI_InitStruct9.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStruct9);
    EXTI_ClearITPendingBit(EXTI_Line9);//清除初始化造成的标志位
  • SDA的下降沿时,SCL保持为高电平则为起始信号:

  • SDA的上升沿时,SCL仍然保持高电平则为停止信号:

  • 此处由于上升沿和下降沿都会触发中断产生,所以要区分具体是哪种类型触发的中断,就需要结合上一次的SDA的状态来判断,例如下图的last_sda(保存SDA上次的状态):

1.若last_sda :1,sda : 0 则判断为下降沿,这时scl:1则可以确定检测到起始信号。
2. 若last_sda :0,sda : 1 则判断为上升沿,这时scl:1则可以确定检测到停止信号。

  //起始停止信号的判断
    if(EXTI_GetITStatus(EXTI_Line9) != RESET) {   //判断起始及停止信号
        uint8_t scl = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8);
        uint8_t sda = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9);        
        static uint8_t last_sda = 1;
        EXTI_ClearITPendingBit(EXTI_Line9);              
        // START 条件:SDA 下降沿,且 SCL 为高
        if(last_sda == 1 && sda == 0 && scl == 1){
            state = ADDR;
            bit_count = 0;
            Rx_byte = 0;
        }else if(last_sda == 0 && sda == 1 && scl == 1) { //STOP
            state = IDLE;
            bit_count = 0;
            Rx_byte = 0;
            current_index = 0;           
        }
        last_sda = sda;       
    }
  • 在IIC的通信中,一段完整的通信格式:起始信号--地址(7bit)+读写(1bit)--应答--数据--停止信号。
  • 这么多不同类型的数据,就需要状态机来记录当前过程的状态例如下段代码中的state;
  • 在检测到起始信号后,会将状态置为state=ADDR,表示接下来接收的数据应为地址
 if(last_sda == 1 && sda == 0 && scl == 1){
    state = ADDR;
    bit_count = 0;
    Rx_byte = 0;
 }
  • 接收地址:若地址正确,则根据读写位,将状态state更改位RECEIVING、SENDING、若地址不匹配则改为IDLE;
 if(state == ADDR){ 
     if((Rx_byte>>1) == 地址){                                        
        if(Rx_byte & 0x01){
           state = SENDING; 
           send_flag = SENDING;//发送数据标志位     
        }else {
           state = RECEIVING;
        }
        IIC_SendAck(0);
    }else { //地址不正确
        IIC_SendAck(1);
        state = IDLE;
    }                   
 }else if(state == RECEIVING) {
   //数据存储
 }
  • 应答:在接收一个字节数据后,需要发送应答
    在SCL为低电平时,将SDA拉低,下一个SCL为低电平时,从机释放SDA(SDA拉高)
/* 发动应答 */
void IIC_SendAck(uint8_t Ackbit)
{
    SDA_Out();//IO口切换为输出
    while(IIC_R_SCL()==1);//等待SCL低电平 
    IIC_W_SDA(Ackbit);//Ackbit:0 拉低SDA,准备ACK  1:非应答
    while( IIC_R_SCL()==0); //等待主机升高SCL    
    //while(IIC_R_SCL()==1);//等待SCL低电平---为正确时序段释放SDA做准备    
    SDA_In(); //释放SDA,回到输入模式(上拉输入)   
    EXTI_ClearITPendingBit(EXTI_Line8); //清除SCL上升沿触发的中断标志位
    EXTI_ClearITPendingBit(EXTI_Line9); //清除将IO口改为输入模式时置为的中断标志位  
}

 
2.2 数据的发送:

  • 数据发送:数据发送是将每个字节数据1bit按SCL节奏(SCL:0)发送到SDA上,这就需要考虑SCL电平的变化:
  • 数据的发送:SCL稳定在低电平时,改变SDA的高低电平。
  • 发送完一个字节后数据,需要留一个SCL信号读取主机返回的响应。
    *关于发送数据时,检测SCL电平的变化,用while最合适,用上升沿,下降沿不好用。
//发送数据
 void Send_Host_data(uint8_t *arry,uint8_t len)
{
    uint8_t i;
    SDA_Out();//IO口切换为输出
    for(i=0;i<len;i++){
       uint8_t j;
       for(j=0;j<8;j++){  //单个字节的读取
         while(IIC_R_SCL()==1);//等待SCL稳定在低电平  
         IIC_W_SDA((arry[i] & (0x80 >> j)));
         while( IIC_R_SCL()==0); //等待主机升高SCL    
       }            
       //接收应答
       while( IIC_R_SCL()==1); //等待下一个低电平 
       IIC_W_SDA(1);//释放SDA的控制权,SDA空闲      
       while(IIC_R_SCL()==0);//等待SCL拉高来接收应答
       //读取SDA上的电平      
    }
    send_flag  = IDLE;//清除标志位
    EXTI_ClearITPendingBit(EXTI_Line8); //清除SCL上升沿触发的中断标志位
    EXTI_ClearITPendingBit(EXTI_Line9); //清除将IO口改为输入模式时置为的中断标志位   
}

2.3 主函数逻辑
主函数里面主要进行初始化配置,及根据状态机state状态来发送数据

int main(void)
{	
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 
    delay_init();
    Initial_Configuration()   
    while(1)
    { 
       if(state == SENDING){ 
          if(send_flag){ 
             if(数据类型1){
               Send_Host_data(sendbuffer,len);
             } else if(数据类型2)){
               Send_Host_data(sendbuffer,len);
             } ... 
          }                   
       }             
    }      
}

三、总结

  • 关于写IIC的从机,需要非常注意时序,错一个,就会导致接收数据不对,调试过程中最好借助类似与示波器的工具。

  • 同时要注意IO电平切换是的延时,否则中间几个电平变换,最后就执行了一个电平。

  • SCL的电平变换由主设备控制节奏。---这个也是我最开始写从机时思考的问题。

  • 从机需要捕捉SDA和SCL的电平变化(外部中断实现),来实现数据的接收,停止及启动信号的检测。

  • IO口开漏输出模式下同时可以读取IO电平。

四、声明
    在这个过程中,我也查看过其他博主的文章做参考找灵感,在此表示感谢!本文意在将此次自己所学的进行知识梳理,同时也希望能给其他同样正在写IIC从机这个方面的人提供一些参考,文章整理不易,引用时希望指明出处,谢谢。

posted @ 2025-06-06 16:35  骑着自行车,上月球  阅读(579)  评论(0)    收藏  举报