Skip to content

Commit d42a301

Browse files
bikeNomaddpgeorge
authored andcommitted
zephyr/machine_adc: Add ADC support.
This commit adds support for ADC peripherals in the Zephyr port. As is typical for Zephyr, the ADC channel setup is done in the devicetree (typically using an overlay). This code requires ADC channels to be listed in the io-channels property of the zephyr,user root node. Signed-off-by: Ned Konz <[email protected]>
1 parent c9a16e8 commit d42a301

File tree

4 files changed

+247
-2
lines changed

4 files changed

+247
-2
lines changed

docs/zephyr/quickref.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,19 @@ Hardware SPI is accessed via the :ref:`machine.SPI <machine.SPI>` class::
119119
spi.write_readinto(b'abcd', buf) # write to MOSI and read from MISO into the buffer
120120
spi.write_readinto(buf, buf) # write buf to MOSI and read back into the buf
121121

122+
Analog to Digital Converter (ADC)
123+
----------------------------------
124+
125+
Use the :ref:`machine.ADC <machine.ADC>` class.
126+
127+
Example of using ADC to read a pin's analog value (the ``zephyr,user`` node must contain
128+
the ``io-channels`` property containing all the ADC channels)::
129+
130+
from machine import ADC
131+
132+
adc = ADC(("adc", 0))
133+
adc.read_uv()
134+
122135
Disk Access
123136
-----------
124137

ports/zephyr/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ Features supported at this time:
1919
* `machine.Pin` class for GPIO control, with IRQ support.
2020
* `machine.I2C` class for I2C control.
2121
* `machine.SPI` class for SPI control.
22-
* `machine.PWM` class for PWM control
22+
* `machine.PWM` class for PWM control.
23+
* `machine.ADC` class for ADC control.
2324
* `socket` module for networking (IPv4/IPv6).
2425
* "Frozen modules" support to allow to bundle Python modules together
2526
with firmware. Including complete applications, including with
@@ -119,7 +120,7 @@ To blink an LED:
119120
time.sleep(0.5)
120121

121122
The above code uses an LED location for a FRDM-K64F board (port B, pin 21;
122-
following Zephyr conventions port are identified by their devicetree node
123+
following Zephyr conventions ports are identified by their devicetree node
123124
label. You will need to adjust it for another board (using board's reference
124125
materials). To execute the above sample, copy it to clipboard, in MicroPython
125126
REPL enter "paste mode" using Ctrl+E, paste clipboard, press Ctrl+D to finish
@@ -153,6 +154,13 @@ Example of using SPI to write a buffer to the MOSI pin:
153154
spi.init(baudrate=500000, polarity=1, phase=1, bits=8, firstbit=SPI.MSB)
154155
spi.write(b'abcd')
155156

157+
Example of using ADC to read a pin's analog value (the 'zephyr,user' node must contain
158+
the 'io-channels' property with all the ADC channels):
159+
160+
from machine import ADC
161+
162+
adc = ADC(("adc", 0))
163+
adc.read_uv()
156164

157165
Minimal build
158166
-------------

