Analog Input/Output Examples - Raspberry Pi Pico

The following short Python programs will demonstrate essential operation of the Raspberry Pi Pico board. These assume one or more analog input circuits are externally attached to an ADC input. Each can be run by copying the program into code.py on the CIRCUITPY drive offered by the board. The text can be pasted directly from this page, or each file can be downloaded from the CircuitPython sample code folder on this site.

Related Pages

Analog Input

Demonstration of reading a single analog input channel and applying thresholding with hysteresis. For sample circuits, please see Exercise: Analog Sensing with the Pico.

Direct download: analog_input.py.

 1# analog_input.py
 2
 3# Raspberry Pi Pico - Analog Input demo
 4
 5# Read an analog input with the ADC, drive the onboard LED, and print messages
 6# to the serial console only when the input state changes.
 7
 8import board
 9import time
10import analogio
11import digitalio
12
13#---------------------------------------------------------------
14# Set up the hardware: script equivalent to Arduino setup()
15
16# Set up built-in green LED for output.
17led = digitalio.DigitalInOut(board.LED)  # GP25
18led.direction = digitalio.Direction.OUTPUT
19
20# Set up an analog input on ADC0 (GP26), which is physically pin 31.
21# E.g., this may be attached to photocell or photointerrupter with associated pullup resistor.
22sensor = analogio.AnalogIn(board.A0)
23
24# These may be need to be adjusted for your particular hardware.  The Pico has
25# 12-bit analog-to-digital conversion so the actual conversion has 4096 possible
26# values, but the results are scaled to a 16-bit unsigned integer with range
27# from 0 to 65535.
28lower_threshold =  8000
29upper_threshold = 45000
30
31#---------------------------------------------------------------
32# Run the main loop: script equivalent to Arduino loop()
33
34# The following logic detects input transitions using a state machine with two
35# possible states.  The state index reflects the current estimated state of the
36# input.  The transition rules apply hysteresis in the form of assymmetric
37# thresholds to reduce switching noise: the sensor value must rise higher than
38# the upper threshold or lower than the lower threshold to trigger a state
39# change.
40state_index = False
41
42while True:
43
44    # Read the sensor once per cycle.
45    sensor_level = sensor.value
46
47    # uncomment the following to print tuples to plot with the mu editor
48    # print((sensor_level,))
49    # time.sleep(0.02)  # slow sampling to avoid flooding
50    
51    if state_index is False:
52        if sensor_level < lower_threshold:
53            led.value = True
54            state_index = True
55            print("On")
56
57    elif state_index is True:
58        if sensor_level > upper_threshold:
59            led.value = False
60            state_index = False
61            print("Off")
62
63    

Analog Input Speed Test

Direct download: ain_speed_test.py.

 1# ain_speed_test.py
 2
 3# Raspberry Pi Pico - Analog Input Speed Test
 4
 5# Read an analog input with the ADC and measure the number of conversions
 6# possible per second from CircuitPython.  The underlying hardware
 7# on the RP2040 chip is rated up to 500 kS/sec.
 8
 9# Result: actual single-channel conversion rates run about 63 kS/sec.
10# As a control case, an empty loop runs at around 192 kHz.
11
12import board
13import time
14import analogio
15
16#---------------------------------------------------------------
17# Set up an analog input on ADC0 (GP26), which is physically pin 31.
18# E.g., this may be attached to photocell or photointerrupter with associated pullup resistor.
19sensor = analogio.AnalogIn(board.A0)
20
21#---------------------------------------------------------------
22# Run the main loop: script equivalent to Arduino loop()
23
24while True:
25
26    # Read a single ADC channel many times in a tight loop.
27    num_samples = 100000    
28    start = time.monotonic_ns()
29    for i in range(num_samples):
30        sensor_level = sensor.value
31    end = time.monotonic_ns()
32    elapsed = 1e-9 * (end - start)
33    rate = num_samples / elapsed
34    print(f"Read {num_samples} samples in {elapsed} seconds, {rate} samples/sec.")
35
36    # Pause briefly.
37    time.sleep(1)
38    
39    # Control case: null loop
40    start = time.monotonic_ns()
41    for i in range(num_samples):
42        pass
43    end = time.monotonic_ns()
44    elapsed = 1e-9 * (end - start)
45    rate = num_samples / elapsed    
46    print(f"Performed empty loop {num_samples} times in {elapsed} seconds, {rate} loops/sec.")
47
48    # Pause briefly.
49    time.sleep(1)

