diff --git a/.gitignore b/.gitignore index 4a8546b5..db493d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,34 +6,6 @@ __pycache__/ # C extensions *.so -# Distribution / packaging -.Python -#env/ -#build/ -#develop-eggs/ -#dist/ -#downloads/ -#eggs/ -#.eggs/ -#lib/ -#lib64/ -#parts/ -#sdist/ -#var/ -#*.egg-info/ -#.installed.cfg -#*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - # Unit test / coverage reports htmlcov/ .tox/ diff --git a/.travis.yml b/.travis.yml index b541bbd6..d530f0a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ # adapted from https://github.com/andysworkshop/stm32plus/blob/master/.travis.yml branches: - only: - - master - - devel + only: + - master + - devel + - /^fw-v/ language: c sudo: false @@ -15,39 +16,42 @@ addons: cache: directories: - - $HOME/dl + - "$HOME/dl" install: - - export GCC_DIR=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4 - - export GCC_ARCHIVE=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 - - export GCC_URL=https://launchpad.net/gcc-arm-embedded/5.0/5-2015-q4-major/+download/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 - - if [ ! -e $GCC_DIR/bin/arm-none-eabi-gcc ]; then wget $GCC_URL -O $GCC_ARCHIVE; tar xfj $GCC_ARCHIVE -C $HOME/dl; fi - - export PATH=$PATH:$GCC_DIR/bin - - export TUP_DIR=$HOME/dl/tup_0.7.5-0~16.04.york0_amd64 - - export TUP_ARCHIVE=$HOME/dl/tup_0.7.5-0~16.04.york0_amd64.deb - - export TUP_URL=http://ppa.launchpad.net/jonathonf/tup/ubuntu/pool/main/t/tup/tup_0.7.5-0~16.04.york0_amd64.deb - - if [ ! -e $TUP_DIR/bin/tup ]; then wget $TUP_URL -O $TUP_ARCHIVE; dpkg-deb -R $TUP_ARCHIVE $TUP_DIR; fi - - export PATH=$PATH:$TUP_DIR/usr/bin +- export GCC_DIR=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4 +- export GCC_ARCHIVE=$HOME/dl/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 +- export GCC_URL=https://launchpad.net/gcc-arm-embedded/5.0/5-2015-q4-major/+download/gcc-arm-none-eabi-5_2-2015q4-20151219-linux.tar.bz2 +- if [ ! -e $GCC_DIR/bin/arm-none-eabi-gcc ]; then wget $GCC_URL -O $GCC_ARCHIVE; tar xfj $GCC_ARCHIVE -C $HOME/dl; fi +- export PATH=$PATH:$GCC_DIR/bin +- export TUP_DIR=$HOME/dl/tup_0.7.5-0~16.04.york0_amd64 +- export TUP_ARCHIVE=$HOME/dl/tup_0.7.5-0~16.04.york0_amd64.deb +- export TUP_URL=http://ppa.launchpad.net/jonathonf/tup/ubuntu/pool/main/t/tup/tup_0.7.5-0~16.04.york0_amd64.deb +- if [ ! -e $TUP_DIR/bin/tup ]; then wget $TUP_URL -O $TUP_ARCHIVE; dpkg-deb -R $TUP_ARCHIVE $TUP_DIR; fi +- export PATH=$PATH:$TUP_DIR/usr/bin env: # Build default configuration for each board - - "CONFIG_BOARD_VERSION=v3.2 DEPLOY=v3.2" - - "CONFIG_BOARD_VERSION=v3.3 DEPLOY=v3.3" - - "CONFIG_BOARD_VERSION=v3.4-24V DEPLOY=v3.4-24V" - - "CONFIG_BOARD_VERSION=v3.4-48V DEPLOY=v3.4-48V" + - CONFIG_BOARD_VERSION=v3.2 DEPLOY=v3.2 + - CONFIG_BOARD_VERSION=v3.3 DEPLOY=v3.3 + - CONFIG_BOARD_VERSION=v3.4-24V DEPLOY=v3.4-24V + - CONFIG_BOARD_VERSION=v3.4-48V DEPLOY=v3.4-48V - # Various protocol combinations - - "CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=native-stream CONFIG_UART_PROTOCOL=native" - - "CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=none CONFIG_UART_PROTOCOL=none" + # Various protocol combinations + - CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=native-stream CONFIG_UART_PROTOCOL=native + - CONFIG_BOARD_VERSION=v3.4-24V CONFIG_USB_PROTOCOL=none CONFIG_UART_PROTOCOL=none script: - - ./tools/build.sh +- "./Firmware/build.sh" deploy: provider: releases - api_key: "GITHUB OAUTH TOKEN" - file: Firmware/deploy/* + api_key: + secure: RM66joGTn11Z5PmG7Nlj8cRcVY0w7ga5qUl2ahinbDm6jMV8OxroaXKivEa478alx9fygMHVeKdJZUlrAkRJ5OYJ1trfpW+43S3OYEnfy1nyXEXRwhgeIlb9LqdrumXVAp7TZ0Vppfom8A2ZWbxKxW3lG/EAmA4G9fnxHf0S9rF0y95YVfGrxdapTKcxvbP7Yojo53474ZI6+VYrqx8lq0JAnn4FwNT9ZJ1QASrmIw4w08f60XXv25BzndCTscvLb2qUu0AaGLbQUosde0Bb7P+aQsBVY6uSkg9MWV8gWPQjtO3u5IRR1bTshxf2kPqtzwK+SpcYrddoGN6BkKAB3lVorJIW5VguUkRmtPZ1K9+NhIztNevB2qr0ASutumNLF3aqMt19KL3A+SRx6froj5VhRHf4i/Xjm3SDLTaTcc8ZIh2PEE6scUMUMs5Mzu8LQWjInRe25MSb+pQB1mNOHmoFBtVb0J3u7Nvs8jdImN5gQWvvowWXfXRNE0ncT1YsLmevwi3q+YdEjpAIPnrD/rouY8WaqQZ/vE15JM9uwdQRqKAbzGtMaKHDk7EZ7ANTyaP+UrQ/M5cVDa0bWsWSvqSqDJMy4IVHRlirYA/5u74lXNhmA8DGDB/gFVlVCmoEzas/pnYiAE1hh4RpsYxts78Ix+wbeo1hmt7t65X8cyo= skip_cleanup: true + file_glob: true + file: Firmware/deploy/* on: + repo: madcowswe/ODrive branch: master tags: true diff --git a/Firmware/.vscode/c_cpp_properties.json b/Firmware/.vscode/c_cpp_properties.json index b337bc20..de6eb463 100644 --- a/Firmware/.vscode/c_cpp_properties.json +++ b/Firmware/.vscode/c_cpp_properties.json @@ -5,17 +5,17 @@ "includePath": [ "${workspaceRoot}", "${workspaceRoot}/MotorControl", - "${workspaceRoot}/Board/v3.3/Inc", - "${workspaceRoot}/Board/v3.3/Drivers/CMSIS/Include", - "${workspaceRoot}/Board/v3.3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Board/v3.3/Drivers/DRV8301", - "${workspaceRoot}/Board/v3.3/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Board/v3.3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Board/v3.3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Board/v3.3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Board/v3.3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Board/v3.3/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Board/v3.3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", + "${workspaceRoot}/Drivers/DRV8301", + "${workspaceRoot}/Board/v3/Inc", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", "c:/program files (x86)/gnu tools arm embedded/6 2017-q1-update/arm-none-eabi/include/c++/6.3.1", "c:/program files (x86)/gnu tools arm embedded/6 2017-q1-update/arm-none-eabi/include/c++/6.3.1/arm-none-eabi", "c:/program files (x86)/gnu tools arm embedded/6 2017-q1-update/arm-none-eabi/include/c++/6.3.1/backward", @@ -39,79 +39,82 @@ "limitSymbolsToIncludedHeaders": true } }, - { - "name": "Linux", - "includePath": [ - "${workspaceRoot}", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Drivers/DRV8301", - "${workspaceRoot}/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Drivers/CMSIS/Include", - "${workspaceRoot}/Inc", - "${workspaceRoot}/MotorControl", - "/usr/lib/gcc/arm-none-eabi/4.9.3/include", - "/usr/lib/arm-none-eabi/include" - ], - "defines": [ + { + "name": "Linux", + "includePath": [ + "${workspaceRoot}", + "${workspaceRoot}/MotorControl", + "${workspaceRoot}/Drivers/DRV8301", + "${workspaceRoot}/Board/v3/Inc", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", + "/usr/lib/gcc/arm-none-eabi/4.9.3/include", + "/usr/lib/arm-none-eabi/include" + ], + "defines": [ "STM32F405xx", "USE_HAL_DRIVER", "__weak=\"__attribute__((weak))\"", "__packed=\"__attribute__((__packed__))\"", "__GNUC__" - ], - "intelliSenseMode": "clang-x64", - "browse": { - "path": [ + ], + "intelliSenseMode": "clang-x64", + "browse": { + "path": [ "${workspaceRoot}", - "/usr/lib/gcc/arm-none-eabi/4.9.3/include", - "/usr/lib/arm-none-eabi/include" - ], - "limitSymbolsToIncludedHeaders": true, - "databaseFilename": "" - } - }, - { - "name": "Mac", - "includePath": [ - "${workspaceRoot}", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/include", - "${workspaceRoot}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", - "${workspaceRoot}/Drivers/DRV8301", - "${workspaceRoot}/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", - "${workspaceRoot}/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", - "${workspaceRoot}/Drivers/STM32F4xx_HAL_Driver/Inc", - "${workspaceRoot}/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", - "${workspaceRoot}/Drivers/CMSIS/Device/ST/STM32F4xx/Include", - "${workspaceRoot}/Drivers/CMSIS/Include", - "${workspaceRoot}/Inc", - "${workspaceRoot}/MotorControl", - "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include", - "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1", - "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1/arm-none-eabi", - "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/lib/gcc/arm-none-eabi/6.3.1/include" - ], - "defines": [ + "/usr/lib/gcc/arm-none-eabi/4.9.3/include", + "/usr/lib/arm-none-eabi/include" + ], + "limitSymbolsToIncludedHeaders": true, + "databaseFilename": "" + }, + "compilerPath": "arm-none-eabi-gcc -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++14" + }, + { + "name": "Mac", + "includePath": [ + "${workspaceRoot}", + "${workspaceRoot}/MotorControl", + "${workspaceRoot}/Drivers/DRV8301", + "${workspaceRoot}/Board/v3/Inc", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/include", + "${workspaceRoot}/Board/v3/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", + "${workspaceRoot}/Board/v3/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc", + "${workspaceRoot}/Board/v3/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Device/ST/STM32F4xx/Include", + "${workspaceRoot}/Board/v3/Drivers/CMSIS/Include", + "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include", + "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1", + "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1/arm-none-eabi", + "/usr/local/Caskroom/gcc-arm-embedded/6-2017-q2-update/gcc-arm-none-eabi-6-2017-q2-update/lib/gcc/arm-none-eabi/6.3.1/include" + ], + "defines": [ "STM32F405xx", "USE_HAL_DRIVER", "__weak=\"__attribute__((weak))\"", "__packed=\"__attribute__((__packed__))\"", "__GNUC__" - ], - "intelliSenseMode": "clang-x64", - "browse": { - "path": [ + ], + "intelliSenseMode": "clang-x64", + "browse": { + "path": [ "${workspaceRoot}", "/usr/local/Caskroom/gcc-arm-embedded/" - ], - "limitSymbolsToIncludedHeaders": true, - "databaseFilename": "" + ], + "limitSymbolsToIncludedHeaders": true, + "databaseFilename": "" } } ], diff --git a/Firmware/Board/v3/0001-display-correct-ODrive-version-in-USB-descriptor.patch b/Firmware/Board/v3/0001-display-correct-ODrive-version-in-USB-descriptor.patch index f57298b0..0b031242 100644 --- a/Firmware/Board/v3/0001-display-correct-ODrive-version-in-USB-descriptor.patch +++ b/Firmware/Board/v3/0001-display-correct-ODrive-version-in-USB-descriptor.patch @@ -14,7 +14,7 @@ index 94dc49b..37b4302 100644 @@ -96,7 +96,9 @@ #define USBD_LANGID_STRING 1033 #define USBD_MANUFACTURER_STRING "ODrive Robotics" - #define USBD_PID_FS 0x0D33 + #define USBD_PID_FS 0x0D32 -#define USBD_PRODUCT_STRING_FS "ODrive v3.3" +#define USBD_PRODUCT_XSTR(s) USBD_PRODUCT_STR(s) +#define USBD_PRODUCT_STR(s) #s diff --git a/Firmware/Board/v3/Inc/FreeRTOSConfig.h b/Firmware/Board/v3/Inc/FreeRTOSConfig.h index 6da2e557..cf2d6f0e 100644 --- a/Firmware/Board/v3/Inc/FreeRTOSConfig.h +++ b/Firmware/Board/v3/Inc/FreeRTOSConfig.h @@ -102,8 +102,7 @@ #define configTICK_RATE_HZ ((TickType_t)1000) #define configMAX_PRIORITIES ( 7 ) #define configMINIMAL_STACK_SIZE ((uint16_t)128) -#define configTOTAL_HEAP_SIZE ((size_t)65536) // FreeRTOS heap takes up the entire 64kB core-coupled memory -#define configAPPLICATION_ALLOCATED_HEAP 1 +#define configTOTAL_HEAP_SIZE ((size_t)65536) #define configMAX_TASK_NAME_LEN ( 16 ) #define configUSE_16_BIT_TICKS 0 #define configUSE_MUTEXES 1 diff --git a/Firmware/Board/v3/Inc/main.h b/Firmware/Board/v3/Inc/main.h index 50256101..ac9bd857 100644 --- a/Firmware/Board/v3/Inc/main.h +++ b/Firmware/Board/v3/Inc/main.h @@ -68,6 +68,7 @@ #define TIM_APB1_CLOCK_HZ 84000000 #define TIM_APB1_PERIOD_CLOCKS 4096 #define TIM_APB1_DEADTIME_CLOCKS 40 +#define configAPPLICATION_ALLOCATED_HEAP 1 #define M0_nCS_Pin GPIO_PIN_13 #define M0_nCS_GPIO_Port GPIOC diff --git a/Firmware/Board/v3/Odrive.ioc b/Firmware/Board/v3/Odrive.ioc index b27ffb67..583cb7f7 100644 --- a/Firmware/Board/v3/Odrive.ioc +++ b/Firmware/Board/v3/Odrive.ioc @@ -101,9 +101,11 @@ Dma.UART4_TX.1.PeriphDataAlignment=DMA_PDATAALIGN_BYTE Dma.UART4_TX.1.PeriphInc=DMA_PINC_DISABLE Dma.UART4_TX.1.Priority=DMA_PRIORITY_LOW Dma.UART4_TX.1.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode +FREERTOS.FootprintOK=true FREERTOS.INCLUDE_vTaskDelayUntil=1 -FREERTOS.IPParameters=Tasks01,INCLUDE_vTaskDelayUntil +FREERTOS.IPParameters=Tasks01,INCLUDE_vTaskDelayUntil,configTOTAL_HEAP_SIZE,FootprintOK FREERTOS.Tasks01=defaultTask,-3,256,StartDefaultTask,Default +FREERTOS.configTOTAL_HEAP_SIZE=65536 File.Version=6 KeepUserPlacement=true Mcu.Family=STM32F4 @@ -186,7 +188,7 @@ Mcu.Pin8=PC3 Mcu.Pin9=PA0-WKUP Mcu.PinsNb=56 Mcu.ThirdPartyNb=0 -Mcu.UserConstants=TIM_1_8_CLOCK_HZ,168000000;TIM_1_8_PERIOD_CLOCKS,10192;TIM_1_8_DEADTIME_CLOCKS,20;TIM_APB1_CLOCK_HZ,84000000;TIM_APB1_PERIOD_CLOCKS,4096;TIM_APB1_DEADTIME_CLOCKS,40 +Mcu.UserConstants=TIM_1_8_CLOCK_HZ,168000000;TIM_1_8_PERIOD_CLOCKS,10192;TIM_1_8_DEADTIME_CLOCKS,20;TIM_APB1_CLOCK_HZ,84000000;TIM_APB1_PERIOD_CLOCKS,4096;TIM_APB1_DEADTIME_CLOCKS,40;configAPPLICATION_ALLOCATED_HEAP,1 Mcu.UserName=STM32F405RGTx MxCube.Version=4.24.0 MxDb.Version=DB.4.0.240 @@ -597,7 +599,7 @@ USB_DEVICE.APP_TX_DATA_SIZE-CDC_FS=64 USB_DEVICE.CLASS_NAME_FS=CDC USB_DEVICE.IPParameters=VirtualMode-CDC_FS,VirtualModeFS,CLASS_NAME_FS,MANUFACTURER_STRING-CDC_FS,PRODUCT_STRING_CDC_FS,VID-CDC_FS,PID_CDC_FS,SERIALNUMBER_STRING_CDC_FS,APP_RX_DATA_SIZE-CDC_FS,APP_TX_DATA_SIZE-CDC_FS USB_DEVICE.MANUFACTURER_STRING-CDC_FS=ODrive Robotics -USB_DEVICE.PID_CDC_FS=0x0D33 +USB_DEVICE.PID_CDC_FS=0x0D32 USB_DEVICE.PRODUCT_STRING_CDC_FS=ODrive v3.3 USB_DEVICE.SERIALNUMBER_STRING_CDC_FS=000000000001 USB_DEVICE.VID-CDC_FS=0x1209 diff --git a/Firmware/Board/v3/Src/usbd_desc.c b/Firmware/Board/v3/Src/usbd_desc.c index 71b353a8..5fb9ac37 100644 --- a/Firmware/Board/v3/Src/usbd_desc.c +++ b/Firmware/Board/v3/Src/usbd_desc.c @@ -95,7 +95,7 @@ #define USBD_VID 0x1209 #define USBD_LANGID_STRING 1033 #define USBD_MANUFACTURER_STRING "ODrive Robotics" -#define USBD_PID_FS 0x0D33 +#define USBD_PID_FS 0x0D32 #define USBD_PRODUCT_XSTR(s) USBD_PRODUCT_STR(s) #define USBD_PRODUCT_STR(s) #s #define USBD_PRODUCT_STRING_FS ODrive version HW_VERSION_MAJOR.HW_VERSION_MINOR diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index 4a026b9a..1ffc7ef9 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -1,10 +1,33 @@ # Unreleased Features Please add a note of your changes below this heading if you make a Pull Request. +### Added + * `make write_otp` command to burn the board version onto the ODrive's one-time programmable memory. If you have an ODrive v3.4 or older, you should run this once for a better firmware update user experience in the future. Run the command without any options for more details. Once set, the board version is exposed through the `board_version_[...]` properties. + * bake Git-derived firmware version into firmware binary. The firmware version is exposed through the `fw_version_[...]` properties. + * infrastructure to publish the python tools to PyPi. See `tools/setup.py` for details. + +### Changed + * The DFU script now verifies the flash after writing + * Refactor python tools + * The scripts `explore_odrive.py`, `liveplotter.py`, `drv_status.py` and `rate_test.py` have been merged into one single `odrivetool` script. Running this script without any arguments provides the shell that `explore_odrive.py` used to provide. + * The command line options of `odrivetool` have changed compared to the original `explore_odrive.py`. See `odrivetool --help` for more details. + * `odrivetool` (previously `explore_odrive.py`) now supports controlling multiple ODrives concurrently (`odrv0`, `odrv1`, ...) + * No need to restart the `odrivetool` shell when devices get disconnected and reconnected + * ODrive accesses from within python tools are now thread-safe. That means you can read from the same remote property from multiple threads concurrently. + * The liveplotter (`odrivetool liveplotter`, formerly `liveplotter.py`) does no longer steal focus and closes as expected + * (experimental: start liveplotter from `odrivetool` shell by typing `start_liveplotter(lambda: odrv0.motor0.encoder.encoder_state)`) + +### Fixed + +# Releases + +## [0.3.6] - 2018-03-26 + ### Added * **Storing of configuration parameters to Non Volatile Memory** * **USB Bootloader** -* `make erase_config` to erase the configuration with an STLink (the configuration can also be erased from within explore_odrive.py, using `my_odrive.erase_configuration()`) +* `make erase_config` to erase the configuration with an STLink (the configuration can also be erased from within explore_odrive.py, using `odrv0.erase_configuration()`) +* Travis-CI builds firmware for all board versions and deploys the binaries when a tag is pushed to master ### Changed * Most of the code from `lowlevel.c` moved to `axis.cpp`, `encoder.cpp`, `controller.cpp`, `sensorless_estimator.cpp`, `motor.cpp` and the corresponding header files @@ -15,19 +38,10 @@ Please add a note of your changes below this heading if you make a Pull Request. * Update CubeMX generated STM platform code to version 1.19.0 * Remove `UUID_0`, `UUID_1` and `UUID_2` from USB protocol. Use `serial_number` instead. * Freertos memory pool (task stacks, etc) now uses Core Coupled Memory. -* Refactor python tools - * `explore_odrive.py` now supports controlling multiple ODrives concurrently (`odrv0`, `odrv1`, ...) - * No need to restart `explore_odrive.py` when devices get disconnected and reconnected - * The command line arguments of `explore_odrive.py` have changed. See `explore_odrive.py --help` for more details. - * ODrive accesses from within python tools are now thread-safe - * Liveplotter does no longer steal focus and closes as expected - * (experimental: start liveplotter from `explore_odrive.py` by typing `start_liveplotter(lambda: odrv0.motor0.encoder.encoder_state)`) ### Fixed * malloc now fails if we run out of memory (before it would always succeed even if we are out of ram...) -# Releases - ## [0.3.5] - 2018-03-04 ### Added diff --git a/Firmware/Makefile b/Firmware/Makefile index 1fdd4897..899f043c 100644 --- a/Firmware/Makefile +++ b/Firmware/Makefile @@ -16,7 +16,7 @@ gdb: all arm-none-eabi-gdb $(FIRMWARE) -x openocd.gdbinit dfu: all - ../tools/dfu.py $(if $(value SERIAL_NUMBER),--serial-number $(SERIAL_NUMBER),) $(FIRMWARE_HEX) + ../tools/odrivetool $(if $(value SERIAL_NUMBER),--serial-number $(SERIAL_NUMBER),) dfu $(FIRMWARE_HEX) bmp: all arm-none-eabi-gdb --ex 'target extended-remote /dev/stlink' \ @@ -32,6 +32,53 @@ erase: erase_config: openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c init -c reset\ halt -c flash\ erase_address\ 0x80C0000\ 0x40000 -c reset\ init -c reset\ run -c exit +# The one-time programmable memory stores the board version +# has the following format: +# - OTP format version (0xFE: version 1) +# - vendor ID (01: ODrive Robotics - do not use this on custom incompatible hardware!) +# - product ID (01: ODrive) +# - hardware major version +# - hardware minor version +# - hardware variant (00: 24V, 01: 48V) +# Bits in the OTP can only ever be set to 0 but never back to 1. +# Therefore do not try to run this command on the same board +# twice with different data. +# +# This OpenOCD command is intended for a STM32F405 and does the following: +# FLASH_KEYR = 0x45670123; // unlock FLASH_CR +# FLASH_KEYR = 0xCDEF89AB; // unlock FLASH_CR +# FLASH_CR = (1 << FLASH_CR_PG); // unlock flash memory +# [write OTP] +write_otp: +ifeq ($(ODRV_FACTORY),TRUE) + # Data: + openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg \ + -c init \ + -c 'reset halt' \ + -c 'mww 0x40023C04 0x45670123' \ + -c 'mww 0x40023C04 0xCDEF89AB' \ + -c 'mww 0x40023C10 0x00000001' -c 'sleep 10' \ + -c 'mwb 0x1fff7800 0xFE' -c 'sleep 10' \ + -c 'mwb 0x1fff7801 0x01' -c 'sleep 10' \ + -c 'mwb 0x1fff7802 0x01' -c 'sleep 10' \ + -c 'mwb 0x1fff7803 0x03' -c 'sleep 10' \ + -c 'mwb 0x1fff7804 0x04' -c 'sleep 10' \ + -c 'mwb 0x1fff7805 0x01' -c 'sleep 10' \ + -c 'reset run' \ + -c exit + +else + @echo "The one-time programmable memory can only be" + @echo "written ONCE on every board (what a surprise)." + @echo "If you're on an ODrive v3.5 or later we already did this for you." + @echo "Otherwise, if you're mentally ready for this irreversible action," + @echo "take the following steps:" + @echo " 1. open the Makefile and look at the write_otp target" + @echo " 2. understand the structure of the OTP" + @echo " 3. edit the bytes that are written to match your board version" + @echo "Run this command again, this time with ODRV_FACTORY=TRUE" +endif + clean: -rm -fR .dep $(BUILD_DIR) diff --git a/Firmware/MotorControl/communication.cpp b/Firmware/MotorControl/communication.cpp index 8f951fbb..a1a91c71 100644 --- a/Firmware/MotorControl/communication.cpp +++ b/Firmware/MotorControl/communication.cpp @@ -12,6 +12,7 @@ #include "protocol.hpp" #include "freertos_vars.h" #include "utils.h" +#include "../build/version.h" // autogenerated based on Git state #ifdef ENABLE_LEGACY_PROTOCOL #include "legacy_commands.h" @@ -37,6 +38,28 @@ extern USBD_HandleTypeDef hUsbDeviceFS; uint64_t serial_number; /* Private constant data -----------------------------------------------------*/ + +#if HW_VERSION_MAJOR == 3 +const uint8_t* otp_ptr = + *(uint8_t*)0x1fff7800 == 0xfe ? (uint8_t*)0x1fff7800 : + *(uint8_t*)0x1fff7800 != 0x00 ? NULL : + *(uint8_t*)0x1fff7810 == 0xfe ? (uint8_t*)0x1fff7810 : NULL; + +// Read hardware version from OTP if available, otherwise fall back +// to software defined version. +const uint8_t board_version_major = otp_ptr ? otp_ptr[3] : HW_VERSION_MAJOR; +const uint8_t board_version_minor = otp_ptr ? otp_ptr[4] : HW_VERSION_MINOR; +const uint8_t board_version_variant = otp_ptr ? otp_ptr[5] : (HW_VERSION_VOLTAGE == 24 ? 0 : 1); +#else +#error "not implemented" +#endif + +// the corresponding macros are defined in the autogenerated version.h +const uint8_t fw_version_major = FW_VERSION_MAJOR; +const uint8_t fw_version_minor = FW_VERSION_MINOR; +const uint8_t fw_version_revision = FW_VERSION_REVISION; +const uint8_t fw_version_unreleased = FW_VERSION_UNRELEASED; // 0 for official releases, 1 otherwise + /* Private variables ---------------------------------------------------------*/ static uint8_t* usb_buf; @@ -172,6 +195,13 @@ static inline auto make_obj_tree() { make_protocol_ro_property("vbus_voltage", &vbus_voltage), make_protocol_ro_property("comm_stack_info", &comm_stack_info), make_protocol_ro_property("serial_number", &serial_number), + make_protocol_ro_property("board_version_major", &board_version_major), + make_protocol_ro_property("board_version_minor", &board_version_minor), + make_protocol_ro_property("board_version_variant", &board_version_variant), + make_protocol_ro_property("fw_version_major", &fw_version_major), + make_protocol_ro_property("fw_version_minor", &fw_version_minor), + make_protocol_ro_property("fw_version_revision", &fw_version_revision), + make_protocol_ro_property("fw_version_unreleased", &fw_version_unreleased), make_protocol_object("config", make_protocol_property("brake_resistance", &board_config.brake_resistance), // TODO: changing this currently requires a reboot - fix this diff --git a/Firmware/README.md b/Firmware/README.md index 6c7124e8..dbc7c87b 100644 --- a/Firmware/README.md +++ b/Firmware/README.md @@ -137,7 +137,7 @@ For working with the ODrive code you don't need an IDE, but the open-source IDE ## Communicating over USB or UART Warning: If testing USB or UART communication for the first time it is recommend that your motors are free to spin continuously and are not connected to a drivetrain with limited travel. ### From Linux/Windows/macOS -There are two example python scripts to help you get started with controlling the ODrive using python. One will drop you into an interactive shell to query settings, parameters, and variables, and let you send setpoints manually ([tools/explore_odrive.py](../tools/explore_odrive.py)). The other is a demo application to show you how to control the ODrive programmatically ([tools/demo.py](../tools/demo.py)). Below follows a step-by-step guide on how to run these. +There are two example python scripts to help you get started with controlling the ODrive using python. One will drop you into an interactive shell to query settings, parameters, and variables, and let you send setpoints manually ([tools/odrivetool](../tools/odrivetool)). The other is a demo application to show you how to control the ODrive programmatically ([tools/odrive_demo.py](../tools/odrive_demo.py)). Below follows a step-by-step guide on how to run these. * __Windows__: It is recommended to use a Unix style command prompt, such as Git Bash that comes with [Git for windows](https://git-scm.com/download/win). @@ -163,9 +163,9 @@ pip install pyusb pyserial 5. __Windows__: Use the [Zadig](http://zadig.akeo.ie/) utility to set ODrive (not STLink!) driver to libusb-win32. * If 'Odrive version 3.x' is not in the list of devices upon opening Zadig, check 'List All Devices' from the options menu. With the Odrive selected in the device list choose 'libusb-win32' from the target driver list and select the large 'install driver' button. 6. Open the bash prompt in the `ODrive/tools/` folder. -7. Run `python3 demo.py` or `python3 explore_odrive.py`. -- `demo.py` is a very simple script which will make motor 0 turn back and forth. Use this as an example if you want to control the ODrive yourself programatically. -- `explore_odrive.py` drops you into an interactive python shell where you can explore and edit the parameters that are available on your device. For instance `my_odrive.motor0.pos_setpoint = 10000` makes motor0 move to position 10000. To connect over serial instead of USB run `./tools/explore_odrive.py --path serial`. +7. Run `python3 odrive_demo.py` or `python3 odrivetool`. +- `odrive_demo.py` is a very simple script which will make motor 0 turn back and forth. Use this as an example if you want to control the ODrive yourself programatically. +- `odrivetool` drops you into an interactive python shell when started without any arguments. There you can explore and edit the parameters that are available on your device. For instance `odrv0.motor0.pos_setpoint = 10000` makes motor0 move to position 10000. To connect over serial instead of USB run `./tools/odrivetool --path serial`. Run `./tools/odrivetool --help` to see what else you can do with the script. ### From Arduino [See ODrive Arduino Library](https://github.com/madcowswe/ODriveArduino) @@ -178,30 +178,30 @@ See the [protocol specification](protocol.md) or the [legacy protocol specificat The majority of the important parameters you would want to set after flashing the ODrive with firmware are configurable over the USB communication interface. These include some mandatory parameters that you must set for correct operation, as well as tuning and optional parameters. To start the configuration session: -* Launch `./tools/explore_odrive.py`. This will give you a command prompt where you can modify using simple assignments. -* Configure parameters of the `my_odrive.[...].config` objects. - * For example to adjust the position gain: `my_odrive.motor0.config.pos_gain = 30` Enter. +* Launch `./tools/odrivetool`. This will give you a command prompt where you can modify using simple assignments. +* Configure parameters of the `odrv0.[...].config` objects. + * For example to adjust the position gain: `odrv0.motor0.config.pos_gain = 30` Enter. * The complete list of configurable parameters is: - * `my_odrive.motorN.config.*` - * `my_odrive.axisN.config.*` + * `odrv0.motorN.config.*` + * `odrv0.axisN.config.*` * where N is a valid motor number (0 or 1). -* Save the configuration into non-volatile memory: `my_odrive.save_configuration()` Enter +* Save the configuration into non-volatile memory: `odrv0.save_configuration()` Enter * This will save the properties of all the `[...].config` objects and no other parameters. -* Reboot the drive: `my_odrive.reboot()` Enter +* Reboot the drive: `odrv0.reboot()` Enter -Note that a firmware upgrade at this point will preserve the configuration if and only if the parameters of both firmware versions are identical. Should you need to reset the configuration, you can run `my_odrive.erase_configuration()`. +Note that a firmware upgrade at this point will preserve the configuration if and only if the parameters of both firmware versions are identical. Should you need to reset the configuration, you can run `odrv0.erase_configuration()`. __Developers__: Be aware that you can also modify the compile-time defaults for all of these parameters. Most of them you will find at the top of [MotorControl/low_level.c](MotorControl/low_level.c#L50). Note that the configuration parameters there are somewhat intertwined with runtime variables and hardware specific configuration that should not be changed. Also note that all parameters occur twice. ### Mandatory parameters You must set for every motor: -* `my_odrive.motorN.encoder.config.cpr`: Encoder Count Per Revolution (CPR). This is 4x the Pulse Per Revolution (PPR) value. -* `my_odrive.motorN.config.pole_pairs`: This is the number of magnet poles in the rotor, **divided by two**. You can simply count the number of permanent magnets in the rotor, if you can see them. Note: this is not the same as the number of coils in the stator. -* `my_odrive.config.brake_resistance` [Ohm]: This is the resistance of the brake resistor. If you are not using it, you may set it to 0.0f. -* `my_odrive.motorN.config.motor_type`: This is the type of motor being used. Currently two types of motors are supported -- High-current motors (`MOTOR_TYPE_HIGH_CURRENT`) and Gimbal motors (`MOTOR_TYPE_GIMBAL`). +* `odrv0.motorN.encoder.config.cpr`: Encoder Count Per Revolution (CPR). This is 4x the Pulse Per Revolution (PPR) value. +* `odrv0.motorN.config.pole_pairs`: This is the number of magnet poles in the rotor, **divided by two**. You can simply count the number of permanent magnets in the rotor, if you can see them. Note: this is not the same as the number of coils in the stator. +* `odrv0.config.brake_resistance` [Ohm]: This is the resistance of the brake resistor. If you are not using it, you may set it to 0.0f. +* `odrv0.motorN.config.motor_type`: This is the type of motor being used. Currently two types of motors are supported -- High-current motors (`MOTOR_TYPE_HIGH_CURRENT`) and Gimbal motors (`MOTOR_TYPE_GIMBAL`). #### Motor Modes -The firwmare currently supports two different types of motors, high-current motors, and Gimbal motors. If you're using a regular hobby brushless motor like [this](https://hobbyking.com/en_us/turnigy-aerodrive-sk3-5065-236kv-brushless-outrunner-motor.html) one, you should set `my_odrive.motorN.config.motor_type` to `MOTOR_TYPE_HIGH_CURRENT`. For high-torque gimbal motors like [this](https://hobbyking.com/en_us/turnigy-hd-5208-brushless-gimbal-motor-bldc.html) one, you should choose `MOTOR_TYPE_GIMBAL`. +If you're using a regular hobby brushless motor like [this](https://hobbyking.com/en_us/turnigy-aerodrive-sk3-5065-236kv-brushless-outrunner-motor.html) one, you should set `motor_mode` to `MOTOR_TYPE_HIGH_CURRENT`. For low-current gimbal motors like [this](https://hobbyking.com/en_us/turnigy-hd-5208-brushless-gimbal-motor-bldc.html) one, you should choose `MOTOR_TYPE_GIMBAL`. Do not use `MOTOR_TYPE_GIMBAL` on a motor that is not a gimbal motor, as it may overheat the motor or the ODrive. **Further detail:** If 100's of mA of current noise is "small" for you, you can choose `MOTOR_TYPE_HIGH_CURRENT`. @@ -210,15 +210,15 @@ If 100's of mA current noise is "large" for you, and you intend to spin the moto ### Tuning parameters The most important parameters are the limits: -* The current limit: `my_odrive.motorN.current_control.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains. +* The current limit: `odrv0.motorN.current_control.config.current_lim` [A]. The default current limit, for safety reasons, is set to 10A. This is quite weak, and good for making sure the drive is stable. Once you have tuned the drive, you can increase this to 75A to get some performance. Note that above 75A, you must change the current amplifier gains. * Note: The motor current and the current drawn from the power supply is not the same in general. You should not look at the power supply current to see what is going on with the motor current. -* The velocity limit: `my_odrive.motorN.config.vel_limit` [counts/s]. The motor will be limited to this speed; again the default value is quite slow. -* You can change `my_odrive.motorN.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. +* The velocity limit: `odrv0.motorN.config.vel_limit` [counts/s]. The motor will be limited to this speed; again the default value is quite slow. +* You can change `odrv0.motorN.config.calibration_current` [A] to the largest value you feel comfortable leaving running through the motor continously when the motor is stationary. The motion control gains are currently manually tuned: -* `my_odrive.motorN.config.pos_gain = 20.0f` [(counts/s) / counts] -* `my_odrive.motorN.config.vel_gain = 15.0f / 10000.0f` [A/(counts/s)] -* `my_odrive.motorN.config.vel_integrator_gain = 10.0f / 10000.0f` [A/(counts/s * s)] +* `odrv0.motorN.config.pos_gain = 20.0f` [(counts/s) / counts] +* `odrv0.motorN.config.vel_gain = 15.0f / 10000.0f` [A/(counts/s)] +* `odrv0.motorN.config.vel_integrator_gain = 10.0f / 10000.0f` [A/(counts/s * s)] An upcoming feature will enable automatic tuning. Until then, here is a rough tuning procedure: * Set the integrator gain to 0 @@ -231,14 +231,14 @@ An upcoming feature will enable automatic tuning. Until then, here is a rough tu ### Optional parameters By default both motors are enabled, and the default control mode is position control. -If you want a different mode, you can change `my_odrive.motorN.config.control_mode`. +If you want a different mode, you can change `odrv0.motorN.config.control_mode`. Possible values are: * `CTRL_MODE_POSITION_CONTROL` * `CTRL_MODE_VELOCITY_CONTROL` * `CTRL_MODE_CURRENT_CONTROL` * `CTRL_MODE_VOLTAGE_CONTROL` - this one is not normally used. -To disable a motor at startup, set `my_odrive.axisN.config.enable_control` and `my_odrive.axisN.config.do_calibration` to `False`. +To disable a motor at startup, set `odrv0.axisN.config.enable_control` and `odrv0.axisN.config.do_calibration` to `False`.

## Encoder Calibration @@ -251,10 +251,10 @@ If you have an encoder with an index (Z) signal, you may avoid having to do the * All the parameters we will be modifying are in the motor structs at the top of [MotorControl/low_level.c](MotorControl/low_level.c). * Set `.encoder.use_index = true` and `.encoder.calibrated = false`. * Flash this configuration, and let the motor scan for the index pulse and then complete the encoder calibration. -* Run `explore_odrive.py`, check [Communicating over USB or UART](#communicating-over-usb-or-uart) for instructions on how to do that. +* Run `odrivetool`, check [Communicating over USB or UART](#communicating-over-usb-or-uart) for instructions on how to do that. * Enter the following to print out the calibration parameters (substitute the motor number you are calibrating for ``): - * `my_odrive.motor.encoder.encoder_offset` - This should print a number, like -326 or 1364. - * `my_odrive.motor.encoder.motor_dir` - This should print 1 or -1. + * `odrv0.motor.encoder.encoder_offset` - This should print a number, like -326 or 1364. + * `odrv0.motor.encoder.motor_dir` - This should print 1 or -1. * Copy these numbers to the corresponding entries in low_level.c: `.encoder.encoder_offset` and `.encoder.motor_dir`. * _Warning_: Please be careful to enter the correct numbers, and not to confuse the motor channels. Incorrect values may cause the motor to spin out of control. * Set `.encoder.calibrated = true`. @@ -265,7 +265,7 @@ If you have an encoder with an index (Z) signal, you may avoid having to do the

