feat(encoder): CRTP magnetic_encoder base, espp::Timer update path, AS5600 12-bit fix - #718
feat(encoder): CRTP magnetic_encoder base, espp::Timer update path, AS5600 12-bit fix#718finger563 wants to merge 2 commits into
Conversation
…mer, fix AS5600 12-bit read Refactor the as5600 and mt6701 magnetic encoders, which were ~700 lines of verbatim-duplicated logic differing only in the per-device sensor read. - Add header-only `components/magnetic_encoder` providing the CRTP base `espp::MagneticEncoderBase<Derived, UseHighResTimer, CountsPerRevolution, MinDiff, RegAddr, UseAddress>`. It holds the shared update loop, get_* accessors, start/stop/initialize, and the periodic driver, calling `static_cast<Derived*>(this)->read(ec)` (zero-cost static dispatch, no vtable, no overhead at 1-2 kHz). As5600 and Mt6701<I> derive from it and provide only read(). - Remove the "override the update task" constructor `(Config, Task::Config)` and `initialize(..., Task::Config, ...)` overloads from both encoders. - Switch the non-HRT update path from espp::Task (drifting cv.wait_until loop) to espp::Timer, which schedules against an absolute wake-up time so it is periodic and in phase - important for stable velocity / accumulator state. The choice stays a compile-time Kconfig switch, renamed `*_USE_TIMER` -> `*_USE_HIGH_RESOLUTION_TIMER` (y = HighResolutionTimer, n = espp::Timer). - Fix an AS5600 correctness bug: the AS5600 is 12-bit (4096 counts / rev) but was read as 14-bit (16384) with the low 2 bits dropped, reporting ~1/16 of the true angle. read() now packs the full 12-bit value and COUNTS_PER_REVOLUTION is 4096. This changes the reported angle scale 16x (now correct per the datasheet). - Widen the accumulator from atomic<int> to atomic<int64_t> so continuous rotation no longer overflows (~131072 rev previously). get_accumulator() now returns int64_t. No in-tree consumers use it; BldcMotor uses only update/get_radians/get_rpm/get_mechanical_radians/needs_zero_search (unchanged). - MT6701 SSI: compute the 6-bit CRC and warn (rate-limited) on mismatch (the sample is still used pending hardware verification of the CRC); remove the unused MagneticFieldStatus struct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
Pull request overview
Refactors magnetic encoders around a shared CRTP base while correcting AS5600 scaling and improving MT6701 diagnostics.
Changes:
- Adds shared timer-driven encoder logic with 64-bit accumulation.
- Migrates AS5600 and MT6701 to the new base.
- Adds MT6701 CRC observability and updates documentation/configuration.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
doc/en/encoder/mt6701.rst |
Documents timer-based updates. |
doc/en/encoder/magnetic_encoder.rst |
Adds base-class documentation. |
doc/en/encoder/index.rst |
Links the new documentation. |
doc/en/encoder/as5600.rst |
Documents timer-based updates. |
doc/Doxyfile |
Includes the new header. |
components/mt6701/Kconfig |
Renames timer configuration. |
components/mt6701/include/mt6701.hpp |
Adopts CRTP base and adds CRC checking. |
components/mt6701/idf_component.yml |
Adds the new dependency. |
components/mt6701/CMakeLists.txt |
Updates build dependencies. |
components/magnetic_encoder/include/magnetic_encoder_base.hpp |
Implements shared encoder behavior. |
components/magnetic_encoder/idf_component.yml |
Defines the registry component. |
components/magnetic_encoder/CMakeLists.txt |
Registers the component. |
components/as5600/Kconfig |
Renames timer configuration. |
components/as5600/include/as5600.hpp |
Adopts CRTP base and fixes 12-bit reads. |
components/as5600/idf_component.yml |
Adds the new dependency. |
components/as5600/CMakeLists.txt |
Updates build dependencies. |
Suppressed comments (2)
components/magnetic_encoder/include/magnetic_encoder_base.hpp:220
- This limit is symmetric, but the comparison only warns for positive rotation. A negative velocity at the measurement limit is never reported; compare the magnitude instead.
if (raw_velocity >= max_velocity) {
components/magnetic_encoder/include/magnetic_encoder_base.hpp:329
- When initialized with
run_task = false,prev_time_us_remains zero, so the first documented manualupdate()computes velocity over time since boot rather than time since initialization. Seed the timestamp alongside the accumulator;start()can still refresh it for timer mode.
accumulator_ = count_.load();
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…s, registry - update(): commit prev_time_us_ only after a successful read(), so a failed read no longer advances the timestamp (which made the next successful sample divide movement by the time since the failed attempt, inflating velocity). - init(): seed prev_time_us_ after the initial read so the first manual update() (run_task = false) measures dt from init time rather than since boot. - update(): compare |raw_velocity| against the measurement limit so the "nearing limit" warning fires for both rotation directions, not just positive. - Add components/magnetic_encoder/README.md and list the component in upload_components.yml (ahead of as5600 / mt6701, which depend on it) so the registry package is published and its dependency resolves. - Update the as5600 / mt6701 component READMEs to describe the timer (not task), and clarify in the example READMEs that the polling Task is the example's own (the encoder maintains its state with an internal timer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the review in d855c4b: Correctness (magnetic_encoder_base.hpp)
Registry / docs
One item intentionally not done: the request for a standalone example project + CI build-matrix entry for |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
components/as5600/include/as5600.hpp:21
- The class-level comment still states it "starts its own measurement task", but the implementation now uses an internal timer (HighResolutionTimer/Timer) rather than a task. Update this wording to avoid misleading API users.
/**
* @brief Class for position and velocity measurement using a AS5600 magnetic
* encoder. This class starts its own measurement task at the specified
components/mt6701/include/mt6701.hpp:27
- The class-level comment still states it "starts its own measurement task", but the implementation now uses an internal timer (HighResolutionTimer/Timer) rather than a task. Update this wording to keep the API documentation accurate.
/**
* @brief Class for position and velocity measurement using a MT6701 magnetic
* encoder. This class starts its own measurement task at the specified
components/as5600/include/as5600.hpp:74
- The field name
run_tasknow controls whether the internal timer runs on startup, which is inconsistent and can confuse users (especially since task-based override APIs were removed). Consider introducing a clearer name (e.g.,run_timer/auto_start) while keepingrun_taskas a backward-compatible alias/deprecated field, or update public docs to explicitly call out thatrun_taskrefers to the timer.
bool run_task{true}; ///< Whether to run the timer on startup. If false, you must call update()
| // that the timer does not fire until start() is called. | ||
| if constexpr (UseHighResTimer) { | ||
| timer_ = std::make_unique<espp::HighResolutionTimer>(espp::HighResolutionTimer::Config{ | ||
| .name = std::string(name), .callback = [this]() { update_task(); }}); |
Summary
Refactors the
as5600andmt6701magnetic encoders, which were ~700 lines of verbatim-duplicated logic differing only in the per-device sensor read.components/magnetic_encoder.espp::MagneticEncoderBase<Derived, UseHighResTimer, CountsPerRevolution, MinDiff, RegAddr, UseAddress>holds the shared update loop, allget_*accessors,start/stop/initialize, and the periodic driver. It refreshes state viastatic_cast<Derived*>(this)->read(ec)— zero-cost static dispatch, no vtable, so no overhead even at 1–2 kHz.As5600andMt6701<I>derive from it and supply onlyread().(Config, espp::Task::Config)constructor and theinitialize(..., espp::Task::Config, ...)overload are gone from both encoders. Users can no longer swap the update task out from under the encoder.espp::Taskwith a manualcv.wait_untilloop that drifts (fixed-delay). It now usesespp::Timer, which schedules against an absolute wake-up time (start + k*period) — periodic and in phase, which matters for stable velocity / accumulator state. The choice stays a compile-time Kconfig switch, renamed*_USE_TIMER→*_USE_HIGH_RESOLUTION_TIMER(y=HighResolutionTimer,n=espp::Timer).read()now packs the full 12-bit value andCOUNTS_PER_REVOLUTIONis 4096.int→int64_t.atomic<int>overflowed after ~131072 rev (~44 min @ 3000 RPM).get_accumulator()now returnsint64_t.MagneticFieldStatusstruct.get_accumulator()return typeint→int64_t(minor source-compat; no in-tree consumers use it).*_USE_TIMER→*_USE_HIGH_RESOLUTION_TIMER(a mildsdkconfigbreak; existingyconfigs still select the HighResolutionTimer).Note on the MT6701 CRC
The CRC-6 check is observability-only (warn on mismatch, the sample is still used). The CRC table/algorithm hasn't been verified against hardware yet — rejecting samples with a subtly wrong implementation would brick the encoder. Once verified on-device, it can be promoted to set
ecand skip the bad sample. Watch the logs forCRC mismatchwarnings during clean reads.Consumer impact
BldcMotor(and the MotorGo boards) call onlyupdate / get_radians / get_rpm / get_mechanical_radians / needs_zero_searchon the encoder — all unchanged signatures. Nothing in-tree callsget_accumulator, so theint64_twidening breaks no callers. The two encoder examples build unchanged (they used only the plainConfigconstructor).Verification
Built an esp32s3 project instantiating
As5600,Mt6701<I2C>, andMt6701<SSI>exercising the full public API + the fmt formatters. Compiles cleanly in both Kconfig branches (HighResolutionTimer default andUSE_HIGH_RESOLUTION_TIMER=n→espp::Timer).static_asserts lock inint64_t get_accumulator()and the 4096 / 16384 resolutions.🤖 Generated with Claude Code