拓冰建站拓冰建站
首页 / 资讯中心 / 正文

APM32F072移植moonglow固件:打造Linux/Windows双兼容的USB-CAN工具

如果你手里有一块用APM32F072做的USB-CAN小板子大概率会碰到一个很尴尬的情况硬件长得跟canable、CANtact这些板子差不多但固件要么是闭源的要么功能简陋Linux下没法直接当SocketCAN设备用Windows下也没有Kvaser那种驱动级别的体验。我前阵子把moonglow这套开源固件移植到了APM32F072上刷进去之后板子直接脱胎换骨Linux下能被gs_usb识别成标准CAN网卡切到Kvaser兼容模式后Windows下装官方驱动就能用PCAN-View、CanKing这些软件。整个过程比预想中折腾不少踩了几个挺深的坑写出来给同样手痒的朋友当个参考。这篇文章不是什么“照着敲就能成”的保姆级教程而是更偏向一次完整的移植复盘从为什么要刷、芯片差异在哪里、源码怎么改、编译烧录怎么搞到验证时的翻车现场和排查思路都会讲到。适合手里有国产F072核心的USB-CAN板子想跑开源固件但又不想直接硬刷STM32二进制的人。1. 为什么放着原厂固件不用偏要折腾moonglow先说个现实问题。市面上大量USB-CAN适配器用的都是STM32F072或者跟它兼容的国产芯片APM32F072就是其中之一。这类板子的原厂固件大多只配套自家上位机功能上往往就是“打开软件—选择通道—收发报文”三板斧。你要是想在Linux服务器上用candump抓CAN日志或者在车辆诊断里跑一段Python脚本通过CAN口发UDS报文原厂那套工具链基本帮不上忙甚至有些厂家压根不提供Linux驱动。moonglow这个固件解决的正是“固件生态”的问题。它属于candleLight固件的一个延续分支核心目标是把USB-CAN适配器做成一个“协议上开放”的硬件。刷上它之后板卡默认走gs_usb协议这是Linux内核原生支持的标准协议不用装任何第三方驱动插上就能在SocketCAN体系里操作。与此同时固件里还内置了Kvaser Leaf Light的协议模拟切到那个模式后Windows下可以直接装Kvaser官方驱动于是Kvaser自带的各种工具、canlib二次开发接口、甚至某些只认Kvaser硬件的商业诊断软件都能把这块板子当成正版Kvaser Leaf Light来用。我个人的看法是与其说moonglow是个“破解平替方案”不如说它把固件层的能力边界打开了一个口子。对个人开发者、DIY爱好者、做车载测试的小团队来说这套固件意味着你不再被硬件厂商的软件生态绑架板子的行为完全可控甚至能自己改协议细节。这也是我折腾它的核心动力。当然原厂固件也不是一无是处。如果你只需要个简单的CAN调试工具原厂软件打开就能用那完全没必要折腾。但如果你想要的是“一个USB-CAN设备在Linux下表现得像一块网卡在Windows下能被工业软件直接驱动”那moonglow这条路是绕不开的。2. 移植前先摸清芯片底细APM32F072与STM32F072到底差在哪动手之前我先把APM32F072和STM32F072的差异梳理了一遍。这不是例行公事而是直接决定移植策略是可以直接烧编译好的STM32固件还是必须源码级迁移。2.1 先看硬件参数对比项目STM32F072APM32F072内核Cortex-M0Cortex-M0主频48MHz48MHzFlash32/64/128KB同容量等级SRAM4/6/16KB兼容USBUSB 2.0 FS DeviceUSB 2.0 FS DeviceCANbxCAN 2.0BCAN 2.0B供电2.0~3.6V兼容引脚封装LQFP32/48/64等兼容常见封装单看参数表格两者几乎是一个模子刻出来的。APM32F072在设计目标上就是冲着硬件兼容STM32F072去的引脚定义、电源域、大部分外设功能都能对得上。但“硬件兼容”不等于“软件代码直接通用”这里面的坑主要在寄存器级和外设库API上。2.2 “兼容”的真实含义三处决定成败的差异第一处差异是内核和外设寄存器映射。APM32F072的内核是标准Cortex-M0调试接口、中断控制器这块跟ST完全一致这部分不用担心。但极海的外设寄存器虽然参考了ST的设计具体到某些控制位、保留位和状态标志位和ST的参考手册并非逐位一致。尤其是RCC时钟控制、PLL配置、USB和CAN这两个外设模块极海有自己的实现细节直接用ST的HAL库去访问编译能过跑起来可能完全不对。第二处是USB模块。STM32F072的USB设备控制器直接挂在APB总线上没有独立的USB PHYD和D-引脚内部集成收发器。APM32F072的USB模块在功能上等价但寄存器偏移和端点缓冲区的组织方式不一定完全相同。也就是说ST的USB Device库不能直接编译到APM32上跑要么用极海SDK里自带的USB驱动要么自己适配底层寄存器操作。第三处是CAN模块。ST的bxCAN是个非常经典的CAN控制器很多国产芯片在CAN外设上都会“参照”它。APM32F072的CAN控制器从功能上支持标准帧、扩展帧、FIFO和过滤器跟bxCAN的用法很接近但模式字、位时序寄存器和ST的不完全对齐。这直接导致一个常见的问题你在ST的例程里把CAN波特率算好了、寄存器值填进去了换到APM32上可能就差一位然后总线怎么都对不上。2.3 移植策略选择秒刷还是源码级移植在动手前我建议你先明确策略。市面上有一部分人直接拿STM32F072的固件二进制烧进APM32F072这属于“赌兼容”。我实测过部分外设确实能跑USB枚举、CAN收发在这种“硬刷”状态下竟然也能工作因为APM32F072在寄存器层面做了不少兼容设计。但这种方式的风险在于一旦碰到芯片具体版本差异、特殊时钟配置或者某个外设在极海上面修正过的模块就会表现得非常诡异而且出了问题没有源码没法查。我最终选的是源码级移植。原因很简单我要的不是“碰巧能用”而是要把固件的行为完全掌握在自己手里。而且极海官方提供了APM32F0xx的SDK里面有标准外设库、USB例程和CAN例程把这些作为底层支撑往上移植moonglow的协议层和应用层工作量并没有想象中那么大。3. 移植过程中绕不开的三座山时钟、USB、CAN把源码拉下来之后我发现moonglow以及candleLight系工程的代码结构其实挺清晰的板级配置、USB协议处理、CAN驱动、主循环几个模块分得很清楚。对APM32F072的移植来说最核心的改动集中在三个方面系统时钟怎么给USB供出精确的48MHz、USB底层怎么从ST库切到极海库、CAN驱动怎么用极海的API重新实现。3.1 先认识一下固件的工程结构我用的仓库基本结构是这样的firmware/ ├── board/ # 板级配置不同硬件在这里区分 │ ├── board.h │ └── board.c ├── usb/ # USB Device层和协议处理 ├── can/ # CAN驱动抽象 ├── src/ # 主循环、命令处理 └── Makefile这个结构最大的好处是CAN和USB协议处理是跟具体芯片无关的。比如gs_usb的请求解析、Kvaser协议的模拟逻辑、CAN报文到USB消息的转换这些代码直接挪过来不用动。需要改的是驱动层板级初始化、时钟配置、GPIO操作、USB底层、CAN底层。换句话说移植的本质是把“驱动层”从“芯片无关层”下面抽出来换成APM32F072的SDK实现。3.2 第一座山USB时钟必须严格48MHzUSB全速设备对时钟的要求非常苛刻必须在48MHz偏差超过0.25%就可能导致枚举失败。STM32F072的做法是用内部的PLL把某个时钟源倍频到48MHz然后通过时钟选择位把PLL输出作为USB外设时钟。APM32F072的逻辑类似但RCC寄存器的位域分配可能不同。我当时的时钟配置思路是/* 移植思路示意具体API以你的极海SDK版本为准 */ void SystemClock_Config(void) { /* 目标系统时钟48MHzUSB时钟PLL输出 */ RCC_Config_T rccConfig; rccConfig.clockSource RCC_CLKSOURCE_HSI; /* 内部8MHz */ rccConfig.pllSource RCC_PLLSOURCE_HSI; /* PLL输入为HSI */ rccConfig.pllMul RCC_PLL_MUL6; /* 8MHz * 6 48MHz */ RCC_Config(rccConfig); RCC_USB_ClockConfig(); /* 关键让USB用PLL时钟而不是系统时钟 */ }这里有个特别容易踩的坑很多人移植的时候只把系统主频配置到了48MHz却忘了设置“USB外设的时钟源选择”。结果就是CAN收发什么的都正常但USB插到电脑上没反应或者识别到设备但枚举失败。量D引脚电平会发现USB的帧起始包根本没出来基本就是时钟源没选对。另外如果板子用的是外部晶振还要确认晶振频率。有些国产板子为了省成本用的是8MHz晶振也有一些用12MHz。PLL配置必须跟着实际晶体频率走。我在项目里就遇到过标称8MHz晶振实际焊接时给了不同频率的情况导致USB枚举不稳定。这个后面验证部分再细说。3.3 第二座山USB底层移植保留协议层替换寄存器层moonglow的USB协议栈原本是面向STM32实现的底层直接操作USB寄存器或者在工程里依赖ST的USB Device库。到了APM32F072上最稳妥的做法是足够熟悉USB枚举流程的前提下用极海SDK的USB Device库来实现一个功能等价的底层驱动并且保留上层原固件里的class handler和协议解析逻辑。USB协议层的细节确实多但核心就两件事设备描述符VID/PID、端点描述符要跟原固件保持一致或者改成你自己的VID/PID。moonglow默认配置里gs_usb模式用的是canable标准的VID/PID0x1D50/0x606FKvaser模式则模拟Kvaser Leaf Light的标识。这块不需要动逻辑但要注意如果你的板子和别人共用同一套VID/PID多台设备同时插电脑时可能会出现设备节点混淆建议个人DIY的话要么保留默认、要么改成独立PID。端点处理canable板卡用的是两个端点一个IN端点用于往主机传CAN报文一个OUT端点用于接收主机下发的命令和待发送报文。APM32F072的USB端点缓冲区和ST的编排方式可能不一样所以在极海SDK里初始化端点时要关注缓冲区地址对齐和DTSDMA传输状态的配置。给你看一段我当时改的思路static void usb_ep_init(void) { /* 使用极海USB驱动的端点初始化接口 */ USBD_EP_Config(EP_IN, USB_EP_TYPE_BULK, CAN_EP_PACKET_SIZE); USBD_EP_Config(EP_OUT, USB_EP_TYPE_BULK, CAN_EP_PACKET_SIZE); /* 准备好接收主机的下发命令 */ USBD_EP_Receive(EP_OUT, ep_rx_buffer, CAN_EP_PACKET_SIZE); }上层收到端点数据的处理函数完全沿用原固件的逻辑解析gs_usb报文、执行CAN控制命令、返回状态。这部分是整个移植中“复制粘贴成本最低”的部分也是收益最大的部分。3.4 第三座山CAN驱动重新对接CAN驱动的移植踩坑最多。moonglow原本的CAN驱动针对STM32的bxCAN寄存器写的直接搬到APM32上会有两个问题库函数没有、寄存器位段可能不同。我的做法是用极海SDK的CAN外设标准库把原固件里“CAN初始化、报文发送、报文接收、错误处理”这几个功能重新实现一遍。初始化时最要注意的是波特率static void can_init(uint32_t bitrate) { CAN_Config_T canConfig; /* 计算预分频和位段参数保证采样点尽量在75%~80% */ canConfig.baudRatePrescaler ...; canConfig.syncJumpWidth CAN_SJW_1TQ; canConfig.timeSegment1 ...; canConfig.timeSegment2 ...; CAN_Config(canConfig); /* 配置过滤器默认接收全部标准帧和扩展帧 */ CAN_SetFilterAllPass(); }这里要特别强调一下不同固件对CAN波特率定义的方式不一样。有的直接给寄存器分频值有的是按“想要的波特率”传进来让驱动自己算。moonglow采用的是后者好处是上层代码可读性强但底层换算必须重新实现。bitrate 外设时钟 / (预分频器 * (1 BS1 BS2))这个公式大家都会但APM32F072的CAN外设时钟源是从APB1来的而APB1的时钟来自系统时钟分频。如果系统时钟不是48MHz或者APB1分频系数不是1:1波特率的计算结果就会跟预期差一大截。我在实际验证时曾遇到过代码里写的500kbps实际总线报文时间间隔完全对不上后来查了半天发现是APB1时钟被默认分频成了36MHzCAN外设时钟根本不是48MHz。3.5 板级适配别忽略LED、跳线和引脚映射moonglow的一个好处是板级配置独立成文件。原版canable的默认引脚分配是给特定板子设计的哪两个引脚接CAN收发器、哪个引脚控制LED、哪个引脚接跳线用于切换Kvaser模式都不一样。移植到自己的板子上一定要先翻原理图把引脚映射改到board.h里。我手里这块板子用的主控是APM32F072C8T6LQFP48封装CAN收发器是TJA1050CAN_TX/RX分别接到了PA12/PA11LED在PA1USB_D/D-是PA11/PA12……等等这里就出现了一个经典的引脚冲突问题PA11/PA12在F072上是USB引脚但也被复用为CAN_TX/CAN_RX。当时我一度怀疑自己看错了原理图后来仔细核对才发现板子设计时USB用的是PA11/PA12CAN实际接的是PB8/PB9这一类带重映射的引脚。这种问题只能靠对着“板子原理图 芯片数据手册”一个个引脚确认没有捷径。4. 编译和烧录从Makefile到JTAG/ISP/DFU的完整通路源码改完只是个开始真正的折磨从编译器和烧录工具开始。这个项目用的编译链是arm-none-eabi-gcc工程自带Makefile整体来说对Linux用户非常友好Windows下则需要自己搭好环境。这也是为什么我推荐尽量在Linux或者WSL里编译省去一大堆环境变量和路径问题。4.1 编译环境和工程调整我用的编译环境是Ubuntu 22.04直接安装sudo apt install gcc-arm-none-eabi make然后进入固件源码目录先make clean再make。第一次编译大概率会报错多半是链接脚本里Flash/RAM大小和芯片型号不匹配。APM32F072C8T6是64KB Flash、16KB SRAM而默认的链接脚本可能按128KB Flash写的需要把芯片型号定义和链接脚本改成实际参数。改法不复杂核心就是让链接器知道“代码能放在哪个地址范围”MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (rwx) : ORIGIN 0x20000000, LENGTH 16K }编译成功后产物里会有.bin或.hex文件。这个就是待烧录固件。4.2 SWD烧录最省心的方式如果你的板子预留了SWD调试口那用SWD烧录是最省心的。ST-Link、J-Link、DAP-Link都行OpenOCD配置里选择STM32F0x的target因为APM32F072的内核和调试接口跟STM32F072完全兼容。命令大致是openocd -f interface/stlink.cfg -f target/stm32f0x.cfg -c program build/moonglow.bin 0x08000000 verify reset exit注意别把“内核调试接口兼容”和“外设寄存器兼容”搞混。SWD能识别、能烧录不代表固件跑起来没问题这是两码事。4.3 串口ISP烧录没有调试器时的出路很多DIY板子为了省一个调试座只留了UART引脚。这时候就要用芯片内置的ISP bootloader。APM32F072在BOOT0引脚拉高、复位后会进入系统bootloader通过UART接收协议数据。极海在ISP协议上做了兼容设计所以常用的stm32flash工具可以直接用# 先把BOOT0拉高上电/复位然后执行 stm32flash -w build/moonglow.bin -v -g 0x0 /dev/ttyUSB0这里有两个容易被坑的点波特率。ISP模式下工具默认波特率通常没问题但如果你的USB转串口模块质量一般长固件传输中途可能丢包。建议降速比如加个-b 115200参数强制试试。BOOT0跳线。刷完固件后必须把BOOT0拉回低电平再复位否则芯片重新进入bootloader跑的是ISP而不是你的固件板子看起来就跟“没刷进去”一样。4.4 后续升级DFU方式如果以后想随时通过USB刷固件可以再移植一个DFU bootloader。原版canable生态里有独立的dfu固件它占用Flash前段通过USB的DFU协议接收固件。但这里有个先后问题你得先把DFU bootloader刷进去以后才能用dfu-util刷应用固件。DFU的适配同样涉及USB时钟和描述符的移植比应用固件简单一些因为只需要枚举和Flash擦写。我个人的建议是第一次移植先用SWD或者ISP把整个流程跑通DFU作为后续优化项再搞。一上来就想着DFU万一bootloader和应用固件的地址分配没对好很容易把板子刷成砖还得回来找调试器。5. 验证和踩坑记录不是刷进去就完事刷完固件仅仅是第一步验证才是真正让人头大的环节。我总结了两个平台上的验证流程和一些高频坑都是实打实遇到过的。5.1 Linux下验证SocketCAN的即插即用Linux下验证这套固件是体验最好的环节。板子插上USB后先看内核日志dmesg | tail -20正常情况会出现gs_usb相关日志然后多出一个名为can0或can1的网络接口。如果没有先试着手动加载内核模块modprobe gs_usb确认接口出来后配置波特率并启动sudo ip link set can0 up type can bitrate 500000然后用candump监听candump can0在总线上另找一个节点发包这里我用的是另一块USB-CAN板子。如果能收到报文就说明CAN收发链路已经通了。5.2 Windows下验证Kvaser兼容模式moonglow里有一个切换模式的办法短接特定引脚或通过控制命令让固件进入Kvaser兼容模式。具体切换方式看板子配置有的是一个跳线有的通过USB控制命令。切换成功后Windows下把设备插上系统识别到的是“Kvaser Leaf Light v2”之类的设备此时安装Kvaser官方驱动。驱动装好后打开PCAN-View或者CanKing选择对应的Kvaser通道就能正常收发CAN报文。5.3 我踩过的五个坑以及排查链路第一个坑USB枚举失败电脑完全无反应。排查链路是先量芯片供电是否3.3V稳定→再查USB D/D-是否反接→然后用逻辑分析仪看D上拉信号是否在插入时被拉高→最后怀疑时钟配置。我这次的问题是在PLL配置上漏了一条USB没有拿到48MHz时钟导致上拉虽然瞬间有但后面没有响应主机复位请求。第二个坑CAN波特率不对通讯总是不稳定。排查的时候我先确定是不是收发器方向接反后来发现是APB1时钟的问题。APM32F072的APB1分频默认不是1如果系统时钟配置里没有把APB1配成48MHzCAN外设的时钟源就有偏差波特率自然不对。解决办法是重新梳理RCC配置确保APB1 48MHz。第三个坑Windows下识别到设备但驱动装不上。Kvaser官方驱动对设备有校验自编译固件的USB描述符和标准Kvaser硬件存在差异时Windows会提示驱动安装失败。这个问题的根源在于固件里的Kvaser模式虽然协议是兼容的但USB描述符的某些字段跟真实硬件不一样。解决方式要么是把描述符细节对齐要么在Windows高级启动里禁用驱动签名强制后手动安装。注意手动安装驱动属于应急手段只建议在开发调试阶段使用。第四个坑总线上CAN报文丢帧严重。排查发现不是固件问题而是我这块板子没有接终端电阻。CAN总线两端必须有120Ω终端电阻如果板子上没有焊接需要在连接器里外接一个120Ω电阻。这是个容易出现误判的硬件细节很多丢帧问题其实是信号反射导致的。第五个坑多块同样配置的板子插同一台电脑设备节点混乱。原因是所有板子固件里的VID/PID和序列号都一样gs_usb驱动区分设备时容易出现错乱。解决之道是在固件里给每块板子生成一个独立序列号或者修改PID区分不同硬件版本。这块逻辑原版固件已经预留了接口只是需要自己改成从芯片UID读取。6. 刷完之后的日常这套固件怎么用才顺手移植完成、验证通过之后这套固件带来的使用体验变化是很直观的。Linux下把can0网卡配起来之后我可以在一个系统里直接做CAN报文分析、录制回放、甚至写shell脚本做自动化测试。配合can-utils里的一堆工具能实现不少原本需要商业软件才能做的事。比如我用Python的python-can库在Linux下走socketcan接口几行代码就能发UDS诊断请求import can bus can.interface.Bus(channelcan0, bustypesocketcan, bitrate500000) msg can.Message(arbitration_id0x7DF, data[0x02,0x10,0x03], is_extended_idFalse) bus.send(msg)而Windows下切到Kvaser兼容模式后又可以配合Kvaser的CANLib SDK跑测试脚本兼容一些只支持Kvaser硬件的商用工具。这种“一套硬件、两种生态”的能力是原厂固件给不了的。还有一个很实用的点是由于固件开源你可以按需裁剪功能。比如不需要Kvaser模式的话可以把那段协议代码直接去掉减小固件体积想改采样点位置、增加CAN FD支持如果电路和收发器支持也能从源码层面着手。最后给一个个人建议如果你只是为了满足好奇心硬刷STM32的二进制碰碰运气也无可厚非但如果你要把这块板子用于长期项目、甚至作为测试工具链的一部分一定要走源码级移植这条路。只有源码在自己手上你才能在遇到奇怪问题时不至于两眼一抹黑。我这次移植APM32F072总共花了一个周末加两个晚上大部分时间都耗在时钟和CAN波特率这两个问题上。但跑通之后那种“板子完全受自己控制”的感觉确实比直接用原厂固件爽太多了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门