作为一个常年折腾嵌入式小工具的玩家,我最近把一个吃灰已久的APM32F072核心板打造成了USB-CAN分析仪,刷的是moonglow和kvaser这两个开源固件。折腾过程不算难,但坑确实不少,尤其是时钟配置、底层移植、上位机联调这几块,稍不注意就白屏、枚举失败或者CAN收发没反应。这篇文章把我踩过的坑和验证过的操作完整写出来,给想自己搞一个低成本CAN分析仪的朋友做个参考。
1. 内容整体设计与思路拆解
1.1 为什么要选APM32F072做USB-CAN
先说选型逻辑。市面上现成的USBCAN分析仪,动辄几百上千,对于偶尔调个BMS、看看CAN报文的人来说,性价比确实不高。而APM32F072这颗芯片很有意思:它内置了USB 2.0 FS Device控制器和CAN 2.0B控制器,而且引脚和STM32F072高度兼容,这意味着很多基于STM32F072的开源固件可以直接迁移过来,省去了重新设计底层驱动的成本。
APM32F072采用的是Cortex-M0+内核,主频最高48MHz,Flash有64KB,SRAM 8KB(我这里用的是APM32F072VBT6,Flash 128KB版本也有)。虽然M0+内核没有M3/M4那么风光,但跑USB Device协议栈和CAN收发完全够用。最关键的一点是,这颗芯片的USB和CAN外设寄存器布局和ST的F072几乎一致,所以moonglow这种为STM32F072量身定做的固件,理论上只需要修改启动文件、时钟配置和外设库的头文件映射,就能跑起来。
个人实测下来,APM32F072的USB枚举稳定性和CAN波特率精度都不错,至少在这个应用场景下,没比ST的原厂芯片差到哪去。再加上极海这颗芯片现在很容易买到,价格也便宜,用来做DIY工具非常合适。
1.2 moonglow和kvaser固件分别是什么
这里需要先理清概念。moonglow是一个开源的USB-CAN固件项目,底层基于STM32F072的USB库和bxCAN外设,实现了PCAN-View、BUS Master、canoe等上位机软件需要的协议转换逻辑。刷入moonglow固件后,设备插入电脑会枚举成一个标准的PCAN-USB设备,可以直接用PCAN-View读取CAN报文。
而kvaser固件,从项目名字里的kvaser来看,是让设备伪装成Kvaser品牌的CAN分析仪。Kvaser官方提供的CanKing等软件,配合对应的驱动,也能识别这种开源的USB-CAN设备。两者本质上都是把MCU的USB虚拟成串口或者HID设备,再通过协议转换把上层软件的数据包转发给CAN控制器。
我这次移植的思路很简单:先搞定moonglow固件,因为它的代码结构清晰、依赖少,容易作为底版;跑通之后再尝试kvaser固件,两个固件共用同一套底层初始化,主要区别在USB描述符和上位机协议解析部分。
1.3 移植的整体路线图
整个移植过程我拆成了四步走:
- 建立APM32F072的固件工程骨架,替换启动文件和时钟配置
- 解决底层外设库的依赖问题,让USB和CAN的驱动代码能编译过
- 编译烧录,验证USB枚举和CAN收发功能
- 分别刷入moonglow和kvaser固件,搭配上位机做完整联调
另外说明一点:这个项目能成立,很大程度上要归功于开源社区,moonglow原版是针对STM32F072的,网上也有不少人在其他厂商的F072兼容芯片上移植成功。我的做法是拿官方代码做底,把硬件差异用宏定义隔离出来,这样后续升级固件、切换芯片型号都不用大改。
2. 硬件准备与工程移植前置条件
2.1 需要的硬件材料清单
既然要实操,先把东西列全:
- APM32F072核心板或最小系统板(我用的是APM32F072VBT6,带USB座子和CAN收发器更好)
- USB转TTL模块(用来烧录固件,也可以用ST-Link)
- CAN收发器模块,我这里用的是TJA1050,如果你的板子不带收发器,需要外接
- 杜邦线若干,用于连接CAN_H、CAN_L、GND
- 另一块CAN节点设备(我用的是另一个STN992评估板和一个CANalyst-II分析仪做对比验证)
- 电脑上装好PCAN-View或者Kvaser CanKing
很多人会忽略CAN收发器,这里必须提醒:MCU的CAN控制器引脚是不能直接挂到CAN总线上的,必须经过CAN收发器将TTL电平转换成差分信号。TJA1050是比较经典的收发器,3.3V供电即可工作,和APM32F072的IO电平兼容,接线也很简单:TXD接MCU的CAN_TX(PA12),RXD接CAN_RX(PA11),VCC接3.3V,CANH、CANL分别接总线上的CANH、CANL。
2.2 建工程:从STM32F072到APM32F072的代码迁移
拿到moonglow源码后,第一步就是把工程文件改成APM32F072可用。最常见的做法是直接用极海官方提供的APM32F0xx标准外设库来替换原来ST的标准外设库。
具体来说,要做三件事:
- 将工程里的启动文件startup_stm32f072.s换成APM32F0xx的启动文件
- 将CMSIS头文件stm32f0xx.h换成apm32f0xx.h,同时把所有依赖ST头文件的代码include路径改掉
- 外设库源文件替换:原工程用的stm32f0xx_gpio.c、stm32f0xx_usb.c等,换成apm32f0xx_gpio.c、apm32f0xx_usb.c
这里的坑在于,APM32F0xx的库函数命名和ST有细微差异,比如GPIO_InitTypeDef的结构体成员名可能略有不同,寄存器位的宏定义也可能改了名字。最稳妥的方式是先把编译错误全部解决,再逐个检查USB和CAN初始化部分是否真的对应到了正确的寄存器。
2.3 时钟树配置:最容易翻车的地方
时钟配置是整个移植过程中最坑的环节。moonglow原版代码里的SystemInit函数是面向STM32F072的,直接换成APM32F072之后,如果不修改时钟配置,USB外设的48MHz时钟可能就不对,枚举失败、识别不到设备都是从这里开始的。
APM32F072和STM32F072一样,系统时钟源可以从HSI(8MHz)或HSE(外部晶振)获取。USB外设需要精确的48MHz时钟,这个48MHz通常是由PLL从8MHz倍频得到的。我的板子上没有焊接外部晶振,所以直接用HSI 8MHz,配置PLL倍频6倍得到48MHz。
具体配置方法如下:
- 系统时钟源选择HSI
- PLL源选择HSI/2(也就是4MHz),倍频系数12,得到48MHz系统时钟
- USB时钟选择PLL时钟直接输出48MHz
如果板子上有8MHz晶振,也可以选择HSE作为PLL输入,倍频系数6得到48MHz。注意:如果USB时钟频率偏差过大,设备会枚举失败或者枚举后频繁掉线。USB规范要求全速设备的时钟精度在±0.25%以内,所以晶振或者HSI的精度非常关键。APM32F072内部HSI经过校准后,精度实测在±1%以内,但USB对时钟敏感,建议有条件的还是用外部晶振。
3. 核心细节解析与实操要点
3.1 USB Device底层初始化流程
USB这块,moonglow固件用的是ST官方的USB Device库,封装层次是USBD -> USB Device Class -> USB底层硬件操作。移植到APM32F072时,底层硬件操作函数需要适配。
USB初始化分四步:
- 使能USB和GPIO时钟
- 配置USB相关的GPIO引脚(PA11为USB_DM,PA12为USB_DP,PA13为USB_DETECT可选)
- 调用USB_Init函数初始化USB IP核
- 调用USBD_Start函数启动USB设备
很多人在这一步卡住,是因为APM32F072的USB IP核虽然和ST兼容,但寄存器的使能位可能有差异。我在实际操作中发现,APM32F072的USB模块使能位在RCU(复位时钟单元)里的位置和ST的RCC不一样,直接赋值会导致USB时钟没打开。所以底层RCC配置这里,不能想当然地沿用ST代码,要打开APM32F0xx参考手册,核对一下RCU_APB2ENR或者RCU_APB1ENR里USBEN位的偏移。
3.2 CAN控制器初始化与波特率计算
CAN部分相对简单,bxCAN的寄存器布局在两个厂商之间基本一致。初始化步骤是:
- 使能CAN时钟
- 配置CAN引脚(PA11为RX,PA12为TX,注意复用功能)
- 设置CAN工作模式(正常模式/环回模式)
- 配置波特率
- 配置过滤器
波特率这块必须要动手算。bxCAN的位时间由三部分组成:SYNC_SEG(固定1个时间量子)、BT1(传播段+相位缓冲段1)、BT2(相位缓冲段2)。波特率 = CAN时钟频率 / (1 + BT1 + BT2) / 预分频值。
APM32F072的CAN外设挂在APB1总线上,默认时钟是系统时钟,也就是48MHz。如果我要得到500kbps的波特率,公式是这样的:
- 48MHz / (1 + 4 + 3) / 8 = 48000000 / 8 / 8 = 750000,不对
- 重新算:48MHz / 16 = 3MHz,然后3MHz / 6 = 500kbps,也就是预分频6,位时间共16个时间量子
- 分配下来:SYNC_SEG = 1,BT1 = 13,BT2 = 2
我配的寄存器值:
- CAN_BTR = 0x000A0011,表示预分频值为6,BT1=13,BT2=2,采样点在(1+13)/(1+13+2)=87.5%,这个采样点对500kbps的CAN总线来说比较合适
实际操作过程中,我建议先用环回模式(LoopBack)测试CAN收发,这样可以排除总线上的干扰和终端电阻问题,确认MCU的CAN控制器本身工作正常,再接外部设备联调。
3.3 关键代码段解析:moonglow的USB描述符与协议转换
moonglow固件里有一个非常核心的文件叫做usb_desc.c(不同版本可能叫法略有不同),里面定义了设备描述符、配置描述符、接口描述符和端点描述符。PCAN-View识别设备时,就是通过VID(Vendor ID)和PID(Product ID)来匹配驱动的。
moonglow原版默认的VID/PID是PCAN的0x0C72/0x000C,理论上如果装了PCAN驱动,就能直接识别。但如果你的电脑之前装过其他CAN工具的驱动,可能会出现设备冲突。这种情况下,可以修改描述符,把VID/PID改成自己定义的,然后安装对应的WinUSB驱动,或者用Zadig工具手动绑定驱动。
协议转换逻辑在usbd_pcan_core.c里,主要是处理上位机发过来的命令包。PCAN协议的命令包格式一般是这样的:
- 字节0:命令类型(0x00表示发送CAN帧,0x01表示设置波特率等)
- 字节1-4:参数(CAN ID、帧类型、数据长度等)
- 字节5-12:CAN数据
moonglow收到这些命令后,解包并调用CAN发送函数。反过来,CAN接收中断里收到报文后,会按照PCAN协议格式打包,通过USB IN端点发送给上位机。
3.4 kvaser固件的特殊之处
相比moonglow,kvaser固件的上层协议走的是Kvaser的CanKing接口。Kvaser的设备在Windows下通过专门的驱动通信,而开源kvaser固件通常是模拟出一组和Kvaser硬件兼容的USB端点。
刷了kvaser固件后,设备枚举出来会显示成Kvaser品牌的设备名,配合Kvaser的驱动和CanKing软件,就可以正常收发CAN报文。我在移植时发现,kvaser固件对USB描述符里的字符串非常敏感,如果Product String和原版差异太大,驱动会拒绝加载。建议保留原版的厂商字符串和产品字符串,等设备能正常识别后,再按需修改。
4. 实操过程与核心环节实现
4.1 第一步:搭建编译环境
我用的编译环境是Keil MDK 5,因为moonglow源码本身就是Keil工程,直接打开就能用。需要安装的组件包括:
- Keil MDK 5.37或更新版本
- APM32F0xx器件支持包(极海官网可以下载,包含Flash算法和器件定义)
- ARM Compiler 5或6(Keil自带)
如果不想用Keil,也可以用STM32CubeIDE或者VS Code + arm-none-eabi-gcc + Makefile的方式,但工作量会大一些,需要自己重写链接脚本和启动文件。个人建议先用Keil把流程跑通,后面再考虑迁移到其他工具链。
4.2 第二步:将moonglow工程迁移到APM32F072
具体操作路径如下:
- 从GitHub拉取moonglow源码(建议用master分支)
- 打开Keil工程文件,在Device选项里把芯片型号从STM32F072改成APM32F072
- 替换启动文件和CMSIS头文件:将ST的startup_stm32f072.s和stm32f0xx.h替换成极海官方提供的版本
- 修改SystemInit函数:这一步最关键,我直接参考了APM32F072标准库里的system_apm32f0xx.c,把时钟初始化逻辑换成极海的
- 修改USB和CAN的驱动底层:把stm32f0xx_usb.c换成apm32f0xx_usb.c,同理CAN部分
- 编译,一个一个解决报错
我在步骤4、5上面花了最多时间,因为ST和极海库函数的命名差异导致几十个编译错误。最笨也最有效的办法是:先把所有报错函数名汇总,打开APM32F0xx标准库的头文件,逐个对比,把函数名或者结构体成员名批量替换掉。
4.3 第三步:CAN初始化与环回测试
在整合USB之前,我建议先单独测试CAN控制器。写一个简单的测试程序,把CAN配置成环回模式,自发自收。
关键初始化代码如下:
void CAN_Init_Config(void) { CAN_TimingTypeDef timing; CAN_FilterTypeDef filter; // 使能CAN时钟和GPIO时钟 RCU_EnableAPB1PeriphClock(RCU_APB1_PERIPH_CAN); RCU_EnableAHBPeriphClock(RCU_AHB_PERIPH_GPIOA); // 配置PA11为CAN_RX,PA12为CAN_TX GPIO_ConfigPinAF(GPIOA, GPIO_PIN_11 | GPIO_PIN_12, GPIO_AF_4); // 初始化CAN外设 CAN_Config(CAN, CAN_MODE_LOOPBACK); // 设置波特率 500kbps,时钟48MHz,预分频6,位时间16个TQ timing.SyncJumpWidth = CAN_SJW_1TQ; timing.TimeSegment1 = 13; timing.TimeSegment2 = 2; timing.Prescaler = 6; CAN_ConfigTiming(CAN, &timing); // 配置过滤器为接收所有帧 filter.FilterNumber = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterIdHigh = 0x0000; filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x0000; filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_FIFO_0; filter.FilterActivation = ENABLE; CAN_ConfigFilter(&filter); // 开启接收中断 CAN_EnableInterrupt(CAN, CAN_INT_RX_FIFO0_MSG_PENDING); }环回模式下,CAN_TX发送的报文会在芯片内部直接回环到CAN_RX,不需要外部总线连接。发送一帧数据后,查询接收标志位,如果能收到,说明CAN控制器基本没问题。这一步通过后,再连到外部总线上测试。
4.4 第四步:USB枚举测试
CAN测试通过后,烧录完整的moonglow固件。插入USB线,先在设备管理器里看看有没有未知设备。正常情况会出现一个未知设备或者带感叹号的设备(因为驱动还没安装)。
然后打开Zadig,选择这个设备,安装WinUSB驱动。安装完成后,设备管理器里应该能看到一个名为PCAN-USB或者Kvaser的条目。
如果这里出了问题,最常见的三种情况:
- 设备管理器里完全没有新设备出现:说明USB的D+上拉电阻或者USB时钟有问题,检查硬件连接和时钟配置
- 设备出现但反复重置:USB描述符有问题,或者供电不足
- 设备枚举成功但驱动装不上:VID/PID不匹配,用Zadig手动指定驱动
4.5 第五步:PCAN-View联调实测
驱动装好后,打开PCAN-View。软件会自动检测到PCAN-USB设备,如果它询问你选择硬件,手动选PCAN-USB就行。
联调流程:
- 设置波特率为500kbps
- 将APM32F072的CAN_H和CAN_L连接到另一路CAN节点上(我这里连的是CANalyst-II,注意两边波特率必须一致,且总线两端各接一个120欧终端电阻)
- 在PCAN-View里点发送,选择标准帧,ID设为0x123,数据随便填8个字节
- 如果CANalyst-II那边能收到这帧报文,说明上行通路OK
- 反过来,在CANalyst-II那边发送一帧,PCAN-View里能收到,说明下行通路OK
实测下来,PCAN-View发送标准帧、扩展帧、远程帧都没问题,收发速率在500kbps下跑满负载也没出现丢帧。不过要注意:USB全速设备理论上可以承载很高的CAN流量,但实际受限于USB帧间隔和上位机软件处理能力,如果你跑1Mbps总线,同时大量收发,可能需要优化USB批量传输的缓冲区大小。
4.6 第六步:刷入kvaser固件验证
moonglow验证通过后,刷入kvaser固件。操作方式和前面一样,区别在于驱动:Kvaser的设备需要安装Kvaser官方驱动,或者用Kvaser提供的第三方驱动。
装好kvaser固件后,插上USB,Windows会识别到硬件,然后去Kvaser官网下载并安装Kvaser Driver。安装完成后,打开CanKing,选择对应的设备,同样设置500kbps,发一帧测试数据。
这里遇到一个比较典型的坑:CanKing默认会尝试打开设备的一些高级功能(比如报文记录、总线统计),如果固件没有实现这些功能,软件会报错打不开设备。解决办法是在CanKing的设备设置里,把那些不支持的功能关掉,只用最基础的CAN收发功能。
5. 常见问题与排查技巧实录
5.1 USB枚举失败:时钟配置不对
这个问题出现的频率最高。如果你发现设备插入电脑后,USB口完全没反应,或者过了几秒钟就提示无法识别的USB设备,大概率是USB时钟配置不对。
排查顺序:
- 检查RCU配置里的USB时钟源:APM32F072的USB必须使用48MHz时钟,如果PLL倍频算错,USB时钟就是45MHz或者50MHz,偏得不多,但USB协议栈对时钟精度要求高,偏一点就枚举失败
- 检查USB D+上拉电阻:全速USB设备要求在D+线上有1.5k欧姆的上拉电阻,有些核心板把上拉电阻和USB_DP引脚直连了,有些则通过软件控制。moonglow固件默认配置的是软件控制模式,如果你的板子是硬件直连,可能不需要配置,但也要确认IO口没有被初始化成其他复用功能
- 用示波器看D+和D-上的波形:正常枚举时,D+在复位后应该被拉高,如果全程低电平,说明USB IP核没有启动
5.2 CAN不收发:终端电阻和电平问题
如果USB枚举正常,上位机也连上了设备,但CAN报文发不出去也收不到,先别急着怀疑固件。
优先级最高的检查项是物理层:
- CAN_H和CAN_L是否正确连接,是否接反
- 总线上是不是没有终端电阻。低速实验可以没有终端电阻,但高速、长距离通信时,没有终端电阻会导致信号反射,波形畸变
- CAN收发器供电是否正常。TJA1050如果是5V供电,和APM32F072的3.3V之间可能会有电平不匹配问题,需要确认CAN控制器的IO和TJA1050的TXD/RXD电平是否兼容
然后是配置层:
- 波特率是否一致。两边节点必须都是500kbps,或者都在同一个波特率
- 通讯是否被过滤器屏蔽。如果你在过滤器里配置了只接收特定ID,那其他帧会被硬件过滤掉,收不到是正常的
5.3 驱动安装失败:VID/PID冲突
moonglow固件默认使用的是PCAN的VID/PID,这在安装了PCAN官方驱动的电脑上是可以直接识别的。但也有一种情况:电脑上装着别的CAN工具软件,这个软件抢占了USB设备资源,导致设备枚举后无法绑定PCAN驱动。
解决办法有两个:
- 在设备管理器里手动卸载冲突驱动,然后重新插拔设备,让Windows重新安装PCAN驱动
- 用Zadig强制修改驱动绑定关系,把设备的驱动从冲突驱动改成WinUSB
如果你打算用kvaser固件,那必须先把PCAN驱动卸载干净,否则两个厂商的驱动会打架。
5.4 固件刷入后无法启动:启动文件和链接脚本不匹配
这个问题在自制工程里比较常见。如果你从moonglow源码拷贝了启动文件,但链接脚本(.sct文件)里的Flash起始地址和芯片型号不匹配,程序会跑飞。
APM32F072VBT6的Flash起始地址是0x08000000,大小128KB,SRAM起始地址0x20000000,大小16KB。在Keil工程的Target选项和Linker选项里确认这些参数是对的。
如果程序启动后卡死在HardFault_Handler里,大概率是时钟配置里某个外设时钟没开,或者中断向量表有问题。可以在HardFault_Handler里打断点,看看调用的函数栈,定位到出错的位置。
5.5 上位机收发不稳定:USB缓冲区溢出问题
如果你用PCAN-View连续大量发送报文,偶尔会出现数据丢失或者软件卡死,这很可能是USB端点缓冲区溢出导致的。
moonglow固件默认的USB缓冲区大小是128字节,对于每个USB帧最多能承载几十个CAN报文来说,其实算够用。但如果你的CAN总线波特率特别高(比如1Mbps),并且报文很短(数据场只有1字节),一毫秒内可能产生几百帧报文,USB根本来不及传完,缓冲区就溢出了。
解决办法:
- 上位机软件里降低发送频率,或者加发送延时(PCAN-View的Transfer菜单里有周期发送选项)
- 修改固件里的USB端点缓冲区大小,把IN端点的最大包长从64字节改成128字节(需要重新编译固件)
- 在CAN接收中断里加一个简单的流量控制:如果USB发送缓冲区满,直接丢弃新到的CAN帧,保证老数据能送出去
6. 我的实操心得与几个后续改进方向
整个项目从开始折腾到最终稳定运行,花了大概一个周末的时间。最大的体会是:像moonglow和kvaser这种开源固件,它们往往都是为特定芯片和特定板卡设计的,想要移植到其他芯片上,最核心的工作不是改代码,而是把芯片之间的差异梳理清楚。APM32F072和STM32F072确实引脚兼容,但寄存器位、时钟树、库函数封装都不可能做到100%一致,把这些差异逐个消化掉,移植自然就顺了。
另外,USB-CAN分析仪这个东西,硬件方案本身并不复杂,但固件移植过程中踩过的坑非常多,主要集中在USB时钟精度和驱动匹配上。如果你用的是别的国产芯片,比如GD32F072或者AT32F072,操作路径是类似的:先把启动文件和时钟树确认好,再解决外设库的兼容性,最后验证USB和CAN的协同工作。
如果后续想把这块板子继续玩下去,我建议从这几个方向入手:
- 把固件里的CAN过滤器做成上位机可配置的,这样就可以在PCAN-View里动态过滤CAN ID,不用每次修改固件重新烧录
- 增加CAN报文时间戳功能:用MCU的定时器给每个收到的CAN帧打上精确到微秒的时间戳,这个在上位机分析总线时序时非常有用
- 加入CAN唤醒功能:当总线上出现唤醒报文时,设备从低功耗模式自动切换到正常工作模式,适合车载和电池管理场景
说实话,用一颗几十块钱的MCU做出一台功能不输几百元商业设备的CAN分析仪,这种满足感不是金钱能衡量的。你自己动手把系统跑通的那一刻,会发现自己对USB协议栈、CAN协议和MCU底层外设的理解,都上了一个台阶。如果你也正在折腾类似的移植项目,别怕报错,每一步报错都是在帮你更深入地理解芯片手册。