Repository navigation
Fix confirmed logic bugs found while auditing the library sources (13 fixes, one per commit) - #571
stijncarelsbergh wants to merge 15 commits into
Conversation
Fixed broken link on README.md
…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.
|
Thanks for this. Once again a lot of changes.... First of all we only accept the PRs against the dev branch. And some of the fixes have already been merged by other people. If not I'm going to do my best to check it but it will take time. |
|
Split into one PR per fix, all against
Dropped because Each branch contains exactly that one fix on top of One note on the checks, in case it saves you time: the STM32 jobs were failing on this PR while the same sketch ( |
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.