# Hardware communication protocol
Reference:[成大資工Wiki](https://wiki.csie.ncku.edu.tw/embedded/USART)
## Introduction
本篇紀錄硬體通訊協定的學習筆記:
* UART
* I²C
* SPI
## UART
### UART principle
串列傳輸為 CPU 與周邊裝置或 CPU 與 CPU 間的資料傳輸方法之一,而USART(universal synchronous asynchronous receiver transmitter)通用同步/非同步收發傳輸器,則常被用於一般的串列傳輸應用中,例如將訊息 print 到 monitor 上(除錯和監控)。
UART 依照 NRZ 工業非同步資料傳輸格式,與其他設備進行資料交換。
#### connection schematic

MCU 與一個 device 進行 UART 通訊需要最少兩條線(TX, RX),如果想要啟用同步通訊則要再多加一條時間源 CLK。
#### NRZ: Nonreturn to Zero
是最原始的基頻傳輸編碼方式,```1```代表高電位,```0```代表低電位。

#### 半雙工 / 全雙工
* 半雙工 : 允許二台設備之間的雙向資料傳輸,但不能同時進行。因此同一時間只允許一設備傳送資料,若另一設備要傳送資料,需等原來傳送資料的設備傳送完成後再處理。例:無線電
* 全雙工 : 允許二台設備間同時進行雙向資料傳輸。例:手機
#### 同步 / 非同步
* 同步 : 額外提供時脈訊號,使兩端機器在溝通時能夠藉此同步收發資料。比起非同步傳輸,同步傳輸不需要start/stop bit,因此能夠一次傳較多的資料。
* 非同步 : 在傳送資料時插入額外資訊,表示資料起始、結束。好處是設定時間短、硬體成本低、機器時脈不同也能傳資料,缺點是單次傳輸的資料量較少。

#### data format
傳輸或接收資料之前,由Idle Line表示。
* 一個start bit
* 一個資料word,可為8/9 bits,用least significant bit做資料排序。根據USART_CR1暫存器中的M位選擇8或9位元決定資料長度。
* 一組0.5, 1, 1.5, 2 stop bits,用以表示該次frame傳輸完畢。

#### data transmit
流程圖如下,我們可以知道把資料放入TDR暫存器,硬體就會把資料傳送出去。

簡單了解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

I²C 使用兩條雙向 open-drain:
* SDA : Serial Data Line, holds Data or address signal.
* SCL : Serial Clock Line, holds Clock signal.
#### data format
I²C 通訊的基本數據幀結構由起始位和停止位包圍,典型結構如下:
1. 起始位(S)
2. 從地址(7+1):
包括地址及讀/寫位(R/W)。
3. 應答位(ACK/NACK):
從設備回應確認。
4. 數據(8):
主設備發送或接收的數據。
5. 應答位(ACK/NACK):
每次數據傳輸後的確認。
6. 停止位(P)

:::info
:book:
當對同一裝置傳送多筆資料時,不需再次傳送地址+R/W位,但仍需要在每次資料位後加上 ACK 訊號,所以 I²C的 傳輸是不連續的。

:::
## SPI
### SPI principle
SPI(Serial Peripheral Interface)是一種常見的串列通訊協議,用於在MCU和外部設備之間進行高速數據傳輸。
#### connection schematic

1. SCLK(Serial Clock):主機生成的時鐘訊號,用於同步數據傳輸。
2. MISO(Master In Slave Out):從機到主機的數據傳輸線,用於將數據從從機傳輸到主機。
3. MOSI(Master Out Slave In):主機到從機的數據傳輸線,用於將數據從主機傳輸到從機。
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 format


由於 SPI 不需要起始位、停止位跟確認位,所以資料是可以連續傳送不中斷的。
### LIS3DSH Sensor
利用 SPI 對開發板上的三軸加速器做操作,首先我們參考 LIS3DSH 的 document 對 SPI 做初始化配置。特別注意 CPOL, CPHA, BaudRate的設置。
* CPOL(Clock Polarity):時鐘極性
- CPOL = 0:時鐘閒置時為低電平。
- CPOL = 1:時鐘閒置時為高電平。
* CPHA(Clock Phase):時鐘相位
- CPHA = 0:數據在時鐘的第一個邊沿(上升或下降)采樣。
- CPHA = 1:數據在時鐘的第二個邊沿(上升或下降)采樣。


於是設定 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);
}
}
```


讀到的值是正確的,讚讚。
#### detect the acceleration data
接著來試著讀取加速度,首先配置相關 register

## 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 個從設備)|通常只有兩個設備|