# Hardware communication protocol Reference:[成大資工Wiki](https://wiki.csie.ncku.edu.tw/embedded/USART), [工程師の師](https://www.youtube.com/@Joyous-Code_Teacher), [爱上半导体](https://www.youtube.com/@idiode) ## Introduction 本篇紀錄硬體通訊協定的學習筆記: * UART * I²C * SPI ## UART ### UART principle 串列傳輸為 CPU 與周邊裝置或 CPU 與 CPU 間的資料傳輸方法之一,而USART(universal synchronous asynchronous receiver transmitter)通用同步/非同步收發傳輸器,則常被用於一般的串列傳輸應用中,例如將訊息 print 到 monitor 上(除錯和監控)。 UART 依照 NRZ 工業非同步資料傳輸格式,與其他設備進行資料交換。 #### connection schematic ![image](https://hackmd.io/_uploads/HJUm5LZQyl.png) MCU 與一個 device 進行 UART 通訊需要最少兩條線(TX, RX),如果想要啟用同步通訊則要再多加一條時間源 CLK。 #### NRZ: Nonreturn to Zero 是最原始的基頻傳輸編碼方式,```1```代表高電位,```0```代表低電位。 ![image](https://hackmd.io/_uploads/rykk8T4g1g.png) #### 半雙工 / 全雙工 * 半雙工 : 允許二台設備之間的雙向資料傳輸,但不能同時進行。因此同一時間只允許一設備傳送資料,若另一設備要傳送資料,需等原來傳送資料的設備傳送完成後再處理。例:無線電 * 全雙工 : 允許二台設備間同時進行雙向資料傳輸。例:手機 #### 同步 / 非同步 * 同步 : 額外提供時脈訊號,使兩端機器在溝通時能夠藉此同步收發資料。比起非同步傳輸,同步傳輸不需要start/stop bit,因此能夠一次傳較多的資料。 * 非同步 : 在傳送資料時插入額外資訊,表示資料起始、結束。好處是設定時間短、硬體成本低、機器時脈不同也能傳資料,缺點是單次傳輸的資料量較少。 ![image](https://hackmd.io/_uploads/rJguxw64g1x.png) #### data frame 傳輸或接收資料之前,由Idle Line表示。 * 一個start bit * 一個資料word,可為7/8/9 bits,用<font color="#f00">LSB(least significant bit)做資料排序。</font>根據USART_CR1暫存器中的M位選擇8或9位元決定資料長度。 * 一個奇偶校正位 parity bit (optinal, 現在已很少用)。 * 一組1/2 stop bits,用以表示該次frame傳輸完畢。 ![image](https://hackmd.io/_uploads/Hk8LdTVlke.png) ![image](https://hackmd.io/_uploads/HJVDh4CP1x.png) #### data transmit 在UART做初始化`HAL_UART_Init`時,會透過`UART_SetConfig(huart)`對register做設定。 ![image](https://hackmd.io/_uploads/SyEF-E0DJx.png) :::info :book: **USART 寄存器作用** 1. USART_CR1 (Control Register 1): * 用於控制 USART 的基本功能: * 開啟或關閉 USART(UE 位)。 * 控制資料長度(M 位,例如 8 位或 9 位)。 * 啟用或禁用接收(RE 位)和傳輸(TE 位)。 * 啟用中斷(例如接收中斷 RXNEIE)。 2. USART_CR2 (Control Register 2): * 配置同步模式、停止位數、時鐘源等。 3. USART_CR3 (Control Register 3): * 配置DMA傳輸、硬體流控制、設置錯誤檢測。 4. USART_BRR (Baud Rate Register): * 設定波特率,值根據核心時鐘(usart_ker_ck)和分頻器。(USART_PRESC)來計算 5. USART_RQR (Request Register): * 用於手動發出請求,如清空 FIFO 緩衝區或重新啟動。 6. USART_ISR (Interrupt and Status Register): * 用於檢查狀態標誌(如是否有資料準備好接收或發送)。常見的標誌包括: * TXE:傳輸資料寄存器空。 * RXNE:接收資料寄存器非空(有新資料)。 * TC:傳輸完成。 7. USART_ICR (Interrupt Flag Clear Register): * 用於清除中斷標誌。例如,清除傳輸完成標誌(TC)。 8. USART_RDR (Receive Data Register): * 用於讀取接收到的資料。 9. USART_TDR (Transmit Data Register): * <font color="#f00">用於向 USART 傳輸資料。當資料寫入此寄存器後,會自動進入傳輸流程。</font> 10. USART_PRESC (Prescaler Register): * 設置波特率的分頻器,用於調整 USART 時鐘源。 11. USART_RTOR (Receiver Timeout Register): * 設置接收資料時的超時時間(例如用於多路通信時的超時檢測)。 12. USART_GTPR (Guard Time and Prescaler Register): * 用於 Smartcard 模式或紅外通信模式,設置保護時間和分頻器。 ::: 流程圖如下,我們可以知道把資料放入TDR暫存器,硬體就會把資料傳送出去。 ![image](https://hackmd.io/_uploads/Sk7wtaEe1g.png) 簡單了解UART傳輸原理後,就可以不透過API,直接操作register來傳送資料。 ### Implement ```printf()``` 一般我們在呼叫```printf()```時,格式會像這樣 ```c int a = 10; int b = 0xFF; char c = 'a'; char s[10] = "Hello"; printf("%%% DECint: %d, HEXint: 0x%x, char: %c, string: %s %%%" , a, b, c, s); ``` 大致上有5種可能的輸出格式: 1. 10進位整數 2. 16進位整數 3. 字元 4. 字串 5. 單純的 '%' 於是我們就可以先把大架構設計出來 ```c /* * @param format is a string which we want to print out */ void print_all(const char* format, ...) //case: %d %x %s %c and other { va_list args; va_start(args, format); while(*format != '\0') { if (*format == '%') { format++; switch (*format) { case 'c': send_char((uint8_t)va_arg(args, uint32_t)); break; case 's': send_str(va_arg(args, uint8_t* )); break; case 'd': send_int(va_arg(args, int32_t), 10); break; case 'x': send_int(va_arg(args, uint32_t), 16); break; default: send_char('%'); send_char((uint8_t)*format); break; } } else send_char((uint8_t)*format); format++; } va_end(args); } ``` :::info :point_right: **可變參數```va_list```?** ```va_list```是一個解決function的輸入參數是不定的型別,被定義在```<stdarg.h>```中,它的功能相當於一個函式參數的迭代器。我們必須先以```va_start```初始化迭代器的起始位置,再透過```va_arg```依次讀取各個參數,最後再以 ```va_end```釋放所有```va_list```所需的資源。 ::: 接著各別考量不同情形 - [ ] ```send_char()``` ```c void send_char(const uint8_t pData) { uint32_t tickstart = HAL_GetTick(); if (UART_WaitOnFlagUntilTimeout(&huart2, UART_FLAG_TXE, RESET, tickstart, 0xffffffff) == HAL_OK) { huart2.Instance->TDR = (uint8_t)(pData & 0xFFU); } } ``` 藉由前一章我們知道可以透過將資料存入 TDR(Transmit Data Register)來傳送資料,且 UART 一次 format 8 bits 的資料;在寫入TDR前呼叫```UART_WaitOnFlagUntilTimeout()```確保傳輸緩衝區空標誌```TXE```被設置。 - [ ] ```send_str()``` ```c void send_str(const uint8_t* str) { while(*str != '\0') { send_char(*str); str++; } } ``` 字串利用指標一個一個字元傳送。 - [ ] ```send_int``` ```c void send_int(int32_t target, uint16_t b) { uint8_t buffer[10]; // can show 9 number mostly uint8_t* ptr = buffer+9; if (target == 0) { buffer[0] = '0'; buffer[1] = '\0'; send_str(buffer); return; } /* negative case */ int neg = 0; if (b == 10 && target < 0) { neg = 1; target = -target; } uint32_t temp; *ptr = '\0'; while(target) { temp = target % b; if (temp > 9) temp += ('A' - 10) - '0'; *(--ptr) = temp + '0'; target /= b; } if (neg == 1) *(--ptr) = '-'; send_str(ptr); } ``` 利用餘數取得最後一位,在透過ASCII轉換成字串。 ```c #define DB_printf(format, ...) print_all(format, ##__VA_ARGS__) ``` 最後定義巨集處理不定參數即可。 :::success :book: **補充 : 將字串轉換成16進位** ```c const uint16_t str2int(uint8_t* data) { uint16_t res = 0; while(*data != '\0') { res <<= 4; if (*data >= '0' && *data <= '9') res += (*data - '0'); else res += (*data - 'a') + 10; data++; } return res; } ``` ::: ## I²C ### I²C principle I²C(Inter-Integrated Circuit)是內部整合電路的稱呼,是一種串列通訊匯流排,由Philips公司在1980年代為了讓主機板、手機及嵌入式系統用以連接低速周邊裝置而發展,主要應用在board-to-board,它的設計並不能應用到長距離裝置的通訊。 #### connection schematic ![image](https://hackmd.io/_uploads/Sk9daI-71g.png) I²C 使用兩條雙向 open-drain: * SDA : Serial Data Line, holds Data or address signal. * SCL : Serial Clock Line, holds Clock signal. :::info :book: 1. 在 I2C 通訊協議 中,SDA(數據線)與 SCL(時鐘線) 採用 開漏(Open-Drain)或開集極(Open-Collector)架構,這代表: * 設備只能將線路拉低,無法主動拉高。 * 為了確保訊號能正確回到高電位(Vcc),需要加上拉電阻,避免Floating。 2. 為什麼拉低比拉高快? * 拉低時,I2C設備內部的 MOSFET 會直接將 SDA/SCL 拉到 GND,這個過程幾乎是瞬間完成的,因為內部電阻很低(一般為幾歐姆)。 * 拉高時,設備釋放 SDA/SCL,線路會依靠 Pull-up 電阻慢慢充電回到 Vcc。這個充電過程受到 Pull-up 電阻值(R)和匯流排寄生電容(C)的影響,符合 RC 時間常數(τ = R × C),導致電壓上升有滯後效應。 3. Pull-up 電阻值選擇 * Higher Resistance(10kΩ 或更大): * 優點:較小的功耗(較小的電流)。 * 缺點:拉高時間(Pull-up time)較長,降低 I2C 匯流排速度,因為信號恢復到 High 需要較長時間。 * Lower Resistance(較低電阻值,如 1kΩ 或更小) * 優點:拉高時間更快,可提升 I2C 通訊速度(如 400 kHz 以上的 Fast Mode 或 1 MHz 的 Fast Mode+)。 * 缺點:電阻越小,待機功耗越高,因為當 SDA/SCL 被拉低時,會有較大的電流流過 Pull-up 電阻。 ![image](https://hackmd.io/_uploads/HJnpv3au1g.png) <font color="#f00">**因此,Pull-up 電阻的選擇存在 trade off,常見範圍是 1kΩ ~ 10kΩ 之間,根據 I2C 匯流排速度、功耗需求、設備驅動能力來選擇。**</font> ::: #### data frame I²C 通訊的基本數據幀結構由起始位和停止位包圍,典型結構如下: 1. 起始位(S): SDA高轉低電位。 3. 從地址(7+1): 包括地址及讀/寫位(R=1/W=0),<font color="#f00">以MSB(Most significant bit)傳送。</font>。 3. 應答位(ACK=0/NACK=1): 主/從設備回應確認,由接收訊息端發出。 4. 數據(8): 主設備發送或接收的數據,<font color="#f00">以MSB(Most significant bit)傳送。</font> 5. 停止位(P): SDA低轉高電位。 ![image](https://hackmd.io/_uploads/Hy9S2XJuyl.png) ![image](https://hackmd.io/_uploads/r1UjvD-Xye.png) <font color="#f00"> *當SCL高電位,晶片讀取SDA資料 當SCL低電位,SDA可以改變電位高/低* </font> ![image](https://hackmd.io/_uploads/SJlGBBcUyg.png) #### data transmit I²C的傳輸流程如下: ![image](https://hackmd.io/_uploads/SJXB44ku1g.png) 1. 數據準備: * MCU 將要傳輸的數據寫入 I2C_TXDR(Transmit Data Register)。 * 每次數據傳輸後,TXE(Transmit Empty)標誌設置為 1,表示可以寫入新的數據。 2. 將<font color="#f00">**下一筆數據**</font>傳送到Shift Register: * 當 I2C_TXDR 中有有效數據(TXE=0)時,裡面的內容會在第9個 SCL 脈衝(ACK)後複製到 Shift Register。 * Shift Register 會 bit-by-bit 將數據輸出到SDA總線。 * 如果 I2C_TXDR 中沒有數據(TXE=1),SCL 會被拉低(SCL Stretch)暫停時鐘,直到 MCU 寫入新的數據到 I2C_TXDR。 3. 接收方確認: * 每完成一個字節(8 bits)的傳輸,會有第9個 SCL 脈衝(ACK/NACK 信號)。 * ACK,接收方拉低 SDA 線,表示成功接收數據,傳輸繼續。 * NACK,主機通常會生成 STOP 信號,結束傳輸。 4. 等待下一個字節的傳輸: * 在接收到ACK的情況下,MCU 必須在下次傳輸開始前,將新數據寫入 I2C_TXDR(如圖所示),如果沒有寫入新數據,SCL 線會保持低電平(SCL Stretch),直到新的數據寫入。 :::info :book: **I²C暫存器作用** 1. `CR1` (Control Register 1):控制 I2C 外設的啟用與基本功能。 * `PE`:I2C 外設啟用位。 * `TXIE`、`RXIE`:啟用傳輸和接收中斷。 2. `CR2` (Control Register 2):控制 I2C 的傳輸參數和行為,用於設置目標從設備的地址、傳輸的字節數量和讀寫方向。 * `SADD`:從設備地址(7 位或 10 位)。 * `RD_WRN`:讀寫控制位。 * `NBYTES`:設置傳輸的數據字節數。 3. `OAR1` (Own Address 1 Register):設置 I2C 外設作為從設備時的主要地址。 * `OA1`:自己的 I2C 地址(7 位或 10 位)。 * `OA1MODE`:地址模式。 * `OA1EN`:地址啟用位。 4. `OAR2` (Own Address 2 Register):設置 I2C 外設作為從設備時的次要地址(第二個地址)。 5. `TIMINGR` (Timing Register):配置 I2C 時序(如 SCL 的高低電平時間)以支持不同的波特率(如標準模式、高速模式)。 * `SCLL`、`SCLH`:SCL 時鐘的低電平和高電平時間。 * `PRESC`:分頻器,用於調整內部時鐘以生成 SCL。 6. `TIMEOUTR` (Timeout Register):用於配置 I2C 超時機制。 7. `ISR` (Interrupt and Status Register):提供 I2C 外設的當前狀態和中斷標誌。 * `TXIS`:TX 寄存器空標誌,表示可以發送數據。 * `RXNE`:RX 寄存器非空標誌,表示數據已接收。 * `TC`:傳輸完成標誌。 8. `ICR` (Interrupt Clear Register):用於清除中斷標誌。 9. `PECR` (PEC Register):用於校驗數據的完整性。 10. `RXDR` (Receive Data Register):用於存儲接收到的數據。 11. `TXDR` (Transmit Data Register):用於存儲要發送的數據。 ::: <font color="#f00">**只能Master direct to Slave,無法Slave direct to Slave。每個slave都要有一個特定且唯一的位址;但支持多主模式,可以讓一個從設備變成主設備,發起通信。** </font> :::info :book: 當對同一裝置傳送多筆資料時,不需再次傳送地址+R/W位,但仍需要在每次資料位後加上 ACK 訊號,所以 I²C的 傳輸是不連續的。 ![image](https://hackmd.io/_uploads/rJ11CPWQJg.png) ::: ## SPI ### SPI principle SPI(Serial Peripheral Interface)是一種常見的串列通訊協議,用於在MCU和外部設備之間進行高速數據傳輸。 #### connection schematic ![image](https://hackmd.io/_uploads/BkAEzq-m1e.png) ![image](https://hackmd.io/_uploads/HJhfqPkukl.png) 1. SCLK(Serial Clock):主機生成的時鐘訊號,用於同步數據傳輸。 2. MISO(Master In Slave Out):從機到主機的數據傳輸線,用於將數據從從機傳輸到主機,<font color="#f00">支援MSB及LSB。</font> 3. MOSI(Master Out Slave In):主機到從機的數據傳輸線,用於將數據從主機傳輸到從機,<font color="#f00">支援MSB及LSB。</font> 4. CS/SS(Chip Select/Slave Select):選擇要與主機通訊的從機的信號線。當CS/SS訊號為低電位時,表示該從機被選擇。 從線路圖可以發現,片選訊號每個 device 都會有獨立一條,所以如果今天 MCU 需要用 SPI 跟 n 個 device 通訊,總共會需要 3+n 條線。 :::info :book: 需特別注意的是,SPI 並不像 I²C 有統一規範的 data frame;只要 Master 跟 Slave 兩者訂好 data frame 即可。之前有聽過因為硬體設計考量(想要省SS訊號線),把某一個 bit 當作片選 bit。 ::: #### data frame ![image](https://hackmd.io/_uploads/rkCRY5ZmJe.png) ![image](https://hackmd.io/_uploads/S1IeiqbXJg.png) <font color="#f00">**由於 SPI 不需要起始位、停止位跟確認位,所以資料是可以連續傳送不中斷的。**</font> #### data transmit ![image](https://hackmd.io/_uploads/Skthqw1uJl.png) ![image](https://hackmd.io/_uploads/Sy-iTPk_kx.png) 基本上傳輸數據的步驟與 I²C大致雷同,原理都是一樣的,這邊就不多贅述。 :::info :book: **SPI暫存器作用** 1. `CR1` (Control Register 1):配置 SPI 的主要操作模式和參數。 * `SPE`: 打開或關閉 SPI。 * `LSBFIRST`: 指定數據傳輸的順序(LSB 或 MSB)。 * `CPOL`、`CPHA`: 設定數據與時鐘的同步特性。 * `MSTR`: 設置 SPI 為主設備(Master)或從設備(Slave)。 2. `CR2` (Control Register 2):配置 SPI 的中斷和進階功能。 3. `SR` (Status Register):提供 SPI 當前狀態的即時信息。 * `TXE`: 當 TX 緩衝區空時置位,可以寫入數據。 * `RXNE`: 當 RX 緩衝區有新數據時置位,可以讀取數據。 4. `DR` (Data Register): 用於數據的發送和接收。 5. `CRCPR` (CRC Polynomial Register): 設置 CRC 的多項式,用於校驗傳輸數據的完整性。 6. `RXCRCR` (RX CRC Register): 存儲接收數據的 CRC 校驗值。 7. `TXCRCR` (TX CRC Register): 存儲發送數據的 CRC 校驗值。 <font color="#f00"> ^**^*I²S部分忽略*</font> ::: :::success :book: **addition information about `DR`** 有別於I²C,SPI並未區分`TXDR`, `RXDR`,那在全雙工的情況下,不會發生競爭情況嗎? 答案是不會的,雖然`DR`同時負責傳送和接收數據,但由於硬體的設計和工作流程,傳送和接收操作並不會產生衝突。 1. `DR`僅作為數據進入或退出的入口,實際傳輸過程是由 Shift Register 完成的,避免了同時操作`DR`的衝突。 2. `TXE` 和 `RXNE` 標誌位提供了清晰的傳輸與接收狀態,保證數據操作的同步性。 3. 現代 SPI 控制器通常配備了 TXFIFO 和 RXFIFO,緩解高頻通信時的數據讀寫壓力。 ::: ### LIS3DSH Sensor 利用 SPI 對開發板上的三軸加速器做操作,首先我們參考 LIS3DSH 的 document 對 SPI 做初始化配置。特別注意 CPOL, CPHA, BaudRate的設置。 * CPOL(Clock Polarity):時鐘極性 - CPOL = 0:時鐘閒置時為低電平。 - CPOL = 1:時鐘閒置時為高電平。 * CPHA(Clock Phase):時鐘相位 - CPHA = 0:數據在時鐘的第一個邊沿(上升或下降)采樣。 - CPHA = 1:數據在時鐘的第二個邊沿(上升或下降)采樣。 <font color="#f00"> ^**^*補充:CPHA的定義並不是依照上升緣/下降緣,而是晶片採樣資料的時機是在SS訊號下降之後,SCLK的奇數次變化時(CPHA = 0)或是偶數次變化時(CPHA = 1)。*</font> ![image](https://hackmd.io/_uploads/BkVJnP1_Je.png) ![image](https://hackmd.io/_uploads/H1mBBhb71e.png) ![image](https://hackmd.io/_uploads/SyYYr2ZmJe.png) 於是設定 Mode 3。 #### Read / Write 接著分別寫讀跟寫的函式: ```c void MEMS_Read(uint8_t MemAddr, uint8_t* rData) { MemAddr |= 0x80; //set read bit HAL_GPIO_WritePin(CS_I2C_SPI_GPIO_Port, CS_I2C_SPI_Pin, GPIO_PIN_RESET);//select device HAL_SPI_Transmit(&hspi1, &MemAddr, 1, HAL_MAX_DELAY); //select register and Read operation HAL_SPI_Receive(&hspi1, rData, 1, HAL_MAX_DELAY); //receive the data HAL_GPIO_WritePin(CS_I2C_SPI_GPIO_Port, CS_I2C_SPI_Pin, GPIO_PIN_SET); //un-select device } ``` ```c void MEMS_Write(uint8_t MemAddr, uint8_t* wData) { MemAddr &= 0x7F; //set write bit HAL_GPIO_WritePin(CS_I2C_SPI_GPIO_Port, CS_I2C_SPI_Pin, GPIO_PIN_RESET);//select device HAL_SPI_Transmit(&hspi1, &MemAddr, 1, HAL_MAX_DELAY); //select register and write operation HAL_SPI_Transmit(&hspi1, wData, 1, HAL_MAX_DELAY); //write the data HAL_GPIO_WritePin(CS_I2C_SPI_GPIO_Port, CS_I2C_SPI_Pin, GPIO_PIN_SET); //un-select device } ``` 然後,我們先試著讀取 LIS3DSH 的```WHO_AM_I```register 確認有讀到正確的裝置, ```c void Sensor_task(void*) { for(;;) { MEMS_Read(WHO_AM_I, &rBuffer); _printf("Device: 0x%x\n\r", rBuffer); vTaskDelay(1000); } } ``` ![image](https://hackmd.io/_uploads/SyX5_2Zmkg.png) ![image](https://hackmd.io/_uploads/ryc2unZmJl.png) 讀到的值是正確的,讚讚。 #### detect the acceleration data 接著來試著讀取加速度,首先配置相關 register ![image](https://hackmd.io/_uploads/Hklys3-71l.png) ## Compare | 特性 | SPI | I²C | UART | | -------- | -------- | -------- |--------| | 數據方向 | 全雙工 | 半雙工 | 皆可 | | 訊號線數量|4 條(SCLK、MOSI、MISO、SS)|2 條(SCL、SDA)|2 條(TX、RX)| |速度|高(通常都有幾十Mbps)|中(標準 100 kbps,高速 400 kbps)|中(常見 9600 到 115200 bps)| |主/從架構|1 主多從|1 主多從| 點對點(無主從)| |同步方式|同步(有時鐘)|同步(有時鐘)|異步(無時鐘),但支持同步| |支持設備數量|多,取決於 SS 信號線數量|多(7 位地址模式最多 127 個從設備)|通常只有兩個設備| |優勢|高速,全雙工|支持多主從,僅兩條線可連接多設備|硬體簡單,易用| |劣勢|不支持多主,需要較多線|速度相較慢,時序及機制複雜|點對點,速度最慢| |適用場合|對性能要求高的應用|節省引腳資源,多外設應用|調試,長距離通訊|