作者:沉尸([email protected])
前言:
本章重点研究时间戳(timestamp)在软件中的使用,可以先复习一下:
《ODrive0.5.5源码分析(2) 时钟和定时器》
软件中使用时间戳的地方比较多,这里将仅仅聚焦于FOC相关的代码部分。
1)
软件中的时间戳(timestamp)均以TIM8中的定时器计数值作为标准。
TIM8的定时器是从0开始计数的,而TIM1的计数初始值领先TIM8 “TIM1_INIT_COUNT”个计数值。
有些ADC的触发是被TIM1触发的,所以要注意软件中经常会对时间戳参数采取减去“TIM1_INIT_COUNT”的处理,比如:
上图取自“ControlLoop_IRQHandler()”
参数“timestamp”对应着TIM8发生update中断这个时刻的“时间戳”,函数中要传入“电流测量这个时刻”的时间戳,对于M0来说,它的电流检测ADC是由TIM1触发的,时间上要稍微早一点,于是针对M0的计算,就要减去“TIM1_INIT_COUNT”。
软件中还有好几处类似的处理,以后就不再赘述。
2)
先从“timestamp_”开始,这是一个全局变量,记录的是“TIM8_UP_TIM13_IRQHandler”中断时的时间戳
上图中“A”箭头所指,正是“TIM8_UP_TIM13_IRQHandler”中断的时间间隔,于是“timestamp_”可以描述成最近一次“TIM8_UP_TIM13_IRQHandler”中断对应的时间戳,直到又一次中断发生,“timestamp_”才会被更新。
现在开始看函数“ControlLoop_IRQHandler”
523 524 525 |
void ControlLoop_IRQHandler(void) { COUNT_IRQ(ControlLoop_IRQn); uint32_t timestamp = timestamp_; |
刚才已经分析过了“timestamp_”, 而“TIM8_UP_TIM13_IRQHandler”下溢出中断处理结束后马上就会调用“ControlLoop_IRQHandler”。
这里将“timestamp_”赋值给了临时变量“timestamp”,然后后面好几个地方都用这个临时变量的时间戳作为实参进行传递。
549 550 |
motors[0].current_meas_cb(timestamp - TIM1_INIT_COUNT, current0); motors[1].current_meas_cb(timestamp, current1); |
这个是电流测量回调,电流在什么时候被测量?正是“TIM8_UP_TIM13_IRQHandler”的update事件触发电流ADC,而M0是TIM1触发的,TIM1比TIM8要提早“TIM1_INIT_COUNT”个计数值。
从“current_meas_cb”调用开始追溯,最后会调用“FieldOrientedController::on_measurement”
这里要注意变量“i_timestamp_”,在后面函数中会被用到,函数“current_meas_cb()”被执行完后,这个“i_timestamp_”就对应着相电流被采集时的时间戳了。
继续看函数“ControlLoop_IRQHandler”:
553 566 567 570 571 |
odrv.control_loop_cb(timestamp); ... 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)); |
Ln553:
继续使用“TIM8_UP_TIM13_IRQHandler”中断时的时间戳,分析函数“control_loop_cb”,传入的“timestamp”作为了一系列update操作的“时间戳”,比如“电角度”phase等等数据的更新。
Ln566 ~ Ln567:
程序走到此处时,已经又一次遇到了“TIM8_UP_TIM13_IRQHandler”之update中断,具体分析见《ODrive0.5.5源码分析(2) 时钟和定时器》中的分析。
Ln570 ~ Ln571:
乍一看有点令人迷惑: “3* TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1)”,为啥要用3来乘?其实看前面一些文章,我们就可以知道:FOC的计算周期正是:“3* TIM_1_8_PERIOD_CLOCKS * (TIM_1_8_RCR + 1)”
这里从“pwm_update_cb”入手,先来看调用层次:
于是引出了“FieldOrientedController::get_alpha_beta_output()”,这是第2个我们需要重点分析的函数
66 67 68 69 70 71 72 73 74 75 76 90 91 92 97 98 99 100 161 162 163 164 165 |
ODriveIntf::MotorIntf::Error FieldOrientedController::get_alpha_beta_output( uint32_t output_timestamp, std::optional std::optional<float>* ibus) { if (!vbus_voltage_measured_.has_value() || !Ialpha_beta_measured_.has_value()) { // FOC didn't receive a current measurement yet. return Motor::ERROR_CONTROLLER_INITIALIZING; } else if (abs((int32_t)(i_timestamp_ - ctrl_timestamp_)) > MAX_CONTROL_LOOP_UPDATE_TO_CURRENT_UPDATE_DELTA) { // Data from control loop and current measurement are too far apart. return Motor::ERROR_BAD_TIMING; } ... ... auto [Vd, Vq] = *Vdq_setpoint_; float phase = *phase_; float phase_vel = *phase_vel_; ... ... // Park transform if (Ialpha_beta_measured_.has_value()) { auto [Ialpha, Ibeta] = *Ialpha_beta_measured_; float I_phase = phase + phase_vel * ((float)(int32_t)(i_timestamp_ - ctrl_timestamp_) / (float)TIM_1_8_CLOCK_HZ); ... } else { ... } // Inverse park transform float pwm_phase = phase + phase_vel * ((float)(int32_t)(output_timestamp - ctrl_timestamp_) / (float)TIM_1_8_CLOCK_HZ); float c_p = our_arm_cos_f32(pwm_phase); float s_p = our_arm_sin_f32(pwm_phase); ... ... } |
Ln73:
我们来看“ctrl_timestamp_”,只有一个地方进行了更新:
“control_loop_cb()”中进行了update,于是我们可以知道:正常情况下,在执行到Ln73的时候,i_timestamp_ 和 ctrl_timestamp_是一样大小。实际上Odrive的设计,它认为“ctrl_timestamp_”是一个即时更新的量,它对应着“phase”计算出来的时间点,只不过目前代码中简化了,让它等于了“i_timestamp_”,它应该要比“i_timestamp_”时间戳晚一点。因为:先是采集到了电流(对应“i_timestamp_”),然后各种update获取phase等值(对应“ctrl_timestamp_”),而调用到Ln73这里的时候,是因为“pwm_update_cb”的调用引起的,时间又过去了一小会了。这个分析很重要,否则后面的Park转换时的phase的计算就会迷惑。
Ln100:
“phase_”的update获取,在电流的获取时间后面,它对应着时间戳“ctrl_timestamp_”, 而电流的获取对应着时间戳“i_timestamp_”,那么我们计算的时候显然要获的在电流采集时这个“瞬间”的“电角度”,这个“电角度”显然比起“phase_”要前一些,再来看计算:
通过“i_timestamp_ - ctrl_timestamp_”正好向前推导出电流测量瞬间的“电角度”,虽然我们会发现实际运行中这两个值刚好相等,但是我们的软件具有在不相等时的处理能力!
Ln161:
当前获得的“phase_”对应着的时间戳是“ctrl_timestamp_”,那么计算“output_timestamp”这个未来时间戳处的“电角度”,计算公式就没啥疑问了。
现在开始回到前面说的问题:为啥是3倍的时间间隔问题!
我们先来画出大概的时间戳时序图:
在上图中,我们进行计算的大概位置是在“C”处,再下一次进行计算的大概位置就在“B”时间戳附近了,从“C”->“B”这一段时间都会用同样方向的“矢量力矩”进行电机驱动,这个“矢量”的方向,我们就选择了未来“B”处的矢量。现在再来看这个“3倍”,问题就很清晰明了了。当然,如果我们就用“C”处的矢量,行不行呢?肯定也是可以的,不过选择“未来的”量,比起一个即将过时的量,我觉得当然是目前软件中选择的好!!!