
前言:車上那麼多控制器,靠一條線就講完
一台車裡有引擎電腦、煞車、車窗、儀表板,全部都要互相講話。如果每個裝置之間都拉一組線,線束會比引擎還重。CAN Bus 的答案是:全部掛在同一對線上,用 ID 決定誰先講。
它最厲害的地方有兩個:抗干擾(量的是兩條線的電壓差)與非破壞性仲裁(同時發話也不會撞掉資料)。這兩個設計讓它從 1980 年代活到今天,還在工業與車用領域霸佔位置。
一條匯流排掛很多節點

每個節點都是「MCU + CAN Controller + Transceiver」的組合。Controller 負責協議,Transceiver 負責把邏輯訊號轉成 CAN_H/CAN_L 的差分電壓。
物理層:為什麼大家都強調那兩顆 120 Ω

CAN 的邏輯不是看某一條線的電位,而是看 CAN_H 減 CAN_L 的差值:差值大於 0.9 V 是顯性(0),接近 0 V 是隱性(1)。這個設計讓兩條線一起被雜訊拉高時,接收端完全不受影響。
至於那兩顆 120 Ω 終端電阻,作用是消除訊號反射。少了它們,低速時可能還勉強能動,一旦超過 125 kbps 就會開始不穩 —— 這也是現場最常見的「線看起來都對,但就是不通」。

一個 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 | 幀間間隔 |
注意 Data Field 最多只有 8 bytes(Classic CAN)。這不是設計缺陷,而是刻意的取捨:短訊框代表確定的延遲上限,對需要即時反應的車用系統比「一次傳很多」更重要。
仲裁:ID 越小,講話越大聲

這個機制值得慢慢看:每個節點在送出 ID 的同時也在監聽匯流排。如果它送了隱性位元(1)卻讀到顯性(0),就立刻知道有人 ID 比自己小 → 直接退讓、轉為接收。
結果是:優先級最高的訊息自動獲勝,而且沒有任何資料遺失。ID = 0x001 的煞車指令,永遠比 0x100 的車窗控制先出去。
四種幀類型

日常通訊幾乎都是「資料幀」,但知道另外三種的存在會讓你更快看懂匯流排上的異常:錯誤幀是全網一起發出的警報,過載幀則是節點在說「我等一下」。
STM32 實作(bxCAN)
先初始化:設定時脈、模式與 bit timing。這一段通常由 CubeMX 產生,但知道它做了什麼很重要:
#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);
}
最容易卡住的算式是「一個 bit 要多少個時脈」:
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
意思是:APB1 的 42 MHz 除以 Prescaler 42,得到 1 MHz 的 CAN 時脈,再依 BS1/BS2 分配比例組成一個完整的 bit。算錯這一步,波特率就會差一點 —— 而 CAN 對波特率差異是零容忍的。
接著是濾波器。它不是「過濾雜訊」,而是決定這顆 MCU 要收哪些 ID 的訊息,沒設定好的話你可能一個中斷都不會進:
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);
}
發送與接收的程式碼很制式,但欄位要填對:
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);
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 實作(TWAI)
ESP32 內建的控制器叫 TWAI,Arduino 環境有現成函式庫可以直接用:
#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);
}
IDF 環境則是直接用 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();
}
}
實戰:多節點溫度監控
原文的範例把多個節點接成一條匯流排,每顆 MCU 各掛一個溫度感測器:
// ┌──────────┐ ┌──────────┐ ┌──────────┐
// │ Node A │ │ Node B │ │ Node C │
// │ STM32F4 │ CAN │ ESP32 │ CAN │ STM32F1 │
// │ DS18B20 │════════│ OLED │════════│ Relay │
// │ Sensor │ Bus │ Display │ Bus │ Actuator │
// └──────────┘ └──────────┘ └──────────┘
// 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);
}
這個架構可以延伸得很遠:節點變多不用拉新線,只要分配不同的 ID。這也是 CAN 在工業現場打敗 RS485 的原因 —— 它自己有優先級、有錯誤處理、有 ACK,不用你在應用層再補一套。
通訊不上?先查這五件事

這五項的排查順序建議照排:先用電表量匯流排兩端的 120 Ω(會量到約 60 Ω),再確認兩邊 baud rate 是否一致,最後才懷疑程式。CAN 的問題有九成在線與設定上。
錯誤計數器:節點是怎麼被請出場的

CAN controller 內部有兩個計數器:REC(接收錯誤)與 TEC(發送錯誤)。超過 127 進入 Error Passive,超過 255 就直接 Bus Off、不再參與通訊。
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");
}
這段檢查程式碼建議在開發階段就放進去,出問題時直接印出是哪一種錯誤:ACK Error 通常是沒有其他節點回應、Stuff Error 多半是波特率不一致、CRC Error 就是訊號品質問題。
總結
CAN 是一套「連錯誤都設計進去」的協定。四個重點:
- 差分訊號:看的是兩線差值,抗干擾能力來自這裡
- 非破壞性仲裁:ID 越小越優先,而且不會撞掉資料
- 短訊框 + CRC + ACK:延遲可預測,錯了也能立刻知道
- 錯誤計數器:壞節點會被自動隔離,不會拖垮全網
把這四點弄懂,之後去看 CAN FD 或車用的 J1939,都會發現它們只是同一套思路的延伸。
文章評論