You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Audit follow-up: items left out of #570/#571 (hardware verification, semantics decisions, performance and refactors) #572
While going through the library sources against the documentation I collected a number of
findings. The ones that are unambiguous and low-risk to change are in two PRs:
The items below were deliberately left out of those PRs because they either need hardware
verification, change documented semantics, or are proposals rather than fixes. Listing them here
so they don't get lost - happy to turn any of them into a PR if you agree with the direction.
Everything below is from static reading/modelling; I have not measured any of it on hardware.
1. Needs hardware verification: _micros() on AVR (src/common/time_utils.cpp:25)
With TCCR0B prescaler 1 the chip runs Timer0 at F_CPU (16 MHz on the Uno), while micros() assumes the standard prescaler 64 (1 MHz) - its overflow counter adds 1024 us per
overflow instead of 16 us, i.e. it is 64x too large, not 32x. If that reading is right the
workaround should be /64. It obviously works for the users who run with a modified prescaler
(there is a reason this code exists), so this one needs an oscilloscope rather than a reader:
measure _micros()/micros() with the prescaler left at 1 and see which divisor matches. I did
not want to change timing code that many setups depend on without that measurement.
2. Needs a semantic decision: move() downsampling is off by one (FOCMotor.cpp:679)
monitor_downsample = 10 means "every 10th loop", motion_downsample = 10 means "every 11th
loop". The documented parameter is shared by both (foc_control.pdf describes downsampling as
running the control loop every N loops), so this is a discrepancy rather than an obvious bug -
but changing it alters the effective control period for every user who sets motion_downsampling/motion_downsample.
3. Silicon Labs EFR32: dead time silently collapses (efr32_pwm.cpp:300, efr32_mcu.cpp:285)
unsignedint dtiTime = (CMU_ClockFreqGet(timerClock) / 1e3f) * config->deadTimeNs / 1e6f;
if (dtiTime > 64) dtiTime = SILABBS_DEFAULT_DEAD_TIME; // <- 3 ticks, i.e. tens of ns
When the requested dead time does not fit in the register, it is replaced by 3 timer ticks
(~40 ns at 76.8 MHz) instead of being clamped to the maximum, i.e. a requested dead time
becomes an order of magnitude too small - the opposite of the safe direction (shoot-through).
deadTimeNs >> 1 halves the dead time for the complementary outputs; it is not obvious whether
that is compensating for something else in the driver, so both need a scope check together.
The macro is misspelled SILABBS_DEFAULT_DEAD_TIME while the rest of the same files use SILABS_* (efr32_pwm.h:8-9 vs efr32_mcu.h:25, SILABS_DEFAULT_DEAD_ZONE). Since it is a
public-by-accident override hook, renaming it is a (small) API break, hence not in the PRs.
float resistance = voltage / (correction_factor * (r_currents.d - zerocurrent.d)); // R / cf
...
inductanced += fabsf(-(resistance * dt) / log(...)) / correction_factor; // L / cf
The resistance estimate is already scaled by 1/cf, and the inductance expression is linear in
the resistance, so the inductance ends up scaled by 1/cf^2 while the resistance is scaled by 1/cf. Either the inductance line should not divide again, or the correction factor means
something different from what the comment suggests. I did not want to guess which one is intended
it changes the numbers reported by motor.characteriseMotor() and the R/L used for tuning.
5. Reported, needs a cheap fix design: switching away from trapezoid leaves a phase off
BLDCMotor::setPhaseVoltage() calls driver->setPhaseState(..., PHASE_OFF, ...) in the trapezoid
branches, but the SinePWM/SpaceVectorPWM branch never re-enables the phases. A 6-PWM driver
stores the phase state and re-applies it on every write, so after a runtime switch from trapezoid
to sine the motor keeps running on two phases (or not at all) until disable()/enable().
The straightforward fix - calling setPhaseState(PHASE_ON, PHASE_ON, PHASE_ON) in the sine branch -
adds a call (and up to three digitalWrites for drivers with enable pins) in the PWM hot path,
so it wants a cheap change detector (e.g. remembering the last foc_modulation used). Left out of #571 because that adds a member to BLDCMotor and I would rather have your opinion on the
approach first.
6. Smaller inconsistencies, all safe to change but not urgent
where
what
HallSensor.h
decodeDirection(int,int) is declared private but never defined or used (dead declaration)
HallSensor.h
velocity_max = 1000.0f is declared ("variable used to filter outliers") but never read
CurrentSense.cpp:628,701
float ca[3]/float cb[3] are computed and never used
MagneticSensorAnalog vs MagneticSensorPWM
cpr = max - min vs cpr = max - min + 1 for the same "raw counts" concept; the +1 makes the two sensors' resolutions differ by one count
MagneticSensorAnalog::getSensorAngle()
no clamping of raw_count to [min,max], unlike the PWM sensor (one sample outside the range gives a jump of a full sector)
MagneticSensorI2C.cpp:19
the AS5048_I2C preset uses lsb_mask = 0xFF, lsb_shift = 0, which keeps the two parity bits in the result (up to 3 counts of error, ~0.07 deg at 14 bit)
Commander.cpp
add() casts away const when storing the label ((char*)label) while call_label could be const char*
7. Performance proposals (measured estimates, not micro-benchmarks)
From the same audit, ranked by (impact x confidence). All numbers are static estimates; the
measurement recipe is motor.loopfoc_time_us / motor.move_time_us before/after.
Calibration is dominated by fixed delays: 1.7 s of _delay() per initFOC() plus 100x
sample averaging just for the current-sense alignment (CurrentSense.cpp, readAverageCurrents()).
Cutting the ramps/_delay(500)s proportionally would remove roughly 1.5 s from every initFOC().
MagneticSensorSPI::read() has an unconditional delayMicroseconds(50) on ESP32 with a
comment from the author saying it is not needed; removing it removes 50 us per control loop on
ESP32 (at 1 kHz that is 5 % of the budget, and it is inside the getSensorAngle() path).
_sincos() does two table lookups where one would do (the table is already in cache) - two
divisions/roundings per call saved, called twice per setPhaseVoltage().
_normalizeAngle() is called several times per loop and could have a fast path for the
common in-range case; open-loop increments could wrap directly.
PID/LPF: recomputing Tf/(Tf+Ts) and friends every call, plus a division in the filter;
a constant-Ts fast path would remove divisions from the hot path.
AVR: move the 65-entry sine table to flash (PROGMEM) - 130 bytes of the Uno's 2 KB SRAM.
Commander table/buffer sizes are hard-coded (20 callbacks, MAX_COMMAND_LENGTH chars);
making them configurable costs nothing at runtime and saves RAM on small targets.
8. Refactors (no behaviour change, larger diff)
De-duplicate the three motor classes (BLDCMotor, StepperMotor, HybridStepperMotor
share a lot of the cascade/PID/limits/characterisation code).
Commander print/parse refactor - the print/parse switch statements are ~400 lines of
duplicated format strings.
Docs QA in CI - link/anchor/API-name checks exist as throwaway scripts in my workspace;
they could live in simplefoc.github.io as a GitHub Action so broken links and renamed API
calls are caught on PRs.
Happy to split any of 1-6 into reviewable PRs, and to contribute 8-11 in stages, if that is
useful - just say which ones you want and in which order.
Status update, now that the library work is based on dev rather than master.
Already fixed upstream (so no longer on my list):
MagneticSensorAnalog now subtracts min_raw_countand uses cpr = max - min + 1, so
both the ~5 deg offset and the off-by-one I had under "smaller inconsistencies" are gone.
The phase-B half of the current-sense alignment was rewritten (it now uses the same
max-ratio detection as phase A), so the missing-fabs() comparison described in item 5 no
longer exists. The A-C/B-C comparisons still have it - that part is now PR fix(current sense): compare current magnitudes, not signed values #575.
Still present on dev, re-checked just now, unchanged:
FOCMotor.cpp:692 - if(motion_cnt++ < motion_downsample) return; (still N+1 vs the monitor path's N)
efr32_pwm.cpp:300 - if (dtiTime > 64) dtiTime = SILABBS_DEFAULT_DEAD_TIME; and the SILABBS_ vs SILABS_ macro spelling
characteriseMotor() - resistance scaled once by correction_factor, inductanced
divided by it as well on top of that, i.e. still 1/cf^2 for L against 1/cf for R
Moved out of this issue: the bugs that were concrete and low risk went into PR #571, which
has now been split into one PR per fix (#573-#584) at the maintainer's request. The items above
stay here because they need either a hardware measurement (AVR _micros(), EFR32 dead time) or a
decision on documented behaviour (move() downsampling, correction factor).
The performance proposals in item 7 and the refactors in item 8 are also unchanged - I have not
re-checked those line by line, since they are proposals rather than defects.
Context
While going through the library sources against the documentation I collected a number of
findings. The ones that are unambiguous and low-risk to change are in two PRs:
The items below were deliberately left out of those PRs because they either need hardware
verification, change documented semantics, or are proposals rather than fixes. Listing them here
so they don't get lost - happy to turn any of them into a PR if you agree with the direction.
Everything below is from static reading/modelling; I have not measured any of it on hardware.
1. Needs hardware verification:
_micros()on AVR (src/common/time_utils.cpp:25)With
TCCR0Bprescaler 1 the chip runs Timer0 atF_CPU(16 MHz on the Uno), whilemicros()assumes the standard prescaler 64 (1 MHz) - its overflow counter adds 1024 us peroverflow instead of 16 us, i.e. it is 64x too large, not 32x. If that reading is right the
workaround should be
/64. It obviously works for the users who run with a modified prescaler(there is a reason this code exists), so this one needs an oscilloscope rather than a reader:
measure
_micros()/micros()with the prescaler left at 1 and see which divisor matches. I didnot want to change timing code that many setups depend on without that measurement.
2. Needs a semantic decision:
move()downsampling is off by one (FOCMotor.cpp:679)monitor_downsample = 10means "every 10th loop",motion_downsample = 10means "every 11thloop". The documented parameter is shared by both (
foc_control.pdfdescribes downsampling asrunning the control loop every N loops), so this is a discrepancy rather than an obvious bug -
but changing it alters the effective control period for every user who sets
motion_downsampling/motion_downsample.3. Silicon Labs EFR32: dead time silently collapses (
efr32_pwm.cpp:300,efr32_mcu.cpp:285)(~40 ns at 76.8 MHz) instead of being clamped to the maximum, i.e. a requested dead time
becomes an order of magnitude too small - the opposite of the safe direction (shoot-through).
deadTimeNs >> 1halves the dead time for the complementary outputs; it is not obvious whetherthat is compensating for something else in the driver, so both need a scope check together.
SILABBS_DEFAULT_DEAD_TIMEwhile the rest of the same files useSILABS_*(efr32_pwm.h:8-9vsefr32_mcu.h:25,SILABS_DEFAULT_DEAD_ZONE). Since it is apublic-by-accident override hook, renaming it is a (small) API break, hence not in the PRs.
4.
characteriseMotor()appliescorrection_factorinconsistently (FOCMotor.cpp:142,208)The resistance estimate is already scaled by
1/cf, and the inductance expression is linear inthe resistance, so the inductance ends up scaled by
1/cf^2while the resistance is scaled by1/cf. Either the inductance line should not divide again, or the correction factor meanssomething different from what the comment suggests. I did not want to guess which one is intended
motor.characteriseMotor()and the R/L used for tuning.5. Reported, needs a cheap fix design: switching away from trapezoid leaves a phase off
BLDCMotor::setPhaseVoltage()callsdriver->setPhaseState(..., PHASE_OFF, ...)in the trapezoidbranches, but the
SinePWM/SpaceVectorPWMbranch never re-enables the phases. A 6-PWM driverstores the phase state and re-applies it on every write, so after a runtime switch from trapezoid
to sine the motor keeps running on two phases (or not at all) until
disable()/enable().The straightforward fix - calling
setPhaseState(PHASE_ON, PHASE_ON, PHASE_ON)in the sine branch -adds a call (and up to three
digitalWrites for drivers with enable pins) in the PWM hot path,so it wants a cheap change detector (e.g. remembering the last
foc_modulationused). Left out of#571 because that adds a member to
BLDCMotorand I would rather have your opinion on theapproach first.
6. Smaller inconsistencies, all safe to change but not urgent
HallSensor.hdecodeDirection(int,int)is declared private but never defined or used (dead declaration)HallSensor.hvelocity_max = 1000.0fis declared ("variable used to filter outliers") but never readCurrentSense.cpp:628,701float ca[3]/float cb[3]are computed and never usedMagneticSensorAnalogvsMagneticSensorPWMcpr = max - minvscpr = max - min + 1for the same "raw counts" concept; the +1 makes the two sensors' resolutions differ by one countMagneticSensorAnalog::getSensorAngle()raw_countto[min,max], unlike the PWM sensor (one sample outside the range gives a jump of a full sector)MagneticSensorI2C.cpp:19AS5048_I2Cpreset useslsb_mask = 0xFF, lsb_shift = 0, which keeps the two parity bits in the result (up to 3 counts of error, ~0.07 deg at 14 bit)Commander.cppadd()casts awayconstwhen storing the label ((char*)label) whilecall_labelcould beconst char*7. Performance proposals (measured estimates, not micro-benchmarks)
From the same audit, ranked by (impact x confidence). All numbers are static estimates; the
measurement recipe is
motor.loopfoc_time_us/motor.move_time_usbefore/after._delay()perinitFOC()plus 100xsample averaging just for the current-sense alignment (
CurrentSense.cpp,readAverageCurrents()).Cutting the ramps/
_delay(500)s proportionally would remove roughly 1.5 s from everyinitFOC().MagneticSensorSPI::read()has an unconditionaldelayMicroseconds(50)on ESP32 with acomment from the author saying it is not needed; removing it removes 50 us per control loop on
ESP32 (at 1 kHz that is 5 % of the budget, and it is inside the
getSensorAngle()path)._sincos()does two table lookups where one would do (the table is already in cache) - twodivisions/roundings per call saved, called twice per
setPhaseVoltage()._normalizeAngle()is called several times per loop and could have a fast path for thecommon in-range case; open-loop increments could wrap directly.
Tf/(Tf+Ts)and friends every call, plus a division in the filter;a constant-
Tsfast path would remove divisions from the hot path.PROGMEM) - 130 bytes of the Uno's 2 KB SRAM.MAX_COMMAND_LENGTHchars);making them configurable costs nothing at runtime and saves RAM on small targets.
8. Refactors (no behaviour change, larger diff)
BLDCMotor,StepperMotor,HybridStepperMotorshare a lot of the cascade/PID/limits/characterisation code).
print/parseswitch statements are ~400 lines ofduplicated format strings.
_sin,_atan2, PID, LPF,_normalizeAngle,the current-sense alignment arithmetic with a fake ADC). The alignment bugs fixed in Fix confirmed logic bugs found while auditing the library sources (13 fixes, one per commit) #571 are
exactly the class of thing a small host test would catch: they need no hardware, only a
simulated set of currents.
they could live in
simplefoc.github.ioas a GitHub Action so broken links and renamed APIcalls are caught on PRs.
Happy to split any of 1-6 into reviewable PRs, and to contribute 8-11 in stages, if that is
useful - just say which ones you want and in which order.