手里有APM32F072开发板,想把它做成一个带Kvaser协议兼容的USB-CAN分析仪,用Kvaser官方软件直接收发报文——这个需求听起来小众,但实际做起来非常有意思,而且能省下一大笔买原装进口CAN卡的钱。moonglow这个开源项目,恰好就是干这件事的:它让你手头的普通USB-CAN硬件,刷上固件之后直接变成Windows下免驱识别的Kvaser设备。
这篇文章我按自己实际折腾的完整流程来写,从选型依据、编译环境、移植改动到烧录验证、踩坑记录,全部记录下来。目标是把APM32F072这颗芯片跑通moonglow/kvaser固件的整个路径讲清楚,你在复现的时候,能少走几步弯路。
1. 为什么选APM32F072做USB-CAN:不只是“便宜大碗”
很多人一提到USB-CAN适配器,第一反应是STM32F103系列,毕竟资料多、用的人多。但如果你真的打算跑moonglow这类固件,F103其实是个比较尴尬的选择——它的USB控制器是Device模式,做全速USB没问题,但架构本身就比较老,DMA支持和中断响应在高速CAN报文场景下会显得吃力。相比之下,APM32F072属于M0+内核,虽然是Cortex-M0+,但它的USB模块是带内部收发器的全速USB Device,配合CAN控制器,恰好踩在moonglow固件的设计预期上。
1.1 极海APM32F072和STM32F072的兼容性
APM32F072是极海半导体推出的、和STM32F072引脚级兼容的32位MCU。这个“引脚级兼容”意味着PCB不用改,直接把STM32F072的封装替换成APM32F072就能跑同一套原理图。更关键的是,它的SVD文件和寄存器映射基本对齐了ST原厂,所以moonglow里针对STM32F072写的USB和CAN驱动代码,理论上可以直接复用。
这颗芯片的特点很明确:
- 内核:ARM Cortex-M0+,主频48MHz
- Flash:128KB,SRAM:16KB
- USB:全速Device,内置收发器
- CAN:1路CAN 2.0B,带3个发送邮箱、2个接收FIFO
- 供电:2.8V~3.6V,USB口供电完全没问题
moonglow固件对硬件的核心需求是:一个USB Device接口,一个CAN控制器,再加上足够的Flash空间存固件。APM32F072这三点全都满足,而且ST生态的调试工具链可以直接用,这就省去了搭新环境的大量麻烦。
1.2 moonglow固件到底做了什么
moonglow是一个基于ECAN项目的开源固件,目标很简单:让兼容Kvaser通讯协议的硬件,刷上它之后能够被Kvaser官方软件(如Kvaser CANKing、Kvaser Database Editor等)直接识别和使用。说白了,就是把普通CAN卡“升级”成支持Kvaser协议的高端分析仪。
固件的核心价值有两块:
第一,协议层实现。Kvaser设备走的是USB批量传输,自定义的CAN报文封装格式。moonglow用ST的USB库把端点配置好,再在协议层把Kvaser的报文格式解析出来,映射到CAN控制器的收发FIFO里。这部分代码逻辑清晰,适合作为学习USB-CAN协议栈的入门范例。
第二,免驱能力。Windows下Kvaser设备有自己的驱动,moonglow刷进去之后,设备插上电脑会被识别成Kvaser旗下对应的型号,这时候装好Kvaser官方驱动,系统就把它当成一个正规CAN卡来用。
所以整个项目的技术链条是这样的:APM32F072的USB模块负责和PC通信,CAN模块负责总线上报文的收发,固件内部做协议转换——一边是Kvaser的USB协议,一边是CAN 2.0B的帧格式。
1.3 为什么推荐直接买APM32F072核心板来玩
动手之前先泼盆冷水:如果你只是想快速体验moonglow/Kvaser固件,不要从零画板子。万用板手飞线搞USB和CAN走线,信号完整性会很痛苦,尤其USB的D+/D-差分对,手工焊接飞线试试就知道了。而APM32F072的核心板现在很便宜,二三十块钱一片,板上已经把USB座子、CAN收发器、稳压电路全做好了,拿来直接就能刷固件。
我自己用的就是一块最小系统板,主控APM32F072C8T6,板载一个USB TYPE-C座子和一个TJA1050 CAN收发器。烧录口是SWD,四根线就能搞定。这块板子刷完moonglow之后,插电脑直接识别为“Kvaser Leaf Light HS v2”,属实香。
2. moonglow固件的编译准备:从拉代码到出hex
前置环境没有太多花活,但第一次搞的时候确实有几个地方容易卡住。我按自己实际敲过的命令和流程来讲。
2.1 交叉编译工具链的安装
moonglow的Makefile基于ARM GCC工具链,用arm-none-eabi-gcc编译。系统环境我用的Ubuntu 22.04,在Windows底下用WSL也可以。
安装工具链的方式很简单,Ubuntu直接APT装:
sudo apt update sudo apt install gcc-arm-none-eabi make git有个细节:线上源的arm-none-eabi-gcc版本可能不是最新,但编译moonglow足够了。真正要留意的是make版本,太老的GNU Make在一些目标规则的解析上会报错,建议确保make在4.x以上:
make --version2.2 拉取moonglow源码和子模块
moonglow项目基于ECAN,仓库里有一些USB库、CMSIS相关的子模块,直接git clone不会自动拉子模块,所以必须用--recursive参数:
git clone --recursive https://github.com/moonglow/ecan.git cd ecan如果你已经clone完了才发现没带上子模块,也可以补拉:
git submodule update --init --recursive这一步必须做,否则编译的时候找不到USB库头文件,直接一堆红字报错。
2.3 选对板卡配置:从generic到自己的板子
moonglow仓库里包含多个板卡模板,其中和APM32F072/STM32F072相关的配置在boards目录下,有通用的ecan板、也有针对某个特定开发板的适配。编译之前需要先确定Makefile目标。
查看目标列表:
make help如果你手上的板子刚好有对应模板,直接指定BOARD编译即可:
make BOARD=STM32F072_DISCOVERY但我用的板子是第三方的,仓库里没有现成模板。这就得自己改配置了,下文会讲具体改动。
如果没有现成模板,可以先找一个最接近的作为baseline,然后针对自己的板子改引脚映射。这里要去看板子的原理图,确认USB D+/D-接到了PA11/PA12,CAN RX/TX接到了哪两个引脚。
2.4 编译产物在哪里
编译完成后,build目录下会生成.bin和.hex文件,这两个就是要烧录的固件镜像。bin和hex的区别只是格式,烧录器两种都认,看个人习惯选。
ls -lh build/*.hex build/*.bin我习惯烧hex,因为J-Flash、ST-LINK Utility这些工具对hex格式的地址信息识别得更明确。
3. 移植到APM32F072的关键改动:不只是一颗芯片的事
这里说句实话,moonglow固件对APM32F072的支持并不是开箱即用,虽然它是STM32F072的兼容替代,但极海原厂库和ST库在个别外设定义上还是有点差别。我在移植时遇到过几个问题,逐个说下解决办法。
3.1 时钟配置:48MHz USB需要的时钟链路
APM32F072内部有HSI(高速内部时钟)和HSE(外部晶振)两个时钟源。USB外设对时钟精度要求很严格,全速USB要求48MHz,而且这个48MHz的时钟源误差不能太大,否则PC端会识别不到设备。
moonglow默认用的是HSE驱动PLL来给USB提供48MHz。如果你的板子没有外部晶振或者晶振频率和默认配置不一致,系统时钟跑错,USB直接歇菜。
这部分要看板子图纸确认晶振频率。常见的072最小系统板用的是8MHz晶振,也有12MHz的。检查固件里时钟配置的宏定义:
#if defined(USE_HSE_8MHz) #define HSE_VALUE ((uint32_t)8000000) #define PLL_MUL 6 // 8MHz * 6 = 48MHz #elif defined(USE_HSE_12MHz) #define HSE_VALUE ((uint32_t)12000000) #define PLL_MUL 4 // 12MHz * 4 = 48MHz #endif如果你手头的板子用的晶振频率和配置不一样,要修改对应的PLL倍频系数,确保最后USB时钟稳定落在48MHz。
3.2 USB描述符:改VID/PID,让系统认出Kvaser
moonglow固件默认的设备VID/PID对应的是ECAN原版设备(VID 0x0C72,PID 0x1005)。刷进去之后,Windows设备管理器里能看到一个未知设备,但不会识别成Kvaser。
要让系统把它认成Kvaser设备,需要修改固件里的USB描述符,改成Kvaser对应型号的VID/PID。Kvaser Leaf Light HS v2的VID是0x0C72,PID有好几个子型号对应的数值,moonglow的README里提到了几个可以用在ECAN上的PID。
修改位置在usb_desc.c或者usb_prop.c里,找到设备描述符数组:
const uint8_t USBD_DeviceDesc[18] = { 0x12, USB_DESC_TYPE_DEVICE, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0x72, 0x0C, // idVendor = 0x0C72 0x05, 0x10, // idProduct = 0x1005 0x00, 0x01, 0x01, 0x02, 0x03, 0x00 };把idVendor保持0x0C72不变,idProduct改成你想要模拟的型号。Kvaser Leaf Light HS v2对应PID是0x1005,这个组合实测能稳定识别。
还要同步改字符串描述符里的iProduct名称,否则Windows驱动安装完后,设备名会显示成奇怪的名称。这个纯文字,不影响硬件功能,但影响体验,改了舒服很多。
3.3 引脚重新映射:从默认模板到目标板
不同板子的USB和CAN引脚接法可能差异很大。APM32F072的USB D+/D-可以映射到PA11/PA12,这个基本是固定的,不用改。但CAN的TX/RX就要看板子原理图了。
查看原理图的时候重点关注两个地方:CAN收发器(如TJA1050)的TXD/RXD接在MCU的哪两个引脚,以及CAN收发器有没有STB(Standby)引脚需要软件拉低才能正常工作。TJA1050的STB脚不能悬空,必须接GND或者由单片机输出低电平,否则CAN总线收发不工作。
moonglow的Board配置里,一般是用宏定义来指定CAN引脚:
#define GPIO_CAN_TX_PORT GPIOA #define GPIO_CAN_TX_PIN GPIO_PIN_12 #define GPIO_CAN_RX_PORT GPIOA #define GPIO_CAN_RX_PIN GPIO_PIN_11如果你的板子把CAN接在了PB口或者别的地方,按实际原理图改这三个宏就行。
3.4 APM32F072和STM32F072库函数差异的处理
APM32F072的固件库函数名和ST的标准外设库非常接近,但个别地方不完全一致。moonglow原来用的是ST标准外设库的API,在GPIO、RCC、CAN、USB这些外设上,极海的库要么函数名一样,要么功能完全等价,只是放在不同的头文件里。
最稳的办法是,编译报错哪里就改哪里。比如极海库的头文件是apm32f0xx.h而不是stm32f0xx.h,需要在几个关键C文件里把include路径切过来。直接在Makefile的CFLAGS里加-I参数指定极海库的inc目录,再把源文件里包含的头文件名改成apm32f0xx.h,编译通过率极高。
4. 烧录、识别、验证:从hex到真正跑起Kvaser协议
编译烧录的环节,我用的是极海官方的烧录工具,其实ST-LINK也能烧APM32F072,因为这个芯片支持SWD协议。这里分步骤说下完整过程。
4.1 SWD接线与烧录
APM32F072核心板预留了SWD接口,四根线:SWDIO、SWCLK、GND、3V3。用ST-LINK或者DAP-Link连接电脑,然后用STM32CubeProgrammer或者极海自家烧录工具:
STM32_Programmer_CLI -c port=SWD mode=UR -w build/ecan.hex -v如果烧录工具识别不到芯片,先检查SWDIO和SWCLK有没有接反,再看板子的供电是不是从USB取电、核心板的3V3和ST-LINK的3V3有没有冲突。这里有个很坑的细节:如果ST-LINK给板子供了3.3V,板子上的USB也供电,两边电压不一样就会导致烧录不稳定。建议烧录时只保留ST-LINK供电,拔掉USB线。
4.2 第一次插上电脑:设备枚举的瞬间
烧录完成后,拔掉ST-LINK,用USB线把核心板连到电脑。这时候Windows右下角应该弹出新设备提示,打开设备管理器,在“通用串行总线设备”下能看到“Kvaser Leaf Light HS v2”或者类似名称。
如果没有正确枚举,常见原因有三个:
- USB描述符没有改对。Windows对VID/PID识别非常严格,IDVendor写成别的厂商会导致驱动不匹配。
- 时钟配置不对。USB枚举对48MHz时钟的稳定度要求高,HSE频率设错或者PLL倍频不对,USB引擎根本起不来。用示波器测XOUT引脚有没有波形,能快速判断外部晶振是否起振。
- USB D+/D-上拉电阻问题。APM32F072内部USB收发器依赖外部1.5k上拉电阻到3.3V,用来告诉主机这是全速设备。如果板子在D+上没有这个1.5k上拉,USB枚举必然失败。多数成品核心板都设计好了,但有些精简板会偷工减料,遇到未识别的设备时要检查这个电阻是否存在。
4.3 安装Kvaser驱动并验证识别
设备枚举成功不代表驱动装好了。Windows需要安装Kvaser官方驱动才能把USB设备映射成CAN接口。到Kvaser官网下载驱动安装包,装上之后重新插拔一次设备。
此时再打开设备管理器,展开“Kvaser”分支,能看到类似“Kvaser Leaf Light HS v2”的设备节点。到这一步,系统已经把这个几十块钱的板子当成一块正经CAN卡了。
在命令行可以验证设备是否就绪。用Kvaser的CANKing软件,选择对应通道,如果能够打开通道不报错,说明协议栈已经正常通信了。
4.4 用实际报文验证CAN收发功能
设备能识别只算成功了一半,真正要验证的是CAN收发功能是否正常。我手头有另一块CAN分析仪,两种方法可以快速测试:
方法一:双机环回把你的USB-CAN板子A的CAN_H和CAN_H短接,CAN_L和CAN_L短接,再接一个120Ω终端电阻,用Kvaser CANKing发报文,另一块板子接收,如果收到,说明Kvaser协议栈收发通路都通了。
方法二:自发自收很多CAN控制器支持Loopback模式,即CAN控制器内部把发送的数据回环到接收路径上。在Kvaser软件里把CAN控制器配置成环回模式,如果能收到自己发的报文,说明CAN控制器工作正常,问题可能出在外部收发器上。
实际上,我用CANKing发了一组ID为0x123的标准帧数据,在另一块CAN卡上成功收到了完全一致的报文,CRC校验也通过,确认整个链路没问题。
5. 踩坑记录:移植过程中遇到的几个典型问题
这部分是实战中印象最深刻的教训,每个问题都花了不少时间排查,写出来帮你避坑。
5.1 极海库和ST标准外设库混编导致的编译冲突
moonglow仓库里有些驱动文件用的是ST标准外设库的写法,包含着stm32f0xx.h。APM32F072虽然是兼容芯片,但极海的库头文件名是apm32f0xx.h,两者混在一起,编译器要么找不到头文件,要么出现重复定义的宏冲突。
我的解决思路是:在Makefile里建立一个board_config.h,把极海库的头文件和ST头文件做一个统一转发,这样源文件里原来的include不用每个都改,只要在编译选项里指定-include这个转发头文件即可。
具体操作是建一个stm32f0xx.h的替身文件,内容只有一行:
#include "apm32f0xx.h"把工程里的include path优先指向这个转发目录,这样原来源码里所有include "stm32f0xx.h"就会自动变成包含apm32f0xx.h,不动源码就完成了库切换。
5.2 USB枚举不稳定:时而识别时而不识别
在开发过程中遇到过一次USB设备插上之后偶尔能被识别、偶尔不能的情况。用逻辑分析仪看D+/D-的电平变化,发现D+的上拉电阻值不稳定,焊接虚了。重新焊了那个1.5k电阻,问题消失。
另一个隐藏因素是供电。USB总线供电的核心板,如果板上还有其他器件在吃电流,特别是CAN收发器在总线活动时瞬间电流较大,会造成USB的VCC电压跌落,进而影响USB收发器工作。解决方法是给后级加一个电容稳压,100μF电解电容并联100nF陶瓷电容,能明显改善瞬态响应。
5.3 CAN波特率配置不对导致报错
moonglow固件默认的CAN波特率是500kbps,但我第一次用Kvaser软件打开通道的时候,软件提示总线错误,通道红灯闪。排查了半天,发现是总线上另一端的设备工作在250kbps,两边波特率不一致,CAN错误计数器直接爆表。
这不是固件问题,是使用习惯问题。在Kvaser CANKing的设置里,把总线波特率改成和总线对手一致即可。moonglow支持在USB命令里动态配置波特率,所以不需要重新编译固件,直接用软件修改就行。
5.4 收发器TJA1050不工作的问题
还有一次,焊好板子后Kvaser识别到了,但CAN报文怎么都发不出去。用示波器量MCU的CAN_TX引脚,波形正常,问题出在CAN收发器上。TJA1050的STB引脚悬空,导致收发器一直处于待机模式,总线驱动器不工作。
把STB引脚直接接地,问题解决。买TJA1050模块的朋友要注意,有些模块上的STB引脚默认已经拉低了,有些没有,拿到手先看原理图再连线。
6. 进阶玩法:去掉开发板,做成独立USB-CAN分析仪
核心板玩通之后,有人会想把它做成真正的硬件产品,脱离开发板,直接集成到自己的电路里。这块我给一个精简的设计思路。
6.1 最小系统电路设计
一个完整的USB-CAN分析仪,核心部分其实就四个模块:
- 主控部分:APM32F072C8T6,48MHz主频,LQFP48封装,配8MHz晶振、复位电路、BOOT0下拉到地
- USB部分:TYPE-C座子,D+/D-直接连PA11/PA12,D+上的1.5k上拉电阻不要省,VBUS接5V再经过稳压到3.3V
- CAN部分:TJA1050收发器,TXD接PA12,RXD接PA11,CANH/CANL经过共模电感后接DB9或者接线端子,两端加120Ω终端电阻选择跳线
- 电源部分:USB的5V通过AMS1117-3.3降到3.3V,加π型滤波
特别注意,CAN总线的终端电阻是标配,很多小白第一次做CAN卡,不知道要在总线上加120Ω电阻,导致通信不稳定、误码率高。
6.2 外壳和接口选择:产品化的细节
做产品化的时候,外壳开孔要提前规划好USB座子和CAN接插件的位置。CAN接插件建议用标准DB9公头,因为市面上的CAN调试线缆基本都是DB9接口,直接插就能用,不用自己焊线。
DB9的引脚定义有个行业惯性要注意:引脚2是CAN_L,引脚7是CAN_H,引脚3是GND,这个不是Kvaser独有,而是CiA(CAN in Automation)推荐的DS-102标准。跟着这个标准走,后续对接其他CAN设备就顺。
一个实用的小设计是加一个LED状态灯,固件里可以用一个GPIO控制LED,平时慢闪表示USB已连接,CAN报文收发时快速闪烁,方便现场排查问题。
7. 后续拓展:把moonglow固件改成你的私有CAN工具
到这里,moonglow固件刷机、识别、收发验证的完整路径已经走通了。最后聊一个很多人关心的方向:固件差分包更新和远程维护,这也是我在实际项目里下一步准备做的事。
moonglow本身没有OTA功能,但它的Flash分区是固定的,bootloader区域和应用区域可以分离。做个IAP引导程序,放在0x08000000区域,应用固件放在0x08004000之后,PC端配合一个简单的Python脚本,通过USB口把新的应用固件分包发送到设备端,设备在ram里缓存完整个包,校验CRC后再擦写Flash,这就是一个最基础的差分升级流程。
核心逻辑不复杂:
// IAP跳转函数 void jump_to_app(void) { uint32_t app_addr = 0x08004000; // 应用固件起始地址 void (*app_reset)(void) = (void (*)(void))(*(volatile uint32_t *)(app_addr + 4)); // 设置栈顶指针 __set_MSP(*(volatile uint32_t *)app_addr); // 跳转到应用复位向量 app_reset(); }配合一个host端脚本算出新旧固件的diff数据,只把差异部分传下去,能大幅减少USB传输的数据量。这在带宽受限或者报文密集的场景下,比整包升级实用得多。
另一个可以拓展的方向是通过USB-CAN通道做远程固件更新:把固件分片封装成CAN报文,通过另一端的CAN设备发送过来,实现不拆机远程升级。这个玩法虽然操作上更复杂,但用到量产现场维护上很香,尤其在设备装进机箱不方便拆开的时候。
我自己目前已经把moonglow跑在APM32F072上,识别为Kvaser Leaf Light HS v2,日常配合CANKing和Python-can库做总线调试,体验和原装Kvaser没有感知差异。如果你手头也有APM32F072或者类似兼容芯片的板子,强烈建议照着这思路试一遍,你会打开一个低价高性能CAN调试工具的大门。