Gesture Game

Filtering of analog signals typically assumes a constant sampling rate. However, not every activity in the system needs to run at this rate, so in practice this usually means that the signal processing activity is interleaved with other computation.

The following example highlights a strategy for interleaving multiple activities running at different rates. The main event loop of the system is kept cycling quickly and polling other objects to allow them to update their state at independent rates. The accelerometer signal processing runs at 100 Hz, the LED blink logic runs at a variable rate but typically around 1-2 Hz, and the main game logic runs at only 2Hz. The game logic implements a state machine which responds to a timed series of tilts of the accelerometer.

../_images/Pico-ADXL335-example.png

A sample circuit for connecting a three-channel analog accelerometer, useful for measuring tilt and motion.

Direct downloads:

  • gestures.py (to be copied into code.py on CIRCUITPY)

  • biquad.py (to be copied to CIRCUITPY without changing name)

  • linear.py (to be copied to CIRCUITPY without changing name)

  1# gestures.py
  2
  3# Raspberry Pi Pico - Accelerometer Processing Demo
  4
  5# Read an analog accelerometer input with the ADC and detect patterns of motion.
  6# Drive the onboard LED and print messages to indicate state.
  7# Assumes a three-axis analog accelerometer is connected to the three ADC inputs.
  8
  9import board
 10import time
 11import analogio
 12import digitalio
 13
 14# Import filters from the 'signals' directory provided with the course.  These
 15# files should be copied to the top-level directory of the CIRCUITPY filesystem
 16# on the Pico.
 17
 18import linear
 19import biquad
 20
 21#---------------------------------------------------------------
 22
 23# Coefficients for Low-Pass Butterworth IIR digital filter, generated using
 24# filter_gen.py.  Sampling rate: 100.0 Hz, frequency: 20.0 Hz.  Filter is order
 25# 4, implemented as second-order sections (biquads). Reference:
 26# https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.butter.html
 27low_pass_100_20 = [
 28    [[ 1.00000000, -0.32897568,  0.06458765],   # A coeff for section 0
 29     [ 0.04658291,  0.09316581,  0.04658291]],  # B coeff for section 0
 30    [[ 1.00000000, -0.45311952,  0.46632557],   # A coeff for section 1
 31     [ 1.00000000,  2.00000000,  1.00000000]],  # B coeff for section 1
 32]
 33
 34#---------------------------------------------------------------
 35class Accelerometer:
 36    def __init__(self):
 37        """Interface to a three-axis analog accelerometer including input calibration
 38        and filtering."""
 39
 40        self.x_in = analogio.AnalogIn(board.A0)
 41        self.y_in = analogio.AnalogIn(board.A1)
 42        self.z_in = analogio.AnalogIn(board.A2)
 43        self.sampling_interval = 10000000           # period of 100 Hz in nanoseconds
 44        self.sampling_timer = 0
 45        self.raw  = [0, 0, 0]           # most recent raw value
 46        self.grav = [0.0, 0.0, 0.0]     # current calibrated and smoothed value
 47
 48        # create a set of filters to smooth the input signal
 49        self.filters = [biquad.BiquadFilter(low_pass_100_20) for i in range(3)]
 50        
 51    def poll(self, elapsed):
 52        """Polling function to be called as frequently as possible from the event loop
 53        to read and process new samples.
 54
 55        :param elapsed: nanoseconds elapsed since the last cycle.
 56        """
 57
 58        self.sampling_timer -= elapsed
 59        if self.sampling_timer < 0:
 60            self.sampling_timer += self.sampling_interval
 61
 62            # read the ADC values as synchronously as possible
 63            self.raw = [self.x_in.value, self.y_in.value, self.z_in.value]
 64
 65            # apply linear calibration to find the unit gravity vector direction
 66            self.grav = [linear.map(self.raw[0], 26240, 39120, -1.0, 1.0),
 67                         linear.map(self.raw[1], 26288, 39360, -1.0, 1.0),
 68                         linear.map(self.raw[2], 26800, 40128, -1.0, 1.0)]
 69
 70            # pipe each calibrated value through the filter
 71            self.grav = [filt.update(sample) for filt, sample in zip(self.filters, self.grav)]
 72            
 73#---------------------------------------------------------------
 74class Blinker:
 75    def __init__(self):
 76        """Interface to the onboard LED featuring variable rate blinking."""
 77        # Set up built-in green LED for output.
 78        self.led = digitalio.DigitalInOut(board.LED)  # GP25
 79        self.led.direction = digitalio.Direction.OUTPUT
 80        self.update_timer = 0
 81        self.set_rate(1.0)
 82            
 83    def poll(self, elapsed):
 84        """Polling function to be called as frequently as possible from the event loop
 85        with the nanoseconds elapsed since the last cycle."""
 86        self.update_timer -= elapsed
 87        if self.update_timer < 0:
 88            self.update_timer += self.update_interval
 89            self.led.value = not self.led.value
 90
 91    def set_rate(self, Hz):
 92        self.update_interval = int(500000000 / Hz) # blink half-period in nanoseconds
 93        
 94#---------------------------------------------------------------
 95class Logic:
 96    def __init__(self, accel, blinker):
 97        """Application state machine."""
 98        self.accel   = accel
 99        self.blinker = blinker
