作为一个常年折腾嵌入式小工具的玩家我最近把一个吃灰已久的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内核主频最高48MHzFlash有64KBSRAM 8KB我这里用的是APM32F072VBT6Flash 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-LinkCAN收发器模块我这里用的是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_TXPA12RXD接CAN_RXPA11VCC接3.3VCANH、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一样系统时钟源可以从HSI8MHz或HSE外部晶振获取。USB外设需要精确的48MHz时钟这个48MHz通常是由PLL从8MHz倍频得到的。我的板子上没有焊接外部晶振所以直接用HSI 8MHz配置PLL倍频6倍得到48MHz。具体配置方法如下系统时钟源选择HSIPLL源选择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_DMPA12为USB_DPPA13为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为RXPA12为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 1BT1 13BT2 2我配的寄存器值CAN_BTR 0x000A0011表示预分频值为6BT113BT22采样点在(113)/(1132)87.5%这个采样点对500kbps的CAN总线来说比较合适实际操作过程中我建议先用环回模式LoopBack测试CAN收发这样可以排除总线上的干扰和终端电阻问题确认MCU的CAN控制器本身工作正常再接外部设备联调。3.3 关键代码段解析moonglow的USB描述符与协议转换moonglow固件里有一个非常核心的文件叫做usb_desc.c不同版本可能叫法略有不同里面定义了设备描述符、配置描述符、接口描述符和端点描述符。PCAN-View识别设备时就是通过VIDVendor ID和PIDProduct 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-12CAN数据moonglow收到这些命令后解包并调用CAN发送函数。反过来CAN接收中断里收到报文后会按照PCAN协议格式打包通过USB IN端点发送给上位机。3.4 kvaser固件的特殊之处相比moonglowkvaser固件的上层协议走的是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或6Keil自带如果不想用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_RXPA12为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固件后插上USBWindows会识别到硬件然后去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大小128KBSRAM起始地址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底层外设的理解都上了一个台阶。如果你也正在折腾类似的移植项目别怕报错每一步报错都是在帮你更深入地理解芯片手册。