ports/zephyr/machine_adc.c

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* This file is part of the MicroPython project, http://micropython.org/
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2025 NED KONZ <[email protected]>
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in
16+
* all copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*/
26+
27+
// This file is never compiled standalone, it's included directly from
28+
// extmod/machine_adc.c via MICROPY_PY_MACHINE_ADC_INCLUDEFILE.
29+
30+
/*
31+
* The ADC peripheral and pinmux is configured in the board's .dts file. Make
32+
* sure that the ADC is enabled (status = "okay";).
33+
*
34+
* In addition to that, this driver requires an ADC channel specified in the
35+
* io-channels property of the zephyr,user node. This is usually done with a
36+
* devicetree overlay.
37+
*
38+
* Configuration of channels (settings like gain, reference, or acquisition time)
39+
* also needs to be specified in devicetree, in ADC controller child nodes. Also
40+
* the ADC resolution and oversampling setting (if used) need to be specified
41+
* there.
42+
*
43+
* Here is an excerpt from a devicetree overlay that configures an ADC
44+
* with one channel that would be referred to as ('adc', 0) in the constructor
45+
* of the ADC object:
46+
*
47+
* / {
48+
* zephyr,user {
49+
* io-channels = <&adc 0>;
50+
* };
51+
* };
52+
*
53+
* &adc {
54+
* #address-cells = <1>;
55+
* #size-cells = <0>;
56+
*
57+
* channel@0 {
58+
* reg = <0>;
59+
* zephyr,gain = "ADC_GAIN_1_6";
60+
* zephyr,reference = "ADC_REF_INTERNAL";
61+
* zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
62+
* zephyr,input-positive = <NRF_SAADC_AIN0>;
63+
* zephyr,resolution = <12>;
64+
* };
65+
* // other channels here (1, 4, 5, 7)
66+
* };
67+
*
68+
*/
69+
70+
#include <inttypes.h>
71+
#include <stddef.h>
72+
#include <stdint.h>
73+
#include <zephyr/device.h>
74+
#include <zephyr/devicetree.h>
75+
#include <zephyr/kernel.h>
76+
#include <zephyr/sys/printk.h>
77+
#include <zephyr/sys/util.h>
78+
#include <zephyr/drivers/adc.h>
79+
#include "py/mphal.h"
80+
#include "py/mperrno.h"
81+
#include "zephyr_device.h"
82+
83+
static uint16_t sample_buffer;
84+
85+
static struct adc_sequence adc_sequence = {
86+
.options = NULL, // No options set by default
87+
.channels = 0,
88+
.buffer = &sample_buffer,
89+
.buffer_size = sizeof(sample_buffer), // bytes, not samples
90+
.resolution = 12, // Default resolution, can be changed later
91+
.oversampling = 0, // Default oversampling, can be changed later
92+
.calibrate = false, // Default to no calibration
93+
};
94+
95+
#define USER_NODE DT_PATH(zephyr_user)
96+
97+
#define DT_SPEC_AND_COMMA(node_id, prop, idx) \
98+
ADC_DT_SPEC_GET_BY_IDX(node_id, idx),
99+
100+
// Data of ADC io-channels specified in devicetree.
101+
static const struct adc_dt_spec adc_channels[] = {
102+
// We require that the user node has an io-channels property.
103+
#if DT_NODE_HAS_PROP(USER_NODE, io_channels)
104+
DT_FOREACH_PROP_ELEM(USER_NODE, io_channels,
105+
DT_SPEC_AND_COMMA)
106+
#endif
107+
};
108+
109+
#define NUM_CHANNELS ARRAY_SIZE(adc_channels)
110+
111+
typedef struct _machine_adc_obj_t {
112+
mp_obj_base_t base;
113+
char const *name; // name given by the user in the tuple
114+
const struct adc_dt_spec *spec; // ADC device channel specification
115+
} machine_adc_obj_t;
116+
117+
118+
static void mp_machine_adc_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
119+
machine_adc_obj_t *self = MP_OBJ_TO_PTR(self_in);
120+
121+
mp_printf(print, "ADC(%s.%d)", self->name, self->spec->channel_id);
122+
}
123+
124+
// constructor((adcblock, channel) | adc_obj, ...)
125+
static mp_obj_t mp_machine_adc_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
126+
mp_arg_check_num(n_args, n_kw, 1, MP_OBJ_FUN_ARGS_MAX, true);
127+
128+
machine_adc_obj_t *self = NULL;
129+
if (mp_obj_is_type(args[0], &machine_adc_type)) {
130+
// Already an ADC object, just return it
131+
self = MP_OBJ_TO_PTR(args[0]);
132+
} else if (mp_obj_is_type(args[0], &mp_type_tuple)) {
133+
// Get the wanted (adcblock, channel) values.
134+
mp_obj_t *items;
135+
mp_obj_get_array_fixed_n(args[0], 2, &items);
136+
const char *dev_name = mp_obj_str_get_str(items[0]);
137+
int channel_id = mp_obj_get_int(items[1]);
138+
const struct device *wanted_port = zephyr_device_find(items[0]);
139+
140+
// Find the desired channel by device name and channel ID
141+
struct adc_dt_spec const *wanted_adc_channel = NULL;
142+
143+
for (size_t i = 0U; i < ARRAY_SIZE(adc_channels); i++) {
144+
if (adc_channels[i].dev == wanted_port &&
145+
adc_channels[i].channel_id == channel_id) {
146+
wanted_adc_channel = adc_channels + i;
147+
break;
148+
}
149+
}
150+
151+
if (!wanted_adc_channel) {
152+
mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("ADC channel (%s, %d) not found"), dev_name, channel_id);
153+
}
154+
155+
if (!adc_is_ready_dt(wanted_adc_channel)) {
156+
mp_raise_OSError(MP_EIO);
157+
}
158+
159+
int err = adc_channel_setup_dt(wanted_adc_channel);
160+
if (err < 0) {
161+
mp_raise_OSError(MP_EINVAL);
162+
}
163+
164+
self = mp_obj_malloc(machine_adc_obj_t, &machine_adc_type);
165+
self->spec = wanted_adc_channel;
166+
self->name = dev_name;
167+
} else {
168+
// Unknown type, raise a ValueError
169+
mp_raise_ValueError(MP_ERROR_TEXT("ADC must be initialized with a tuple of (adcblock, channel) or an ADC object"));
170+
}
171+
172+
return MP_OBJ_FROM_PTR(self);
173+
}
174+
175+
static mp_uint_t zephyr_adc_read(const struct adc_dt_spec *spec) {
176+
int err = adc_sequence_init_dt(spec, &adc_sequence);
177+
if (err < 0) {
178+
mp_raise_OSError(MP_EOPNOTSUPP);
179+
}
180+
181+
err = adc_read_dt(spec, &adc_sequence);
182+
if (err < 0) {
183+
mp_raise_OSError(MP_EIO);
184+
}
185+
186+
return (mp_uint_t)sample_buffer; // Return the read value as a 16-bit integer
187+
}
188+
189+
static mp_int_t mp_machine_adc_read_u16(machine_adc_obj_t *self) {
190+
mp_uint_t raw = zephyr_adc_read(self->spec);
191+
// Scale raw reading to 16 bit value using a Taylor expansion (for 8 <= bits <= 16)
192+
mp_int_t bits = self->spec->resolution;
193+
mp_uint_t u16 = raw << (16 - bits) | raw >> (2 * bits - 16);
194+
return u16;
195+
}
196+
197+
198+
#if MICROPY_PY_MACHINE_ADC_READ_UV
199+
static mp_int_t mp_machine_adc_read_uv(machine_adc_obj_t *self) {
200+
int32_t raw = (int32_t)zephyr_adc_read(self->spec);
201+
int err = adc_raw_to_millivolts_dt(self->spec, &raw);
202+
if (err < 0) {
203+
mp_raise_OSError(MP_EOPNOTSUPP);
204+
}
205+
return (mp_int_t)raw * 1000UL; // Convert to microvolts
206+
}
207+
#endif
208+
209+
210+
#if MICROPY_PY_MACHINE_ADC_READ
211+
static mp_int_t mp_machine_adc_read(machine_adc_obj_t *self) {
212+
mp_uint_t raw = zephyr_adc_read(self->spec);
213+
return (mp_int_t)raw;
214+
}
215+
#endif
216+
217+
#define MICROPY_PY_MACHINE_ADC_CLASS_CONSTANTS

ports/zephyr/mpconfigport.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ void mp_hal_signal_event(void);
143143
#define MICROPY_HW_MCU_NAME "unknown-cpu"
144144
#endif
145145

146+
#ifdef CONFIG_ADC
147+
#define MICROPY_PY_MACHINE_ADC (1)
148+
#define MICROPY_PY_MACHINE_ADC_INCLUDEFILE "ports/zephyr/machine_adc.c"
149+
#define MICROPY_PY_MACHINE_ADC_READ (1)
150+
#define MICROPY_PY_MACHINE_ADC_READ_UV (1)
151+
#endif
152+
146153
typedef intptr_t mp_int_t; // must be pointer size
147154
typedef uintptr_t mp_uint_t; // must be pointer size
148155
typedef long mp_off_t;

0 commit comments

Comments
 (0)