简介:ODrive3.4固件(Keil移植版)面向电机控制与嵌入式开发者,将开源伺服驱动平台ODrive v0.3.6固件完整迁移至Keil μVision环境,解决原工程在MDK下编译、调试不便的问题,适用于机器人、自动化设备及高精度定位系统的FOC/PID算法研究与二次开发。资源包共435个文件、5.5MB,涵盖68个C源码、97个C头文件、Keil工程文件(uvprojx/uvoptx)、编译产物(hex/bin/axf/elf)、ARM Cortex-M4数学库(libarm_cortexM4lf_math.a)及Python构建脚本、配置文件、说明文档等,类型覆盖从源码到烧录镜像的全链路。已有4710人学习下载,对于需要掌控电机控制底层的工程师具有实操参考价值。开发者可直接用Keil打开工程,完成编译、烧录和在线调试,借助μVision的断点与变量观察窗分析FOC/PID控制流程;同时可参考其中的外设初始化、中断配置及RTOS集成方式,将固件灵活迁移至不同硬件平台,从而缩短运动控制系统的开发周期。
1. ODrive 3.4 固件为什么要做 Keil 移植版
ODrive 是开源无刷电机驱动器中最常被抄作业的那一类项目,原生固件基于 ARM GCC 工具链构建,配套的 odrivetool 通过 USB/UART 调参。但国内很多做机器人、云台、卡丁车改造的工程师日常主力环境是 Keil MDK,手头没有 Linux 或不想折腾 CMake 交叉编译链。把 ODrive 3.4 固件移植到 Keil 工程里,本质是把一套基于 GCC 的 STM32F405 工程,重新组织成 µVision 能直接编译、烧录、在 Debug 窗口里看变量和寄存器的一套工程。ODrive 3.4 这版固件对 HAL 库的依赖较深,还引入了一些编译期宏来切换板级配置,移植时牵涉启动文件、链接脚本、FPU 选项和中断优先级设置,不是把 .c 文件全部拖进来就能过编译的。这篇就把 ODrive 3.4 固件在 Keil 下从建工程到电机转起来的关键步骤说清楚。
2. 移植前先拆解 ODrive 3.4 固件的构建链路
2.1 ODrive 3.4 固件源码结构与构建方式
ODrive 3.4 固件源码主要在仓库的 Firmware 目录下,核心目录包括 MotorControl(电机控制环路)、hal(硬件抽象层)、low_level(定时器、ADC、GPIO 底层驱动)、boards(板级配置)、tests(自检代码)。原生构建采用 CMake 加 arm-none-eabi-gcc,构建时先处理版本文件生成,再按 target 编译出 elf、hex、bin。Keil 移植版的本质工作,是把这套构建流程翻译成 µVision 的工程组织方式,源文件可以原样引用,但宏定义、头文件路径、启动文件和分散加载文件必须手动配置。
移植前先把原生构建时传给编译器的宏梳理出来。我一般会先跑一次原生编译并打开 verbose 输出,把-D后面的定义全部抄下来。ODrive 3.4 里和硬件强相关的宏主要有这么几组:
| 宏定义 | 作用 | Keil 移植时是否必须 |
|---|---|---|
STM32F405xx | 选择 ST 官方头文件对应的芯片系列 | 必须 |
USE_HAL_DRIVER | 启用 HAL 驱动层 | 必须 |
HSE_VALUE=8000000 | 外部晶振频率,ODrive 3.4 板载 8MHz 晶振 | 必须与硬件一致 |
ARM_MATH_CM4 | 启用 CMSIS-DSP 的 Cortex-M4 指令优化 | 建议加上 |
__FPU_PRESENT=1 | 声明内核带 FPU,否则 DSP 库数学函数会走软浮点 | 必须 |
这些宏在 Keil 里填到 C/C++ 选项卡的 Define 输入框,分号分隔。HSE_VALUE这一步最容易漏,填错后系统时钟初始化会偏,ODrive 3.4 的电机控制频率不准,转起来噪音和震动都异常。
2.2 链接脚本与内存布局的差异
ODrive 3.4 原生链接脚本针对 STM32F405RGT6,Flash 1MB,RAM 192KB(128KB 常规 RAM 加 64KB CCM RAM)。原生脚本把部分电机控制缓冲区放进了 CCM,这样 CPU 访问更快。移植到 Keil 后,默认的 scatter 文件不会自动分配 CCM,如果不手动改分散加载描述,代码能跑但性能上限会变低,高频电流环表现受影响。
我一般会在 Keil 工程里把分散加载文件改成显式描述 Flash、RAM 和 CCM 三个区域,并把 ODrive 3.4 中用__attribute__((section(".ccmram")))标记的变量映射进 CCM。一个最小可用的 scatter 写法如下:
LR_IROM1 0x08000000 0x00100000 { ER_IROM1 0x08000000 0x00100000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) } RW_IRAM2 0x10000000 0x00010000 { .ccmram* (+RW +ZI) } }这段 scatter 把 1MB Flash 全部用作只读代码区,常规 RAM 放在 0x20000000 起始的 128KB,CCM 放在 0x10000000 起始的 64KB。需要注意 CCM 不能用作 DMA 目标地址,ODrive 3.4 里放在 CCM 的变量基本是控制环路里的中间量,不做 DMA 传输,这个分配是安全的。改完 scatter 后,编译出的镜像在 Keil 的 Memory Map 里能直观看到 CCM 被利用起来。
2.3 头文件路径与 HAL 库版本核对
ODrive 3.4 固件依赖 STM32CubeF4 的 HAL 库,具体版本在原生工程的 CMake 里会锁定。移植时最容易踩的坑是直接使用 Keil Pack 里自带的 HAL 版本,两套 HAL 的 API 有细微差异,比如部分新版本 HAL 把HAL_TIM_PWM_PulseFinishedCallback的参数从TIM_HandleTypeDef*改成了别的形式,编译时才发现函数签名对不上。建议从 ODrive 3.4 工程里引用的 HAL 源码目录整体拷贝进 Keil 工程,而不是另拉一份 Pack 里的 HAL。
头文件路径需要在 C/C++ Include Paths 里逐个添加:Firmware、MotorControl、hal、low_level、boards 各自对应的目录,外加 HAL 的 Inc 和 Inc/ST 目录。少加一层路径,编译报错会是一堆xxx.h: No such file or directory,而且不是第一个报错的文件,是源码 include 顺序里的第一个头文件,定位起来稍费时间。我习惯先在 Keil 里把 Include Paths 配完,再开始加源文件,这样每一步新增源文件后都能立刻验证头文件是否可见。
3. 用 Keil MDK 搭建 ODrive 3.4 工程的最小步骤
3.1 新建工程与选择芯片型号
打开 µVision,Project 菜单里 New uVision Project,先选芯片。ODrive 3.4 主控是 STM32F405RGT6,在 Device 对话框搜索 STM32F405RG 即可。新建工程后不要直接勾选 Manage Run-Time Environment 里的默认启动文件和系统初始化组件,ODrive 3.4 有自己的 startup 文件和系统时钟初始化逻辑,Keil 默认生成的 startup_stm32f405xx.s 和 system_stm32f4xx.c 可以保留 system 文件,但启动文件必须换成 ODrive 3.4 源码自带的版本,否则中断向量表对不上,电机控制中断进不来。
工程结构我一般按功能分区建组:MotorControl、HAL_Driver、LowLevel、Board_Config、Startup。分组的目的不是美观,而是便于在编译报错时快速定位是哪个模块的问题,也有利于后续单独优化某个目录的编译选项。
3.2 源文件分组与必要裁剪
ODrive 3.4 的 Firmware 根目录下有少量测试文件,比如tests目录里的自检代码,这些不需要加进 Keil 工程。核心必须添加的源文件有这几个:
MotorControl/odrive_main.c // 主循环、状态机、USB/串口命令处理 MotorControl/motor.c // 电机控制器,电流环速度环位置环 MotorControl/encoder.c // 编码器读数与校准 MotorControl/controller.c // 位置/速度闭环控制逻辑 low_level/timers.c // 定时器与 PWM 生成 low_level/adc.c // 相电流采样 low_level/gpio.c // GPIO 初始化与门控逻辑 hal/uart.c // 串口通信odrive_main.c 是入口文件,里面包含main()函数。原生工程中 main 函数会初始化时钟、外设,然后进入主循环。Keil 移植时,SystemInit()由系统文件完成,但HAL_Init()、SystemClock_Config()这些调用需要确认是否被编译进来。ODrive 3.4 里时钟树配置依赖HSE_VALUE,如果按默认的 25MHz 晶振值编译,板载 8MHz 晶振会导致 PLL 配置错误,系统主频跑不到 168MHz,USB 枚举失败,odrivetool 也连不上。
3.3 C/C++ 编译器选项的关键配置
ODrive 3.4 固件对编译器选项比较敏感。下面这份是我多次移植后固定下来的一组配置:
Define: STM32F405xx,USE_HAL_DRIVER,HSE_VALUE=8000000,ARM_MATH_CM4,__FPU_PRESENT=1 FPU: Single Precision Optimization: -O2 或 -Otime浮点选项必须在 Target 选项卡里选 Single Precision,否则编译出的代码没有硬浮点指令,电机控制环路里的sqrtf、sinf这类计算性能明显下降。__FPU_PRESENT和ARM_MATH_CM4同时定义,CMSIS-DSP 数学库才会启用硬件 FPU 优化路径。
代码生成选项里,我通常把 C99 模式打开,ODrive 3.4 源码里有for(int i=0; ...)这类 C99 风格写法。µVision 的 AC5 编译器默认不是 C99,编译时会报变量声明位置错误,改成 C99 模式即可。如果用的 AC6,默认对 C99 支持较好,但语法严格程度更高,ODrive 3.4 里部分隐式类型转换会升级成警告或错误,需要额外处理。
3.4 手工生成 firmware_version.h
ODrive 3.4 源码中odrive_main.c会 includefirmware_version.h,这个头文件原生构建时由 CMake 脚本根据 git 版本自动生成,仓库里没有现成文件。Keil 移植版必须手工补一个,否则第一步编译就报找不到头文件。最常见的手工版本:
#define FW_VERSION_MAJOR 0 #define FW_VERSION_MINOR 5 #define FW_VERSION_PATCH 4 #define FW_VERSION_STRING "0.5.4-keil" #define GIT_COMMIT_HASH "keil-port"版本号要和你的实际固件对应。ODrive 3.4 固件版本号不是 3.4,3.4 是硬件版本,固件版本是 0.5.x 系列,这一点移植前就要分清楚。odrivetool 连接时会上报这个字符串,如果版本号和连接工具不匹配,工具可能提示固件过旧或过新,但不影响基本控制命令。
4. 编译报错与运行时异常的逐项适配
4.1 GCC 内联汇编与attribute语法的转换
ODrive 3.4 源码里存在少量用 GCC 语法写的内联汇编,主要在底层临界区保护和特定寄存器操作里。Keil MDK 的 AC5 编译器不认识 GCC 内联汇编语法,编译到这类代码会报条码错误。常见做法是直接改写为 CMSIS 提供的函数或 C 代码。
临界区保护我一般替换为:
__disable_irq(); // 临界区代码 __enable_irq();如果原代码里有保存中断状态并恢复的写法,CMSIS 提供了__get_PRIMASK()和__set_PRIMASK():
uint32_t primask = __get_PRIMASK(); __disable_irq(); // 临界区代码 __set_PRIMASK(primask);ODrive 3.4 中用__attribute__((packed))定义的结构体,在 Keil 下可以用__packed关键字或直接忽略。__packed在某些编译选项下会降低结构体访问效率,如果只是做串口协议解析,影响很小。
4.2 启动文件与中断向量表核对
ODrive 3.4 的中断向量表里注册了 TIM1 更新中断、ADC 中断、UART 中断、USB 中断等。Keil 新建工程默认生成的 startup 文件用的是 ST 官方的中断向量表,和 ODrive 3.4 的中断处理函数名一一对应才能正常链接。移植过程中最常见的报错是链接阶段出现Undefined symbol TIM1_UP_TIM10_IRQHandler之类,这是因为源码里实现了中断处理函数,但启动文件里没有对应向量入口。
解决办法是把 ODrive 3.4 源码中实际使用的中断向量名和 startup 文件逐一对照。ODrive 3.4 用到的关键中断有TIM1_UP_TIM10_IRQHandler、TIM3_IRQHandler、ADC_IRQHandler、UART5_IRQHandler等。缺失的向量在启动文件里补齐:
extern void TIM1_UP_TIM10_IRQHandler(void); // 在向量表中添加 TIM1_UP_TIM10_IRQHandler,还有一种更省事但不太推荐的做法:直接使用 ODrive 3.4 源码里自带的 startup 文件。如果源码里没有,就从同系列 STM32 的 GCC 启动文件手动转换成 Keil 格式,转换时注意栈大小和堆大小要满足 ODrive 3.4 的需求,默认 0x400 字节的栈可能不够。
4.3 浮点单元配置错误导致的链接报错
链接阶段出现undefined symbol __aeabi_fadd或__aeabi_dmul,基本可以断定编译器启用了软浮点,而项目里又引用了硬浮点库或 DSP 库。Keil 里检查两处:一是 Target 选项卡的 Floating Point Hardware 是否选到 Single Precision,二是编译选项中是否误加了-mfloat-abi=softfp之类的 GNU 风格参数。ODrive 3.4 的电机控制代码大量使用 float 运算,FPU 没开启时能编译能链接,但电机跑起来后速度环和电流环的实时性不够,现象是同样的给定速度,相比 GCC 原生固件,震荡明显且动态响应慢。
4.4 串口日志定位启动卡死位置
ODrive 3.4 在启动阶段如果检测到硬件异常,会停在某个断言处。烧录后如果电机没反应,第一步不是查电机,而是连串口看日志。ODrive 3.4 默认调试串口是 UART5,波特率 115200,直接接 USB 转串口模块观察。
正常启动日志最后会输出当前固件版本和板卡信息。如果日志停在某个assert相关输出之前,说明初始化阶段有问题。最常见的几个位置:
- 时钟树配置失败,现象是日志完全没有输出,程序卡在
HardFault_Handler - 编码器检测失败,ODrive 3.4 启动时会尝试读取编码器方向,报
encoder not found - FLASH 参数读取异常,ODrive 3.4 会把校准参数存到内部 Flash,如果 Flash 没有正确初始化,启动流程会卡在参数加载
出现这类情况时,先在 Keil 的 Debug 模式下打断点看HAL_Init()之后的返回值,确认 HAL 库初始化正常,再逐步定位具体外设。
我一般还会把Error_Handler()里加一个 while(1) 和串口打印,这样任何 HAL 函数出错都能立刻知道错在哪个外设:
void Error_Handler(void) { printf("HAL Error at %s line %d\r\n", __FILE__, __LINE__); while(1) { } }__FILE__和__LINE__宏在 Keil 调试时能直接定位到出错的外设初始化函数。
5. Flash 烧录后的验证步骤与电机首次上电测试
5.1 生成 hex 并用命令行烧录
Keil 工程在 Output 选项卡勾选 Create HEX File 后,编译会在 Objects 目录生成 hex。烧录工具我固定用 J-Flash 的命令行模式,方便重复执行:
JFlash -openprjODrive_Keil.jflash -openODrive.hex -connect -erase -program -verify -exit参数说明:-openprj加载烧录配置工程,-open指定 hex 路径,-connect建立连接,-erase擦除整片 Flash,-program写入固件,-verify烧录后回读校验,-exit完成后退出。用命令行而不是 J-Flash GUI 的目的在于,移植版固件会频繁迭代,脚本化烧录能省下大量重复操作。
烧录完成后检查-verify是否通过,不通过时先查 SWD 连接速率,ODrive 3.4 板载 SWD 接口建议用 1MHz 以下速率连,部分仿真器在 4MHz 下会不稳定,导致校验失败。
5.2 首次上电前的接线与供电顺序
ODrive 3.4 移植版固件第一次上电,先不接电机。只接 USB 供电,观察板载 LED,正常会进入 idle 状态。然后用 odrivetool 输入odrivetool连接,如果串口能枚举到设备,说明 USB 外设初始化成功。这一步能快速区分是固件问题还是电机问题。
接线顺序我习惯是:先接编码器,再接电机相线。ODrive 3.4 上电时如果编码器没有反馈,会报编码器错误,但不会损坏硬件。电机相线接错顺序会导致电机抖动或堵转,校准前务必确认 U、V、W 三相一一对应。
5.3 用 odrivetool 验证 Keil 移植版固件功能
连接成功后执行以下命令验证基本功能:
odrv0.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL这段命令让电机进入闭环控制状态。能进入闭环说明定时器 PWM、ADC 采样、编码器读数链路都正常。再给一个速度指令:
odrv0.axis0.controller.config.control_mode = CONTROL_MODE_VELOCITY_CONTROL odrv0.axis0.controller.input_vel = 5.0input_vel单位是圈每秒,设 5.0 表示每秒 5 圈。如果电机平稳转到这个速度,Keil 移植版固件的基本功能就算验证通过了。最后用odrv0.save_configuration()保存参数。这一步如果报错,通常是 Flash 写入时序问题,回头检查 HAL_FLASH 相关代码是否被裁剪。
本文还有配套的精品资源,点击获取