mirror of
https://github.com/odriverobotics/ODrive.git
synced 2026-09-25 19:13:39 +08:00
Merge branch 'devel' into hall-calib
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps and configuration necessary to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Windows 10]
|
||||
- odrivetool Version (`odrivetool --version`)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
run: |
|
||||
Invoke-WebRequest -Uri "http://gittup.org/tup/win32/tup-latest.zip" -OutFile ".\tup-latest.zip"
|
||||
Expand-Archive ".\tup-latest.zip" -DestinationPath ".\tup-latest" -Force
|
||||
echo "::add-path::$(Resolve-Path .)\tup-latest"
|
||||
echo "$(Resolve-Path .)\tup-latest" >> $GITHUB_PATH
|
||||
|
||||
choco install gcc-arm-embedded # downloads https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/gcc-arm-none-eabi-9-2019-q4-major-win32.zip
|
||||
|
||||
|
||||
@@ -16,8 +16,12 @@ jobs:
|
||||
steps:
|
||||
- name: Install odrivetool
|
||||
run: |
|
||||
pip install monotonic # TODO: this is dishonest. Must be removed as soon as v0.5.0 is published!
|
||||
pip install odrive
|
||||
# TODO: this is a workaround for https://github.com/pypa/setuptools/issues/2353 and we
|
||||
# can remove it as soon as python3-setuptools on GitHub's ubuntu-latest
|
||||
# moves to version 50.1+.
|
||||
pip3 list | grep setuptools # show version
|
||||
export SETUPTOOLS_USE_DISTUTILS=stdlib
|
||||
pip3 install odrive
|
||||
|
||||
# This one currently fails because Github Actions runs pip as non-root
|
||||
#- name: Check if udev rules were set up properly
|
||||
@@ -27,7 +31,7 @@ jobs:
|
||||
# This step is mentioned in the user guide
|
||||
- name: Add ~/.local/bin to path
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: echo "::add-path::~/.local/bin"
|
||||
run: echo "~/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Launch odrivetool
|
||||
# This returns a non-zero exit code if the odrivetool throws an exception
|
||||
|
||||
@@ -58,6 +58,7 @@ Please add a note of your changes below this heading if you make a Pull Request.
|
||||
* Several properties were changed to readonly.
|
||||
* `<axis>.encoder.config.offset` was renamed to ``<axis>.encoder.config.phase_offset`
|
||||
* `<axis>.encoder.config.offset_float` was renamed to ``<axis>.encoder.config.phase_offset_float`
|
||||
* `<odrv>.config.brake_resistance == 0.0` is no longer a valid way to disable the brake resistor. Use `<odrv>.config.enable_brake_resistor` instead.
|
||||
|
||||
# Releases
|
||||
## [0.5.1] - 2020-09-27
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
#define DEFAULT_BRAKE_RESISTANCE (0.47f) // [ohm]
|
||||
#endif
|
||||
|
||||
#define DEFAULT_ERROR_PIN 0
|
||||
#define DEFAULT_MIN_DC_VOLTAGE 8.0f
|
||||
|
||||
#define DEFAULT_GPIO_MODES \
|
||||
ODriveIntf::GPIO_MODE_DIGITAL, \
|
||||
ODriveIntf::GPIO_MODE_UART_A, \
|
||||
|
||||
@@ -82,14 +82,14 @@ void MX_DMA_Init(void)
|
||||
HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 10, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn);
|
||||
/* DMA1_Stream5_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 10, 0); // SPI TX - must have higher priority than SPI RX
|
||||
// and higher priority than the control loop handler
|
||||
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 10, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
|
||||
/* DMA1_Stream6_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA1_Stream6_IRQn, 10, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA1_Stream6_IRQn);
|
||||
/* DMA1_Stream7_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA1_Stream7_IRQn, 3, 0);
|
||||
HAL_NVIC_SetPriority(DMA1_Stream7_IRQn, 3, 0); // SPI TX - must have higher priority than SPI RX
|
||||
// and higher priority than the control loop handler
|
||||
HAL_NVIC_EnableIRQ(DMA1_Stream7_IRQn);
|
||||
/* DMA2_Stream0_IRQn interrupt configuration */
|
||||
// Dear STM, no we _don't_ want to fire an interrupt for this DMA
|
||||
|
||||
@@ -80,6 +80,9 @@ void NMI_Handler(void)
|
||||
|
||||
__attribute__((used))
|
||||
void get_regs(void** stack_ptr) {
|
||||
TIM1->BDTR &= ~(TIM_BDTR_AOE_Msk | TIM_BDTR_MOE_Msk); // disable M0 PWM
|
||||
TIM8->BDTR &= ~(TIM_BDTR_AOE_Msk | TIM_BDTR_MOE_Msk); // disable M1 PWM
|
||||
|
||||
void* volatile r0 __attribute__((unused)) = stack_ptr[0];
|
||||
void* volatile r1 __attribute__((unused)) = stack_ptr[1];
|
||||
void* volatile r2 __attribute__((unused)) = stack_ptr[2];
|
||||
@@ -127,6 +130,8 @@ void MemManage_Handler(void)
|
||||
while (1)
|
||||
{
|
||||
/* USER CODE BEGIN W1_MemoryManagement_IRQn 0 */
|
||||
TIM1->BDTR &= ~(TIM_BDTR_AOE_Msk | TIM_BDTR_MOE_Msk); // disable M0 PWM
|
||||
TIM8->BDTR &= ~(TIM_BDTR_AOE_Msk | TIM_BDTR_MOE_Msk); // disable M1 PWM
|
||||
/* USER CODE END W1_MemoryManagement_IRQn 0 */
|
||||
}
|
||||
/* USER CODE BEGIN MemoryManagement_IRQn 1 */
|
||||
@@ -145,6 +150,8 @@ void BusFault_Handler(void)
|
||||
while (1)
|
||||
{
|
||||
/* USER CODE BEGIN W1_BusFault_IRQn 0 */
|
||||
TIM1->BDTR &= ~(TIM_BDTR_AOE_Msk | TIM_BDTR_MOE_Msk); // disable M0 PWM
|
||||
TIM8->BDTR &= ~(TIM_BDTR_AOE_Msk | TIM_BDTR_MOE_Msk); // disable M1 PWM
|
||||
/* USER CODE END W1_BusFault_IRQn 0 */
|
||||
}
|
||||
/* USER CODE BEGIN BusFault_IRQn 1 */
|
||||
@@ -163,6 +170,8 @@ void UsageFault_Handler(void)
|
||||
while (1)
|
||||
{
|
||||
/* USER CODE BEGIN W1_UsageFault_IRQn 0 */
|
||||
TIM1->BDTR &= ~(TIM_BDTR_AOE_Msk | TIM_BDTR_MOE_Msk); // disable M0 PWM
|
||||
TIM8->BDTR &= ~(TIM_BDTR_AOE_Msk | TIM_BDTR_MOE_Msk); // disable M1 PWM
|
||||
/* USER CODE END W1_UsageFault_IRQn 0 */
|
||||
}
|
||||
/* USER CODE BEGIN UsageFault_IRQn 1 */
|
||||
|
||||
@@ -203,7 +203,7 @@ void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
|
||||
__HAL_LINKDMA(uartHandle,hdmatx,hdma_usart2_tx);
|
||||
|
||||
/* USART2 interrupt Init */
|
||||
HAL_NVIC_SetPriority(USART2_IRQn, 5, 0);
|
||||
HAL_NVIC_SetPriority(USART2_IRQn, 10, 0);
|
||||
HAL_NVIC_EnableIRQ(USART2_IRQn);
|
||||
/* USER CODE BEGIN USART2_MspInit 1 */
|
||||
|
||||
@@ -240,7 +240,7 @@ void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
|
||||
/* Peripheral clock disable */
|
||||
__HAL_RCC_USART2_CLK_DISABLE();
|
||||
|
||||
/* UART4 DMA DeInit */
|
||||
/* USART2 DMA DeInit */
|
||||
HAL_DMA_DeInit(uartHandle->hdmarx);
|
||||
HAL_DMA_DeInit(uartHandle->hdmatx);
|
||||
|
||||
|
||||
+20
-17
@@ -297,7 +297,7 @@ bool board_init() {
|
||||
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 1, 0);
|
||||
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
|
||||
|
||||
HAL_NVIC_SetPriority(ControlLoop_IRQn, 5, 0); // must be on the same level as ADC interrupt
|
||||
HAL_NVIC_SetPriority(ControlLoop_IRQn, 5, 0);
|
||||
HAL_NVIC_EnableIRQ(ControlLoop_IRQn);
|
||||
|
||||
HAL_NVIC_SetPriority(TIM8_UP_TIM13_IRQn, 0, 0);
|
||||
@@ -364,19 +364,10 @@ void start_timers() {
|
||||
hadc3.Instance->CR2 &= ~(ADC_CR2_EXTEN | ADC_CR2_JEXTEN);
|
||||
|
||||
/*
|
||||
* Initial intention of the synchronization:
|
||||
* Synchronize TIM1, TIM8 and TIM13 such that:
|
||||
* 1. The triangle waveform of TIM1 leads the triangle waveform of TIM8 by a
|
||||
* 90° phase shift.
|
||||
* 2. The timer update events of TIM1 and TIM8 are symmetrically interleaved.
|
||||
* 3. Each TIM13 reload coincides with a TIM1 lower update event.
|
||||
*
|
||||
* However right now this synchronization only ensures point (1) and (3) but because
|
||||
* TIM1 and TIM3 only trigger an update on every third reload, this does not
|
||||
* allow for (2).
|
||||
*
|
||||
* TODO: revisit the timing topic in general.
|
||||
*
|
||||
* 2. Each TIM13 reload coincides with a TIM1 lower update event.
|
||||
*/
|
||||
Stm32Timer::start_synchronously<3>(
|
||||
{&htim1, &htim8, &htim13},
|
||||
@@ -396,12 +387,8 @@ void start_timers() {
|
||||
__HAL_ADC_CLEAR_FLAG(&hadc1, ADC_FLAG_OVR);
|
||||
__HAL_ADC_CLEAR_FLAG(&hadc2, ADC_FLAG_OVR);
|
||||
__HAL_ADC_CLEAR_FLAG(&hadc3, ADC_FLAG_OVR);
|
||||
|
||||
__HAL_TIM_CLEAR_IT(&htim8, TIM_IT_UPDATE);
|
||||
|
||||
// it's sufficient to enable interrupts for one ADC only because they all trigger simultaneously
|
||||
//__HAL_ADC_ENABLE_IT(&hadc3, ADC_IT_JEOC);
|
||||
//__HAL_ADC_ENABLE_IT(&hadc3, ADC_IT_EOC);
|
||||
|
||||
__HAL_TIM_ENABLE_IT(&htim8, TIM_IT_UPDATE);
|
||||
}
|
||||
}
|
||||
@@ -466,6 +453,8 @@ volatile uint32_t timestamp_ = 0;
|
||||
volatile bool counting_down_ = false;
|
||||
|
||||
void TIM8_UP_TIM13_IRQHandler(void) {
|
||||
COUNT_IRQ(TIM8_UP_TIM13_IRQn);
|
||||
|
||||
// Entry into this function happens at 21-23 clock cycles after the timer
|
||||
// update event.
|
||||
__HAL_TIM_CLEAR_IT(&htim8, TIM_IT_UPDATE);
|
||||
@@ -517,6 +506,18 @@ void ControlLoop_IRQHandler(void) {
|
||||
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);
|
||||
|
||||
@@ -524,7 +525,9 @@ void ControlLoop_IRQHandler(void) {
|
||||
|
||||
// By this time the ADCs for both M0 and M1 should have fired again. But
|
||||
// let's wait for them just to be sure.
|
||||
while (!(ADC2->SR & ADC_SR_EOC));
|
||||
MEASURE_TIME(odrv.task_times_.dc_calib_wait) {
|
||||
while (!(ADC2->SR & ADC_SR_EOC));
|
||||
}
|
||||
|
||||
if (!fetch_and_reset_adcs(¤t0, ¤t1)) {
|
||||
motors[0].disarm_with_error(Motor::ERROR_BAD_TIMING);
|
||||
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
* If the gate driver was in ready state and the new configuration is
|
||||
* different from the old one then the gate driver will exit ready state.
|
||||
*
|
||||
* In any case cnahges to the configuration only take effect with a call to
|
||||
* In any case changes to the configuration only take effect with a call to
|
||||
* init().
|
||||
*/
|
||||
bool config(float requested_gain, float* actual_gain);
|
||||
|
||||
@@ -50,13 +50,18 @@ struct CriticalSectionContext {
|
||||
CriticalSectionContext(const CriticalSectionContext&&) = delete;
|
||||
void operator=(const CriticalSectionContext&) = delete;
|
||||
void operator=(const CriticalSectionContext&&) = delete;
|
||||
operator bool() { return true; };
|
||||
CriticalSectionContext() : mask_(cpu_enter_critical()) {}
|
||||
~CriticalSectionContext() { cpu_exit_critical(mask_); }
|
||||
uint32_t mask_;
|
||||
bool exit_ = false;
|
||||
};
|
||||
|
||||
#ifdef __clang__
|
||||
#define CRITICAL_SECTION() for (CriticalSectionContext __critical_section_context; !__critical_section_context.exit_; __critical_section_context.exit_ = true)
|
||||
#else
|
||||
#define CRITICAL_SECTION() if (CriticalSectionContext __critical_section_context{})
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
|
||||
#include "async_estimator.hpp"
|
||||
#include "acim_estimator.hpp"
|
||||
#include <board.h>
|
||||
|
||||
void AsyncEstimator::update(uint32_t timestamp) {
|
||||
std::optional<float> rotor_phase = rotor_phase_src_.get_current();
|
||||
std::optional<float> rotor_phase_vel = rotor_phase_vel_src_.get_current();
|
||||
std::optional<float2D> idq = idq_src_.get_current();
|
||||
void AcimEstimator::update(uint32_t timestamp) {
|
||||
std::optional<float> rotor_phase = rotor_phase_src_.present();
|
||||
std::optional<float> rotor_phase_vel = rotor_phase_vel_src_.present();
|
||||
std::optional<float2D> idq = idq_src_.present();
|
||||
|
||||
if (!rotor_phase.has_value() || !rotor_phase_vel.has_value() || !idq.has_value()) {
|
||||
active_ = false;
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
#ifndef __ASYNC_ESTIMATOR_HPP
|
||||
#define __ASYNC_ESTIMATOR_HPP
|
||||
#ifndef __ACIM_ESTIMATOR_HPP
|
||||
#define __ACIM_ESTIMATOR_HPP
|
||||
|
||||
#include <component.hpp>
|
||||
#include <cmath>
|
||||
#include <autogen/interfaces.hpp>
|
||||
|
||||
class AsyncEstimator : public ComponentBase {
|
||||
class AcimEstimator : public ComponentBase {
|
||||
public:
|
||||
struct Config_t {
|
||||
float slip_velocity = 14.706f; // [rad/s electrical] = 1/rotor_tau
|
||||
@@ -33,4 +33,4 @@ public:
|
||||
OutputPort<float> stator_phase_ = 0.0f; // [rad] rotor flux phase angle estimate
|
||||
};
|
||||
|
||||
#endif // __ASYNC_ESTIMATOR_HPP
|
||||
#endif // __ACIM_ESTIMATOR_HPP
|
||||
@@ -104,10 +104,12 @@ void Axis::start_thread() {
|
||||
* @brief Blocks until at least one complete control loop has been executed.
|
||||
*/
|
||||
bool Axis::wait_for_control_iteration() {
|
||||
uint16_t control_iteration_num = odrv.n_evt_control_loop_;
|
||||
while (odrv.n_evt_control_loop_ == control_iteration_num) {
|
||||
osDelay(1);
|
||||
}
|
||||
osSignalWait(0x0001, osWaitForever); // this might return instantly
|
||||
osSignalWait(0x0001, osWaitForever); // this might be triggered at the
|
||||
// end of a control loop iteration
|
||||
// which was started before we entered
|
||||
// this function
|
||||
osSignalWait(0x0001, osWaitForever);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -203,11 +205,11 @@ bool Axis::run_lockin_spin(const LockinConfig_t &lockin_config, bool remain_arme
|
||||
motor_.current_control_.Vdq_setpoint_src_.connect_to(&open_loop_controller_.Vdq_setpoint_);
|
||||
|
||||
motor_.current_control_.phase_src_.connect_to(&open_loop_controller_.phase_);
|
||||
async_estimator_.rotor_phase_src_.connect_to(&open_loop_controller_.phase_);
|
||||
acim_estimator_.rotor_phase_src_.connect_to(&open_loop_controller_.phase_);
|
||||
|
||||
motor_.phase_vel_src_.connect_to(&open_loop_controller_.phase_vel_);
|
||||
motor_.current_control_.phase_vel_src_.connect_to(&open_loop_controller_.phase_vel_);
|
||||
async_estimator_.rotor_phase_vel_src_.connect_to(&open_loop_controller_.phase_vel_);
|
||||
acim_estimator_.rotor_phase_vel_src_.connect_to(&open_loop_controller_.phase_vel_);
|
||||
}
|
||||
wait_for_control_iteration();
|
||||
|
||||
@@ -218,8 +220,8 @@ bool Axis::run_lockin_spin(const LockinConfig_t &lockin_config, bool remain_arme
|
||||
float dir = lockin_config.vel >= 0.0f ? 1.0f : -1.0f;
|
||||
|
||||
while ((requested_state_ == AXIS_STATE_UNDEFINED) && motor_.is_armed_) {
|
||||
bool reached_target_vel = std::abs(open_loop_controller_.phase_vel_.get_any().value_or(0.0f) - lockin_config.vel) <= std::numeric_limits<float>::epsilon();
|
||||
bool reached_target_dist = open_loop_controller_.total_distance_.get_any().value_or(0.0f) * dir >= lockin_config.finish_distance * dir;
|
||||
bool reached_target_vel = std::abs(open_loop_controller_.phase_vel_.any().value_or(0.0f) - lockin_config.vel) <= std::numeric_limits<float>::epsilon();
|
||||
bool reached_target_dist = open_loop_controller_.total_distance_.any().value_or(0.0f) * dir >= lockin_config.finish_distance * dir;
|
||||
|
||||
// Check if terminal condition is reached
|
||||
bool terminal_condition = (reached_target_vel && lockin_config.finish_on_vel)
|
||||
@@ -291,7 +293,7 @@ bool Axis::start_closed_loop_control() {
|
||||
if (controller_.config_.control_mode >= Controller::CONTROL_MODE_POSITION_CONTROL) {
|
||||
std::optional<float> pos_init = (controller_.config_.circular_setpoints ?
|
||||
controller_.pos_estimate_circular_src_ :
|
||||
controller_.pos_estimate_linear_src_).get_any();
|
||||
controller_.pos_estimate_linear_src_).any();
|
||||
if (!pos_init.has_value()) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -313,12 +315,12 @@ bool Axis::start_closed_loop_control() {
|
||||
|
||||
OutputPort<float>* phase_src = sensorless_mode ? &sensorless_estimator_.phase_ : &encoder_.phase_;
|
||||
motor_.current_control_.phase_src_.connect_to(phase_src);
|
||||
async_estimator_.rotor_phase_src_.connect_to(phase_src);
|
||||
acim_estimator_.rotor_phase_src_.connect_to(phase_src);
|
||||
|
||||
OutputPort<float>* phase_vel_src = sensorless_mode ? &sensorless_estimator_.phase_vel_ : &encoder_.phase_vel_;
|
||||
motor_.phase_vel_src_.connect_to(phase_vel_src);
|
||||
motor_.current_control_.phase_vel_src_.connect_to(phase_vel_src);
|
||||
async_estimator_.rotor_phase_vel_src_.connect_to(phase_vel_src);
|
||||
acim_estimator_.rotor_phase_vel_src_.connect_to(phase_vel_src);
|
||||
|
||||
if (sensorless_mode) {
|
||||
// Make the final velocity of the loĉk-in spin the setpoint of the
|
||||
@@ -433,20 +435,6 @@ bool Axis::run_idle_loop() {
|
||||
|
||||
// Infinite loop that does calibration and enters main control loop as appropriate
|
||||
void Axis::run_state_machine_loop() {
|
||||
|
||||
// Wait for up to 2s for motor to become ready to allow for error-free
|
||||
// startup. This delay gives the current sensor calibration time to
|
||||
// converge. If the DRV chip is unpowered, the motor will not become ready
|
||||
// but we still enter idle state.
|
||||
for (size_t i = 0; i < 2000; ++i) {
|
||||
if (motor_.current_meas_.has_value()) {
|
||||
break;
|
||||
}
|
||||
osDelay(1);
|
||||
}
|
||||
|
||||
sensorless_estimator_.error_ &= ~SensorlessEstimator::ERROR_UNKNOWN_CURRENT_MEASUREMENT;
|
||||
|
||||
for (;;) {
|
||||
// Load the task chain if a specific request is pending
|
||||
if (requested_state_ != AXIS_STATE_UNDEFINED) {
|
||||
@@ -488,10 +476,18 @@ void Axis::run_state_machine_loop() {
|
||||
bool status;
|
||||
switch (current_state_) {
|
||||
case AXIS_STATE_MOTOR_CALIBRATION: {
|
||||
// These error checks are a hacky way to force legacy behavior
|
||||
// when an error is raised. TODO: remove this when we overhaul
|
||||
// the error architecture
|
||||
// (https://github.com/madcowswe/ODrive/issues/526).
|
||||
if (odrv.any_error())
|
||||
goto invalid_state_label;
|
||||
status = motor_.run_calibration();
|
||||
} break;
|
||||
|
||||
case AXIS_STATE_ENCODER_INDEX_SEARCH: {
|
||||
if (odrv.any_error())
|
||||
goto invalid_state_label;
|
||||
if (!motor_.is_calibrated_)
|
||||
goto invalid_state_label;
|
||||
|
||||
@@ -499,6 +495,8 @@ void Axis::run_state_machine_loop() {
|
||||
} break;
|
||||
|
||||
case AXIS_STATE_ENCODER_DIR_FIND: {
|
||||
if (odrv.any_error())
|
||||
goto invalid_state_label;
|
||||
if (!motor_.is_calibrated_)
|
||||
goto invalid_state_label;
|
||||
|
||||
@@ -525,22 +523,30 @@ void Axis::run_state_machine_loop() {
|
||||
} break;
|
||||
|
||||
case AXIS_STATE_HOMING: {
|
||||
if (odrv.any_error())
|
||||
goto invalid_state_label;
|
||||
status = run_homing();
|
||||
} break;
|
||||
|
||||
case AXIS_STATE_ENCODER_OFFSET_CALIBRATION: {
|
||||
if (odrv.any_error())
|
||||
goto invalid_state_label;
|
||||
if (!motor_.is_calibrated_)
|
||||
goto invalid_state_label;
|
||||
status = encoder_.run_offset_calibration();
|
||||
} break;
|
||||
|
||||
case AXIS_STATE_LOCKIN_SPIN: {
|
||||
if (odrv.any_error())
|
||||
goto invalid_state_label;
|
||||
if (!motor_.is_calibrated_ || encoder_.config_.direction==0)
|
||||
goto invalid_state_label;
|
||||
status = run_lockin_spin(config_.general_lockin, false);
|
||||
} break;
|
||||
|
||||
case AXIS_STATE_CLOSED_LOOP_CONTROL: {
|
||||
if (odrv.any_error())
|
||||
goto invalid_state_label;
|
||||
if (!motor_.is_calibrated_ || (encoder_.config_.direction==0 && !config_.enable_sensorless_mode))
|
||||
goto invalid_state_label;
|
||||
watchdog_feed();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class Axis;
|
||||
|
||||
#include "encoder.hpp"
|
||||
#include "async_estimator.hpp"
|
||||
#include "acim_estimator.hpp"
|
||||
#include "sensorless_estimator.hpp"
|
||||
#include "controller.hpp"
|
||||
#include "open_loop_controller.hpp"
|
||||
@@ -39,7 +39,7 @@ public:
|
||||
TaskTimer can_heartbeat;
|
||||
TaskTimer controller_update;
|
||||
TaskTimer open_loop_controller_update;
|
||||
TaskTimer async_estimator_update;
|
||||
TaskTimer acim_estimator_update;
|
||||
TaskTimer motor_update;
|
||||
TaskTimer current_controller_update;
|
||||
TaskTimer dc_calib;
|
||||
@@ -162,7 +162,7 @@ public:
|
||||
Config_t config_;
|
||||
|
||||
Encoder& encoder_;
|
||||
AsyncEstimator async_estimator_;
|
||||
AcimEstimator acim_estimator_;
|
||||
SensorlessEstimator& sensorless_estimator_;
|
||||
Controller& controller_;
|
||||
OpenLoopController open_loop_controller_;
|
||||
@@ -173,7 +173,7 @@ public:
|
||||
MechanicalBrake& mechanical_brake_;
|
||||
TaskTimes task_times_;
|
||||
|
||||
osThreadId thread_id_;
|
||||
osThreadId thread_id_ = 0;
|
||||
const uint32_t stack_size_ = 2048; // Bytes
|
||||
volatile bool thread_id_valid_ = false;
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ public:
|
||||
/**
|
||||
* @brief Initializes the output port with the specified value.
|
||||
*
|
||||
* An initialization value is required for get_any() to work properly.
|
||||
* get_current() and get_previous() cannot be used to fetch the
|
||||
* An initialization value is required for any() to work properly.
|
||||
* present() and previous() cannot be used to fetch the
|
||||
* initialization value.
|
||||
*/
|
||||
OutputPort(T val) : content_(val) {}
|
||||
@@ -60,7 +60,7 @@ public:
|
||||
* of this class.
|
||||
*/
|
||||
void reset() {
|
||||
// This will eventually overflow to 0 so get_current() could
|
||||
// This will eventually overflow to 0 so present() could
|
||||
// theoretically return a very old value however it is very likely that
|
||||
// the motor will be long disarmed by then.
|
||||
age_++;
|
||||
@@ -70,7 +70,7 @@ public:
|
||||
* @brief Returns the value from this control loop iteration or std::nullopt
|
||||
* if the value was not yet set during this control loop iteration.
|
||||
*/
|
||||
std::optional<T> get_current() {
|
||||
std::optional<T> present() {
|
||||
if (age_ == 0) {
|
||||
return content_;
|
||||
} else {
|
||||
@@ -85,7 +85,7 @@ public:
|
||||
* overwritten during this control loop iteration then this function returns
|
||||
* std::nullopt.
|
||||
*/
|
||||
std::optional<T> get_previous() {
|
||||
std::optional<T> previous() {
|
||||
if (age_ == 1) {
|
||||
return content_;
|
||||
} else {
|
||||
@@ -99,7 +99,7 @@ public:
|
||||
*
|
||||
* This function is thread-safe if load/store operations of T are atomic.
|
||||
*/
|
||||
std::optional<T> get_any() {
|
||||
std::optional<T> any() {
|
||||
return content_;
|
||||
}
|
||||
|
||||
@@ -134,10 +134,10 @@ public:
|
||||
content_ = (OutputPort<T>*)nullptr;
|
||||
}
|
||||
|
||||
std::optional<T> get_current() {
|
||||
std::optional<T> present() {
|
||||
if (content_.index() == 2) {
|
||||
OutputPort<T>* ptr = std::get<2>(content_);
|
||||
return ptr ? ptr->get_current() : std::nullopt;
|
||||
return ptr ? ptr->present() : std::nullopt;
|
||||
} else if (content_.index() == 1) {
|
||||
T* ptr = std::get<1>(content_);
|
||||
return ptr ? std::make_optional(*ptr) : std::nullopt;
|
||||
@@ -150,10 +150,10 @@ public:
|
||||
// ok for this input port to fetch the value from the last iteration.
|
||||
// This would provide a general way to resolve same-iteration data path cycles.
|
||||
|
||||
//std::optional<T> get_previous() {
|
||||
//std::optional<T> previous() {
|
||||
// if (content_.index() == 2) {
|
||||
// OutputPort<T>* ptr = std::get<2>(content_);
|
||||
// return ptr ? ptr->get_previous() : std::nullopt;
|
||||
// return ptr ? ptr->previous() : std::nullopt;
|
||||
// } else if (content_.index() == 1) {
|
||||
// T* ptr = std::get<1>(content_);
|
||||
// return ptr ? std::make_optional(*ptr) : std::nullopt;
|
||||
@@ -162,10 +162,10 @@ public:
|
||||
// }
|
||||
//}
|
||||
|
||||
std::optional<T> get_any() {
|
||||
std::optional<T> any() {
|
||||
if (content_.index() == 2) {
|
||||
OutputPort<T>* ptr = std::get<2>(content_);
|
||||
return ptr ? ptr->get_any() : std::nullopt;
|
||||
return ptr ? ptr->any() : std::nullopt;
|
||||
} else if (content_.index() == 1) {
|
||||
T* ptr = std::get<1>(content_);
|
||||
return ptr ? std::make_optional(*ptr) : std::nullopt;
|
||||
|
||||
@@ -97,13 +97,13 @@ static float limitVel(const float vel_limit, const float vel_estimate, const flo
|
||||
}
|
||||
|
||||
bool Controller::update() {
|
||||
std::optional<float> pos_estimate_linear = pos_estimate_linear_src_.get_current();
|
||||
std::optional<float> pos_estimate_circular = pos_estimate_circular_src_.get_current();
|
||||
std::optional<float> pos_wrap = pos_wrap_src_.get_current();
|
||||
std::optional<float> vel_estimate = vel_estimate_src_.get_current();
|
||||
std::optional<float> pos_estimate_linear = pos_estimate_linear_src_.present();
|
||||
std::optional<float> pos_estimate_circular = pos_estimate_circular_src_.present();
|
||||
std::optional<float> pos_wrap = pos_wrap_src_.present();
|
||||
std::optional<float> vel_estimate = vel_estimate_src_.present();
|
||||
|
||||
std::optional<float> anticogging_pos_estimate = axis_->encoder_.pos_estimate_.get_current();
|
||||
std::optional<float> anticogging_vel_estimate = axis_->encoder_.vel_estimate_.get_current();
|
||||
std::optional<float> anticogging_pos_estimate = axis_->encoder_.pos_estimate_.present();
|
||||
std::optional<float> anticogging_vel_estimate = axis_->encoder_.vel_estimate_.present();
|
||||
|
||||
if (config_.anticogging.calib_anticogging) {
|
||||
if (!anticogging_pos_estimate.has_value() || !anticogging_vel_estimate.has_value()) {
|
||||
@@ -156,8 +156,8 @@ bool Controller::update() {
|
||||
} break;
|
||||
case INPUT_MODE_MIRROR: {
|
||||
if (config_.axis_to_mirror < AXIS_COUNT) {
|
||||
std::optional<float> other_pos = axes[config_.axis_to_mirror].encoder_.pos_estimate_.get_current();
|
||||
std::optional<float> other_vel = axes[config_.axis_to_mirror].encoder_.vel_estimate_.get_current();
|
||||
std::optional<float> other_pos = axes[config_.axis_to_mirror].encoder_.pos_estimate_.present();
|
||||
std::optional<float> other_vel = axes[config_.axis_to_mirror].encoder_.vel_estimate_.present();
|
||||
|
||||
if (!other_pos.has_value() || !other_vel.has_value()) {
|
||||
set_error(ERROR_INVALID_ESTIMATE);
|
||||
@@ -262,7 +262,7 @@ bool Controller::update() {
|
||||
float vel_gain = config_.vel_gain;
|
||||
float vel_integrator_gain = config_.vel_integrator_gain;
|
||||
if (axis_->motor_.config_.motor_type == Motor::MOTOR_TYPE_ACIM) {
|
||||
float effective_flux = axis_->async_estimator_.rotor_flux_;
|
||||
float effective_flux = axis_->acim_estimator_.rotor_flux_;
|
||||
float minflux = axis_->motor_.config_.acim_gain_min_flux;
|
||||
if (std::abs(effective_flux) < minflux)
|
||||
effective_flux = std::copysignf(minflux, effective_flux);
|
||||
|
||||
@@ -82,8 +82,8 @@ bool Encoder::do_checks(){
|
||||
void Encoder::enc_index_cb() {
|
||||
if (config_.use_index) {
|
||||
set_circular_count(0, false);
|
||||
if (config_.zero_count_on_find_idx)
|
||||
set_linear_count(0); // Avoid position control transient after search
|
||||
if (config_.use_index_offset)
|
||||
set_linear_count((int32_t)(config_.index_offset * config_.cpr));
|
||||
if (config_.pre_calibrated) {
|
||||
is_ready_ = true;
|
||||
if(axis_->controller_.config_.anticogging.pre_calibrated){
|
||||
@@ -351,18 +351,18 @@ bool Encoder::run_offset_calibration() {
|
||||
axis_->open_loop_controller_.target_voltage_ = axis_->motor_.config_.motor_type != Motor::MOTOR_TYPE_GIMBAL ? 0.0f : axis_->motor_.config_.calibration_current;
|
||||
axis_->open_loop_controller_.target_vel_ = 0.0f;
|
||||
axis_->open_loop_controller_.total_distance_ = 0.0f;
|
||||
axis_->open_loop_controller_.phase_ = wrap_pm_pi(0 - config_.calib_scan_distance / 2.0f);
|
||||
axis_->open_loop_controller_.phase_ = axis_->open_loop_controller_.initial_phase_ = wrap_pm_pi(0 - config_.calib_scan_distance / 2.0f);
|
||||
|
||||
axis_->motor_.current_control_.enable_current_control_src_ = (axis_->motor_.config_.motor_type != Motor::MOTOR_TYPE_GIMBAL);
|
||||
axis_->motor_.current_control_.Idq_setpoint_src_.connect_to(&axis_->open_loop_controller_.Idq_setpoint_);
|
||||
axis_->motor_.current_control_.Vdq_setpoint_src_.connect_to(&axis_->open_loop_controller_.Vdq_setpoint_);
|
||||
|
||||
axis_->motor_.current_control_.phase_src_.connect_to(&axis_->open_loop_controller_.phase_);
|
||||
axis_->async_estimator_.rotor_phase_src_.connect_to(&axis_->open_loop_controller_.phase_);
|
||||
axis_->acim_estimator_.rotor_phase_src_.connect_to(&axis_->open_loop_controller_.phase_);
|
||||
|
||||
axis_->motor_.phase_vel_src_.connect_to(&axis_->open_loop_controller_.phase_vel_);
|
||||
axis_->motor_.current_control_.phase_vel_src_.connect_to(&axis_->open_loop_controller_.phase_vel_);
|
||||
axis_->async_estimator_.rotor_phase_vel_src_.connect_to(&axis_->open_loop_controller_.phase_vel_);
|
||||
axis_->acim_estimator_.rotor_phase_vel_src_.connect_to(&axis_->open_loop_controller_.phase_vel_);
|
||||
}
|
||||
axis_->wait_for_control_iteration();
|
||||
|
||||
@@ -392,7 +392,7 @@ bool Encoder::run_offset_calibration() {
|
||||
|
||||
// scan forward
|
||||
while ((axis_->requested_state_ == Axis::AXIS_STATE_UNDEFINED) && axis_->motor_.is_armed_) {
|
||||
bool reached_target_dist = axis_->open_loop_controller_.total_distance_.get_any().value_or(-INFINITY) >= config_.calib_scan_distance;
|
||||
bool reached_target_dist = axis_->open_loop_controller_.total_distance_.any().value_or(-INFINITY) >= config_.calib_scan_distance;
|
||||
if (reached_target_dist) {
|
||||
break;
|
||||
}
|
||||
@@ -431,7 +431,7 @@ bool Encoder::run_offset_calibration() {
|
||||
|
||||
// scan backwards
|
||||
while ((axis_->requested_state_ == Axis::AXIS_STATE_UNDEFINED) && axis_->motor_.is_armed_) {
|
||||
bool reached_target_dist = axis_->open_loop_controller_.total_distance_.get_any().value_or(INFINITY) <= 0.0f;
|
||||
bool reached_target_dist = axis_->open_loop_controller_.total_distance_.any().value_or(INFINITY) <= 0.0f;
|
||||
if (reached_target_dist) {
|
||||
break;
|
||||
}
|
||||
@@ -447,9 +447,9 @@ bool Encoder::run_offset_calibration() {
|
||||
|
||||
axis_->motor_.disarm();
|
||||
|
||||
config_.phase_offset = encvaluesum / (num_steps * 2);
|
||||
int32_t residual = encvaluesum - ((int64_t)config_.phase_offset * (int64_t)(num_steps * 2));
|
||||
config_.phase_offset_float = (float)residual / (float)(num_steps * 2) + 0.5f; // add 0.5 to center-align state to phase
|
||||
config_.phase_offset = encvaluesum / num_steps;
|
||||
int32_t residual = encvaluesum - ((int64_t)config_.phase_offset * (int64_t)num_steps);
|
||||
config_.phase_offset_float = (float)residual / (float)num_steps + 0.5f; // add 0.5 to center-align state to phase
|
||||
|
||||
is_ready_ = true;
|
||||
return true;
|
||||
@@ -783,7 +783,7 @@ bool Encoder::update() {
|
||||
// TODO: we should strictly require that this value is from the previous iteration
|
||||
// to avoid spinout scenarios. However that requires a proper way to reset
|
||||
// the encoder from error states.
|
||||
float pos_circular = pos_circular_.get_any().value_or(0.0f);
|
||||
float pos_circular = pos_circular_.any().value_or(0.0f);
|
||||
pos_circular += wrap_pm((pos_cpr_counts_ - pos_cpr_counts_last) / (float)config_.cpr, 1.0f);
|
||||
pos_circular = fmodf_pos(pos_circular, axis_->controller_.config_.circular_setpoint_range);
|
||||
pos_circular_ = pos_circular;
|
||||
@@ -815,7 +815,7 @@ bool Encoder::update() {
|
||||
|
||||
if (is_ready_) {
|
||||
phase_ = wrap_pm_pi(ph) * config_.direction;
|
||||
phase_vel_ = (2*M_PI) * *vel_estimate_.get_current() * axis_->motor_.config_.pole_pairs * config_.direction;
|
||||
phase_vel_ = (2*M_PI) * *vel_estimate_.present() * axis_->motor_.config_.pole_pairs * config_.direction;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -16,22 +16,26 @@ public:
|
||||
|
||||
struct Config_t {
|
||||
Mode mode = MODE_INCREMENTAL;
|
||||
float calib_range = 0.02f; // Accuracy required to pass encoder cpr check
|
||||
float calib_scan_distance = 16.0f * M_PI; // rad electrical
|
||||
float calib_scan_omega = 4.0f * M_PI; // rad/s electrical
|
||||
float bandwidth = 1000.0f;
|
||||
int32_t phase_offset = 0; // Offset between encoder count and rotor electrical phase
|
||||
float phase_offset_float = 0.0f; // Sub-count phase alignment offset
|
||||
int32_t cpr = (2048 * 4); // Default resolution of CUI-AMT102 encoder,
|
||||
float index_offset = 0.0f;
|
||||
uint16_t abs_spi_cs_gpio_pin = 1;
|
||||
uint16_t sincos_gpio_pin_sin = 3;
|
||||
uint16_t sincos_gpio_pin_cos = 4;
|
||||
bool use_index = false;
|
||||
bool pre_calibrated = false; // If true, this means the offset stored in
|
||||
// configuration is valid and does not need
|
||||
// be determined by run_offset_calibration.
|
||||
// In this case the encoder will enter ready
|
||||
// state as soon as the index is found.
|
||||
bool zero_count_on_find_idx = true;
|
||||
int32_t cpr = (2048 * 4); // Default resolution of CUI-AMT102 encoder,
|
||||
int32_t phase_offset = 0; // Offset between encoder count and rotor electrical phase
|
||||
float phase_offset_float = 0.0f; // Sub-count phase alignment offset
|
||||
int32_t direction = 0.0f; // direction with respect to motor
|
||||
int32_t direction = 0; // direction with respect to motor
|
||||
bool use_index_offset = true;
|
||||
bool enable_phase_interpolation = true; // Use velocity to interpolate inside the count state
|
||||
float calib_range = 0.02f; // Accuracy required to pass encoder cpr check
|
||||
float calib_scan_distance = 16.0f * M_PI; // rad electrical
|
||||
float calib_scan_omega = 4.0f * M_PI; // rad/s electrical
|
||||
float bandwidth = 1000.0f;
|
||||
bool find_idx_on_lockin_only = false; // Only be sensitive during lockin scan constant vel state
|
||||
bool ignore_illegal_hall_state = false; // dont error on bad states like 000 or 111
|
||||
uint8_t hall_polarity = 0;
|
||||
@@ -41,6 +45,7 @@ public:
|
||||
uint16_t sincos_gpio_pin_sin = 3;
|
||||
uint16_t sincos_gpio_pin_cos = 4;
|
||||
|
||||
|
||||
// custom setters
|
||||
Encoder* parent = nullptr;
|
||||
void set_use_index(bool value) { use_index = value; parent->set_idx_subscribe(); }
|
||||
|
||||
@@ -183,9 +183,9 @@ void FieldOrientedController::update(uint32_t timestamp) {
|
||||
CRITICAL_SECTION() {
|
||||
ctrl_timestamp_ = timestamp;
|
||||
enable_current_control_ = enable_current_control_src_;
|
||||
Idq_setpoint_ = Idq_setpoint_src_.get_current();
|
||||
Vdq_setpoint_ = Vdq_setpoint_src_.get_current();
|
||||
phase_ = phase_src_.get_current();
|
||||
phase_vel_ = phase_vel_src_.get_current();
|
||||
Idq_setpoint_ = Idq_setpoint_src_.present();
|
||||
Vdq_setpoint_ = Vdq_setpoint_src_.present();
|
||||
phase_ = phase_src_.present();
|
||||
phase_vel_ = phase_vel_src_.present();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,9 @@ bool brake_resistor_saturated = false;
|
||||
// @brief Arms the brake resistor
|
||||
void safety_critical_arm_brake_resistor() {
|
||||
CRITICAL_SECTION() {
|
||||
for (size_t i = 0; i < AXIS_COUNT; ++i) {
|
||||
axes[i].motor_.I_bus_ = 0.0f;
|
||||
}
|
||||
brake_resistor_armed = true;
|
||||
htim2.Instance->CCR3 = 0;
|
||||
htim2.Instance->CCR4 = TIM_APB1_PERIOD_CLOCKS + 1;
|
||||
@@ -164,7 +167,9 @@ void start_adc_pwm() {
|
||||
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
|
||||
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
|
||||
|
||||
safety_critical_arm_brake_resistor();
|
||||
if (odrv.config_.enable_brake_resistor) {
|
||||
safety_critical_arm_brake_resistor();
|
||||
}
|
||||
}
|
||||
|
||||
// @brief ADC1 measurements are written to this buffer by DMA
|
||||
@@ -310,31 +315,41 @@ void update_brake_current() {
|
||||
Ibus_sum += axes[i].motor_.I_bus_;
|
||||
}
|
||||
}
|
||||
|
||||
float brake_duty;
|
||||
|
||||
if (odrv.config_.enable_brake_resistor) {
|
||||
if (!(odrv.config_.brake_resistance > 0.0f)) {
|
||||
odrv.disarm_with_error(ODrive::ERROR_INVALID_BRAKE_RESISTANCE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't start braking until -Ibus > regen_current_allowed
|
||||
float brake_current = -Ibus_sum - odrv.config_.max_regen_current;
|
||||
float brake_duty = brake_current * odrv.config_.brake_resistance / vbus_voltage;
|
||||
|
||||
if (odrv.config_.enable_dc_bus_overvoltage_ramp && (odrv.config_.brake_resistance > 0.0f) && (odrv.config_.dc_bus_overvoltage_ramp_start < odrv.config_.dc_bus_overvoltage_ramp_end)) {
|
||||
brake_duty += std::max((vbus_voltage - odrv.config_.dc_bus_overvoltage_ramp_start) / (odrv.config_.dc_bus_overvoltage_ramp_end - odrv.config_.dc_bus_overvoltage_ramp_start), 0.0f);
|
||||
}
|
||||
// Don't start braking until -Ibus > regen_current_allowed
|
||||
float brake_current = -Ibus_sum - odrv.config_.max_regen_current;
|
||||
brake_duty = brake_current * odrv.config_.brake_resistance / vbus_voltage;
|
||||
|
||||
if (odrv.config_.enable_dc_bus_overvoltage_ramp && (odrv.config_.brake_resistance > 0.0f) && (odrv.config_.dc_bus_overvoltage_ramp_start < odrv.config_.dc_bus_overvoltage_ramp_end)) {
|
||||
brake_duty += std::max((vbus_voltage - odrv.config_.dc_bus_overvoltage_ramp_start) / (odrv.config_.dc_bus_overvoltage_ramp_end - odrv.config_.dc_bus_overvoltage_ramp_start), 0.0f);
|
||||
}
|
||||
|
||||
if (is_nan(brake_duty)) {
|
||||
// Shuts off all motors AND brake resistor, sets error code on all motors.
|
||||
odrv.disarm_with_error(ODrive::ERROR_BRAKE_DUTY_CYCLE_NAN);
|
||||
return;
|
||||
}
|
||||
if (is_nan(brake_duty)) {
|
||||
// Shuts off all motors AND brake resistor, sets error code on all motors.
|
||||
odrv.disarm_with_error(ODrive::ERROR_BRAKE_DUTY_CYCLE_NAN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (brake_duty >= 0.95f) {
|
||||
brake_resistor_saturated = true;
|
||||
}
|
||||
if (brake_duty >= 0.95f) {
|
||||
brake_resistor_saturated = true;
|
||||
}
|
||||
|
||||
// Duty limit at 95% to allow bootstrap caps to charge
|
||||
brake_duty = std::clamp(brake_duty, 0.0f, 0.95f);
|
||||
// Duty limit at 95% to allow bootstrap caps to charge
|
||||
brake_duty = std::clamp(brake_duty, 0.0f, 0.95f);
|
||||
|
||||
// Special handling to avoid the case 0.0/0.0 == NaN, or divide by 0.
|
||||
if (odrv.config_.brake_resistance > 0.0f) {
|
||||
// This cannot result in NaN (safe for race conditions) because we check
|
||||
// brake_resistance != 0 further up.
|
||||
Ibus_sum += brake_duty * vbus_voltage / odrv.config_.brake_resistance;
|
||||
} else {
|
||||
brake_duty = 0;
|
||||
}
|
||||
|
||||
ibus_ += odrv.ibus_report_filter_k_ * (Ibus_sum - ibus_);
|
||||
|
||||
@@ -107,19 +107,25 @@ static bool config_apply_all() {
|
||||
return success;
|
||||
}
|
||||
|
||||
void ODrive::save_configuration(void) {
|
||||
size_t config_size = 0;
|
||||
bool success = config_manager.prepare_store()
|
||||
&& config_write_all()
|
||||
&& config_manager.start_store(&config_size)
|
||||
&& config_write_all()
|
||||
&& config_manager.finish_store();
|
||||
if (success) {
|
||||
user_config_loaded_ = config_size;
|
||||
} else {
|
||||
printf("saving configuration failed\r\n");
|
||||
osDelay(5);
|
||||
bool ODrive::save_configuration(void) {
|
||||
bool success;
|
||||
|
||||
CRITICAL_SECTION() {
|
||||
bool any_armed = std::any_of(axes.begin(), axes.end(),
|
||||
[](auto& axis){ return axis.motor_.is_armed_; });
|
||||
if (any_armed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t config_size = 0;
|
||||
success = config_manager.prepare_store()
|
||||
&& config_write_all()
|
||||
&& config_manager.start_store(&config_size)
|
||||
&& config_write_all()
|
||||
&& config_manager.finish_store();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
void ODrive::erase_configuration(void) {
|
||||
@@ -151,6 +157,17 @@ void ODrive::enter_dfu_mode() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ODrive::any_error() {
|
||||
return error_ != ODrive::ERROR_NONE
|
||||
|| std::any_of(axes.begin(), axes.end(), [](Axis& axis){
|
||||
return axis.error_ != Axis::ERROR_NONE
|
||||
|| axis.motor_.error_ != Motor::ERROR_NONE
|
||||
|| axis.sensorless_estimator_.error_ != SensorlessEstimator::ERROR_NONE
|
||||
|| axis.encoder_.error_ != Encoder::ERROR_NONE
|
||||
|| axis.controller_.error_ != Controller::ERROR_NONE;
|
||||
});
|
||||
}
|
||||
|
||||
void ODrive::clear_errors() {
|
||||
for (auto& axis: axes) {
|
||||
axis.motor_.error_ = Motor::ERROR_NONE;
|
||||
@@ -161,6 +178,9 @@ void ODrive::clear_errors() {
|
||||
axis.error_ = Axis::ERROR_NONE;
|
||||
}
|
||||
error_ = ERROR_NONE;
|
||||
if (odrv.config_.enable_brake_resistor) {
|
||||
safety_critical_arm_brake_resistor();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
@@ -286,9 +306,9 @@ void ODrive::control_loop_cb(uint32_t timestamp) {
|
||||
// TODO: maybe we should add a check to output ports that prevents
|
||||
// double-setting the value.
|
||||
for (auto& axis: axes) {
|
||||
axis.async_estimator_.slip_vel_.reset();
|
||||
axis.async_estimator_.stator_phase_vel_.reset();
|
||||
axis.async_estimator_.stator_phase_.reset();
|
||||
axis.acim_estimator_.slip_vel_.reset();
|
||||
axis.acim_estimator_.stator_phase_vel_.reset();
|
||||
axis.acim_estimator_.stator_phase_.reset();
|
||||
axis.controller_.torque_output_.reset();
|
||||
axis.encoder_.phase_.reset();
|
||||
axis.encoder_.phase_vel_.reset();
|
||||
@@ -361,8 +381,17 @@ void ODrive::control_loop_cb(uint32_t timestamp) {
|
||||
axis.motor_.update(timestamp); // uses torque from controller and phase_vel from encoder
|
||||
|
||||
MEASURE_TIME(axis.task_times_.current_controller_update)
|
||||
axis.motor_.current_control_.update(timestamp); // uses the output of controller_ or open_loop_contoller_ and encoder_ or sensorless_estimator_ or async_estimator_
|
||||
axis.motor_.current_control_.update(timestamp); // uses the output of controller_ or open_loop_contoller_ and encoder_ or sensorless_estimator_ or acim_estimator_
|
||||
}
|
||||
|
||||
// Tell the axis threads that the control loop has finished
|
||||
for (auto& axis: axes) {
|
||||
if (axis.thread_id_) {
|
||||
osSignalSet(axis.thread_id_, 0x0001);
|
||||
}
|
||||
}
|
||||
|
||||
get_gpio(odrv.config_.error_gpio_pin).write(odrv.any_error());
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +433,14 @@ uint32_t ODrive::get_dma_status(uint8_t stream_num) {
|
||||
return (is_reset ? 0 : 0x80000000) | ((channel & 0x7) << 2) | (priority & 0x3);
|
||||
}
|
||||
|
||||
uint32_t ODrive::get_gpio_states() {
|
||||
// TODO: get values that were sampled synchronously with the control loop
|
||||
uint32_t val = 0;
|
||||
for (size_t i = 0; i < GPIO_COUNT; ++i) {
|
||||
val |= ((gpios[i].read() ? 1UL : 0UL) << i);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Main thread started from main().
|
||||
@@ -443,11 +480,30 @@ static void rtos_main(void*) {
|
||||
}
|
||||
|
||||
for(auto& axis: axes){
|
||||
axis.async_estimator_.idq_src_.connect_to(&axis.motor_.Idq_setpoint_);
|
||||
axis.acim_estimator_.idq_src_.connect_to(&axis.motor_.Idq_setpoint_);
|
||||
}
|
||||
|
||||
// Start PWM and enable adc interrupts/callbacks
|
||||
start_adc_pwm();
|
||||
start_analog_thread();
|
||||
|
||||
// Wait for up to 2s for motor to become ready to allow for error-free
|
||||
// startup. This delay gives the current sensor calibration time to
|
||||
// converge. If the DRV chip is unpowered, the motor will not become ready
|
||||
// but we still enter idle state.
|
||||
for (size_t i = 0; i < 2000; ++i) {
|
||||
bool motors_ready = std::all_of(axes.begin(), axes.end(), [](auto& axis) {
|
||||
return axis.motor_.current_meas_.has_value();
|
||||
});
|
||||
if (motors_ready) {
|
||||
break;
|
||||
}
|
||||
osDelay(1);
|
||||
}
|
||||
|
||||
for (auto& axis: axes) {
|
||||
axis.sensorless_estimator_.error_ &= ~SensorlessEstimator::ERROR_UNKNOWN_CURRENT_MEASUREMENT;
|
||||
}
|
||||
|
||||
// Start state machine threads. Each thread will go through various calibration
|
||||
// procedures and then run the actual controller loops.
|
||||
@@ -456,8 +512,6 @@ static void rtos_main(void*) {
|
||||
axes[i].start_thread();
|
||||
}
|
||||
|
||||
start_analog_thread();
|
||||
|
||||
odrv.system_stats_.fully_booted = true;
|
||||
|
||||
// Main thread finished starting everything and can delete itself now (yes this is legal).
|
||||
@@ -577,6 +631,7 @@ extern "C" int main(void) {
|
||||
mode == ODriveIntf::GPIO_MODE_DIGITAL_PULL_UP ||
|
||||
mode == ODriveIntf::GPIO_MODE_DIGITAL_PULL_DOWN ||
|
||||
mode == ODriveIntf::GPIO_MODE_MECH_BRAKE ||
|
||||
mode == ODriveIntf::GPIO_MODE_STATUS ||
|
||||
mode == ODriveIntf::GPIO_MODE_ANALOG_IN) {
|
||||
GPIO_InitStruct.Alternate = 0;
|
||||
} else {
|
||||
@@ -678,6 +733,11 @@ extern "C" int main(void) {
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
|
||||
} break;
|
||||
case ODriveIntf::GPIO_MODE_STATUS: {
|
||||
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
|
||||
} break;
|
||||
default: {
|
||||
odrv.misconfigured_ = true;
|
||||
continue;
|
||||
|
||||
@@ -63,7 +63,7 @@ struct ResistanceMeasurementControlLaw : AlphaBetaFrameController {
|
||||
return test_voltage_ / target_current_;
|
||||
}
|
||||
|
||||
const float kI = 10.0f; // [(V/s)/A]
|
||||
const float kI = 1.0f; // [(V/s)/A]
|
||||
float max_voltage_ = 0.0f;
|
||||
float actual_current_ = 0.0f;
|
||||
float target_current_ = 0.0f;
|
||||
@@ -193,13 +193,15 @@ bool Motor::arm(PhaseControlLaw<3>* control_law) {
|
||||
|
||||
// Reset controller states, integrators, setpoints, etc.
|
||||
axis_->controller_.reset();
|
||||
axis_->async_estimator_.rotor_flux_ = 0.0f;
|
||||
axis_->acim_estimator_.rotor_flux_ = 0.0f;
|
||||
if (control_law_) {
|
||||
control_law_->reset();
|
||||
}
|
||||
|
||||
if (brake_resistor_armed) {
|
||||
if (!odrv.config_.enable_brake_resistor || brake_resistor_armed) {
|
||||
is_armed_ = true;
|
||||
} else {
|
||||
error_ |= Motor::ERROR_BRAKE_RESISTOR_DISARMED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ bool Motor::arm(PhaseControlLaw<3>* control_law) {
|
||||
*/
|
||||
void Motor::apply_pwm_timings(uint16_t timings[3], bool tentative) {
|
||||
CRITICAL_SECTION() {
|
||||
if (!brake_resistor_armed) {
|
||||
if (odrv.config_.enable_brake_resistor && !brake_resistor_armed) {
|
||||
disarm_with_error(ERROR_BRAKE_RESISTOR_DISARMED);
|
||||
}
|
||||
|
||||
@@ -371,7 +373,7 @@ float Motor::effective_current_lim() {
|
||||
//Note - for ACIM motors, available torque is allowed to be 0.
|
||||
float Motor::max_available_torque() {
|
||||
if (config_.motor_type == Motor::MOTOR_TYPE_ACIM) {
|
||||
float max_torque = effective_current_lim_ * config_.torque_constant * axis_->async_estimator_.rotor_flux_;
|
||||
float max_torque = effective_current_lim_ * config_.torque_constant * axis_->acim_estimator_.rotor_flux_;
|
||||
max_torque = std::clamp(max_torque, 0.0f, config_.torque_lim);
|
||||
return max_torque;
|
||||
} else {
|
||||
@@ -492,19 +494,19 @@ bool Motor::run_calibration() {
|
||||
}
|
||||
|
||||
void Motor::update(uint32_t timestamp) {
|
||||
std::optional<float> torque = torque_setpoint_src_.get_current();
|
||||
std::optional<float> torque = torque_setpoint_src_.present();
|
||||
|
||||
if (!torque.has_value()) {
|
||||
error_ |= ERROR_UNKNOWN_TORQUE;
|
||||
return;
|
||||
}
|
||||
|
||||
auto [id, iq] = Idq_setpoint_.get_previous()
|
||||
auto [id, iq] = Idq_setpoint_.previous()
|
||||
.value_or(float2D{0.0f, 0.0f}); // Id doubles as a state variable
|
||||
|
||||
// Convert torque to current
|
||||
if (axis_->motor_.config_.motor_type == Motor::MOTOR_TYPE_ACIM) {
|
||||
iq = *torque / (axis_->motor_.config_.torque_constant * std::max(axis_->async_estimator_.rotor_flux_, config_.acim_gain_min_flux));
|
||||
iq = *torque / (axis_->motor_.config_.torque_constant * std::max(axis_->acim_estimator_.rotor_flux_, config_.acim_gain_min_flux));
|
||||
} else {
|
||||
iq = *torque / axis_->motor_.config_.torque_constant;
|
||||
}
|
||||
@@ -532,13 +534,13 @@ void Motor::update(uint32_t timestamp) {
|
||||
// in this function.
|
||||
// A cleaner fix would be to take the feedforward calculation out of here
|
||||
// and turn it into a separate component.
|
||||
MEASURE_TIME(axis_->task_times_.async_estimator_update)
|
||||
axis_->async_estimator_.update(timestamp);
|
||||
MEASURE_TIME(axis_->task_times_.acim_estimator_update)
|
||||
axis_->acim_estimator_.update(timestamp);
|
||||
|
||||
float vd = 0.0f;
|
||||
float vq = 0.0f;
|
||||
|
||||
std::optional<float> phase_vel = phase_vel_src_.get_current();
|
||||
std::optional<float> phase_vel = phase_vel_src_.present();
|
||||
|
||||
if (config_.R_wL_FF_enable) {
|
||||
if (!phase_vel.has_value()) {
|
||||
@@ -609,6 +611,9 @@ void Motor::current_meas_cb(uint32_t timestamp, std::optional<Iph_ABC_t> current
|
||||
float Inorm_sq = 2.0f / 3.0f * (SQ(current_meas_->phA)
|
||||
+ SQ(current_meas_->phB)
|
||||
+ SQ(current_meas_->phC));
|
||||
|
||||
// Hack: we disable the current check during motor calibration because
|
||||
// it tends to briefly overshoot when the motor moves to align flux with I_alpha
|
||||
if (Inorm_sq > SQ(Itrip)) {
|
||||
disarm_with_error(ERROR_CURRENT_LIMIT_VIOLATION);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ struct BoardConfig_t {
|
||||
bool enable_ascii_protocol_on_usb = true;
|
||||
float max_regen_current = 0.0f;
|
||||
float brake_resistance = DEFAULT_BRAKE_RESISTANCE;
|
||||
float dc_bus_undervoltage_trip_level = 8.0f; //<! [V] minimum voltage below which the motor stops operating
|
||||
bool enable_brake_resistor = false;
|
||||
float dc_bus_undervoltage_trip_level = DEFAULT_MIN_DC_VOLTAGE; //<! [V] minimum voltage below which the motor stops operating
|
||||
float dc_bus_overvoltage_trip_level = 1.07f * HW_VERSION_VOLTAGE; //<! [V] maximum voltage above which the motor stops operating.
|
||||
//<! This protects against cases in which the power supply fails to dissipate
|
||||
//<! the brake power if the brake resistor is disabled.
|
||||
@@ -103,6 +104,7 @@ struct BoardConfig_t {
|
||||
|
||||
float dc_max_positive_current = INFINITY; // Max current [A] the power supply can source
|
||||
float dc_max_negative_current = -0.000001f; // Max current [A] the power supply can sink. You most likely want a non-positive value here. Set to -INFINITY to disable.
|
||||
uint32_t error_gpio_pin = DEFAULT_ERROR_PIN;
|
||||
PWMMapping_t pwm_mappings[4];
|
||||
PWMMapping_t analog_mappings[GPIO_COUNT];
|
||||
};
|
||||
@@ -111,6 +113,7 @@ struct TaskTimes {
|
||||
TaskTimer sampling;
|
||||
TaskTimer control_loop_misc;
|
||||
TaskTimer control_loop_checks;
|
||||
TaskTimer dc_calib_wait;
|
||||
};
|
||||
|
||||
|
||||
@@ -164,10 +167,11 @@ static Stm32Gpio get_gpio(size_t gpio_num) {
|
||||
// general system functions defined in main.cpp
|
||||
class ODrive : public ODriveIntf {
|
||||
public:
|
||||
void save_configuration() override;
|
||||
bool save_configuration() override;
|
||||
void erase_configuration() override;
|
||||
void reboot() override { NVIC_SystemReset(); }
|
||||
void enter_dfu_mode() override;
|
||||
bool any_error();
|
||||
void clear_errors() override;
|
||||
|
||||
float get_adc_voltage(uint32_t gpio) override {
|
||||
@@ -188,6 +192,7 @@ public:
|
||||
|
||||
uint32_t get_interrupt_status(int32_t irqn);
|
||||
uint32_t get_dma_status(uint8_t stream_num);
|
||||
uint32_t get_gpio_states();
|
||||
void disarm_with_error(Error error);
|
||||
|
||||
Error error_ = ERROR_NONE;
|
||||
@@ -229,10 +234,12 @@ public:
|
||||
bool& brake_resistor_saturated_ = ::brake_resistor_saturated; // TODO: make this the actual variable
|
||||
|
||||
SystemStats_t system_stats_;
|
||||
|
||||
// Edit these to suit your capture needs
|
||||
Oscilloscope oscilloscope_{
|
||||
&axes[0].motor_.current_control_.v_current_control_integral_d_, // trigger_src
|
||||
nullptr, // trigger_src
|
||||
0.5f, // trigger_threshold
|
||||
nullptr // &axes[0].motor_.current_control_.Ialpha_measured_ // data_src TODO: change data type
|
||||
nullptr // data_src TODO: change data type
|
||||
};
|
||||
|
||||
BoardConfig_t config_;
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
#include <board.h>
|
||||
|
||||
void OpenLoopController::update(uint32_t timestamp) {
|
||||
auto [prev_Id, prev_Iq] = Idq_setpoint_.get_previous().value_or(float2D{0.0f, 0.0f});
|
||||
auto [prev_Vd, prev_Vq] = Vdq_setpoint_.get_previous().value_or(float2D{0.0f, 0.0f});
|
||||
float phase = phase_.get_previous().value_or(0.0f);
|
||||
float phase_vel = phase_vel_.get_previous().value_or(0.0f);
|
||||
auto [prev_Id, prev_Iq] = Idq_setpoint_.previous().value_or(float2D{0.0f, 0.0f});
|
||||
auto [prev_Vd, prev_Vq] = Vdq_setpoint_.previous().value_or(float2D{0.0f, 0.0f});
|
||||
float phase = phase_.previous().value_or(initial_phase_);
|
||||
float phase_vel = phase_vel_.previous().value_or(0.0f);
|
||||
|
||||
(void)prev_Iq; // unused
|
||||
(void)prev_Vq; // unused
|
||||
@@ -25,6 +25,6 @@ void OpenLoopController::update(uint32_t timestamp) {
|
||||
phase_vel = std::clamp(target_vel_, phase_vel - max_phase_vel_ramp_ * dt, phase_vel + max_phase_vel_ramp_ * dt);
|
||||
phase_vel_ = phase_vel;
|
||||
phase_ = wrap_pm_pi(phase + phase_vel * dt);
|
||||
total_distance_ = total_distance_.get_previous().value_or(0.0f) + phase_vel * dt;
|
||||
total_distance_ = total_distance_.previous().value_or(0.0f) + phase_vel * dt;
|
||||
timestamp_ = timestamp;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public:
|
||||
float target_vel_ = 0.0f;
|
||||
float target_current_ = 0.0f;
|
||||
float target_voltage_ = 0.0f;
|
||||
float initial_phase_ = 0.0f;
|
||||
|
||||
// State/Outputs
|
||||
uint32_t timestamp_ = 0;
|
||||
|
||||
@@ -5,25 +5,23 @@
|
||||
#define OSCILLOSCOPE_SIZE 4096
|
||||
|
||||
void Oscilloscope::update() {
|
||||
// Edit these to suit your capture needs
|
||||
float trigger_data = trigger_src_ ? *trigger_src_ : 0.0f;
|
||||
float trigger_threshold = trigger_threshold_;
|
||||
float sample_data = data_src_ ? *data_src_ : 0.0f;
|
||||
float sample_data = data_src_ ? **data_src_ : 0.0f;
|
||||
|
||||
static bool ready = false;
|
||||
static bool capturing = false;
|
||||
if (trigger_data < trigger_threshold) {
|
||||
ready = true;
|
||||
ready_ = true;
|
||||
}
|
||||
if (ready && trigger_data >= trigger_threshold) {
|
||||
capturing = true;
|
||||
ready = false;
|
||||
if (ready_ && trigger_data >= trigger_threshold) {
|
||||
capturing_ = true;
|
||||
ready_ = false;
|
||||
}
|
||||
if (capturing) {
|
||||
data_[pos_] = sample_data;
|
||||
if (++pos_ >= OSCILLOSCOPE_SIZE) {
|
||||
if (capturing_) {
|
||||
if (pos_ < OSCILLOSCOPE_SIZE) {
|
||||
data_[pos_++] = sample_data;
|
||||
} else {
|
||||
pos_ = 0;
|
||||
capturing = false;
|
||||
capturing_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
class Oscilloscope : public ODriveIntf::OscilloscopeIntf {
|
||||
public:
|
||||
Oscilloscope(float* trigger_src, float trigger_threshold, float* data_src)
|
||||
Oscilloscope(float* trigger_src, float trigger_threshold, float** data_src)
|
||||
: trigger_src_(trigger_src), trigger_threshold_(trigger_threshold), data_src_(data_src) {}
|
||||
|
||||
float get_val(uint32_t index) override {
|
||||
@@ -20,10 +20,12 @@ public:
|
||||
const uint32_t size_ = OSCILLOSCOPE_SIZE;
|
||||
const float* trigger_src_;
|
||||
const float trigger_threshold_;
|
||||
const float* data_src_;
|
||||
float* const * data_src_;
|
||||
|
||||
float data_[OSCILLOSCOPE_SIZE] = {0};
|
||||
size_t pos_ = 0;
|
||||
bool ready_ = false;
|
||||
bool capturing_ = false;
|
||||
};
|
||||
|
||||
#endif // __OSCILLOSCOPE_HPP
|
||||
@@ -81,7 +81,7 @@ bool SensorlessEstimator::update() {
|
||||
V_alpha_beta_memory_[0] = axis_->motor_.current_control_.final_v_alpha_;
|
||||
V_alpha_beta_memory_[1] = axis_->motor_.current_control_.final_v_beta_;
|
||||
|
||||
float phase_vel = phase_vel_.get_previous().value_or(0.0f);
|
||||
float phase_vel = phase_vel_.previous().value_or(0.0f);
|
||||
|
||||
// predict PLL phase with velocity
|
||||
pll_pos_ = wrap_pm_pi(pll_pos_ + current_meas_period * phase_vel);
|
||||
|
||||
@@ -16,15 +16,10 @@ end
|
||||
python_command = find_python3()
|
||||
print('Using python command "'..python_command..'"')
|
||||
|
||||
tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'}
|
||||
tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'}
|
||||
tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --generate-endpoints ODrive --template %f --output %o', outputs='autogen/endpoints.hpp'}
|
||||
tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'}
|
||||
|
||||
-- TODO: use CI to verify that on PRs the enums.py file is consistent with the YAML.
|
||||
-- Note: we currently check this file into source control for two reasons:
|
||||
-- - Don't require tup to run in order to use odrivetool from the repo
|
||||
-- - On Windows, tup is unhappy with writing outside of the tup directory
|
||||
-- TODO: use CI to verify that on PRs the enums.py file is consistent with the YAML.
|
||||
--tup.frule{command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template enums_template.j2 --output ../tools/odrive/enums.py'}
|
||||
|
||||
tup.frule{
|
||||
@@ -34,6 +29,7 @@ tup.frule{
|
||||
|
||||
board_v3 = {
|
||||
dir = 'Board/v3',
|
||||
root_interface = 'ODrive3',
|
||||
sources = {'Drivers/DRV8301/drv8301.cpp', 'Board/v3/board.cpp'},
|
||||
flags = {'-DSTM32F405xx', '-DARM_MATH_CM4', '-mcpu=cortex-m4', '-mfpu=fpv4-sp-d16', '-DFPU_FPV4'},
|
||||
ldflags = {'-TBoard/v3/STM32F405RGTx_FLASH.ld', '-LBoard/v3/Drivers/CMSIS/Lib', '-larm_cortexM4lf_math', '-mcpu=cortex-m4', '-mfpu=fpv4-sp-d16'}
|
||||
@@ -177,6 +173,12 @@ for src in string.gmatch(vars['C_INCLUDES'] or '', "%S+") do
|
||||
stm_includes += board.dir..'/'..string.sub(src, 3, -1) -- remove "-I" from each include path
|
||||
end
|
||||
|
||||
-- Autogen files from YAML interface definitions
|
||||
tup.frule{inputs={'fibre/cpp/interfaces_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/interfaces.hpp'}
|
||||
tup.frule{inputs={'fibre/cpp/function_stubs_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/function_stubs.hpp'}
|
||||
tup.frule{inputs={'fibre/cpp/endpoints_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --generate-endpoints '..board.root_interface..' --template %f --output %o', outputs='autogen/endpoints.hpp'}
|
||||
tup.frule{inputs={'fibre/cpp/type_info_template.j2'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'}
|
||||
|
||||
-- TODO: cleaner separation of the platform code and the rest
|
||||
stm_includes += '.'
|
||||
--stm_includes += 'Drivers/DRV8301'
|
||||
@@ -200,7 +202,7 @@ sources = {
|
||||
'MotorControl/thermistor.cpp',
|
||||
'MotorControl/encoder.cpp',
|
||||
'MotorControl/endstop.cpp',
|
||||
'MotorControl/async_estimator.cpp',
|
||||
'MotorControl/acim_estimator.cpp',
|
||||
'MotorControl/mechanical_brake.cpp',
|
||||
'MotorControl/controller.cpp',
|
||||
'MotorControl/foc.cpp',
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
|
||||
/* Private variables ---------------------------------------------------------*/
|
||||
|
||||
static Introspectable root_obj = ODriveTypeInfo<ODrive>::make_introspectable(odrv);
|
||||
#if HW_VERSION_MAJOR == 3
|
||||
static Introspectable root_obj = ODrive3TypeInfo<ODrive>::make_introspectable(odrv);
|
||||
#endif
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
|
||||
@@ -284,8 +286,8 @@ void cmd_get_feedback(char * pStr, StreamSink& response_channel, bool use_checks
|
||||
} else {
|
||||
Axis& axis = axes[motor_number];
|
||||
respond(response_channel, use_checksum, "%f %f",
|
||||
(double)axis.encoder_.pos_estimate_.get_any().value_or(0.0f),
|
||||
(double)axis.encoder_.vel_estimate_.get_any().value_or(0.0f));
|
||||
(double)axis.encoder_.pos_estimate_.any().value_or(0.0f),
|
||||
(double)axis.encoder_.vel_estimate_.any().value_or(0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,8 +173,8 @@ int32_t CANSimple::get_encoder_estimates_callback(const Axis& axis) {
|
||||
txmsg.isExt = axis.config_.can.is_extended;
|
||||
txmsg.len = 8;
|
||||
|
||||
can_setSignal<float>(txmsg, axis.encoder_.pos_estimate_.get_any().value_or(0.0f), 0, 32, true);
|
||||
can_setSignal<float>(txmsg, axis.encoder_.vel_estimate_.get_any().value_or(0.0f), 32, 32, true);
|
||||
can_setSignal<float>(txmsg, axis.encoder_.pos_estimate_.any().value_or(0.0f), 0, 32, true);
|
||||
can_setSignal<float>(txmsg, axis.encoder_.vel_estimate_.any().value_or(0.0f), 32, 32, true);
|
||||
|
||||
return odCAN->write(txmsg);
|
||||
}
|
||||
@@ -189,7 +189,7 @@ int32_t CANSimple::get_sensorless_estimates_callback(const Axis& axis) {
|
||||
static_assert(sizeof(float) == sizeof(axis.sensorless_estimator_.pll_pos_));
|
||||
|
||||
can_setSignal<float>(txmsg, axis.sensorless_estimator_.pll_pos_, 0, 32, true);
|
||||
can_setSignal<float>(txmsg, axis.sensorless_estimator_.vel_estimate_.get_any().value_or(0.0f), 32, 32, true);
|
||||
can_setSignal<float>(txmsg, axis.sensorless_estimator_.vel_estimate_.any().value_or(0.0f), 32, 32, true);
|
||||
|
||||
return odCAN->write(txmsg);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ void
|
||||
[%- endmacro %]
|
||||
|
||||
[%- macro render_interface(intf) %]
|
||||
class [[intf.name | to_pascal_case]]Intf {
|
||||
class [[intf.name | to_pascal_case]]Intf[% if intf.implements %] :[%- for base_intf in intf.implements %] public [[base_intf.c_name]][% endfor %][% endif %] {
|
||||
public:
|
||||
[%- for intf in intf.interfaces -%]
|
||||
[[render_interface(intf) | indent(4)]]
|
||||
|
||||
@@ -26,7 +26,7 @@ struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo {
|
||||
T* ptr = *(T**)&obj;
|
||||
introspectable_storage_t res;
|
||||
switch (idx) {
|
||||
[%- for property in intf.attributes.values() %]
|
||||
[%- for property in intf.get_all_attributes().values() %]
|
||||
case [[loop.index0]]: *(decltype([[intf.c_name]]::get_[[property.name]](std::declval<T*>()))*)(&res) = [[intf.c_name]]::get_[[property.name]](ptr); break;
|
||||
[%- endfor %]
|
||||
}
|
||||
@@ -38,7 +38,7 @@ struct [[intf.fullname | to_pascal_case]]TypeInfo : TypeInfo {
|
||||
[% for intf in interfaces.values() %][% if not intf.builtin %]
|
||||
template<typename T>
|
||||
const PropertyInfo [[intf.fullname | to_pascal_case]]TypeInfo<T>::property_table[] = {
|
||||
[%- for property in intf.attributes.values() %]
|
||||
[%- for property in intf.get_all_attributes().values() %]
|
||||
{"[[property.name]]", &[[(property.type.purename or property.type.fullname) | to_pascal_case]]TypeInfo<std::remove_reference_t<decltype(*[[intf.c_name]]::get_[[property.name]](std::declval<T*>()))>>::singleton},
|
||||
[%- endfor %]
|
||||
};
|
||||
|
||||
@@ -19,6 +19,10 @@ definitions:
|
||||
c_name: {type: string}
|
||||
brief: {type: string}
|
||||
doc: {type: string}
|
||||
implements:
|
||||
anyOf:
|
||||
- {"$ref": "#/definitions/intf_type_ref"}
|
||||
- {type: array, items: {"$ref": "#/definitions/intf_type_ref"}}
|
||||
functions:
|
||||
type: object
|
||||
additionalProperties: {"$ref": "#/definitions/function"}
|
||||
@@ -42,6 +46,11 @@ definitions:
|
||||
__column__: {type: object}
|
||||
additionalProperties: false
|
||||
|
||||
intf_type_ref:
|
||||
anyOf:
|
||||
- {"type": "string"}
|
||||
- {"$ref": "#/definitions/interface"}
|
||||
|
||||
intf_or_val_type:
|
||||
anyOf:
|
||||
- {"$ref": "#/definitions/interface"}
|
||||
@@ -166,53 +175,8 @@ enums = OrderedDict()
|
||||
interfaces = OrderedDict()
|
||||
userdata = OrderedDict() # Arbitrary data passed from the definition file to the template
|
||||
|
||||
def make_property_type(typeargs):
|
||||
value_type = resolve_valuetype('', typeargs['fibre.Property.type'])
|
||||
mode = typeargs.get('fibre.Property.mode', 'readwrite')
|
||||
name = 'Property<' + value_type['fullname'] + ', ' + mode + '>'
|
||||
fullname = join_name('fibre', name)
|
||||
if fullname in interfaces:
|
||||
return interfaces[fullname]
|
||||
|
||||
c_name = 'Property<' + ('const ' if mode == 'readonly' else '') + value_type['c_name'] + '>'
|
||||
prop_type = {
|
||||
'name': name,
|
||||
'fullname': fullname,
|
||||
'purename': 'fibre.Property',
|
||||
'c_name': c_name,
|
||||
'value_type': value_type, # TODO: should be a metaarg
|
||||
'mode': mode, # TODO: should be a metaarg
|
||||
'builtin': True,
|
||||
'attributes': OrderedDict(),
|
||||
'functions': OrderedDict()
|
||||
}
|
||||
if mode != 'readonly':
|
||||
prop_type['functions']['exchange'] = {
|
||||
'name': 'exchange',
|
||||
'fullname': join_name(fullname, 'exchange'),
|
||||
'in': OrderedDict([('obj', {'name': 'obj', 'type': {'c_name': c_name}}), ('value', {'name': 'value', 'type': value_type, 'optional': True})]),
|
||||
'out': OrderedDict([('value', {'name': 'value', 'type': value_type})]),
|
||||
#'implementation': 'fibre_property_exchange<' + value_type['c_name'] + '>'
|
||||
}
|
||||
else:
|
||||
prop_type['functions']['read'] = {
|
||||
'name': 'read',
|
||||
'fullname': join_name(fullname, 'read'),
|
||||
'in': OrderedDict([('obj', {'name': 'obj', 'type': {'c_name': c_name}})]),
|
||||
'out': OrderedDict([('value', {'name': 'value', 'type': value_type})]),
|
||||
#'implementation': 'fibre_property_read<' + value_type['c_name'] + '>'
|
||||
}
|
||||
|
||||
interfaces[fullname] = prop_type
|
||||
return prop_type
|
||||
|
||||
generics = {
|
||||
'fibre.Property': make_property_type # TODO: improve generic support
|
||||
}
|
||||
|
||||
|
||||
def make_ref_type(interface):
|
||||
name = 'Ref<' + interface['fullname'] + '>'
|
||||
name = 'Ref<' + interface.fullname + '>'
|
||||
fullname = join_name('fibre', name)
|
||||
if fullname in interfaces:
|
||||
return interfaces[fullname]
|
||||
@@ -221,7 +185,7 @@ def make_ref_type(interface):
|
||||
'builtin': True,
|
||||
'name': name,
|
||||
'fullname': fullname,
|
||||
'c_name': interface['fullname'].replace('.', 'Intf::') + 'Intf*'
|
||||
'c_name': interface.fullname.replace('.', 'Intf::') + 'Intf*'
|
||||
}
|
||||
value_types[fullname] = ref_type
|
||||
|
||||
@@ -260,13 +224,14 @@ def regularize_attribute(parent, name, elem, c_is_class):
|
||||
elem['type'] = {}
|
||||
if 'attributes' in elem: elem['type']['attributes'] = elem.pop('attributes')
|
||||
if 'functions' in elem: elem['type']['functions'] = elem.pop('functions')
|
||||
if 'implements' in elem: elem['type']['implements'] = elem.pop('implements')
|
||||
if 'c_is_class' in elem: elem['type']['c_is_class'] = elem.pop('c_is_class')
|
||||
if 'values' in elem: elem['type']['values'] = elem.pop('values')
|
||||
if 'flags' in elem: elem['type']['flags'] = elem.pop('flags')
|
||||
if 'nullflag' in elem: elem['type']['nullflag'] = elem.pop('nullflag')
|
||||
|
||||
elem['name'] = name
|
||||
elem['fullname'] = join_name(parent['fullname'], name)
|
||||
elem['fullname'] = join_name(parent.fullname, name)
|
||||
elem['parent'] = parent
|
||||
elem['typeargs'] = elem.get('typeargs', {})
|
||||
elem['c_name'] = elem.get('c_name', None) or (elem['name'] + ('_' if c_is_class else ''))
|
||||
@@ -277,40 +242,145 @@ def regularize_attribute(parent, name, elem, c_is_class):
|
||||
if isinstance(elem['type'], str) and elem['type'].startswith('readonly '):
|
||||
elem['typeargs']['fibre.Property.mode'] = 'readonly'
|
||||
elem['typeargs']['fibre.Property.type'] = elem['type'][len('readonly '):]
|
||||
elem['type'] = 'fibre.Property'
|
||||
elem['type'] = InterfaceRefElement(parent.fullname, None, 'fibre.Property', elem['typeargs'])
|
||||
if elem['typeargs']['fibre.Property.mode'] == 'readonly' and 'c_setter' in elem: elem.pop('c_setter')
|
||||
elif ('flags' in elem['type']) or ('values' in elem['type']):
|
||||
elem['typeargs']['fibre.Property.mode'] = elem['typeargs'].get('fibre.Property.mode', None) or 'readwrite'
|
||||
elem['typeargs']['fibre.Property.type'] = regularize_valuetype(parent['fullname'], to_pascal_case(name), elem['type'])
|
||||
elem['type'] = 'fibre.Property'
|
||||
elem['typeargs']['fibre.Property.type'] = regularize_valuetype(parent.fullname, to_pascal_case(name), elem['type'])
|
||||
elem['type'] = InterfaceRefElement(parent.fullname, None, 'fibre.Property', elem['typeargs'])
|
||||
if elem['typeargs']['fibre.Property.mode'] == 'readonly' and 'c_setter' in elem: elem.pop('c_setter')
|
||||
else:
|
||||
elem['type'] = regularize_interface(parent['fullname'], to_pascal_case(name), elem['type'])
|
||||
elem['type'] = InterfaceRefElement(parent.fullname, to_pascal_case(name), elem['type'], elem['typeargs'])
|
||||
return elem
|
||||
|
||||
class InterfaceRefElement():
|
||||
def __init__(self, scope, name, elem, typeargs):
|
||||
if isinstance(elem, str):
|
||||
self._intf = None
|
||||
self._scope = scope
|
||||
self._name = elem
|
||||
else:
|
||||
self._intf = InterfaceElement(scope, name, elem)
|
||||
self._scope = None
|
||||
self._name = None
|
||||
self._typeargs = typeargs
|
||||
|
||||
def regularize_interface(path, name, elem):
|
||||
if elem is None:
|
||||
elem = {}
|
||||
if isinstance(elem, str):
|
||||
return elem # will be resolved during type resolution
|
||||
#if path is None:
|
||||
# max_anonymous_type = max([int((re.findall('^' + join_name(path, 'AnonymousType') + '([1-9]+)$', x) + ['0'])[0]) for x in interfaces.keys()])
|
||||
# path = 'AnonymousType' + str(max_anonymous_type + 1)
|
||||
elem['name'] = split_name(name)[-1]
|
||||
elem['fullname'] = path = join_name(path, name)
|
||||
elem['c_name'] = elem.get('c_name', elem['fullname'].replace('.', 'Intf::')) + 'Intf'
|
||||
interfaces[path] = elem
|
||||
elem['functions'] = OrderedDict((name, regularize_func(path, name, func, {'obj': {'type': make_ref_type(elem)}}))
|
||||
for name, func in get_dict(elem, 'functions').items())
|
||||
if not 'c_is_class' in elem:
|
||||
raise Exception(elem)
|
||||
treat_as_class = elem['c_is_class'] # TODO: add command line arg to make this selectively optional
|
||||
elem['attributes'] = OrderedDict((name, regularize_attribute(elem, name, prop, treat_as_class))
|
||||
for name, prop in get_dict(elem, 'attributes').items())
|
||||
elem['interfaces'] = []
|
||||
elem['enums'] = []
|
||||
return elem
|
||||
def resolve(self):
|
||||
"""
|
||||
Resolves this interface reference to an actual InterfaceElement instance.
|
||||
The innermost scope is searched first.
|
||||
At every scope level, if no matching interface is found, it is checked if a
|
||||
matching value type exists. If so, the interface type fibre.Property<value_type>
|
||||
is returned.
|
||||
"""
|
||||
if not self._intf is None:
|
||||
return self._intf
|
||||
|
||||
typeargs = self._typeargs
|
||||
if 'fibre.Property.type' in typeargs:
|
||||
typeargs['fibre.Property.type'] = resolve_valuetype(self._scope, typeargs['fibre.Property.type'])
|
||||
|
||||
scope = self._scope.split('.')
|
||||
for probe_scope in [join_name(*scope[:(len(scope)-i)]) for i in range(len(scope)+1)]:
|
||||
probe_name = join_name(probe_scope, self._name)
|
||||
#print('probing ' + probe_name)
|
||||
if probe_name in interfaces:
|
||||
return interfaces[probe_name]
|
||||
elif probe_name in value_types:
|
||||
typeargs['fibre.Property.type'] = value_types[probe_name]
|
||||
return make_property_type(typeargs)
|
||||
elif probe_name in generics:
|
||||
return generics[probe_name](typeargs)
|
||||
|
||||
raise Exception('could not resolve type {} in {}. Known interfaces are: {}. Known value types are: {}'.format(self._name, self._scope, list(interfaces.keys()), list(value_types.keys())))
|
||||
|
||||
class InterfaceElement():
|
||||
def __init__(self, path, name, elem):
|
||||
if elem is None:
|
||||
elem = {}
|
||||
assert(isinstance(elem, dict))
|
||||
|
||||
path = join_name(path, name)
|
||||
interfaces[path] = self
|
||||
|
||||
self.name = split_name(name)[-1]
|
||||
self.fullname = path
|
||||
self.c_name = elem.get('c_name', self.fullname.replace('.', 'Intf::')) + 'Intf'
|
||||
|
||||
if not 'implements' in elem:
|
||||
elem['implements'] = []
|
||||
elif isinstance(elem['implements'], str):
|
||||
elem['implements'] = [elem['implements']]
|
||||
self.implements = [InterfaceRefElement(path, None, elem, {}) for elem in elem['implements']]
|
||||
self.functions = OrderedDict((name, regularize_func(path, name, func, {'obj': {'type': make_ref_type(self)}}))
|
||||
for name, func in get_dict(elem, 'functions').items())
|
||||
if not 'c_is_class' in elem:
|
||||
raise Exception(elem)
|
||||
treat_as_class = elem['c_is_class'] # TODO: add command line arg to make this selectively optional
|
||||
self.attributes = OrderedDict((name, regularize_attribute(self, name, prop, treat_as_class))
|
||||
for name, prop in get_dict(elem, 'attributes').items())
|
||||
self.interfaces = []
|
||||
self.enums = []
|
||||
|
||||
def get_all_attributes(self, stack=[]):
|
||||
result = OrderedDict()
|
||||
for intf in self.implements:
|
||||
assert(not self in stack)
|
||||
result.update(intf.get_all_attributes(stack + [self]))
|
||||
result.update(self.attributes)
|
||||
return result
|
||||
|
||||
def get_all_functions(self, stack=[]):
|
||||
result = OrderedDict()
|
||||
for intf in self.implements:
|
||||
assert(not self in stack)
|
||||
result.update(intf.get_all_functions(stack + [self]))
|
||||
result.update(self.functions)
|
||||
return result
|
||||
|
||||
class PropertyInterfaceElement(InterfaceElement):
|
||||
def __init__(self, name, fullname, mode, value_type):
|
||||
self.name = name
|
||||
self.fullname = fullname
|
||||
self.purename = 'fibre.Property'
|
||||
self.c_name = 'Property<' + ('const ' if mode == 'readonly' else '') + value_type['c_name'] + '>'
|
||||
self.value_type = value_type # TODO: should be a metaarg
|
||||
self.mode = mode # TODO: should be a metaarg
|
||||
self.builtin = True
|
||||
self.attributes = OrderedDict()
|
||||
self.functions = OrderedDict()
|
||||
if mode != 'readonly':
|
||||
self.functions['exchange'] = {
|
||||
'name': 'exchange',
|
||||
'fullname': join_name(fullname, 'exchange'),
|
||||
'in': OrderedDict([('obj', {'name': 'obj', 'type': {'c_name': self.c_name}}), ('value', {'name': 'value', 'type': value_type, 'optional': True})]),
|
||||
'out': OrderedDict([('value', {'name': 'value', 'type': value_type})]),
|
||||
#'implementation': 'fibre_property_exchange<' + value_type['c_name'] + '>'
|
||||
}
|
||||
else:
|
||||
self.functions['read'] = {
|
||||
'name': 'read',
|
||||
'fullname': join_name(fullname, 'read'),
|
||||
'in': OrderedDict([('obj', {'name': 'obj', 'type': {'c_name': self.c_name}})]),
|
||||
'out': OrderedDict([('value', {'name': 'value', 'type': value_type})]),
|
||||
#'implementation': 'fibre_property_read<' + value_type['c_name'] + '>'
|
||||
}
|
||||
|
||||
interfaces[fullname] = self # TODO: not good to write to a global here
|
||||
|
||||
def make_property_type(typeargs):
|
||||
value_type = resolve_valuetype('', typeargs['fibre.Property.type'])
|
||||
mode = typeargs.get('fibre.Property.mode', 'readwrite')
|
||||
name = 'Property<' + value_type['fullname'] + ', ' + mode + '>'
|
||||
fullname = join_name('fibre', name)
|
||||
if fullname in interfaces:
|
||||
return interfaces[fullname]
|
||||
else:
|
||||
return PropertyInterfaceElement(name, fullname, mode, value_type)
|
||||
|
||||
generics = {
|
||||
'fibre.Property': make_property_type # TODO: improve generic support
|
||||
}
|
||||
|
||||
def regularize_valuetype(path, name, elem):
|
||||
if elem is None:
|
||||
@@ -351,34 +421,6 @@ def regularize_valuetype(path, name, elem):
|
||||
|
||||
return elem
|
||||
|
||||
def resolve_interface(scope, name, typeargs):
|
||||
"""
|
||||
Resolves a type name (i.e. interface name or value type name) given as a
|
||||
string to an interface object. The innermost scope is searched first.
|
||||
At every scope level, if no matching interface is found, it is checked if a
|
||||
matching value type exists. If so, the interface type fibre.Property<value_type>
|
||||
is returned.
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
return name
|
||||
|
||||
if 'fibre.Property.type' in typeargs:
|
||||
typeargs['fibre.Property.type'] = resolve_valuetype(scope, typeargs['fibre.Property.type'])
|
||||
|
||||
scope = scope.split('.')
|
||||
for probe_scope in [join_name(*scope[:(len(scope)-i)]) for i in range(len(scope)+1)]:
|
||||
probe_name = join_name(probe_scope, name)
|
||||
#print('probing ' + probe_name)
|
||||
if probe_name in interfaces:
|
||||
return interfaces[probe_name]
|
||||
elif probe_name in value_types:
|
||||
typeargs['fibre.Property.type'] = value_types[probe_name]
|
||||
return make_property_type(typeargs)
|
||||
elif probe_name in generics:
|
||||
return generics[probe_name](typeargs)
|
||||
|
||||
raise Exception('could not resolve type {} in {}. Known interfaces are: {}. Known value types are: {}'.format(name, join_name(*scope), list(interfaces.keys()), list(value_types.keys())))
|
||||
|
||||
def resolve_valuetype(scope, name):
|
||||
"""
|
||||
Resolves a type name given as a string to the type object.
|
||||
@@ -404,19 +446,19 @@ def map_to_fibre01_type(t):
|
||||
return t['fullname']
|
||||
|
||||
def generate_endpoint_for_property(prop, attr_bindto, idx):
|
||||
prop_intf = interfaces[prop['type']['fullname']]
|
||||
prop_intf = interfaces[prop['type'].fullname]
|
||||
|
||||
endpoint = {
|
||||
'id': idx,
|
||||
'function': prop_intf['functions']['read' if prop['type']['mode'] == 'readonly' else 'exchange'],
|
||||
'function': prop_intf.functions['read' if prop['type'].mode == 'readonly' else 'exchange'],
|
||||
'in_bindings': OrderedDict([('obj', attr_bindto)]),
|
||||
'out_bindings': OrderedDict()
|
||||
}
|
||||
endpoint_definition = {
|
||||
'name': prop['name'],
|
||||
'id': idx,
|
||||
'type': map_to_fibre01_type(prop['type']['value_type']),
|
||||
'access': 'r' if prop['type']['mode'] == 'readonly' else 'rw',
|
||||
'type': map_to_fibre01_type(prop['type'].value_type),
|
||||
'access': 'r' if prop['type'].mode == 'readonly' else 'rw',
|
||||
}
|
||||
return endpoint, endpoint_definition
|
||||
|
||||
@@ -430,10 +472,10 @@ def generate_endpoint_table(intf, bindto, idx):
|
||||
endpoint_definitions = []
|
||||
cnt = 0
|
||||
|
||||
for k, prop in intf['attributes'].items():
|
||||
property_value_type = re.findall('^fibre\.Property<([^>]*), (readonly|readwrite)>$', prop['type']['fullname'])
|
||||
for k, prop in intf.get_all_attributes().items():
|
||||
property_value_type = re.findall('^fibre\.Property<([^>]*), (readonly|readwrite)>$', prop['type'].fullname)
|
||||
#attr_bindto = join_name(bindto, bindings_map.get(join_name(intf['fullname'], k), k + ('_' if len(intf['functions']) or (intf['fullname'] in treat_as_classes) else '')))
|
||||
attr_bindto = intf['c_name'] + '::get_' + prop['name'] + '(' + bindto + ')'
|
||||
attr_bindto = intf.c_name + '::get_' + prop['name'] + '(' + bindto + ')'
|
||||
if len(property_value_type):
|
||||
# Special handling for Property<...> attributes: they resolve to one single endpoint
|
||||
endpoint, endpoint_definition = generate_endpoint_for_property(prop, attr_bindto, idx + cnt)
|
||||
@@ -450,7 +492,7 @@ def generate_endpoint_table(intf, bindto, idx):
|
||||
})
|
||||
cnt += inner_cnt
|
||||
|
||||
for k, func in intf['functions'].items():
|
||||
for k, func in intf.get_all_functions().items():
|
||||
endpoints.append({
|
||||
'id': idx + cnt,
|
||||
'function': func,
|
||||
@@ -463,14 +505,14 @@ def generate_endpoint_table(intf, bindto, idx):
|
||||
endpoint, endpoint_definition = generate_endpoint_for_property({
|
||||
'name': arg['name'],
|
||||
'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readwrite'})
|
||||
}, intf['c_name'] + '::get_' + func['name'] + '_in_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + 1 + i)
|
||||
}, intf.c_name + '::get_' + func['name'] + '_in_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + 1 + i)
|
||||
endpoints.append(endpoint)
|
||||
in_def.append(endpoint_definition)
|
||||
for i, (k_arg, arg) in enumerate(func['out'].items()):
|
||||
endpoint, endpoint_definition = generate_endpoint_for_property({
|
||||
'name': arg['name'],
|
||||
'type': make_property_type({'fibre.Property.type': arg['type'], 'fibre.Property.mode': 'readonly'})
|
||||
}, intf['c_name'] + '::get_' + func['name'] + '_out_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + len(func['in']) + i)
|
||||
}, intf.c_name + '::get_' + func['name'] + '_out_' + k_arg + '_' + '(' + bindto + ')', idx + cnt + len(func['in']) + i)
|
||||
endpoints.append(endpoint)
|
||||
out_def.append(endpoint_definition)
|
||||
|
||||
@@ -541,7 +583,7 @@ for definition_file in definition_files:
|
||||
|
||||
# Regularize everything into a wellknown form
|
||||
for k, item in list(interfaces.items()):
|
||||
regularize_interface('', k, item)
|
||||
InterfaceElement('', k, item)
|
||||
for k, item in list(value_types.items()):
|
||||
regularize_valuetype('', k, item)
|
||||
|
||||
@@ -554,15 +596,16 @@ if len(clashing_names):
|
||||
print("**Error**: Found both an interface and a value type with the name {}. This is not allowed, interfaces and value types (such as enums) share the same namespace.".format(clashing_names[0]), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Resolve all types into references
|
||||
# Resolve all types to references
|
||||
for _, item in list(interfaces.items()):
|
||||
for _, prop in item['attributes'].items():
|
||||
prop['type'] = resolve_interface(item['fullname'], prop['type'], prop['typeargs'])
|
||||
for _, func in item['functions'].items():
|
||||
item.implements = [ref.resolve() for ref in item.implements]
|
||||
for _, prop in item.attributes.items():
|
||||
prop['type'] = prop['type'].resolve()
|
||||
for _, func in item.functions.items():
|
||||
for _, arg in func['in'].items():
|
||||
arg['type'] = resolve_valuetype(item['fullname'], arg['type'])
|
||||
arg['type'] = resolve_valuetype(item.fullname, arg['type'])
|
||||
for _, arg in func['out'].items():
|
||||
arg['type'] = resolve_valuetype(item['fullname'], arg['type'])
|
||||
arg['type'] = resolve_valuetype(item.fullname, arg['type'])
|
||||
|
||||
# Attach interfaces to their parents
|
||||
toplevel_interfaces = []
|
||||
@@ -573,8 +616,8 @@ for k, item in list(interfaces.items()):
|
||||
else:
|
||||
if k[:-1] != ['fibre']: # TODO: remove special handling
|
||||
parent = interfaces[join_name(*k[:-1])]
|
||||
parent['interfaces'].append(item)
|
||||
item['parent'] = parent
|
||||
parent.interfaces.append(item)
|
||||
item.parent = parent
|
||||
toplevel_enums = []
|
||||
for k, item in list(enums.items()):
|
||||
k = split_name(k)
|
||||
@@ -583,7 +626,7 @@ for k, item in list(enums.items()):
|
||||
else:
|
||||
if k[:-1] != ['fibre']: # TODO: remove special handling
|
||||
parent = interfaces[join_name(*k[:-1])]
|
||||
parent['enums'].append(item)
|
||||
parent.enums.append(item)
|
||||
item['parent'] = parent
|
||||
|
||||
|
||||
@@ -643,7 +686,7 @@ def tokenize(text, interface, interface_transform, value_type_transform, attribu
|
||||
if not attr is None:
|
||||
return attribute_transform(token, attr)
|
||||
|
||||
print('Warning: cannot resolve "{}" in {}'.format(token, interface['fullname']))
|
||||
print('Warning: cannot resolve "{}" in {}'.format(token, interface.fullname))
|
||||
return "`" + token + "`"
|
||||
|
||||
return re.sub(r'`([A-Za-z\._]+)`', token_transform, text)
|
||||
|
||||
@@ -4,7 +4,8 @@ import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
exec(open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'fibre', 'tools', 'interface_generator.py')).read())
|
||||
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'fibre', 'tools', 'interface_generator.py')
|
||||
exec(compile(open(path).read(), path, 'exec'))
|
||||
except ImportError as ex:
|
||||
print(str(ex), file=sys.stderr)
|
||||
print("Note that there are new compile-time dependencies since around v0.5.1.", file=sys.stderr)
|
||||
|
||||
+197
-163
@@ -69,6 +69,7 @@ interfaces:
|
||||
DcBusOverCurrent: {doc: too much current pulled out of the power supply}
|
||||
BrakeDeadtimeViolation:
|
||||
BrakeDutyCycleNan:
|
||||
InvalidBrakeResistance: {doc: '`config.brake_resistance` is non-positive or NaN.'}
|
||||
# BrakeResistorDisarmed:
|
||||
# doc: The brake resistor was unexpectedly disarmed.
|
||||
|
||||
@@ -121,6 +122,7 @@ interfaces:
|
||||
sampling: TaskTimer
|
||||
control_loop_misc: TaskTimer
|
||||
control_loop_checks: TaskTimer
|
||||
dc_calib_wait: TaskTimer
|
||||
system_stats:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
@@ -154,153 +156,6 @@ interfaces:
|
||||
addr_match_cnt: readonly uint32
|
||||
rx_cnt: readonly uint32
|
||||
error_cnt: readonly uint32
|
||||
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
# TODO: add support for arrays
|
||||
gpio1_mode: {type: GpioMode, doc: Mode of GPIO1 (changes take effect after reboot), c_name: 'gpio_modes[1]'}
|
||||
gpio2_mode: {type: GpioMode, doc: Mode of GPIO2 (changes take effect after reboot), c_name: 'gpio_modes[2]'}
|
||||
gpio3_mode: {type: GpioMode, doc: Mode of GPIO3 (changes take effect after reboot), c_name: 'gpio_modes[3]'}
|
||||
gpio4_mode: {type: GpioMode, doc: Mode of GPIO4 (changes take effect after reboot), c_name: 'gpio_modes[4]'}
|
||||
gpio5_mode: {type: GpioMode, doc: Mode of GPIO5 (changes take effect after reboot), c_name: 'gpio_modes[5]'}
|
||||
gpio6_mode: {type: GpioMode, doc: Mode of GPIO6 (changes take effect after reboot), c_name: 'gpio_modes[6]'}
|
||||
gpio7_mode: {type: GpioMode, doc: Mode of GPIO7 (changes take effect after reboot), c_name: 'gpio_modes[7]'}
|
||||
gpio8_mode: {type: GpioMode, doc: Mode of GPIO8 (changes take effect after reboot), c_name: 'gpio_modes[8]'}
|
||||
gpio9_mode: {type: GpioMode, doc: Mode of GPIO9 (changes take effect after reboot), c_name: 'gpio_modes[9]'}
|
||||
gpio10_mode: {type: GpioMode, doc: Mode of GPIO10 (changes take effect after reboot), c_name: 'gpio_modes[10]'}
|
||||
gpio11_mode: {type: GpioMode, doc: Mode of GPIO11 (changes take effect after reboot), c_name: 'gpio_modes[11]'}
|
||||
gpio12_mode: {type: GpioMode, doc: Mode of GPIO12 (changes take effect after reboot), c_name: 'gpio_modes[12]'}
|
||||
gpio13_mode: {type: GpioMode, doc: Mode of GPIO13 (changes take effect after reboot), c_name: 'gpio_modes[13]'}
|
||||
gpio14_mode: {type: GpioMode, doc: Mode of GPIO14 (changes take effect after reboot), c_name: 'gpio_modes[14]'}
|
||||
gpio15_mode: {type: GpioMode, doc: Mode of GPIO15 (changes take effect after reboot), c_name: 'gpio_modes[15]'}
|
||||
gpio16_mode: {type: GpioMode, doc: Mode of GPIO16 (changes take effect after reboot), c_name: 'gpio_modes[16]'}
|
||||
|
||||
enable_uart_a:
|
||||
type: bool
|
||||
brief: Enables/disables UART_A.
|
||||
doc: |
|
||||
You also need to set the corresponding GPIOs to GPIO_MODE_UART_A.
|
||||
Refer to [interfaces](interfaces.md) to see which pins support UART_A.
|
||||
Changing this requires a reboot.
|
||||
enable_uart_b:
|
||||
type: bool
|
||||
brief: Enables/disables UART_B.
|
||||
doc: |
|
||||
You also need to set the corresponding GPIOs to GPIO_MODE_UART_B.
|
||||
Refer to [interfaces](interfaces.md) to see which pins support UART_B.
|
||||
Changing this requires a reboot.
|
||||
enable_uart_c: {type: bool, doc: Not supported on ODrive v3.x.}
|
||||
uart_a_baudrate:
|
||||
type: uint32
|
||||
unit: baud/s
|
||||
brief: Defines the baudrate used on the UART interface.
|
||||
doc: |
|
||||
Some baudrates will have a small timing error due to hardware limitations.
|
||||
|
||||
Here's an (incomplete) list of baudrates for ODrive v3.x:
|
||||
|
||||
Configured | Actual | Error [%]
|
||||
-------------|---------------|-----------
|
||||
1.2 KBps | 1.2 KBps | 0
|
||||
2.4 KBps | 2.4 KBps | 0
|
||||
9.6 KBps | 9.6 KBps | 0
|
||||
19.2 KBps | 19.195 KBps | 0.02
|
||||
38.4 KBps | 38.391 KBps | 0.02
|
||||
57.6 KBps | 57.613 KBps | 0.02
|
||||
115.2 KBps | 115.068 KBps | 0.11
|
||||
230.4 KBps | 230.769 KBps | 0.16
|
||||
460.8 KBps | 461.538 KBps | 0.16
|
||||
921.6 KBps | 913.043 KBps | 0.93
|
||||
1.792 MBps | 1.826 MBps | 1.9
|
||||
1.8432 MBps | 1.826 MBps | 0.93
|
||||
|
||||
For more information refer to Section 30.3.4 and Table 142 (the column with f_PCLK = 42 MHz) in the
|
||||
[STM datasheet](https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf).
|
||||
uart_b_baudrate:
|
||||
type: uint32
|
||||
unit: baud/s
|
||||
brief: Defines the baudrate used on the UART interface.
|
||||
doc: See `uart_a_baudrate` for details.
|
||||
uart_c_baudrate: {type: uint32, doc: Not supported on ODrive v3.x.}
|
||||
enable_can_a:
|
||||
type: bool
|
||||
doc: |
|
||||
Enables CAN. Changing this setting requires a reboot.
|
||||
enable_i2c_a:
|
||||
type: bool
|
||||
doc: |
|
||||
Enables I2C. The I2C pins on ODrive v3.x are in conflict with CAN.
|
||||
This setting has no effect if `enable_can_a` is also true.
|
||||
This setting has no effect on ODrive v3.2 or earlier.
|
||||
Changing this setting requires a reboot.
|
||||
enable_ascii_protocol_on_usb: bool
|
||||
max_regen_current: float32
|
||||
brake_resistance:
|
||||
type: float32
|
||||
unit: Ohm
|
||||
brief: Value of the brake resistor connected to the ODrive.
|
||||
doc: Set to 0 to disable.
|
||||
|
||||
dc_bus_undervoltage_trip_level:
|
||||
type: float32
|
||||
unit: V
|
||||
brief: Minimum voltage below which the motor stops operating.
|
||||
dc_bus_overvoltage_trip_level:
|
||||
type: float32
|
||||
unit: V
|
||||
brief: Maximum voltage above which the motor stops operating.
|
||||
doc: |
|
||||
This protects against cases in which the power supply fails to dissipate
|
||||
the brake power if the brake resistor is disabled.
|
||||
The default is 26V for the 24V board version and 52V for the 48V board version.
|
||||
|
||||
enable_dc_bus_overvoltage_ramp:
|
||||
type: bool
|
||||
status: experimental
|
||||
brief: Enables the DC bus overvoltage ramp feature.
|
||||
doc: |
|
||||
If enabled, if the measured DC voltage exceeds `dc_bus_overvoltage_ramp_start`,
|
||||
the ODrive will sink more power than usual into the the brake resistor
|
||||
in an attempt to bring the voltage down again.
|
||||
|
||||
The brake duty cycle is increased by the following amount:
|
||||
|
||||
* `vbus_voltage` == `dc_bus_overvoltage_ramp_start` => brake_duty_cycle += 0%
|
||||
* `vbus_voltage` == `dc_bus_overvoltage_ramp_end` => brake_duty_cycle += 100%
|
||||
|
||||
Remarks:
|
||||
- This feature is active even when all motors are disarmed.
|
||||
- This feature is disabled if `brake_resistance` is non-positive.
|
||||
dc_bus_overvoltage_ramp_start:
|
||||
type: float32
|
||||
status: experimental
|
||||
brief: See `enable_dc_bus_overvoltage_ramp`.
|
||||
doc: Do not set this lower than your usual `vbus_voltage`,
|
||||
unless you like fried brake resistors.
|
||||
dc_bus_overvoltage_ramp_end:
|
||||
type: float32
|
||||
status: experimental
|
||||
brief: See `enable_dc_bus_overvoltage_ramp`.
|
||||
doc: Must be larger than `dc_bus_overvoltage_ramp_start`,
|
||||
otherwise the ramp feature is disabled.
|
||||
|
||||
dc_max_positive_current:
|
||||
type: float32
|
||||
unit: A
|
||||
brief: Max current the power supply can source.
|
||||
dc_max_negative_current:
|
||||
type: float32
|
||||
unit: A
|
||||
brief: Max current the power supply can sink.
|
||||
doc: You most likely want a non-positive value here. Set to -INFINITY to disable.
|
||||
|
||||
gpio1_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[0]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_PWM`.}
|
||||
gpio2_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[1]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_PWM`.}
|
||||
gpio3_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[2]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_PWM`.}
|
||||
gpio4_pwm_mapping: {type: Endpoint, c_name: 'pwm_mappings[3]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_PWM`.}
|
||||
gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[3]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_ANALOG_IN`.}
|
||||
gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[4]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_ANALOG_IN`.}
|
||||
user_config_loaded: readonly uint32
|
||||
misconfigured:
|
||||
# TODO: make this a system error
|
||||
@@ -323,8 +178,6 @@ interfaces:
|
||||
capability were both used as interrupt input.
|
||||
Example: `step_gpio_pin` of both axes were set to the same GPIO.
|
||||
|
||||
axis0: {type: Axis, c_name: get_axis(0)}
|
||||
axis1: {type: Axis, c_name: get_axis(1)}
|
||||
oscilloscope: {type: Oscilloscope}
|
||||
can: {type: Can, c_name: get_can()}
|
||||
test_property: uint32
|
||||
@@ -332,7 +185,7 @@ interfaces:
|
||||
functions:
|
||||
test_function: {in: {delta: int32}, out: {cnt: int32}}
|
||||
get_adc_voltage: {in: {gpio: uint32}, out: {voltage: float32}, doc: Reads the ADC voltage of the specified GPIO. The GPIO should be in `GPIO_MODE_ANALOG_IN`.}
|
||||
save_configuration:
|
||||
save_configuration: {out: {success: bool}}
|
||||
erase_configuration:
|
||||
reboot:
|
||||
enter_dfu_mode:
|
||||
@@ -358,9 +211,155 @@ interfaces:
|
||||
bits 1:0: priority (3 is highest priority)
|
||||
0xffffffff if the specified number is not a valid DMA stream number.
|
||||
doc: Returns information about the specified DMA stream.
|
||||
get_gpio_states:
|
||||
out: {status: {type: uint32}}
|
||||
doc: Returns the logic states of all GPIOs. Bit i represents the state of GPIOi.
|
||||
clear_errors:
|
||||
doc: Clear all the errors of this device including all contained submodules.
|
||||
|
||||
ODrive.Config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
enable_uart_a:
|
||||
type: bool
|
||||
brief: Enables/disables UART_A.
|
||||
doc: |
|
||||
You also need to set the corresponding GPIOs to GPIO_MODE_UART_A.
|
||||
Refer to [interfaces](interfaces.md) to see which pins support UART_A.
|
||||
Changing this requires a reboot.
|
||||
enable_uart_b:
|
||||
type: bool
|
||||
brief: Enables/disables UART_B.
|
||||
doc: |
|
||||
You also need to set the corresponding GPIOs to GPIO_MODE_UART_B.
|
||||
Refer to [interfaces](interfaces.md) to see which pins support UART_B.
|
||||
Changing this requires a reboot.
|
||||
enable_uart_c: {type: bool, doc: Not supported on ODrive v3.x.}
|
||||
uart_a_baudrate:
|
||||
type: uint32
|
||||
unit: baud/s
|
||||
brief: Defines the baudrate used on the UART interface.
|
||||
doc: |
|
||||
Some baudrates will have a small timing error due to hardware limitations.
|
||||
|
||||
Here's an (incomplete) list of baudrates for ODrive v3.x:
|
||||
|
||||
Configured | Actual | Error [%]
|
||||
-------------|---------------|-----------
|
||||
1.2 KBps | 1.2 KBps | 0
|
||||
2.4 KBps | 2.4 KBps | 0
|
||||
9.6 KBps | 9.6 KBps | 0
|
||||
19.2 KBps | 19.195 KBps | 0.02
|
||||
38.4 KBps | 38.391 KBps | 0.02
|
||||
57.6 KBps | 57.613 KBps | 0.02
|
||||
115.2 KBps | 115.068 KBps | 0.11
|
||||
230.4 KBps | 230.769 KBps | 0.16
|
||||
460.8 KBps | 461.538 KBps | 0.16
|
||||
921.6 KBps | 913.043 KBps | 0.93
|
||||
1.792 MBps | 1.826 MBps | 1.9
|
||||
1.8432 MBps | 1.826 MBps | 0.93
|
||||
|
||||
For more information refer to Section 30.3.4 and Table 142 (the column with f_PCLK = 42 MHz) in the
|
||||
[STM datasheet](https://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf).
|
||||
uart_b_baudrate:
|
||||
type: uint32
|
||||
unit: baud/s
|
||||
brief: Defines the baudrate used on the UART interface.
|
||||
doc: See `uart_a_baudrate` for details.
|
||||
uart_c_baudrate: {type: uint32, doc: Not supported on ODrive v3.x.}
|
||||
enable_can_a:
|
||||
type: bool
|
||||
doc: |
|
||||
Enables CAN. Changing this setting requires a reboot.
|
||||
enable_i2c_a:
|
||||
type: bool
|
||||
doc: |
|
||||
Enables I2C. The I2C pins on ODrive v3.x are in conflict with CAN.
|
||||
This setting has no effect if `enable_can_a` is also true.
|
||||
This setting has no effect on ODrive v3.2 or earlier.
|
||||
Changing this setting requires a reboot.
|
||||
enable_ascii_protocol_on_usb: bool
|
||||
max_regen_current: float32
|
||||
brake_resistance:
|
||||
type: float32
|
||||
unit: Ohm
|
||||
brief: Value of the brake resistor connected to the ODrive.
|
||||
doc: |
|
||||
If you set this to a lower value than the true brake resistance
|
||||
then the ODrive will not meed the `max_regen_current` constraint
|
||||
during braking, that is it will sink more than `max_regen_current`
|
||||
into the power supply. Some power supplies don't like this.
|
||||
|
||||
If you set this to a higher value than the true brake resistance
|
||||
then the ODrive will unnecessarily burn more power than required
|
||||
during braking.
|
||||
enable_brake_resistor:
|
||||
type: bool
|
||||
brief: Enable/disable the use of a brake resistor.
|
||||
doc: |
|
||||
Setting this to False even though a brake resistor is connected is
|
||||
harmless. Setting this to True even though no brake resistor is
|
||||
connected can break the power supply.
|
||||
Changes to this value require a reboot to take effect.
|
||||
|
||||
dc_bus_undervoltage_trip_level:
|
||||
type: float32
|
||||
unit: V
|
||||
brief: Minimum voltage below which the motor stops operating.
|
||||
dc_bus_overvoltage_trip_level:
|
||||
type: float32
|
||||
unit: V
|
||||
brief: Maximum voltage above which the motor stops operating.
|
||||
doc: |
|
||||
This protects against cases in which the power supply fails to dissipate
|
||||
the brake power if the brake resistor is disabled.
|
||||
The default is 26V for the 24V board version and 52V for the 48V board version.
|
||||
|
||||
enable_dc_bus_overvoltage_ramp:
|
||||
type: bool
|
||||
status: experimental
|
||||
brief: Enables the DC bus overvoltage ramp feature.
|
||||
doc: |
|
||||
If enabled, if the measured DC voltage exceeds `dc_bus_overvoltage_ramp_start`,
|
||||
the ODrive will sink more power than usual into the the brake resistor
|
||||
in an attempt to bring the voltage down again.
|
||||
|
||||
The brake duty cycle is increased by the following amount:
|
||||
|
||||
* `vbus_voltage` == `dc_bus_overvoltage_ramp_start` => brake_duty_cycle += 0%
|
||||
* `vbus_voltage` == `dc_bus_overvoltage_ramp_end` => brake_duty_cycle += 100%
|
||||
|
||||
Remarks:
|
||||
- This feature is active even when all motors are disarmed.
|
||||
- This feature is disabled if `brake_resistance` is non-positive.
|
||||
dc_bus_overvoltage_ramp_start:
|
||||
type: float32
|
||||
status: experimental
|
||||
brief: See `enable_dc_bus_overvoltage_ramp`.
|
||||
doc: Do not set this lower than your usual `vbus_voltage`,
|
||||
unless you like fried brake resistors.
|
||||
dc_bus_overvoltage_ramp_end:
|
||||
type: float32
|
||||
status: experimental
|
||||
brief: See `enable_dc_bus_overvoltage_ramp`.
|
||||
doc: Must be larger than `dc_bus_overvoltage_ramp_start`,
|
||||
otherwise the ramp feature is disabled.
|
||||
|
||||
dc_max_positive_current:
|
||||
type: float32
|
||||
unit: A
|
||||
brief: Max current the power supply can source.
|
||||
dc_max_negative_current:
|
||||
type: float32
|
||||
unit: A
|
||||
brief: Max current the power supply can sink.
|
||||
doc: You most likely want a non-positive value here. Set to -INFINITY to disable.
|
||||
|
||||
error_gpio_pin: {type: uint32}
|
||||
|
||||
gpio3_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[3]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_ANALOG_IN`.}
|
||||
gpio4_analog_mapping: {type: Endpoint, c_name: 'analog_mappings[4]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_ANALOG_IN`.}
|
||||
|
||||
ODrive.Can:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
@@ -483,7 +482,7 @@ interfaces:
|
||||
motor: Motor
|
||||
controller: Controller
|
||||
encoder: Encoder
|
||||
async_estimator: AsyncEstimator
|
||||
acim_estimator: AcimEstimator
|
||||
sensorless_estimator: SensorlessEstimator
|
||||
trap_traj: TrapezoidalTrajectory
|
||||
min_endstop: Endstop
|
||||
@@ -499,7 +498,7 @@ interfaces:
|
||||
can_heartbeat: TaskTimer
|
||||
controller_update: TaskTimer
|
||||
open_loop_controller_update: TaskTimer
|
||||
async_estimator_update: TaskTimer
|
||||
acim_estimator_update: TaskTimer
|
||||
motor_update: TaskTimer
|
||||
current_controller_update: TaskTimer
|
||||
dc_calib: TaskTimer
|
||||
@@ -788,7 +787,7 @@ interfaces:
|
||||
functions:
|
||||
get_val: {in: {index: uint32}, out: {val: float32}}
|
||||
|
||||
ODrive.AsyncEstimator:
|
||||
ODrive.AcimEstimator:
|
||||
c_is_class: True
|
||||
attributes:
|
||||
rotor_flux: {type: readonly float32, unit: A, doc: estimated magnitude of the rotor flux}
|
||||
@@ -796,7 +795,7 @@ interfaces:
|
||||
type: readonly float32
|
||||
unit: rad/s
|
||||
doc: estimated slip between physical and electrical angular velocity}
|
||||
c_getter: slip_vel_.get_any().value_or(0.0f)
|
||||
c_getter: slip_vel_.any().value_or(0.0f)
|
||||
phase_offset:
|
||||
type: readonly float32
|
||||
unit: rad
|
||||
@@ -805,12 +804,12 @@ interfaces:
|
||||
type: readonly float32
|
||||
unit: rad/s
|
||||
doc: calculated setpoint for the electrical velocity}
|
||||
c_getter: stator_phase_vel_.get_any().value_or(0.0f)
|
||||
c_getter: stator_phase_vel_.any().value_or(0.0f)
|
||||
stator_phase:
|
||||
type: readonly float32
|
||||
unit: rad
|
||||
doc: calculated setpoint for the electrical phase}
|
||||
c_getter: stator_phase_.get_any().value_or(0.0f)
|
||||
c_getter: stator_phase_.any().value_or(0.0f)
|
||||
config:
|
||||
c_is_class: False
|
||||
attributes:
|
||||
@@ -970,14 +969,14 @@ interfaces:
|
||||
shadow_count: readonly int32
|
||||
count_in_cpr: readonly int32
|
||||
interpolation: readonly float32
|
||||
phase: {type: readonly float32, c_getter: phase_.get_any().value_or(0.0f)}
|
||||
pos_estimate: {type: readonly float32, c_getter: pos_estimate_.get_any().value_or(0.0f)}
|
||||
phase: {type: readonly float32, c_getter: phase_.any().value_or(0.0f)}
|
||||
pos_estimate: {type: readonly float32, c_getter: pos_estimate_.any().value_or(0.0f)}
|
||||
pos_estimate_counts: readonly float32
|
||||
pos_cpr_counts: readonly float32
|
||||
delta_pos_cpr_counts: readonly float32
|
||||
pos_circular: {type: readonly float32, c_getter: pos_circular_.get_any().value_or(0.0f)}
|
||||
hall_state: readonly uint8
|
||||
vel_estimate: {type: readonly float32, c_getter: vel_estimate_.get_any().value_or(0.0f)}
|
||||
vel_estimate: {type: readonly float32, c_getter: vel_estimate_.any().value_or(0.0f)}
|
||||
vel_estimate_counts: readonly float32
|
||||
calib_scan_response: readonly float32
|
||||
pos_abs: int32
|
||||
@@ -987,9 +986,10 @@ interfaces:
|
||||
attributes:
|
||||
mode: Mode
|
||||
use_index: {type: bool, c_setter: set_use_index}
|
||||
index_offset: float32
|
||||
use_index_offset: bool
|
||||
find_idx_on_lockin_only: {type: bool, c_setter: set_find_idx_on_lockin_only}
|
||||
abs_spi_cs_gpio_pin: {type: uint16, c_setter: set_abs_spi_cs_gpio_pin, doc: Make sure that the GPIO is in `GPIO_MODE_DIGITAL`.}
|
||||
zero_count_on_find_idx: bool
|
||||
cpr: int32
|
||||
phase_offset: int32
|
||||
phase_offset_float: float32
|
||||
@@ -1021,10 +1021,10 @@ interfaces:
|
||||
flags:
|
||||
UnstableGain:
|
||||
UnknownCurrentMeasurement:
|
||||
phase: {type: readonly float32, unit: rad, c_getter: phase_.get_any().value_or(0.0f)}
|
||||
phase: {type: readonly float32, unit: rad, c_getter: phase_.any().value_or(0.0f)}
|
||||
pll_pos: {type: readonly float32, unit: rad}
|
||||
phase_vel: {type: readonly float32, unit: rad/s, c_getter: phase_vel_.get_any().value_or(0.0f)}
|
||||
vel_estimate: {type: readonly float32, unit: turns/s, c_getter: vel_estimate_.get_any().value_or(0.0f)}
|
||||
phase_vel: {type: readonly float32, unit: rad/s, c_getter: phase_vel_.any().value_or(0.0f)}
|
||||
vel_estimate: {type: readonly float32, unit: turns/s, c_getter: vel_estimate_.any().value_or(0.0f)}
|
||||
# pll_kp: float32
|
||||
# pll_ki: float32
|
||||
config:
|
||||
@@ -1082,6 +1082,39 @@ interfaces:
|
||||
length: readonly uint32
|
||||
max_length: uint32
|
||||
|
||||
ODrive3:
|
||||
c_is_class: True
|
||||
implements: ODrive
|
||||
attributes:
|
||||
config:
|
||||
c_is_class: False
|
||||
implements: ODrive.Config
|
||||
attributes:
|
||||
# TODO: add support for arrays
|
||||
gpio1_mode: {type: ODrive.GpioMode, doc: Mode of GPIO1 (changes take effect after reboot), c_name: 'gpio_modes[1]'}
|
||||
gpio2_mode: {type: ODrive.GpioMode, doc: Mode of GPIO2 (changes take effect after reboot), c_name: 'gpio_modes[2]'}
|
||||
gpio3_mode: {type: ODrive.GpioMode, doc: Mode of GPIO3 (changes take effect after reboot), c_name: 'gpio_modes[3]'}
|
||||
gpio4_mode: {type: ODrive.GpioMode, doc: Mode of GPIO4 (changes take effect after reboot), c_name: 'gpio_modes[4]'}
|
||||
gpio5_mode: {type: ODrive.GpioMode, doc: Mode of GPIO5 (changes take effect after reboot), c_name: 'gpio_modes[5]'}
|
||||
gpio6_mode: {type: ODrive.GpioMode, doc: Mode of GPIO6 (changes take effect after reboot), c_name: 'gpio_modes[6]'}
|
||||
gpio7_mode: {type: ODrive.GpioMode, doc: Mode of GPIO7 (changes take effect after reboot), c_name: 'gpio_modes[7]'}
|
||||
gpio8_mode: {type: ODrive.GpioMode, doc: Mode of GPIO8 (changes take effect after reboot), c_name: 'gpio_modes[8]'}
|
||||
gpio9_mode: {type: ODrive.GpioMode, doc: Mode of GPIO9 (changes take effect after reboot), c_name: 'gpio_modes[9]'}
|
||||
gpio10_mode: {type: ODrive.GpioMode, doc: Mode of GPIO10 (changes take effect after reboot), c_name: 'gpio_modes[10]'}
|
||||
gpio11_mode: {type: ODrive.GpioMode, doc: Mode of GPIO11 (changes take effect after reboot), c_name: 'gpio_modes[11]'}
|
||||
gpio12_mode: {type: ODrive.GpioMode, doc: Mode of GPIO12 (changes take effect after reboot), c_name: 'gpio_modes[12]'}
|
||||
gpio13_mode: {type: ODrive.GpioMode, doc: Mode of GPIO13 (changes take effect after reboot), c_name: 'gpio_modes[13]'}
|
||||
gpio14_mode: {type: ODrive.GpioMode, doc: Mode of GPIO14 (changes take effect after reboot), c_name: 'gpio_modes[14]'}
|
||||
gpio15_mode: {type: ODrive.GpioMode, doc: Mode of GPIO15 (changes take effect after reboot), c_name: 'gpio_modes[15]'}
|
||||
gpio16_mode: {type: ODrive.GpioMode, doc: Mode of GPIO16 (changes take effect after reboot), c_name: 'gpio_modes[16]'}
|
||||
|
||||
gpio1_pwm_mapping: {type: ODrive.Endpoint, c_name: 'pwm_mappings[0]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_PWM`.}
|
||||
gpio2_pwm_mapping: {type: ODrive.Endpoint, c_name: 'pwm_mappings[1]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_PWM`.}
|
||||
gpio3_pwm_mapping: {type: ODrive.Endpoint, c_name: 'pwm_mappings[2]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_PWM`.}
|
||||
gpio4_pwm_mapping: {type: ODrive.Endpoint, c_name: 'pwm_mappings[3]', doc: Make sure the corresponding GPIO is in `GPIO_MODE_PWM`.}
|
||||
axis0: {type: ODrive.Axis, c_name: get_axis(0)}
|
||||
axis1: {type: ODrive.Axis, c_name: get_axis(1)}
|
||||
|
||||
valuetypes:
|
||||
ODrive.GpioMode:
|
||||
values:
|
||||
@@ -1110,6 +1143,7 @@ valuetypes:
|
||||
Enc1: {doc: The pin is used by quadrature encoder 1.}
|
||||
Enc2: {doc: This mode is not supported on ODrive v3.x.}
|
||||
MechBrake: {doc: This is to support external mechanical brakes.}
|
||||
Status: {doc: The pin is used for status output (see `config.error_gpio_pin`)}
|
||||
|
||||
ODrive.Can.Protocol:
|
||||
values: {Simple: }
|
||||
|
||||
@@ -143,11 +143,7 @@ If the flashing worked, you can connect to the board using the [odrivetool](gett
|
||||
|
||||
<br><br>
|
||||
## Testing
|
||||
The script `tools/run_tests.py` runs a sequence of automated tests for several firmware features as well as high power burn-in tests. Some tests only need one ODrive and one motor/encoder pair while other tests need a back-to-back test rig such as [this one](https://cad.onshape.com/documents/026bda35ad5dff4d73c1d37f/w/ae302174f402737e1fdb3783/e/5ca143a6e5e24daf1fe8e434). In any case, to run the tests you need to provide a YAML file that lists the parameters of your test setup. An example can be found at [`tools/test-rig-parallel.yaml`](tools/test-rig-parallel.yaml`). The programmer serial number can be found by running `Firmware/find_programmer.sh` (make sure it has the latest firmware from STM).
|
||||
|
||||
<div class="alert" markdown="span">The test script commands the ODrive to high currents and high motor speeds so if your ODrive is connected to anything other than a stirdy test-rig (or free spinning motors), it will probably break your machine.</div>
|
||||
|
||||
Example usage: `./run_tests.py --test-rig-yaml ../tools/test-rig-parallel.yaml`
|
||||
_Main article: [Testing](testing.md)_
|
||||
|
||||
<br><br>
|
||||
## Debugging
|
||||
|
||||
@@ -19,6 +19,7 @@ GPIO_MODE_ENC0 = 11
|
||||
GPIO_MODE_ENC1 = 12
|
||||
GPIO_MODE_ENC2 = 13
|
||||
GPIO_MODE_MECH_BRAKE = 14
|
||||
GPIO_MODE_STATUS = 15
|
||||
|
||||
# ODrive.Can.Protocol
|
||||
PROTOCOL_SIMPLE = 0
|
||||
@@ -77,6 +78,7 @@ ODRIVE_ERROR_DC_BUS_OVER_REGEN_CURRENT = 0x00000008
|
||||
ODRIVE_ERROR_DC_BUS_OVER_CURRENT = 0x00000010
|
||||
ODRIVE_ERROR_BRAKE_DEADTIME_VIOLATION = 0x00000020
|
||||
ODRIVE_ERROR_BRAKE_DUTY_CYCLE_NAN = 0x00000040
|
||||
ODRIVE_ERROR_INVALID_BRAKE_RESISTANCE = 0x00000080
|
||||
|
||||
# ODrive.Can.Error
|
||||
CAN_ERROR_NONE = 0x00000000
|
||||
|
||||
@@ -34,6 +34,8 @@ class TestMotorCalibration():
|
||||
axis_ctx.handle.motor.config.phase_inductance = 0.0
|
||||
axis_ctx.handle.motor.config.pre_calibrated = False
|
||||
axis_ctx.handle.config.enable_watchdog = False
|
||||
axis_ctx.parent.handle.config.brake_resistance = float(axis_ctx.parent.yaml['brake-resistance'])
|
||||
axis_ctx.parent.handle.config.enable_brake_resistor = True
|
||||
|
||||
axis_ctx.parent.handle.clear_errors()
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@ class TestClosedLoopControlBase():
|
||||
axis_ctx.handle.motor.config.phase_inductance = float(motor_ctx.yaml['phase-inductance'])
|
||||
axis_ctx.handle.motor.config.pre_calibrated = True
|
||||
|
||||
# Set brake resistor settings
|
||||
axis_ctx.parent.handle.config.brake_resistance = float(axis_ctx.parent.yaml['brake-resistance'])
|
||||
# The docs say this requires a reboot but here's a small secret:
|
||||
# Since the brake resistor is also started in clear_errors() this
|
||||
# circumvents the need for a reboot.
|
||||
axis_ctx.parent.handle.config.enable_brake_resistor = True
|
||||
|
||||
# Set calibration settings
|
||||
axis_ctx.handle.encoder.config.direction = 0
|
||||
axis_ctx.handle.encoder.config.use_index = False
|
||||
@@ -203,8 +210,10 @@ class TestRegenProtection(TestClosedLoopControlBase):
|
||||
time.sleep(1.0)
|
||||
test_assert_no_error(axis_ctx)
|
||||
|
||||
|
||||
logger.debug(f'Brake control test with brake resistor disabled')
|
||||
# once more, but this time without brake resistor
|
||||
axis_ctx.parent.handle.config.brake_resistance = 0
|
||||
axis_ctx.parent.handle.config.enable_brake_resistor = False
|
||||
# accelerate...
|
||||
axis_ctx.handle.controller.input_vel = nominal_rps
|
||||
time.sleep(1.0)
|
||||
@@ -218,6 +227,29 @@ class TestRegenProtection(TestClosedLoopControlBase):
|
||||
test_assert_eq(axis_ctx.handle.motor.error & MOTOR_ERROR_SYSTEM_LEVEL, MOTOR_ERROR_SYSTEM_LEVEL)
|
||||
test_assert_eq(axis_ctx.handle.error, 0)
|
||||
|
||||
# Do test again with wrong brake resistance setting
|
||||
logger.debug(f'Brake control test with brake resistor = 100')
|
||||
axis_ctx.parent.handle.clear_errors()
|
||||
time.sleep(1.0)
|
||||
axis_ctx.parent.handle.config.brake_resistance = 100
|
||||
axis_ctx.parent.handle.config.dc_max_negative_current = -0.5
|
||||
request_state(axis_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL)
|
||||
|
||||
# accelerate...
|
||||
axis_ctx.handle.controller.input_vel = nominal_rps
|
||||
time.sleep(1.0)
|
||||
test_assert_no_error(axis_ctx)
|
||||
|
||||
# ... and brake
|
||||
axis_ctx.handle.controller.input_vel = 0
|
||||
time.sleep(1.0) # expect DC_BUS_OVER_REGEN_CURRENT
|
||||
time.sleep(0.1)
|
||||
test_assert_eq(axis_ctx.parent.handle.error, ODRIVE_ERROR_DC_BUS_OVER_REGEN_CURRENT)
|
||||
test_assert_eq(axis_ctx.handle.motor.error & MOTOR_ERROR_SYSTEM_LEVEL, MOTOR_ERROR_SYSTEM_LEVEL)
|
||||
test_assert_eq(axis_ctx.handle.error, 0)
|
||||
|
||||
|
||||
|
||||
|
||||
class TestVelLimitInTorqueControl(TestClosedLoopControlBase):
|
||||
"""
|
||||
|
||||
@@ -106,6 +106,8 @@ class TestSimpleCANClosedLoop():
|
||||
# Make sure there are no funny configurations active
|
||||
logger.debug('Setting up clean configuration...')
|
||||
axis_ctx.parent.erase_config_and_reboot()
|
||||
axis_ctx.parent.handle.config.enable_brake_resistor = True
|
||||
axis_ctx.parent.save_config_and_reboot()
|
||||
|
||||
# run calibration
|
||||
axis_ctx.handle.requested_state = AXIS_STATE_FULL_CALIBRATION_SEQUENCE
|
||||
@@ -162,7 +164,7 @@ class TestSimpleCANClosedLoop():
|
||||
test_assert_eq(axis_ctx.handle.config.can.node_id, node_id+20)
|
||||
|
||||
# Reset node ID to default value
|
||||
asyncio.run(command(canbus.handle, node_id+20, extended_id, 'set_node_id', node_id=node_id))
|
||||
command(canbus.handle, node_id+20, extended_id, 'set_node_id', node_id=node_id)
|
||||
fence()
|
||||
test_assert_eq(axis_ctx.handle.config.can.node_id, node_id)
|
||||
|
||||
|
||||
+10
-8
@@ -7,7 +7,6 @@ import platform
|
||||
import subprocess
|
||||
import os
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from fibre.utils import Event
|
||||
import odrive.enums
|
||||
from odrive.enums import *
|
||||
@@ -49,6 +48,7 @@ def calculate_thermistor_coeffs(degree, Rload, R_25, Beta, Tmin, Tmax, plot = Fa
|
||||
fit_temps = p1(V)
|
||||
|
||||
if plot:
|
||||
import matplotlib.pyplot as plt
|
||||
print(fit)
|
||||
plt.plot(V, temps, label='actual')
|
||||
plt.plot(V, fit_temps, label='fit')
|
||||
@@ -547,17 +547,19 @@ def dump_dma(odrv):
|
||||
"*" if (status & 0x80000000) else " "))
|
||||
|
||||
def dump_timing(odrv, n_samples=100, path='/tmp/timings.png'):
|
||||
import matplotlib.pyplot as plt
|
||||
import re
|
||||
|
||||
timings = []
|
||||
|
||||
for attr in dir(odrv.task_times):
|
||||
if not attr.startswith('_'):
|
||||
timings.append((attr, getattr(odrv.task_times, attr), [], [])) # (name, obj, start_times, lengths)
|
||||
for attr in dir(odrv.axis0.task_times):
|
||||
if not attr.startswith('_'):
|
||||
timings.append(('axis0.' + attr, getattr(odrv.axis0.task_times, attr), [], [])) # (name, obj, start_times, lengths)
|
||||
for attr in dir(odrv.axis1.task_times):
|
||||
if not attr.startswith('_'):
|
||||
timings.append(('axis1.' + attr, getattr(odrv.axis1.task_times, attr), [], [])) # (name, obj, start_times, lengths)
|
||||
for k in dir(odrv):
|
||||
if re.match(r'axis[0-9]+', k):
|
||||
for attr in dir(getattr(odrv, k).task_times):
|
||||
if not attr.startswith('_'):
|
||||
timings.append((k + '.' + attr, getattr(getattr(odrv, k).task_times, attr), [], [])) # (name, obj, start_times, lengths)
|
||||
|
||||
# Take a couple of samples
|
||||
print("sampling...")
|
||||
@@ -574,7 +576,7 @@ def dump_timing(odrv, n_samples=100, path='/tmp/timings.png'):
|
||||
|
||||
plt.rcParams['figure.figsize'] = 21, 9
|
||||
plt.figure()
|
||||
plt.grid('both')
|
||||
plt.grid(True)
|
||||
plt.barh(
|
||||
[-i for i in range(len(timings))], # y positions
|
||||
[np.mean(lengths) for name, obj, start_times, lengths in timings], # lengths
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
components:
|
||||
- type: generalpurpose
|
||||
name: homenet
|
||||
net: homenet
|
||||
|
||||
- type: generalpurpose
|
||||
name: rpi
|
||||
ssh: odrv
|
||||
net: homenet
|
||||
components:
|
||||
- type: uart
|
||||
name: uart0
|
||||
port: /dev/ttyS0
|
||||
connected-to: main_uart
|
||||
- type: can
|
||||
name: can0
|
||||
interface: can0
|
||||
connected-to: odrive.can
|
||||
# need to specify GPIOs explicitly for the generalpurpose type
|
||||
- {type: gpio, num: 16}
|
||||
- {type: gpio, num: 19}
|
||||
- {type: gpio, num: 20}
|
||||
- {type: gpio, num: 26}
|
||||
|
||||
# - type: programmer
|
||||
# name: The Blue STLink/v2
|
||||
# id: '\x53\x3f\x75\x06\x49\x3f\x49\x51\x44\x54\x19\x3f'
|
||||
|
||||
- type: odrive
|
||||
name: odrive
|
||||
board-version: v3.6-58V
|
||||
serial-number: "2061398A4D4D"
|
||||
brake-resistance: 0.47
|
||||
usb: auto
|
||||
can: main_canbus
|
||||
vbus-voltage: 24 # [V]
|
||||
max-brake-power: 150 # [W]
|
||||
encoder0: virtual_encoder0
|
||||
encoder1: virtual_encoder1
|
||||
motor0: D5065-270KV_0
|
||||
motor1: floating
|
||||
|
||||
- type: motor
|
||||
name: D5065-270KV_0
|
||||
phase-resistance: 0.039
|
||||
phase-inductance: 1.57e-05
|
||||
pole-pairs: 7
|
||||
direction: 1
|
||||
kv: 270
|
||||
max-current: 70
|
||||
max-voltage: 40
|
||||
|
||||
- type: encoder
|
||||
name: real_encoder
|
||||
cpr: 8192
|
||||
max-rpm: 7000
|
||||
|
||||
- type: teensy
|
||||
name: teensy
|
||||
|
||||
- {type: lpf, name: lpf0}
|
||||
- {type: lpf, name: lpf1}
|
||||
|
||||
connections:
|
||||
- ['odrive.can', 'rpi.can0']
|
||||
- ['teensy.program', 'rpi.gpio26']
|
||||
- ['teensy.gpio12', 'rpi.uart0.tx']
|
||||
- ['teensy.gpio13', 'rpi.uart0.rx']
|
||||
- ['teensy.gpio11', 'odrive.gpio1']
|
||||
- ['teensy.gpio10', 'odrive.gpio2']
|
||||
- ['teensy.gpio9', 'odrive.gpio3']
|
||||
- ['teensy.gpio8', 'odrive.gpio4']
|
||||
- ['teensy.gpio14', 'odrive.gpio5']
|
||||
- ['teensy.gpio15', 'odrive.gpio6']
|
||||
- ['teensy.gpio16', 'odrive.gpio7']
|
||||
- ['teensy.gpio17', 'odrive.gpio8']
|
||||
- ['teensy.gpio6', 'rpi.gpio20']
|
||||
- ['teensy.gpio7', 'rpi.gpio19']
|
||||
- ['teensy.gpio23', 'odrive.encoder0.z']
|
||||
- ['teensy.gpio22', 'odrive.encoder0.b']
|
||||
- ['teensy.gpio21', 'odrive.encoder0.a']
|
||||
- ['teensy.gpio20', 'odrive.encoder1.z']
|
||||
- ['teensy.gpio19', 'odrive.encoder1.b']
|
||||
- ['teensy.gpio18', 'odrive.encoder1.a']
|
||||
- ['teensy.gpio0', 'real_encoder.z']
|
||||
- ['teensy.gpio1', 'real_encoder.a']
|
||||
- ['teensy.gpio2', 'real_encoder.b']
|
||||
- ['teensy.gpio3', 'odrive.spi.mosi']
|
||||
- ['teensy.gpio4', 'odrive.spi.miso']
|
||||
- ['teensy.gpio5', 'odrive.spi.sck']
|
||||
- ['odrive.axis0', 'D5065-270KV_0']
|
||||
- ['D5065-270KV_0', 'real_encoder']
|
||||
- ['odrive.gpio3', 'lpf0']
|
||||
- ['odrive.gpio4', 'lpf1']
|
||||
- ['lpf0.en', 'lpf1.en', 'rpi.gpio16']
|
||||
@@ -31,7 +31,7 @@ components:
|
||||
name: odrive
|
||||
board-version: v3.6-58V
|
||||
serial-number: "20703595524B"
|
||||
brake-resistance: 0.47
|
||||
brake-resistance: 2.0
|
||||
usb: auto
|
||||
can: main_canbus
|
||||
vbus-voltage: 24 # [V]
|
||||
|
||||
Reference in New Issue
Block a user