# STM32 教學系列 第 11 節：RS-485 通訊

## 🎯 學習目標

1. **理解 RS-485 差分信號** - 掌握半雙工通訊
2. **配置 MAX485 收發器** - 實現長距離多點通訊
3. **實現發送/接收自動切換** - 控制方向線 (DE/RE)

---

## 🎓 RS-485 基礎概念

### RS-485 vs RS-232

| 特性 | RS-232 | RS-485 |
|------|--------|--------|
| **信號線** | 單端（信號 + GND） | 差分（A、B） |
| **距離** | ≤ 15 公尺 | ≤ 1200 公尺 |
| **速度** | 最高 115.2 kbps | 最高 10 Mbps |
| **設備數** | 1 對 1 | 多點（最多 32） |
| **模式** | 全雙工 | 半雙工 |

### RS-485 訊號線

| 信號 | 說明 |
|------|------|
| **A 線** | 不反轉資料線 |
| **B 線** | 反轉資料線（邏輯電平反轉） |
| **GND** | 共同接地 |

邏輯定義：
- **邏輯 1**：A > B（A 電壓高於 B）
- **邏輯 0**：A < B（A 電壓低於 B）

### MAX485 收發器引腳

| 引腳 | 名稱 | 方向 | 功能 |
|------|------|------|------|
| **DI** | Data In | ← | 來自 UART TX |
| **RO** | Receiver Out | → | 至 UART RX |
| **DE** | Driver Enable | ← | 發送使能（高=發送） |
| **RE** | Receiver Enable | ← | 接收使能（低=接收） |
| **A、B** | 差分線 | ↔ | 連接到總線 |

---

## 🛠️ 硬體接線

### 所需元件

| 元件 | 數量 | 說明 |
|------|------|------|
| MAX485 收發器 | 2 | 一發一收 |
| STM32 Nucleo | 2 | 主控板 |
| 跳線 | 多條 | 連接 |
| 120Ω 終端電阻 | 2 | 總線兩端（可選） |

### 接線圖

```
Nucleo 1 (發送)        MAX485             RS-485 總線          MAX485          Nucleo 2 (接收)
──────────────        ──────────          ────────────         ──────────       ──────────────
PA9 (TX) ────────────→ DI                                                       
PA10 (RX) ←────────── RO                                                        
PA0 ────────────────→ DE                                                        
      ─────────────→ RE                                                        
3.3V ───────────────→ VCC                                                       ← VCC ────── 3.3V
GND ────────────────→ GND                                                       ← GND ────── GND
               ┌─────→ A  ═════════════════════════════════════════════════ A  ──┐
               │                                                           │
               └─────→ B  ═════════════════════════════════════════════════ B  ──┘
                                      [120Ω]              [120Ω]
                                        │                   │
                                       GND                 GND

                                                    ↓ 
                                      ┌────────────────────────┐
                                      │   TX ─→ PA9           │
                                      │   RX ← PA10           │
                                      │   DE ← PA0 (GPIO OUT) │
                                      │   RE ← PA0 (GPIO OUT) │
                                      │   3.3V               │
                                      │   GND                │
                                      └────────────────────────┘
```

---

## ⚙️ CubeMX 配置步驟

### 步驟 1：啟用 USART1

1. 在 Pinout 中配置：
   - **PA9**：USART1_TX
   - **PA10**：USART1_RX
   - **PA0**：GPIO_Output（用於 DE/RE 控制）

### 步驟 2：配置 UART 參數

1. 左側選 **Connectivity** → **USART1**
2. **Mode**：Asynchronous
3. 設定：
   - **Baud Rate**：9600
   - **Word Length**：8 Bits
   - **Parity**：None
   - **Stop Bits**：1

### 步驟 3：啟用中斷

NVIC Settings：
- **USART1 global interrupt** ✓

### 步驟 4：生成程式碼

---

## 💻 完整程式碼

### 發送方：main.c

