-
Notifications
You must be signed in to change notification settings - Fork 474
fix(ledc): compute ESP32-H2 low-speed timer divisor from the selected source clock #5941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yvf
wants to merge
5
commits into
esp-rs:main
Choose a base branch
from
yvf:fix/esp32h2-ledc-ls-source-clock
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+191
−0
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
b0ae142
fix(ledc): compute ESP32-H2 low-speed divisor from the selected sourc…
yvf 71299e4
test(ledc): HIL test asserting output frequency matches configuration
yvf e0926c3
Merge branch 'main' into fix/esp32h2-ledc-ls-source-clock
yvf a087444
cargo fmt changes
yvf e012bde
Merge branch 'main' into fix/esp32h2-ledc-ls-source-clock
yvf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| //! LEDC output-frequency HIL test. | ||
| //! | ||
| //! Verifies that a LowSpeed LEDC timer produces PWM at the frequency it was | ||
| //! configured for, by measuring the real signal period on hardware. | ||
| //! | ||
| //! This guards the timer-divisor computation, which derives the divisor from the | ||
| //! *source clock* the LEDC slow-clock mux actually selects. If the driver models | ||
| //! the wrong source frequency, the divisor is miscalculated and the output comes | ||
| //! out at a fixed ratio of the requested rate - a mistake that a purely | ||
| //! register-level or `frequency()`-getter check cannot catch, because the getter | ||
| //! returns the *requested* value, not the achieved one. Concretely, on the | ||
| //! ESP32-H2 `set_global_slow_clock(APBClk)` selects the 32 MHz XTAL while the | ||
| //! divisor was briefly computed from the 96 MHz "APB" figure, so every timer ran | ||
| //! at 1/3 of its configured frequency (see PR #5941). | ||
| //! | ||
| //! No external wiring: one `common_test_pins!` pin is split into an input and an | ||
| //! output driver, LEDC drives the output half and the input half samples it. | ||
|
|
||
| //% CHIP_FILTER: ledc_driver_supported | ||
| //% FEATURES: unstable | ||
|
|
||
| #![no_std] | ||
| #![no_main] | ||
|
|
||
| use esp_hal::{ | ||
| delay::Delay, | ||
| gpio::{AnyPin, DriveMode, Flex, Input, Pin}, | ||
| ledc::{ | ||
| LSGlobalClkSource, | ||
| Ledc, | ||
| LowSpeed, | ||
| channel::{self, ChannelIFace}, | ||
| timer::{self, TimerIFace}, | ||
| }, | ||
| peripherals::LEDC, | ||
| time::{Instant, Rate}, | ||
| }; | ||
| #[allow(unused_imports)] | ||
| use hil_test::{assert, assert_eq}; | ||
|
|
||
| /// Rising edges to average the period over. More edges tighten the estimate; 64 | ||
| /// edges is ~32 ms at 2 kHz, comfortably inside the default test timeout. | ||
| const PERIOD_SAMPLE_EDGES: usize = 64; | ||
|
|
||
| /// Allowed deviation of the measured period from the configured one. GPIO-polled | ||
| /// edge timing carries some jitter, so the bound is generous; the bug this test | ||
| /// exists for shifts the frequency by ~3x (a ~200% error), an order of magnitude | ||
| /// past this tolerance. | ||
| const PERIOD_TOLERANCE_PERCENT: u32 = 20; | ||
|
|
||
| /// Measure the average PWM period (in microseconds) over `edges` rising edges of | ||
| /// `input`, using the hardware timer for timestamps (not a loop counter, so it is | ||
| /// insensitive to polling-loop speed). | ||
| /// | ||
| /// A stuck (never-toggling) output never reaches `edges` and surfaces as a test | ||
| /// timeout rather than a wrong value - still a failure, which is the point. | ||
| fn average_period_us(input: &Input<'_>, edges: usize) -> u32 { | ||
| assert!(edges > 1); | ||
|
|
||
| let mut prev = input.is_high(); | ||
|
|
||
| // Align to the first rising edge so the first measured interval is a full period. | ||
| loop { | ||
| let now = input.is_high(); | ||
| if !prev && now { | ||
| break; | ||
| } | ||
| prev = now; | ||
| core::hint::spin_loop(); | ||
| } | ||
|
|
||
| let first_edge = Instant::now(); | ||
| let mut last_edge = first_edge; | ||
| let mut seen_edges = 1usize; | ||
| prev = true; | ||
|
|
||
| while seen_edges < edges { | ||
| let now = input.is_high(); | ||
| if !prev && now { | ||
| seen_edges += 1; | ||
| last_edge = Instant::now(); | ||
| } | ||
| prev = now; | ||
| core::hint::spin_loop(); | ||
| } | ||
|
|
||
| let total_us = (last_edge - first_edge).as_micros() as u32; | ||
| total_us / (edges as u32 - 1) | ||
| } | ||
|
|
||
| /// Configure a LowSpeed timer+channel at `frequency` (50% duty) on `test_pin` and | ||
| /// assert the measured output period matches, within `PERIOD_TOLERANCE_PERCENT`. | ||
| fn assert_output_frequency( | ||
| ledc: LEDC<'static>, | ||
| test_pin: AnyPin<'static>, | ||
| delay: Delay, | ||
| frequency: Rate, | ||
| ) { | ||
| let pin = Flex::new(test_pin); | ||
| // SAFETY: the output half is driven only by LEDC and the input half is only sampled. | ||
| let (input, output) = unsafe { pin.split_into_drivers() }; | ||
|
|
||
| let mut ledc = Ledc::new(ledc); | ||
| ledc.set_global_slow_clock(LSGlobalClkSource::APBClk); | ||
|
|
||
| let mut timer0 = ledc.timer::<LowSpeed>(timer::Number::Timer0); | ||
| timer0 | ||
| .configure(timer::config::Config { | ||
| duty: timer::config::Duty::Duty8Bit, | ||
| clock_source: timer::LSClockSource::APBClk, | ||
| frequency, | ||
| }) | ||
| .unwrap(); | ||
|
|
||
| let mut channel0 = ledc.channel(channel::Number::Channel0, output); | ||
| channel0 | ||
| .configure(channel::config::Config { | ||
| timer: &timer0, | ||
| duty_pct: 50, | ||
| drive_mode: DriveMode::PushPull, | ||
| }) | ||
| .unwrap(); | ||
|
|
||
| // Let the output settle before sampling. | ||
| delay.delay_millis(4); | ||
|
|
||
| let measured_us = average_period_us(&input, PERIOD_SAMPLE_EDGES); | ||
|
|
||
| let expected_us = 1_000_000u32 / frequency.as_hz(); | ||
| let min_us = expected_us * (100 - PERIOD_TOLERANCE_PERCENT) / 100; | ||
| let max_us = expected_us * (100 + PERIOD_TOLERANCE_PERCENT) / 100; | ||
|
|
||
| assert!( | ||
| measured_us >= min_us && measured_us <= max_us, | ||
| "measured period {} us at {} Hz, expected {} us (+/- {}%)", | ||
| measured_us, | ||
| frequency.as_hz(), | ||
| expected_us, | ||
| PERIOD_TOLERANCE_PERCENT | ||
| ); | ||
| } | ||
|
|
||
| struct Context { | ||
| ledc: LEDC<'static>, | ||
| test_pin: AnyPin<'static>, | ||
| delay: Delay, | ||
| } | ||
|
|
||
| #[embedded_test::tests(default_timeout = 3, executor = hil_test::Executor::new())] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[init] | ||
| fn init() -> Context { | ||
| let peripherals = esp_hal::init(esp_hal::Config::default()); | ||
| let (_, test_pin) = hil_test::common_test_pins!(peripherals); | ||
|
|
||
| Context { | ||
| ledc: peripherals.LEDC, | ||
| test_pin: test_pin.degrade(), | ||
| delay: Delay::new(), | ||
| } | ||
| } | ||
|
|
||
| /// The core regression guard: a mid-range frequency must appear at the output. | ||
| /// Off-by-source-clock divisor bugs make this a fixed ratio too high or low. | ||
| #[test] | ||
| fn output_frequency_matches_configuration_2khz(ctx: Context) { | ||
| assert_output_frequency(ctx.ledc, ctx.test_pin, ctx.delay, Rate::from_khz(2)); | ||
| } | ||
|
|
||
| /// A second, 4x-lower frequency: since a wrong source clock scales *every* | ||
| /// frequency by the same factor, this both re-confirms the fix and catches a | ||
| /// regression where the output ignores the configured rate altogether. | ||
| #[test] | ||
| fn output_frequency_matches_configuration_500hz(ctx: Context) { | ||
| assert_output_frequency(ctx.ledc, ctx.test_pin, ctx.delay, Rate::from_hz(500)); | ||
| } | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please adhere to the coding guidelines we have. In this case, that would be using
cfg_select. There's also no need for any of that comment. The difference will be encoded in the per-device clock tree data in the future.