0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 未分類
  3. 正文

CAN Bus 通訊協定完整教學:從物理層到 STM32/ESP32 實作

2026 年 5 月 25 日 9點熱度 0人點贊 0條評論

前言

CAN Bus 通訊協定(Controller Area Network)是工業4.0、汽車電子與嵌入式系統中最具代表性的通訊協定之一。從汽車的 ECU 網路到工業自動化設備,CAN Bus 以其高可靠性、實時性和優異的錯誤處理機制,成為工業通訊的中流砥柱。如果你已經熟悉了 RS485 和 Modbus,這篇文章將帶你進入 CAN Bus 的世界,從物理層、資料鏈結層一路講到 STM32 與 ESP32 的實戰程式碼。

一、CAN Bus 基礎概念

1.1 什麼是 CAN Bus?

CAN Bus 由 Robert Bosch GmbH 在 1986 年發明,最初是為了汽車內部的多節點通訊而設計。它的核心特點:

  • 多主結構(Multi-Master):任一節點都可發起通訊,沒有主從關係
  • 訊息廣播:所有節點同時接收同一筆資料
  • 基於優先級的仲裁:ID 越小的訊息優先級越高,衝突時低位元 ID 自動獲勝
  • 差分信號(CAN_H / CAN_L):抗干擾能力強,適合工業環境
  • 完整的錯誤檢測:CRC、位元監控、位元填充、ACK 確認

1.2 標準 vs 擴展幀

CAN 2.0 協定定義了兩種規格:

  • CAN 2.0A(標準幀):11-bit Identifier,最多 2032 個不同 ID
  • CAN 2.0B(擴展幀):29-bit Identifier,支援超過 5 億個 ID

CAN 2.0A 標準幀

圖 1:CAN 2.0A 標準資料幀格式 — 從 SOF 開始,依序為 11-bit ID、RTR、IDE、DLC、Data(0~8 bytes)、CRC(15-bit + 1 delimiter)、ACK(Slot + Delimiter)、EOF(7 bits)。

CAN 2.0B 擴展幀

圖 2:CAN 2.0B 擴展資料幀格式 — 29-bit ID,比標準幀多了 SRR、IDE 和 18 個額外 ID 位元。

1.3 CAN 幀位元說明

欄位 位元數 說明
SOF(Start of Frame) 1 同步位元,顯性(0)
Identifier(ID) 11 / 29 訊息識別碼,決定優先級
RTR(Remote Transmission Request) 1 0=資料幀,1=遠端幀
IDE(Identifier Extension) 1 0=標準幀,1=擴展幀
DLC(Data Length Code) 4 資料長度(0~8 bytes)
Data Field 0~64 實際資料(0~8 bytes)
CRC 15+1 15-bit CRC + 1 delimiter
ACK 1+1 ACK Slot + ACK Delimiter
EOF(End of Frame) 7 7-bit 隱性(1)
IFS(Inter Frame Space) 3 幀間間隔

二、CAN Bus 物理層

2.1 差分信號

CAN Bus 使用兩條線傳輸:

  • CAN_H(CAN High)
  • CAN_L(CAN Low)

邏輯狀態以差分電壓表示:

  • 顯性(Dominant):CAN_H - CAN_L > 0.9V(邏輯 0),強制覆蓋隱性位元
  • 隱性(Recessive):CAN_H - CAN_L ≈ 0V(邏輯 1),被動狀態

2.2 終端電阻

CAN Bus 的兩端需要各接一顆 120Ω 終端電阻。這是為了消除信號反射,確保阻抗匹配。缺少終端電阻會導致通訊不穩,尤其在高速(>125 kbps)時特別明顯。

CAN Bus 多節點架構

圖 4:CAN Bus 多節點架構 — 每個節點由 MCU + CAN Controller(內建或獨立)+ CAN Transceiver 組成,匯流排兩端須接 120Ω 終端電阻。

2.3 CAN Transceiver

MCU 的 CAN controller 輸出的是 RX/TX 邏輯信號,必須透過 CAN Transceiver 轉換為 CAN_H/CAN_L 差分信號。常見晶片:

  • SN65HVD230:3.3V,適合 STM32F4 / ESP32
  • MCP2551:5V,適合 STM32F1
  • TJA1050:5V,高速(1 Mbps)

2.4 CAN 四種幀類型

CAN 四種幀類型

圖 5:CAN 四種幀類型 — 資料幀、遠端幀、錯誤幀、過載幀各司其職。

三、CAN Arbitration(仲裁機制)

CAN Bus 最巧妙的設計之一就是 非破壞性逐位元仲裁(Non-destructive Bit-wise Arbitration)。當多個節點同時發送時:

  1. 每個節點在發送 ID 的同時監控匯流排狀態
  2. 如果某節點發送了隱性位元(1)但讀到匯流排是顯性(0),它立即知道有更高優先級的節點在發送
  3. 該節點停止發送並轉為接收模式
  4. 仲裁過程沒有資料遺失,優先級最高的訊息自動獲勝

這意味著 ID 越小的訊息優先級越高。例如 ID=0x001 的煞車指令永遠比 ID=0x100 的車窗控制優先傳送。

四、CAN Bit Stuffing 與錯誤處理

CAN Bus 採用 NRZ(Non-Return-to-Zero)編碼。為了保證接收端能正確恢復時脈,CAN 協定規定:

連續 5 個相同位元後,自動插入 1 個反向位元。

Bit Stuffing 示意

圖 3:Bit Stuffing 示意 — 原始資料有連續 6 個 1 → 在第 5 個 1 之後插入 0;連續 6 個 0 → 在第 5 個 0 之後插入 1。

CAN Bus 的錯誤檢測機制非常嚴謹:

  1. 位元監控(Bit Monitoring):發送節點同時監控匯流排,確認發送的位元與讀到的一致
  2. 位元填充規則檢查(Bit Stuffing Check):接收端檢查是否有違反位元填充規則的序列
  3. CRC 檢查:15-bit CRC,Hamming Distance = 6
  4. ACK 確認:所有接收節點在 ACK Slot 拉低匯流排
  5. 訊息格式檢查(Form Check):固定格式位元是否正確

一個 CAN 節點偵測到錯誤後,會發送 錯誤幀(Error Frame) 破壞當前傳輸,強制發送節點重傳。

五、STM32 CAN 實作(HAL Library + bxCAN)

STM32 的 bxCAN(Basic Extended CAN)支援 CAN 2.0A 和 2.0B,硬體上有 3 個發送信箱和 2 個接收 FIFO。

5.1 CAN 初始化

#include "stm32f4xx_hal.h"

CAN_HandleTypeDef hcan1;

void MX_CAN1_Init(void)
{
    hcan1.Instance = CAN1;
    hcan1.Init.Prescaler = 42;           // 42 MHz / 42 = 1 MHz
    hcan1.Init.Mode = CAN_MODE_NORMAL;   // NORMAL / LOOPBACK / SILENT
    hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
    hcan1.Init.TimeSeg1 = CAN_BS1_12TQ;  // tBS1 = 12 TQ
    hcan1.Init.TimeSeg2 = CAN_BS2_3TQ;   // tBS2 = 3 TQ
    hcan1.Init.TimeTriggeredMode = DISABLE;
    hcan1.Init.AutoBusOff = DISABLE;
    hcan1.Init.AutoWakeUp = DISABLE;
    hcan1.Init.AutoRetransmission = ENABLE;
    hcan1.Init.ReceiveFifoLocked = DISABLE;
    hcan1.Init.TransmitFifoPriority = DISABLE;
    HAL_CAN_Init(&hcan1);
}

Baud Rate 計算:

CAN Clock = APB1 Clock / Prescaler = 42 MHz / 42 = 1 MHz
Bit Time = tSYNC_SEG + tBS1 + tBS2 = 1 + 12 + 3 = 16 TQ
Baud Rate = 1 MHz / 16 = 62.5 kbps

5.2 濾波器配置(Filter)

CAN 濾波器可以過濾不想處理的 ID,減輕 CPU 負擔:

void CAN_Filter_Config(void)
{
    CAN_FilterTypeDef filter = {0};

    filter.FilterBank = 0;
    filter.FilterMode = CAN_FILTERMODE_IDMASK;  // ID Mask 模式
    filter.FilterScale = CAN_FILTERSCALE_32BIT;
    filter.FilterIdHigh = 0x0000;
    filter.FilterIdLow  = 0x0000;
    filter.FilterMaskIdHigh = 0x0000;           // 接收所有 ID
    filter.FilterMaskIdLow  = 0x0000;
    filter.FilterFIFOAssignment = CAN_RX_FIFO0;
    filter.FilterActivation = ENABLE;
    HAL_CAN_ConfigFilter(&hcan1, &filter);
}

5.3 發送訊息

CAN_TxHeaderTypeDef tx_header;
uint8_t tx_data[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint32_t tx_mailbox;

tx_header.StdId = 0x123;          // 11-bit ID
tx_header.ExtId = 0;
tx_header.IDE = CAN_ID_STD;       // 標準幀
tx_header.RTR = CAN_RTR_DATA;     // 資料幀
tx_header.DLC = 8;                // 8 bytes

HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, &tx_mailbox);

5.4 中斷接收

CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];

// 主程式啟動接收中斷
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

// 回呼函數
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    if (hcan->Instance == CAN1)
    {
        HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);

        uint16_t received_id = rx_header.StdId;
        uint8_t  dlc = rx_header.DLC;

        // 根據不同 ID 處理資料
        switch (received_id)
        {
            case 0x100:  // 感測器資料
                process_sensor_data(rx_data, dlc);
                break;
            case 0x200:  // 控制指令
                process_control_cmd(rx_data, dlc);
                break;
            default:
                break;
        }
    }
}

六、ESP32 CAN 實作(TWAI + Arduino)

ESP32 的 CAN controller 稱為 TWAI(Two-Wire Automotive Interface),完全相容 CAN 2.0。

6.1 Arduino 框架(ESP32-CAN 函式庫)

#include <CAN.h>

#define CAN_TX_PIN  5   // GPIO5
#define CAN_RX_PIN  4   // GPIO4

void setup()
{
    Serial.begin(115200);

    if (!CAN.begin(500E3))  // 500 kbps
    {
        Serial.println("CAN init failed!");
        while (1);
    }

    CAN.filter(0x100, 0x7FF);  // 只接收 ID=0x100
    Serial.println("CAN Bus ready");
}

void loop()
{
    // 發送
    CAN.beginPacket(0x123);
    CAN.write(0xAA);
    CAN.write(0xBB);
    CAN.write(0xCC);
    CAN.write(0xDD);
    CAN.endPacket();

    // 接收
    int packetSize = CAN.parsePacket();
    if (packetSize > 0)
    {
        Serial.printf("CAN ID: 0x%03X, DLC: %d, Data: ", CAN.packetId(), packetSize);
        while (CAN.available())
        {
            Serial.printf("%02X ", CAN.read());
        }
        Serial.println();
    }

    delay(1000);
}

6.2 ESP-IDF 框架(TWAI Driver)

#include "driver/twai.h"

#define TX_GPIO  GPIO_NUM_5
#define RX_GPIO  GPIO_NUM_4

void can_init(void)
{
    twai_general_config_t gen_config = TWAI_GENERAL_CONFIG_DEFAULT(
        TX_GPIO, RX_GPIO, TWAI_MODE_NORMAL);
    twai_timing_config_t timing_config = TWAI_TIMING_CONFIG_500KBITS();
    twai_filter_config_t filter_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

    if (twai_driver_install(&gen_config, &timing_config, &filter_config) == ESP_OK)
    {
        twai_start();
        Serial.println("TWAI driver installed & started");
    }
}

void can_send(uint16_t id, uint8_t *data, uint8_t len)
{
    twai_message_t msg = {0};
    msg.identifier = id;
    msg.data_length_code = len;
    msg.extd = 0;      // 標準幀
    msg.rtr = 0;       // 資料幀
    for (int i = 0; i < len; i++) msg.data[i] = data[i];

    if (twai_transmit(&msg, pdMS_TO_TICKS(100)) == ESP_OK)
        Serial.println("Message sent");
    else
        Serial.println("Transmit failed");
}

void can_recv(void)
{
    twai_message_t msg;
    if (twai_receive(&msg, pdMS_TO_TICKS(50)) == ESP_OK)
    {
        Serial.printf("ID: 0x%03X, DLC: %d, Data: ", msg.identifier, msg.data_length_code);
        for (int i = 0; i < msg.data_length_code; i++)
            Serial.printf("%02X ", msg.data[i]);
        Serial.println();
    }
}

七、實戰專案:CAN Bus 多節點溫度監控

以下架構展示一個完整的 CAN Bus 溫度監控系統:

// ┌──────────┐        ┌──────────┐        ┌──────────┐
// │ Node A   │        │ Node B   │        │ Node C   │
// │ STM32F4  │ CAN    │ ESP32    │ CAN    │ STM32F1  │
// │ DS18B20  │════════│ OLED     │════════│ Relay    │
// │ Sensor   │   Bus  │ Display  │   Bus  │ Actuator │
// └──────────┘        └──────────┘        └──────────┘

節點定義:

  • Node A (ID=0x100):DS18B20 溫度感測器,每秒發送一次溫度值
  • Node B (ID=0x200):OLED 顯示器,顯示所有節點狀態
  • Node C (ID=0x300):繼電器執行器,接收控制指令
// Node A — 溫度發送端(STM32)
typedef struct {
    int16_t temperature;  // 放大 100 倍,如 25.43°C → 2543
    uint8_t  sensor_id;
    uint8_t  crc8;
} __attribute__((packed)) SensorPacket;

void send_temperature(void)
{
    SensorPacket pkt;
    pkt.temperature = (int16_t)(ds18b20_get_temp() * 100);
    pkt.sensor_id = 1;
    pkt.crc8 = calc_crc8((uint8_t *)&pkt, sizeof(pkt) - 1);

    CAN_TxHeaderTypeDef header;
    header.StdId = 0x100;
    header.DLC = sizeof(SensorPacket);
    header.IDE = CAN_ID_STD;
    header.RTR = CAN_RTR_DATA;

    uint32_t mailbox;
    HAL_CAN_AddTxMessage(&hcan1, &header, (uint8_t *)&pkt, &mailbox);
}

八、常見問題與除錯

8.1 通訊不上?

  1. 終端電阻:兩端是否都有 120Ω?
  2. Baud Rate 不一致:所有節點 baud rate 必須完全相同
  3. 收發器電壓:SN65HVD230 需要 3.3V,MCP2551 需要 5V
  4. CAN_H/CAN_L 接反:兩條線接反會導致通訊異常
  5. 共地:所有節點必須共用 GND

8.2 錯誤計數器

CAN controller 內部有兩個錯誤計數器:

  • REC(Receive Error Counter):接收錯誤累加
  • TEC(Transmit Error Counter):發送錯誤累加

當任一個計數器超過 127,節點進入 Error Passive 狀態;超過 255 則進入 Bus Off 狀態,不再參與匯流排通訊。

// 檢查 STM32 CAN 錯誤狀態
if (HAL_CAN_GetState(&hcan1) == HAL_CAN_STATE_ERROR)
{
    uint32_t error = HAL_CAN_GetError(&hcan1);
    if (error & HAL_CAN_ERROR_ACK)      Serial.println("ACK Error");
    if (error & HAL_CAN_ERROR_BOF)      Serial.println("Bus Off");
    if (error & HAL_CAN_ERROR_STF)      Serial.println("Stuff Error");
    if (error & HAL_CAN_ERROR_FOR)      Serial.println("Form Error");
    if (error & HAL_CAN_ERROR_CRC)      Serial.println("CRC Error");
}

九、總結

CAN Bus 是工業通訊領域中不可或缺的技能。相比 UART 和 RS485,CAN Bus 提供了更完善的錯誤處理與多主節點通訊能力。本次學習的重點:

  1. 物理層:差分信號、120Ω 終端、CAN Transceiver 選型
  2. 資料鏈結層:CAN 2.0A/2.0B 幀格式、Bit Stuffing、仲裁機制
  3. 實作:STM32 bxCAN HAL、ESP32 TWAI Arduino/ESP-IDF
  4. 容錯:錯誤幀、錯誤計數器、Bus Off 恢復

如果你已經有 RS485 的基礎,轉換到 CAN Bus 會相當順利。兩者都是差分信號、都適合工業環境,但 CAN Bus 在錯誤處理和即時性上更上一層樓。

標籤: RS485 工業通訊
最後更新:2026 年 5 月 25 日

shi6a

這個人很懶,什麼都沒留下

點贊
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang