STM8L051的硬件I2C調試

I2C是現代一種極爲常見的低速外設通信協議,比起SPI或者UART,它最大的優勢應該就是節省芯片管腳了:理論上只要地址夠用,多少外設掛I2C總線上都沒問題,只佔兩個管腳。但也因此,I2C的協議就相對複雜一些,以面對多個外設。同時,過多的外設也使得通信速率難以提升,一般只在100kbps或以下。本文不專門介紹I2C的時序和協議,而介紹我在調試STM8L051的硬件I2C的過程以及遇到的問題,和大家分享。

我的實驗電路由兩個獨立的STM8L051模塊組成,做一發一收。這兩個模塊的電路是我自己設計的,通過排針插在麪包板上,如圖所示。這兩個芯片的硬件I2C在PC0和PC1,將他們連起來並用4.7K電阻上拉(請原諒我沒有直插電阻然後用貼片湊合的無奈T T)。右邊是做接收的模塊,將它的串口接出好觀察結果(我非常喜歡用串口調試,幾乎拿到什麼板子,第一件事情就是把串口先調出來)。
這裏寫圖片描述
首先是用庫函數進行開發還是直接寫寄存器編程的問題。因爲懶,我個人更喜歡用庫函數,這次調試也是用庫函數編程。其實我感覺意法半導體的單片機(尤其STM32)能夠流行,其設計合理的庫函數是一個關鍵原因。另外ST官方也爲庫函數寫了大量的例程,使得參考和移植都會方便。但使用庫函數其實會運行很多沒必要的代碼,以及各種函數調用,都會耗費時間和存儲資源,在資源本身就緊張8位單片機上用庫函數其實是很低效的。我一個師兄表示他在STM8上一直都是直接寫寄存器。
ST官方庫函數的例程中,有兩個板子對通的程序,他的設計是,先進行主機發送、從機接收,然後從機發送,主機接收,主機收回後比較數據,判斷傳輸是否有誤。爲了方便研究,我將例程分開,分別測試主發從收和主收從發兩個過程。

一、 主機發送,從機接收

爲了觀看傳輸結果,我會事先配置串口。串口配置程序和串口輸出字符串程序如下:

void USART_Config(void)
{
  USART_DeInit(USART1); // DeInit
  CLK_PeripheralClockConfig(CLK_Peripheral_USART1, ENABLE); // SysClk for USART1
  SYSCFG_REMAPPinConfig(REMAP_Pin_USART1TxRxPortA, ENABLE); // Remap TX on PA2 and RX on PA3
  USART_Init(USART1, (uint32_t)9600, USART_WordLength_8b, USART_StopBits_1, \
             USART_Parity_No, USART_Mode_Tx);
  USART_Cmd(USART1, ENABLE);
}
void UART_SendStr(char *str)
{
  int i = 0;
  for(i=0;str[i]!=0;i++)
  {
    while (!(USART1->SR & 0x80));   /* wait for READY */
    USART_SendData8(USART1,str[i]);
  }
}

程序中串口被remap到了PA2和PA3,這主要是因爲STM8L051芯片沒有PC2和PC3,所以必須remap。波特率設爲9600,只進行輸出,不提供中斷。

