Repository navigation
Fix confirmed logic bugs found while auditing the library sources (13 fixes, one per commit) - #571
Open
stijncarelsbergh wants to merge 13 commits into
Open
stijncarelsbergh wants to merge 13 commits into
stijncarelsbergh wants to merge 13 commits into
Conversation
…tself The alignment routine relabels the pins when the highest current was measured on another channel, but the corresponding sample swap was written as `_swap(c_a.b, c_a.b)` - a no-op. The polarity check that follows (`_sign(c_a.a) < 0`) therefore inspects the sample of the wrong channel and can invert the gain of the phase it just repaired, which turns the current feedback into positive feedback at low currents. Also fixed the same pattern in the 'A-(C)NC' branch, which swapped c_a.b with c_a.c while the pins swapped were A and C. Reported in the alignment audit; trigger is exactly the miswiring this feature exists to correct.
…elabelled Same defect as the BLDC alignment: when the stepper alignment decides that the measured phase A is on the other ADC channel, it swaps the pins/offsets/gains but not the samples, so the following `if (c.a < 0)` tests the sample of the channel that is *not* carrying the current (and that reads ~0). The phase A gain ends up with the wrong sign for exactly the wiring the routine is meant to correct.
Three flaws in the same alignment step: - the A-C branch compared `fabs(c.a) - fabs(c.c)` against a threshold without taking the absolute value of the difference, so a channel that reads *higher* than the driven phase passes the check instead of raising the error; - the B-C branch compared c.a with c.c (copy-paste from the branch above) while the message and the comment refer to phase B; - the phase-B section had the same missing fabs().
call_list/call_ids/call_label hold 20 entries and call_count was never checked, so the 21st add() writes a function pointer, a char and a pointer past the end of the object - out-of-bounds write, memory corruption, symptoms depending on layout.
…tart-up The constructor left hall_state, electric_sector, electric_rotations, total_interrupts, pulse_diff, pulse_timestamp, direction, old_direction and use_interrupt uninitialised - indeterminate values for any sensor that is not in the BSS section, and wrong FOC start-up behaviour even for globals. init() then called updateState(), which compares the measured sector against that uninitialised/zeroed sector and, when the rotor happens to sit in sector 4 or 5 at power-up, interprets the difference as an electrical underflow and starts with electric_rotations = -1 (2 of the 6 power-up positions). The angle and the accumulated full rotations are offset by one electrical rotation until something resets them. Instead of relying on updateState() the first time, the current hall state is adopted directly.
`const static word data_mask` is initialised once and then reused by every MagneticSensorSPI instance, so a second sensor with a different bit resolution gets the mask (and therefore the angle) of the first one. Two sensors of different resolution on the same MCU silently read wrong angles.
…count The constructor stores min_raw_count and computes cpr = max_raw_count - min_raw_count, but getSensorAngle() divided the raw reading by cpr without subtracting the minimum. The reported angle therefore has a constant offset of min_raw_count/(max-min)*2*PI - about 5 degrees for the 14..1020 range used in the examples and docs - and the span is still exactly 2*PI, which is why it looks almost right. The docs describe min_raw_count as 'the smallest expected reading' and warn that getting it wrong causes a click per revolution, i.e. it is meant to be removed.
…rrectly For a left-aligned sensor the remaining bits sit in the *upper* part of the low byte, so both the mask and the shift have to account for the unused low bits. The constructor applied the right-aligned mask (0x3F for 6 remaining bits) and then shifted the result right by 8-lsb_used, so it kept the status/parity bits and dropped the real data - the angle is wrong for every sensor configured through this constructor. The preset configurations in the header (e.g. MT6701 with lsb_mask 0xFC, lsb_shift 2) show the intended convention.
… for Uq < 0 The trapezoid branches used `center = Uq` for non-centred modulation, which works for Uq > 0 but makes all three phase voltages <= 0 for Uq < 0; setPwm() clamps them to 0, so the motor gets no voltage at all in one direction. The sine/SVPWM branches handle this by shifting the phases up by their minimum, which is what non-centred modulation means for every mode. Using the same min-clamp for the trapezoid modes is exactly equivalent for Uq > 0 (every sector map contains a -1, so min = -Uq and the result is map*Uq + Uq as before) and gives the correct shifted waveform for Uq < 0.
angleOpenloop() moves in both directions but always stored a positive shaft_velocity, while velocityOpenloop() stores the signed value. Anything using shaft_velocity (monitoring, and the back-EMF term of estimated_current torque control) therefore sees a positive speed while moving backwards.
…equency The frequency-aware constructor documents the AS5600 PWM modes (115/230/460/920 Hz) and computes the raw counts from them, but left the read timeout at the default 1200 us. One period at 115 Hz is ~8.7 ms and the high pulse can be ~8.4 ms, so pulseIn() times out, returns 0 and the reported angle sticks at min_raw_count: a silently dead sensor for an officially supported configuration. Only 920 Hz (max pulse ~1.05 ms) fitted into the old timeout. The timeout is now 1.2 periods, i.e. ~1.2 ms at 920 Hz (compatible with the behaviour before) and ~10 ms at 115 Hz.
The comment says 'inject FLT_MIN in denominator to avoid division by zero' but the term is missing (it was dropped from the ODrive original), so _atan2(0,0) is 0/0 and returns NaN instead of 0. Restores the original guard, which only affects the degenerate (0,0) case.
…locity() direction is written by the interrupt handler (updateState()) and was read after interrupts() had been re-enabled, so a sector change in between mixes the old pulse timing with the new direction and produces one velocity sample with the wrong sign and magnitude. Sampled together with the pulse variables instead.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix confirmed logic bugs found while auditing the library sources
This is the code-change PR that goes with the documentation/comment clean-up (#570).
Every commit here is a separate, self-contained bug fix so that individual commits can be
dropped, reverted or discussed independently - there is no need for a new PR per disagreement.
All of these were found by reading the sources against the documented behaviour and modelling
the arithmetic by hand; none of them are style/refactor changes. Two platform builds were run
(see "How this was checked") but there was no hardware in the loop, so please treat the
behavioural claims as reasoning + build evidence, not as measured results.
The fixes
Memory safety
1.
Commander::add()wrote past its fixed arrays (commander-bounds)call_list[20],call_ids[20],call_label[20]with an uncheckedcall_count++: the 21stadd()writes a function pointer, a char and a pointer past the end of the object. Now guardedwith
sizeofso the arrays can grow without touching the check.Current sense (this is the "auto alignment" feature that fixes bad wiring)
2.
alignBLDCDriver()swapped a variable with itself (cs-swap)_swap(c_a.b, c_a.b)is a no-op, so after relabelling the pins the polarity check(
_sign(c_a.a) < 0) looked at the sample of the other channel and inverted the gain of thephase it had just repaired - positive feedback in
foc_current/dc_currentmode. Same defect inthe
A-(B)NCbranch, and theA-(C)NCbranch swapped the wrong pair (c_a.b, c_a.cwhile thepins swapped were A and C).
3.
alignStepperDriver()had the same problem (cs-stepper)The pins/offsets/gains move but the samples do not, so
if (c.a < 0)tested a channel that reads~0 and the polarity decision was effectively random for the wiring the routine exists to correct.
4. Three magnitude comparisons in the hybrid alignment were ineffective (
cs-hybrid-mag)(fabs(c.a) - fabs(c.c)) > 0.1fpasses silently when the second channel reads higher (missingfabs); the B-C branch comparedc.awithc.c(copy-paste) while its message refers to phase B;the phase-B section had the same missing
fabs().Sensors
5.
HallSensorstarted from uninitialised state (hall-init)The constructor never initialised
hall_state,electric_sector,electric_rotations,total_interrupts,pulse_diff,pulse_timestamp,direction,old_direction,use_interrupt- indeterminate for heap/stack instances, and wrong even for globals:init()call
updateState()before any state was adopted, and when the rotor sits in sector 4 or 5 atpower-up the sector difference (4-5) is read as an electrical underflow, so the sensor starts with
electric_rotations = -1(2 of the 6 possible power-up positions).init()now adopts themeasured hall state directly instead.
6.
MagneticSensorSPIshared one data mask between all instances (spi-mask)const static word data_mask = 0xFFFF >> (16 - bit_resolution);is initialised once, so a secondsensor with a different resolution silently gets the first sensor's mask (and therefore wrong
angles). Removing
staticcosts one shift per read.7.
MagneticSensorAnalognever subtractedmin_raw_count(analog-min)cpr = max_raw_count - min_raw_countwas computed andmin_raw_countstored, but the angle wasraw_count / cpr * 2*PI. With the 14..1020 range used in the docs/examples that is a constantoffset of 14/10062PI ~ 5 degrees, while the span is still exactly 2*PI - which is why it looks
almost right.
8.
MagneticSensorI2Cthrew away the left-aligned LSB bits (i2c-lsb)For a left-aligned sensor the remaining bits sit in the upper part of the low byte, so the mask
must be shifted too. The constructor used the right-aligned mask (
0x3Ffor 6 remaining bits) andthen shifted the result right by
8 - lsb_used, keeping the status/parity bits and dropping thereal data. The preset configurations in the header (
MT6701_I2Cwithlsb_mask 0xFC,lsb_shift 2) show the intended convention.9.
MagneticSensorPWMread timeout was too short for the documented frequencies (pwm-timeout)The 5-argument constructor documents the AS5600 PWM modes (115/230/460/920 Hz) and computes the
raw counts from them, but left
timeout_usat 1200 us. One period at 115 Hz is ~8.7 ms and thehigh pulse can be ~8.4 ms, so
pulseIn()times out, returns 0, the angle sticks atmin_raw_count- a silently dead sensor for an officially supported configuration (only 920 Hzfitted into the old timeout). The timeout is now 1.2 periods.
10.
HallSensor::getVelocity()readdirectionoutside the critical section (hall-velocity)directionis written by the ISR and was read afterinterrupts(); a sector change in betweenmixes the old pulse timing with the new direction. Now sampled together with
pulse_diff.Control
11. Non-centred trapezoid commutation produced no output for
Uq < 0(trap-negative)center = ... : Uqmakes all three phase voltages <= 0 for negativeUq;setPwm()clamps themto 0, so the motor gets nothing in one direction:
Uqmap*Uq + UqThe trapezoid branches now use the same "shift up by the minimum" idiom as the sine/SVPWM
branches. Every sector map contains a -1, so for
Uq > 0the result is provably identical tobefore (
map*Uq + Uq), and forUq < 0it produces the correct waveform.12.
angleOpenloop()stored an unsigned speed (focmotor-velocity)It moves in both directions but always wrote a positive
shaft_velocity, whilevelocityOpenloop()stores the signed value. Monitoring and the back-EMF term ofestimated_currenttorque control therefore saw a positive speed while moving backwards.13.
_atan2()lost itsFLT_MINguard (atan2-guard)The comment says "inject FLT_MIN in denominator to avoid division by zero" but the term is missing
(it was dropped from the ODrive original), so
_atan2(0,0)is 0/0 -> NaN. Restored; only thedegenerate (0,0) case changes.
How this was checked
arduino:avr:mega(39708 bytes flash / 1383 bytes RAM) andesp32:esp32:esp32(360796 bytes / 24700 bytes) using the example
examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial.The Arduino build compiles every file under
src/, so all ten touched files were compiled onboth platforms.
(/),[/]and{/}in every modified file isunchanged relative to
master(10 files, 0 unbalanced).git diff --stat masteris 10 files, +71/-20.What is deliberately not in this PR
These were found in the same audit but need a maintainer decision or are not simple bug fixes;
I have opened a separate issue for them:
_micros()on AVR (/32vs/64- needs anoscilloscope/hardware check), the
move()downsampling off-by-one, the EFR32 dead-time clamp andits
SILABBSmacro, thecharacteriseMotor()correction-factor inconsistency, thetrapezoid -> sine phase re-enable (needs a cheap way to detect a modulation change in the PWM
hot path), the performance items and the motor-class/Commander refactoring.
Suggested review path
git log --onelinegives 13 commits, one per bug. Reviewingcs-swap,hall-initandtrap-negativein detail is probably the most valuable; the rest are one to five lines each.Commits that change observable behaviour (analog offset, trapezoid
Uq < 0, PWM timeout, theHallSensorstart-up angle) are worth a quick hardware sanity check before release.