Merge pull request #601 from odriverobotics/fw-v0.5.3

Firmware v0.5.3
This commit is contained in:
samuelsadok
2021-09-03 17:23:30 +02:00
committed by GitHub
60 changed files with 1390 additions and 375 deletions
+11 -1
View File
@@ -5,7 +5,7 @@ on:
branches: [master, devel]
tags: ['fw-v*']
push:
branches: [master, devel]
branches: [master, devel, 'fw-v*']
tags: ['fw-v*']
jobs:
@@ -112,6 +112,16 @@ jobs:
mv tup_build.sh tup_build.bat # in reality this is a .bat script on windows
.\tup_build.bat
- name: Upload binary
if: ${{ matrix.os == 'ubuntu-latest' && matrix.debug == false }}
uses: actions/upload-artifact@v2
with:
name: firmware-${{ matrix.board_version }}
path: |
Firmware/build/ODriveFirmware.elf
Firmware/build/ODriveFirmware.bin
Firmware/build/ODriveFirmware.hex
code-checks:
strategy:
fail-fast: false
-4
View File
@@ -1,4 +0,0 @@
[submodule "Firmware/Private"]
path = Firmware/Private
url = git@github.com:madcowswe/ODrivePrivate.git
branch = submodule
+7 -2
View File
@@ -42,7 +42,7 @@ void ODriveArduino::SetCurrent(int motor_number, float current) {
serial_ << "c " << motor_number << " " << current << "\n";
}
void ODriveArduino::TrapezoidalMove(int motor_number, float position){
void ODriveArduino::TrapezoidalMove(int motor_number, float position) {
serial_ << "t " << motor_number << " " << position << "\n";
}
@@ -50,11 +50,16 @@ float ODriveArduino::readFloat() {
return readString().toFloat();
}
float ODriveArduino::GetVelocity(int motor_number){
float ODriveArduino::GetVelocity(int motor_number) {
serial_<< "r axis" << motor_number << ".encoder.vel_estimate\n";
return ODriveArduino::readFloat();
}
float ODriveArduino::GetPosition(int motor_number) {
serial_ << "r axis" << motor_number << ".encoder.pos_estimate\n";
return ODriveArduino::readFloat();
}
int32_t ODriveArduino::readInt() {
return readString().toInt();
}
+2 -12
View File
@@ -3,21 +3,10 @@
#define ODriveArduino_h
#include "Arduino.h"
#include "ODriveEnums.h"
class ODriveArduino {
public:
enum AxisState_t {
AXIS_STATE_UNDEFINED = 0, //<! will fall through to idle
AXIS_STATE_IDLE = 1, //<! disable PWM and do nothing
AXIS_STATE_STARTUP_SEQUENCE = 2, //<! the actual sequence is defined by the config.startup_... flags
AXIS_STATE_FULL_CALIBRATION_SEQUENCE = 3, //<! run all calibration procedures, then idle
AXIS_STATE_MOTOR_CALIBRATION = 4, //<! run motor calibration
AXIS_STATE_SENSORLESS_CONTROL = 5, //<! run sensorless control
AXIS_STATE_ENCODER_INDEX_SEARCH = 6, //<! run encoder index search
AXIS_STATE_ENCODER_OFFSET_CALIBRATION = 7, //<! run encoder offset calibration
AXIS_STATE_CLOSED_LOOP_CONTROL = 8 //<! run closed loop control
};
ODriveArduino(Stream& serial);
// Commands
@@ -30,6 +19,7 @@ public:
void TrapezoidalMove(int motor_number, float position);
// Getters
float GetVelocity(int motor_number);
float GetPosition(int motor_number);
// General params
float readFloat();
int32_t readInt();
+199
View File
@@ -0,0 +1,199 @@
#ifndef ODriveEnums_h
#define ODriveEnums_h
/* TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON.
** To regenerate this file, nagivate to the top level of the ODrive repository and run:
** python Firmware/interface_generator_stub.py --definitions Firmware/odrive-interface.yaml --template tools/arduino_enums_template.j2 --output Arduino/ODriveArduino/ODriveEnums.h
*/
// ODrive.GpioMode
enum GpioMode {
GPIO_MODE_DIGITAL = 0,
GPIO_MODE_DIGITAL_PULL_UP = 1,
GPIO_MODE_DIGITAL_PULL_DOWN = 2,
GPIO_MODE_ANALOG_IN = 3,
GPIO_MODE_UART_A = 4,
GPIO_MODE_UART_B = 5,
GPIO_MODE_UART_C = 6,
GPIO_MODE_CAN_A = 7,
GPIO_MODE_I2C_A = 8,
GPIO_MODE_SPI_A = 9,
GPIO_MODE_PWM = 10,
GPIO_MODE_ENC0 = 11,
GPIO_MODE_ENC1 = 12,
GPIO_MODE_ENC2 = 13,
GPIO_MODE_MECH_BRAKE = 14,
GPIO_MODE_STATUS = 15,
};
// ODrive.StreamProtocolType
enum StreamProtocolType {
STREAM_PROTOCOL_TYPE_FIBRE = 0,
STREAM_PROTOCOL_TYPE_ASCII = 1,
STREAM_PROTOCOL_TYPE_STDOUT = 2,
STREAM_PROTOCOL_TYPE_ASCII_AND_STDOUT = 3,
};
// ODrive.Can.Protocol
enum Protocol {
PROTOCOL_SIMPLE = 0x00000001,
};
// ODrive.Axis.AxisState
enum AxisState {
AXIS_STATE_UNDEFINED = 0,
AXIS_STATE_IDLE = 1,
AXIS_STATE_STARTUP_SEQUENCE = 2,
AXIS_STATE_FULL_CALIBRATION_SEQUENCE = 3,
AXIS_STATE_MOTOR_CALIBRATION = 4,
AXIS_STATE_ENCODER_INDEX_SEARCH = 6,
AXIS_STATE_ENCODER_OFFSET_CALIBRATION = 7,
AXIS_STATE_CLOSED_LOOP_CONTROL = 8,
AXIS_STATE_LOCKIN_SPIN = 9,
AXIS_STATE_ENCODER_DIR_FIND = 10,
AXIS_STATE_HOMING = 11,
AXIS_STATE_ENCODER_HALL_POLARITY_CALIBRATION = 12,
AXIS_STATE_ENCODER_HALL_PHASE_CALIBRATION = 13,
};
// ODrive.Encoder.Mode
enum EncoderMode {
ENCODER_MODE_INCREMENTAL = 0,
ENCODER_MODE_HALL = 1,
ENCODER_MODE_SINCOS = 2,
ENCODER_MODE_SPI_ABS_CUI = 256,
ENCODER_MODE_SPI_ABS_AMS = 257,
ENCODER_MODE_SPI_ABS_AEAT = 258,
ENCODER_MODE_SPI_ABS_RLS = 259,
ENCODER_MODE_SPI_ABS_MA732 = 260,
};
// ODrive.Controller.ControlMode
enum ControlMode {
CONTROL_MODE_VOLTAGE_CONTROL = 0,
CONTROL_MODE_TORQUE_CONTROL = 1,
CONTROL_MODE_VELOCITY_CONTROL = 2,
CONTROL_MODE_POSITION_CONTROL = 3,
};
// ODrive.Controller.InputMode
enum InputMode {
INPUT_MODE_INACTIVE = 0,
INPUT_MODE_PASSTHROUGH = 1,
INPUT_MODE_VEL_RAMP = 2,
INPUT_MODE_POS_FILTER = 3,
INPUT_MODE_MIX_CHANNELS = 4,
INPUT_MODE_TRAP_TRAJ = 5,
INPUT_MODE_TORQUE_RAMP = 6,
INPUT_MODE_MIRROR = 7,
INPUT_MODE_TUNING = 8,
};
// ODrive.Motor.MotorType
enum MotorType {
MOTOR_TYPE_HIGH_CURRENT = 0,
MOTOR_TYPE_GIMBAL = 2,
MOTOR_TYPE_ACIM = 3,
};
// ODrive.Error
enum ODriveError {
ODRIVE_ERROR_NONE = 0x00000000,
ODRIVE_ERROR_CONTROL_ITERATION_MISSED = 0x00000001,
ODRIVE_ERROR_DC_BUS_UNDER_VOLTAGE = 0x00000002,
ODRIVE_ERROR_DC_BUS_OVER_VOLTAGE = 0x00000004,
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
enum CanError {
CAN_ERROR_NONE = 0x00000000,
CAN_ERROR_DUPLICATE_CAN_IDS = 0x00000001,
};
// ODrive.Axis.Error
enum AxisError {
AXIS_ERROR_NONE = 0x00000000,
AXIS_ERROR_INVALID_STATE = 0x00000001,
AXIS_ERROR_WATCHDOG_TIMER_EXPIRED = 0x00000800,
AXIS_ERROR_MIN_ENDSTOP_PRESSED = 0x00001000,
AXIS_ERROR_MAX_ENDSTOP_PRESSED = 0x00002000,
AXIS_ERROR_ESTOP_REQUESTED = 0x00004000,
AXIS_ERROR_HOMING_WITHOUT_ENDSTOP = 0x00020000,
AXIS_ERROR_OVER_TEMP = 0x00040000,
AXIS_ERROR_UNKNOWN_POSITION = 0x00080000,
};
// ODrive.Motor.Error
enum MotorError {
MOTOR_ERROR_NONE = 0x00000000,
MOTOR_ERROR_PHASE_RESISTANCE_OUT_OF_RANGE = 0x00000001,
MOTOR_ERROR_PHASE_INDUCTANCE_OUT_OF_RANGE = 0x00000002,
MOTOR_ERROR_DRV_FAULT = 0x00000008,
MOTOR_ERROR_CONTROL_DEADLINE_MISSED = 0x00000010,
MOTOR_ERROR_MODULATION_MAGNITUDE = 0x00000080,
MOTOR_ERROR_CURRENT_SENSE_SATURATION = 0x00000400,
MOTOR_ERROR_CURRENT_LIMIT_VIOLATION = 0x00001000,
MOTOR_ERROR_MODULATION_IS_NAN = 0x00010000,
MOTOR_ERROR_MOTOR_THERMISTOR_OVER_TEMP = 0x00020000,
MOTOR_ERROR_FET_THERMISTOR_OVER_TEMP = 0x00040000,
MOTOR_ERROR_TIMER_UPDATE_MISSED = 0x00080000,
MOTOR_ERROR_CURRENT_MEASUREMENT_UNAVAILABLE = 0x00100000,
MOTOR_ERROR_CONTROLLER_FAILED = 0x00200000,
MOTOR_ERROR_I_BUS_OUT_OF_RANGE = 0x00400000,
MOTOR_ERROR_BRAKE_RESISTOR_DISARMED = 0x00800000,
MOTOR_ERROR_SYSTEM_LEVEL = 0x01000000,
MOTOR_ERROR_BAD_TIMING = 0x02000000,
MOTOR_ERROR_UNKNOWN_PHASE_ESTIMATE = 0x04000000,
MOTOR_ERROR_UNKNOWN_PHASE_VEL = 0x08000000,
MOTOR_ERROR_UNKNOWN_TORQUE = 0x10000000,
MOTOR_ERROR_UNKNOWN_CURRENT_COMMAND = 0x20000000,
MOTOR_ERROR_UNKNOWN_CURRENT_MEASUREMENT = 0x40000000,
MOTOR_ERROR_UNKNOWN_VBUS_VOLTAGE = 0x80000000,
MOTOR_ERROR_UNKNOWN_VOLTAGE_COMMAND = 0x100000000,
MOTOR_ERROR_UNKNOWN_GAINS = 0x200000000,
MOTOR_ERROR_CONTROLLER_INITIALIZING = 0x400000000,
MOTOR_ERROR_UNBALANCED_PHASES = 0x800000000,
};
// ODrive.Controller.Error
enum ControllerError {
CONTROLLER_ERROR_NONE = 0x00000000,
CONTROLLER_ERROR_OVERSPEED = 0x00000001,
CONTROLLER_ERROR_INVALID_INPUT_MODE = 0x00000002,
CONTROLLER_ERROR_UNSTABLE_GAIN = 0x00000004,
CONTROLLER_ERROR_INVALID_MIRROR_AXIS = 0x00000008,
CONTROLLER_ERROR_INVALID_LOAD_ENCODER = 0x00000010,
CONTROLLER_ERROR_INVALID_ESTIMATE = 0x00000020,
CONTROLLER_ERROR_INVALID_CIRCULAR_RANGE = 0x00000040,
CONTROLLER_ERROR_SPINOUT_DETECTED = 0x00000080,
};
// ODrive.Encoder.Error
enum EncoderError {
ENCODER_ERROR_NONE = 0x00000000,
ENCODER_ERROR_UNSTABLE_GAIN = 0x00000001,
ENCODER_ERROR_CPR_POLEPAIRS_MISMATCH = 0x00000002,
ENCODER_ERROR_NO_RESPONSE = 0x00000004,
ENCODER_ERROR_UNSUPPORTED_ENCODER_MODE = 0x00000008,
ENCODER_ERROR_ILLEGAL_HALL_STATE = 0x00000010,
ENCODER_ERROR_INDEX_NOT_FOUND_YET = 0x00000020,
ENCODER_ERROR_ABS_SPI_TIMEOUT = 0x00000040,
ENCODER_ERROR_ABS_SPI_COM_FAIL = 0x00000080,
ENCODER_ERROR_ABS_SPI_NOT_READY = 0x00000100,
ENCODER_ERROR_HALL_NOT_CALIBRATED_YET = 0x00000200,
};
// ODrive.SensorlessEstimator.Error
enum SensorlessEstimatorError {
SENSORLESS_ESTIMATOR_ERROR_NONE = 0x00000000,
SENSORLESS_ESTIMATOR_ERROR_UNSTABLE_GAIN = 0x00000001,
SENSORLESS_ESTIMATOR_ERROR_UNKNOWN_CURRENT_MEASUREMENT = 0x00000002,
};
#endif
@@ -75,15 +75,15 @@ void loop() {
int motornum = c-'0';
int requested_state;
requested_state = ODriveArduino::AXIS_STATE_MOTOR_CALIBRATION;
requested_state = AXIS_STATE_MOTOR_CALIBRATION;
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
if(!odrive.run_state(motornum, requested_state, true)) return;
requested_state = ODriveArduino::AXIS_STATE_ENCODER_OFFSET_CALIBRATION;
requested_state = AXIS_STATE_ENCODER_OFFSET_CALIBRATION;
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
if(!odrive.run_state(motornum, requested_state, true, 25.0f)) return;
requested_state = ODriveArduino::AXIS_STATE_CLOSED_LOOP_CONTROL;
requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL;
Serial << "Axis" << c << ": Requesting state " << requested_state << '\n';
if(!odrive.run_state(motornum, requested_state, false /*don't wait*/)) return;
}
@@ -112,8 +112,7 @@ void loop() {
unsigned long start = millis();
while(millis() - start < duration) {
for (int motor = 0; motor < 2; ++motor) {
odrive_serial << "r axis" << motor << ".encoder.pos_estimate\n";
Serial << odrive.readFloat() << '\t';
Serial << odrive.GetPosition(motor) << '\t';
}
Serial << '\n';
}
+24
View File
@@ -1,6 +1,25 @@
# Unreleased Features
Please add a note of your changes below this heading if you make a Pull Request.
# Releases
## [0.5.3] - 2021-09-03
### Fixed
* ASCII protocol commands with multiline responses (`i`, `h`) now return the expected response (in v0.5.2 the response was corrupted)
* odrivetool no longer shows the message `<Task pending coro=... running at ...>` when closing
* Homing used to erroneously complete with `is_homed == True` even if it failed for some reason
* When entering closed loop control in trapezoidal trajectory mode the axis no longer snaps to the 0 position
* Fix python DFU firmware version prerelease status resolution to use correct attribute
* Fixed firmware compiled-in version number
### Added
* `brake_resistor_current` added to interface for reading the commanded brake resistor current
### Changed
* Removed `odrivetool generate-code`. This feature was broken in 0.5.2. Use [`interface_generator.py`](https://github.com/odriverobotics/ODrive/blob/master/tools/fibre-tools/interface_generator.py) instead (see Tupfile.lua for examples).
* Firmware boots on devices with unset OTP.
* Changed CAN heartbeat message to include "trajectory done" flag
# Releases
## [0.5.2] - 2021-05-21
@@ -27,6 +46,8 @@ Please add a note of your changes below this heading if you make a Pull Request.
* Added torque mirroring to INPUT_MODE_MIRROR
* `mechanical_power_bandwidth`, `electrical_power_bandwidth`, `spinout_electrical_power_threshold`, `spinout_mechanical_power_threshold` added to `controller.config` for spinout detection.
* `mechanical_power` and `electrical_power` added to `controller`.
* Added autogenerated enums header file [ODriveEnums.h](../Arduino/ODriveArduino/ODriveEnums.h) for Arduino use. Created Jinja template and edited Makefile to autogenerate it. Reflected change in Dockerfile and added note in developer-guide markdown file for updating ODriveEnums.h alongside enums.py.
* Added GetPosition member function in ODriveArduino class to complement existing GetVelocity, SetVelocity, and SetPosition functions.
### Changed
* Step/dir performance improved! Dual axis step rates up to 250kHz have been tested
@@ -55,6 +76,9 @@ Please add a note of your changes below this heading if you make a Pull Request.
* Added `torque_mirror_ratio` and use it to feed-forward `controller_.torque_output` in `INPUT_MODE_MIRROR`
* Accumulate integer steps in step/dir to avoid float precision errors
* Circular setpoint mode must be enabled when the step/dir interface is used.
* Replaced inline enum in ODriveArduino class by including new autogenerated ODriveEnums.h header file.
* Changed the example ODriveArduinoTest.ino file to reflect the new GetPosition member function. Also removed the scope resolution operator to access the enums as it can now be accessed from the global namespace.
* `save_configuration()` reboots the board.
### API Migration Notes
* `axis.config.turns_per_step` changed to `axis.controller.config.steps_per_circular_range`
+4
View File
@@ -22,6 +22,10 @@ CMD \
--definitions odrive-interface.yaml \
--template ../tools/enums_template.j2 \
--output ../tools/odrive/enums.py && \
python interface_generator_stub.py \
--definitions odrive-interface.yaml \
--template ../tools/arduino_enums_template.j2 \
--output ../Arduino/ODriveArduino/ODriveEnums.h && \
# Hack around Tup's dependency on FUSE
tup init && \
tup generate build.sh && \
+50 -25
View File
@@ -1,31 +1,32 @@
{
"configurations": [
{
"name": "Win32",
"name": "ODrive v3.6 Windows",
"includePath": [
"${workspaceFolder}/Private/**",
"${workspaceFolder}/**"
"${workspaceFolder}/**",
"${workspaceFolder}/ThirdParty/FreeRTOS/Source/portable/GCC/ARM_CM4F",
"${workspaceFolder}/fibre-cpp/include"
],
"compilerPath": "${ARM_GCC_ROOT}/bin/arm-none-eabi-g++.exe",
"intelliSenseMode": "gcc-arm",
"defines": [
"__arm__",
"STM32F722xx",
"FPU_FPV5",
"STM32F405xx",
"FPU_FPV4",
"USE_HAL_DRIVER",
"HW_VERSION_MAJOR=4",
"HW_VERSION_MINOR=1",
"HW_VERSION_VOLTAGE=58",
"HW_VERSION_MAJOR=3",
"HW_VERSION_MINOR=6",
"HW_VERSION_VOLTAGE=56",
"FIBRE_ENABLE_SERVER",
"FIBRE_ENABLE_CLIENT",
"__weak=\"__attribute__((weak))\"",
"__packed=\"__attribute__((__packed__))\"",
"__GNUC__"
],
"intelliSenseMode": "gcc-arm",
"compilerPath": "arm-none-eabi-g++.exe",
"compilerArgs": [
"-mthumb",
"-mcpu=cortex-m7",
"-mfpu=fpv5-sp-d16",
"-mcpu=cortex-m4",
"-mfpu=fpv4-sp-d16",
"-mfloat-abi=hard",
"-specs=nosys.specs",
"-specs=nano.specs",
@@ -36,49 +37,73 @@
"cppStandard": "c++17"
},
{
"name": "Linux",
"name": "ODrive 3.6 Linux",
"includePath": [
"${workspaceFolder}/**"
"${workspaceFolder}/**",
"${workspaceFolder}/ThirdParty/FreeRTOS/Source/portable/GCC/ARM_CM4F",
"${workspaceFolder}/fibre-cpp/include"
],
"compilerPath": "arm-none-eabi-g++",
"intelliSenseMode": "gcc-arm",
"defines": [
"__arm__",
"STM32F405xx",
"FPU_FPV4",
"USE_HAL_DRIVER",
"HW_VERSION_MAJOR=4",
"HW_VERSION_MINOR=1",
"HW_VERSION_VOLTAGE=58",
"HW_VERSION_MAJOR=3",
"HW_VERSION_MINOR=6",
"HW_VERSION_VOLTAGE=56",
"FIBRE_ENABLE_SERVER",
"FIBRE_ENABLE_CLIENT",
"__weak=\"__attribute__((weak))\"",
"__packed=\"__attribute__((__packed__))\"",
"__GNUC__"
],
"intelliSenseMode": "gcc-arm",
"compilerPath": "arm-none-eabi-g++ -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -specs=nosys.specs -specs=nano.specs -u _printf_float -u _scanf_float",
"compilerArgs": [
"-mthumb",
"-mcpu=cortex-m4",
"-mfpu=fpv4-sp-d16",
"-mfloat-abi=hard",
"-specs=nosys.specs",
"-specs=nano.specs",
"-u _printf_float",
"-u _scanf_float"
],
"cStandard": "c11",
"cppStandard": "c++17"
},
{
"name": "Mac",
"name": "ODrive 3.6 Mac",
"includePath": [
"${workspaceFolder}/**"
"${workspaceFolder}/**",
"${workspaceFolder}/ThirdParty/FreeRTOS/Source/portable/GCC/ARM_CM4F",
"${workspaceFolder}/fibre-cpp/include"
],
"intelliSenseMode": "gcc-arm",
"defines": [
"__arm__",
"STM32F405xx",
"FPU_FPV4",
"USE_HAL_DRIVER",
"HW_VERSION_MAJOR=4",
"HW_VERSION_MINOR=1",
"HW_VERSION_VOLTAGE=58",
"HW_VERSION_MAJOR=3",
"HW_VERSION_MINOR=6",
"HW_VERSION_VOLTAGE=56",
"FIBRE_ENABLE_SERVER",
"FIBRE_ENABLE_CLIENT",
"__weak=\"__attribute__((weak))\"",
"__packed=\"__attribute__((__packed__))\"",
"__GNUC__"
],
"intelliSenseMode": "gcc-arm",
"compilerArgs": [
"-mthumb",
"-mcpu=cortex-m4",
"-mfpu=fpv4-sp-d16",
"-mfloat-abi=hard",
"-specs=nosys.specs",
"-specs=nano.specs",
"-u _printf_float",
"-u _scanf_float"
],
"cStandard": "c11",
"cppStandard": "c++17"
}
+1 -1
View File
@@ -27,7 +27,7 @@ extern "C" void SystemClock_Config(void); // defined in main.c generated by Cube
// used during manufacturing to test the struct that will go to the OTP before
// _actually_ putting anything into OTP. This avoids bulk-destroying STM32's if
// we introduce unintended breakage in our manufacturing scripts.
uint8_t __attribute__((section(".testdata"))) fake_otp[FLASH_OTP_END + 1 - FLASH_OTP_BASE];
uint8_t __attribute__((section(".testdata"))) fake_otp[FLASH_OTP_END + 1 - FLASH_OTP_BASE] = {0, 0, 0, HW_VERSION_MAJOR, HW_VERSION_MINOR, HW_VERSION_VOLTAGE};
Stm32SpiArbiter spi3_arbiter{&hspi3};
Stm32SpiArbiter& ext_spi_arbiter = spi3_arbiter;
+1 -1
View File
@@ -16,7 +16,7 @@ const Stm32Gpio Stm32Gpio::none{nullptr, 0};
* Note that all GPIOs with the same pin number map to the same IRQn,
* no matter which port they belong to.
*/
IRQn_Type get_irq_number(uint16_t pin_number) {
static inline IRQn_Type get_irq_number(uint16_t pin_number) {
switch (pin_number) {
case 0: return EXTI0_IRQn;
case 1: return EXTI1_IRQn;
+1
View File
@@ -32,6 +32,7 @@ all:
@$(PY_CMD) ../tools/odrive/version.py --output autogen/version.c
@tup --quiet -no-environ-check
@$(PY_CMD) interface_generator_stub.py --definitions odrive-interface.yaml --template ../tools/enums_template.j2 --output ../tools/odrive/enums.py
@$(PY_CMD) interface_generator_stub.py --definitions odrive-interface.yaml --template ../tools/arduino_enums_template.j2 --output ../Arduino/ODriveArduino/ODriveEnums.h
# Copy libfibre files to odrivetool if they were built
@ ! test -f "fibre-cpp/build/libfibre-linux-amd64.so" || cp fibre-cpp/build/libfibre-linux-amd64.so ../tools/odrive/pyfibre/fibre/
+14 -3
View File
@@ -287,7 +287,6 @@ bool Axis::start_closed_loop_control() {
}
// To avoid any transient on startup, we intialize the setpoint to be the current position
// note - input_pos_ is not set here. It is set to 0 earlier in this method and velocity control is used.
if (controller_.config_.control_mode >= Controller::CONTROL_MODE_POSITION_CONTROL) {
std::optional<float> pos_init = (controller_.config_.circular_setpoints ?
controller_.pos_estimate_circular_src_ :
@@ -386,10 +385,14 @@ bool Axis::run_homing() {
homing_.is_homed = false;
error_ &= ~ERROR_MIN_ENDSTOP_PRESSED;
bool done = false;
start_closed_loop_control();
// Driving toward the endstop
while ((requested_state_ == AXIS_STATE_UNDEFINED) && motor_.is_armed_ && !min_endstop_.get_state()) {
while ((requested_state_ == AXIS_STATE_UNDEFINED) && motor_.is_armed_ && !(done = min_endstop_.get_state())) {
osDelay(1);
}
@@ -397,6 +400,10 @@ bool Axis::run_homing() {
controller_.input_vel_ = 0.0f;
if (!done) {
return false;
}
error_ &= ~ERROR_MIN_ENDSTOP_PRESSED; // clear this error since we deliberately drove into the endstop
std::optional<float> pos_estimate_local = encoder_.pos_estimate_.any();
@@ -415,12 +422,16 @@ bool Axis::run_homing() {
controller_.vel_setpoint_ = 0.0f;
controller_.input_pos_updated();
while ((requested_state_ == AXIS_STATE_UNDEFINED) && motor_.is_armed_ && !controller_.trajectory_done_) {
while ((requested_state_ == AXIS_STATE_UNDEFINED) && motor_.is_armed_ && !(done = controller_.trajectory_done_)) {
osDelay(1);
}
stop_closed_loop_control();
if (!done) {
return false;
}
// Set the current position to 0.
encoder_.set_linear_count(0);
controller_.input_pos_ = 0;
+7 -5
View File
@@ -9,7 +9,7 @@ bool Controller::apply_config() {
}
void Controller::reset() {
pos_setpoint_ = 0.0f;
// pos_setpoint is initialized in start_closed_loop_control
vel_setpoint_ = 0.0f;
vel_integrator_torque_ = 0.0f;
torque_setpoint_ = 0.0f;
@@ -224,9 +224,11 @@ bool Controller::update() {
} break;
case INPUT_MODE_TUNING: {
autotuning_phase_ = wrap_pm_pi(autotuning_phase_ + (2.0f * M_PI * autotuning_.frequency * current_meas_period));
pos_setpoint_ = autotuning_.pos_amplitude * our_arm_sin_f32(autotuning_phase_ + autotuning_.pos_phase);
vel_setpoint_ = autotuning_.vel_amplitude * our_arm_sin_f32(autotuning_phase_ + autotuning_.vel_phase);
torque_setpoint_ = autotuning_.torque_amplitude * our_arm_sin_f32(autotuning_phase_ + autotuning_.torque_phase);
float c = our_arm_cos_f32(autotuning_phase_);
float s = our_arm_sin_f32(autotuning_phase_);
pos_setpoint_ = autotuning_.pos_amplitude * s; // + pos_amp_c * c
vel_setpoint_ = autotuning_.vel_amplitude * c;
torque_setpoint_ = autotuning_.torque_amplitude * -s;
} break;
default: {
set_error(ERROR_INVALID_INPUT_MODE);
@@ -331,7 +333,7 @@ bool Controller::update() {
}
// Velocity limiting in current mode
if (config_.control_mode < CONTROL_MODE_VELOCITY_CONTROL && config_.enable_current_mode_vel_limit) {
if (config_.control_mode < CONTROL_MODE_VELOCITY_CONTROL && config_.enable_torque_mode_vel_limit) {
if (!vel_estimate.has_value()) {
set_error(ERROR_INVALID_ESTIMATE);
return false;
+1 -5
View File
@@ -17,11 +17,8 @@ public:
struct Autotuning_t {
float frequency = 0.0f;
float pos_amplitude = 0.0f;
float pos_phase = 0.0f;
float vel_amplitude = 0.0f;
float vel_phase = 0.0f;
float torque_amplitude = 0.0f;
float torque_phase = 0.0f;
};
struct Config_t {
@@ -46,7 +43,7 @@ public:
bool enable_gain_scheduling = false;
bool enable_vel_limit = true;
bool enable_overspeed_error = true;
bool enable_current_mode_vel_limit = true; // enable velocity limit in current control mode (requires a valid velocity estimator)
bool enable_torque_mode_vel_limit = true; // enable velocity limit in current control mode (requires a valid velocity estimator)
uint8_t axis_to_mirror = -1;
float mirror_ratio = 1.0f;
float torque_mirror_ratio = 0.0f;
@@ -62,7 +59,6 @@ public:
void set_steps_per_circular_range(uint32_t value) { steps_per_circular_range = value > 0 ? value : steps_per_circular_range; }
};
Controller() {}
bool apply_config();
+6 -3
View File
@@ -33,6 +33,7 @@ float vbus_voltage = 12.0f;
float ibus_ = 0.0f; // exposed for monitoring only
bool brake_resistor_armed = false;
bool brake_resistor_saturated = false;
float brake_resistor_current = 0.0f;
/* Private constant data -----------------------------------------------------*/
/* CPU critical section helpers ----------------------------------------------*/
@@ -324,8 +325,8 @@ void update_brake_current() {
}
}
float brake_duty;
float brake_duty = 0.0f;
float brake_current = 0.0f;
if (odrv.config_.enable_brake_resistor) {
if (!(odrv.config_.brake_resistance > 0.0f)) {
odrv.disarm_with_error(ODrive::ERROR_INVALID_BRAKE_RESISTANCE);
@@ -333,7 +334,7 @@ void update_brake_current() {
}
// Don't start braking until -Ibus > regen_current_allowed
float brake_current = -Ibus_sum - odrv.config_.max_regen_current;
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)) {
@@ -355,11 +356,13 @@ void update_brake_current() {
// This cannot result in NaN (safe for race conditions) because we check
// brake_resistance != 0 further up.
brake_current = brake_duty * vbus_voltage / odrv.config_.brake_resistance;
Ibus_sum += brake_duty * vbus_voltage / odrv.config_.brake_resistance;
} else {
brake_duty = 0;
}
brake_resistor_current = brake_current;
ibus_ += odrv.ibus_report_filter_k_ * (Ibus_sum - ibus_);
if (Ibus_sum > odrv.config_.dc_max_positive_current) {
+1
View File
@@ -21,6 +21,7 @@ extern float vbus_voltage;
extern float ibus_;
extern bool brake_resistor_armed;
extern bool brake_resistor_saturated;
extern float brake_resistor_current;
extern uint16_t adc_measurements_[ADC_CHANNEL_COUNT];
/* Exported macro ------------------------------------------------------------*/
/* Exported functions --------------------------------------------------------*/
+2
View File
@@ -216,6 +216,7 @@ public:
bool& brake_resistor_armed_ = ::brake_resistor_armed; // TODO: make this the actual variable
bool& brake_resistor_saturated_ = ::brake_resistor_saturated; // TODO: make this the actual variable
float& brake_resistor_current_ = ::brake_resistor_current;
SystemStats_t system_stats_;
@@ -239,6 +240,7 @@ public:
uint32_t n_evt_control_loop_ = 0;
bool task_timers_armed_ = false;
TaskTimes task_times_;
const bool otp_valid_ = ((uint8_t*)FLASH_OTP_BASE)[0] != 0xff;
};
extern ODrive odrv; // defined in main.cpp
@@ -166,6 +166,10 @@ not necessary for to use this port. They are defined so the common demo files
#define portFORCE_INLINE inline __attribute__(( always_inline))
#endif
#ifndef portDONT_DISCARD
#define portDONT_DISCARD __attribute__(( used ))
#endif
portFORCE_INLINE static BaseType_t xPortIsInsideInterrupt( void )
{
uint32_t ulCurrentInterrupt;
@@ -166,6 +166,10 @@ not necessary for to use this port. They are defined so the common demo files
#define portFORCE_INLINE inline __attribute__(( always_inline))
#endif
#ifndef portDONT_DISCARD
#define portDONT_DISCARD __attribute__(( used ))
#endif
portFORCE_INLINE static BaseType_t xPortIsInsideInterrupt( void )
{
uint32_t ulCurrentInterrupt;
+8 -3
View File
@@ -239,10 +239,10 @@ board_v3 = {
'../../ThirdParty/FreeRTOS/Source/portable/GCC/ARM_CM4F',
},
code_files = {
'startup_stm32f405xx.s',
'../../ThirdParty/FreeRTOS/Source/portable/GCC/ARM_CM4F/port.c',
'../../Drivers/DRV8301/drv8301.cpp',
'board.cpp',
'startup_stm32f405xx.s',
'Src/stm32f4xx_hal_timebase_TIM.c',
'Src/tim.c',
'Src/dma.c',
@@ -279,10 +279,10 @@ board_v4 = {
'../../ThirdParty/FreeRTOS/Source/portable/GCC/ARM_CM7/r0p1',
},
code_files = {
'startup_stm32f722xx.s',
'../../ThirdParty/FreeRTOS/Source/portable/GCC/ARM_CM7/r0p1/port.c',
'../Drivers/DRV8353/drv8353.cpp',
'../Drivers/status_led.cpp',
'startup_stm32f722xx.s',
'board.cpp',
'Src/main.c',
'Src/gpio.c',
@@ -343,6 +343,7 @@ CFLAGS += '-Wno-psabi' -- suppress unimportant note about ABI compatibility in G
CFLAGS += { '-Wall', '-Wdouble-promotion', '-Wfloat-conversion', '-fdata-sections', '-ffunction-sections'}
CFLAGS += '-g'
CFLAGS += '-DFIBRE_ENABLE_SERVER'
CFLAGS += '-Wno-nonnull'
-- linker flags
LDFLAGS += '-flto -lc -lm -lnosys' -- libs
@@ -418,10 +419,10 @@ tup.frule{inputs={'fibre-cpp/endpoints_template.j2', extra_inputs='odrive-interf
tup.frule{inputs={'fibre-cpp/type_info_template.j2', extra_inputs='odrive-interface.yaml'}, command=python_command..' interface_generator_stub.py --definitions odrive-interface.yaml --template %f --output %o', outputs='autogen/type_info.hpp'}
add_pkg(board)
add_pkg(freertos_pkg)
add_pkg(cmsis_pkg)
add_pkg(stm32_usb_device_library_pkg)
add_pkg(board)
add_pkg(fibre_pkg)
add_pkg(odrive_firmware_pkg)
@@ -444,6 +445,10 @@ tup.frule{inputs={'build/ODriveFirmware.elf'}, command='arm-none-eabi-size %f'}
tup.frule{inputs={'build/ODriveFirmware.elf'}, command='arm-none-eabi-objcopy -O ihex %f %o', outputs={'build/ODriveFirmware.hex'}}
tup.frule{inputs={'build/ODriveFirmware.elf'}, command='arm-none-eabi-objcopy -O binary -S %f %o', outputs={'build/ODriveFirmware.bin'}}
if tup.getconfig('ENABLE_DISASM') == 'true' then
tup.frule{inputs={'build/ODriveFirmware.elf'}, command='arm-none-eabi-objdump %f -dSC > %o', outputs={'build/ODriveFirmware.asm'}}
end
if tup.getconfig('DOCTEST') == 'true' then
TEST_INCLUDES = '-I. -I./MotorControl -I./fibre-cpp/include -I./Drivers/DRV8301 -I./doctest'
tup.foreach_rule('Tests/*.cpp', 'g++ -O3 -std=c++17 '..TEST_INCLUDES..' -c %f -o %o', 'Tests/bin/%B.o')
+10 -33
View File
@@ -42,25 +42,27 @@ static Introspectable root_obj = ODrive4TypeInfo<ODrive>::make_introspectable(od
// @brief Sends a line on the specified output.
template<typename ... TArgs>
void AsciiProtocol::respond(bool include_checksum, const char * fmt, TArgs&& ... args) {
size_t len = snprintf(tx_buf_, sizeof(tx_buf_), fmt, std::forward<TArgs>(args)...);
char tx_buf[64];
size_t len = snprintf(tx_buf, sizeof(tx_buf), fmt, std::forward<TArgs>(args)...);
// Silently truncate the output if it's too long for the buffer.
len = std::min(len, sizeof(tx_buf_));
len = std::min(len, sizeof(tx_buf));
if (include_checksum) {
uint8_t checksum = 0;
for (size_t i = 0; i < len; ++i)
checksum ^= tx_buf_[i];
len += snprintf(tx_buf_ + len, sizeof(tx_buf_) - len, "*%u", checksum);
checksum ^= tx_buf[i];
len += snprintf(tx_buf + len, sizeof(tx_buf) - len, "*%u\r\n", checksum);
} else {
len += snprintf(tx_buf_ + len, sizeof(tx_buf_) - len, "\r\n");
len += snprintf(tx_buf + len, sizeof(tx_buf) - len, "\r\n");
}
// Silently truncate the output if it's too long for the buffer.
len = std::min(len, sizeof(tx_buf_));
len = std::min(len, sizeof(tx_buf));
tx_end_ = (const uint8_t*)tx_buf_ + len;
tx_channel_->start_write({(const uint8_t*)tx_buf_, tx_end_}, &tx_handle_, MEMBER_CB(this, on_write_finished));
sink_.write({(const uint8_t*)tx_buf, len});
sink_.maybe_start_async_write();
}
@@ -407,24 +409,6 @@ void AsciiProtocol::cmd_unknown(char * pStr, bool use_checksum) {
respond(use_checksum, "unknown command");
}
void AsciiProtocol::on_write_finished(WriteResult result) {
tx_handle_ = 0;
if (result.status == kStreamOk && result.end < tx_end_) {
// Not everything was written. Try again.
tx_channel_->start_write({result.end, tx_end_}, &tx_handle_, MEMBER_CB(this, on_write_finished));
return;
}
if (rx_end_) {
uint8_t* rx_end = rx_end_;
rx_end_ = nullptr;
on_read_finished({kStreamOk, rx_end});
}
}
void AsciiProtocol::on_read_finished(ReadResult result) {
if (result.status != kStreamOk) {
return;
@@ -440,13 +424,6 @@ void AsciiProtocol::on_read_finished(ReadResult result) {
}
if (read_active_) {
if (tx_handle_) {
// TX is busy - inhibit processing of the incoming data until
// on_write_finished() is invoked.
rx_end_ = result.end;
return;
}
process_line({rx_buf_, end_of_line});
} else {
// Ignoring this line cause it didn't start at a new-line character
+3 -6
View File
@@ -2,13 +2,14 @@
#define __ASCII_PROTOCOL_HPP
#include <fibre/async_stream.hpp>
#include <fibre/../../stream_utils.hpp>
#define MAX_LINE_LENGTH ((size_t)256)
class AsciiProtocol {
public:
AsciiProtocol(fibre::AsyncStreamSource* rx_channel, fibre::AsyncStreamSink* tx_channel)
: rx_channel_(rx_channel), tx_channel_(tx_channel) {}
: rx_channel_(rx_channel), sink_(*tx_channel) {}
void start();
@@ -34,16 +35,12 @@ private:
void on_read_finished(fibre::ReadResult result);
fibre::AsyncStreamSource* rx_channel_ = nullptr;
fibre::AsyncStreamSink* tx_channel_ = nullptr;
fibre::TransferHandle tx_handle_ = 0; // non-zero while a TX operation is in progress
uint8_t* rx_end_ = nullptr; // non-zero if an RX operation has finished but wasn't handled yet because the TX channel was busy
const uint8_t* tx_end_ = nullptr;
uint8_t rx_buf_[MAX_LINE_LENGTH];
bool read_active_ = true;
char tx_buf_[64];
fibre::BufferedStreamSink<512> sink_;
};
#endif // __ASCII_PROTOCOL_HPP
+20 -1
View File
@@ -141,6 +141,9 @@ void CANSimple::do_command(Axis& axis, const can_Message_t& msg) {
case MSG_CLEAR_ERRORS:
clear_errors_callback(axis, msg);
break;
case MSG_SET_LINEAR_COUNT:
set_linear_count_callback(axis, msg);
break;
default:
break;
}
@@ -376,7 +379,23 @@ bool CANSimple::send_heartbeat(const Axis& axis) {
txmsg.len = 8;
can_setSignal(txmsg, axis.error_, 0, 32, true);
can_setSignal(txmsg, axis.current_state_, 32, 32, true);
can_setSignal(txmsg, uint8_t(axis.current_state_), 32, 8, true);
// Motor flags
uint8_t motorFlags = 0; // reserved
// Encoder flags
uint8_t encoderFlags = 0; // reserved
// Controller flags
uint8_t controllerFlags = 0;
uint8_t trajDone = uint8_t(axis.controller_.trajectory_done_) << 7;
controllerFlags |= trajDone;
can_setSignal(txmsg, motorFlags, 40, 8, true);
can_setSignal(txmsg, encoderFlags, 48, 8, true);
can_setSignal(txmsg, controllerFlags, 56, 8, true);
// can_setSignal(txmsg, axis.current_state_, 32, 32, true);
return canbus_->send_message(txmsg);
}
@@ -32,6 +32,7 @@ class CANSimple {
MSG_RESET_ODRIVE,
MSG_GET_VBUS_VOLTAGE,
MSG_CLEAR_ERRORS,
MSG_SET_LINEAR_COUNT,
MSG_CO_HEARTBEAT_CMD = 0x700, // CANOpen NMT Heartbeat SEND
};
@@ -82,6 +82,7 @@ public:
#include <tuple>
#include <functional>
#include <unordered_map>
#include <stdlib.h>
//#include <ostream>
/* Backport features from C++14 and C++17 ------------------------------------*/
+7 -1
View File
@@ -19,11 +19,17 @@ public:
* (TODO: this is not true yet, see comment in function)
*/
void write(cbufptr_t buf) {
size_t read_idx = read_idx_; // read_idx_ could change during this function
if ((read_idx + 1) % I == write_idx_) {
return;
}
// We subtract 1 from the read index because we never want the write
// pointer to catch up with the read pointer, cause then
// `write_idx_ == read_idx_` could mean both "full" and "empty".
size_t read_idx = (read_idx_ + I - 1) % I; // read_idx_ could change during this function
read_idx = (read_idx + I - 1) % I;
if (write_idx_ > read_idx) {
size_t n_copy = std::min(I - write_idx_, buf.size());
+1 -1
View File
@@ -9,5 +9,5 @@ try:
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)
print("Check out https://github.com/madcowswe/ODrive/blob/devel/docs/developer-guide.md#prerequisites for details.", file=sys.stderr)
print("Check out https://github.com/odriverobotics/ODrive/blob/devel/docs/developer-guide.md#prerequisites for details.", file=sys.stderr)
exit(1)
+73 -29
View File
@@ -68,7 +68,9 @@ interfaces:
DC_BUS_OVER_REGEN_CURRENT:
doc: |
Current flowing back into the power supply exceeded `config.dc_max_negative_current`.
This can happen if your brake resistor is unable to handle the braking current. Check that
This can happen if your brake resistor is disabled or unable to handle the braking current.
Check that `config.enable_brake_resistor` is `True` and that
`(V_power_supply / Brake_resistance) > (total motor.config.current_lim + total motor.config.current_lim_margin)`.
DC_BUS_OVER_CURRENT:
doc: |
@@ -111,8 +113,10 @@ interfaces:
type: readonly uint8
doc: 0 for official releases, 1 otherwise
brake_resistor_armed: readonly bool
brake_resistor_saturated: bool
brake_resistor_saturated: readonly bool
brake_resistor_current:
type: readonly float32
doc: Commanded brake resistor current
# Diagnostics & performance monitoring
n_evt_sampling: {type: readonly uint32, doc: Number of input sampling events since startup (modulo 2^32)}
n_evt_control_loop: {type: readonly uint32, doc: Number of control loop iterations since startup (modulo 2^32)}
@@ -187,11 +191,12 @@ interfaces:
oscilloscope: {type: Oscilloscope}
can: {type: Can}
test_property: uint32
otp_valid: readonly bool
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: {out: {success: bool}}
save_configuration: {out: {success: bool}, doc: Saves the current configuration to non-volatile memory and reboots the board.}
erase_configuration:
reboot:
enter_dfu_mode:
@@ -684,12 +689,18 @@ interfaces:
The DC current sourced/sunk by this motor exceeded the configured
hard limits. More specifically `I_bus` fell outside of the range
`config.I_bus_hard_min` ... `config.I_bus_hard_max`.
BRAKE_RESISTOR_DISARMED:
BRAKE_RESISTOR_DISARMED:
doc: |
An attempt was made to run the motor PWM while the brake resistor was enabled but disarmed.
The brake resistor can be disarmed for many reasons, but this usually happens if an error
is thrown that disables the motor. Check for other errors, then run `odrvX.clear_errors()`
to clear the errors and rearm the brake resistor.
An attempt was made to run the motor PWM while the brake resistor was configured as enabled
(`config.enable_brake_resistor`) but disarmed.
The most common cause is that you just set `config.enable_brake_resistor` to `True` but didn't
arm the brake resistor yet (by either rebooting or running `odrvX.clear_errors()`).
Otherwise, the brake resistor can be disarmed due to various system-wide errors.
The root cause will usually show up under `system:` when you run `dump_errors(odrvX)`.
To re-arm the brake resistor reboot the ODrive or run `odrvX.clear_errors()`.
SYSTEM_LEVEL:
doc: |
The motor had to be disarmed because of a system level error.
@@ -892,14 +903,15 @@ interfaces:
trajectory_done: readonly bool
vel_integrator_torque: float32
anticogging_valid: bool
autotuning_phase: float32
config:
c_is_class: False
attributes:
gain_scheduling_width: float32
enable_vel_limit: bool
enable_current_mode_vel_limit:
enable_torque_mode_vel_limit:
type: bool
doc: Enable velocity limit in current control mode (requires a valid velocity estimator).
doc: Enable velocity limit in torque control mode (requires a valid velocity estimator).
enable_gain_scheduling: bool
enable_overspeed_error: bool
control_mode: ControlMode
@@ -983,14 +995,12 @@ interfaces:
unit: Watt
autotuning:
c_is_class: False
doc: Automatically generate sine waves for frequency-domain response tuning
attributes:
frequency: float32
pos_amplitude: float32
pos_phase: float32
vel_amplitude: float32
vel_phase: float32
torque_amplitude: float32
torque_phase: float32
frequency: {type: float32, unit: Hz}
pos_amplitude: {type: float32, unit: turns}
vel_amplitude: {type: float32, unit: turns/sec}
torque_amplitude: {type: float32, unit: N-m}
mechanical_power:
type: readonly float32
unit: Watt
@@ -1060,20 +1070,52 @@ interfaces:
HALL_NOT_CALIBRATED_YET:
is_ready: readonly bool
index_found: readonly bool
shadow_count: readonly int32
count_in_cpr: readonly int32
shadow_count:
type: readonly int32
unit: counts
doc: Raw linear count from the encoder.
count_in_cpr:
type: readonly int32
unit: counts
doc: Raw circular count from the encoder on [0, cpr)
interpolation: readonly float32
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_.any().value_or(0.0f)}
pos_estimate:
type: readonly float32
c_getter: pos_estimate_.any().value_or(0.0f)
unit: turns
doc: Linear position estimate of the encoder, in turns. Also known as "multi-turn" position.
pos_estimate_counts:
type: readonly float32
unit: counts
doc: Linear position estimate of the encoder, in counts. Equal to `pos_estimate * config.cpr`
pos_circular:
type: readonly float32
c_getter: pos_circular_.any().value_or(0.0f)
unit: turns
doc: Circular position estimate of the encoder, as a decimal from [0, 1). Also known as "single-turn" position.
pos_cpr_counts:
type: readonly float32
unit: counts
doc: Circular position estimate of the encoder, on the space [0, cpr).
delta_pos_cpr_counts:
type: readonly float32
unit: counts
doc: Circular position delta of the encoder in the most recent loop. Primarily for debug purposes, it indicates much the encoder changed since the last time it was checked.
hall_state: readonly uint8
vel_estimate: {type: readonly float32, c_getter: vel_estimate_.any().value_or(0.0f)}
vel_estimate_counts: readonly float32
vel_estimate:
type: readonly float32
c_getter: vel_estimate_.any().value_or(0.0f)
unit: turns/s
doc: Estimate of the linear velocity of an axis in turns/s
vel_estimate_counts:
type: readonly float32
unit: counts/sec
doc: Estimate of the linear velocity of an axis, in counts/s.
calib_scan_response: readonly float32
pos_abs: int32
pos_abs:
type: int32
doc: The last (valid) position from an absolute encoder, if used.
spi_error_rate: readonly float32
config:
c_is_class: False
@@ -1257,7 +1299,9 @@ valuetypes:
AsciiAndStdout: {doc: Combination of `Ascii` and `Stdout`.}
ODrive.Can.Protocol:
flags: {SIMPLE: }
flags:
SIMPLE:
doc: CANSimple, an ODrive-specific protocol for basic functionality
ODrive.Axis.AxisState: # TODO: remove redundant "Axis" in name
values:
+4
View File
@@ -308,6 +308,10 @@ let odriveEnums = {
{
text: "Mirror",
value: 7
},
{
text: "Tuning",
value: 8
}
]
}
+2
View File
@@ -29,6 +29,8 @@ sections:
url: /hoverboard
- title: Migration Guide
url: /migration
- title: CAN Guide
url: /can-guide
- title: Interfaces & Protocols
docs:
- title: Pinout
+2 -2
View File
@@ -16,9 +16,9 @@ anticogging_enabled | bool | Enable or disable anticogging. A valid anticogging
## Calibration
To calibrate anticogging, first make sure you can adequately control the motor in . It should respond to position commands.
To calibrate anticogging, first make sure you can adequately control the motor. It should respond to position commands.
Start by putting the axis in `AXIS_STATE_CLOSED_LOOP` with `CONTROL_MODE_POSITION_CONTROL` and `INPUT_MODE_PASSTHROUGH`. Make sure you have good control of the motor in this state (it responds to position commands). Now, tune the motor to be very stiff - high `pos_gain` and relatively high `vel_integrator_gain`. This will help in calibration.
Start by putting the axis in `AXIS_STATE_CLOSED_LOOP_CONTROL` with `CONTROL_MODE_POSITION_CONTROL` and `INPUT_MODE_PASSTHROUGH`. Make sure you have good control of the motor in this state (it responds to position commands). Now, tune the motor to be very stiff - high `pos_gain` and relatively high `vel_integrator_gain`. This will help in calibration.
Run `controller.start_anticogging_calibration()`. The motor will start turning slowly, calibrating each point. If you like, you can start a liveplotter session before running this command so that you can watch the position move.
+134
View File
@@ -0,0 +1,134 @@
# CAN Bus Guide for ODrive
ODrive v3 supports CAN 2.0b. We've built a [simple protocol](can-protocol.md) (named CANSimple) so that most ODrive functions can be controlled without a full CAN Open or similar stack. This guide is intended for beginners to set up CAN on the ODrive and on their host device. We will be focusing on Raspberry Pi and Arduino-compatible devices using the MCP2515 CAN Controller.
## What is CAN bus?
Borrowing from [Wikipeda](https://en.wikipedia.org/wiki/CAN_bus):
> A Controller Area Network (CAN bus) is a robust vehicle bus standard designed to allow microcontrollers and devices to communicate with each other's applications without a host computer. It is a message-based protocol, designed originally for multiplex electrical wiring within automobiles to save on copper, but it can also be used in many other contexts. For each device, the data in a frame is transmitted sequentially but in such a way that if more than one device transmits at the same time, the highest priority device can continue while the others back off. Frames are received by all devices, including by the transmitting device.
In simple terms, CAN is a way of communicating between many devices over a single twisted pair of wires. The signal is transmitted as the difference in voltage between the two wires (differential signalling), which makes it very robust against noise. Instead of using a unique address (like I2C) or a select pin (like SPI), CAN *messages* have a unique ID that also acts as the priority. At the beginning of a message frame, all devices talk and read at the same time. As the message ID is transmitted, the lowest value "wins" and that message will be transmitted (ID **0** has the *highest* priority). All other devices will wait for the next chance to send. If two devices send the same message ID at the same time, they will conflict and a bus failure may occur. Make sure your devices can never send the same message ID at the same time!
![CAN picture](screenshots/CAN_Bus_Drawing.png)
## Why use CAN?
CAN is convenient for its simple and robust Physical Layer (PHY) that requires only a twisted pair of wires and a 120ohm termination resistor at each end. It has low jitter and low latency, because there is no host computer. It is relatively fast (CAN 2.0b supports 1 Mbps). Messages are easy to configure and load with data. Transceivers and controllers are inexpensive and widely available, thanks to its use in automotive.
## Hardware Setup
ODrive assumes the CAN PHY is a standard differential twisted pair in a linear bus configuration with 120 ohm termination resistance at each end. ODrive versions less than V3.5 include a soldered 120 ohm termination resistor, but ODrive versions V3.5 and greater implement a dip switch to toggle the termination. ODrive uses 3.3v as the high output, but conforms to the CAN PHY requirement of achieving a differential voltage > 1.5V to represent a "0". As such, it is compatible with standard 5V bus architectures.
## Setting up CAN on ODrive
CANSimple breaks the CAN Message ID into two parts: An axis ID and a command ID. By default, CAN is enabled on the ODrive, where Axis 0 has ID 0, and Axis 1 has ID 1. The ID of each axis should be unique; each should be set via `odrivetool` before connecting to the bus with the command:
`<odrv>.<axis>.config.can.node_id = <number>`
By default, ODrive supports a value up to 63 (`0x3F`). See [can-protocol.md](can-protocol.md) for more information.
You should also set the CAN bus speed on ODrive with the command `<odrv>.can.config.baud_rate = <number>`
| Speed | Value |
| --------- | ------- |
| 125 kbps | 125000 |
| 250 kbps | 250000 |
| 500 kbps | 500000 |
| 1000 kbps | 1000000 |
That's it! You're ready to set up your host device.
### Example
```Python
odrv0.axis0.config.can.node_id = 0
odrv0.axis1.config.can.node_id = 1
odrv0.can.config.baud_rate = 250000
```
## Setting up a Raspberry Pi for CAN communications
First, you will need a CAN Hat for your Raspberry Pi. We are using [this CAN hat](https://www.amazon.com/Raspberry-Long-Distance-Communication-Transceiver-SN65HVD230/dp/B07DQPYFYV).
Setting up the Raspberry Pi essentially involves the following:
1. Enable SPI communications to the MCP2515
2. Install `can-utils` with `apt-get install can-utils`
3. Creating a connection between your application and the `can0` socket
There are many tutorials for this process. [This one is pretty good](https://www.hackster.io/youness/how-to-connect-raspberry-pi-to-can-bus-b60235), and [this recent forum post](https://www.raspberrypi.org/forums/viewtopic.php?t=296117) also works. However, be careful. You have to set the correct parameters for the particular CAN hat you're using!
1. Set the correct oscillator value
We configure the MCP2515 in section 2.2 of the tutorial, but the hat we recommend uses a 12MHz crystal instead of a 16 MHz crystal. If you're not sure what value to use, the top of the [oscillator](https://en.wikipedia.org/wiki/Crystal_oscillator) will have the value printed on it in MHz.
My Settings:
```
dtparam=spi-on
dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25
dtoverlay=spi0-hw-cs
```
2. Use the correct CAN baud rate
By default, ODrive uses 250 kbps (250000) but the tutorial is using 500 kbps. Make sure you use the value set earlier on the ODrive.
```
sudo ip link set can0 up type can bitrate 250000
```
### Wiring ODrive to CAN
The CANH and CANL pins on J2 are used for CAN communication. Connect CANH to CANH on all other devices, and CANL to CANL.
If your ODrive is the "last" (furthest) device on the bus, you can use the on-board 120 Ohm termination resistor by switching the DIP switch to "CAN 120R". Otherwise, add an external resistor.
### Verifying Communcation
By default, each ODrive axis will send a heartbeat message at 10Hz. We can confirm our ODrive communication is working by starting the `can0` interface, and then reading from it:
```
sudo ip link set can0 up type can bitrate 250000
candump can0 -xct z -n 10
```
This will read the first 10 messages from the ODrive and stop. If you'd like to see all messages, remove the `-n 10` part (hit CTRL+C to exit). The other flags (x, c, t) are adding extra information, colouring, and a timestamp, respectively.
```
$ candump can0 -xct z -n 10
(000.000000) can0 RX - - 001 [8] 00 00 00 00 01 00 00 00
(000.001995) can0 RX - - 021 [8] 00 00 00 00 08 00 00 00
(000.099978) can0 RX - - 001 [8] 00 00 00 00 01 00 00 00
(000.101963) can0 RX - - 021 [8] 00 00 00 00 08 00 00 00
(000.199988) can0 RX - - 001 [8] 00 00 00 00 01 00 00 00
(000.201980) can0 RX - - 021 [8] 00 00 00 00 08 00 00 00
(000.299986) can0 RX - - 001 [8] 00 00 00 00 01 00 00 00
(000.301976) can0 RX - - 021 [8] 00 00 00 00 08 00 00 00
(000.399986) can0 RX - - 001 [8] 00 00 00 00 01 00 00 00
(000.401972) can0 RX - - 021 [8] 00 00 00 00 08 00 00 00
```
Alternatively, if you have python can installed (`pip3 install python-can`), you can use the can.viewer script:
`python3 -m can.viewer -c "can0" -i "socketcan"` which will give you a nice readout. See [the python-can docs](https://python-can.readthedocs.io/en/master/scripts.html#can-viewer) for an example.
## Commanding the ODrive
Now that we've verified the communication is working, we can try commanding the ODrive. Make sure your ODrive is configured and working properly over USB with `odrivetool` before continuing. See the [Getting Started Guide](getting-started.md) for help with first-time configuration.
To move the ODrive, we use the command `Set Input Pos`, or cmd ID `0x00C`. First we create a message with this ID, and then "OR" in the axis ID. Then we create an 8-byte array of data with input position that we want, with a float value turned into bytes... this can be a pain though.
## DBC Files
A DBC file (.dbc) is a database of all the messages and signals in a CAN protocol. This file can be used with Python cantools to serialize and deserialize messages without having to handle the bitshifting etc yourself. We have generated a .dbc for CANSimple for you!
* [CANSimple DBC File](../tools/odrive-cansimple.dbc)
* [CANSimple DBC Generator Script](../tools/create_can_dbc.py)
Instead of manually writing values into the data, we can create a dictionary of signal:value pairs and serialize the data according to the database definition.
1. Load the database into memory
2. Use `encode_message()` to get a byte array representation of data for sending
3. Use `decode_message()` to get a dictionary representation of data for receiving
The [CAN DBC Example](../tools/can_dbc_example.py) script shows you how this can be used. This is the recommended method of serializing and deserializing.
If you're using C++, then you can use the [CANHelpers](..firmware/communication/../../../Firmware/communication/can/can_helpers.hpp) single-header library to do this instead, although the DBC file isn't used.
+36 -40
View File
@@ -1,13 +1,7 @@
# CAN Protocol
## Hardware Setup
ODrive assumes the CAN PHY is a standard differential twisted pair in a linear bus configuration with 120 ohm termination resistance at each end. ODrive versions less than V3.5 include a soldered 120 ohm termination resistor, but ODrive versions V3.5 and greater implement a dip switch to toggle the termination. ODrive uses 3.3v as the high output, but conforms to the CAN PHY requirement of achieving a differential voltage > 1.5V to represent a "0". As such, it is compatible with standard 5V bus architectures.
This document describes the CAN Protocol. For examples of usage, check out our [CAN Guide!](can-guide.md)
ODrive currently supports the following CAN baud rates:
* 125 kbps
* 250 kbps (default)
* 500 kbps
* 1000 kbps
---
## Configuring ODrive for CAN
@@ -49,36 +43,38 @@ For example, an Axis ID of `0x01` with a command of `0x0C` would be result in `0
### Messages
CMD ID | Name | Sender | Signals | Start byte | Signal Type | Bits | Factor | Offset | Byte Order
--: | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :--
0x000 | CANOpen NMT Message\*\* | Master | - | - | - | - | - | - | -
0x001 | ODrive Heartbeat Message | Axis | Axis Error<br>Axis Current State | 0<br>4 | Unsigned Int<br>Unsigned Int | 32<br>32 | 1<br>1 | 0<br>0 | Intel<br>Intel
0x002 | ODrive Estop Message | Master | - | - | - | - | - | - | -
0x003 | Get Motor Error\* | Axis | Motor Error | 0 | Unsigned Int | 64 | 1 | 0 | Intel
0x004 | Get Encoder Error\* | Axis | Encoder Error | 0 | Unsigned Int | 32 | 1 | 0 | Intel
0x005 | Get Sensorless Error\* | Axis | Sensorless Error | 0 | Unsigned Int | 32 | 1 | 0 | Intel
0x006 | Set Axis Node ID | Master | Axis CAN Node ID | 0 | Unsigned Int | 32 | 1 | 0 | Intel
0x007 | Set Axis Requested State | Master | Axis Requested State | 0 | Unsigned Int | 32 | 1 | 0 | Intel
0x008 | Set Axis Startup Config | Master | - Not yet implemented - | - | - | - | - | - | -
0x009 | Get Encoder Estimates\* | Master | Encoder Pos Estimate<br>Encoder Vel Estimate | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0 | Intel<br>Intel
0x00A | Get Encoder Count\* | Master | Encoder Shadow Count<br>Encoder Count in CPR | 0<br>4 | Signed Int<br>Signed Int | 32<br>32 | 1<br>1 | 0<br>0 | Intel<br>Intel
0x00B | Set Controller Modes | Master | Control Mode<br>Input Mode | 0<br>4 | Signed Int<br>Signed Int | 32<br>32 | 1<br>1 | 0<br>0 | Intel<br>Intel
0x00C | Set Input Pos | Master | Input Pos<br>Vel FF<br>Torque FF | 0<br>4<br>6 | IEEE 754 Float<br>Signed Int<br>Signed Int | 32<br>16<br>16 | 1<br>0.001<br>0.001 | 0<br>0<br>0 | Intel<br>Intel<br>Intel
0x00D | Set Input Vel | Master | Input Vel<br>Torque FF | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0 | Intel<br>Intel
0x00E | Set Input Torque | Master | Input Torque | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel
0x00F | Set Limits | Master | Velocity Limit<br>Current Limit | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br> | 1<br>1 | 0<br>0 | Intel
0x010 | Start Anticogging | Master | - | - | - | - | - | - | -
0x011 | Set Traj Vel Limit | Master | Traj Vel Limit | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel
0x012 | Set Traj Accel Limits | Master | Traj Accel Limit<br>Traj Decel Limit | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0 | Intel<br>Intel
0x013 | Set Traj Inertia | Master | Traj Inertia | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel
0x014 | Get IQ\* | Axis | Iq Setpoint<br>Iq Measured | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0 | Intel<br>Intel
0x015 | Get Sensorless Estimates\* | Master | Sensorless Pos Estimate<br>Sensorless Vel Estimate | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0 | Intel<br>Intel
0x016 | Reboot ODrive | Master\*\*\* | - | - | - | - | - | - | -
0x017 | Get Vbus Voltage | Master\*\*\* | Vbus Voltage | 0 | IEEE 754 Float | 32 | 1 | 0 | Intel
0x018 | Clear Errors | Master | - | - | - | - | - | - | -
0x019 | Set Linear Count | Master | Position | 0 | Signed Int | 32 | 1 | 0 | Intel
0x700 | CANOpen Heartbeat Message\*\* | Slave | - | - | - | - | - | - | -
-|-|-|----------------------------------|-|--------------------|-|-|-|_
CMD ID | Name | Sender | Signals | Start byte | Signal Type | Bits | Factor | Offset
--: | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :--
0x000 | CANOpen NMT Message\*\* | Master | - | - | - | - | - | -
0x001 | ODrive Heartbeat Message | Axis | Axis Error<br>Axis Current State<br>Controller Status | 0<br>4<br>7 | Unsigned Int<br>Unsigned Int<br>Bitfield | 32<br>8<br>8 | -<br>-<br>- | -<br>-<br>-
0x002 | ODrive Estop Message | Master | - | - | - | - | - | -
0x003 | Get Motor Error\* | Axis | Motor Error | 0 | Unsigned Int | 64 | 1 | 0
0x004 | Get Encoder Error\* | Axis | Encoder Error | 0 | Unsigned Int | 32 | 1 | 0
0x005 | Get Sensorless Error\* | Axis | Sensorless Error | 0 | Unsigned Int | 32 | 1 | 0
0x006 | Set Axis Node ID | Master | Axis CAN Node ID | 0 | Unsigned Int | 32 | 1 | 0
0x007 | Set Axis Requested State | Master | Axis Requested State | 0 | Unsigned Int | 32 | 1 | 0
0x008 | Set Axis Startup Config | Master | - Not yet implemented - | - | - | - | - | -
0x009 | Get Encoder Estimates\* | Master | Encoder Pos Estimate<br>Encoder Vel Estimate | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0
0x00A | Get Encoder Count\* | Master | Encoder Shadow Count<br>Encoder Count in CPR | 0<br>4 | Signed Int<br>Signed Int | 32<br>32 | 1<br>1 | 0<br>0
0x00B | Set Controller Modes | Master | Control Mode<br>Input Mode | 0<br>4 | Signed Int<br>Signed Int | 32<br>32 | 1<br>1 | 0<br>0
0x00C | Set Input Pos | Master | Input Pos<br>Vel FF<br>Torque FF | 0<br>4<br>6 | IEEE 754 Float<br>Signed Int<br>Signed Int | 32<br>16<br>16 | 1<br>0.001<br>0.001 | 0<br>0<br>0
0x00D | Set Input Vel | Master | Input Vel<br>Torque FF | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0
0x00E | Set Input Torque | Master | Input Torque | 0 | IEEE 754 Float | 32 | 1 | 0
0x00F | Set Limits | Master | Velocity Limit<br>Current Limit | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br> | 1<br>1 | 0<br>0
0x010 | Start Anticogging | Master | - | - | - | - | - | -
0x011 | Set Traj Vel Limit | Master | Traj Vel Limit | 0 | IEEE 754 Float | 32 | 1 | 0
0x012 | Set Traj Accel Limits | Master | Traj Accel Limit<br>Traj Decel Limit | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0
0x013 | Set Traj Inertia | Master | Traj Inertia | 0 | IEEE 754 Float | 32 | 1 | 0
0x014 | Get IQ\* | Axis | Iq Setpoint<br>Iq Measured | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0
0x015 | Get Sensorless Estimates\* | Master | Sensorless Pos Estimate<br>Sensorless Vel Estimate | 0<br>4 | IEEE 754 Float<br>IEEE 754 Float | 32<br>32 | 1<br>1 | 0<br>0
0x016 | Reboot ODrive | Master\*\*\* | - | - | - | - | - | -
0x017 | Get Vbus Voltage | Master\*\*\* | Vbus Voltage | 0 | IEEE 754 Float | 32 | 1 | 0
0x018 | Clear Errors | Master | - | - | - | - | - | -
0x019 | Set Linear Count | Master | Position | 0 | Signed Int | 32 | 1 | 0
0x700 | CANOpen Heartbeat Message\*\* | Slave | - | - | - | - | - | -
-|-|-|----------------------------------|-|--------------------|-|-|-
All multibyte values are little endian (aka Intel format, aka least significant byte first).
\* Note: These messages are call & response. The Master node sends a message with the RTR bit set, and the axis responds with the same ID and specified payload.
\*\* Note: These CANOpen messages are reserved to avoid bus collisions with CANOpen devices. They are not used by CAN Simple.
@@ -89,12 +85,12 @@ CMD ID | Name | Sender | Signals | Start byte | Signal Type | Bits | Factor | Of
### Interoperability with CANopen
You can deconflict with CANopen like this:
`odrv0.axis0.can_node_id = 0x010` - Reserves messages 0x200 through 0x21F
`odrv0.axis1.can_node_id = 0x018` - Reserves messages 0x300 through 0x31F
`odrv0.axis0.config.can.node_id = 0x010` - Reserves messages 0x200 through 0x21F
`odrv0.axis1.config.can.node_id = 0x018` - Reserves messages 0x300 through 0x31F
It may not be obvious, but this allows for some compatibility with CANOpen. Although the address space 0x200 and 0x300 correspond to receive PDO base addresses, we can guarantee they will not conflict if all CANopen node IDs are >= 32. E.g.:
CANopen nodeID = 35 = 0x23
CANopen nodeID = 35 = 0x23
Receive PDO 0x200 + nodeID = 0x223, which does not conflict with the range [0x200 : 0x21F]
Be careful that you don't assign too many nodeIDs per PDO group. Four CAN Simple nodes (32*4) is all of the available address space of a single PDO. If the bus is strictly ODrive CAN Simple nodes, a simple sequential Node ID assignment will work fine.
+1 -1
View File
@@ -332,4 +332,4 @@ When filing a PR please go through this checklist:
- Also, for each removed/moved/renamed API item use your IDE's search feature to search for occurrences of this name. Update the places you found (this will usually be documentation and test scripts).
- If you added things to `odrive-interface.yaml` make sure the new things have decent documentation in the YAML file. We don't expect 100% coverage but use good sense of what to document.
- Make sure your PR doesn't contain spurious changes that unnecessarily add or remove whitespace. These add noise and make the reviewer's lifes harder.
- If you changed any enums in `odrive-interface.yaml`, make sure you update [enums.py](../tools/odrive/enums.py). The file includes instructions on how to do this. Check the diff to verify that none of the existing enumerators changed their value.
- If you changed any enums in `odrive-interface.yaml`, make sure you update [enums.py](../tools/odrive/enums.py) and [ODriveEnums.h](../Arduino/ODriveArduino/ODriveEnums.h). The file includes instructions on how to do this. Check the diff to verify that none of the existing enumerators changed their value.
+9 -4
View File
@@ -16,7 +16,6 @@ offset | float | 0.0
debounce_ms | float | 50.0
enabled | boolean | false
is_active_high | boolean | false
pullup | boolean | true
### gpio_num
The GPIO pin number, according to the silkscreen labels on ODrive. Set with these commands:
@@ -33,7 +32,7 @@ Enables/disables detection of the endstop. If disabled, homing and e-stop canno
```
### offset
This is the position of the endstops on the relevant axis, in counts. For example, if you want a position command of `0` to represent a position 100 counts away from the endstop, the offset would be `-100.0` (because the endstop is located at axis position `-100.0`).
This is the position of the endstops on the relevant axis, in turns. For example, if you want a position command of `0` to represent a position 3 turns away from the endstop, the offset would be `-3.0` (because the endstop is located at axis position `-3.0`).
```
<odrv>.<axis>.min_endstop.config.offset = <int>
@@ -54,8 +53,13 @@ This is how you configure the endstop to be either "NPN" or "PNP". An "NPN" con
Typically configuration **1** or **3** is preferred when using mechanical switches as the most common failure mode leaves the switch open.
### pullup
Match the pullup value to the configuration. If `true`, it enables the GPIO pullup resistor. If `false`, it enables the GPIO pull*down* resistor.
### GPIO configuration
The GPIOs that are used for the endstops need to be configured according to the diagram below.
Assuming your endstop is connected to GPIO X:
- Configuration 1, 2: `<odrv>.config.gpioX_mode = GPIO_MODE_DIGITAL_PULL_DOWN`
- Configuration 3, 4: `<odrv>.config.gpioX_mode = GPIO_MODE_DIGITAL_PULL_DOWN`
![Endstop configuration](Endstop_configuration.png)
@@ -70,6 +74,7 @@ If we want to configure a 3D printer-style (configuration 4) minimum endstop for
<odrv>.<axis>.min_endstop.config.is_active_high = False
<odrv>.<axis>.min_endstop.config.offset = -1.0*(8912/4)
<odrv>.<axis>.min_endstop.config.enabled = True
<odrv>.config.gpio5_mode = GPIO_MODE_DIGITAL_PULL_UP
```
### Testing The Endstops
+26 -13
View File
@@ -9,18 +9,30 @@ permalink: /
### Table of contents
<!-- TOC depthFrom:2 depthTo:2 -->
- [Hardware Requirements](#hardware-requirements)
- [Wiring up the ODrive](#wiring-up-the-odrive)
- [Downloading and Installing Tools](#downloading-and-installing-tools)
- [Firmware](#firmware)
- [Start `odrivetool`](#start-odrivetool)
- [Debugging](#debugging)
- [Configure M0](#configure-m0)
- [Position control of M0](#position-control-of-m0)
- [Other control modes](#other-control-modes)
- [Watchdog Timer](#watchdog-timer)
- [What's next?](#whats-next)
- [Upgrading from 0.4.12](#upgrading-from-0412)
- [Getting Started](#getting-started)
- [Table of contents](#table-of-contents)
- [Hardware Requirements](#hardware-requirements)
- [You will need:](#you-will-need)
- [Wiring up the ODrive](#wiring-up-the-odrive)
- [Wiring up the motors](#wiring-up-the-motors)
- [Wiring up the encoders](#wiring-up-the-encoders)
- [Safety & Power UP](#safety--power-up)
- [Downloading and Installing Tools](#downloading-and-installing-tools)
- [Windows](#windows)
- [OSX](#osx)
- [Linux](#linux)
- [Firmware](#firmware)
- [Start `odrivetool`](#start-odrivetool)
- [Debugging](#debugging)
- [Configure M0](#configure-m0)
- [1. Set the limits:](#1-set-the-limits)
- [2. Set other hardware parameters](#2-set-other-hardware-parameters)
- [3. Save configuration](#3-save-configuration)
- [Position control of M0](#position-control-of-m0)
- [Other control modes](#other-control-modes)
- [Watchdog Timer](#watchdog-timer)
- [What's next?](#whats-next)
- [Upgrading from 0.4.12](#upgrading-from-0412)
<!-- /TOC -->
@@ -250,7 +262,7 @@ This is 4x the Pulse Per Revolution (PPR) value. Usually this is indicated in th
Please see the [Thermistors](thermistors.md) page for setup.
### 3. Save configuration
You can save all `.config` parameters to persistent memory so the ODrive remembers them between power cycles.
You can save all `.config` parameters to persistent memory so the ODrive remembers them between power cycles. This will reboot the board.
* `odrv0.save_configuration()` <kbd>Enter</kbd>.
@@ -307,6 +319,7 @@ You can now:
* See what other [commands and parameters](commands.md) are available, in order to better control the ODrive.
* Control the ODrive from your own program or hook it up to an existing system through one of it's [interfaces](pinout.md).
* See how you can improve the behavior during the startup procedure, like [bypassing encoder calibration](encoders.md#encoder-with-index-signal).
* The CAN communication is the most reliable way of talking to ODrive in a real application. Check out the [CAN Guide](can-guide.md) and [CAN Protocol](can-protocol.md)
If you have any issues or any questions please get in touch. The [ODrive Community](https://discourse.odriverobotics.com/) warmly welcomes you.
+2
View File
@@ -50,6 +50,8 @@ You can use ODrive Tool to back up and restore device configurations or transfer
* To save the configuration to a file on the PC, run `odrivetool backup-config my_config.json`.
* To restore the configuration form such a file, run `odrivetool restore-config my_config.json`.
Note that encoder offset calibration is not restored because this would be dangerous if you transfer the calibration values of one axis to another axis.
## Device Firmware Update
<div class="note" markdown="span">__ODrive v3.4 or earlier__: DFU is not supported on these devices. You need to [flash with the external programmer](#flashing-with-an-stlink) instead.</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

+1
View File
@@ -14,6 +14,7 @@ Table of Contents:
If your ODrive is not working as expected, run `odrivetool` and type `dump_errors(odrv0)` <kbd>Enter</kbd>. This will dump a list of all the errors that are present. To clear all the errors, you can run `odrv0.clear_errors()`.
With this information you can look up the API documentation for your error(s):
* System error flags documented [here](api/odrive.error).
* Axis error flags documented [here](api/odrive.axis.error).
* Motor error flags documented [here](api/odrive.motor.error).
* Encoder error flags documented [here](api/odrive.encoder.error).
+23
View File
@@ -0,0 +1,23 @@
#ifndef ODriveEnums_h
#define ODriveEnums_h
/* TODO: This file is dangerous because the enums could potentially change between API versions. Should transmit as part of the JSON.
** To regenerate this file, nagivate to the top level of the ODrive repository and run:
** python Firmware/interface_generator_stub.py --definitions Firmware/odrive-interface.yaml --template tools/arduino_enums_template.j2 --output Arduino/ODriveArduino/ODriveEnums.h
*/
[%- for _, enum in value_types.items() %]
[%- if enum.is_enum %]
// [[enum.fullname]]
enum [[(enum.parent.name if enum.name in ['Error', 'Mode'] else '') + enum.name ]] {
[%- for k, value in enum['values'].items() %]
[[((((enum.parent.name if enum.name in ['Error', 'Mode'] else '') + enum.name) | to_macro_case) + "_" + (k | to_macro_case)).ljust(40)]] = [% if enum.is_flags %]0x[['%08x' | format(value.value)]][% else %][[value.value]][% endif %],
[%- endfor %]
};
[%- endif %]
[%- endfor %]
#endif
+12
View File
@@ -0,0 +1,12 @@
import can
bus1 = can.interface.Bus('can0', bustype='virtual')
bus2 = can.interface.Bus('can0', bustype='virtual')
msg1 = can.Message(arbitration_id=0xabcde, data=[1,2,3])
bus1.send(msg1)
msg2 = bus2.recv()
print(hex(msg1.arbitration_id))
print(hex(msg2.arbitration_id))
assert msg1.arbitration_id == msg2.arbitration_id
+80
View File
@@ -0,0 +1,80 @@
import math
import can
import cantools
import time
db = cantools.database.load_file("odrive-cansimple.dbc")
# print(db)
# bus = can.Bus("vcan0", bustype="virtual")
bus = can.Bus("can0", bustype="socketcan")
axisID = 0x1
print("\nRequesting AXIS_STATE_FULL_CALIBRATION_SEQUENCE (0x03) on axisID: " + str(axisID))
msg = db.get_message_by_name('Set_Axis_State')
data = msg.encode({'Axis_Requested_State': 0x03})
msg = can.Message(arbitration_id=msg.frame_id | axisID << 5, is_extended_id=False, data=data)
print(db.decode_message('Set_Axis_State', msg.data))
print(msg)
try:
bus.send(msg)
print("Message sent on {}".format(bus.channel_info))
except can.CanError:
print("Message NOT sent! Please verify can0 is working first")
print("Waiting for calibration to finish...")
# Read messages infinitely and wait for the right ID to show up
while True:
msg = bus.recv()
if msg.arbitration_id == ((axisID << 5) | db.get_message_by_name('Heartbeat').frame_id):
current_state = db.decode_message('Heartbeat', msg.data)['Axis_State']
if current_state == 0x1:
print("\nAxis has returned to Idle state.")
break
for msg in bus:
if msg.arbitration_id == ((axisID << 5) | db.get_message_by_name('Heartbeat').frame_id):
errorCode = db.decode_message('Heartbeat', msg.data)['Axis_Error']
if errorCode == 0x00:
print("No errors")
else:
print("Axis error! Error code: "+str(hex(errorCode)))
break
print("\nPutting axis",axisID,"into AXIS_STATE_CLOSED_LOOP_CONTROL (0x08)...")
data = db.encode_message('Set_Axis_State', {'Axis_Requested_State': 0x08})
msg = can.Message(arbitration_id=0x07 | axisID << 5, is_extended_id=False, data=data)
print(msg)
try:
bus.send(msg)
print("Message sent on {}".format(bus.channel_info))
except can.CanError:
print("Message NOT sent!")
for msg in bus:
if msg.arbitration_id == 0x01 | axisID << 5:
print("\nReceived Axis heartbeat message:")
msg = db.decode_message('Heartbeat', msg.data)
print(msg)
if msg['Axis_State'] == 0x8:
print("Axis has entered closed loop")
else:
print("Axis failed to enter closed loop")
break
target = 0
data = db.encode_message('Set_Limits', {'Velocity_Limit':10.0, 'Current_Limit':10.0})
msg = can.Message(arbitration_id=axisID << 5 | 0x00F, is_extended_id=False, data=data)
bus.send(msg)
t0 = time.monotonic()
while True:
setpoint = 4.0 * math.sin((time.monotonic() - t0)*2)
print("goto " + str(setpoint))
data = db.encode_message('Set_Input_Pos', {'Input_Pos':setpoint, 'Vel_FF':0.0, 'Torque_FF':0.0})
msg = can.Message(arbitration_id=axisID << 5 | 0x00C, data=data, is_extended_id=False)
bus.send(msg)
time.sleep(0.01)
+53
View File
@@ -0,0 +1,53 @@
import can
bus = can.Bus("can0", bustype="socketcan")
axisID = 0x1
print("Requesting AXIS_STATE_FULL_CALIBRATION_SEQUENCE (0x03) on axisID: " + str(axisID))
msg = can.Message(arbitration_id=axisID << 5 | 0x07, data=[3, 0, 0, 0, 0, 0, 0, 0], dlc=8, is_extended_id=False)
print(msg)
try:
bus.send(msg)
print("Message sent on {}".format(bus.channel_info))
except can.CanError:
print("Message NOT sent! Please verify can0 is working first")
print("Waiting for calibration to finish...")
# Read messages infinitely and wait for the right ID to show up
while True:
msg = bus.recv()
if msg.arbitration_id == (axisID << 5 | 0x01):
current_state = msg.data[4] | msg.data[5] << 8 | msg.data[6] << 16 | msg.data[7] << 24
if current_state == 0x1:
print("\nAxis has returned to Idle state.")
break
for msg in bus:
if(msg.arbitration_id == (axisID << 5 | 0x01)):
errorCode = msg.data[0] | msg.data[1] << 8 | msg.data[2] << 16 | msg.data[3] << 24
print("\nReceived Axis heartbeat message:")
if errorCode == 0x0:
print("No errors")
else:
print("Axis error! Error code: "+str(hex(errorCode)))
break
print("\nPutting axis",axisID,"into AXIS_STATE_CLOSED_LOOP_CONTROL (0x08)...")
msg = can.Message(arbitration_id=axisID << 5 | 0x07, data=[8, 0, 0, 0, 0, 0, 0, 0], dlc=8, is_extended_id=False)
print(msg)
try:
bus.send(msg)
print("Message sent on {}".format(bus.channel_info))
except can.CanError:
print("Message NOT sent!")
for msg in bus:
if msg.arbitration_id == (axisID << 5 | 0x01):
print("\nReceived Axis heartbeat message:")
if msg.data[4] == 0x8:
print("Axis has entered closed loop")
else:
print("Axis failed to enter closed loop")
break
+178
View File
@@ -0,0 +1,178 @@
import cantools
# 0x00 - NMT Message (Reserved)
# 0x001 - Heartbeat
axisError = cantools.database.can.Signal("Axis_Error", 0, 32)
axisState = cantools.database.can.Signal("Axis_State", 32, 32)
heartbeatMsg = cantools.database.can.Message(
0x001, "Heartbeat", 8, [axisError, axisState]
)
# 0x002 - E-Stop Message
estopMsg = cantools.database.can.Message(0x002, "Estop", 0, [])
# 0x003 - Motor Error
motorError = cantools.database.can.Signal("Motor_Error", 0, 32)
motorErrorMsg = cantools.database.can.Message(0x003, "Get_Motor_Error", 8, [motorError])
# 0x004 - Encoder Error
encoderError = cantools.database.can.Signal("Encoder_Error", 0, 32)
encoderErrorMsg = cantools.database.can.Message(
0x004, "Get_Encoder_Error", 8, [encoderError]
)
# 0x005 - Sensorless Error
sensorlessError = cantools.database.can.Signal("Sensorless_Error", 0, 32)
sensorlessErrorMsg = cantools.database.can.Message(
0x005, "Get_Sensorless_Error", 8, [sensorlessError]
)
# 0x006 - Axis Node ID
axisNodeID = cantools.database.can.Signal("Axis_Node_ID", 0, 32)
axisNodeMsg = cantools.database.can.Message(0x006, "Set_Axis_Node_ID", 8, [axisNodeID])
# 0x007 - Requested State
axisRequestedState = cantools.database.can.Signal("Axis_Requested_State", 0, 32)
setAxisState = cantools.database.can.Message(
0x007, "Set_Axis_State", 8, [axisRequestedState]
)
# 0x008 - Startup Config (Reserved)
# 0x009 - Encoder Estimates
encoderPosEstimate = cantools.database.can.Signal("Pos_Estimate", 0, 32, is_float=True)
encoderVelEstimate = cantools.database.can.Signal("Vel_Estimate", 32, 32, is_float=True)
encoderEstimates = cantools.database.can.Message(
0x009, "Get_Encoder_Estimates", 8, [encoderPosEstimate, encoderVelEstimate]
)
# 0x00A - Get Encoder Count
encoderShadowCount = cantools.database.can.Signal("Shadow_Count", 0, 32)
encoderCountInCPR = cantools.database.can.Signal("Count_in_CPR", 32, 32)
encoderCountMsg = cantools.database.can.Message(
0x00A, "Get_Encoder_Count", 8, [encoderShadowCount, encoderCountInCPR]
)
# 0x00B - Set Controller Modes
controlMode = cantools.database.can.Signal("Control_Mode", 0, 32)
inputMode = cantools.database.can.Signal("Input_Mode", 32, 32)
setControllerModeMsg = cantools.database.can.Message(
0x00B, "Set_Controller_Mode", 8, [controlMode, inputMode]
)
# 0x00C - Set Input Pos
inputPos = cantools.database.can.Signal("Input_Pos", 0, 32, is_float=True)
velFF = cantools.database.can.Signal("Vel_FF", 32, 16, is_signed=True, scale=0.001)
torqueFF = cantools.database.can.Signal(
"Torque_FF", 48, 16, is_signed=True, scale=0.001
)
setInputPosMsg = cantools.database.can.Message(
0x00C, "Set_Input_Pos", 8, [inputPos, velFF, torqueFF]
)
# 0x00D - Set Input Vel
inputVel = cantools.database.can.Signal("Input_Vel", 0, 32, is_float=True)
inputTorqueFF = cantools.database.can.Signal("Input_Torque_FF", 32, 32, is_float=True)
setInputVelMsg = cantools.database.can.Message(
0x00D, "Set_Input_Vel", 8, [inputVel, inputTorqueFF]
)
# 0x00E - Set Input Torque
inputTorque = cantools.database.can.Signal("Input_Torque", 0, 32, is_float=True)
setInputTqMsg = cantools.database.can.Message(
0x00E, "Set_Input_Torque", 8, [inputTorque]
)
# 0x00F - Set Velocity Limit
velLimit = cantools.database.can.Signal("Velocity_Limit", 0, 32, is_float=True)
currentLimit = cantools.database.can.Signal("Current_Limit", 32, 32, is_float=True)
setVelLimMsg = cantools.database.can.Message(
0x00F, "Set_Limits", 8, [velLimit, currentLimit]
)
# 0x010 - Start Anticogging
startAnticoggingMsg = cantools.database.can.Message(0x010, "Start_Anticogging", 0, [])
# 0x011 - Set Traj Vel Limit
trajVelLim = cantools.database.can.Signal("Traj_Vel_Limit", 0, 32, is_float=True)
setTrajVelMsg = cantools.database.can.Message(
0x011, "Set_Traj_Vel_Limit", 8, [trajVelLim]
)
# 0x012 - Set Traj Accel Limits
trajAccelLim = cantools.database.can.Signal("Traj_Accel_Limit", 0, 32, is_float=True)
trajDecelLim = cantools.database.can.Signal("Traj_Decel_Limit", 32, 32, is_float=True)
setTrajAccelMsg = cantools.database.can.Message(
0x012, "Set_Traj_Accel_Limits", 8, [trajAccelLim, trajDecelLim]
)
# 0x013 - Set Traj Inertia
trajInertia = cantools.database.can.Signal("Traj_Inertia", 0, 32, is_float=True)
trajInertiaMsg = cantools.database.can.Message(
0x013, "Set_Traj_Inertia", 8, [trajInertia]
)
# 0x014 - Get Iq
iqSetpoint = cantools.database.can.Signal("Iq_Setpoint", 0, 32, is_float=True)
iqMeasured = cantools.database.can.Signal("Iq_Measured", 32, 32, is_float=True)
getIqMsg = cantools.database.can.Message(0x014, "Get_Iq", 8, [iqSetpoint, iqMeasured])
# 0x015 - Get Sensorless Estimates
sensorlessPosEstimate = cantools.database.can.Signal(
"Sensorless_Pos_Estimate", 0, 32, is_float=True
)
sensorlessVelEstimate = cantools.database.can.Signal(
"Sensorless_Vel_Estimate", 32, 32, is_float=True
)
getSensorlessEstMsg = cantools.database.can.Message(
0x015, "Get_Sensorless_Estimates", 8, [sensorlessPosEstimate, sensorlessVelEstimate]
)
# 0x016 - Reboot ODrive
rebootMsg = cantools.database.can.Message(0x016, "Reboot", 0, [])
# 0x017 - Get vbus Voltage
vbusVoltage = cantools.database.can.Signal("Vbus_Voltage", 0, 32, is_float=True)
getVbusVMsg = cantools.database.can.Message(0x017, "Get_Vbus_Voltage", 8, [vbusVoltage])
# 0x018 - Clear Errors
clearErrorsMsg = cantools.database.can.Message(0x018, "Clear_Errors", 0, [])
# 0x019 - Set Linear Count
position = cantools.database.can.Signal("Position", 0, 32, is_signed=True)
setLinearCountMsg = cantools.database.can.Message(0x019, "Set_Linear_Count", 8, [position])
db = cantools.database.can.Database(
[
heartbeatMsg,
estopMsg,
motorErrorMsg,
encoderErrorMsg,
sensorlessErrorMsg,
axisNodeMsg,
setAxisState,
encoderEstimates,
encoderCountMsg,
setControllerModeMsg,
setInputPosMsg,
setInputVelMsg,
setInputTqMsg,
setVelLimMsg,
startAnticoggingMsg,
setTrajVelMsg,
setTrajAccelMsg,
trajInertiaMsg,
getIqMsg,
getSensorlessEstMsg,
rebootMsg,
getVbusVMsg,
clearErrorsMsg,
setLinearCountMsg,
]
)
cantools.database.dump_file(db, "odrive-cansimple.dbc")
db = cantools.database.load_file("odrive-cansimple.dbc")
print(db)
+144
View File
@@ -0,0 +1,144 @@
VERSION ""
NS_ :
NS_DESC_
CM_
BA_DEF_
BA_
VAL_
CAT_DEF_
CAT_
FILTER
BA_DEF_DEF_
EV_DATA_
ENVVAR_DATA_
SGTYPE_
SGTYPE_VAL_
BA_DEF_SGTYPE_
BA_SGTYPE_
SIG_TYPE_REF_
VAL_TABLE_
SIG_GROUP_
SIG_VALTYPE_
SIGTYPE_VALTYPE_
BO_TX_BU_
BA_DEF_REL_
BA_REL_
BA_DEF_DEF_REL_
BU_SG_REL_
BU_EV_REL_
BU_BO_REL_
SG_MUL_VAL_
BS_:
BU_:
BO_ 1 Heartbeat: 8 Vector__XXX
SG_ Axis_State : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Axis_Error : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2 Estop: 0 Vector__XXX
BO_ 3 Get_Motor_Error: 8 Vector__XXX
SG_ Motor_Error : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 4 Get_Encoder_Error: 8 Vector__XXX
SG_ Encoder_Error : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 5 Get_Sensorless_Error: 8 Vector__XXX
SG_ Sensorless_Error : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 6 Set_Axis_Node_ID: 8 Vector__XXX
SG_ Axis_Node_ID : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 7 Set_Axis_State: 8 Vector__XXX
SG_ Axis_Requested_State : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 9 Get_Encoder_Estimates: 8 Vector__XXX
SG_ Vel_Estimate : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Pos_Estimate : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 10 Get_Encoder_Count: 8 Vector__XXX
SG_ Count_in_CPR : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Shadow_Count : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 11 Set_Controller_Mode: 8 Vector__XXX
SG_ Input_Mode : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Control_Mode : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 12 Set_Input_Pos: 8 Vector__XXX
SG_ Torque_FF : 48|16@1- (0.001,0) [0|0] "" Vector__XXX
SG_ Vel_FF : 32|16@1- (0.001,0) [0|0] "" Vector__XXX
SG_ Input_Pos : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 13 Set_Input_Vel: 8 Vector__XXX
SG_ Input_Torque_FF : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Input_Vel : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 14 Set_Input_Torque: 8 Vector__XXX
SG_ Input_Torque : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 15 Set_Limits: 8 Vector__XXX
SG_ Current_Limit : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Velocity_Limit : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 16 Start_Anticogging: 0 Vector__XXX
BO_ 17 Set_Traj_Vel_Limit: 8 Vector__XXX
SG_ Traj_Vel_Limit : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 18 Set_Traj_Accel_Limits: 8 Vector__XXX
SG_ Traj_Decel_Limit : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Traj_Accel_Limit : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 19 Set_Traj_Inertia: 8 Vector__XXX
SG_ Traj_Inertia : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 20 Get_Iq: 8 Vector__XXX
SG_ Iq_Measured : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Iq_Setpoint : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 21 Get_Sensorless_Estimates: 8 Vector__XXX
SG_ Sensorless_Vel_Estimate : 32|32@1+ (1,0) [0|0] "" Vector__XXX
SG_ Sensorless_Pos_Estimate : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 22 Reboot: 0 Vector__XXX
BO_ 23 Get_Vbus_Voltage: 8 Vector__XXX
SG_ Vbus_Voltage : 0|32@1+ (1,0) [0|0] "" Vector__XXX
BO_ 24 Clear_Errors: 0 Vector__XXX
BO_ 25 Set_Linear_Count: 8 Vector__XXX
SG_ Position : 0|32@1- (1,0) [0|0] "" Vector__XXX
SIG_VALTYPE_ 9 Pos_Estimate : 1;
SIG_VALTYPE_ 9 Vel_Estimate : 1;
SIG_VALTYPE_ 12 Input_Pos : 1;
SIG_VALTYPE_ 13 Input_Vel : 1;
SIG_VALTYPE_ 13 Input_Torque_FF : 1;
SIG_VALTYPE_ 14 Input_Torque : 1;
SIG_VALTYPE_ 15 Velocity_Limit : 1;
SIG_VALTYPE_ 15 Current_Limit : 1;
SIG_VALTYPE_ 17 Traj_Vel_Limit : 1;
SIG_VALTYPE_ 18 Traj_Accel_Limit : 1;
SIG_VALTYPE_ 18 Traj_Decel_Limit : 1;
SIG_VALTYPE_ 19 Traj_Inertia : 1;
SIG_VALTYPE_ 20 Iq_Setpoint : 1;
SIG_VALTYPE_ 20 Iq_Measured : 1;
SIG_VALTYPE_ 21 Sensorless_Pos_Estimate : 1;
SIG_VALTYPE_ 21 Sensorless_Vel_Estimate : 1;
SIG_VALTYPE_ 23 Vbus_Voltage : 1;
+77 -21
View File
@@ -15,42 +15,98 @@ del get_version_str
from .utils import get_serial_number_str, get_serial_number_str_sync
import threading
import time
default_usb_search_path = 'usb:idVendor=0x1209,idProduct=0x0D32,bInterfaceClass=0,bInterfaceSubClass=1,bInterfaceProtocol=0'
default_search_path = default_usb_search_path
def find_any(path=default_search_path, serial_number=None,
search_cancellation_token=None, channel_termination_token=None,
timeout=None, logger=fibre.Logger(verbose=False)):
"""
Blocks until the first matching ODrive object is connected and then returns that object
"""
result = []
_discovery_lock = threading.Lock()
_discovery_started = [False]
_discovery_path = [None]
_discovery_signal = threading.Condition()
_objects = []
done_signal = fibre.Event(search_cancellation_token)
channel_termination_token = fibre.Event(channel_termination_token)
def _start_discovery(path):
_domain_termination_token = fibre.Event()
async def discovered_object(obj):
if not (serial_number is None) and ((await get_serial_number_str(obj)) != serial_number):
return # ignore this device
def lost_object(_):
idx = [i for i, (o, _) in enumerate(_objects) if o == obj][0]
_objects.pop(idx)
obj._on_lost.add_done_callback(lambda x: channel_termination_token.set())
result.append(obj)
done_signal.set()
_objects.append((obj, await get_serial_number_str(obj)))
obj._on_lost.add_done_callback(lost_object)
with _discovery_signal:
_discovery_signal.notify_all()
def domain_thread():
with fibre.Domain(path) as domain:
discovery = domain.run_discovery(discovered_object)
channel_termination_token.wait()
_domain_termination_token.wait()
discovery.stop()
threading.Thread(target=domain_thread, daemon=True).start()
def find_any(path=default_search_path, serial_number=None, cancellation_token=None, timeout=None):
"""
Blocks until the first matching ODrive object is connected and then returns
that object.
If find_any() is called multiple times, the same object may be returned (
depending on the serial_number argument).
The first call to find_any() will start a background thread that handles
the backend. This background thread will keep running until the program is
terminated.
threading.Thread(target=domain_thread).start()
If you want finer grained control over object discovery
consider using fibre.Domain directly.
"""
assert(cancellation_token is None or isinstance(cancellation_token, fibre.Event))
# Start backend if it's not already started
with _discovery_lock:
if not _discovery_started[0]:
_start_discovery(path)
_discovery_started[0] = True
_discovery_path[0] = path
elif path != _discovery_path[0]:
raise Exception("Cannot change discovery path between multiple find_any() "
"calls: {} != {}. Use fibre.Domain() directly for finer "
"grained discovery control.".format(path, _discovery_path))
cancelled = [False]
def cancel():
with _discovery_signal:
cancelled[0] = True
_discovery_signal.notify_all()
try:
done_signal.wait(timeout=timeout)
except:
channel_termination_token.set()
raise
if cancellation_token:
cancellation_token.subscribe(cancel)
wait_start = time.monotonic()
with _discovery_signal:
while True:
# If the ODrive was already found, return it now
for (obj, s) in _objects:
if (serial_number is None) or (serial_number == s):
return obj
current_timeout = None if timeout is None else min(0, timeout - (time.monotonic() - wait_start))
_discovery_signal.wait(current_timeout)
# TODO: it would be more sensible to raise an exception here but
# DFU implementation assumes that None is returned on cancellation.
if cancelled[0]:
return None
finally:
if cancellation_token:
cancellation_token.unsubscribe(cancel)
return result[0] if len(result) > 0 else None
-69
View File
@@ -1,69 +0,0 @@
import jinja2
import os
import json
def get_flat_endpoint_list(json, prefix, id_offset):
flat_list = []
for item in json:
item = item.copy()
if 'id' in item:
item['id'] -= id_offset
if 'type' in item:
if item['type'] in {'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64'}:
item['type'] += '_t'
is_property = True
elif item['type'] in {'bool', 'float'}:
is_property = True
elif item['type'] in {'function'}:
if len(item.get('arguments', [])) == 0 and len(item.get('inputs', [])) == 0 and len(item.get('outputs', [])) == 0:
item['type'] = 'void'
is_property = True
else:
is_property = False
else:
is_property = False
if is_property:
item['name'] = prefix + item['name']
flat_list.append(item)
if 'members' in item:
flat_list = flat_list + get_flat_endpoint_list(item['members'], prefix + item['name'] + '.', id_offset)
return flat_list
def generate_code(odrv, template_file, output_file):
json_data = odrv._json_data
json_crc = odrv._json_crc
axis0_json = [item for item in json_data if item['name'].startswith("axis0")][0]
axis1_json = [item for item in json_data if item['name'].startswith("axis1")][0]
json_data = [item for item in json_data if not item['name'].startswith("axis")]
endpoints = get_flat_endpoint_list(json_data, '', 0)
per_axis_offset = axis1_json['members'][0]['id'] - axis0_json['members'][0]['id']
axis_endpoints = get_flat_endpoint_list(axis0_json['members'], 'axis.', 0)
axis_endpoints_copy = get_flat_endpoint_list(axis1_json['members'], 'axis.', per_axis_offset)
if axis_endpoints != axis_endpoints_copy:
raise Exception("axis0 and axis1 don't look exactly equal")
env = jinja2.Environment(
#loader = jinja2.FileSystemLoader("/Data/Projects/")
#trim_blocks=True,
#lstrip_blocks=True
)
# Expose helper functions to jinja template code
#env.filters["delimit"] = camel_case_to_words
#import ipdb; ipdb.set_trace()
# Load and render template
template = env.from_string(template_file.read())
output = template.render(
json_crc=json_crc,
endpoints=endpoints,
per_axis_offset=per_axis_offset,
axis_endpoints=axis_endpoints,
output_name=os.path.basename(output_file.name)
)
# Output
output_file.write(output)
+54 -16
View File
@@ -163,7 +163,7 @@ class FirmwareFromGithub(Firmware):
"""
if self.hex is None:
print("Downloading firmware {}...".format(get_fw_version_string(self.fw_version)))
response = requests.get('https://api.github.com/repos/madcowswe/ODrive/releases/assets/' + str(self.github_asset_id),
response = requests.get('https://api.github.com/repos/odriverobotics/ODrive/releases/assets/' + str(self.github_asset_id),
headers={'Accept': 'application/octet-stream'})
if response.status_code != 200:
raise Exception("failed to download firmware")
@@ -178,7 +178,7 @@ class FirmwareFromFile(Firmware):
return self._file
def get_all_github_firmwares():
response = requests.get('https://api.github.com/repos/madcowswe/ODrive/releases')
response = requests.get('https://api.github.com/repos/odriverobotics/ODrive/releases')
if response.status_code != 200:
raise Exception("could not fetch releases")
response_json = response.json()
@@ -254,6 +254,38 @@ def find_device_in_dfu_mode(serial_number, cancellation_token):
time.sleep(1)
return None
def get_hw_version_in_dfu_mode(dfudev):
"""
Reads the hardware version from one-time-programmable memory.
This is written on all ODrives sold since Summer 2018.
"""
otp_sector = [s for s in dfudev.sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7800][0]
otp_data = dfudev.read_sector(otp_sector)
if otp_data[0] == 0:
otp_data = otp_data[16:]
if otp_data[0] == 0xfe:
return (otp_data[3], otp_data[4], otp_data[5])
else:
return None
def unlock_device(serial_number, cancellation_token):
print("Looking for ODrive in DFU mode...")
print("If the program hangs at this point, try to set the DFU switch to \"DFU\" and power cycle the ODrive.")
stm_device = find_device_in_dfu_mode(serial_number, cancellation_token)
dfudev = DfuDevice(stm_device)
print("Unlocking device (this may take a few seconds)...")
dfudev.unprotect()
print("done")
print("")
print("Now do the following:")
print(" 1. Put the DFU switch on the ODrive to \"DFU\"")
print(" 2. Power-cycle the ODrive")
print(" 3. Run \"odrivetool dfu\" (or any third party DFU tool)")
print(" 4. Put the DFU switch on the ODrive to \"RUN\"")
def update_device(device, firmware, logger, cancellation_token):
"""
Updates the specified device with the specified firmware.
@@ -271,16 +303,8 @@ def update_device(device, firmware, logger, cancellation_token):
if (logger._verbose):
logger.debug("OTP:")
dump_otp(dfudev)
hw_version = get_hw_version_in_dfu_mode(dfudev) or (0, 0, 0)
# Read hardware version from one-time-programmable memory
otp_sector = [s for s in dfudev.sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7800][0]
otp_data = dfudev.read_sector(otp_sector)
if otp_data[0] == 0:
otp_data = otp_data[16:]
if otp_data[0] == 0xfe:
hw_version = (otp_data[3], otp_data[4], otp_data[5])
else:
hw_version = (0, 0, 0)
else:
found_in_dfu = False
serial_number = "{:08X}".format(device.serial_number)
@@ -303,7 +327,7 @@ def update_device(device, firmware, logger, cancellation_token):
fw_version_major = device.fw_version_major if hasattr(device, 'fw_version_major') else 0
fw_version_minor = device.fw_version_minor if hasattr(device, 'fw_version_minor') else 0
fw_version_revision = device.fw_version_revision if hasattr(device, 'fw_version_revision') else 0
fw_version_prerelease = device.fw_version_prerelease if hasattr(device, 'fw_version_prerelease') else True
fw_version_prerelease = device.fw_version_unreleased != 0 if hasattr(device, 'fw_version_unreleased') else True
fw_version = (fw_version_major, fw_version_minor, fw_version_revision, fw_version_prerelease)
print("Found ODrive {} ({}) with firmware {}{}".format(
@@ -365,6 +389,16 @@ def update_device(device, firmware, logger, cancellation_token):
find_odrive_cancellation_token.set()
dfudev = DfuDevice(stm_device)
hw_version = get_hw_version_in_dfu_mode(dfudev)
if hw_version is None:
logger.error("Could not determine hardware version. Flashing precompiled "
"firmware could lead to unexpected results. Please use an "
"STLink/2 to force-update the firmware anyway. Refer to "
"https://docs.odriverobotics.com/developer-guide for details.")
# Jump to application
dfudev.jump_to_application(0x08000000)
return
logger.debug("Sectors on device: ")
for sector in dfudev.sectors:
logger.debug(" {:08X} to {:08X} ({})".format(
@@ -428,14 +462,18 @@ def update_device(device, firmware, logger, cancellation_token):
if not found_in_dfu:
logger.info("Waiting for the device to reappear...")
device = odrive.find_any(odrive.default_usb_search_path, serial_number,
cancellation_token, cancellation_token, timeout=30)
cancellation_token, timeout=30)
if do_backup_config:
temp_config_filename = odrive.configuration.get_temp_config_filename(device)
odrive.configuration.restore_config(device, None, logger)
os.remove(temp_config_filename)
logger.success("Device firmware update successful.")
logger.success("Device firmware update successful.")
else:
logger.success("Firmware upload successful.")
logger.info("To complete the firmware update, set the DFU switch to \"RUN\" and power cycle the board.")
def launch_dfu(args, logger, cancellation_token):
"""
@@ -462,7 +500,7 @@ def launch_dfu(args, logger, cancellation_token):
# Scan for ODrives not in DFU mode
# We only scan on USB because DFU is only implemented over USB
devices[1] = odrive.find_any(odrive.default_usb_search_path, serial_number,
find_odrive_cancellation_token, cancellation_token)
find_odrive_cancellation_token)
find_odrive_cancellation_token.set()
device = devices[0] or devices[1]

Some files were not shown because too many files have changed in this diff Show More