• <li id="00i08"><input id="00i08"></input></li>
  • <sup id="00i08"><tbody id="00i08"></tbody></sup>
    <abbr id="00i08"></abbr>
  • 新聞中心

    EEPW首頁 > 嵌入式系統(tǒng) > 設(shè)計應(yīng)用 > 看時序圖寫I2C驅(qū)動,教你如何自己手?jǐn)]非標(biāo)I2C驅(qū)動函數(shù)

    看時序圖寫I2C驅(qū)動,教你如何自己手?jǐn)]非標(biāo)I2C驅(qū)動函數(shù)

    作者: 時間:2025-08-18 來源:硬件筆記本 收藏

    很多人不知道怎么看著圖寫程序,下面結(jié)合一個非標(biāo)準(zhǔn)的器件,教大家如何寫一個高效的IO模擬


    觀察該,具備的開始信號,I2C的結(jié)束信號,I2C的應(yīng)答、非應(yīng)答、響應(yīng)應(yīng)答,以及寫字節(jié)和讀字節(jié)的基本操作時序。


    下面,我們一步一步分析。


    1、I2C開始信號


    觀察時序圖,在SCLK高電平的狀態(tài)下,在SDIO產(chǎn)生一個下降沿是為開始信號。


    void I2C_Start()
    {  //設(shè)置I2C使用的兩個引腳為輸出模式
      pinMode(SCLK_PIN, OUTPUT);  pinMode(SDIO_PIN, OUTPUT);  //在SCL為高電平的時候讓SDA產(chǎn)生一個下降沿是為開始信號
      digitalWrite(SDIO_PIN, 1);  digitalWrite(SCLK_PIN, 1);  digitalWrite(SDIO_PIN, 0);
    }


    上述代碼即先將兩個引腳設(shè)置為輸出模式,然后在SCLK為高電平的時候在SDIO引腳輸出一個下降沿。


    2、I2C停止信號


    觀察時序圖,在SCLK為高電平的時候在SDIO引腳產(chǎn)生一個上升沿是為停止信號。


    void I2C_Stop()
    {  pinMode(SDIO_PIN, OUTPUT);  //在SCL為高電平的時候讓SDA產(chǎn)生一個上升沿是為停止信號
      digitalWrite(SDIO_PIN, 0);  digitalWrite(SCLK_PIN, 1);  digitalWrite(SDIO_PIN, 1);
    }


    這里采用的是Arduino編寫的IO基本操作,你可以替換成任意單片機的IO操作。


    由于整個過程SCLK引腳一直是輸出狀態(tài),所以僅在開始信號中對SCLK初始化為輸出模式,而過程中可能會修改SDIO的輸入輸出模式,所以其他的函數(shù)開頭都要根據(jù)情況對SDIO引腳的模式進行設(shè)置。


    通過三行代碼實現(xiàn)在SCLK為高電平的時候在SDIO產(chǎn)生一個上升沿,實現(xiàn)停止信號。


    3、寫字節(jié)操作


    接下來,按照時序的順序編寫方便認(rèn)讀


    I2C的讀寫字節(jié)是這么定義的:當(dāng)時鐘線為低電平的時候,允許修改數(shù)據(jù)線的電平狀態(tài),在時鐘線為高電平的時候讀取數(shù)據(jù)線的狀態(tài)。


    因為是寫操作,因此我們要先將時鐘線SCLK拉低,再修改SDIO的值,然后拉高時鐘。拉高后,從機就會從總線上讀取SDIO的狀態(tài),接著一位一位的這么發(fā)送。


    void I2C_Write(uint8_t dat){
      pinMode(SDIO_PIN, OUTPUT);  //拉低時鐘線后可修改數(shù)據(jù)線的狀態(tài)
      digitalWrite(SCLK_PIN, 0); 
      for(int i=0;i<8;i++)
      {
        digitalWrite(SDIO_PIN, (bool)(dat&0x80)); 
        digitalWrite(SCLK_PIN, 1);//在高電平時候送出數(shù)據(jù)
        dat=dat<<1;
        digitalWrite(SCLK_PIN, 0);//拉低準(zhǔn)備下一個位的數(shù)據(jù)發(fā)送
      }
    }


    上述代碼正描述了這一情況:為了保證最后是低電平,這里將SCLK的第一次拉低放到循環(huán)外面,這樣可以用最少的執(zhí)行次數(shù)完成一個字節(jié)的寫任務(wù);同時,結(jié)束完一個字節(jié)寫入后時鐘線是低電平狀態(tài)(時序圖中寫入的第一個字節(jié)為DeviceID,第二個字節(jié)為寄存器地址+讀寫位)。


    寫完一個字節(jié)后,從機會對寫入事件進行應(yīng)答,這個時候主機可以從總線上讀取應(yīng)答信號。


    4、讀取從機應(yīng)答引號


    應(yīng)答信號在寫完一個字節(jié)的低電平后由從機送出,在時鐘為高電平的時候可以讀取出來,我們注意到寫字節(jié)操作后時鐘線已經(jīng)是低電平了,因此這個時候


    只要拉高時鐘線,接下來就可以讀取應(yīng)答信號,讀取應(yīng)答信號根據(jù)時序圖應(yīng)該拉低時鐘準(zhǔn)備下一個字節(jié)的寫入。


    bool I2C_RACK(){  bool ack;
      pinMode(SDIO_PIN, INPUT);
    
    
      digitalWrite(SCLK_PIN, 1);//接收應(yīng)答信號,當(dāng)時鐘拉高時候,從機送出應(yīng)答信號
      ack = digitalRead(SDIO_PIN);
      digitalWrite(SCLK_PIN, 0);//讀取完應(yīng)答信號后拉低時鐘。
      return ack;
    }


    如上代碼所示,即為接收從機應(yīng)答,拉高時鐘,讀取應(yīng)答,再拉低,返回應(yīng)答。如果從機應(yīng)答了,這里會讀取到一個低電平。


    后面就是再寫入一個寄存器+讀寫位的地址,參靠上面的寫入操作。


    寫入寄存器地址后,緊跟著又一個接收從機應(yīng)答信號,然后從機就會送出數(shù)據(jù),送出的數(shù)據(jù)分高字節(jié)和低字節(jié),高低字節(jié)間要有一個主機發(fā)送給從機的應(yīng)答信號,這樣從機就知道主機收到了數(shù)據(jù),就會送出后面的低字節(jié)數(shù)據(jù)。


    5、讀字節(jié)操作


    注意,前面說過,讀寫都是總線在時鐘低電平時候修改數(shù)據(jù)線,在高電平送出。


    因此,主機讀取從機送來的數(shù)據(jù)仍然是在高電平時候讀取。


    uint8_t I2C_Read()
    {  uint8_t dat=0;
      pinMode(SDIO_PIN, INPUT);  for(int i=0;i<8;i++)
      {
        digitalWrite(SCLK_PIN, 1);//讀取數(shù)據(jù)時候是在時鐘的高電平狀態(tài)讀取
        dat=dat<<1;    if(digitalRead(SDIO_PIN))
        {
          dat=dat|1;
        }
        digitalWrite(SCLK_PIN, 0);//拉低時鐘線準(zhǔn)備下一個位的讀取
      }  return dat;
    }


    操作過程是將SDIO數(shù)據(jù)線的IO設(shè)置為輸入模式,準(zhǔn)備讀取,然后拉高時鐘,讀取數(shù)據(jù),移位,拉低循環(huán)讀取8位數(shù)據(jù)。


    注意,操作完一個字節(jié)讀取任務(wù)后,時鐘線還是低電平。


    讀取完一個字節(jié)后,主機要給從機發(fā)送一個應(yīng)答信號,這樣從機會接著發(fā)低字節(jié)數(shù)據(jù)。


    6、主機發(fā)送應(yīng)答信號給從機


    void I2C_ACK()
    {pinMode(SDIO_PIN, OUTPUT);digitalWrite(SDIO_PIN, 0);//給從機發(fā)送應(yīng)答信號,即拉低數(shù)據(jù)線,然后拉高時鐘讓從機讀取該應(yīng)答digitalWrite(SCLK_PIN, 1);digitalWrite(SCLK_PIN, 0);//執(zhí)行完應(yīng)答后拉低時鐘線,準(zhǔn)備下一步動作。}


    拉低數(shù)據(jù)線,然后在高電平的時候讓從機去讀取,之后拉低時鐘線準(zhǔn)備下一步接收動作。


    當(dāng)再接收一個字節(jié)后,就讀取完成了,這個時候就是產(chǎn)生一個非應(yīng)答信號,然后發(fā)給總線結(jié)束信號,告訴從機一個讀寫周期結(jié)束了。


    7、主機非應(yīng)答信號


    什么是非應(yīng)答信號呢?


    就是接收完了數(shù)據(jù),釋放數(shù)據(jù)線,不去拉低數(shù)據(jù)線。


    void I2C_NACK()
    {  //非應(yīng)答信號:即主機不再對從機進行應(yīng)答,主機釋放數(shù)據(jù)線,即拉高數(shù)據(jù)線,然后給時鐘一個周期信號(拉高再拉低)
      pinMode(SDIO_PIN, OUTPUT);  digitalWrite(SDIO_PIN, 1);  digitalWrite(SCLK_PIN, 1);  digitalWrite(SCLK_PIN, 0);
    }


    將SDIO引腳設(shè)置為輸出,拉高數(shù)據(jù)線,即為釋放數(shù)據(jù)線,然后拉高拉低時鐘,即在時鐘線產(chǎn)生一個時鐘周期信號。


    然后發(fā)送結(jié)束信號。結(jié)束信號在開頭已經(jīng)講明,即在時鐘線為高電平的狀態(tài)下,在數(shù)據(jù)線產(chǎn)生一個上升沿。


    觀察以上代碼沒一個多余重復(fù)的操作動作,即完美的視線了時序圖上的所有操作。


    接下來就是利用上述的I2C成分進行對寄存器的讀寫操作了。


    8、讀寄存器


    由于圖中設(shè)備的DeviceID 為0x80,即直接寫進來,從機判斷是讀還是寫的字節(jié)在寄存器地址。


    因此,將寄存器的地址左移一位,在末尾補上是讀(1)還是寫(0)。


    uint16_t read_reg(uint8_t reg){
      uint16_t dat=0;
      reg=(reg<<1)|1;
      I2C_Start();
      I2C_Write(0x80);
      I2C_RACK();
      I2C_Write(reg);
      I2C_RACK();
      dat=I2C_Read();
      dat=dat<<8;
      I2C_ACK();
      dat=dat|I2C_Read();
      I2C_NACK(); 
      I2C_Stop();
      return dat;}


    9、寫寄存器操作


    void write_reg(uint8_t reg, uint16_t dat){
      reg=(reg<<1);
      I2C_Start();
      I2C_Write(0x80);
      I2C_RACK();
      I2C_Write(reg);
      I2C_RACK();
      I2C_Write(dat>>8);
      I2C_RACK();
      I2C_Write(dat&0xFF);
      I2C_NACK();
      I2C_Stop();
    }


    最后,對寄存器讀寫函數(shù)測試。

    void setup() 
    {  Serial.begin(115200);  Serial.println("Hello I2C");  write_reg(0x02,0x2250);  Serial.println(read_reg(0x02),HEX);  write_reg(0x02,0x2281);  Serial.println(read_reg(0x02),HEX);
    }void loop() 
    {
    
    
    }



    讀取的數(shù)值與寫入的是一樣的。


    最后曬出完整的測試代碼:


    #define SCLK_PIN 8#define SDIO_PIN 9void I2C_Start(){  //設(shè)置I2C使用的兩個引腳為輸出模式
      pinMode(SCLK_PIN, OUTPUT);
      pinMode(SDIO_PIN, OUTPUT);  //在SCL為高電平的時候讓SDA產(chǎn)生一個下降沿是為開始信號
      digitalWrite(SDIO_PIN, 1);
      digitalWrite(SCLK_PIN, 1);
      digitalWrite(SDIO_PIN, 0);
    }void I2C_Stop(){
      pinMode(SDIO_PIN, OUTPUT);  //在SCL為高電平的時候讓SDA產(chǎn)生一個上升沿是為停止信號
      digitalWrite(SDIO_PIN, 0);
      digitalWrite(SCLK_PIN, 1);
      digitalWrite(SDIO_PIN, 1);
    }void I2C_Write(uint8_t dat){
      pinMode(SDIO_PIN, OUTPUT);  //拉低時鐘線后可修改數(shù)據(jù)線的狀態(tài)
      digitalWrite(SCLK_PIN, 0); 
      for(int i=0;i<8;i++)
      {
        digitalWrite(SDIO_PIN, (bool)(dat&0x80)); 
        digitalWrite(SCLK_PIN, 1);//在高電平時候送出數(shù)據(jù)
        dat=dat<<1;
        digitalWrite(SCLK_PIN, 0);//拉低準(zhǔn)備下一個位的數(shù)據(jù)發(fā)送
      }
    }uint8_t I2C_Read()
    {  uint8_t dat=0;
      pinMode(SDIO_PIN, INPUT);  for(int i=0;i<8;i++)
      {
        digitalWrite(SCLK_PIN, 1);//讀取數(shù)據(jù)時候是在時鐘的高電平狀態(tài)讀取
        dat=dat<<1;    if(digitalRead(SDIO_PIN))
        {
          dat=dat|1;
        }
        digitalWrite(SCLK_PIN, 0);//拉低時鐘線準(zhǔn)備下一個位的讀取
      }  return dat;
    }bool I2C_RACK(){  bool ack;
      pinMode(SDIO_PIN, INPUT);
    
    
      digitalWrite(SCLK_PIN, 1);//接收應(yīng)答信號,當(dāng)時鐘拉高時候,從機送出應(yīng)答信號
      ack = digitalRead(SDIO_PIN);
      digitalWrite(SCLK_PIN, 0);//讀取完應(yīng)答信號后拉低時鐘。
      return ack;
    }void I2C_ACK(){
      pinMode(SDIO_PIN, OUTPUT);
      digitalWrite(SDIO_PIN, 0);//給從機發(fā)送應(yīng)答信號,即拉低數(shù)據(jù)線,然后拉高時鐘讓從機讀取該應(yīng)答
      digitalWrite(SCLK_PIN, 1);
      digitalWrite(SCLK_PIN, 0);//執(zhí)行完應(yīng)答后拉低時鐘線,準(zhǔn)備下一步動作。}void I2C_NACK(){  //非應(yīng)答信號:即主機不再對從機進行應(yīng)答,主機釋放數(shù)據(jù)線,即拉高數(shù)據(jù)線,然后給時鐘一個周期信號(拉高再拉低)
      pinMode(SDIO_PIN, OUTPUT);
      digitalWrite(SDIO_PIN, 1);
      digitalWrite(SCLK_PIN, 1);
      digitalWrite(SCLK_PIN, 0);
    }uint16_t read_reg(uint8_t reg)
    {  uint16_t dat=0;
      reg=(reg<<1)|1;
      I2C_Start();
      I2C_Write(0x80);
      I2C_RACK();
      I2C_Write(reg);
      I2C_RACK();
      dat=I2C_Read();
      dat=dat<<8;
      I2C_ACK();
      dat=dat|I2C_Read();
      I2C_NACK(); 
      I2C_Stop();  return dat;
    }void write_reg(uint8_t reg, uint16_t dat){
      reg=(reg<<1);
      I2C_Start();
      I2C_Write(0x80);
      I2C_RACK();
      I2C_Write(reg);
      I2C_RACK();
      I2C_Write(dat>>8);
      I2C_RACK();
      I2C_Write(dat&0xFF);
      I2C_NACK();
      I2C_Stop();
    }void setup() {
      Serial.begin(115200);
      Serial.println("Hello I2C");
      write_reg(0x02,0x2250);
      Serial.println(read_reg(0x02),HEX);
      write_reg(0x02,0x2281);
      Serial.println(read_reg(0x02),HEX);
    }void loop() {
    
    
    }


    看完這篇文章,你學(xué)會純手工擼IO模擬I2C時序的代碼了嗎?



    關(guān)鍵詞: I2C 時序

    評論


    相關(guān)推薦

    技術(shù)專區(qū)

    關(guān)閉
    主站蜘蛛池模板: 荃湾区| 满洲里市| 呼和浩特市| 筠连县| 徐闻县| 辽阳县| 巩义市| 耒阳市| 涞水县| 怀仁县| 乌鲁木齐县| 曲沃县| 肇东市| 潮安县| 万荣县| 新邵县| 正定县| 沙洋县| 西安市| 墨江| 昌图县| 长阳| 万安县| 吴江市| 宜兴市| 高邑县| 石景山区| 桑日县| 青海省| 鹤岗市| 鲜城| 滨州市| 双辽市| 蕉岭县| 银川市| 龙胜| 印江| 昂仁县| 庆安县| 德清县| 平塘县|