http://www.henanjusheng.com 2026-07-17 09:10 廣州市微嵌計算機(jī)科技有限公司
CAN總線在工業(yè)自動化里的地位不用多說,PLC、變頻器、伺服驅(qū)動器、傳感器,大量設(shè)備都走CAN協(xié)議通信。但實(shí)際項目中,工控機(jī)如何正確接入CAN總線并穩(wěn)定收發(fā)數(shù)據(jù),坑其實(shí)不少。
本文不講CAN協(xié)議理論,直接講工程實(shí)操——從硬件接口選型、驅(qū)動配置到數(shù)據(jù)幀解析,把完整鏈路走一遍。
## 一、硬件層面:工控機(jī)怎么接CAN總線
工控機(jī)本身沒有CAN接口,需要通過擴(kuò)展方式接入。目前有三種主流方案:
**方案1:PCIe CAN卡。** 插在工控機(jī)主板的PCIe插槽上,延遲最低、穩(wěn)定性最好。適合固定安裝的工控機(jī)。選型時注意看芯片方案——SJA1000是經(jīng)典芯片,兼容性好但速率有限;較新的方案如CAN-FD支持更高速率(最高5Mbps數(shù)據(jù)段,仲裁段保持1Mbps)。另外要確認(rèn)卡上有沒有電氣隔離,沒隔離的卡在工業(yè)環(huán)境中雷擊或浪涌來了一燒就是一片。
**方案2:USB轉(zhuǎn)CAN模塊。** 即插即用,適合便攜調(diào)試或者接口緊張的場景。但USB總線本身有延遲抖動,高負(fù)載時可能出現(xiàn)丟幀。如果你的CAN總線負(fù)載率超過60%,不建議用USB方案。
**方案3:工控機(jī)板載CAN接口。** 部分ARM架構(gòu)工控機(jī)(如RK3568/RK3588平臺)原生帶有CAN控制器,只需在外部接CAN收發(fā)器芯片(如TJA1050或SN65HVD230)就能直接通信。這種方案硬件最簡潔,軟件驅(qū)動也由BSP提供,開發(fā)量最小。
不管哪種方案,CAN收發(fā)器到總線之間必須加120Ω終端電阻。這個說了無數(shù)遍的東西,現(xiàn)場還是經(jīng)常遇到?jīng)]接的,導(dǎo)致通信時好時壞,信號反射把波形搞得一塌糊涂。
## 二、驅(qū)動配置:讓系統(tǒng)認(rèn)到CAN設(shè)備
以Linux環(huán)境下最常用的SocketCAN為例,配置流程如下:
```bash
# 1. 查看CAN設(shè)備是否被識別
ip link show
# 如果是USB轉(zhuǎn)CAN模塊,通常識別為can0
# 如果是PCIe卡,可能識別為can0, can1等
# 2. 設(shè)置CAN總線的波特率
# 常見波特率:250kbps(經(jīng)典工業(yè)設(shè)備)、500kbps(汽車級)、1Mbps(高速CAN)
sudo ip link set can0 type can bitrate 500000
# 3. 啟用CAN接口
sudo ip link set can0 up
# 4. 驗證接口狀態(tài)
ip -details link show can0
# 正常輸出應(yīng)顯示state UP, bitrate 500000
# 5. 如果需要CAN-FD模式(設(shè)備支持的話)
sudo ip link set can0 type can bitrate 500000 dbitrate 2000000 fd on
sudo ip link set can0 up
```
Windows平臺下,各CAN卡廠家都會提供自己的驅(qū)動和API。如果是周立功的卡,用ZLGCAN庫;如果是PCAN,用PCAN-Basic庫。API各不相同,但核心操作都是:打開通道→設(shè)置波特率→初始化→發(fā)送/接收幀。
## 三、數(shù)據(jù)收發(fā):從底層到應(yīng)用層
SocketCAN的好處是它把CAN總線封裝成了網(wǎng)絡(luò)接口,你可以像操作UDP socket一樣收發(fā)CAN幀。
以下是一個C語言的收發(fā)示例:
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
int main() {
int sock;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame;
// 創(chuàng)建SocketCAN套接字
sock = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (sock < 0) {
perror("socket creation failed");
return 1;
}
// 指定CAN接口名
strcpy(ifr.ifr_name, "can0");
ioctl(sock, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind failed");
close(sock);
return 1;
}
// 發(fā)送一幀CAN數(shù)據(jù)
frame.can_id = 0x123; // CAN ID
frame.can_dlc = 8; // 數(shù)據(jù)長度
frame.data[0] = 0x01;
frame.data[1] = 0x02;
frame.data[2] = 0x03;
frame.data[3] = 0x04;
frame.data[4] = 0x05;
frame.data[5] = 0x06;
frame.data[6] = 0x07;
frame.data[7] = 0x08;
int bytes_sent = write(sock, &frame, sizeof(struct can_frame));
printf("Sent %d bytes, CAN ID: 0x%X\n", bytes_sent, frame.can_id);
// 接收CAN數(shù)據(jù)幀(阻塞模式)
int bytes_read = read(sock, &frame, sizeof(struct can_frame));
if (bytes_read > 0) {
printf("Received CAN ID: 0x%X, DLC: %d, Data: ",
frame.can_id, frame.can_dlc);
for (int i = 0; i < frame.can_dlc; i++) {
printf("%02X ", frame.data[i]);
}
printf("\n");
}
close(sock);
return 0;
}
```
## 四、過濾規(guī)則:別什么幀都收
CAN總線上可能有大量節(jié)點(diǎn)在不停發(fā)幀,如果全部接收會讓CPU忙不過來。SocketCAN支持硬件級過濾,只接收你關(guān)心的CAN ID。
```c
struct can_filter rfilter[2];
// 只接收CAN ID為0x100和0x200的幀
rfilter[0].can_id = 0x100;
rfilter[0].can_mask = CAN_SFF_MASK; // 標(biāo)準(zhǔn)幀,全匹配
rfilter[1].can_id = 0x200;
rfilter[1].can_mask = CAN_SFF_MASK;
setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
```
如果需要接收一段范圍的ID,利用mask的通配位實(shí)現(xiàn)。比如接收0x100到0x10F:
```c
rfilter[0].can_id = 0x100;
rfilter[0].can_mask = 0x7F0; // 低4位為通配,高7位匹配
```
## 五、實(shí)際項目中的幾個坑
**坑1:波特率必須完全一致。** 總線上所有節(jié)點(diǎn)的波特率設(shè)置必須一模一樣,差一點(diǎn)都不行。500kbps和499kbps看著差不多,實(shí)際通信會間歇性出錯?,F(xiàn)場排查時用示波器看CAN_H和CAN_L的波形周期,算出實(shí)際波特率。
**坑2:總線負(fù)載率別超70%。** CAN是仲裁式總線,負(fù)載太高時優(yōu)先級低的幀會頻繁丟幀。用can-utils里的cansend配合candump可以統(tǒng)計總線負(fù)載率:
```bash
# 持續(xù)監(jiān)控CAN總線流量
candump -t can0 | awk '{count++} END {print count}'
```
**坑3:多線程接收要用epoll。** 如果同時監(jiān)聽多個CAN通道或者CAN+串口混合使用,read()阻塞模式下容易顧此失彼。用epoll統(tǒng)一管理多個fd,效率高且不會漏幀。
**坑4:CAN幀時間戳。** 有些應(yīng)用需要精確記錄每幀的到達(dá)時間。SocketCAN默認(rèn)支持時間戳,但精度取決于內(nèi)核配置。需要納秒級精度的,開啟`CAN_RAW_RECV_OWN_MSGS`和`SO_TIMESTAMPNS`選項。
**坑5:擴(kuò)展幀和標(biāo)準(zhǔn)幀混用。** 標(biāo)準(zhǔn)幀CAN ID是11位,擴(kuò)展幀是29位。如果總線上兩種幀都有,過濾規(guī)則要分別設(shè)置。判斷幀類型用`frame.can_id & CAN_EFF_FLAG`。
## 六、應(yīng)用層協(xié)議解析
CAN只解決了"把數(shù)據(jù)傳過來"的問題,數(shù)據(jù)幀里那8個字節(jié)到底代表什么,取決于上層協(xié)議。工業(yè)現(xiàn)場最常見的是CANopen和DeviceNet。
以CANopen為例,一個PDO(過程數(shù)據(jù)對象)映射通常包含多個變量的數(shù)據(jù)。解析時需要根據(jù)對象字典(OD)的映射表,把8字節(jié)數(shù)據(jù)拆分成對應(yīng)的變量:
```c
// 假設(shè)PDO映射為:bytes[0-1]=溫度(int16), bytes[2-3]=轉(zhuǎn)速(int16), bytes[4-7]=位置(int32)
int16_t temperature = (frame.data[1] << 8) | frame.data[0];
int16_t rpm = (frame.data[3] << 8) | frame.data[2];
int32_t position = (frame.data[7] << 24) | (frame.data[6] << 16)
| (frame.data[5] << 8) | frame.data[4];
```
注意字節(jié)序問題。CANopen默認(rèn)小端序,但有些設(shè)備廠商不按規(guī)矩來,解析前務(wù)必確認(rèn)設(shè)備手冊的字節(jié)序定義。
## 結(jié)語
CAN總線通信配置看似簡單,但從硬件選型到驅(qū)動配置再到數(shù)據(jù)解析,每個環(huán)節(jié)都有需要注意的工程細(xì)節(jié)。建議在項目初期先搭建測試環(huán)境,用示波器和can-utils把通信鏈路跑通驗證后再進(jìn)入正式部署,避免在現(xiàn)場反復(fù)調(diào)試?yán)速M(fèi)時間。