Skip to content

samd/adc_dac: Implememt adc.read_timed() and dac.write_timed().#9624

Merged
dpgeorge merged 13 commits into
micropython:masterfrom
robert-hh:samd_adc_dac
May 5, 2026
Merged

samd/adc_dac: Implememt adc.read_timed() and dac.write_timed().#9624
dpgeorge merged 13 commits into
micropython:masterfrom
robert-hh:samd_adc_dac

Conversation

@robert-hh
Copy link
Copy Markdown
Contributor

@robert-hh robert-hh commented Oct 14, 2022

@dpgeorge As requested, this is the separate PR for ADC-timed and DAC-timed. Below
is a short documentation.

ADC Constructor:

ADC(Pin, *, average=16, bits=12, vref=3, callback=None)

The callback keyword option is used for timed ADC sampling. The callback is executed when all data has been sampled.

ADC Methods:

adc.read_u16()

Read single value from the Analog Pin using the bits and average setting from the constructors.

adc.read_timed(data, freq)

Read adc values into the data buffer at a supplied frequency. The buffer must be pre-allocated. Values are stored as 16 bit quantities in the binary range given by the bits option. If bits=12, the value range is 0-4095.
The voltage range is defined by the vref option.
If in the constructor a callback was defined, it will be called after all data has been read. Alternatively, the method busy() can be used to tell, if the capture has finished.

adc.busy()

busy() returns True while the data acquisition using read_timed() is ongoing, False otherwise.

adc.deinit()

Deinitialize as ADC object and release the resources used by it, especially the ADC channel and the timer used for read_timed().

DAC Constructor

DAC(id, *, vref=3, callback=None)

The resolution of the DAC is 12 bit for SAMD51 and 10 bit for SAMD21. SAMD21 devices have 1 DAC channel at GPIO PA02, accepting only 0 as id. SAMD51 devices have 2 DAC channels at GPIO PA02 and PA05 with values 0 and 1 for the id.

DAC Methods

dac.write(value)

Write a single value to the selected DAC output. The value range is 0-1023 for SAMD21 and 0-4095 for SAMD51. The voltage range depends on the vref setting.

dac.write_timed(data, freq [, count=1])

The call to dac_timed() allows to output a series of analogue values at a given rate. data must be a buffer with 16 bit values in the range of the DAC (10 bit of 12 bit). freq may have a range of 1Hz to ~200kHz for SAMD21 and 1 Hz to ~500kHz for SAMD51. The optional argument count specifies,
how often data output will be repeated. The range is 1 - 2**32. If count == 0, the data output will be repeated until stopped by a call to deinit(). If the data has been output count times, a callback will
be called, if given.

dac.busy()

Tells, whether a dac.write_timed() activity is ongoing. It returns True if yes, False otherwise.

dac.deinit()

Deinitialize the DAC and release the resources used by it, especially the DMA channel and the Timer. On most SAMD21 boards, there is just one timer available for dac.write_timed() and adc.read_timed_into(). So they cannot run both at the same time, and releasing the timer may be important. The DAC driver consumes a substantial amount of current. Calling deinit()
will reduce that as well.

@robert-hh robert-hh force-pushed the samd_adc_dac branch 2 times, most recently from ffb6c55 to 96495ce Compare October 26, 2022 18:44
@robert-hh robert-hh force-pushed the samd_adc_dac branch 2 times, most recently from 7a4b1f4 to 5ea4230 Compare November 11, 2022 07:09
@robert-hh robert-hh force-pushed the samd_adc_dac branch 4 times, most recently from cac0bfb to c57be77 Compare December 15, 2022 07:01
@robert-hh robert-hh force-pushed the samd_adc_dac branch 3 times, most recently from 1cf0992 to b829186 Compare May 22, 2023 13:34
@robert-hh
Copy link
Copy Markdown
Contributor Author

Just a conflict resolved after rebase.

@robert-hh
Copy link
Copy Markdown
Contributor Author

@dpgeorge You mentioned about this PR that you would have to consider a proper API for that ADC/DAC timed feature. I guess you did not have time and it got lost under all other requests you have. The actual PR's API is close to that of the STM32 port.

@robert-hh
Copy link
Copy Markdown
Contributor Author

Re-based and Re-tested. Still fine.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Oct 24, 2023

Code size report:

Reference:  docs/develop/porting: Update session log for example port. [9f396bb]
Comparison: samd/machine_adc: Fix the configuration with averaging enabled. [merge of 21b3a51]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd: +2504 +0.911% ADAFRUIT_ITSYBITSY_M4_EXPRESS[incl +36(data) +284(bss)]
  qemu rv32:    +0 +0.000% VIRT_RV32

@codecov
Copy link
Copy Markdown

codecov Bot commented Oct 24, 2023

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.46%. Comparing base (9f396bb) to head (21b3a51).
⚠️ Report is 13 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9624   +/-   ##
=======================================
  Coverage   98.46%   98.46%           
=======================================
  Files         176      176           
  Lines       22811    22811           
=======================================
  Hits        22460    22460           
  Misses        351      351           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@robert-hh robert-hh force-pushed the samd_adc_dac branch 2 times, most recently from 1cf57d7 to d59e450 Compare October 24, 2023 09:41
@robert-hh robert-hh force-pushed the samd_adc_dac branch 3 times, most recently from 4070f1d to 3fea431 Compare November 6, 2023 14:19
@robert-hh
Copy link
Copy Markdown
Contributor Author

Looking at the datasheet it says TC3 is paired with TC4, and that seems to be how init_us_counter() is implemented, but the comment for that function is wrong (says TC4/TC5).

Actually TC4 and TC5 are combined. The datasheet seems to be wrong. Otherwise, TC3 could not be used.

Since SysTick is enabled and running it already produces a millisecond counter, and can also use that to get a microsecond counter like stm32 does it.

But with a 1000µs resolution.

@dpgeorge
Copy link
Copy Markdown
Member

But with a 1000µs resolution.

No, stm32's mp_hal_ticks_us() has 1us resolution. That's because SysTick->VAL counts at the CPU frequency. So it could potentially have resolution much better than 1us.

@robert-hh robert-hh force-pushed the samd_adc_dac branch 3 times, most recently from 2e2d257 to d31eaff Compare April 21, 2026 19:18
@dpgeorge
Copy link
Copy Markdown
Member

dpgeorge commented May 1, 2026

I tested this again on ADAFRUIT_ITSYBITSY_M0_EXPRESS, using ADC and DAC separately.

I used DAC.write_timed(buf, n, 0) to generate a continuous sine wave, measured using a scope, and it worked really well!

Testing ADC.read_timed() also looks good.

I think this is very close to being merged now. @robert-hh is there anything more you want to do? Some of the commits need to be squashed. Feel free to do that yourself, or I can do it during merge.

Comment thread docs/samd/quickref.rst Outdated
Comment thread extmod/machine_adc.c Outdated
Comment thread extmod/machine_adc.c Outdated
Comment thread ports/samd/mcu/samd21/mpconfigmcu.h Outdated
@robert-hh
Copy link
Copy Markdown
Contributor Author

@dpgeorge Thank you for the review. I incorporated the suggested changes. Reducing the number of commits seems difficult. I dropped two, and some could be combined, like two that add the callback option.

@dpgeorge
Copy link
Copy Markdown
Member

dpgeorge commented May 1, 2026

Reducing the number of commits seems difficult.

OK, we can leave them mostly as they are.

The main thing I saw was the change to extmod/machine_adc.c was combined with other changes. Ideally the additions to extmod/machine_adc.c would be in their own commit.

@robert-hh
Copy link
Copy Markdown
Contributor Author

The main thing I saw was the change to extmod/machine_adc.c was combined with other changes.

That is commit 1219b6ee6 2023-10-24 samd/machine_adc: Factor out machine.adc_timed()., which by it's nature affects more than one file.

robert-hh added 13 commits May 5, 2026 14:49
Used for allocation of DMA channels. It will be needed for planned
modules and methods like adc_timed(), dac_timed(), I2S.

It includes management code for DMA IRQ handlers, similar to what
was made for Sercom.

Signed-off-by: robert-hh <[email protected]>
These functions are use to allocate, free and configure a set
of TC counter instances. The SAMxx MCU have between 3 to 5 (SAMD21) and
4 to 8 (SAMD51) TC instances. Two of them are used for the µs counter,
the remaining 1 - 6 instances are administered here for use by
various functions, like timed DMA transfers.

Signed-off-by: robert-hh <[email protected]>
Used as:

    dac.write_timed(data, freq [, count])
    dac.deinit()

Working range for dac_timed():

    SAMD21: 1 Hz - 100 kHz (1 MHz clock, 10 bit)
    SAMD51: 1 Hz - ~500 kHz (8 MHz clock, 12 bit)

The buffer has to be a byte array or a halfword array,
and the data is sent once.

The default for count is 1. If set to a value > 0, the data will be
transmitted count times. If set to 0 or  < 0, the date will be
transmitted until deliberately stopped. The playback
can be stopped with dac.deinit().

dac.deinit() just releases the timer and DMA channel needed by
dac_timed(). The DAC object itself does not have to be released.

Signed-off-by: robert-hh <[email protected]>
Used as:

    adc.read_timed(buffer, freq)

Buffer must be preallocated. The size determines the number of 16 bit
words to be read. The numeric range of the results is that of the raw
ADC. The call returns immediately, and the data transfer is done by DMA.
The caller must wait sufficiently long until the data is sampled
and can be noticed by a callback. No internal checks are made for
a too-high freq value.

Read speeds depends on Average and bit length setting:

    SAMD21: Max. 350kS/s (8 bit, Average 1)
    SAMD51: Max. 1 MS/s (8 bit, Average 1)

Signed-off-by: robert-hh <[email protected]>
The callback is called when a dac_timed() sequence finishes. It will be
reset with callback=None or omitting the callback option in the
constructor.

Side change: Set the clock freq. to 48Mhz.

Signed-off-by: robert-hh <[email protected]>
Enabling a callback that will be called when a adc.read_timed_into() run
is finished. That's especially useful with slow sampling rates and/or
many samples, avoiding to guess the sampling time.
Raise an error is adc.read_u16() is called while a read_timed_into()
is active.

Other ADC changes:
- SAMD51: use ADC1 if both ADC1 and ADC0 are available at a Pin.

Signed-off-by: robert-hh <[email protected]>
These return True, while a timed action is ongoing.

Side change:
Reorder some code in machine_dac.c and do not reset DAC twice.

Signed-off-by: robert-hh <[email protected]>
Since the two channels of a SAMD51 are not completely independent,
dac.deinit() now clears both channels, and both channels have to
be re-instantiated after a deinit().

Side change:
- rearrange some code lines.

Signed-off-by: robert-hh <[email protected]>
Both together require ~1.9k of flash space, including the DMA-manager
and the TC-manager. adc.read_timed() uses ~700 bytes, dac.write_timed()
~600 bytes.

Signed-off-by: robert-hh <[email protected]>
Fixes:
- Leave no half-initialized device if init fails.
- Fix dac_deinit_channel(). Perform deinit only for channels that
  had been initilized.

Signed-off-by: robert-hh <[email protected]>
After machine.ADC has been moved to extmod/machine_adc.c.
Adding adc.read_timed() and adc.busy() to extmod/machine_adc.c with
a corresponding flag to enable them.
ADC/DAC timed are by default enabled only at all SAMD51 devices and
at SAMD21 devices with an external flash for the file system.

Add class constants for the reference voltage source.
As far as possible the STM32 names are used, except where they should
match common board silkscreen labels.

Signed-off-by: robert-hh <[email protected]>
When averaging is selected, the resolution is fixed to 12 bit.
The configuration has to be changed to cater for the result shifts.

Side change: Remove a duplicated code line in init().

Signed-off-by: robert-hh <[email protected]>
Copy link
Copy Markdown
Member

@dpgeorge dpgeorge left a comment

Choose a reason for hiding this comment

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

OK, this is good to go!

@dpgeorge dpgeorge merged commit 21b3a51 into micropython:master May 5, 2026
70 of 71 checks passed
@dpgeorge
Copy link
Copy Markdown
Member

dpgeorge commented May 5, 2026

Thanks @robert-hh for keeping this PR alive for so long, and updating it to latest master.

@robert-hh
Copy link
Copy Markdown
Contributor Author

Thank you very much for merging.

@robert-hh robert-hh deleted the samd_adc_dac branch May 5, 2026 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants