0x6A Logbook

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

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

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

CAN Bus 通訊協定完整教學封面

前言:車上那麼多控制器,靠一條線就講完

一台車裡有引擎電腦、煞車、車窗、儀表板,全部都要互相講話。如果每個裝置之間都拉一組線,線束會比引擎還重。CAN Bus 的答案是:全部掛在同一對線上,用 ID 決定誰先講。

它最厲害的地方有兩個:抗干擾(量的是兩條線的電壓差)與非破壞性仲裁(同時發話也不會撞掉資料)。這兩個設計讓它從 1980 年代活到今天,還在工業與車用領域霸佔位置。

一條匯流排掛很多節點

CAN Bus 多節點架構

每個節點都是「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 收發器型號

一個 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 越小,講話越大聲

CAN 非破壞性仲裁流程

這個機制值得慢慢看:每個節點在送出 ID 的同時也在監聽匯流排。如果它送了隱性位元(1)卻讀到顯性(0),就立刻知道有人 ID 比自己小 → 直接退讓、轉為接收。

結果是:優先級最高的訊息自動獲勝,而且沒有任何資料遺失。ID = 0x001 的煞車指令,永遠比 0x100 的車窗控制先出去。

四種幀類型

CAN 四種幀類型

日常通訊幾乎都是「資料幀」,但知道另外三種的存在會讓你更快看懂匯流排上的異常:錯誤幀是全網一起發出的警報,過載幀則是節點在說「我等一下」。

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,不用你在應用層再補一套。

通訊不上?先查這五件事

CAN 通訊不上的五個檢查點

這五項的排查順序建議照排:先用電表量匯流排兩端的 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,都會發現它們只是同一套思路的延伸。

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

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