前言
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
圖 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)。
圖 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)時特別明顯。

圖 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 四種幀類型

圖 5:CAN 四種幀類型 — 資料幀、遠端幀、錯誤幀、過載幀各司其職。
三、CAN Arbitration(仲裁機制)
CAN Bus 最巧妙的設計之一就是 非破壞性逐位元仲裁(Non-destructive Bit-wise Arbitration)。當多個節點同時發送時:
- 每個節點在發送 ID 的同時監控匯流排狀態
- 如果某節點發送了隱性位元(1)但讀到匯流排是顯性(0),它立即知道有更高優先級的節點在發送
- 該節點停止發送並轉為接收模式
- 仲裁過程沒有資料遺失,優先級最高的訊息自動獲勝
這意味著 ID 越小的訊息優先級越高。例如 ID=0x001 的煞車指令永遠比 ID=0x100 的車窗控制優先傳送。
四、CAN Bit Stuffing 與錯誤處理
CAN Bus 採用 NRZ(Non-Return-to-Zero)編碼。為了保證接收端能正確恢復時脈,CAN 協定規定:
連續 5 個相同位元後,自動插入 1 個反向位元。
圖 3:Bit Stuffing 示意 — 原始資料有連續 6 個 1 → 在第 5 個 1 之後插入 0;連續 6 個 0 → 在第 5 個 0 之後插入 1。
CAN Bus 的錯誤檢測機制非常嚴謹:
- 位元監控(Bit Monitoring):發送節點同時監控匯流排,確認發送的位元與讀到的一致
- 位元填充規則檢查(Bit Stuffing Check):接收端檢查是否有違反位元填充規則的序列
- CRC 檢查:15-bit CRC,Hamming Distance = 6
- ACK 確認:所有接收節點在 ACK Slot 拉低匯流排
- 訊息格式檢查(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 通訊不上?
- 終端電阻:兩端是否都有 120Ω?
- Baud Rate 不一致:所有節點 baud rate 必須完全相同
- 收發器電壓:SN65HVD230 需要 3.3V,MCP2551 需要 5V
- CAN_H/CAN_L 接反:兩條線接反會導致通訊異常
- 共地:所有節點必須共用 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 提供了更完善的錯誤處理與多主節點通訊能力。本次學習的重點:
- 物理層:差分信號、120Ω 終端、CAN Transceiver 選型
- 資料鏈結層:CAN 2.0A/2.0B 幀格式、Bit Stuffing、仲裁機制
- 實作:STM32 bxCAN HAL、ESP32 TWAI Arduino/ESP-IDF
- 容錯:錯誤幀、錯誤計數器、Bus Off 恢復
如果你已經有 RS485 的基礎,轉換到 CAN Bus 會相當順利。兩者都是差分信號、都適合工業環境,但 CAN Bus 在錯誤處理和即時性上更上一層樓。
文章評論