CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32
Giải thích CAN bus: hai dây CAN_H, CAN_L, mức trội và mức lặn, khung tin có ID 11 bit, phân xử ưu tiên, CRC và ACK. Kèm hai chương trình ESP32 dùng bộ điều khiển TWAI: tự kiểm tra và hai nút gửi nhận.
CAN là mạng truyền dữ liệu sinh ra cho ô tô và nay có mặt trong máy công nghiệp, robot, thang máy, xe điện. Điểm khác biệt lớn nhất so với RS-485 là phần "thông minh" nằm ngay trong phần cứng: bộ điều khiển CAN tự phân xử khi hai nút gửi cùng lúc, tự tính CRC, tự xác nhận và tự gửi lại khi lỗi. ESP32 có sẵn một bộ điều khiển CAN, tên trong tài liệu là TWAI. Bài viết giải thích tín hiệu, khung tin và cơ chế phân xử ưu tiên của CAN, rồi dùng ESP32 để tự kiểm tra và để hai nút gửi nhận với nhau.
Đây là phần 1. Phần 2 nói về bộ lọc, tốc độ bit và xử lý lỗi.
1. Đường truyền CAN
- Hai dây CAN_H và CAN_L xoắn vào nhau, mang tín hiệu vi sai. Hai đầu đường dây có hai điện trở 120 Ω.
- Mỗi nút gồm một bộ điều khiển CAN (nằm trong vi điều khiển) và một bộ thu phát (IC riêng) nối ra đường dây.
- Không có nút chủ. Nút nào có dữ liệu thì gửi, và mọi nút khác đều nhận được khung đó.
- Tốc độ càng cao, đường dây càng phải ngắn: khoảng 40 m ở 1 Mbit/s, 100 m ở 500 kbit/s, 250 m ở 250 kbit/s, 500 m ở 125 kbit/s. Mọi nút trên một đường dây phải dùng cùng tốc độ.
2. Mức trội và mức lặn
- Lặn (recessive, bit 1): không nút nào tác động, hai dây cùng ở khoảng 2,5 V.
- Trội (dominant, bit 0): bộ thu phát kéo CAN_H lên khoảng 3,5 V và CAN_L xuống khoảng 1,5 V.
- Khi một nút phát bit trội và nút khác phát bit lặn, đường dây ở mức trội. Mức trội luôn thắng. Mọi cơ chế của CAN đều dựa trên tính chất này.
3. Khung tin CAN
- Mã nhận dạng (ID) 11 bit cho biết khung chứa nội dung gì, ví dụ "tốc độ động cơ" hay "nhiệt độ ắc quy". ID không phải địa chỉ nút nhận: nút nào quan tâm tới nội dung đó thì dùng.
- DLC là số byte dữ liệu, từ 0 đến 8.
- CRC 15 bit do bộ điều khiển tự tính và tự kiểm tra.
- ACK: nút gửi để trống một bit ở mức lặn; bất kỳ nút nào nhận đúng khung sẽ phát bit trội vào chỗ đó. Nếu không ai xác nhận, nút gửi coi như gửi lỗi và tự gửi lại.
- Ngoài khung chuẩn còn có khung mở rộng với ID 29 bit, và chuẩn CAN FD mang được nhiều dữ liệu hơn. Bài này chỉ dùng khung chuẩn.
4. Hai nút gửi cùng lúc thì sao?
- Nút nào cũng đọc lại đường dây trong lúc gửi. Trong phần ID, nếu một nút phát bit lặn mà đọc lại thấy mức trội, nó biết có nút khác đang gửi ID nhỏ hơn và lập tức ngừng.
- Khung của nút thắng không bị hỏng, không mất thời gian. Nút thua tự gửi lại ngay khi đường dây rảnh.
- Vì vậy ID càng nhỏ thì mức ưu tiên càng cao. Khi thiết kế, dữ liệu khẩn cấp được gán ID nhỏ.
- Hai nút không được gửi cùng một ID: khi đó không có gì để phân xử và dữ liệu sẽ đè lên nhau.
5. Chuẩn bị
- Hai mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340). Riêng chương trình 1 chỉ cần một mạch.
- Hai module thu phát CAN, nên dùng loại chạy 3,3 V (IC SN65HVD230 hoặc tương đương).
- Một đoạn dây đôi xoắn nối CANH với CANH, CANL với CANL.
- Breadboard và dây cắm.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Trình điều khiển CAN có sẵn trong gói này, không cần cài thêm thư viện.
6. Chương trình 1: tự kiểm tra với một mạch
Bình thường một nút CAN không gửi được nếu không có nút khác xác nhận. Bộ điều khiển của ESP32 có chế độ không cần xác nhận kèm tính năng tự nhận lại khung vừa gửi, đủ để kiểm tra phần mềm với một mạch. Nối một dây từ D5 sang D4 để tín hiệu phát quay thẳng về chân thu.
/*
PYWORLD.VN - CAN trên ESP32: tự kiểm tra bộ điều khiển CAN (TWAI) bằng cách tự gửi tự nhận một khung tin
Chỉ cần một mạch: nối một dây từ D5 (TX của CAN) sang D4 (RX của CAN), hoặc gắn một module thu phát CAN
Chế độ không cần xác nhận (NO_ACK) cho phép gửi khi trên đường truyền chưa có nút nào khác
*/
#include <driver/twai.h>
#define CAN_TX_PIN 5
#define CAN_RX_PIN 4
void setup() {
Serial.begin(115200);
twai_general_config_t cauHinh = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t)CAN_TX_PIN, (gpio_num_t)CAN_RX_PIN, TWAI_MODE_NO_ACK);
twai_timing_config_t tocDo = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t boLoc = TWAI_FILTER_CONFIG_ACCEPT_ALL();
if (twai_driver_install(&cauHinh, &tocDo, &boLoc) != ESP_OK || twai_start() != ESP_OK) {
Serial.println("\nKhong khoi dong duoc bo dieu khien CAN");
while (true) {
delay(1000);
}
}
Serial.println("\nCAN 500 kbit/s, che do tu kiem tra");
}
void loop() {
static uint8_t soDem = 0;
soDem++;
// Khung chuẩn: mã nhận dạng 11 bit, tối đa 8 byte dữ liệu
twai_message_t gui = {};
gui.identifier = 0x123;
gui.data_length_code = 4;
gui.data[0] = soDem;
gui.data[1] = 0xAA;
gui.data[2] = 0x55;
gui.data[3] = (uint8_t)(millis() / 1000);
gui.self = 1; // Yêu cầu tự nhận lại chính khung này
if (twai_transmit(&gui, pdMS_TO_TICKS(100)) != ESP_OK) {
Serial.println("Khong dua duoc khung vao hang doi gui");
delay(1000);
return;
}
twai_message_t nhan;
if (twai_receive(&nhan, pdMS_TO_TICKS(200)) == ESP_OK) {
Serial.printf("Nhan lai ID 0x%03X, %d byte:", (unsigned)nhan.identifier, nhan.data_length_code);
for (int i = 0; i < nhan.data_length_code; i++) {
Serial.printf(" %02X", nhan.data[i]);
}
Serial.println(nhan.data[0] == soDem ? " -> dung" : " -> SAI");
} else {
Serial.println("Khong nhan lai duoc khung: kiem tra day noi D5 voi D4 hoac module thu phat");
}
delay(1000);
}
7. Sơ đồ nối module thu phát
- Tháo dây nối D5 với D4 của chương trình 1 trước khi nối module.
- Module thu phát CAN thường có sẵn một điện trở 120 Ω. Với hai module ở hai đầu dây là vừa đủ. Khi mắc nhiều nút, chỉ giữ điện trở ở hai nút đầu và cuối.
- Module dùng IC chạy 5 V (ví dụ TJA1050) phát mức 5 V ở chân thu: phải qua mạch chia áp trước khi vào D4.
- Nối chung GND giữa các nút khi chúng dùng nguồn khác nhau.
8. Chương trình 2: hai nút gửi và nhận
Nạp cùng chương trình cho hai mạch, chỉ đổi ID_CUA_TOI: một mạch 0x101, mạch kia 0x102. Mỗi nút gửi một khung 4 byte mỗi giây và in mọi khung nhận được.
/*
PYWORLD.VN - CAN trên ESP32: hai nút gửi và nhận khung tin qua module thu phát CAN
Nạp cho hai mạch, mỗi mạch một mã nhận dạng riêng (ID_CUA_TOI = 0x101 và 0x102); nối CANH với CANH, CANL với CANL
Mỗi nút gửi một khung mỗi giây và in mọi khung nhận được; module thu phát nối TX = D5, RX = D4
*/
#include <driver/twai.h>
#define CAN_TX_PIN 5
#define CAN_RX_PIN 4
const uint32_t ID_CUA_TOI = 0x101; // Đổi thành 0x102 khi nạp cho mạch thứ hai
const unsigned long CHU_KY_GUI_MS = 1000;
uint16_t soDem = 0;
unsigned long lucGuiCuoi = 0;
void setup() {
Serial.begin(115200);
twai_general_config_t cauHinh = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t)CAN_TX_PIN, (gpio_num_t)CAN_RX_PIN, TWAI_MODE_NORMAL);
twai_timing_config_t tocDo = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t boLoc = TWAI_FILTER_CONFIG_ACCEPT_ALL();
if (twai_driver_install(&cauHinh, &tocDo, &boLoc) != ESP_OK || twai_start() != ESP_OK) {
Serial.println("\nKhong khoi dong duoc bo dieu khien CAN");
while (true) {
delay(1000);
}
}
Serial.printf("\nNut CAN ID 0x%03X, 500 kbit/s\n", (unsigned)ID_CUA_TOI);
}
void loop() {
// Gửi định kỳ: bộ đếm 16 bit và số giây đã chạy, byte cao trước
if (millis() - lucGuiCuoi >= CHU_KY_GUI_MS) {
lucGuiCuoi = millis();
soDem++;
uint16_t soGiay = (uint16_t)(millis() / 1000);
twai_message_t gui = {};
gui.identifier = ID_CUA_TOI;
gui.data_length_code = 4;
gui.data[0] = soDem >> 8;
gui.data[1] = soDem & 0xFF;
gui.data[2] = soGiay >> 8;
gui.data[3] = soGiay & 0xFF;
esp_err_t ketQua = twai_transmit(&gui, pdMS_TO_TICKS(50));
if (ketQua == ESP_OK) {
Serial.printf("Gui ID 0x%03X: dem %u, %u giay\n", (unsigned)ID_CUA_TOI, soDem, soGiay);
} else {
// Hàng đợi đầy hoặc bộ điều khiển đã ngừng: thường do không có nút nào khác xác nhận
Serial.printf("Khong gui duoc (loi 0x%X): kiem tra nut con lai va day CANH, CANL\n", (unsigned)ketQua);
}
}
// Nhận mọi khung đang chờ; nút nào trên đường truyền cũng thấy mọi khung
twai_message_t nhan;
while (twai_receive(&nhan, 0) == ESP_OK) {
Serial.printf("Nhan ID 0x%03X, %d byte:", (unsigned)nhan.identifier, nhan.data_length_code);
for (int i = 0; i < nhan.data_length_code; i++) {
Serial.printf(" %02X", nhan.data[i]);
}
if (nhan.data_length_code == 4) {
Serial.printf(" (dem %u, %u giay)", (nhan.data[0] << 8) | nhan.data[1], (nhan.data[2] << 8) | nhan.data[3]);
}
Serial.println();
}
delay(5);
}
- Chương trình chỉ điền ID, số byte và dữ liệu vào cấu trúc
twai_message_t. Mọi việc còn lại do bộ điều khiển lo. - Ý nghĩa của từng byte do người thiết kế hệ thống quy ước. Ở đây hai byte đầu là bộ đếm, hai byte sau là số giây, byte cao trước.
- Bộ lọc đang để nhận mọi ID. Phần 2 sẽ cho nút chỉ nhận các ID mình cần.
9. Kết quả mong đợi
- Chương trình 1: mở Tools > Serial Monitor ở 115200 baud. Mỗi giây hiện một khung nhận lại đúng. Rút dây D5 với D4: báo không nhận lại được.
- Chương trình 2: mỗi mạch in khung mình gửi và khung của mạch kia.
- Tắt một mạch: mạch còn lại bắt đầu báo không gửi được sau vài giây, vì không còn ai xác nhận khung của nó.
10. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Chương trình 1 không nhận lại được khung | Chưa nối D5 với D4, hoặc sai chân | Nối đúng một dây D5 sang D4; nếu dùng module thì kiểm tra nguồn và hai dây TX, RX |
| Hai nút không thấy nhau | Nối ngược CANH với CANL, hai nút khác tốc độ, hoặc nối nhầm TX với RX của module | CANH nối CANH; cùng 500 kbit/s; D5 vào chân phát, D4 vào chân thu |
| Báo không gửi được liên tục | Trên đường dây chỉ có một nút đang chạy nên không ai xác nhận | Bật nút còn lại; kiểm tra dây CANH, CANL |
| Chạy được với dây ngắn, lỗi với dây dài | Thiếu điện trở đầu cuối, hoặc tốc độ quá cao so với chiều dài | Đặt 120 Ω ở hai đầu (đo được 60 Ω giữa CANH và CANL khi tắt nguồn); giảm tốc độ |
| ESP32 khởi động chập chờn khi gắn module | GPIO5 là chân cấu hình lúc khởi động | Đổi chân TX của CAN sang chân khác, ví dụ GPIO21, và sửa CAN_TX_PIN |
| Hai nút cùng ID làm dữ liệu sai | Trùng mã nhận dạng | Mỗi loại khung chỉ do một nút gửi |
11. Xem thêm
- Ngắt GPIO trên ESP32: nguồn sự kiện thường được gửi lên CAN.
- ESP32 điều khiển module relay: một nút chấp hành điển hình trên mạng CAN.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm