ODrive Fireware v0.5.6 — Board文件夹源码解析

解析范围:ODrive-Fireware-v0.5.6/Firmware/Board/v3/ 以及同层的 board.cpp / board.h

目标:帮助你快速理解 ODrive v3.x 在 STM32F4 平台上的 硬件抽象层(Board Support Layer) 是如何完成外设初始化、PWM/ADC 同步采样、控制回路中断调度、以及不同硬件小版本(v3.1~v3.6)兼容的。


0. 目录结构总览

Firmware/Board/v3/ 基本是 CubeMX 生成的工程骨架 + ODrive 做的少量改造

Firmware/Board/v3/
├─ board.cpp                    # ODrive 的“板级聚合层”:外设对象/中断控制回路
├─ Inc/
│  ├─ board.h                   # ODrive v3.x 板级配置宏/外设句柄/全局对象声明
│  ├─ main.h                    # CubeMX 生成的 GPIO 引脚宏定义(*_Pin / *_GPIO_Port)
│  ├─ mxconstants.h             # 工程常量(PWM 频率、死区、TIM 时钟等)
│  ├─ adc.h / gpio.h / tim.h ...# 各外设 HAL 初始化头文件
│  ├─ stm32f4xx_it.h            # 中断声明
│  ├─ FreeRTOSConfig.h          # FreeRTOS 配置
│  └─ prev_board_ver/           # 旧版本(3.2/3.4)的 GPIO/ADC 初始化代码
└─ Src/
   ├─ main.c                    # CubeMX 的入口(SystemClock_Config 等)
   ├─ adc.c / tim.c / spi.c ... # HAL 外设初始化实现
   ├─ freertos.c                # 默认 RTOS 任务(基本只启动 USB)
   ├─ stm32f4xx_it.c            # 默认中断入口(HardFault 等)
   ├─ stm32f4xx_hal_timebase_TIM.c # HAL tick 的 TIM 实现(替代 SysTick)
   ├─ usb_device.c + usbd_*     # USB CDC 设备栈
   └─ prev_board_ver/           # 旧版本(3.2/3.4)的 GPIO/ADC 初始化代码

需要关心的“Board 层逻辑”主要集中在两块:

  1. board.h / board.cpp(ODrive 自己写的板级聚合层)
  2. tim.c / adc.c(PWM—ADC 同步采样链路的 CubeMX 配置)

1. 启动链路(系统初始化 → 外设初始化 → 控制回路运行)

ODrive 固件的总体启动顺序可以理解为:

  1. system_init():HAL & 时钟 & OTP 硬件版本检查
  2. board_init():CubeMX 外设初始化 + 中断优先级配置 + 驱动芯片复位
  3. start_timers():TIM1/TIM8/TIM13 同步启动,打开 ADC 触发
  4. 控制回路中断链
    • TIM8_UP_TIM13_IRQHandler()(PWM 更新事件)
    • 软件触发 ControlLoop_IRQHandler()(实际 FOC/控制计算)

在 ODrive v3 中,实时闭环控制不依赖 FreeRTOS 的普通任务调度,而是依赖**“PWM定时器更新中断 + 软件中断触发”**的硬实时链路。


2. board.h(板级配置与全局对象声明)

文件:Firmware/Board/v3/Inc/board.h

2.1 关键宏与参数

(1) 硬件参数:分流电阻
#if HW_VERSION_MINOR <= 3
#define SHUNT_RESISTANCE (675e-6f)
#else
#define SHUNT_RESISTANCE (500e-6f)
#endif
  • ODrive 通过 HW_VERSION_MINOR(板子小版本)对硬件参数做兼容。
  • 这个电阻会影响电流换算(ADC → 电流)。
(2) GPIO 计数与编号
#define AXIS_COUNT (2)
#define GPIO_COUNT  (17)
  • ODrive v3.x 双轴(M0/M1)。
  • GPIO_COUNT=17 包含:GPIO1~GPIO8、编码器相关引脚、CAN 引脚,以及 GPIO0 dummy。
(3) 默认 GPIO 模式

DEFAULT_GPIO_MODES 直接给出 ODrive 的默认 GPIO 功能映射(数字/串口/模拟输入/编码器/CAN 等)。

(4) 控制周期与 TIM 关联
#define TIM_TIME_BASE TIM14
#define CONTROL_TIMER_PERIOD_TICKS  (2 * TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1))
#define TIM1_INIT_COUNT (TIM_1_8_PERIOD_CLOCKS / 2 - 1 * 128)
  • TIM1/TIM8:中心对齐 PWM + 触发 ADC。
  • TIM13:用于和 TIM1/TIM8 同步(辅助定时/校准采样节奏)。
  • TIM14:被用作 HAL tick 的 timebase(见 stm32f4xx_hal_timebase_TIM.c)。
(5) 电压采样分压比
#if HW_VERSION_VOLTAGE >= 48
#define VBUS_S_DIVIDER_RATIO 19.0f
#elif HW_VERSION_VOLTAGE == 24
#define VBUS_S_DIVIDER_RATIO 11.0f
#endif

用于 VBUS 电压转换:ADC 采样电压 × 分压比 → 母线真实电压。

2.2 关键全局对象(C++ 区域)

board.h 将驱动层对象控制对象进行声明,供 board.cpp 定义。

  • axes:两轴 Axis 对象数组(Axis0/Axis1)
  • motors[]:Motor0/Motor1
  • encoders[]:Encoder0/Encoder1
  • gpios[]:GPIO 索引到具体端口/引脚
  • alternate_functions[][]:GPIO 复用功能表(UART/PWM/I2C/CAN/ENC)
  • usb_dev_handle:USB CDC 设备句柄
  • ext_spi_arbiter:SPI3 仲裁器(允许多个外设共享 SPI)
  • uart_a / uart_b / uart_c:串口对象指针
  • pwm0_input:PWM 输入捕获(典型用于 RC PWM 控制信号)

3. board.cpp(板级聚合层:对象实例化 + 控制回路中断)

文件:Firmware/Board/v3/board.cpp

这是 Board 文件夹中 最核心的“业务逻辑文件”,可以分为 4 类内容:

  1. 全局外设对象与驱动对象实例化
  2. system_init() / board_init() / start_timers()
  3. ADC 采样读取与复位逻辑
  4. 中断回调:PWM 更新中断 + 控制回路软中断 + SPI DMA 回调

3.1 全局对象:SPI 仲裁器与驱动芯片

(1) SPI3 仲裁器
Stm32SpiArbiter spi3_arbiter{&hspi3};
Stm32SpiArbiter& ext_spi_arbiter = spi3_arbiter;
  • ODrive 把外部 SPI 设备统一挂在 SPI3。
  • Stm32SpiArbiter 用于“多设备共享 SPI 总线”的冲突管理。
(2) 门极驱动芯片:两颗 DRV8301
Drv8301 m0_gate_driver{ ... M0_nCS ... nFAULT };
Drv8301 m1_gate_driver{ ... M1_nCS ... nFAULT };
  • 两轴各一颗 DRV8301。
  • EN 引脚是共享的,因此 EN 不在 drv8301 内部控制,而在 board_init() 中统一拉高拉低。

3.2 温度限流:板载热敏电阻

OnboardThermistorCurrentLimiter fet_thermistors[AXIS_COUNT] = {...};
OffboardThermistorCurrentLimiter motor_thermistors[AXIS_COUNT];
  • fet_thermistors:测 MOSFET 温度,用于热保护/限流。
  • ADC 通道会随硬件小版本变化(HW_VERSION_MINOR >= 3 时 M1 的通道不同)。

3.3 Motor / Encoder / Axis 对象绑定

(1) Motor
Motor motors[2] = {
  { &htim1, mask, 1/SHUNT, m0_gate_driver, ... },
  { &htim8, mask, 1/SHUNT, m1_gate_driver, ... }
};
  • 轴0 用 TIM1 作为 PWM 定时器
  • 轴1 用 TIM8 作为 PWM 定时器
(2) Encoder
Encoder encoders[2] = {
  { &htim3, ... &spi3_arbiter },
  { &htim4, ... &spi3_arbiter }
};
  • TIM3/TIM4 配置为 Encoder Mode(见 tim.c)。
  • SPI 仲裁器用于 SPI 绝对值编码器/磁编码器类设备。
(3) Axis 聚合

axes 把控制器、估计器、轨迹等与 motor/encoder 绑定。

  • 注意:Axis1 的 step/dir GPIO 引脚编号会在 HW_VERSION_MINOR >= 5 时发生变化(兼容不同 PCB)。

3.4 GPIO 映射表与复用功能表

(1) gpios[]:软件 GPIO 编号 → MCU 端口引脚

不同硬件小版本(v3.1~v3.6)GPIO 排针复用布局不同,所以这里用 #if HW_VERSION_MINOR 分段定义。

典型结构:

Stm32Gpio gpios[GPIO_COUNT] = {
  {nullptr,0}, // GPIO0 dummy
  {GPIOA, GPIO_PIN_0}, // GPIO1
  ...
  {GPIOB, GPIO_PIN_8}, // CAN_R
  {GPIOB, GPIO_PIN_9}, // CAN_D
};
(2) alternate_functions[]:GPIO 模式 → AF 复用号
std::array<GpioFunction, 3> alternate_functions[GPIO_COUNT] = {
  /* GPIO1 */ { UART4_AF8, TIM5_AF2 },
  /* ENC0_A */ { TIM3_AF2 },
  /* CAN_R */ { CAN1_AF9, I2C1_AF4 },
};

这个表让 ODrive 在运行时能够“根据配置把 GPIO 切换成 UART/PWM/I2C/CAN/ENC”等。


4. system_init() / board_init() / start_timers()

4.1 system_init()

职责:

  1. HAL_Init()
  2. SystemClock_Config()(CubeMX 生成在 main.c
  3. OTP 硬件版本校验

关键点:固件编译时的硬件版本必须与 OTP 中记录的一致,否则死循环停机。

实现逻辑:

  • 若 OTP 首字节为 0xFF(未烧录),则用 .testdata 段的 fake_otp(用于生产测试)。
  • check_board_version() 对比 HW_VERSION_MAJOR/MINOR/VOLTAGE

这一步保证:

  • 不会把 24V 固件烧到 48V 板子上
  • 不会在硬件 pinout 变化的版本上跑错配置

4.2 board_init()

职责:完成外设初始化、中断优先级配置、可选接口初始化、DRV8301 复位。

(1) CubeMX 外设初始化顺序
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_ADC2_Init();
MX_TIM1_Init();
MX_TIM8_Init();
MX_TIM3_Init();
MX_TIM4_Init();
MX_SPI3_Init();
MX_ADC3_Init();
MX_TIM2_Init();
MX_TIM5_Init();
MX_TIM13_Init();

这里的顺序体现了依赖关系:

  • GPIO/DMA 先初始化
  • ADC 与 TIM(触发源)必须准备好
  • SPI/UART/I2C/CAN 视配置启用
(2) EXTI 中断线统一开启

EXTI0/1/2/3/4/9_5/15_10 全部启用,真正每根线的用途与模式在 stm32_gpio.cpp 才会具体配置。

(3) 控制回路中断
HAL_NVIC_SetPriority(ControlLoop_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(ControlLoop_IRQn);

HAL_NVIC_SetPriority(TIM8_UP_TIM13_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM8_UP_TIM13_IRQn);
  • TIM8_UP_TIM13_IRQn 优先级最高(0):硬实时定时基准。
  • ControlLoop_IRQn 优先级稍低(5):控制计算。

注意:这里 ControlLoop_IRQn 被宏重命名为 OTG_HS_IRQn,即 复用一个 USB HS 的 IRQ 号作为“软件中断入口”

(4) UART A / UART B 条件初始化

只有当 odrv.config_.enable_uart_* 为 true 才会设置波特率并调用 MX_UART4_Init() / MX_USART2_UART_Init()

(5) I2C 地址由 GPIO 组合决定

当启用 I2C 时,固件会读取 GPIO3/4/5 三个输入脚的电平,拼出 I2C 地址:

i2c_stats_.addr = (0xD << 3);
addr |= gpio3?0x1:0;
addr |= gpio4?0x2:0;
addr |= gpio5?0x4:0;
MX_I2C1_Init(addr);

这相当于板上用 3bit 拨码/跳线决定从机地址。

(6) CAN 引脚模式保护

ODrive 要求 CAN 模式不被后续 GPIO 初始化覆盖,因此会检查配置是否把 CAN_R/CAN_D 设成 CAN。

(7) Debug freeze

为了防止调试暂停时 PWM 还在跑:

__HAL_DBGMCU_FREEZE_TIM1();
__HAL_DBGMCU_FREEZE_TIM8();
__HAL_DBGMCU_FREEZE_TIM13();
(8) DRV8301 全局复位

EN_GATE 既控制驱动使能也控制 SPI 接口上电,因此必须复位:

drv_enable_gpio.write(false);
delay_us(40);
drv_enable_gpio.write(true);
delay_us(20000);

4.3 start_timers():同步启动 TIM1 / TIM8 / TIM13

这部分是 ODrive 控制链路的关键。

(1) 临时关闭 ADC 外部触发

防止启动定时器时产生意外触发:

hadc1.CR2 &= ~JEXTEN;
hadc2.CR2 &= ~(EXTEN | JEXTEN);
hadc3.CR2 &= ~(EXTEN | JEXTEN);
(2) 三个定时器同步启动 + 相位关系
Stm32Timer::start_synchronously<3>(
  {&htim1, &htim8, &htim13},
  {TIM1_INIT_COUNT, 0, TIM1_INIT_COUNT/2}
);

解释:

  • TIM1 与 TIM8 的中心对齐三角波之间引入固定相位差(文中注释说 TIM1 领先 TIM8 90°)。
  • TIM13 reload 与 TIM1 的某个 update 对齐,用于辅助控制/校准节拍。
(3) 重新打开 ADC 外部触发并清标志

开启 ADC 外触发后,会清 JEOC/EOC/OVR,确保第一拍采样是干净的。

(4) 使能 TIM8 Update 中断
__HAL_TIM_CLEAR_IT(&htim8, TIM_IT_UPDATE);
__HAL_TIM_ENABLE_IT(&htim8, TIM_IT_UPDATE);

TIM8 update 是整个控制循环的“心跳”。


5. PWM—ADC—控制回路的中断链(最重要部分)

5.1 fetch_and_reset_adcs():读取 3 路 ADC 并清状态

函数签名:

static bool fetch_and_reset_adcs(
  std::optional<Iph_ABC_t>* current0,
  std::optional<Iph_ABC_t>* current1);

职责:

  1. 判断 ADC1/2/3 是否全部完成采样(EOC/JEOC)
  2. 读取 Vbus 与两轴电流 ADC 值
  3. 将 ADC 原始值转换成相电流(phB/phC,然后合成 phA=-B-C)
  4. 清 ADC SR 标志,准备下一次采样

关键点:

  • M0 电流来自 ADC2->JDR1ADC3->JDR1(注入通道)
  • M1 电流来自 ADC2->DRADC3->DR(规则通道)
  • Vbus 来自 ADC1->JDR1

这体现出:两个电机在同一时刻采样,但通过“注入/规则组”分离通道,减少 ADC 资源冲突。


5.2 TIM8_UP_TIM13_IRQHandler():PWM 更新中断(硬实时)

触发源:TIM8 Update Event(中心对齐 PWM 的更新点)。

核心逻辑:

  1. 清 TIM8 Update 中断标志
  2. 判断当前计数方向(上数/下数)
  3. 维护 timestamp_(时间戳)
  4. 在“上数半周期”执行采样回调并触发控制软中断
  5. 在“下数半周期”预设 PWM 为 50%,防止 deadline missed

关键代码行为解释:

(1) 计数方向用于区分“真实电流采样”和“零矢量采样”
bool counting_down = TIM8->CR1 & TIM_CR1_DIR;
  • 上数:通常对应 SVM 矢量区间内的“真实导通”采样
  • 下数:通常对应零矢量,电流接近 0,用于 offset/dc 校准
(2) timer_update_missed 检测
bool timer_update_missed = (counting_down_ == counting_down);

如果连续两次进入中断都处于同一方向,说明漏了一次 update 事件,属于严重时序错误:

  • Motor::ERROR_TIMER_UPDATE_MISSED
(3) 上数时:采样 + 触发控制软中断
odrv.sampling_cb();
NVIC->STIR = ControlLoop_IRQn;
  • sampling_cb():通常处理 DMA/测量等采样相关逻辑
  • STIR:软件触发中断,相当于“立即调度控制回路计算”
(4) 下数时:设置 PWM 为 50%(安全默认值)
TIM1->CCR1 = ... = TIM_1_8_PERIOD_CLOCKS/2;
TIM8->CCR1 = ... = TIM_1_8_PERIOD_CLOCKS/2;

含义:

  • 如果控制计算没赶上截止时间,下一个周期 PWM 就会被强制拉回“中性占空比”,避免异常输出。

5.3 ControlLoop_IRQHandler():控制回路软中断(核心控制计算)

触发方式:由 TIM8_UP_TIM13_IRQHandler 写 NVIC->STIR 触发。

控制回路步骤可以拆成 6 个阶段:

Stage A:确保 ADC 采样完成

调用 fetch_and_reset_adcs(),如果 ADC 未完成,则报时序错误:

  • Motor::ERROR_BAD_TIMING
Stage B:若 PWM 未使能,则电流置零

MOE 没开(电机未臂动),电流测量无意义:

if (!(TIM1->BDTR & TIM_BDTR_MOE_Msk)) current0 = {0,0,0};
Stage C:电流测量回调
motors[0].current_meas_cb(timestamp - TIM1_INIT_COUNT, current0);
motors[1].current_meas_cb(timestamp, current1);

注意 M0 的时间戳带 -TIM1_INIT_COUNT 偏移,说明 M0/M1 的采样相位不同(与 start_timers 同步策略一致)。

Stage D:主控制回调
odrv.control_loop_cb(timestamp);

这是整个 ODrive 的核心闭环计算入口(FOC、电流环、速度环、位置环等)。

Stage E:等待 ADC 进入下一次采样(DC 校准)
while (!(ADC2->SR & ADC_SR_EOC));

然后再次 fetch_and_reset_adcs() 获取下一组数据,用于:

  • dc_calib_cb():零电流 offset 校准
Stage F:PWM 更新回调 + deadline 检查
motors[x].pwm_update_cb(...);

最后检查 timestamp_ 是否严格前进一个周期,若不满足,说明控制计算超时:

  • Motor::ERROR_CONTROL_DEADLINE_MISSED

这套结构体现了 ODrive 的硬实时设计目标:

  • TIM8 update 是硬时钟
  • ControlLoop 是软触发中断
  • 两者之间用 timestamp 检测“漏拍/超时”

5.4 SPI DMA 完成回调(仲裁器解锁)

当 SPI3 DMA Tx/Rx 完成:

spi3_arbiter.on_complete();

这用于通知上层“本次 SPI 事务结束”,释放总线给下一设备。


5.5 TIM5_IRQHandler():PWM 输入捕获

pwm0_input.on_capture();

TIM5 被配置为输入捕获/测量 PWM 脉宽(例如 RC PWM)。


6. CubeMX 外设初始化模块(Inc/Src)

这部分代码多为“外设寄存器配置”,但 ODrive 有一些关键点值得强调。


6.1 gpio.c / gpio.h(基础 GPIO 配置)

文件:Src/gpio.c

这里只配置了最基础且“必须在早期生效”的引脚:

  • M0_nCS / M1_nCS:默认拉高(不选中 SPI 从设备)
  • EN_GATE:默认拉低(关闭驱动)
  • nFAULT:输入上拉

另外它会根据硬件版本包含旧版本配置:

#if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR == 1 || 2
  #include "prev_board_ver/gpio_V3_2.c"
#elif HW_VERSION_MINOR == 3 || 4
  #include "prev_board_ver/gpio_V3_4.c"
#endif

6.2 dma.c / dma.h(DMA 流配置)

主要是开启 DMA 控制器时钟、设置中断优先级。

在 ODrive 中 DMA 主要服务于:

  • SPI3 Tx/Rx
  • UART4/USART2 Rx/Tx
  • I2C1 Rx/Tx

特别注意 SPI3:

  • TX DMA 优先级更高于 RX(避免时序被打断)。

6.3 adc.c / adc.h(ADC1/2/3:电流与母线电压采样)

ODrive 使用 3 路 ADC:

  • ADC1:VBUS(注入通道为主)
  • ADC2:M0/M1 相电流采样(注入 + 规则)
  • ADC3:M0/M1 相电流采样(注入 + 规则)

adc.c 同样带硬件版本兼容:

#if HW_VERSION_MINOR == 1 || 2
  #include "prev_board_ver/adc_V3_2.c"
#elif HW_VERSION_MINOR == 3 || 4
  #include "prev_board_ver/adc_V3_4.c"
#endif

设计要点

  1. 中心对齐 PWM + ADC 外部触发:保证采样落在电流波形最稳定的位置。
  2. 注入组 + 规则组:在同一 ADC 上交替服务两台电机,降低资源占用。

6.4 tim.c / tim.h(定时器:PWM/编码器/PWM输入/同步)

ODrive v3 中定时器分工非常清晰:

(1) TIM1:M0 三相互补 PWM(中心对齐)

  • CounterMode = CENTERALIGNED3
  • PWM2 输出模式
  • DeadTime = TIM_1_8_DEADTIME_CLOCKS
  • TRGO_UPDATE:作为 ADC 触发源之一

(2) TIM8:M1 三相互补 PWM(中心对齐)

与 TIM1 类似,是第二个电机的 PWM 定时器。

(3) TIM13:同步辅助定时器

Period 由公式计算,确保与 TIM1/TIM8 的周期关系满足同步。

(4) TIM3 / TIM4:编码器接口

  • HAL_TIM_Encoder_Init()
  • EncoderMode = TI12
  • IC Filter = 4

(5) TIM5:PWM 输入捕获

TIM5 使能中断 TIM5_IRQn,由 TIM5_IRQHandler() 调用 pwm0_input.on_capture()


6.5 spi.c / spi.h(SPI3:16-bit + DMA)

SPI3 配置特点:

  • 16-bit 数据帧(适配编码器/驱动寄存器读写)
  • CLKPhase = 2EDGE
  • MOSI/SCK 默认下拉(空闲为 0)
  • MISO 上拉(用于断线检测/偶校验设备稳定电平)

DMA:

  • TX:DMA1_Stream7
  • RX:DMA1_Stream0

完成中断由 HAL_SPI_TxRxCpltCallback() 进入仲裁器 on_complete()


6.6 usart.c / usart.h(UART4/USART2:DMA + 环形接收)

  • UART4:通常映射为 UART_A
  • USART2:作为 UART_B(注释中提到未来板子版本可支持更多)

DMA RX 采用 DMA_CIRCULAR(典型串口持续接收方案)。

中断优先级设置为 10(相对控制回路更低)。


6.7 i2c.c / i2c.h(I2C1:地址可配置 + DMA)

MX_I2C1_Init(uint8_t addr):与常规 CubeMX 不同之处在于:

  • OwnAddress1 由外部传入,用于 I2C 从机模式

DMA:

  • RX:DMA1_Stream0 circular
  • TX:DMA1_Stream6 normal

6.8 can.c / can.h(CAN1:2Mbps 配置)

CAN 初始化采用 hcan1 直接静态初始化结构体(而不是 MX_CAN1_Init())。

典型参数:

  • Prescaler=8
  • SJW=4TQ
  • BS1=16TQ
  • BS2=4TQ

并开启 CAN1_TX/RX0/RX1/SCE 中断。


6.9 usb_device.c + usbd_*(USB CDC 虚拟串口)

这部分是 STM32 USB Device Library 的标准实现:

  • usb_device.c:初始化入口
  • usbd_cdc_if.c:CDC 类接口(收发回调)
  • usbd_conf.c:PCD 底层(中断/端点)
  • usbd_desc.c:设备描述符

FreeRTOS 默认任务 StartDefaultTask() 里只做一件事:

MX_USB_DEVICE_Init();

ODrive 其它任务/线程通常由 odrive_main 自己创建与调度。


7. stm32f4xx_it.c(异常与中断入口)

这里主要关注两个点:

7.1 HardFault 捕获寄存器 + 立刻关 PWM

HardFault_Handler() 用 naked 汇编拿到堆栈指针,跳转 get_regs()

get_regs() 做了两件非常关键的安全动作:

  1. 关闭 TIM1/TIM8 的 MOE(停止 PWM)
  2. 把 r0/r1/r2/r3/pc/lr/psr/cfsr 等寄存器读出来(便于调试)

并通过 while(stay_looping); 卡死,方便你接 JTAG 查看现场。

7.2 统计 IRQ 调用次数

多处看到:

COUNT_IRQ(xxx_IRQn);

这通常用于在线监测中断频率/丢拍(ODrive 的实时性诊断手段)。


8. prev_board_ver(旧板兼容)

目录:

  • Inc/prev_board_ver/
  • Src/prev_board_ver/

包含:

  • adc_V3_2.c / adc_V3_4.c
  • gpio_V3_2.c / gpio_V3_4.c

CubeMX 生成的 adc.c/gpio.c 在文件顶部用 #include 直接把旧版本实现“编译进来”。

优点:

  • 同一份固件代码可以通过编译宏覆盖不同的 pinout/ADC 方案。

缺点:

  • 代码组织不够模块化,但对固件团队维护“版本矩阵”很高效。

9. 你在阅读/移植时最需要关注的 10 个关键点

  1. TIM1/TIM8 必须是中心对齐 PWM,并且死区、重复计数器等参数必须匹配 mxconstants.h
  2. ADC 外部触发与采样点 决定电流测量质量,这是 FOC 成败关键。
  3. TIM8_UP_TIM13_IRQHandler 是硬实时心跳,优先级应最高。
  4. ControlLoop_IRQHandler 的 deadline 检测 是 ODrive 安全策略的一部分。
  5. SPI3 DMA 的优先级与 on_complete() 决定多设备 SPI 的稳定性。
  6. EN_GATE 同时影响驱动功率级与 SPI 接口,复位时序必须满足 datasheet。
  7. I2C 地址由 GPIO 组合决定,如果你外接设备,记得避免地址冲突。
  8. 硬件版本 OTP 校验 能防止“固件与板子不匹配”导致烧毁风险。
  9. HardFault 一进来就关 PWM 是安全底线,移植时务必保留。
  10. gpios[]alternate_functions[] 决定了 ODrive 的 GPIO 模式动态切换能力。

附录:文件逐个“模块-函数”速查表

A. 板级聚合层

board.h

  • system_init():系统初始化、OTP 版本校验
  • board_init():外设初始化与中断配置
  • start_timers():同步启动 TIM1/TIM8/TIM13

board.cpp

  • check_board_version():OTP 版本比对
  • system_init():HAL_Init + 时钟 + OTP 校验
  • board_init():外设 init + EXTI/控制中断 + UART/I2C + DRV reset
  • start_timers():同步启动三 TIM,开 ADC 触发
  • fetch_and_reset_adcs():读取电流/Vbus 并清 ADC 状态
  • TIM8_UP_TIM13_IRQHandler():PWM update 心跳 + 触发控制软中断
  • ControlLoop_IRQHandler():闭环计算 + DC 校准 + PWM 更新
  • HAL_SPI_TxRxCpltCallback():SPI3 arbiter 完成通知
  • TIM5_IRQHandler():PWM 输入捕获

B. CubeMX 外设初始化层

main.c

  • SystemClock_Config():系统时钟树配置
  • Error_Handler():错误处理

gpio.c

  • MX_GPIO_Init():最基础 GPIO 初始化(CS/EN/nFAULT 等)

dma.c

  • MX_DMA_Init():DMA 时钟与中断

adc.c

  • MX_ADC1_Init() / MX_ADC2_Init() / MX_ADC3_Init():ADC 配置
  • HAL_ADC_MspInit():GPIO/时钟/DMA(如有)

tim.c

  • MX_TIM1_Init():M0 PWM
  • MX_TIM8_Init():M1 PWM
  • MX_TIM13_Init():同步辅助
  • MX_TIM3_Init() / MX_TIM4_Init():编码器
  • MX_TIM5_Init():PWM 输入捕获

spi.c

  • MX_SPI3_Init():SPI3 + DMA
  • HAL_SPI_MspInit():引脚/DMA 绑定

usart.c

  • MX_UART4_Init():UART_A
  • MX_USART2_UART_Init():UART_B
  • HAL_UART_MspInit():DMA、IRQ

i2c.c

  • MX_I2C1_Init(uint8_t addr):I2C1,从机地址由外部传入

can.c

  • HAL_CAN_MspInit():CAN1 引脚/IRQ(Init 结构体写死在 hcan1)

freertos.c

  • MX_FREERTOS_Init():创建 defaultTask
  • StartDefaultTask():只初始化 USB

stm32f4xx_it.c

  • HardFault_Handler() / get_regs():关 PWM + 现场寄存器
  • 多个 DMA/UART/CAN/USB IRQ 入口

stm32f4xx_hal_timebase_TIM.c

  • HAL tick 替代 SysTick 的实现(基于 TIM14)

usb_device.c + usbd_*.c

  • USB CDC 虚拟串口设备栈初始化与回调
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