Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f3298ec
trigger docs build
samuelsadok Aug 11, 2022
146b67f
[docs] fix version indicator
samuelsadok Aug 11, 2022
e2df8e9
Merge pull request #730 from odriverobotics/devel
Wetmelon Apr 29, 2023
e37a25f
Merge branch 'docs-v0.5.5'
samuelsadok May 2, 2023
a308314
fix release action
samuelsadok May 2, 2023
58fdd3f
Update CHANGELOG.md
samuelsadok May 9, 2023
6c3cefd
add note about compatibility
samuelsadok Mar 21, 2024
ca13288
fix compile on GCC 12
samuelsadok Oct 2, 2024
f9b5419
Remove unused buggy FreeRTOS functions
samuelsadok Oct 2, 2024
3a2e4dd
update deprecated action dependencies
samuelsadok Jan 20, 2026
c9d473b
Trigger GitHub Actions CI build for v3.6-56V
Apr 4, 2026
f2d0665
Add ci-v3.6-56V branch to workflow triggers
Apr 4, 2026
e1595dd
Fix deprecated upload-artifact action version
Apr 4, 2026
ffb07ac
Fix macOS pip install for externally managed Python environment
Apr 4, 2026
5956e49
Remove macOS matrix to avoid unsupported runner failure
Apr 4, 2026
1744f2e
Fix: AS5048A SPI encoder communication issue
beihia Apr 6, 2026
78035e4
Fix: try alternate AMS SPI phase
Apr 6, 2026
1ad9c32
Fix: add SPI CS setup delay
Apr 6, 2026
8c7d458
Fix: reconstruct AMS parity bit
Apr 6, 2026
8c95624
Fix: poll AMS SPI in sampling IRQ
Apr 6, 2026
9bcbdfd
Revert "Fix: poll AMS SPI in sampling IRQ"
Apr 6, 2026
87dce31
Fix: ignore in-flight AMS SPI transactions
Apr 6, 2026
00ec4bf
Fix AS5048A SPI mode and frame parsing
Apr 15, 2026
963771a
Tune AS5048A SPI fallback parsing
Apr 15, 2026
b828c75
Try shifted-frame recovery for AS5048A
Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/actions/upload-release/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ inputs:
odrive_api_key:
description: 'Key to our release index server'
required: true
product:
description: 'ODrive product name (for firmware releases only).'
board:
description: 'ODrive board version triplet ("PRODUCT_LINE.VERSION.VARIANT") (for firmware releases only).'
required: false
app:
description: 'Firmware app name (default, bootloader) (for firmware releases only).'
Expand Down Expand Up @@ -95,6 +95,7 @@ runs:
shell: python
run: |
import asyncio
import re
import sys

import aiohttp
Expand All @@ -115,8 +116,8 @@ runs:
release_api = PrivateReleaseApi(api_client)

qualifiers = {}
if '${{ inputs.product }}':
qualifiers['product'] = '${{ inputs.product }}'
if '${{ inputs.board }}':
qualifiers['board'] = tuple(int(i) for i in re.match(r'^([0-9]+)\.([0-9]+).([0-9]+)$', '${{ inputs.board }}').groups())
if '${{ inputs.app }}':
qualifiers['app'] = '${{ inputs.app }}'
if '${{ inputs.variant }}':
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
print("::set-output name=version::" + version)

- name: Cache pip
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-sphinx-sphinx-tabs-sphinx-design-sphinx_copybutton-sphinx_panels-sphinx_rtd_theme
Expand Down
21 changes: 15 additions & 6 deletions .github/workflows/firmware.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ on:
pull_request:
branches: [master, devel]
push:
branches: [master, devel]
branches: [master, devel, ci-v3.6-56V]
tags: ['fw-v*']

jobs:
compile:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
os: [ubuntu-latest, windows-latest]
board_version: [v3.6-56V]
debug: [true]

Expand Down Expand Up @@ -80,11 +80,12 @@ jobs:
# See https://github.com/osxfuse/osxfuse/issues/801#issuecomment-833419942
brew install --cask macfuse
brew install gromgit/fuse/tup-mac
pip3 install PyYAML Jinja2 jsonschema
python3 -m pip install --user PyYAML Jinja2 jsonschema
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $GITHUB_ENV


- name: Cache chocolatey
uses: actions/cache@v2
uses: actions/cache@v4
if: startsWith(matrix.os, 'windows-')
with:
path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\gcc-arm-embedded
Expand Down Expand Up @@ -134,7 +135,7 @@ jobs:

- name: Upload binary as artifact
if: ${{ matrix.os == 'ubuntu-latest' && matrix.debug == false }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: firmware-${{ matrix.board_version }}
path: |
Expand All @@ -147,6 +148,14 @@ jobs:
mkdir ${{ github.workspace }}/out
cp ${{ github.workspace }}/Firmware/build/ODriveFirmware.elf ${{ github.workspace }}/out/firmware.elf

- name: Parse and format matrix.board_version
if: ${{ matrix.os == 'ubuntu-latest' }}
id: format-board-version
run: |
formatted_version=$(echo "${{ matrix.board_version }}" | sed 's/v\([0-9]\+\)\.\([0-9]\+\)-\([0-9]\+\)V/\1.\2.\3/')
echo "Formatted board version: $formatted_version"
echo "::set-output name=formatted_board_version::$formatted_version"

- name: Upload firmware to ODrive release system
if: ${{ steps.release-info.outputs.channel == 'master' && matrix.os == 'ubuntu-latest' && matrix.debug == false && (startsWith(matrix.board_version, 'v3.5-') || startsWith(matrix.board_version, 'v3.6-')) }}
uses: ./.github/actions/upload-release
Expand All @@ -157,7 +166,7 @@ jobs:
do_access_key: ${{ secrets.DIGITALOCEAN_ACCESS_KEY }}
do_secret_key: ${{ secrets.DIGITALOCEAN_SECRET_KEY }}
odrive_api_key: ${{ secrets.ODRIVE_API_KEY }}
product: ODrive ${{ matrix.board_version }}
board: ${{ steps.format-board-version.outputs.formatted_board_version }}
app: default
variant: public

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

## [0.5.6] - Unreleased
## [0.5.6] - 2023-04-29

### Fixed

Expand Down
4 changes: 4 additions & 0 deletions Firmware/Drivers/STM32/stm32_spi_arbiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ bool Stm32SpiArbiter::start() {
__HAL_SPI_ENABLE(hspi_);
}
task.ncs_gpio.write(false);
// Give the slave a brief setup time before the first SPI clock edge.
for (volatile int i = 0; i < 64; ++i) {
__NOP();
}

HAL_StatusTypeDef status = HAL_ERROR;

Expand Down
42 changes: 32 additions & 10 deletions Firmware/MotorControl/encoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ void Encoder::setup() {
.Direction = SPI_DIRECTION_2LINES,
.DataSize = SPI_DATASIZE_16BIT,
.CLKPolarity = (mode_ == MODE_SPI_ABS_AEAT || mode_ == MODE_SPI_ABS_MA732) ? SPI_POLARITY_HIGH : SPI_POLARITY_LOW,
.CLKPhase = SPI_PHASE_2EDGE,
.CLKPhase = (mode_ == MODE_SPI_ABS_AMS) ? SPI_PHASE_1EDGE : SPI_PHASE_2EDGE,
.NSS = SPI_NSS_SOFT,
.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16,
.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128,
.FirstBit = SPI_FIRSTBIT_MSB,
.TIMode = SPI_TIMODE_DISABLE,
.CRCCalculation = SPI_CRCCALCULATION_DISABLE,
Expand Down Expand Up @@ -535,6 +535,7 @@ bool Encoder::abs_spi_start_transaction() {
spi_task_.on_complete = [](void* ctx, bool success) { ((Encoder*)ctx)->abs_spi_cb(success); };
spi_task_.on_complete_ctx = this;
spi_task_.next = nullptr;
abs_spi_transaction_pending_ = true;

spi_arbiter_->transfer_async(&spi_task_);
} else {
Expand Down Expand Up @@ -562,18 +563,36 @@ uint8_t cui_parity(uint16_t v) {
void Encoder::abs_spi_cb(bool success) {
uint16_t pos;

abs_spi_transaction_pending_ = false;

if (!success) {
goto done;
}

switch (mode_) {
case MODE_SPI_ABS_AMS: {
uint16_t rawVal = abs_spi_dma_rx_[0];
// check if parity is correct (even) and error flag clear
if (ams_parity(rawVal) || ((rawVal >> 14) & 1)) {
uint16_t rawValSwapped = ((rawVal & 0xFF) << 8) | ((rawVal >> 8) & 0xFF);

// Some AS5048A setups appear to miss the first returned bit.
// Rebuild candidate frames by shifting right and recomputing parity.
uint16_t rawValShifted = rawVal >> 1;
rawValShifted = (rawValShifted & 0x7fff) | (ams_parity(rawValShifted) << 15);

uint16_t rawValSwappedShifted = rawValSwapped >> 1;
rawValSwappedShifted = (rawValSwappedShifted & 0x7fff) | (ams_parity(rawValSwappedShifted) << 15);

if (!(ams_parity(rawValShifted) || ((rawValShifted >> 14) & 1))) {
pos = rawValShifted & 0x3fff;
} else if (!(ams_parity(rawValSwappedShifted) || ((rawValSwappedShifted >> 14) & 1))) {
pos = rawValSwappedShifted & 0x3fff;
} else if (!(ams_parity(rawVal) || ((rawVal >> 14) & 1))) {
pos = rawVal & 0x3fff;
} else if (!(ams_parity(rawValSwapped) || ((rawValSwapped >> 14) & 1))) {
pos = rawValSwapped & 0x3fff;
} else {
goto done;
}
pos = rawVal & 0x3fff;
} break;

case MODE_SPI_ABS_CUI: {
Expand Down Expand Up @@ -734,11 +753,14 @@ bool Encoder::update() {
case MODE_SPI_ABS_AEAT:
case MODE_SPI_ABS_MA732: {
if (abs_spi_pos_updated_ == false) {
// Low pass filter the error
spi_error_rate_ += current_meas_period * (1.0f - spi_error_rate_);
if (spi_error_rate_ > 0.05f) {
set_error(ERROR_ABS_SPI_COM_FAIL);
return false;
// Don't count a transfer as failed while its DMA transaction is still in flight.
if (!abs_spi_transaction_pending_) {
// Low pass filter the error
spi_error_rate_ += current_meas_period * (1.0f - spi_error_rate_);
if (spi_error_rate_ > 0.05f) {
set_error(ERROR_ABS_SPI_COM_FAIL);
return false;
}
}
} else {
// Low pass filter the error
Expand Down
1 change: 1 addition & 0 deletions Firmware/MotorControl/encoder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class Encoder : public ODriveIntf::EncoderIntf {
void abs_spi_cb(bool success);
void abs_spi_cs_pin_init();
bool abs_spi_pos_updated_ = false;
bool abs_spi_transaction_pending_ = false;
Mode mode_ = MODE_INCREMENTAL;
Stm32Gpio abs_spi_cs_gpio_;
uint32_t abs_spi_cr1;
Expand Down
143 changes: 4 additions & 139 deletions Firmware/ThirdParty/FreeRTOS/Source/stream_buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -214,146 +214,11 @@ static void prvInitialiseNewStreamBuffer( StreamBuffer_t * const pxStreamBuffer,

/*-----------------------------------------------------------*/

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
// REMOVED IMPLEMENTATIONS OF xStreamBufferGenericCreate and xStreamBufferGenericCreateStatic.
// We're not using them but the implementation we were using contained a
// vulnerability (CVE-2021-31572) and got flagged by analysis tools.
// https://github.com/odriverobotics/ODrive/issues/751

StreamBufferHandle_t xStreamBufferGenericCreate( size_t xBufferSizeBytes, size_t xTriggerLevelBytes, BaseType_t xIsMessageBuffer )
{
uint8_t *pucAllocatedMemory;
uint8_t ucFlags;

/* In case the stream buffer is going to be used as a message buffer
(that is, it will hold discrete messages with a little meta data that
says how big the next message is) check the buffer will be large enough
to hold at least one message. */
if( xIsMessageBuffer == pdTRUE )
{
/* Is a message buffer but not statically allocated. */
ucFlags = sbFLAGS_IS_MESSAGE_BUFFER;
configASSERT( xBufferSizeBytes > sbBYTES_TO_STORE_MESSAGE_LENGTH );
}
else
{
/* Not a message buffer and not statically allocated. */
ucFlags = 0;
configASSERT( xBufferSizeBytes > 0 );
}
configASSERT( xTriggerLevelBytes <= xBufferSizeBytes );

/* A trigger level of 0 would cause a waiting task to unblock even when
the buffer was empty. */
if( xTriggerLevelBytes == ( size_t ) 0 )
{
xTriggerLevelBytes = ( size_t ) 1;
}

/* A stream buffer requires a StreamBuffer_t structure and a buffer.
Both are allocated in a single call to pvPortMalloc(). The
StreamBuffer_t structure is placed at the start of the allocated memory
and the buffer follows immediately after. The requested size is
incremented so the free space is returned as the user would expect -
this is a quirk of the implementation that means otherwise the free
space would be reported as one byte smaller than would be logically
expected. */
xBufferSizeBytes++;
pucAllocatedMemory = ( uint8_t * ) pvPortMalloc( xBufferSizeBytes + sizeof( StreamBuffer_t ) ); /*lint !e9079 malloc() only returns void*. */

if( pucAllocatedMemory != NULL )
{
prvInitialiseNewStreamBuffer( ( StreamBuffer_t * ) pucAllocatedMemory, /* Structure at the start of the allocated memory. */ /*lint !e9087 Safe cast as allocated memory is aligned. */ /*lint !e826 Area is not too small and alignment is guaranteed provided malloc() behaves as expected and returns aligned buffer. */
pucAllocatedMemory + sizeof( StreamBuffer_t ), /* Storage area follows. */ /*lint !e9016 Indexing past structure valid for uint8_t pointer, also storage area has no alignment requirement. */
xBufferSizeBytes,
xTriggerLevelBytes,
ucFlags );

traceSTREAM_BUFFER_CREATE( ( ( StreamBuffer_t * ) pucAllocatedMemory ), xIsMessageBuffer );
}
else
{
traceSTREAM_BUFFER_CREATE_FAILED( xIsMessageBuffer );
}

return ( StreamBufferHandle_t ) pucAllocatedMemory; /*lint !e9087 !e826 Safe cast as allocated memory is aligned. */
}

#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
/*-----------------------------------------------------------*/

#if( configSUPPORT_STATIC_ALLOCATION == 1 )

StreamBufferHandle_t xStreamBufferGenericCreateStatic( size_t xBufferSizeBytes,
size_t xTriggerLevelBytes,
BaseType_t xIsMessageBuffer,
uint8_t * const pucStreamBufferStorageArea,
StaticStreamBuffer_t * const pxStaticStreamBuffer )
{
StreamBuffer_t * const pxStreamBuffer = ( StreamBuffer_t * ) pxStaticStreamBuffer; /*lint !e740 !e9087 Safe cast as StaticStreamBuffer_t is opaque Streambuffer_t. */
StreamBufferHandle_t xReturn;
uint8_t ucFlags;

configASSERT( pucStreamBufferStorageArea );
configASSERT( pxStaticStreamBuffer );
configASSERT( xTriggerLevelBytes <= xBufferSizeBytes );

/* A trigger level of 0 would cause a waiting task to unblock even when
the buffer was empty. */
if( xTriggerLevelBytes == ( size_t ) 0 )
{
xTriggerLevelBytes = ( size_t ) 1;
}

if( xIsMessageBuffer != pdFALSE )
{
/* Statically allocated message buffer. */
ucFlags = sbFLAGS_IS_MESSAGE_BUFFER | sbFLAGS_IS_STATICALLY_ALLOCATED;
}
else
{
/* Statically allocated stream buffer. */
ucFlags = sbFLAGS_IS_STATICALLY_ALLOCATED;
}

/* In case the stream buffer is going to be used as a message buffer
(that is, it will hold discrete messages with a little meta data that
says how big the next message is) check the buffer will be large enough
to hold at least one message. */
configASSERT( xBufferSizeBytes > sbBYTES_TO_STORE_MESSAGE_LENGTH );

#if( configASSERT_DEFINED == 1 )
{
/* Sanity check that the size of the structure used to declare a
variable of type StaticStreamBuffer_t equals the size of the real
message buffer structure. */
volatile size_t xSize = sizeof( StaticStreamBuffer_t );
configASSERT( xSize == sizeof( StreamBuffer_t ) );
} /*lint !e529 xSize is referenced is configASSERT() is defined. */
#endif /* configASSERT_DEFINED */

if( ( pucStreamBufferStorageArea != NULL ) && ( pxStaticStreamBuffer != NULL ) )
{
prvInitialiseNewStreamBuffer( pxStreamBuffer,
pucStreamBufferStorageArea,
xBufferSizeBytes,
xTriggerLevelBytes,
ucFlags );

/* Remember this was statically allocated in case it is ever deleted
again. */
pxStreamBuffer->ucFlags |= sbFLAGS_IS_STATICALLY_ALLOCATED;

traceSTREAM_BUFFER_CREATE( pxStreamBuffer, xIsMessageBuffer );

xReturn = ( StreamBufferHandle_t ) pxStaticStreamBuffer; /*lint !e9087 Data hiding requires cast to opaque type. */
}
else
{
xReturn = NULL;
traceSTREAM_BUFFER_CREATE_STATIC_FAILED( xReturn, xIsMessageBuffer );
}

return xReturn;
}

#endif /* ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
/*-----------------------------------------------------------*/

void vStreamBufferDelete( StreamBufferHandle_t xStreamBuffer )
Expand Down
4 changes: 3 additions & 1 deletion Firmware/fibre-cpp/include/fibre/cpp_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -976,10 +976,12 @@ TRet* dynamic_get(size_t i, const std::tuple<Ts...>& t) {


template<typename TDereferenceable, typename TResult>
class simple_iterator : std::iterator<std::random_access_iterator_tag, TResult> {
class simple_iterator {
TDereferenceable *container_;
size_t i_;
public:
using iterator_category = std::random_access_iterator_tag;
using value_type = TResult;
using reference = TResult;
explicit simple_iterator(TDereferenceable& container, size_t pos) : container_(&container), i_(pos) {}
simple_iterator& operator++() { ++i_; return *this; }
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## Important Note

The firmware in this repository is compatible with the ODrive v3.x (NRND) and is no longer under active development.

Firmware for the new generation of ODrives ([ODrive Pro](https://odriverobotics.com/shop/odrive-pro), [S1](https://odriverobotics.com/shop/odrive-s1), [Micro](https://odriverobotics.com/shop/odrive-micro), etc.) is currently being actively maintained and developed, however its source code is currently not publicly available. Access may be available under NDA, please [reach out to us](mailto:info@odriverobotics.com) for inquiries.

## Overview

![ODrive Logo](https://static1.squarespace.com/static/58aff26de4fcb53b5efd2f02/t/59bf2a7959cc6872bd68be7e/1505700483663/Odrive+logo+plus+text+black.png?format=1000w)

This project is all about accurately driving brushless motors, for cheap. The aim is to make it possible to use inexpensive brushless motors in high performance robotics projects, like [this](https://www.youtube.com/watch?v=WT4E5nb3KtY).
Expand Down
1 change: 1 addition & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ help:
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

Loading