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,主機收到發送到電腦上結果爲: