ODrive0.5.5源码分析(2) 时钟和定时器

作者:沉尸([email protected])

1)时钟分析

我们直接打开文件“Firmware\Board\v3\Odrive.ioc”:

图1-1

从上面可以看出:APB1时钟:42MHz;   APB1相连的定时器时钟:84MHz

APB2时钟:84MHz; APB2相连的定时器时钟:168MHz

下图来自 stm32f407数据手册中“2.2 Device overview Figure 5. STM32F40xxx block diagram

图1-4

总共用到的定时器有TIM1~TIM5,TIM8,TIM13,TIM14

于是可知,本项目中用到的定时器时钟

TIM1TIM8 168MHz

其它                  84MHz

2)TIM14

这个用来作为Hal库的时基定时器。

图2-1

看代码:

图2-2

3)TIM1和TIM8

图3-1

main.h中有定义:

#define TIM_1_8_PERIOD_CLOCKS 3500

图3-2

于是可以知道 TIM1和TIM8的频率为:

168MHz/(3500*2) = 24KHz                        // 因为是中间对齐,所以式子中要*2

直接总结如下:

定时器

输入频率

周期

功能

TIM1

168MHz

168M/(3500*2)=24KHz

#1电机(M0)的UVW驱动

TIM2

84MHz

84M/(4096*2)=10.25KHz

CH3(PB.10)和CH4(PB.11)作为pwm输出,duty相反

TIM3

84MHz

用作计数

#1电机之旋编检测,计满到0xfffff

TIM4

84MHz

用作计数

#2电机之旋编检测,计满到0xfffff

TIM5

84MHz

用作计数

定时器的CH3(PA.2)和CH4(PA.3)作为捕获输入口

TIM8

168MHz

24KHz

#2电机(M1)的UVW驱动

TIM13

84MHz

8KHz

启动时和TIM1及TIM8同步;任务耗时测量

TIM14

84MHz

1KHz

作为HAL库的时基

表3-1

FOC控制,很关键一点是采样相电流,在什么时刻采集?这个是软件设计很关键的一点,下面我们来剖析。

先分析:TIM8_UP_TIM13_IRQHandler()

分析之前看TIM8的初始化代码:

图3-3

TIM8控制的pwm波形为中间对齐,这里设置RCR=2,也就是每(2+1)次更新会中断一次。

 

图3-4

这里可以如此理解:RCR=0, 每次上溢下溢均会中断;RCR=1,间隔1个;RCR=2,间隔2个。

搞清楚中断的间隔问题,现在再来看TIM8_UP_TIM13_IRQHandler()的源代码

 

图3-5

就很容易理解了,因为中断肯定是一次上溢一次下溢,上面“图3-5”红方框中代码,就是检测是否漏掉了一次中断的判断。

修改时间戳“timestamp_”为下一次中断时间点:

图3-6

“TIM_1_8_PERIOD_CLOCKS”是半周期的clock数,间隔了3个“半周期”,从“图3-4”中就可以看到这个间隔数了:刚好3个“半周期”。

这里贴出项目中原始附带的图片文件“ODrive\Firmware\timing_diagram_v3.png”:

 

图3-6

后面我们结合代码围绕“图3-6”进行分析

 

 图3-7

Ln499 ~ Ln502:

在低溢出的更新中断中调用,也就是对应着“图3-6”中的M1,因为程序走到这个地方的时候,计数方向已经转向了。

图3-8

本代码块针对的中断就是上图中箭头所指处,进入代码块:

对于Ln499中“odrv.task_timers_armed_”这个变量是个单次使能量,正常情况下(包括初始化时),

odrv.task_timers_armed_ = false,于是,在Ln499执行后,“TaskTimer::enabled”也会被设置成false。

如果在Ln499执行之前,已经通过外部控制设置了“odrv.task_timers_armed_ = true”,于是Ln499执行完时,“TaskTimer::enabled”也会为“true”。

然后执行到文件“Board\v3\board.cpp”的函数“void ControlLoop_IRQHandler()”末尾:

图3-9

我们可以用真值表的方式来推算一下,运行到“图3-9”后,odrv.task_timers_armed_ 和 TaskTimer::enabled 均会变为false。

所以“odrv.task_timers_armed_”这是一个单次使能的开关量!目的是单次测量下面这几个量:

(来自代码:“MotorControl\task_timer.hpp”)

 图3-10

现在,继续看图3-7中Ln502的代码:

         odrv.sampling_cb();

我们来看调用层次:

 图3-11

现在看“sample_now()”函数的实现

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

void Encoder::sample_now() {

    switch (mode_) {

        case MODE_INCREMENTAL: {

            tim_cnt_sample_ = (int16_t)timer_->Instance->CNT;

        } break;

        case MODE_HALL: {

            // do nothing: samples already captured in general GPIO capture

        } break;

        case MODE_SINCOS: {

            sincos_sample_s_ = get_adc_relative_voltage(get_gpio(config_.sincos_gpio_pin_sin)) - 0.5f;

            sincos_sample_c_ = get_adc_relative_voltage(get_gpio(config_.sincos_gpio_pin_cos)) - 0.5f;

        } break;

        case MODE_SPI_ABS_AMS:

        case MODE_SPI_ABS_CUI:

        case MODE_SPI_ABS_AEAT:

        case MODE_SPI_ABS_RLS:

        case MODE_SPI_ABS_MA732:

        {

            abs_spi_start_transaction();

            // Do nothing

        } break;

        default: {

           set_error(ERROR_UNSUPPORTED_ENCODER_MODE);

        } break;

    }

    // Sample all GPIO digital input data registers, used for HALL sensors for example.

    for (size_t i = 0; i < sizeof(ports_to_sample) / sizeof(ports_to_sample[0]); ++i) {

        port_samples_[i] = ports_to_sample[i]->IDR;

    }

}

图3-12

Ln2 ~ Ln29: 根据编码器的类型读出编码器的值,比如“增量式”的,就读相应定时器的count值,是spi类型,就通过spi总线读出对应的位置值

Ln32 ~ Ln34: 读出需要采样IO口的电平值,这个采样不是ADC采样,是高低电平值,并且是整个IO口作为整体存放起来,比如GPIOA、GPIOB,这里暂时不分具体的个体。

小结一下sampling_cb():

1) 读出编码器的值

2) 获取某些IO口的电平值,是整个IO口(比如:GPIOA、GPIOB等)全部读出先!

还是回到函数“TIM8_UP_TIM13_IRQHandler()”继续将它分析完:

图3-13

Ln503的功能,看起来就是调用了函数“ControlLoop_IRQn”似的,是不是这样? 

图3-14

果然,这是一个软中断!填入了什么中断号,就会跳到相应的中断向量中。

 图3-15

图3-16 

图3-17

将上面连续的图看完,基本就不用多解释了。

特别注意“图3-16”中的定义:

当设置“STIR = ControlLoop_IRQn”, 也就是“OTG_HS_IRQn”值(这个值为77),也就是触发77号中断,

77号中断对应:“OTG_HS_IRQHandler”, “OTG_HS_IRQHandler”又被定义成了“ControlLoop_IRQHandler”。

现在来看看软中断调用的函数:ControlLoop_IRQHandler()

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

560

561

562

563

564

565

566

567

568

569

570

571

572

573

574

575

void ControlLoop_IRQHandler(void) {

    COUNT_IRQ(ControlLoop_IRQn);

    uint32_t timestamp = timestamp_;

    // Ensure that all the ADCs are done

    std::optional<Iph_ABC_t> current0;

    std::optional<Iph_ABC_t> current1;

    if (!fetch_and_reset_adcs(&current0, &current1)) {

        motors[0].disarm_with_error(Motor::ERROR_BAD_TIMING);

        motors[1].disarm_with_error(Motor::ERROR_BAD_TIMING);

    }

    // If the motor FETs are not switching then we can't measure the current

    // because for this we need the low side FET to conduct.

    // So for now we guess the current to be 0 (this is not correct shortly after

    // disarming and when the motor spins fast in idle). Passing an invalid

    // current reading would create problems with starting FOC.

    if (!(TIM1->BDTR & TIM_BDTR_MOE_Msk)) {

        current0 = {0.0f, 0.0f};

    }

    if (!(TIM8->BDTR & TIM_BDTR_MOE_Msk)) {

        current1 = {0.0f, 0.0f};

    }

    motors[0].current_meas_cb(timestamp - TIM1_INIT_COUNT, current0);

    motors[1].current_meas_cb(timestamp, current1);

    odrv.control_loop_cb(timestamp);

    // By this time the ADCs for both M0 and M1 should have fired again. But

    // let's wait for them just to be sure.

    MEASURE_TIME(odrv.task_times_.dc_calib_wait) {

        while (!(ADC2->SR & ADC_SR_EOC));

    }

    if (!fetch_and_reset_adcs(&current0, &current1)) {

        motors[0].disarm_with_error(Motor::ERROR_BAD_TIMING);

        motors[1].disarm_with_error(Motor::ERROR_BAD_TIMING);

    }

    motors[0].dc_calib_cb(timestamp + TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1) - TIM1_INIT_COUNT, current0);

    motors[1].dc_calib_cb(timestamp + TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1), current1);

    motors[0].pwm_update_cb(timestamp + 3 * TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1) - TIM1_INIT_COUNT);

    motors[1].pwm_update_cb(timestamp + 3 * TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1));

    // If we did everything right, the TIM8 update handler should have been

    // called exactly once between the start of this function and now.

    if (timestamp_ != timestamp + TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1)) {

        motors[0].disarm_with_error(Motor::ERROR_CONTROL_DEADLINE_MISSED);

        motors[1].disarm_with_error(Motor::ERROR_CONTROL_DEADLINE_MISSED);

    }

    odrv.task_timers_armed_ = odrv.task_timers_armed_ && !TaskTimer::enabled;

    TaskTimer::enabled = false;

}

图3-18

Ln526 ~ Ln529: 调用函数“fetch_and_reset_adcs()”

1)获取ADC采集出来的M0和M1的B、C相电压,A相电压通过0-(B+C)获得;

2)获取ADC采集的总线电压

3)在调用本函数的时候,如果ADC还没有自动采集完成,返回false,于是在Ln10和Ln11中设置错误“ERROR_BAD_TIMING

在调用本函数的时候,M0和M1的相电压已经采集好了,关于ADC的触发和采集可以看另一篇文章,它们分别是由“TIM1_TRGO”和“TIM8_TRGO”来触发,现在看时序图(不用再问下面这个图从哪来的吧?):

图3-19

我们看上图中两个红色箭头处,M0触发比M1的触发要早一点,而我们正在分析的代码执行的是在M1的中断处,所以此时M0的相电压值已经采集结束了(TIM1的update事件会自动触发采样启动)。如果继续跟踪代码进入函数“fetch_and_reset_adcs()”里面:

图3-20

可以看到:采集M0电机的相电压是注入式,而采样M1电机的相电压是规则式。

详细可以看我的另一篇文章《Odrive0.5.5程序分析(1) ADC的处理》

Ln536 ~ Ln541:源代码的注释中已经比较详细了,判断的位是 “TIMx_BDTR.MOE”:

图3-21

Ln543 ~ Ln544:

电机电流测量后的回调,检测一下电流是否异常(比如超过限制电流值)等等安全检查,然后调用“on_mesurement()”,通过获取的相电压(IAIBIc )计算出

Ln546:

control_loop_cb() 做的工作比较多,执行的时间也很长,执行一系列数据的update,比如“电角度”、“电速度”、“温度”等

Ln555 ~ Ln557:

等待ADC转换完成,再次获取一次电流值

Ln559 ~ Ln560:进行中间电位的校正。

dc_calib_cb()采取的措施是将连续N个时间点(很密)的电流值累加,然后取平均值。本次的调用就是一次时间点的值累加进去了。电流有个特性,它不会陡变!所以这样累加再平均的写法也合理,结果也会很接近中间电位值。

Ln562 ~ Ln563:

pwm_update_cb()会调用FOC核心算法,最后设定好当前duty

Ln568 ~ Ln571:

1timestamp_”在“board.cpp”中Ln474被定义成为一个全局变量,不属于任何类

2在本函数开始的Ln520,局部变量“timestamp”初始化值为“timestamp_”,现在要求:

    timestamp_ == timestamp + TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1)

否则出错,那么:“timestamp_”什么时候增加了“TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1)”?

显然这个问题是接下来我们需要重点搞清楚的地方。

3在Ln565~Ln566的源代码的注释中如此描述:在本中断函数调用的入口到Ln568这里做判断,TIM8的update中断处理函数“TIM8_UP_TIM13_IRQHandler”肯定又已经被调用了一次,被调用就会执行:

    4注意:这里就有点绕了,“TIM8_UP_TIM13_IRQHandler”会触发软中断“ControlLoop_IRQHandler”,软中断执行过程中又会等待“TIM8_UP_TIM13_IRQHandler”再次执行!

    看 board.cpp 中board_init()

图3-22

    “ControlLoop_IRQn”的中断优先级别相对于“TIM8_UP_TIM13_IRQHandler”更低!也就是说即使是在“TIM8_UP_TIM13_IRQHandler”中触发的软中断,这个软中断的执行也是要等到“TIM8_UP_TIM13_IRQHandler”退出后才会被执行。

    我们看下图中的注释:

图3-23

注释中讲的很清楚了:在“ControlLoop_IRQHandler”开始执行后,中途会被“TIM8_UP_TIM13_IRQHandler”打断一次。

为了看清楚这个问题,我们用GPIO7和GPIO8作为辅助测试脚,先来看看是否真是如此?

在“main.cpp”文件中函数“extern "C" int main(void) {”里面添加:

 

图3-24

这样把GPIO7和GPIO8配置成了输出脚,然后

在函数“TIM8_UP_TIM13_IRQHandler”的运行期间置高GPIO7;

在函数“ControlLoop_IRQHandler”的运行期间置高GPIO8

    如下图修改:

图3-25

看示波器波形:

 

                               图3-26                                                          图3-27

图3-27是图3-26的局部版。

从图3-27可以看出:

    1)A脉冲和B脉冲间隔62.5us,回到“图3-4”,理论上验证一下:

两次中断的时间间隔 = 3500*3*(1/168M)(单位:s) = 62.5*10-6(s) = 62.5(us)

2)A脉冲宽度明显大于B脉冲宽度,是因为B脉冲是在执行下面图3-28中的“B程序块”:

3)在C脉冲高电平阶段(执行: ControlLoop_IRQHandler),确实等到了B脉冲高电平(执行: TIM8_UP_TIM13_IRQHandler)

图3-28

继续分析:

再增加一个辅助信号脚GPIO6替代刚才的GPIO7的测试,把GPIO7放在函数“ControlLoop_IRQHandler”内的不同位置进行探测,会发现调用函数“odrv.control_loop_cb”的耗时很长:

 

 图3-28

图3-29

现在把辅助测量放在while语句的前后:

 

 图3-30

 

 图3-31

根据上面示波器(图3-29和图3-31)的结果,还有源代码中的注释,我们可以得到结论:

    1)在函数“control_loop_cb()”的调用中等到了TIM8的update事件,这个update事件于是触发了ADC的转换;

    2)图3-18中Ln546,也就是在“ControlLoop_IRQHandler”中执行函数“control_loop_cb()”结束后,ADC转换暂时还没有完成,直到程序中执行了:

      while (!(ADC2->SR & ADC_SR_EOC));

后,才等待到ADC完成转换!我测试的波形是在电机没有运行时测的,也许运行起来后,“control_loop_cb()”在不同的分支中执行,也许时间会更长,也就是说也可能在“control_loop_cb()” 退出时已经ADC转换完成了,所以程序中进行while循环等待ADC结束,大有必要!

3)从图3-29可以看出,示波器最上面信号(黄色)下降沿和中间信号(粉色)上升沿之间有一段时间差。TIM8的 update事件发生后,触发update中断处理,这个处理的响应有点滞后,感觉有10us+的时间以上。

目前只剩下一个任务了:搞清楚“control_loop_cb()”在搞些啥事情,为啥这么耗时间。

因为本文章已经太长了,所以单独再开一篇文章:

《ODrive0.5.5源码分析(3) control_loop_cb》

3)TIM13

“Board\v3\board.cpp”中函数“start_timers()”片段:

 

图3-1

“Drivers\STM32\stm32_timer.hpp”

   

图3-2

函数“start_synchronously_impl()”的实现,比较简单也容易理解,这里直接总结:

1)停止TIM1、TIM8以及TIM13的计数且设置好它们的初始计数值。

2)然后以最快的“原子”指令方式(关闭中断+最少指令)、近乎同时地启动这三个定时器。如此就达到了近似同步的效果!

这3个定时器在初始值上有差别:

TIM1初始值为“TIM1_INIT_COUNT”,TIM8初始值为“0”。

有定义:

#define TIM1_INIT_COUNT     (TIM_1_8_PERIOD_CLOCKS / 2 - 1 * 128)

TIM_1_8_PERIOD_CLOCKS 对应着TIM1(TIM8)的半个周期(TIM1和TIM8均为中间对齐方式)

所以有:TIM1领先TIM8接近“1/4 周期” (还差128/2=64个clock),这也解释了下图中Ln391中的90度问题。

 图3-3

来自“board.cpp”

现在继续关注“图3-3”中注释的第2点:

    TIM13重装时间和TIM1的下溢更新事件高度重合

如何理解?我们来看TIM13的周期:

 

 图3-4

TIM13的时钟来源是:APB1之定时器时钟(84MHz)

而TIM1和TIM8的时钟来源于:APB2之定时器时钟(168MHz)

图3-5

按照源代码,现在来计算TIM13的周期:

= 2*3500*(2+1) * (84000000/168000000) – 1

= 2*3500*3*0.5 - 1

    = 3500*3 - 1

而TIM1的“更新中断”周期正好也是“3500*3”(中间对齐,RCR=2),高低溢出分别间隔到来,那么低溢出周期就是:“3500*6”了;TIM1的输入频率是TIM13的2倍,TIM13的计数方式是“向上”(不是中间对齐)。所以说TIM13的reload频率和TIM1的低溢出“更新中断频率”是一致的,但是TIM13的初始计数值落后TIM1,落后了“TIM1_INIT_COUNT/2”也就是(3500/2-128)/2=811

所以注释中的第2点:

 

图3-6

还是有一点点不严谨,并没有完全重合!!!

TIM13另一个功能就是作为耗时计算的时间计时:

图3-7

猜你喜欢

转载自blog.csdn.net/danger/article/details/128533481