文章目录
- 原启
- UART 走CAN收发器
- CH343 模拟CAN发送
- CPP ASIO SocketCAN
- VXCAN
- Github Link
原启
早些年自动驾驶激光雷达还不支持PTP之类的时间同步, 很多都是用PPS时间同步, 激光雷达一般装的离控制器或者GNSS天线较远, 车上的线束一般数据电源各种都包在一起的, 如果3.3V直接从域控制器出, 信号将惨不忍睹, 为了解决长距离3.3V PPS传输受干扰的问题, 各种骚操作都整出来:
- 同轴线, 走SMA或者FAKRA接口出
- 高电压大电流, 最简单的拿个三极管反相, 12V出, 接收端再反过来, 同时增大传输线中的电流, 抗干扰效果不错
直到看到某家域控制器居然是拿CANFD收发器来传输PPS, 顿时三观炸裂, 直呼卧槽.
CAN收发器通俗讲只是电平转换, 和串口分TTL, RS232, RS485类似, 协议大体相似, 只是传输电平不同. 传PPS自然没有问题. 那大胆一点, UART远距离传输直接走CAN收发器是不是也行. 毕竟国产的CAN收发器价格上和RS232的电平转换芯片也相差不多了.
UART 走CAN收发器
那就这样测试一下
实物图
果然可以
需要注意的地方:
- UART收发独立, 是全双工的, 如果收发不能时间上错开, 那收发引脚就各自接一片CAN收发器
那再扩展一下, B站上都能拿CH340来当音频设备放歌了, 这CH343能到6Mbps, 模拟下500Kbit/s的CAN发送, 和CAN分析仪通信好像问题不大.
CH343 模拟CAN发送
串口通常的帧格式为 空闲高, 1低电平起始位 + 5到9数据位 + 0或1校验位 + 1到2停止位
以CAN标准数据帧为例, 刚好也是空闲高电平, 1bit帧起始(0), 11bitID, 1bit远程帧, 1bit扩展帧, 1bit占位符(0), 4位的数据长度代码, 0~8字节的数据, 15+1bit的CRC, 1+1bit的ACK(01), 7bit帧结束(1111111), 7bit帧间隔(1111111), 来源 Introduction to the Controller Area Network (CAN) (Rev. B)
还要注意填充位: 在相同逻辑电平的五个连续位之后, 需要插入1bit相反电平, 不然会被认为错误帧.
逻辑分析仪抓出来的波形:
CH343模拟CAN发送 的原理分析:
- 串口助手上可以设置数据位5 6 7 8, 校验位None Even Mark Odd Space, 停止位 1 1.5 2, 也就是串口一个字节最少是1起始位+5数据位+0校验位+1停止位, 也就是7bit, 最多1-8-1-2, 也就是14bit
- 这里500Kbit/s进行CAN测试, CAN或CANFD的位的采样点一般在 75% ~ 87.5%, 串口的起始位和停止位控制不了, 所以要尽量让CAN的采样点落在串口的数据位, 把数据位搞的多多的, 停止位和校验位搞的少少的
- 给串口4Mbps, 要想串口一字节(一帧的意思)对应500Kbit/s的CAN的一位, 那串口一个字节8bit, 对应 1起始位 + 0停止位 + 1停止位 搞到最小, 剩下最多的 6bit 都给数据, CAN采样点 80% 会落在 8 * 80% = 6.4, 刚好在数据的最后1bit, 能控制就挺好
上面逻辑分析仪抓出来的CAN Bits
行数据 000100100011000001010101011111001101110010001011111111
1bit膨胀成UART的1帧(字节), 直接翻译成十六进制串口数据, 0 翻译成00, 1翻译成全1, 6bit数据对应0b111111, 也就是0x3F, 这里测试发现直接写成0xFF也没有问题
00 00 00 FF 00 00 FF 00 00 00 FF FF 00 00 00 00 00 FF 00 FF 00 FF 00 FF 00 FF FF FF FF FF 00 00 FF FF 00 FF FF FF 00 00 FF 00 00 00 FF 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
因为串口空闲也是高电平, 所以最后的FF都不发了, ACK是其它收发器回的, 也不用发, 最终发的是
00 00 00 FF 00 00 FF 00 00 00 FF FF 00 00 00 00 00 FF 00 FF 00 FF 00 FF 00 FF FF FF FF FF 00 00 FF FF 00 FF FF FF 00 00 FF 00 00 00 FF
连线
实物
先打开CAN分析仪 500Kbit/s, 80%采样点, 开启内部终端电阻.
再打开逻辑分析仪, 给通道同时配置UART和CAN协议解析.
最后打开串口调试助手, 4Mbps, 数据位6, 校验位None, 停止位1, 十六进制定时发送, 周期10ms
在CAN分析仪上位机中可以看到收到了数据(指示灯有几秒一次的闪红错误帧, 大概有1/19~1/20的错误帧)
逻辑分析仪的抓取
放大一下
功能上好像还点效果的. 至于偶发的错误帧, 大概是下图这样某帧发送32字节后, 没有连续发送的情形, 导致CAN的CRC算出来不正确, 可能是串口4M高速率的整包被USB截断了?
串口定时发送周期从 10ms 改成 100ms 后, 错误帧出现的概率依然不太变化, 情况并没有改善.
如果不用USB的包传输, 直接使用MCU的串口模拟CAN发送, 应该不会出现这样的问题? 但也要注意MCU的串口一般16字节FIFO, 可能要用DMA来搬数据, 不要出现传16字节断一会的情况.
换成 2Mbps, 数据位6, 校验位None, 停止位1, 十六进制定时发送, 周期10ms, 对应的CAN速率250kbit/s, 效果好了很多, 几乎不出现错误帧了:
下面用 Cpp Asio SocketCAN 写下任意帧发送的代码试试, 在WSL里面测试一下.
CPP ASIO SocketCAN
先把CH343挂载进WSL里面:
下面的代码先把SocketCAN的数据的每1bit, 扩充成串口的每一个字节, bit 0对应0x00, bit1 对应0xFF, 然后计算CRC15, 最后插入填充位, 进行发送:
#include <linux/can.h>#include <asio.hpp>
#include <chrono>
#include <cstdbool>
#include <cstdint>
#include <iostream>
#include <thread>
#include <vector>int main(int argc, char *argv[]) {asio::io_context iocxt;// serial, 1 start bit, 8 data bits, 1 stop bit, no parity, 2.5M baud// CAN 100K, Serial 1M, 1 start bit, 8 data bits, 1 stop bit, no parity// CAN 250K, Serial 2M, 1 start bit, 6 data bits, 1 stop bit, no parity// CAN 500K, Serial 4M, 1 start bit, 6 data bits, 1 stop bit, no parityasio::serial_port ser(iocxt, "/dev/ttyACM0");ser.set_option(asio::serial_port::baud_rate(2000000));ser.set_option(asio::serial_port::character_size(6));ser.set_option(asio::serial_port::stop_bits(asio::serial_port::stop_bits::one));ser.set_option(asio::serial_port::parity(asio::serial_port::parity::none));ser.set_option(asio::serial_port::flow_control(asio::serial_port::flow_control::none));if (!ser.is_open()) {std::cerr << "Failed to open serial port" << std::endl;return -1;}constexpr uint8_t Bit0 = 0x00;constexpr uint8_t Bit1 = 0xFF;// can crc15 calculationauto crc15 = [&](const std::vector<uint8_t> &data) {bool crc[15] = {0};for (int i = 0; i < data.size(); i++) {bool inv = (data[i] == Bit1) ^ crc[14];crc[14] = crc[13] ^ inv;crc[13] = crc[12];crc[12] = crc[11];crc[11] = crc[10];crc[10] = crc[9] ^ inv;crc[9] = crc[8];crc[8] = crc[7] ^ inv;crc[7] = crc[6] ^ inv;crc[6] = crc[5];crc[5] = crc[4];crc[4] = crc[3] ^ inv;crc[3] = crc[2] ^ inv;crc[2] = crc[1];crc[1] = crc[0];crc[0] = inv;}uint16_t res = 0;for (int i = 0; i < 15; i++) {res |= crc[i] << i;}return res;};// fill stuff bitsauto fsb_insert = [&](std::vector<uint8_t> &data) {uint8_t count = 0;bool last = true;std::vector<uint8_t> newdata;for (auto it = data.begin(); it != data.end(); it++) {bool current = *it == Bit0 ? false : true;newdata.push_back(*it);if (current == last) {count++;} else {count = 1;}if (count == 5) {newdata.push_back(current ? Bit0 : Bit1);count = 1;last = !current;} else {last = current;}}return newdata;};// socketcan frame to serial frameauto can2ser = [&](const can_frame &frame) {std::vector<uint8_t> data;uint32_t id = 0;bool is_extended = frame.can_id & CAN_EFF_FLAG ? true : false;bool is_remote = frame.can_id & CAN_RTR_FLAG;uint8_t dlc = frame.len;data.push_back(Bit0); // SOF, Start of frameif (is_extended) {id = frame.can_id & CAN_EFF_MASK;// High 11 bits idfor (uint8_t i = 0; i < 11; i++) {uint8_t tmp = ((uint8_t)(id >> (28 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}data.push_back(Bit1); // SRR, Substitute remote requestdata.push_back(Bit1); // IDE, Identifier extension// Low 18 bits idfor (uint8_t i = 0; i < 18; i++) {uint8_t tmp = ((uint8_t)(id >> (17 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}// RTRif (is_remote) {data.push_back(Bit1);} else {data.push_back(Bit0);}data.push_back(Bit0); // RB1, reserved bit 1} else {id = frame.can_id & CAN_SFF_MASK;for (uint8_t i = 0; i < 11; i++) {uint8_t tmp = ((uint8_t)(id >> (10 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}// RTRif (is_remote) {data.push_back(Bit1);} else {data.push_back(Bit0);}data.push_back(Bit0); // IDE, Identifier extension}data.push_back(Bit0); // RB0, reserved bit 0// 4 bits DLCfor (uint8_t i = 0; i < 4; i++) {uint8_t tmp = ((uint8_t)(dlc >> (3 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}// 0~64 bits datafor (uint8_t i = 0; i < frame.len; i++) {for (uint8_t j = 0; j < 8; j++) {uint8_t tmp = ((frame.data[i] >> (7 - j)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}}// CRC15uint16_t crc = crc15(data);for (uint8_t i = 0; i < 15; i++) {uint8_t tmp = ((crc >> (14 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}data.push_back(Bit1); // CRC Delimiter// ACK, ACK Delimiter, EOF, IFS will not be caredreturn data;};// test can2serauto test = [&]() {static uint8_t cnt = 0;can_frame frame;// use 0x92345678, 8 bytes of 0x3C to test fill stuff bitsframe.can_id = 0x12345678 | CAN_EFF_FLAG;frame.len = 8;frame.__pad = 0;frame.__res0 = 0;frame.len8_dlc = 0;for (uint8_t i = 0; i < 8; i++) {frame.data[i] = cnt + i;}cnt++;auto data0 = can2ser(frame);auto data = fsb_insert(data0);// asio::write(ser, asio::buffer(data));asio::async_write(ser, asio::buffer(data),[&](const asio::error_code &ec, std::size_t bytes_transferred) {if (ec) {std::cerr << "Write error: " << ec.message() << std::endl;}});};// timerauto ms = std::chrono::milliseconds(10);asio::steady_timer t(iocxt, ms);std::function<void(const asio::error_code &)> timer =[&](const asio::error_code &ec) {if (ec) {std::cerr << "Timer error: " << ec.message() << std::endl;} else {test();}t.expires_at(t.expiry() + ms);t.async_wait(timer);};t.async_wait(timer);iocxt.run();return 0;
}
对应的cmake文件
cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
project(chcan LANGUAGES CXX)add_executable(${PROJECT_NAME} main.cpp)# https://github.com/cpm-cmake/CPM.cmake
include(CPM.cmake)# https://github.com/chriskohlhoff/asio
CPMAddPackage("gh:chriskohlhoff/asio#asio-1-28-2@1.28.2")
find_package(Threads REQUIRED)
if(asio_ADDED)add_library(asio INTERFACE)target_include_directories(asio SYSTEM INTERFACE ${asio_SOURCE_DIR}/asio/include)target_compile_definitions(asio INTERFACE ASIO_STANDALONE ASIO_NO_DEPRECATED)target_link_libraries(asio INTERFACE Threads::Threads)
endif()target_link_libraries(${PROJECT_NAME} PRIVATE asio
)
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
编译测试:
$ mkdir build && cd build
$ cmake ..
$ make
$ sudo ./chcan
如图
VXCAN
为了更实用一点, 如直接使用can_utils
的cansend
等命令, 用VXCAN虚拟出一对SocketCAN:
#!/bin/sh
sudo modprobe can_raw
sudo modprobe vxcanif ip link show can0 > /dev/null 2>&1; thensudo ip link delete dev can0 type vxcan
fisudo ip link add dev can0 type vxcan
sudo ip link set up can0
sudo ip link set dev vxcan0 up
运行检测:
$ chmod 777 vxcan.sh
$ ./vxcan.sh
$ ifconfig
下面编写 chvxcan, 使用对内的vxcan0进行收发. (这里在Ubuntu20测试, 里面 frame.len
写成了 frame.can_dlc
)
#include <linux/can.h>
#include <linux/can/raw.h>#include <asio.hpp>
#include <chrono>
#include <cstdbool>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <thread>
#include <vector>int main(int argc, char *argv[]) {asio::io_context iocxt;// serial, 1 start bit, 8 data bits, 1 stop bit, no parity, 2.5M baud// CAN 100K, Serial 1M, 1 start bit, 8 data bits, 1 stop bit, no parity// CAN 250K, Serial 2M, 1 start bit, 6 data bits, 1 stop bit, no parity// CAN 500K, Serial 4M, 1 start bit, 6 data bits, 1 stop bit, no parityasio::serial_port ser(iocxt, "/dev/ttyACM0");ser.set_option(asio::serial_port::baud_rate(2000000));ser.set_option(asio::serial_port::character_size(6));ser.set_option(asio::serial_port::stop_bits(asio::serial_port::stop_bits::one));ser.set_option(asio::serial_port::parity(asio::serial_port::parity::none));ser.set_option(asio::serial_port::flow_control(asio::serial_port::flow_control::none));if (!ser.is_open()) {std::cerr << "Failed to open serial port" << std::endl;return -1;}// canint dev = socket(PF_CAN, SOCK_RAW, CAN_RAW);if (dev < 0) {std::cerr << "Failed to open can" << std::endl;return -1;}struct ifreq ifr;std::strcpy(ifr.ifr_name, "vxcan0");if (ioctl(dev, SIOCGIFINDEX, &ifr) < 0) {std::cerr << "Failed to ioctl can" << std::endl;return -1;}struct sockaddr_can addr;addr.can_family = AF_CAN;addr.can_ifindex = ifr.ifr_ifindex;int enable_canfd = 1;if (setsockopt(dev, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable_canfd,sizeof(enable_canfd)) < 0) {std::cerr << "Failed to setsockopt canfd" << std::endl;return -1;}if (bind(dev, (struct sockaddr *)&addr, sizeof(addr)) < 0) {std::cerr << "Failed to bind can" << std::endl;return -1;}asio::posix::stream_descriptor can(iocxt, dev);constexpr uint8_t Bit0 = 0x00;constexpr uint8_t Bit1 = 0xFF;// can crc15 calculationauto crc15 = [&](const std::vector<uint8_t> &data) {bool crc[15] = {0};for (int i = 0; i < data.size(); i++) {bool inv = (data[i] == Bit1) ^ crc[14];crc[14] = crc[13] ^ inv;crc[13] = crc[12];crc[12] = crc[11];crc[11] = crc[10];crc[10] = crc[9] ^ inv;crc[9] = crc[8];crc[8] = crc[7] ^ inv;crc[7] = crc[6] ^ inv;crc[6] = crc[5];crc[5] = crc[4];crc[4] = crc[3] ^ inv;crc[3] = crc[2] ^ inv;crc[2] = crc[1];crc[1] = crc[0];crc[0] = inv;}uint16_t res = 0;for (int i = 0; i < 15; i++) {res |= crc[i] << i;}return res;};// fill stuff bitsauto fsb_insert = [&](std::vector<uint8_t> &data) {uint8_t count = 0;bool last = true;std::vector<uint8_t> newdata;for (auto it = data.begin(); it != data.end(); it++) {bool current = *it == Bit0 ? false : true;newdata.push_back(*it);if (current == last) {count++;} else {count = 1;}if (count == 5) {newdata.push_back(current ? Bit0 : Bit1);count = 1;last = !current;} else {last = current;}}return newdata;};// socketcan frame to serial frameauto can2ser = [&](const can_frame &frame) {std::vector<uint8_t> data;uint32_t id = 0;bool is_extended = frame.can_id & CAN_EFF_FLAG ? true : false;bool is_remote = frame.can_id & CAN_RTR_FLAG;uint8_t dlc = frame.can_dlc;data.push_back(Bit0); // SOF, Start of frameif (is_extended) {id = frame.can_id & CAN_EFF_MASK;// High 11 bits idfor (uint8_t i = 0; i < 11; i++) {uint8_t tmp = ((uint8_t)(id >> (28 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}data.push_back(Bit1); // SRR, Substitute remote requestdata.push_back(Bit1); // IDE, Identifier extension// Low 18 bits idfor (uint8_t i = 0; i < 18; i++) {uint8_t tmp = ((uint8_t)(id >> (17 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}// RTRif (is_remote) {data.push_back(Bit1);} else {data.push_back(Bit0);}data.push_back(Bit0); // RB1, reserved bit 1} else {id = frame.can_id & CAN_SFF_MASK;for (uint8_t i = 0; i < 11; i++) {uint8_t tmp = ((uint8_t)(id >> (10 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}// RTRif (is_remote) {data.push_back(Bit1);} else {data.push_back(Bit0);}data.push_back(Bit0); // IDE, Identifier extension}data.push_back(Bit0); // RB0, reserved bit 0// 4 bits DLCfor (uint8_t i = 0; i < 4; i++) {uint8_t tmp = ((uint8_t)(dlc >> (3 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}// 0~64 bits datafor (uint8_t i = 0; i < frame.can_dlc; i++) {for (uint8_t j = 0; j < 8; j++) {uint8_t tmp = ((frame.data[i] >> (7 - j)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}}// CRC15uint16_t crc = crc15(data);for (uint8_t i = 0; i < 15; i++) {uint8_t tmp = ((crc >> (14 - i)) & 0x01) ? Bit1 : Bit0;data.push_back(tmp);}data.push_back(Bit1); // CRC Delimiter// ACK, ACK Delimiter, EOF, IFS will not be caredreturn data;};// read socketcan frame and write to serialuint8_t can_buffer[1024];std::function<void(const asio::error_code &, std::size_t)> can2ser_read =[&](const asio::error_code &ec, std::size_t bytes_transferred) {if (ec) {std::cerr << "Read error: " << ec.message() << std::endl;} else {if (bytes_transferred == CAN_MTU) {struct can_frame frame;std::memcpy(&frame, can_buffer, sizeof(can_frame));auto data0 = can2ser(frame);auto data = fsb_insert(data0);asio::async_write(ser, asio::buffer(data),[&](const asio::error_code &ec, std::size_t bytes_transferred) {if (ec) {std::cerr << "Write error: " << ec.message() << std::endl;}});}}can.async_read_some(asio::buffer(can_buffer), can2ser_read);};can.async_read_some(asio::buffer(can_buffer), can2ser_read);iocxt.run();return 0;
}
测试如图
到这里, 就可以用你喜欢的语言, 如Python, Rust, C等对can0进行发送了.
Github Link
domain_controller_orin_x2_tc397/ch343_can at main · weifengdq/domain_controller_orin_x2_tc397 (github.com)