mirror of
https://github.com/odriverobotics/ODrive.git
synced 2026-08-18 01:18:52 +08:00
@@ -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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[submodule "Firmware/Private"]
|
||||
path = Firmware/Private
|
||||
url = git@github.com:madcowswe/ODrivePrivate.git
|
||||
branch = submodule
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 --------------------------------------------------------*/
|
||||
|
||||
@@ -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
|
||||
|
||||
Submodule Firmware/Private deleted from 2bdcd0acc2
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ------------------------------------*/
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -308,6 +308,10 @@ let odriveEnums = {
|
||||
{
|
||||
text: "Mirror",
|
||||
value: 7
|
||||
},
|
||||
{
|
||||
text: "Tuning",
|
||||
value: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||

|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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 |
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user