```c
/* STM32 Lesson 11 - RS-485 (Sender)
 * 功能：透過 RS-485 發送資料
 * 難度：中級
 */

#include "main.h"
#include "usart.h"
#include <stdio.h>
#include <string.h>

#define DE_PORT GPIOA
#define DE_PIN GPIO_PIN_0
#define RE_PORT GPIOA
#define RE_PIN GPIO_PIN_0

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
void RS485_SetTx(void);   /* 設為發送模式 */
void RS485_SetRx(void);   /* 設為接收模式 */
void RS485_Send(uint8_t *data, uint16_t size);

UART_HandleTypeDef huart1;

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  uint8_t counter = 0;

  while (1)
  {
    char buffer[50];
    
    /* 設為發送模式 */
    RS485_SetTx();
    HAL_Delay(10);  /* 等待方向穩定 */

    /* 準備資料 */
    sprintf(buffer, "Msg%d\r\n", counter++);
    
    /* 發送 */
    RS485_Send((uint8_t *)buffer, strlen(buffer));
    
    /* 設為接收模式 */
    RS485_SetRx();

    HAL_Delay(1000);
  }
}

/**
  * @brief 設置為發送模式（DE=高, RE=高）
  */
void RS485_SetTx(void)
{
  HAL_GPIO_WritePin(DE_PORT, DE_PIN, GPIO_PIN_SET);
  HAL_GPIO_WritePin(RE_PORT, RE_PIN, GPIO_PIN_SET);
}

/**
  * @brief 設置為接收模式（DE=低, RE=低）
  */
void RS485_SetRx(void)
{
  HAL_GPIO_WritePin(DE_PORT, DE_PIN, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(RE_PORT, RE_PIN, GPIO_PIN_RESET);
}

/**
  * @brief 透過 RS-485 發送資料
  */
void RS485_Send(uint8_t *data, uint16_t size)
{
  HAL_UART_Transmit(&huart1, data, size, HAL_MAX_DELAY);
  HAL_Delay(5);  /* 等待發送完成 */
}

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 8;
  RCC_OscInitStruct.PLL.PLLN = 336;
  RCC_OscInitStruct.PLL.PLLP = 2;
  RCC_OscInitStruct.PLL.PLLQ = 7;

  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    Error_Handler();

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                              | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
    Error_Handler();
}

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  __HAL_RCC_GPIOA_CLK_ENABLE();

  GPIO_InitStruct.Pin = GPIO_PIN_0;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
}

static void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 9600;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;

  if (HAL_UART_Init(&huart1) != HAL_OK)
    Error_Handler();
}

void Error_Handler(void)
{
  while(1);
}
```

---

## 🔍 測試與除錯

### 預期結果

✅ **發送方 UART 輸出**：
```
Msg0
Msg1
Msg2
```

✅ **接收方 UART 輸出**：
```
Msg0
Msg1
Msg2
```

### 常見問題

| 問題 | 原因 | 解決方案 |
|------|------|--------|
| **無法接收** | 方向線控制錯誤 | 檢查 PA0 GPIO 配置 |
| **資料錯亂** | 波特率不匹配 | 驗證兩端波特率都是 9600 |
| **訊號干擾** | 缺少終端電阻 | 在總線兩端各加 120Ω 電阻 |

---

## 📚 進階應用

### MODBUS 協定實現

RS-485 常用於 MODBUS 協定。基本框架：

```c
/* MODBUS RTU 訊息格式 */
typedef struct {
  uint8_t slave_id;          /* 從設備地址 */
  uint8_t function_code;     /* 功能碼 */
  uint8_t data[252];         /* 資料 */
  uint16_t crc;              /* CRC 校驗（結合第 12 節） */
} ModbusFrame;
```

---

## 🔗 延伸學習

### 下節預覽 - 第 12 節：硬體 CRC 計算

第 12 節將實現：
- **CRC 多項式與校驗原理**
- **STM32 硬體 CRC 引擎**
- **資料完整性驗證**

---

**✨ RS-485 遠距通訊完成！🚀**