---
# System prepended metadata

title: Hardware communication protocol

---

# 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 個從設備）|通常只有兩個設備|
|優勢|高速，全雙工|支持多主從，僅兩條線可連接多設備|硬體簡單，易用|
|劣勢|不支持多主，需要較多線|速度相較慢，時序及機制複雜|點對點，速度最慢|
|適用場合|對性能要求高的應用|節省引腳資源，多外設應用|調試，長距離通訊|