100        self.update_interval = 500000000           # period of 2 Hz in nanoseconds
101        self.update_timer    = 0
102
103        # initialize the state machine
104        self.state = 'init'
105        self.state_changed = False
106        self.state_timer = 0
107
108    def poll(self, elapsed):
109        """Polling function to be called as frequently as possible from the event loop
110        with the nanoseconds elapsed since the last cycle."""
111        self.update_timer -= elapsed
112        if self.update_timer < 0:
113            self.update_timer += self.update_interval
114            self.tick()            # evaluate the state machine
115
116    def transition(self, new_state):
117        """Set the state machine to enter a new state on the next tick."""
118        self.state         = new_state
119        self.state_changed = True
120        self.state_timer   = 0
121        print("Entering state:", new_state)
122
123    def tick(self):
124        """Evaluate the state machine rules."""
125
126        # advance elapsed time in seconds        
127        self.state_timer += 1e-9 * self.update_interval  
128        
129        # set up a flag to process transitions within a state clause
130        entering_state = self.state_changed
131        self.state_changed = False
132        
133        # select the state clause to evaluate
134        if self.state == 'init':
135            self.transition('idle')
136
137        elif self.state == 'idle':
138            if entering_state:
139                self.blinker.set_rate(0.5)
140
141            if self.accel.grav[1] < -0.5:
142                self.transition('right')
143
144        elif self.state == 'right':
145            if entering_state:
146                self.blinker.set_rate(1.0)
147
148            if self.state_timer > 2.0:
149                self.transition('idle')
150
151            if self.accel.grav[1] > 0.5:
152                self.transition('left')
153
154        elif self.state == 'left':
155            if entering_state:
156                self.blinker.set_rate(2.0)
157
158            if self.state_timer > 2.0:
159                self.transition('idle')
160
161            if self.accel.grav[0] < -0.5:
162                self.transition('win')
163                
164        elif self.state == 'win':
165            if entering_state:
166                self.blinker.set_rate(4.0)
167
168            if self.state_timer > 2.0:
169                self.transition('idle')
170                
171#---------------------------------------------------------------
172class Status:
173    def __init__(self, accel, blinker, logic):
174        """Debugging information reporter."""
175        self.accel    = accel
176        self.blinker  = blinker
177        self.logic    = logic
178        # self.update_interval = 1000000000           # period of 1 Hz in nanoseconds
179        self.update_interval = 100000000           # period of 10 Hz in nanoseconds        
180        self.update_timer = 0
181
182    def poll(self, elapsed):
183        """Polling function to be called as frequently as possible from the event loop
184        with the nanoseconds elapsed since the last cycle."""
185        self.update_timer -= elapsed
186        if self.update_timer < 0:
187            self.update_timer += self.update_interval
188            print(tuple(self.accel.grav))
189            # print(tuple(self.accel.raw))
190                    
191#---------------------------------------------------------------
192# Initialize global objects.
193accel   = Accelerometer()
194blinker = Blinker()
195logic   = Logic(accel, blinker)
196status  = Status(accel, blinker, logic)
197
198#---------------------------------------------------------------
199# Main event loop to run each non-preemptive thread.
200
201last_clock = time.monotonic_ns()
202
203while True:
204    # read the current nanosecond clock
205    now = time.monotonic_ns()
206    elapsed = now - last_clock
207    last_clock = now
208
209    # poll each thread
210    accel.poll(elapsed)
211    logic.poll(elapsed)
212    blinker.poll(elapsed)
213    status.poll(elapsed)