STM8L的硬件I2C在其參考手冊RM0031中有詳細的敘述(https://www.st.com/content/ccc/resource/technical/document/reference_manual/2e/3b/8c/8f/60/af/4b/2c/CD00218714.pdf/files/CD00218714.pdf/jcr:content/translations/en.CD00218714.pdf )。爲了方便,我只實現7位地址的I2C通信。
在主發從收通信中,主機會遇到的事件包括EV5(發送完START bit)、EV6(發送完從機地址並收到ACK)、EV8(TXE,發送寄存器空,即發送了一個字節)和EV8_2(發送完成)。主機在中斷中處理這些問題。I2C設置代碼如下:

void I2C_Config(void)
{
  CLK_PeripheralClockConfig(CLK_Peripheral_I2C1, ENABLE);
  I2C_DeInit(I2C1);
  I2C_Init(I2C1, 100000, 0xA0,
           I2C_Mode_I2C, I2C_DutyCycle_2,
           I2C_Ack_Enable, I2C_AcknowledgedAddress_7bit);
  I2C_ITConfig(I2C1, (I2C_IT_TypeDef)(I2C_IT_EVT | I2C_IT_BUF), ENABLE);
}

要發送數據時,I2C先發送開始符號,然後等待發送完成:

I2C_GenerateSTART(I2C1, ENABLE); // Start and into Master Mode
while(NumOfBytes); // Wait for all bytes have been transmitted

主機的中斷服務程序在官方樣例基礎上縮減:

#define SLAVE_ADDRESS 0x30
__IO uint8_t TxBuffer[32] = "Get it!\n";
__IO uint8_t NumOfBytes = 9;
__IO uint8_t Tx_Idx =0;
INTERRUPT_HANDLER(I2C1_SPI2_IRQHandler,29)
{
    switch (I2C_GetLastEvent(I2C1))
  {
      /* EV5 */
    case I2C_EVENT_MASTER_MODE_SELECT :
      /* Send slave Address for write */
      I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Transmitter);
      break;

      /* EV6 */
    case I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED:
      if (NumOfBytes != 0)
      {
        /* Send the first Data */
        I2C_SendData(I2C1, TxBuffer[Tx_Idx++]);

        /* Decrement number of bytes */
        NumOfBytes--;
      }
      if (NumOfBytes == 0)
            {
        I2C_ITConfig(I2C1, I2C_IT_BUF, DISABLE);
      }
      break;

      /* EV8 */
    case I2C_EVENT_MASTER_BYTE_TRANSMITTING:
      /* Transmit Data */
      I2C_SendData(I2C1, TxBuffer[Tx_Idx++]);

      /* Decrement number of bytes */
      NumOfBytes--;

      if (NumOfBytes == 0)
      {
        I2C_ITConfig(I2C1, I2C_IT_BUF, DISABLE);
      }
      break;

      /* EV8_2 */
    case I2C_EVENT_MASTER_BYTE_TRANSMITTED:
      /* Send STOP condition */
      I2C_GenerateSTOP(I2C1, ENABLE);
      I2C_ITConfig(I2C1, I2C_IT_EVT, DISABLE);
      break;

    default:
      break;
  }
}

從機的接收過程更爲簡單。從機設置時將I2C地址設置爲0x30(主機向0x30發送信息),其他和主機相同,然後開啓中斷等待即可。從機接收過程中遇到的事件有EV1(收到主機發送的本機地址)、EV2(收到一個字節數據)和EV4(停止傳輸)。其中斷服務程序也就是爲這些事件準備的:

__IO uint8_t Slave_Buffer_Rx[32];
__IO uint8_t Rx_Idx = 0;
__IO uint16_t Event = 0x00;
uint8_t RecvFlag = 0;

INTERRUPT_HANDLER(I2C1_SPI2_IRQHandler,29)
{
    Event = I2C_GetLastEvent(I2C1);
  switch (Event)
  {
      /******* Slave transmitter ******/
      /* check on EV1 */
    case I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED:
      break;

      /* check on EV3 */
    case I2C_EVENT_SLAVE_BYTE_TRANSMITTING:
      break;
      /******* Slave receiver **********/
      /* check on EV1*/
    case I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED:
      break;

      /* Check on EV2*/
    case I2C_EVENT_SLAVE_BYTE_RECEIVED:
      Slave_Buffer_Rx[Rx_Idx++] = I2C_ReceiveData(I2C1);
      break;

      /* Check on EV4 */
    case (I2C_EVENT_SLAVE_STOP_DETECTED):
            /* write to CR2 to clear STOPF flag */
            I2C1->CR2 |= I2C_CR2_ACK;
            RecvFlag = 1;
      break;

    default:
      break;
  }
}

程序中,我用RecvFlag標記接收完成,主程序在RecvFlag爲1時,將收到的字符串從串口發出。主機發來的是“Get it!\n”,從串口看到結果如下圖所示(發送了2次):
這裏寫圖片描述

二、主機接收,從機發送

