1. 项目背景与硬件选型
去年公司立项开发一款低成本USB转CAN通讯转换器,用于工业设备调试。项目初期采用STM32F103C8T6作为主控,后来因其他项目优先级调整被搁置。出于技术热情,我在业余时间继续完善了这个项目,并衍生出两个硬件版本:
- 基础版:STM32F103C8T6 + CP2102串口芯片
- 进阶版:CH32F103C8T6(内置USB PHY)
选择这两款MCU主要基于三点考量:首先,它们都采用Cortex-M3内核,开发环境熟悉;其次,淘宝核心板价格都在10-15元区间;最重要的是,两者都内置CAN控制器,无需额外扩展芯片。实际采购时发现CH32F103比STM32便宜约20%,且供货稳定。
硬件设计心得:虽然STM32的CAN外设更成熟,但CH32的USB硬件支持更好。若项目对成本敏感且需要免驱特性,CH32是更好的选择。
2. STM32串口版实现详解
2.1 硬件连接方案
采用模块化设计,核心部件包括:
- STM32最小系统板(8MHz晶振)
- TJA1050 CAN收发器(工业级,支持5Mbps)
- CP2102 USB转串口芯片
- 120Ω终端电阻(双绞线必需)
原理图关键点:CAN_H/CAN_L需加TVS二极管防护,串口TX/RX交叉连接,BOOT0通过10K电阻接地确保正常启动模式。
2.2 CAN通信配置
波特率计算是核心难点。假设APB1时钟为36MHz,目标500kbps时:
code复制总线时钟 = 36MHz / 预分频系数 = 6MHz
位时间 = 1/500kHz = 20tq
时间段1(BS1) = 6tq
时间段2(BS2) = 5tq
同步跳转宽度(SJW) = 1tq
实际波特率 = 6MHz / (1+6+5) = 500kbps
对应的库函数配置如原文所示,特别注意:
CAN_ABOM = ENABLE使能自动离线恢复CAN_NART = DISABLE保证重要数据自动重传
2.3 串口协议设计
自定义的帧结构如下:
code复制[0xAA][0x55][ID(4B)][Len(1B)][Data(nB)][Checksum(1B)]
校验采用累加和取反,C#实现示例:
csharp复制byte Checksum(List<byte> data) {
byte sum = 0;
foreach(var b in data) sum += b;
return (byte)~sum;
}
调试技巧:用串口助手发送测试帧时,建议先发送固定模式数据(如0x00-0xFF循环),用逻辑分析仪抓取CAN波形验证物理层。
3. CH32 HID版开发要点
3.1 USB HID配置
CH32的USB外设需要自定义报告描述符,关键参数解析:
c复制0x95, 0x40, // 报告数64个 → 64字节数据包
0x81, 0x02, // 输入报告(设备到主机)
0x91, 0x02 // 输出报告(主机到设备)
设备枚举成功后,需处理以下中断:
USBFS_IRQn:处理USB复位、挂起等事件EP1_OUT_Callback:接收主机下发的CAN指令EP1_IN_Callback:完成CAN数据上传
3.2 数据包处理
由于HID协议要求固定64字节包,设计协议时需注意:
- 帧头标识:0x55AA(与串口版相反防混淆)
- 类型字段:0x01-CAN发送,0x02-CAN接收
- 数据填充:剩余字节补零
示例接收处理逻辑:
c复制void EP1_OUT_Handler(void) {
uint8_t buf[64];
USBFS_RxData(buf);
if(buf[0]==0x55 && buf[1]==0xAA){
if(buf[2] == 0x01){ //CAN发送指令
CAN_Send(buf+3, buf[3]); //buf[3]为数据长度
}
}
}
4. 上位机开发实战
4.1 C#串口版核心功能
采用System.IO.Ports实现:
csharp复制class CANController {
SerialPort port;
public void Open(string com, int baud) {
port = new SerialPort(com, baud);
port.DataReceived += (s,e) => {
byte[] buf = new byte[port.BytesToRead];
port.Read(buf, 0, buf.Length);
ParseFrame(buf);
};
port.Open();
}
void ParseFrame(byte[] data) {
// 解析帧头、校验等
}
}
4.2 HID版通信要点
使用HidLibrary第三方库:
csharp复制var devices = HidDevices.Enumerate(vendorId, productId);
device = devices.FirstOrDefault();
device.OpenDevice();
device.ReadReport(OnReport); //异步读取
void SendCANFrame(uint id, byte[] data) {
var report = new HidReport(64);
report.Data[0] = 0x55; //帧头
report.Data[1] = 0xAA;
Buffer.BlockCopy(BitConverter.GetBytes(id), 0,
report.Data, 2, 4);
report.Data[6] = (byte)data.Length;
Array.Copy(data, 0, report.Data, 7, data.Length);
device.WriteReport(report);
}
5. 典型问题排查指南
5.1 通信失败排查步骤
- 物理层检查:
- 示波器测量CAN_H-CAN_L差分电压(正常2V左右)
- 确认终端电阻阻值(120Ω)
- 协议层检查:
- 波特率误差(建议<1%)
- 帧格式(标准帧/扩展帧)
- 软件层检查:
- 过滤器配置(验收码/屏蔽码)
- 发送缓冲区状态(检查CAN_TSR寄存器)
5.2 结构体对齐问题
C/C++与C#交互时常见问题。解决方案:
- C端使用
#pragma pack(1)强制单字节对齐 - C#端用
[StructLayout(LayoutKind.Sequential, Pack=1)] - 更可靠的方法是转为字节流传输:
c复制// C端发送
uint32_t id;
uint8_t len;
uint8_t data[8];
// 转为字节数组发送
6. 进阶优化方向
-
协议增强:
- 增加心跳包机制
- 实现大数据分包传输
- 添加加密校验(如CRC32)
-
性能优化:
- 使用DMA加速USB传输
- 双CAN缓冲区设计
- 动态调整波特率
-
功能扩展:
- 支持CAN FD协议
- 增加Wi-Fi透传模块
- 集成数据日志存储
这个项目从被搁置的"废案"最终发展成功能完善的工具,过程中最大的收获是:硬件设计要预留调试接口(如SWD引出),软件架构要模块化(通信协议与硬件层解耦)。建议初学者先用CANTest工具测试硬件,再逐步实现自定义功能。