Skip to content

feat(adc): differential sampling support#999

Open
NoahLutz wants to merge 45 commits intoatsamd-rs:masterfrom
VoltServer:adc-differential-sampling
Open

feat(adc): differential sampling support#999
NoahLutz wants to merge 45 commits intoatsamd-rs:masterfrom
VoltServer:adc-differential-sampling

Conversation

@NoahLutz
Copy link
Copy Markdown

@NoahLutz NoahLutz commented Mar 12, 2026

Summary

EDIT: See discussion below for updated approach

Adds support for sampling with differential ADC inputs via AdcInput, a new trait that represents either a differential or single-ended input. This has been implemented as a breaking change, updating the Adc::read() function to use an AdcInput rather than the prior AdcPin trait, however, these changes could also be implemented in a way which maintains the old ADC API by separating the new implementation into a separate read function which accepts the new AdcInput.

As implemented now, the caller now creates either a SingleEndedInput or a DifferentialInput, passing in the appropriate pins/channels, which is then passed into Adc::read()

// ADC initialization omitted

let mut single_ended_pin = ...;
let mut single_ended_input = SingleEndedInput::from_pin(&mut single_ended_pin);

let mut pos_pin = ...;
let mut neg_pin = ...;
let mut differential_input = DifferentialInput::from_pins(&mut pos_pin, &mut neg_pin);

let single_ended_result = adc.read(&mut single_ended_input);
let differential_result = adc.read(&mut differential_input);

I'm open to any and all suggestions or recommendations! This is my first time contributing and I realize that this is quite a big change to the way the ADC driver operates.

Pin & Channel Traits

To support the AdcInput trait, the manner in which the marker traits for ADC pins are defined has been changed. The concept of an ADC channel vs an ADC pin has been separated into different traits, PosChannel and NegChannel for positive and negative ADC channels and PosAdcPin and NegAdcPin, for pins which can be configured as a positive or negative ADC input.

One of the benefits of this approach is that callers can now sample on-chip-only ADC channels, such as DAC0 for the chips which have support.

let mut dac0_input = SingleEndedInput::from_channel(&DAC0.get_channel());
let dac0_value = adc.read(&mut dac0_input);

ADC Channels

The PosChannel and NegChannel traits contain an associated constant of the Muxposselect or Muxnegselect type from the PAC. A new macro channel!() is used to generate the structs for each channel present on the device and implement the PosChannel and NegChannel traits appropriately in adc/d11/channel.rs and adc/d5x/channel.rs. These macros support adding additional marker traits to each channel, such as the CpuVoltageSource trait which indicates which ADC channels measure various CPU voltages and can be passed into Adc::read_cpu_voltage()

ADC Pins

The adc_pins!() macro has been separated into a pos_adc_pins!() macro and a neg_adc_pins!() macro. These work similar to the adc_pins!() macro, however, they now link the ADC-capable pins to their appropriate ADC channel type rather than the mux value directly.

Handling Conversion Result Sign

Since sampling differential inputs can result in negative values, the AdcInput trait requires implementers specify the result type via the associated type AdcInput::Output. This associated type is limited to primitive integers via the num_traits::PrimInt trait. As expected, SingleEndedInput uses u16 and DifferentialInput uses i16. To perform the appropriate casting, the AdcInput trait also requires an implementation for AdcInput::cast_result(), which is called within Adc::read().

Hardware Summation and Averaging

To ensure signed values are handled properly in all cases, including when utilizing hardware summation or averaging, the ability to configure the left-adjust feature of the ADC has been exposed. Setting AdcBuilder.auto_left_adjust to true enables automatic setting of CTRLB.LEFTADJ for DifferentialInput's only, and automatic clearing for SingleEndedInput's. When enabled and sampling a DifferentialInput, the value from the RESULT register is automatically right-shifted back the appropriate amount.

Other Changes

Newly-Exposed ADC Features

In addition to the ADC's left-adjust feature, the reference compensation, offset compensation and rail-to-rail ADC features have also been exposed via AdcBuilder. The left-adjust and reference compensation features are available on all chips, however the offset compensation and rail-to-rail features are only available on D5x family chips. Enabling rail-to-rail or offset compensation for D11/D21 chips has no effect.

Reference compensation and offset compensation may be enabled at initialization and remain enabled unless the ADC is reconfigured. Since rail-to-rail can only be enabled when sampling a differential input, the rail-to-rail feature has been exposed in a similar manner to left-adjust, where it is automatically set and cleared based on the sample mode. Additionally, as stated in the D5x family datasheet, when rail-to-rail is enabled, offset compensation must also be enabled and is automatically enabled/disabled when AdcBuilder.auto_rail_to_rail is enabled.