官方例程中的主機接收代碼沒有用中斷,我這裏也如此操作,以後有時間再試試主機中斷接收。主機設置代碼爲:

void I2C_Config(void)
{
  CLK_PeripheralClockConfig(CLK_Peripheral_I2C1, ENABLE);
  I2C_DeInit(I2C1);
  I2C_Init(I2C1, 100000, 0xA0,
           I2C_Mode_I2C, I2C_DutyCycle_2,
           I2C_Ack_Enable, I2C_AcknowledgedAddress_7bit);
}

在主機接收過程中,其實傳輸進程還是主機控制的。在開始傳輸後,經歷EV5、EV6、EV7(主機接收到從機一個字節數據)和EV7_1(主機接收從機最後一個字節),在main()函數中運行如下代碼來傳輸:

while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1, ENABLE); // Start and into Master Mode
/* Test on EV5 and clear it */
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
/* Send slave Address for write */
I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Receiver);
/* Test on EV6 and clear it */
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
/* While there is data to be read */    
while(NumOfBytes)
{
  /* The last bytes need STOP but not ACK */
  if (NumOfBytes == 1)
  {
    /* Disable Acknowledgement */
    I2C_AcknowledgeConfig(I2C1, DISABLE);

    /* Send STOP Condition */
    I2C_GenerateSTOP(I2C1, ENABLE);

    /* Poll on RxNE Flag */
    while ((I2C_GetFlagStatus(I2C1, I2C_FLAG_RXNE) == RESET));
    /* Read a byte */
    RxBuffer[Rx_Idx++] = I2C_ReceiveData(I2C1);
    /* Decrement the read bytes counter */
    NumOfBytes--;
  }
  /* Test on EV7 and clear it */
  if (I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED) )
  {
    /* Read a byte */
    RxBuffer[Rx_Idx++] = I2C_ReceiveData(I2C1);
    /* Decrement the read bytes counter */
    NumOfBytes--;
  }
}

代碼中,在收到最後一個字節(NumOfBytes == 1)時將ACK Disable併發送STOP結束傳輸過程。

在從機發射端,依然使用中斷來處理髮送過程,從機設置和前一節相同。從機發射要經歷EV1、EV3(TXE=1,發送寄存器空,發送了一個字節數據)和EV3_2(AF=1,未收到ACK)中斷處理代碼如下:

INTERRUPT_HANDLER(I2C1_SPI2_IRQHandler,29)
{
/* check on EV3_2 */
if (I2C_ReadRegister(I2C1, I2C_Register_SR2))
  {
    /* Clears SR2 register */
    I2C1->SR2 = 0;
  }
Event = I2C_GetLastEvent(I2C1);
  switch (Event)
  {
      /******* Slave transmitter ******/
      /* check on EV1 */
    case I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED:
        Tx_Idx = 0;
      break;

      /* check on EV3 */
    case I2C_EVENT_SLAVE_BYTE_TRANSMITTING:
        I2C_SendData(I2C1, Slave_Buffer_Tx[Tx_Idx++]);
      break;
      /******* Slave receiver **********/
      /* check on EV1*/
    case I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED:
      break;

      /* Check on EV2*/
    case I2C_EVENT_SLAVE_BYTE_RECEIVED:
      break;

      /* Check on EV4 */
    case (I2C_EVENT_SLAVE_STOP_DETECTED):
      break;

    default:
      break;
  }
}

STM8L的從機發送的結束機制值得好好吐槽一下。我看到有網絡上帖子說主收從發只能收一次,我之前也刪掉了if (I2C_ReadRegister(I2C1, I2C_Register_SR2))這個判斷,因爲I2C_SR2其實是個錯誤寄存器,我想傳輸沒錯誤的話應該就不用管它了,然後就只能傳一次。直到再次讀手冊RM0031,看到 EV3-2: AF=1, AF is cleared by writing ‘0’ in AF bit of SR2 register.這句話,AF是Acknowledge Failure,也就是說,它其實是根據沒收到ACK來判斷傳輸結束的……將這個寄存器清零後,硬件I2C恢復初始狀態。
最後驗證,從機發送Got it!\n,主機收到發送到電腦上結果爲:
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章