## Checking for error codes -`explore_odrive.py`can also be used to check error codes when your odrive is not working as expected. For example `my_odrive.motor0.error` will list the error code associated with motor 0. +`odrivetool` can also be used to check error codes when your odrive is not working as expected. For example `odrv0.motor0.error` will list the error code associated with motor 0.

The error nummber corresponds to the following: diff --git a/Firmware/Tupfile.lua b/Firmware/Tupfile.lua index d71b2238..ca1d3cce 100644 --- a/Firmware/Tupfile.lua +++ b/Firmware/Tupfile.lua @@ -119,6 +119,11 @@ build{ includes=stm_includes } +tup.frule{ + command='bash dump_version.sh %o', + outputs={'build/version.h'} +} + build{ name='ODriveFirmware', toolchains={toolchain}, diff --git a/Firmware/build.lua b/Firmware/build.lua index ba5ecc30..1d5ce8d6 100644 --- a/Firmware/build.lua +++ b/Firmware/build.lua @@ -67,8 +67,9 @@ function GCCToolchain(prefix, builddir, compiler_flags, linker_flags) else extra_outputs = {} end + if src == 'MotorControl/communication.cpp' then extra_inputs = 'build/version.h' end -- TODO: fix hack tup.frule{ - inputs= { src }, + inputs= { src, extra_inputs=extra_inputs }, command=compiler..' -c %f '.. tostring(compiler_flags)..' '.. -- CFLAGS for this compiler tostring(inc_flags)..' '.. -- CFLAGS for this translation unit diff --git a/tools/build.sh b/Firmware/build.sh similarity index 76% rename from tools/build.sh rename to Firmware/build.sh index a953d480..811a4ab7 100755 --- a/tools/build.sh +++ b/Firmware/build.sh @@ -6,7 +6,7 @@ set -euo pipefail THIS_DIR="$(dirname "$0")" -cd "$THIS_DIR/../Firmware" +cd "$THIS_DIR" # Write all environment variables that start with "CONFIG_" to tup.config rm -rdf build @@ -18,6 +18,6 @@ bash -xe ./tup_build.sh # Deploy if ! [ -z ${DEPLOY+x} ]; then mkdir -p deploy - cp build/ODriveFirmware.elf deploy/ODriveFirmware-"$DEPLOY".elf - cp build/ODriveFirmware.hex deploy/ODriveFirmware-"$DEPLOY".hex + cp build/ODriveFirmware.elf deploy/ODriveFirmware_"$DEPLOY".elf + cp build/ODriveFirmware.hex deploy/ODriveFirmware_"$DEPLOY".hex fi diff --git a/Firmware/dump_version.sh b/Firmware/dump_version.sh new file mode 100755 index 00000000..cdaaa372 --- /dev/null +++ b/Firmware/dump_version.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -euo pipefail + +if [ $# -eq 1 ]; then + OUTPUT="$1" +else + OUTPUT="/dev/stdout" +fi + +# The git root lies outside of the tup root +export GIT_DISCOVERY_ACROSS_FILESYSTEM=1 + +# Get a description of the current Git state +# Examples of what this string may become: +# fw-v0.3.6 The current commit is exactly at tag "fw-v0.3.6" +# There may or may not be untracked files in the +# working directory. +# fw-v0.3.6* The current commit is at tag "fw-v0.3.6" and there +# are uncommitted changes in the working directory. +# fw-v0.3.6-4-g3703ae5 The working directory at a commit with hash 3703ae5, +# 4 commits ahead of tag fw-v0.3.6 and clean. +FW_VERSION="$(git describe --always --tags --dirty=* || echo "[unknown commit]")" + +# Extract version numbers +FW_VERSION_MAJOR="$(sed -n 's/.*v\([0-9a-zA-Z]\).\([0-9a-zA-Z]\).\([0-9a-zA-Z]\)\(.*\)/\1/p' <<< "$FW_VERSION")" +FW_VERSION_MINOR="$(sed -n 's/.*v\([0-9a-zA-Z]\).\([0-9a-zA-Z]\).\([0-9a-zA-Z]\)\(.*\)/\2/p' <<< "$FW_VERSION")" +FW_VERSION_REVISION="$(sed -n 's/.*v\([0-9a-zA-Z]\).\([0-9a-zA-Z]\).\([0-9a-zA-Z]\)\(.*\)/\3/p' <<< "$FW_VERSION")" +FW_VERSION_SUFFIX="$(sed -n 's/.*v\([0-9a-zA-Z]\).\([0-9a-zA-Z]\).\([0-9a-zA-Z]\)\(.*\)/\4/p' <<< "$FW_VERSION")" + +# Fall back to 0 if the verions does not match the expected pattern +[ "$FW_VERSION_MAJOR" == "" ] && FW_VERSION_MAJOR=0 +[ "$FW_VERSION_MINOR" == "" ] && FW_VERSION_MINOR=0 +[ "$FW_VERSION_REVISION" == "" ] && FW_VERSION_REVISION=0 + +if [ "$FW_VERSION_SUFFIX" == "" ]; then + FW_VERSION_UNRELEASED=0 +else + FW_VERSION_UNRELEASED=1 +fi + +cat > "$OUTPUT" < 0: - # check if any segment from the hexfile overlaps with this sector - touched = False - for (start, end) in hexfile.segments(): - if start < addr and end > addr: - touched = True - break - elif start >= addr and start < addr + size: - touched = True - break - - if touched: - # TODO: verify if the section is writable - yield { - 'alt': alt, - 'addr': addr, - 'data': hexfile.tobinarray(addr, addr + size - 1) - } - - addr += size - repeat -= 1 - -def set_alternate_safe(dfudev, alt): - dfudev.set_alternate(alt) - if dfudev.get_state() == dfuse.DfuState.DFU_ERROR: - dfudev.clear_status() - dfudev.wait_while_state(dfuse.DfuState.DFU_ERROR) - -def erase(dfudev, sectors): - for i, sector in enumerate(sectors): - print("Erasing... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) - set_alternate_safe(dfudev, sector['alt']) - dfudev.erase(sector['addr']) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY, timeout=len(sector['data'])/32) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: %r" % status) - print('Erasing... done ') - -def flash(dfudev, sectors): - for i, sector in enumerate(sectors): - print("Flashing... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) - set_alternate_safe(dfudev, sector['alt']) - dfudev.set_address(sector['addr']) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: %r" % status) - - data = sector['data'] - blocks = [data[i:i + TRANSFER_SIZE] for i in range(0, len(data), TRANSFER_SIZE)] - for blocknum, block in enumerate(blocks): - #print('write to {:08X} ({} bytes)'.format( - # sector['addr'] + blocknum * TRANSFER_SIZE, len(block))) - dfudev.write(blocknum, block) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: %r" % status) - print('Flashing... done ') - - -# Results in usb.core.USBError. Probably the device should go to dfuIDLE first, but how? -#def verify(dfudev, sectors): -# for i, sector in enumerate(sectors): -# print("Verifying... (sector {}/{}) \r".format(i, len(sectors)), end='', flush=True) -# set_alternate_safe(dfudev, sector['alt']) -# dfudev.set_address(sector['addr']) -# status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) -# if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: -# raise RuntimeError("An error occured. Device Status: %r" % status) -# -# print("state: {}".format(dfudev.get_state())) -# #dfudev.clear_status() -# print("state: {}".format(dfudev.get_state())) -# data = sector['data'] -# blocks = [data[i:i + TRANSFER_SIZE] for i in range(0, len(data), TRANSFER_SIZE)] -# for blocknum, block in enumerate(blocks): -# print('read at {:08X}'.format(sector['addr'] + blocknum * TRANSFER_SIZE)) -# deviceBlock = dfudev.read(blocknum, TRANSFER_SIZE) -# print(dfudev.get_state()) -# if (deviceBlock != block): -# raise RuntimeError("verification failed at address {:08X}".format(sector['addr'] + blocknum * TRANSFER_SIZE)) -# print('Verifying... done ') - - -def jump_to_application(dfudev, address): - dfudev.set_address(address) - status = dfudev.wait_while_state(dfuse.DfuState.DFU_DOWNLOAD_BUSY) - if status[1] != dfuse.DfuState.DFU_DOWNLOAD_IDLE: - raise RuntimeError("An error occured. Device Status: {}".format(status[1])) - - dfudev.leave() - status = dfudev.wait_while_state(dfuse.DfuState.DFU_MANIFEST_SYNC) - if status[1] != dfuse.DfuState.DFU_MANIFEST: - raise RuntimeError("An error occured. Device Status: {}".format(status[1])) - - -def show_deferred_message(message, cancellation_token): - """ - Shows a message after 10s, unless cancellation_token gets set. - """ - def show_message_thread(message, cancellation_token): - for i in range(1,10): - if cancellation_token.is_set(): - return - time.sleep(1) - if not cancellation_token.is_set(): - print(message) - t = threading.Thread(target=show_message_thread, args=(message, cancellation_token)) - t.daemon = True - t.start() - -def put_odrive_into_dfu_mode(my_drive): - """ - Puts the specified device into DFU mode - """ - if not hasattr(my_drive, "enter_dfu_mode"): - print("The firmware on device {} does not support DFU. You need to \n" - "flash the firmware once using STLink (`make flash`), after that \n" - "DFU with this script should work fine." - .format(my_drive.__channel__.usb_device.serial_number)) - return - print("Putting device {} into DFU mode...".format(my_drive.__channel__.usb_device.serial_number)) - try: - my_drive.enter_dfu_mode() - except odrive.protocol.ChannelBrokenException as ex: - pass # this is expected because the device reboots - if platform.system() == "Windows": - show_deferred_message("Still waiting for the device to reappear.\n" - "Use the Zadig utility to set the driver of 'STM32 BOOTLOADER' to libusb-win32.", - find_odrive_cancellation_token) - -### BEGINNING OF APPLICATION ### - -# parse arguments -parser = argparse.ArgumentParser(description="Program an STM32 in DFU mode. The device can be identified either by it's serial number or UUID." - "You can list all connected devices by running" - "(lsusb -d 1209:0d32 -v; lsusb -d 0483:df11 -v) | grep iSerial") -parser.add_argument('file', metavar='HEX', help='the .hex file to be flashed') -parser.add_argument("-u", "--uuid", - help="The 12-byte UUID of the device. This is a hexadecimal number of the format" - "00000000-00000000-00000000") -parser.add_argument("-s", "--serial-number", - help="The 12-digit serial number of the device. This is a string consisting of 12 upper case hexadecimal digits as displayed in lsusb" - "example: 385F324D3037") -args = parser.parse_args() - -# load hex file -hexfile = IntelHex(args.file) - -#print("Contiguous segments in hex file:") -#for start, end in hexfile.segments(): -# print(" {:08X} to {:08X}".format(start, end - 1)) - -serial_number = args.serial_number - - -app_cancellation_token = threading.Event() -find_odrive_cancellation_token = threading.Event() -try: - print("Waiting for ODrive...") - - # Scan for ODrives not in DFU mode and put them into DFU mode once they appear - # We only scan on USB because DFU is only possible over USB - odrive.discovery.find_all("usb", serial_number, put_odrive_into_dfu_mode, find_odrive_cancellation_token) - - # Poll libUSB until a device in DFU mode is found - while not app_cancellation_token.is_set(): - params = {} if serial_number == None else {'serial_number': serial_number} - stm_device = usb.core.find(idVendor=0x0483, idProduct=0xdf11, **params) - if stm_device != None: - break - time.sleep(1) - find_odrive_cancellation_token.set() # we don't need this thread anymore - if app_cancellation_token.is_set(): - sys.exit(1) - print("Found device {} in DFU mode".format(stm_device.serial_number)) - - dfudev = dfuse.DfuDevice(stm_device) - - - # fill sectors with data - sectors = list(load_sectors(dfudev, hexfile)) - print("Sectors to be flashed: ") - for sector in sectors: - print(" {:08X} to {:08X}".format(sector['addr'], sector['addr'] + len(sector['data']) - 1)) - - # flash! - erase(dfudev, sectors) - flash(dfudev, sectors) - #verify(dfudev, sectors) - - # If the flash operation failed for some reason, your device is bricked now. - # You can unbrick it as long as the device remains powered on. - # (or always with an STLink) - # So for debugging you should comment this last part out. - - # Jump to application - jump_to_application(dfudev, 0x08000000) -finally: - find_odrive_cancellation_token.set() - - -# Note: the flashed image can be verified using: (0x12000 is the number of bytes to read) -# $ openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c init -c flash\ read_bank\ 0\ image.bin\ 0\ 0x12000 -c exit -# $ hexdump -C image.bin > image.bin.txt -# -# If you compare this with a reference image that was flashed with the STLink, you will see -# minor differences. This is because this script fills undefined sections with 0xff. -# $ diff image_ref.bin.txt image.bin.txt -# 21c21 -# < * -# --- -# > 00000180 d9 47 00 08 d9 47 00 08 ff ff ff ff ff ff ff ff |.G...G..........| -# 2553c2553 -# < 00009fc0 9e 46 70 47 00 00 00 00 52 20 96 3c 46 76 50 76 |.FpG....R . 00009fc0 9e 46 70 47 ff ff ff ff 52 20 96 3c 46 76 50 76 |.FpG....R .') - else: - print('Type "odrv0." and press ') - print('This will present you with all the properties that you can reference') - print('') - print('For example: "odrv0.motor0.encoder.pll_pos"') - print('will print the current encoder position on motor 0') - print('and "odrv0.motor0.pos_setpoint = 10000"') - print('will send motor0 to 10000') - print('') - -interactive_variables = {} -interactive_variables["help"] = print_help - - -## Device discovery ## - -discovered_devices = [] - -def did_discover_device(odrive): - """ - Handles the discovery of new devices by displaying a - message and making the device available to the interactive - console - """ - serial_number = odrive.serial_number if hasattr(odrive, 'serial_number') else "[unknown serial number]" - if serial_number in discovered_devices: - verb = "Reconnected" - index = discovered_devices.index(serial_number) - else: - verb = "Connected" - discovered_devices.append(serial_number) - index = len(discovered_devices) - 1 - interactive_name = "odrv" + str(index) - - # Publish new ODrive to interactive console - interactive_variables[interactive_name] = odrive - globals()[interactive_name] = odrive # Add to globals so tab complete works - logger.info("{} to ODrive {:012X} as {}".format(verb, serial_number, interactive_name)) - - # Subscribe to disappearance of the device - odrive.__channel__._channel_broken.subscribe(lambda: did_lose_device(interactive_name)) - -def did_lose_device(interactive_name): - """ - Handles the disappearance of a device by displaying - a message. - """ - if not app_shutdown_token.is_set(): - logger.warn("Oh no {} disappeared".format(interactive_name)) - -# Connect to device -printer("Waiting for device...") -app_shutdown_token = threading.Event() -odrive.discovery.find_all(args.path, args.serial_number, - did_discover_device, app_shutdown_token, - printer=printer) - - -## Launch interactive shell ## - -# Check if IPython is installed -if args.no_ipython: - use_ipython = False -else: - try: - import IPython - use_ipython = True - except: - print("Warning: you don't have IPython installed.") - print("If you want to have an improved interactive console with pretty colors,") - print("you should install IPython\n") - use_ipython = False - -# If IPython is installed, embed IPython shell, otherwise embed regular shell -if use_ipython: - #interactive_variables["lalala"] = print_help - help = print_help # Override help function - console = IPython.terminal.embed.InteractiveShellEmbed(local_ns=interactive_variables, banner1='') - console.runcode = console.run_code # hack to make IPython look like the regular console - interact = console -else: - - # Enable tab complete if possible - try: - import rlcompleter - import readline # Works only on Unix - readline.parse_and_bind("tab: complete") - except: - sudo_prefix = "" if platform.system() == "Windows" else "sudo " - print("Warning: could not enable tab-complete. User experience will suffer.\n" - "Run `{}pip install readline` and then restart this script to fix this." - .format(sudo_prefix)) - - import code - console = code.InteractiveConsole(locals=interactive_variables) - interact = lambda: console.interact(banner='') - -# install hook to hide ChannelBrokenException -console.runcode('import sys') -console.runcode('superexcepthook = sys.excepthook') -console.runcode('def newexcepthook(ex_class,ex,trace):\n' - ' if ex_class.__module__ + "." + ex_class.__name__ != "odrive.protocol.ChannelBrokenException":\n' - ' superexcepthook(ex_class,ex,trace)') -console.runcode('sys.excepthook=newexcepthook') - - -# Launch shell -print_banner() -logger._skip_bottom_line = True -interact() -app_shutdown_token.set() diff --git a/tools/liveplotter.py b/tools/liveplotter.py deleted file mode 100755 index 95c50c24..00000000 --- a/tools/liveplotter.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -""" -Liveplotter -""" - -import time -import threading -import odrive.discovery -from odrive.utils import start_liveplotter - -data_rate = 100 -plot_rate = 10 -num_samples = 1000 - -my_odrive = odrive.discovery.find_any() - -# If you want to plot different values, change them here. -# You can plot any number of values concurrently. -start_liveplotter(lambda: [my_odrive.motor0.encoder.pll_pos, - my_odrive.motor1.encoder.pll_pos]) - diff --git a/tools/odrive/__init__.py b/tools/odrive/__init__.py index e69de29b..6eca987b 100644 --- a/tools/odrive/__init__.py +++ b/tools/odrive/__init__.py @@ -0,0 +1,5 @@ + +# Standard convention is to add a __version__ attribute to the package +from .version import get_version +__version__ = get_version() +del get_version diff --git a/tools/odrive/dfu.py b/tools/odrive/dfu.py new file mode 100755 index 00000000..aad9a3d8 --- /dev/null +++ b/tools/odrive/dfu.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python +""" +Tool for flashing .hex files to the ODrive via the STM built-in USB DFU mode. +""" + +import argparse +import sys +import time +import threading +import platform +import struct +import array +import fractions +import usb.core +import odrive.discovery +from odrive.dfuse import * + +try: + from intelhex import IntelHex +except: + sudo_prefix = "" if platform.system() == "Windows" else "sudo " + print("You need intelhex for this ({}pip install IntelHex)".format(sudo_prefix), file=sys.stderr) + sys.exit(1) + + +SIZE_MULTIPLIERS = {' ': 1, 'K': 1024, 'M' : 1024*1024} +MAX_TRANSFER_SIZE = 2048 + + +def get_device_sectors(dfudev): + """ + Returns a list of all sectors on the device. + Each sector is represented as a dictionary with the following keys: + - name: name of the associated memory region (e.g. "Internal Flash") + - alt: USB alternate setting associated with this memory region + - addr: Start address of the sector (e.g. 0x08004000 for the second flash sectors) + - baseaddr: Start address of the memory region associated with the sector + (e.g. 0x08000000 for all flash sectors) + - len: Number of bytes in the sector + """ + for name, alt in dfudev.alternates(): + # example for name: + # '@Internal Flash /0x08000000/04*016Kg,01*064Kg,07*128Kg' + label, baseaddr, layout = name.split('/') + baseaddr = int(baseaddr, 0) # convert hex to decimal + addr = baseaddr + + for sector in layout.split(','): + repeat, size = map(int, sector[:-2].split('*')) + size *= SIZE_MULTIPLIERS[sector[-2].upper()] + mode = sector[-1] + + while repeat > 0: + # TODO: verify if the section is writable + yield { + 'name': label.strip().strip('@'), + 'alt': alt, + 'baseaddr': baseaddr, + 'addr': addr, + 'len': size, + 'mode': mode + } + + addr += size + repeat -= 1 + +def populate_sectors(sectors, hexfile): + """ + Checks for which on-device sectors there is data in the hex file and + returns a (sector, data) tuple for each touched sector where data + is a byte array of the same size as the sector. + """ + for sector in sectors: + addr = sector['addr'] + size = sector['len'] + # check if any segment from the hexfile overlaps with this sector + touched = False + for (start, end) in hexfile.segments(): + if start < addr and end > addr: + touched = True + break + elif start >= addr and start < addr + size: + touched = True + break + + if touched: + # TODO: verify if the section is writable + yield (sector, hexfile.tobinarray(addr, addr + size - 1)) + +def set_alternate_safe(dfudev, alt): + dfudev.set_alternate(alt) + if dfudev.get_state() == DfuState.DFU_ERROR: + dfudev.clear_status() + dfudev.wait_while_state(DfuState.DFU_ERROR) + +#def clear_error(dfudev) +def set_address_safe(dfudev, addr): + dfudev.set_address(addr) + status = dfudev.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY) + if status[1] != DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + # take device out of DFU_DOWNLOAD_SYNC and into DFU_IDLE + dfudev.abort() + status = dfudev.wait_while_state(DfuState.DFU_DOWNLOAD_SYNC) + if status[1] != DfuState.DFU_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + + +def erase(dfudev, sector): + set_alternate_safe(dfudev, sector['alt']) + dfudev.erase(sector['addr']) + status = dfudev.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY, timeout=sector['len']/32) + if status[1] != DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + +def flash(dfudev, sector, data): + set_alternate_safe(dfudev, sector['alt']) + set_address_safe(dfudev, sector['addr']) + + transfer_size = fractions.gcd(sector['len'], MAX_TRANSFER_SIZE) + + blocks = [data[i:i + transfer_size] for i in range(0, len(data), transfer_size)] + for blocknum, block in enumerate(blocks): + #print('write to {:08X} ({} bytes)'.format( + # sector['addr'] + blocknum * TRANSFER_SIZE, len(block))) + dfudev.write(blocknum, block) + status = dfudev.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY) + if status[1] != DfuState.DFU_DOWNLOAD_IDLE: + raise RuntimeError("An error occured. Device Status: %r" % status) + +def read(dfudev, sector): + """ + Reads data from the specified sector + Returns: a byte array containing the data + """ + set_alternate_safe(dfudev, sector['alt']) + set_address_safe(dfudev, sector['addr']) + + transfer_size = fractions.gcd(sector['len'], MAX_TRANSFER_SIZE) + #blocknum_offset = int((sector['addr'] - sector['baseaddr']) / transfer_size) + + + data = array.array(u'B') + for blocknum in range(int(sector['len'] / transfer_size)): + #print('read at {:08X}'.format(sector['addr'] + blocknum * TRANSFER_SIZE)) + deviceBlock = dfudev.read(blocknum, transfer_size) + data.extend(deviceBlock) + dfudev.abort() # take device into DFU_IDLE + return data + +def get_first_mismatch_index(array1, array2): + """ + Compares two arrays and returns the index of the + first unequal item or None if both arrays are equal + """ + if len(array1) != len(array2): + raise Exception("arrays must be same size") + for pos in range(len(array1)): + if (array1[pos] != array2[pos]): + return pos + return None + + +def jump_to_application(dfudev, address): + set_address_safe(dfudev, address) + #dfudev.set_address(address) + #status = dfudev.wait_while_state(DfuState.DFU_DOWNLOAD_BUSY) + #if status[1] != DfuState.DFU_DOWNLOAD_IDLE: + # raise RuntimeError("An error occured. Device Status: {}".format(status[1])) + + dfudev.leave() + status = dfudev.wait_while_state(DfuState.DFU_MANIFEST_SYNC) + if status[1] != DfuState.DFU_MANIFEST: + raise RuntimeError("An error occured. Device Status: {}".format(status[1])) + + +def dump_otp(): + """ + Dumps the contents of the one-time-programmable + memory. The OTP will be used in future versions of + this script to determine the board version. + """ + # 512 Byte OTP + otp_sector = [s for s in sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7800][0] + data = read(dfudev, otp_sector) + print(' '.join('{:02X}'.format(x) for x in data)) + + # 16 lock bytes + otp_lock_sector = [s for s in sectors if s['name'] == 'OTP Memory' and s['addr'] == 0x1fff7A00][0] + data = read(dfudev, otp_lock_sector) + print(' '.join('{:02X}'.format(x) for x in data)) + +def show_deferred_message(message, cancellation_token): + """ + Shows a message after 10s, unless cancellation_token gets set. + """ + def show_message_thread(message, cancellation_token): + for i in range(1,10): + if cancellation_token.is_set(): + return + time.sleep(1) + if not cancellation_token.is_set(): + print(message) + t = threading.Thread(target=show_message_thread, args=(message, cancellation_token)) + t.daemon = True + t.start() + +def put_odrive_into_dfu_mode(my_drive): + """ + Puts the specified device into DFU mode + """ + if not hasattr(my_drive, "enter_dfu_mode"): + print("The firmware on device {} does not support DFU. You need to \n" + "flash the firmware once using STLink (`make flash`), after that \n" + "DFU with this script should work fine." + .format(my_drive.__channel__.usb_device.serial_number)) + return + print("Putting device {} into DFU mode...".format(my_drive.__channel__.usb_device.serial_number)) + try: + my_drive.enter_dfu_mode() + except odrive.protocol.ChannelBrokenException as ex: + pass # this is expected because the device reboots + if platform.system() == "Windows": + show_deferred_message("Still waiting for the device to reappear.\n" + "Use the Zadig utility to set the driver of 'STM32 BOOTLOADER' to libusb-win32.", + find_odrive_cancellation_token) + +def launch_dfu(args, app_shutdown_token): + """ + Waits for a device that matches args.path and args.serial_number + and then upgrades the device's firmware. + """ + + # load hex file + # TODO: Either use the elf format or pack a custom format with a manifest. + # This way we can for instance verify the target board version and only + # have to publish one file for every board. + hexfile = IntelHex(args.file) + + if (args.verbose): + print("Contiguous segments in hex file:") + for start, end in hexfile.segments(): + print(" {:08X} to {:08X}".format(start, end - 1)) + + serial_number = args.serial_number + + find_odrive_cancellation_token = threading.Event() + app_shutdown_token.subscribe(lambda: find_odrive_cancellation_token.set()) + + print("Waiting for ODrive...") + + # Scan for ODrives not in DFU mode and put them into DFU mode once they appear + # We only scan on USB because DFU is only possible over USB + odrive.discovery.find_all(args.path, serial_number, put_odrive_into_dfu_mode, find_odrive_cancellation_token) + + # Poll libUSB until a device in DFU mode is found + while not app_shutdown_token.is_set(): + params = {} if serial_number == None else {'serial_number': serial_number} + stm_device = usb.core.find(idVendor=0x0483, idProduct=0xdf11, **params) + if stm_device != None: + break + time.sleep(1) + find_odrive_cancellation_token.set() # we don't need this thread anymore + if app_shutdown_token.is_set(): + sys.exit(1) + print("Found device {} in DFU mode".format(stm_device.serial_number)) + + dfudev = DfuDevice(stm_device) + + sectors = list(get_device_sectors(dfudev)) + + if (args.verbose): + print("Sectors on device: ") + for sector in sectors: + print(" {:08X} to {:08X} ({})".format( + sector['addr'], + sector['addr'] + sector['len'] - 1, + sector['name'])) + + # fill sectors with data + touched_sectors = list(populate_sectors(sectors, hexfile)) + + if (args.verbose): + print("The following sectors will be flashed: ") + for sector,_ in touched_sectors: + print(" {:08X} to {:08X}".format(sector['addr'], sector['addr'] + sector['len'] - 1)) + + if (args.verbose): + print("OTP:") + dump_otp() + + # Erase + try: + for i, (sector, data) in enumerate(touched_sectors): + print("Erasing... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True) + erase(dfudev, sector) + print('Erasing... done \r', end='', flush=True) + finally: + print('', flush=True) + + # Flash + try: + for i, (sector, data) in enumerate(touched_sectors): + print("Flashing... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True) + flash(dfudev, sector, data) + print('Flashing... done \r', end='', flush=True) + finally: + print('', flush=True) + + # Verify + try: + for i, (sector, expected_data) in enumerate(touched_sectors): + print("Verifying... (sector {}/{}) \r".format(i, len(touched_sectors)), end='', flush=True) + observed_data = read(dfudev, sector) + mismatch_pos = get_first_mismatch_index(observed_data, expected_data) + if not mismatch_pos is None: + mismatch_pos -= mismatch_pos % 16 + observed_snippet = ' '.join('{:02X}'.format(x) for x in observed_data[mismatch_pos:mismatch_pos+16]) + expected_snippet = ' '.join('{:02X}'.format(x) for x in expected_data[mismatch_pos:mismatch_pos+16]) + raise RuntimeError("Verification failed around address 0x{:08X}:\n".format(sector['addr'] + mismatch_pos) + + " expected: " + expected_snippet + "\n" + " observed: " + observed_snippet) + print('Verifying... done \r', end='', flush=True) + finally: + print('', flush=True) + + + # If the flash operation failed for some reason, your device is bricked now. + # You can unbrick it as long as the device remains powered on. + # (or always with an STLink) + # So for debugging you should comment this last part out. + + # Jump to application + jump_to_application(dfudev, 0x08000000) + + + +# Note: the flashed image can be verified using: (0x12000 is the number of bytes to read) +# $ openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c init -c flash\ read_bank\ 0\ image.bin\ 0\ 0x12000 -c exit +# $ hexdump -C image.bin > image.bin.txt +# +# If you compare this with a reference image that was flashed with the STLink, you will see +# minor differences. This is because this script fills undefined sections with 0xff. +# $ diff image_ref.bin.txt image.bin.txt +# 21c21 +# < * +# --- +# > 00000180 d9 47 00 08 d9 47 00 08 ff ff ff ff ff ff ff ff |.G...G..........| +# 2553c2553 +# < 00009fc0 9e 46 70 47 00 00 00 00 52 20 96 3c 46 76 50 76 |.FpG....R . 00009fc0 9e 46 70 47 ff ff ff ff 52 20 96 3c 46 76 50 76 |.FpG....R .') + else: + print('Type "odrv0." and press ') + print('This will present you with all the properties that you can reference') + print('') + print('For example: "odrv0.motor0.encoder.pll_pos"') + print('will print the current encoder position on motor 0') + print('and "odrv0.motor0.pos_setpoint = 10000"') + print('will send motor0 to 10000') + print('') + + +interactive_variables = {} + +discovered_devices = [] + +def did_discover_device(odrive, logger, app_shutdown_token): + """ + Handles the discovery of new devices by displaying a + message and making the device available to the interactive + console + """ + serial_number = odrive.serial_number if hasattr(odrive, 'serial_number') else "[unknown serial number]" + if serial_number in discovered_devices: + verb = "Reconnected" + index = discovered_devices.index(serial_number) + else: + verb = "Connected" + discovered_devices.append(serial_number) + index = len(discovered_devices) - 1 + interactive_name = "odrv" + str(index) + + # Publish new ODrive to interactive console + interactive_variables[interactive_name] = odrive + globals()[interactive_name] = odrive # Add to globals so tab complete works + logger.info("{} to ODrive {:012X} as {}".format(verb, serial_number, interactive_name)) + + # Subscribe to disappearance of the device + odrive.__channel__._channel_broken.subscribe(lambda: did_lose_device(interactive_name, logger, app_shutdown_token)) + +def did_lose_device(interactive_name, logger, app_shutdown_token): + """ + Handles the disappearance of a device by displaying + a message. + """ + if not app_shutdown_token.is_set(): + logger.warn("Oh no {} disappeared".format(interactive_name)) + +def launch_shell(args, logger, printer, app_shutdown_token): + """ + Launches an interactive python or IPython command line + interface. + As ODrives are connected they are made available as + "odrv0", "odrv1", ... + """ + + # Connect to device + logger.debug("Waiting for device...") + odrive.discovery.find_all(args.path, args.serial_number, + lambda dev: did_discover_device(dev, logger, app_shutdown_token), + app_shutdown_token, + printer=printer) + + # Check if IPython is installed + if args.no_ipython: + use_ipython = False + else: + try: + import IPython + use_ipython = True + except: + print("Warning: you don't have IPython installed.") + print("If you want to have an improved interactive console with pretty colors,") + print("you should install IPython\n") + use_ipython = False + + interactive_variables["help"] = lambda: print_help(args) + + # If IPython is installed, embed IPython shell, otherwise embed regular shell + if use_ipython: + help = lambda: print_help(args) # Override help function # pylint: disable=W0612 + console = IPython.terminal.embed.InteractiveShellEmbed(local_ns=interactive_variables, banner1='') + console.runcode = console.run_code # hack to make IPython look like the regular console + interact = console + else: + # Enable tab complete if possible + try: + import readline # Works only on Unix + readline.parse_and_bind("tab: complete") + except: + sudo_prefix = "" if platform.system() == "Windows" else "sudo " + print("Warning: could not enable tab-complete. User experience will suffer.\n" + "Run `{}pip install readline` and then restart this script to fix this." + .format(sudo_prefix)) + + import code + console = code.InteractiveConsole(locals=interactive_variables) + interact = lambda: console.interact(banner='') + + # install hook to hide ChannelBrokenException + console.runcode('import sys') + console.runcode('superexcepthook = sys.excepthook') + console.runcode('def newexcepthook(ex_class,ex,trace):\n' + ' if ex_class.__module__ + "." + ex_class.__name__ != "odrive.protocol.ChannelBrokenException":\n' + ' superexcepthook(ex_class,ex,trace)') + console.runcode('sys.excepthook=newexcepthook') + + + # Launch shell + print_banner() + logger._skip_bottom_line = True + interact() + app_shutdown_token.set() diff --git a/tools/odrive/utils.py b/tools/odrive/utils.py index 7762494b..79c69b01 100755 --- a/tools/odrive/utils.py +++ b/tools/odrive/utils.py @@ -72,6 +72,44 @@ def start_liveplotter(get_var_callback): threading.Thread(target=plot_data).start() #plot_data() +def print_drv_regs(device): + """ + Dumps the current gate driver regisers for Motor 0 + """ + fault = device.motor0.gate_driver.drv_fault + status_reg_1 = device.motor0.gate_driver.status_reg_1 + status_reg_2 = device.motor0.gate_driver.status_reg_2 + ctrl_reg_1 = device.motor0.gate_driver.ctrl_reg_1 + ctrl_reg_2 = device.motor0.gate_driver.ctrl_reg_2 + + print("DRV Fault Code: " + str(fault)) + print("Status Reg 1: " + str(status_reg_1) + " (" + format(status_reg_1, '#010b') + ")") + print("Status Reg 2: " + str(status_reg_2) + " (" + format(status_reg_2, '#010b') + ")") + print("Control Reg 1: " + str(ctrl_reg_1) + " (" + format(ctrl_reg_1, '#010b') + ")") + print("Control Reg 2: " + str(ctrl_reg_2) + " (" + format(ctrl_reg_2, '#010b') + ")") + +def rate_test(device): + """ + Tests how many integers per second can be transmitted + """ + + import matplotlib.pyplot as plt + plt.ion() + + print("reading 10000 values...") + numFrames = 10000 + vals = [] + for _ in range(numFrames): + vals.append(device.motor0.loop_counter) + + plt.plot(vals) + + loopsPerFrame = (vals[-1] - vals[0])/numFrames + loopsPerSec = (168000000/(2*10192)) + FramePerSec = loopsPerSec/loopsPerFrame + print("Frames per second: " + str(FramePerSec)) + + ## Exceptions ## class TimeoutException(Exception): @@ -230,9 +268,10 @@ class Logger(): COLOR_DEFAULT: 0x07 } - def __init__(self): + def __init__(self, verbose=True): self._prefix = '' self._skip_bottom_line = False # If true, messages are printed one line above the cursor + self._verbose = verbose if platform.system() == 'Windows': self._stdout_buf = win32console.GetStdHandle(win32console.STD_OUTPUT_HANDLE) @@ -280,9 +319,9 @@ class Logger(): # (print text) # ESC 8: restore old cursor position - sys.stdout.write('\x1b7\x1b[1A\x1b[1S\x1b[1L', end='', flush=True) + sys.stdout.write('\x1b7\x1b[1A\x1b[1S\x1b[1L') sys.stdout.write(Logger._VT100Colors[color] + text + Logger._VT100Colors[Logger.COLOR_DEFAULT]) - sys.stdout.write('\x1b8', end='', flush=True) + sys.stdout.write('\x1b8') sys.stdout.flush() def print_colored(self, text, color): @@ -294,7 +333,8 @@ class Logger(): sys.stdout.flush() def debug(self, text): - self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT) + if self._verbose: + self.print_colored(self._prefix + text, Logger.COLOR_DEFAULT) def success(self, text): self.print_colored(self._prefix + text, Logger.COLOR_GREEN) def info(self, text): diff --git a/tools/odrive/version.py b/tools/odrive/version.py new file mode 100644 index 00000000..327c915c --- /dev/null +++ b/tools/odrive/version.py @@ -0,0 +1,46 @@ + +import re +import subprocess +import os +import sys + +def get_version(git_only=False): + """ + Returns the versions of the tools + If git_only is true, the version.txt file is ignored even + if it is present. + """ + script_dir = os.path.dirname(os.path.realpath(__file__)) + + # Try to read the version.txt file that is generated during + # the packaging step + version_file_path = os.path.join(script_dir, 'version.txt') + if os.path.exists(version_file_path) and git_only == False: + with open(version_file_path) as version_file: + return version_file.readline().rstrip('\n') + + try: + # Determine the current git commit version + git_result = subprocess.run(["git", "describe", "--always", "--tags", "--dirty=*"], + cwd=script_dir, + stdout=subprocess.PIPE, timeout=10) + git_tag = git_result.stdout.decode(sys.stdout.encoding) + + regex=r'.*v([0-9a-zA-Z]).([0-9a-zA-Z]).([0-9a-zA-Z])(.*)' + package_version_major = int(re.sub(regex, r"\1", git_tag)) + package_version_minor = int(re.sub(regex, r"\2", git_tag)) + package_version_revision = int(re.sub(regex, r"\3", git_tag)) + package_version_unreleased = (re.sub(regex, r"\4", git_tag) != "") + + if package_version_unreleased: + package_version_revision += 1 + + # TODO: fetch from Git describe + version = '{}.{}.{}'.format(package_version_major, package_version_minor, package_version_revision) + + if package_version_unreleased: + version += ".dev" + except Exception as ex: + print(ex) + version = "whatever version in " + script_dir + return version diff --git a/tools/demo.py b/tools/odrive_demo.py similarity index 100% rename from tools/demo.py rename to tools/odrive_demo.py diff --git a/tools/odrvtool b/tools/odrvtool new file mode 100755 index 00000000..3e8211d7 --- /dev/null +++ b/tools/odrvtool @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +ODrive command line utility +""" + +import argparse +import odrive.discovery +from odrive.utils import Logger, Event + +# Flush stdout by default +import functools +print = functools.partial(print, flush=True) + + +## Parse arguments ## +parser = argparse.ArgumentParser(description='ODrive command line utility\n' + 'Running this tool without any arguments is equivalent to running `odrivetool shell`\n', + formatter_class=argparse.RawTextHelpFormatter) + +# Subcommands +subparsers = parser.add_subparsers(help='sub-command help', dest='command') +shell_parser = subparsers.add_parser('shell', help='Drop into an interactive python shell that lets you interact with the ODrive(s)') +shell_parser.add_argument("--no-ipython", action="store_true", + help="Use the regular Python shell " + "instead of the IPython shell, " + "even if IPython is installed.") + +dfu_parser = subparsers.add_parser('dfu', help="Upgrade the ODrive device firmware") +dfu_parser.add_argument('file', metavar='HEX', help='The .hex file to be flashed. Make sure your firmware board version matches the actual board version.') + +subparsers.add_parser('liveplotter', help="Upgrade the ODrive's Firmware") +subparsers.add_parser('drv-status', help="Show status of the on-board DRV8301 chips (for debugging only)") +subparsers.add_parser('rate-test', help="Estimate the average transmission bandwidth over USB") + +# General arguments +parser.add_argument("-p", "--path", metavar="PATH", action="store", + help="The path(s) where ODrive(s) should be discovered.\n" + "By default the script will connect to any ODrive on USB.\n\n" + "To select a specific USB device:\n" + " --path usb:BUS:DEVICE\n" + "usbwhere BUS and DEVICE are the bus and device numbers as shown in `lsusb`.\n\n" + "To select a specific serial port:\n" + " --path serial:PATH\n" + "where PATH is the path of the serial port. For example \"/dev/ttyUSB0\".\n" + "You can use `ls /dev/tty*` to find the correct port.\n\n" + "You can combine USB and serial specs by separating them with a comma (no space!)\n" + "Example:\n" + " --path usb,serial:/dev/ttyUSB0\n" + "means \"discover any USB device or a serial device on /dev/ttyUSB0\"") +parser.add_argument("-s", "--serial-number", action="store", + help="The 12-digit serial number of the device. " + "This is a string consisting of 12 upper case hexadecimal " + "digits as displayed in lsusb. \n" + " example: 385F324D3037\n" + "You can list all devices connected to USB by running\n" + "(lsusb -d 1209:0d32 -v; lsusb -d 0483:df11 -v) | grep iSerial\n" + "If omitted, any device is accepted.") +parser.add_argument("-v", "--verbose", action="store_true", + help="print debug information") +parser.add_argument("--version", action="store_true", + help="print version information and exit") + +parser.set_defaults(path="usb") +args = parser.parse_args() + +# Default command +if args.command is None: + args.command = 'shell' + args.no_ipython = False + +# We are interactively printing status messages, so flush by default +import functools +print = functools.partial(print, flush=True) + +# TODO: deprecate printer - use logger instead +if (args.verbose): + printer = print +else: + printer = lambda x: None + +logger = Logger(verbose=args.verbose) +logger.debug(str(args)) + +print("ODrive control utility v" + odrive.__version__) + +app_shutdown_token = Event() + +try: + if args.version == True: + pass + + elif args.command == 'shell': + import odrive.shell + odrive.shell.launch_shell(args, logger, printer, app_shutdown_token) + + elif args.command == 'dfu': + import odrive.dfu + odrive.dfu.launch_dfu(args, app_shutdown_token) + + elif args.command == 'liveplotter': + from odrive.utils import start_liveplotter + print("Waiting for ODrive...") + my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number) + + # If you want to plot different values, change them here. + # You can plot any number of values concurrently. + start_liveplotter(lambda: [my_odrive.motor0.encoder.pll_pos, + my_odrive.motor1.encoder.pll_pos]) + + elif args.command == 'drv-status': + from odrive.utils import print_drv_regs + print("Waiting for ODrive...") + my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number) + print_drv_regs(my_odrive) + + elif args.command == 'rate-test': + from odrive.utils import rate_test + print("Waiting for ODrive...") + my_odrive = odrive.discovery.find_any(path=args.path, serial_number=args.serial_number) + rate_test(my_odrive) + + else: + raise Exception("unknown command: " + args.command) + +finally: + app_shutdown_token.set() diff --git a/tools/rate_test.py b/tools/rate_test.py deleted file mode 100755 index 76f359a6..00000000 --- a/tools/rate_test.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 - -import time -import odrive.discovery -import matplotlib.pyplot as plt -import numpy as np - -# Find a connected ODrive (this will block until you connect one) -print("Waiting for ODrive...") -myOdrive = odrive.discovery.find_any() -print("connected") - -plt.ion() - -numFrames = 10000 -vals = [] -for _ in range(numFrames): - vals.append(myOdrive.motor0.loop_counter) - -plt.plot(vals) - -loopsPerFrame = (vals[-1] - vals[0])/numFrames -loopsPerSec = (168000000/(2*10192)) -FramePerSec = loopsPerSec/loopsPerFrame -print(FramePerSec) \ No newline at end of file diff --git a/tools/requirements.txt b/tools/requirements.txt new file mode 100644 index 00000000..7d1e0773 --- /dev/null +++ b/tools/requirements.txt @@ -0,0 +1,3 @@ +--index-url https://pypi.python.org/simple/ + +-e . \ No newline at end of file diff --git a/tools/setup.py b/tools/setup.py new file mode 100644 index 00000000..b503d710 --- /dev/null +++ b/tools/setup.py @@ -0,0 +1,88 @@ +""" +This script is used to deploy the ODrive python tools to PyPi +so that users can install them easily with +"pip install odrive" + +To install the package and its dependencies locally, run: + sudo pip install -r requirements.txt + +To build and package the python tools into a tar archive: + python setup.py sdist + +Warning: Before you proceed, be aware that you can upload a +specific version only ever once. After that you need to increment +the hotfix number. Deleting the release manually on the PyPi +website does not help. + +Use TestPyPi while developing. + +To build, package and upload the python tools to TestPyPi, run: + python setup.py sdist upload -r pypitest +To make a real release ensure you're at the release commit +and then run the above command without the "test". + +To install a prerelease version from test index: + sudo pip install --index-url https://test.pypi.org/simple/ --no-cache-dir odrive + + +PyPi access requires that you have set up ~/.pypirc with your +PyPi credentials and that your account has the rights +to publish packages with the name odrive. +""" + +# TODO: add additional y/n prompt to prevent from erroneous upload + +from distutils.core import setup +import os +import sys + +creating_package = "sdist" in sys.argv + +# Load version from Git tag +import odrive.version +version = odrive.version.get_version(git_only=creating_package) + +# Change this if you already uploaded the current +# version but need to release a hotfix +hotfix = 0 + +if creating_package and (hotfix > 0 or not version[-1].isdigit()): + # Add this for hotfixes + version += "-" + str(hotfix) + + +# If we're currently creating the package we need to autogenerate +# a file that contains the version string +if creating_package: + version_file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'odrive', 'version.txt') + with open(version_file_path, mode='w') as version_file: + version_file.write(version) + + +setup( + name = 'odrive', + packages = ['odrive'], # this must be the same as the name above + scripts = ['odrivetool', 'odrive_demo.py'], + version = version, + description = 'Control utilities for the ODrive high performance motor controller', + author = 'Oskar Weigl', + author_email = 'oskar.weigl@odriverobotics.com', + license='MIT', + url = 'https://github.com/madcowswe/ODrive', + keywords = ['odrive', 'motor', 'motor control'], + install_requires = [ + 'PyUSB', # Required to access USB devices from Python through libusb + 'PySerial', # Required to access serial devices from Python + 'IntelHex', # Used to by DFU to load firmware files + 'matplotlib' # Required to run the liveplotter + ], + package_data={'': ['version.txt']}, + include_package_data=True, + classifiers = [], +) + +# TODO: include README + +# clean up +if creating_package: + os.remove(version_file_path)