New Methods

A method to retrieve the configured resolution of the ADC has been added, Adc::get_resolution(). This utilizes Accumulation::output_resolution() to calculate the effective resolution of the ADC result. This aids in downstream calculations, especially when converting the raw ADC count into a voltage.

Checklist

  • All new or modified code is well documented, especially public items
  • No new warnings or clippy suggestions have been introduced - CI will deny clippy warnings by default! You may #[allow] certain lints where reasonable, but ideally justify those with a short comment.

… de-shifting when returning 12-bit signed result
…n CTRLB.LEFTADJ is enabled (when using differential sampling)
…rail-to-rail operation for differential inputs
…t result when ADC is configured with Accumulation::Single
…l marker trait, updated async API to be compatible with prior changes
@NoahLutz NoahLutz marked this pull request as ready for review March 12, 2026 19:11
Copy link
Copy Markdown
Contributor

@rnd-ash rnd-ash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall a nice PR, but I have some thoughts.

This looks like it will end up adding a lot of overhead to both the user (having to wrap the pins in another wrapper type every time they are used), and also in ADC Read time (Since there is a fair bit more runtime code that runs now for every ADC Read)

As a proposal, I think it would have been slightly better to keep the current ADC Read method, and simply include a read_differential method, rather than using complex a wrapper type for each type of ADC Read - Then the question is if the extra stuff included in the ADCBuilder specifically for differential reading should be included instead to the read_differential method, or kept in the ADC Builder...I'm happy for either idea, since the ADCBuilder logic is only really ran once.

Comment thread hal/src/peripherals/adc/builder.rs
Comment thread hal/src/peripherals/adc/builder.rs
Comment thread hal/src/peripherals/adc/input.rs Outdated

#[inline]
fn cast_result(result: u16) -> Self::Output {
result as Self::Output
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer using u16::cast_signed() method here rather

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like using u16::cast_signed() causes some clippy CI checks to fail as the current MSRV is too low. Is it still preferred over simply using as?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using as will also work, cast_signed is just more explicit, and yeah, I forgot our MSRV is not the latest

#[hal_macro_helper]
pub(super) fn set_sample_mode(&mut self, sample_mode: SampleMode) {
// Disable the ADC if chip is SAMD11 family
// SAMD11 datasheet section 31.8.5 states that the ADC must be disabled to modify
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably document this in the user docs that on D11 the ADC is shut down briefly

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've placed a doc comment on set_sample_mode() however, I realize that since this is not part of the public API that users will not be able to view this info in the documentation. Is there a better place to stick this info other than on the Adc::read*() methods?

@NoahLutz
Copy link
Copy Markdown
Author

NoahLutz commented Mar 16, 2026

@rnd-ash Thanks for the review!

I definitely see your point about the additional overhead with the complex wrapper type. I've gone ahead and refactored as you suggested and restored the read() method and added a new read_differential() method. Both these functions now take the new PosAdcPin/NegAdcPin traits, which I think simplifies the API a bit and has the added benefit of making these changes non-breaking. This should allow any code written previously to still be compatible (updates to the ADC examples have been reverted).

I also added the feature gates to AdcBuilder::enable_offset_compensation() and AdcBuilder::enable_auto_rail_to_rail() as you recommended.

I tried to use u16::cast_signed() as you had mentioned, but ran into some issues with clippy (see failing CI checks). If you have any recommendations for how to resolve those, I'm all ears, otherwise I can swap to as instead.

Let me know if you see anything else obvious that can be simplified or made better!

@NoahLutz NoahLutz changed the title feat(adc)!: differential sampling support feat(adc): differential sampling support Mar 16, 2026
@NoahLutz
Copy link
Copy Markdown
Author

NoahLutz commented Mar 23, 2026

Not sure why the xiao_m0 build suddenly started failing but looks to be unrelated? Interesting that it wasn't a problem for #992 (which was just merged earlier today).

@rnd-ash
Copy link
Copy Markdown
Contributor

rnd-ash commented Mar 23, 2026

Not sure why the xiao_m0 build suddenly started failing but looks to be unrelated? Interesting that it wasn't a problem for #992 (which was just merged earlier today).

This is OK, its something on the nightly rust compiler plus some dependency of a dependency, nothing to worry about

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants