之前在學習 Adruino 時,對於 I²C 的瞭解不是很清楚,偏重在其接線的方式,如 SDA 及 SCL 的連接,以及每種設備都有一個特殊的 ID 等,不是很深入的瞭解其運作原理。同樣的,剛開始學習使用 STM32 庫函式撰寫程式時,使用 SSD1306 OLED 顯示實做的結果,這個顯示器是 I²C 介面,為了要讓實做能順利進行,在網路上找了廠商提供的函式庫就繼續使用,那時還不瞭解 I²C 的通訊原理 ,本篇要對 I²C 的通訊方式進行整理,方便程式撰寫及未來查閱用。
本篇實做我使用 I²C 介面的 AHT10 溫濕度感測器來改寫 I²C 函式庫,包括 I²C 啟動、停止、傳送及接收位元等。過程中,我將 PA0 及 PA1 當作 I²C 的 SCL 及 SDA 引腳,執行程式時,得到的溫濕度都是 0,後來才發現不是每個引腳都能當作 I²C 的時鐘及資料引腳,找到我使用的開發板 STM32F103C8T6 的 Pinout 圖(請參考:STM32筆記(1):我的第一個STM32程式,使用Arduino IDE 介面)找到開發板上共有三組 I²C ,OLED 用掉一組,改選用 PB6 及 PB7 引腳,就可以正常顯示溫濕度值。
I²C 使用兩條線連接,其中一條線為傳輸資料的串列資料線(SDA),另一條線是啟動或停止傳輸以及傳送時鐘序列的串列時脈(SCL)線,如有多個 I²C 裝置,所有的SDA 串行數據都接到相同的 SDA 線,時鐘線 SCL 接到共通的 SCL 線,速率最高400Kbit/s。I2C 允許連接多個設備(如下圖),只能由 Master 與 Slave 之間互傳,無法由 Slave 對 Slave 進行傳輸,且每個 Slave 都要有一個特定且唯一的位址。
上圖可分成三個部分,最左方的起始訊號(Start)、中央的資料傳輸及右方的停止訊號(Stop),I²C 的起始和停止訊號的電位轉換如下:
起始訊號(Start condition):當 SCL 高電位時,SDA 由高電位向低電位轉換;
停止訊號(Stop condition):當 SCL 高電位時,SDA 由低電位向高電位轉換;
將上述 I²C 起始訊號的電位轉換過程寫成程式,如下:
將上述 I²C 停止訊號的電位轉換過程寫成程式,如下:
將 I²C 串列傳輸協定分成以下幾個狀態:起始信號、設備位址發送、數據傳送、應答信號和停止信號。進行資料傳輸與接收的過程如下:
通訊時,如要發送一個位元,程式如下:
通訊時,如要接收一個位元,程式如下:
通訊時,如要發送 ACK 訊號,程式如下:
通訊時,根據 ack 參數,決定是否產生 ACK 應答, ack=1 時,產生 ACK 應答,程式如下:
在 AHT10技術手冊 中提到,在啟動傳輸後,隨後傳輸的 I²C 首位元組包括 7 位元的 I²C 設備位址 0x38 和一個 SDA 方向位(1 表示讀取,0 表示寫入)。在第 8 個 SCL 時鐘下降沿之後,透過拉低 SDA 引腳(ACK位元),指示感測器資料接收正常。在發出初始化命令之後(1110 0001 代表初始化,1010 1100 代表溫濕度測量),MCU 必須等待測量完成。 將上述讀取 AHT10 的方法,參考STM32 AHT10溫濕度傳感器數據機智雲傳輸溫濕度 這篇文章的程式,讀取溫濕度的方法如下:
本篇實做我使用 I²C 介面的 AHT10 溫濕度感測器來改寫 I²C 函式庫,包括 I²C 啟動、停止、傳送及接收位元等。過程中,我將 PA0 及 PA1 當作 I²C 的 SCL 及 SDA 引腳,執行程式時,得到的溫濕度都是 0,後來才發現不是每個引腳都能當作 I²C 的時鐘及資料引腳,找到我使用的開發板 STM32F103C8T6 的 Pinout 圖(請參考:STM32筆記(1):我的第一個STM32程式,使用Arduino IDE 介面)找到開發板上共有三組 I²C ,OLED 用掉一組,改選用 PB6 及 PB7 引腳,就可以正常顯示溫濕度值。
[I²C 串列通訊]
I²C(Inter-Integrated Circuit)字面上的意思是積體電路之間,它其實是I²C Bus簡稱,所以中文應該叫積體匯流排電路,它是一種串列通訊匯流排,使用多主從架構,由飛利浦公司在 1980 年代為了讓主機板、嵌入式系統或手機用以連接低速週邊裝置而發展。I²C的正確讀法為「I平方C」("I-squared-C")。[維基百科]I²C 使用兩條線連接,其中一條線為傳輸資料的串列資料線(SDA),另一條線是啟動或停止傳輸以及傳送時鐘序列的串列時脈(SCL)線,如有多個 I²C 裝置,所有的SDA 串行數據都接到相同的 SDA 線,時鐘線 SCL 接到共通的 SCL 線,速率最高400Kbit/s。I2C 允許連接多個設備(如下圖),只能由 Master 與 Slave 之間互傳,無法由 Slave 對 Slave 進行傳輸,且每個 Slave 都要有一個特定且唯一的位址。
來源:analog.com
[I²C 協定與程式]
I²C 資料的傳輸,是靠 SCL 及 SDA 電位的高低及轉換來進行通訊,通訊的 Bus 協定如下:上圖可分成三個部分,最左方的起始訊號(Start)、中央的資料傳輸及右方的停止訊號(Stop),I²C 的起始和停止訊號的電位轉換如下:
起始訊號(Start condition):當 SCL 高電位時,SDA 由高電位向低電位轉換;
停止訊號(Stop condition):當 SCL 高電位時,SDA 由低電位向高電位轉換;
將上述 I²C 起始訊號的電位轉換過程寫成程式,如下:
void I2C_Start(void) { SDA_OUT(); //SDA線輸出 SDA_High; SCL_High; delay_us(4); SDA_Low; //START:當 SCL 為高電位時,SDA(DATA) 從高電位變成低電位 delay_us(4); SCL_Low; //準備發送或接收資料 }
將上述 I²C 停止訊號的電位轉換過程寫成程式,如下:
void I2C_Stop(void) { SDA_OUT(); //SDA線輸出 SDA_Low; //STOP:當 SCL 為高電位時,SDA(DATA) 從低電位變成高電位 delay_us(4); SCL_High; delay_us(4); SDA_High; //發送I2C匯流排結束信號 delay_us(4); }
將 I²C 串列傳輸協定分成以下幾個狀態:起始信號、設備位址發送、數據傳送、應答信號和停止信號。進行資料傳輸與接收的過程如下:
- SDA 和 SCL 開始都為高,然後 Master 主機將 SDA 電位拉低,表示訊號開始。
- 接下來的 8 個時間週期裡,Master 主機控制 SDA 的高低,傳送 Slave 主機地址。其中第 8 位元如果為 0,表示接下來是寫入操作,即 Master 主機傳輸資料給 Slave 裝置;如果為 1,表示接下來是讀取操作,即 Slave 裝置傳輸資料給 Master 主機;另外,資料傳輸是從最高位到最低位,因此傳輸方式為 MSB(Most Significant Bit)。
- 匯流排中對應 Slave 裝置地址,發出應答訊號。
- 在接下來的 8 個時間週期裡(傳送 1 個 Byte 字元),如果是寫入操作,則 Mater 主機控制 SDA 的電位高低;如果是讀取操作,則由 Slave 裝置控制 SDA 的高低;
- 每次傳輸完成,接收資料的裝置,都發出應答訊號(第 9 個 bit)。
- Mater 主機會在 ACK 的 SCL 時脈釋放 SDA 線( SDA 為 high),如果 Slave 裝置正常收到 DATA 的話,會在第 9 個 bit 將 SDA 電位為 Low,也就是回應 ACK。
- 如果 Slave 裝置有狀況,無法正常接收(如:Bus 上找不到這個 Address 的 Slave 裝置,到了第 9 個 SCL 週期時,當 Mater 主機釋放 SDA 後,就沒有裝置去驅動 SDA,這時 SDA 會因為上拉電阻(Rp)的關係維持在 High, Mater 主機就知道出問題了,不會繼續傳輸,而要進行錯誤處理,這種情況稱之為「non-acknowledge」,或簡稱為「NACK」。
- 最後,在 SCL 為高電位時,Master 主機由低拉高 SDA 電位,表示停止訊號,整個傳輸結束。
通訊時,如要發送一個位元,程式如下:
void IIC_Send_Byte(u8 txd) { SDA_OUT(); SCL_Low; //拉低時鐘開始資料傳輸 for(u8 t=0;t<8;t++) { if((txd&0x80)>>7) SDA_High; else SDA_Low; txd<<=1; delay_us(2); SCL_High; delay_us(2); SCL_Low; delay_us(2); } }
通訊時,如要接收一個位元,程式如下:
u8 I2C_Read_Data(void) { u8 Data = RESET; SDA_IN(); for(u8 i=0;i<8;i++) { SCL_High; delay_us(2); Data <<= 1; if(GPIO_ReadInputDataBit(I2C_Prot,SDA) == SET) { Data |= 0x01; } SCL_Low; delay_us(2); } return Data; }
通訊時,如要發送 ACK 訊號,程式如下:
u8 I2C_Write_Ack(void) { u8 TimeAck = RESET; SDA_IN(); SCL_High; delay_us(2); while(GPIO_ReadInputDataBit(I2C_Prot,SDA)) { if(++TimeAck > 250) { I2C_Stop(); return 1; } } SCL_Low; delay_us(2); return 0; }
通訊時,根據 ack 參數,決定是否產生 ACK 應答, ack=1 時,產生 ACK 應答,程式如下:
void I2C_Is_Ack(u8 ack) { SCL_Low; SDA_OUT(); if(ack) SDA_Low; else SDA_High; delay_us(2); SCL_High; delay_us(2); SCL_Low; }
[AHT10溫濕度感測器]
AHT10 溫濕度感測器的精確度比 DHT11 高,規格如下:- 介面類別型:I2C
- 工作電壓:直流 1.8-3.6V
- 工作濕度:0~8100%RH
- 濕度精度:±2%
- 濕度解析度:0.024%
- 溫度精度:±0.3℃
- 溫度解析度:0.01℃
- 工作溫度:-40℃~85℃
在 AHT10技術手冊 中提到,在啟動傳輸後,隨後傳輸的 I²C 首位元組包括 7 位元的 I²C 設備位址 0x38 和一個 SDA 方向位(1 表示讀取,0 表示寫入)。在第 8 個 SCL 時鐘下降沿之後,透過拉低 SDA 引腳(ACK位元),指示感測器資料接收正常。在發出初始化命令之後(1110 0001 代表初始化,1010 1100 代表溫濕度測量),MCU 必須等待測量完成。 將上述讀取 AHT10 的方法,參考STM32 AHT10溫濕度傳感器數據機智雲傳輸溫濕度 這篇文章的程式,讀取溫濕度的方法如下:
u8 AHT10_Read_Humi_Temp(float *humidity, float *temperature) { u32 humi = 0,temp = 0; I2C_Start(); I2C_Send_Byte(AHT_WRITE); I2C_Write_Ack(); I2C_Send_Byte(0XAC); //觸發測量 I2C_Write_Ack(); I2C_Send_Byte(0X33); I2C_Write_Ack(); I2C_Send_Byte(0X00); I2C_Write_Ack(); I2C_Stop(); delay_ms(80); I2C_Start(); I2C_Send_Byte(AHT_READ); I2C_Write_Ack(); ACK = I2C_Read_Data(); I2C_Is_Ack(1); if((ACK&0X08) == 0) { AHT10_Write_Init(); } if((ACK&0X80) == 0) { //bit7 1 0 for(u8 i=0;i<5;i++){ // 0 1 2 3 4 5 ++i DATA[i] = I2C_Read_Data(); if(i == 4) I2C_Is_Ack(0); else I2C_Is_Ack(1); } I2C_Stop(); humi = (DATA[0]<<12)|(DATA[1]<<4)|(DATA[2]>>4); temp = ((DATA[2]&0X0F)<<16)|(DATA[3]<<8)|(DATA[4]); *humidity = (humi * 100.0/1024/1024+0.5); *temperature = (temp * 2000.0/1024/1024+0.5)/10.0-50; return 0; } I2C_Stop(); return 1; }
[材料]
- STM32F103C8T6 主板 x 1
- OLED SSD1306 顯示器 x 1
- AHT10 溫濕度感測器 x 1
- STLINK V2 模擬下載器 x 1
- 麵包板 x 1
- 連接線 x N 條
[接線與電路圖]
AHT10 分別接 3.3v 及 GND,接腳 SCL 和 SDA 分別接在 STM32F103x 的 PB6、PB7,與 SSD1306 連接的方式如下:STM32F103 | SSD1306 OLED | AHT10 |
---|---|---|
3.3v | VDD | VCC |
GND | GND | GND |
PB8 | SCK/SCL | - |
PB9 | SDA | - |
PB6 | - | SCL |
PB7 | - | SDA |
[程式]
主程式 main.c 如下:#include "stm32f10x.h" #include "delay.h" #include "sys.h" #include "oled.h" #include "bmp.h" #include "AHT10.h" #include "I2C.h" //主函數 int main(void) { float temperature,humidity; uint16_t tmp; delay_init(); //延時函數初始化 OLED_Init(); //初始化OLED OLED_Clear(0); //清除螢幕 OLED_ShowString(2, 2, "Humid :",16); OLED_ShowString(2, 4, "Temper:",16); I2C_Initation(); while(1) { AHT10_Read_Humi_Temp(&humidity,&temperature); OLED_ShowNum(60, 2, humidity,3, 16); OLED_ShowString(83, 2, ".",16); tmp = (uint16_t)(humidity*100) % 100; //小數點後兩位 if (tmp < 10){ OLED_ShowNum(90, 2, tmp, 2, 16); OLED_ShowString(89, 2,"0",16); //個位數時前面補0 } else { OLED_ShowNum(90, 2, tmp, 2, 16); } OLED_ShowNum(60, 4, temperature,3, 16); OLED_ShowString(83, 4, ".",16); tmp = (uint16_t)(temperature*100) % 100; //小數點後兩位 if (tmp < 10){ OLED_ShowNum(90, 4, tmp, 2, 16); OLED_ShowString(89, 4,"0",16); //個位數時前面補0 } else { OLED_ShowNum(90, 4, tmp, 2, 16); } delay_ms(300); } }有關完整 ATH10 及 I2C 完整的程式,請參考 Github:14. AHT10 Detect Temperature & Humidity
[結果]
檢測溫濕度的過程如以下影片:[參考資料]
- STM32F103 Reference manual
- STM32F10xxx參考手冊
- I2C: Inter-Integrated Circuit
- AHT10技術手冊
- STM32 AHT10溫濕度傳感器數據機智雲傳輸溫濕度
張貼留言