CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32
Phần 2 về CAN bus với ESP32: đặt bộ lọc nhận theo mã lọc và mặt nạ, chọn tốc độ bit theo chiều dài dây, hiểu ba trạng thái lỗi của nút và viết chương trình theo dõi bộ đếm lỗi, tự hồi phục sau bus-off.
Phần 1 đã cho hai nút CAN gửi nhận với nhau. Khi mạng có nhiều nút và chạy trong môi trường thật, có ba việc cần biết thêm: làm sao để mỗi nút chỉ nhận những khung nó cần, chọn tốc độ bit nào cho chiều dài dây, và nút phản ứng thế nào khi đường truyền có lỗi. Bài viết giải thích bộ lọc nhận, quan hệ giữa tốc độ và chiều dài, ba trạng thái lỗi của một nút CAN, rồi thực hành trên ESP32 với bộ điều khiển TWAI.
Bài dùng lại sơ đồ nối module thu phát CAN và các chân D5, D4 của phần 1.
1. Bộ lọc nhận
- Trên CAN, nút nào cũng thấy mọi khung. Nếu để chương trình tự xem từng khung rồi bỏ đi, vi điều khiển sẽ bận rộn vô ích khi đường truyền đông.
- Bộ điều khiển CAN có bộ lọc nhận bằng phần cứng: chỉ khung có ID phù hợp mới được đưa vào hàng đợi nhận.
- Bộ lọc gồm hai số: mã lọc (ID mong muốn) và mặt nạ (bit nào phải khớp). Cho phép vài bit thấp "tùy ý" là nhận được cả một dải ID liền nhau.
- Vì vậy khi thiết kế, các khung liên quan thường được xếp ID cạnh nhau để một nút lọc được cả nhóm.
Trên ESP32, với khung chuẩn và bộ lọc đơn, 11 bit ID được đặt ở 11 bit cao nhất của mã lọc 32 bit, tức dịch trái 21 bit. Trong mặt nạ, bit 1 nghĩa là không quan tâm, bit 0 nghĩa là phải khớp. Quy ước này ngược với nhiều bộ điều khiển CAN khác, nên cần xem kỹ khi chuyển mã từ nền tảng khác sang.
2. Tốc độ bit và chiều dài đường dây
- Khi phân xử và khi xác nhận, mọi nút phải thấy cùng một mức trên đường dây trong cùng một bit. Tín hiệu cần thời gian để đi hết chiều dài dây và quay về, nên bit càng ngắn thì dây càng phải ngắn.
- Mọi nút phải đặt cùng một tốc độ. Một nút sai tốc độ sẽ coi mọi khung là lỗi và liên tục phát khung báo lỗi, làm hỏng việc truyền của các nút khác.
- Trong chương trình, đổi tốc độ bằng cách thay hằng
TWAI_TIMING_CONFIG_500KBITS()bằng hằng tương ứng.
3. Ba trạng thái lỗi của một nút
- Mỗi nút có hai bộ đếm: lỗi phát và lỗi thu. Gửi lỗi một lần, bộ đếm lỗi phát tăng 8; gửi thành công một lần, giảm 1.
- Lỗi chủ động (cả hai bộ đếm dưới 128): trạng thái bình thường. Thấy lỗi, nút phát khung báo lỗi để mọi nút cùng bỏ khung hỏng.
- Lỗi bị động (một bộ đếm từ 128 trở lên): nút vẫn gửi nhận nhưng không còn được phá khung của nút khác, và phải chờ lâu hơn trước khi gửi lại.
- Bus-off (bộ đếm lỗi phát trên 255): nút tự ngắt khỏi đường truyền, không gửi và không xác nhận gì nữa.
- Hồi phục: nút chỉ quay lại sau khi chương trình yêu cầu và đường truyền rảnh đủ lâu.
Cơ chế này bảo vệ cả mạng: một nút có bộ thu phát hỏng hoặc đặt sai tốc độ sẽ nhanh chóng tự loại mình, thay vì làm tê liệt mọi nút khác. Riêng trường hợp nút gửi mà không có ai xác nhận, bộ đếm lỗi phát chỉ tăng tới 128 rồi dừng: nút ở trạng thái lỗi bị động và tiếp tục thử gửi, không vào bus-off.
4. Chuẩn bị
- Cho chương trình 1: một mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340) và một dây cắm nối D5 với D4.
- Cho chương trình 2: hai mạch ESP32 và hai module thu phát CAN, nối như ở phần 1. Mạch thứ hai chạy chương trình gửi nhận của phần 1.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Không cần cài thêm thư viện.
5. Chương trình 1: bộ lọc nhận
Chương trình đặt bộ lọc cho dải ID từ 0x100 đến 0x10F, rồi tự gửi lần lượt bảy ID và in ra ID nào lọt qua bộ lọc. Nó chạy ở chế độ tự kiểm tra nên chỉ cần một mạch với một dây nối D5 sang D4.
/*
PYWORLD.VN - CAN trên ESP32: bộ lọc nhận theo mã nhận dạng (ID)
Bộ lọc chỉ cho qua các khung có ID từ 0x100 đến 0x10F; chương trình tự gửi lần lượt nhiều ID và in ID nào lọt qua
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
*/
#include <driver/twai.h>
#define CAN_TX_PIN 5
#define CAN_RX_PIN 4
// Bộ lọc đơn cho khung chuẩn: 11 bit ID nằm ở 11 bit cao nhất của mã lọc 32 bit (dịch trái 21 bit).
// Trong mặt nạ, bit 0 là "phải khớp", bit 1 là "không quan tâm".
const uint32_t ID_GOC = 0x100;
const uint32_t BIT_PHAI_KHOP = 0x7F0; // 7 bit cao của ID phải khớp, 4 bit thấp tùy ý: nhận 0x100 đến 0x10F
const uint32_t MA_LOC = ID_GOC << 21;
const uint32_t MAT_NA_LOC = ~(BIT_PHAI_KHOP << 21);
const uint32_t ID_THU[] = {0x0FF, 0x100, 0x105, 0x10F, 0x110, 0x200, 0x7FF};
const int SO_ID_THU = sizeof(ID_THU) / sizeof(ID_THU[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_NO_ACK);
twai_timing_config_t tocDo = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t boLoc = {};
boLoc.acceptance_code = MA_LOC;
boLoc.acceptance_mask = MAT_NA_LOC;
boLoc.single_filter = true;
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("\nMa loc 0x%08X, mat na 0x%08X: nhan ID 0x%03X den 0x%03X\n",
(unsigned)MA_LOC, (unsigned)MAT_NA_LOC, (unsigned)ID_GOC, (unsigned)(ID_GOC | (~BIT_PHAI_KHOP & 0x7FF)));
}
void loop() {
Serial.println();
for (int i = 0; i < SO_ID_THU; i++) {
twai_message_t gui = {};
gui.identifier = ID_THU[i];
gui.data_length_code = 1;
gui.data[0] = i;
gui.self = 1; // Tự nhận lại: khung đi qua bộ lọc giống như khung của nút khác
twai_transmit(&gui, pdMS_TO_TICKS(100));
twai_message_t nhan;
bool lotQua = twai_receive(&nhan, pdMS_TO_TICKS(100)) == ESP_OK;
Serial.printf("Gui ID 0x%03X: %s\n", (unsigned)ID_THU[i], lotQua ? "lot qua bo loc" : "bi loc bo");
}
delay(5000);
}
Muốn lọc dải khác, đổi ID_GOC và BIT_PHAI_KHOP. Ví dụ để chỉ nhận đúng một ID, đặt BIT_PHAI_KHOP bằng 0x7FF.
6. Chương trình 2: theo dõi lỗi và tự hồi phục
Chương trình gửi một khung mỗi 200 ms, in bộ đếm lỗi mỗi giây và phản ứng với các cảnh báo của bộ điều khiển. Khi vào bus-off, nó yêu cầu hồi phục và khởi động lại bộ điều khiển khi hồi phục xong.
/*
PYWORLD.VN - CAN trên ESP32: theo dõi bộ đếm lỗi, các trạng thái lỗi và tự hồi phục sau khi bị ngắt khỏi đường truyền (bus-off)
Nút gửi một khung mỗi 200 ms và in bộ đếm lỗi mỗi giây; thử rút nút còn lại hoặc chập CANH với CANL để xem trạng thái đổi
Module thu phát CAN nối TX = D5, RX = D4; cần ít nhất một nút khác trên đường truyền để việc gửi được xác nhận
*/
#include <driver/twai.h>
#define CAN_TX_PIN 5
#define CAN_RX_PIN 4
const uint32_t ID_CUA_TOI = 0x201;
const unsigned long CHU_KY_GUI_MS = 200;
const unsigned long CHU_KY_IN_MS = 1000;
unsigned long lucGuiCuoi = 0;
unsigned long lucInCuoi = 0;
uint32_t soGuiThanhCong = 0;
const char *tenTrangThai(twai_state_t trangThai) {
switch (trangThai) {
case TWAI_STATE_STOPPED: return "da dung";
case TWAI_STATE_RUNNING: return "dang chay";
case TWAI_STATE_BUS_OFF: return "bus-off";
case TWAI_STATE_RECOVERING: return "dang hoi phuc";
default: return "khong ro";
}
}
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);
}
}
// Chọn các cảnh báo muốn nhận
twai_reconfigure_alerts(TWAI_ALERT_TX_SUCCESS | TWAI_ALERT_ERR_PASS | TWAI_ALERT_ERR_ACTIVE
| TWAI_ALERT_BUS_OFF | TWAI_ALERT_BUS_RECOVERED, NULL);
Serial.println("\nTheo doi loi CAN, 500 kbit/s");
}
void loop() {
// Xử lý cảnh báo của bộ điều khiển
uint32_t canhBao = 0;
if (twai_read_alerts(&canhBao, pdMS_TO_TICKS(10)) == ESP_OK) {
if (canhBao & TWAI_ALERT_TX_SUCCESS) {
soGuiThanhCong++;
}
if (canhBao & TWAI_ALERT_ERR_PASS) {
Serial.println(">> Chuyen sang trang thai loi bi dong: bo dem loi da toi 128");
}
if (canhBao & TWAI_ALERT_ERR_ACTIVE) {
Serial.println(">> Tro lai trang thai loi chu dong: duong truyen da on");
}
if (canhBao & TWAI_ALERT_BUS_OFF) {
Serial.println(">> BUS-OFF: bo dem loi phat vuot 255, nut tu ngat khoi duong truyen. Bat dau hoi phuc...");
twai_initiate_recovery();
}
if (canhBao & TWAI_ALERT_BUS_RECOVERED) {
Serial.println(">> Da hoi phuc: khoi dong lai bo dieu khien");
twai_start();
}
}
// Gửi định kỳ, chỉ khi bộ điều khiển đang chạy. Khung chưa được xác nhận sẽ nằm lại trong hàng đợi và được gửi lại
twai_status_info_t trangThai;
twai_get_status_info(&trangThai);
if (trangThai.state == TWAI_STATE_RUNNING && millis() - lucGuiCuoi >= CHU_KY_GUI_MS) {
lucGuiCuoi = millis();
twai_message_t gui = {};
gui.identifier = ID_CUA_TOI;
gui.data_length_code = 2;
gui.data[0] = (uint8_t)(soGuiThanhCong >> 8);
gui.data[1] = (uint8_t)soGuiThanhCong;
twai_transmit(&gui, 0); // Hàng đợi đầy thì bỏ khung này, không chờ
}
// Bỏ các khung nhận được để hàng đợi không đầy
twai_message_t nhan;
while (twai_receive(&nhan, 0) == ESP_OK) {
}
if (millis() - lucInCuoi >= CHU_KY_IN_MS) {
lucInCuoi = millis();
Serial.printf("Trang thai: %-13s | loi phat %3u | loi thu %3u | da gui %u | dang cho gui %u\n",
tenTrangThai(trangThai.state), (unsigned)trangThai.tx_error_counter, (unsigned)trangThai.rx_error_counter,
(unsigned)soGuiThanhCong, (unsigned)trangThai.msgs_to_tx);
}
}
twai_reconfigure_alerts()chọn những cảnh báo chương trình muốn nhận;twai_read_alerts()đọc chúng.twai_get_status_info()trả về trạng thái, hai bộ đếm lỗi và số khung đang chờ gửi.- Sau bus-off phải gọi
twai_initiate_recovery(); khi có cảnh báo đã hồi phục thì gọitwai_start()để chạy lại.
7. Kết quả mong đợi
- Chương trình 1: ba ID 0x100, 0x105, 0x10F lọt qua; bốn ID còn lại bị lọc bỏ.
- Chương trình 2, lúc bình thường: hai bộ đếm lỗi bằng 0, số khung đã gửi tăng đều.
- Rút nút còn lại: bộ đếm lỗi phát tăng nhanh tới 128, có thông báo chuyển sang lỗi bị động, hàng đợi gửi đầy. Cắm lại: bộ đếm giảm dần về 0.
- Chập CANH với CANL trong vài giây: nút vào bus-off rồi tự hồi phục sau khi gỡ chập.
8. Lưu ý khi thiết kế mạng CAN
- Lập bảng ID trước: mỗi loại khung một ID, do đúng một nút gửi; ID nhỏ cho dữ liệu khẩn cấp; các khung cùng nhóm đặt ID cạnh nhau để dễ lọc.
- Chu kỳ gửi: đường truyền nên rảnh phần lớn thời gian. Ở 500 kbit/s, một khung 8 byte chiếm khoảng 0,25 ms; vài trăm khung mỗi giây là mức thoải mái.
- Giám sát: trong thiết bị thật, nên ghi lại số lần vào lỗi bị động và bus-off để phát hiện sớm dây lỏng hoặc nhiễu.
- Chuẩn lớp trên: CAN chỉ quy định khung tin. Ý nghĩa của ID và dữ liệu do các chuẩn như CANopen, J1939 hoặc do người thiết kế quy ước.
9. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Bộ lọc không cho khung nào qua | Quên dịch ID 21 bit, hoặc hiểu ngược ý nghĩa của mặt nạ | Mã lọc = ID dịch trái 21 bit; trong mặt nạ, bit 1 là không quan tâm |
| Bộ lọc cho qua nhiều hơn dự kiến | Quá nhiều bit được đặt là tùy ý | Tăng số bit phải khớp trong BIT_PHAI_KHOP |
| Bộ đếm lỗi phát đứng ở 128 | Không có nút nào khác xác nhận | Kiểm tra nút còn lại và dây CANH, CANL |
| Vào bus-off liên tục | Chập hoặc đứt một dây, hai nút khác tốc độ, bộ thu phát hỏng | Đo 60 Ω giữa CANH và CANL khi tắt nguồn; đối chiếu tốc độ; thay module |
| Bộ đếm lỗi thu tăng dù nút không gửi | Nút khác đang gửi ở tốc độ khác, hoặc đường dây nhiễu | Tìm nút sai tốc độ; kiểm tra điện trở đầu cuối và dây xoắn |
| Sau bus-off nút không gửi lại | Quên gọi hồi phục hoặc quên khởi động lại bộ điều khiển | Gọi twai_initiate_recovery() rồi twai_start() như trong chương trình |
10. Xem thêm
- Lưu dữ liệu vào bộ nhớ flash của ESP32: ghi lại số lần lỗi để chẩn đoán.
- Deep Sleep trên ESP32, hữu ích cho nút CAN chạy pin.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm