Project 6.2: "Tilt Control (Accelerometer)"
Â
What youâll learn
- â Read tilt data: Get X, Y, Z acceleration from the ADXL345 (GYâ291) over I2C.
- â Map tilt to motion: Turn âdevice tiltâ into clear commands like FWD/BACK/LEFT/RIGHT.
- â Control by tilt: Use thresholds and deadâzones to avoid jittery control.
- â Gesture commands: Detect simple gestures (shake, nod, double tilt) to trigger actions.
- â Calibration and filtering: Apply offsets and smoothing to make data reliable.
Blocks glossary (used in this project)
- I2C init (Pins 22/21): Opens the I2C bus to talk to the ADXL345.
- Register read/write: Configures sensor power and range; reads axis registers.
- Variables: Store thresholds, offsets, and moving averages.
- If / else: Converts tilt ranges into commands.
- Serial println: Prints âAXIS:X,Y,Zâ, âCMD:FWDâ, âGESTURE:SHAKEâ, and âCAL:OKâ.
What you need
| Part | How many? | Pin connection |
|---|---|---|
| D1 R32 | 1 | USB cable (30 cm) |
| GYâ291 ADXL345 | 1 | I2C: SCL â Pin 22, SDA â Pin 21, VCC, GND |
| Jumper wires | 4â6 | Keep short and secure |
Notes
- Default ADXL345 I2C address is 0x53.
- Share ground between the R32 and the sensor. Keep wires short to reduce noise.
Before you start
- USB cable is plugged in
- Serial monitor is open
- Test print shows:
print("Ready!") # Confirm serial is working so you can see messages
Microprojects 1â5
Microproject 6.2.1 â Reading X, Y, Z axes
Goal: Initialize ADXL345 and print signed X/Y/Z acceleration (raw counts) continuously.
Blocks used:
- I2C init: Pins 22 (SCL), 21 (SDA).
- Register write: POWER_CTL (0x2D) to start measurement; DATA_FORMAT (0x31) to ±2g.
- Register read: DATAX0..DATAZ1 (0x32â0x37) for axes.
MicroPython code:
import machine # Import machine to access I2C and pins
import time # Import time to add small delays between reads
# ADXL345 constants
ADXL_ADDR = 0x53 # I2C address for ADXL345 (default 0x53)
REG_POWER_CTL = 0x2D # Register to control measurement mode
REG_DATA_FORMAT = 0x31 # Register to set data range and format
REG_DATAX0 = 0x32 # First axis data register (X low byte)
# I2C setup
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000) # Create I2C bus on pins 22/21
# Sensor init: measurement ON, ±2g range
i2c.writeto_mem(ADXL_ADDR, REG_POWER_CTL, b'\x08') # Set MEASURE bit (start measurements)
i2c.writeto_mem(ADXL_ADDR, REG_DATA_FORMAT, b'\x00') # Set ±2g, right-justified, 10-bit default
print("Microproject 6.2.1: ADXL345 ready at 0x53") # Confirm sensor initialization
def read_xyz(): # Define a helper to read all axes at once
buf = i2c.readfrom_mem(ADXL_ADDR, REG_DATAX0, 6) # Read 6 bytes: X0,X1,Y0,Y1,Z0,Z1
x = int.from_bytes(buf[0:2], 'little', signed=True) # Convert X bytes to signed int
y = int.from_bytes(buf[2:4], 'little', signed=True) # Convert Y bytes to signed int
z = int.from_bytes(buf[4:6], 'little', signed=True) # Convert Z bytes to signed int
return x, y, z # Return tuple of axes
while True: # Continuous reading loop
x, y, z = read_xyz() # Read axes from sensor
print("AXIS:", x, y, z) # Print raw axis values (counts)
time.sleep(0.1) # Short delay to keep output readable
Reflection: Raw axis values show how the board is tilted or restingâZ is strong when lying flat.
Challenge:
- Easy: Change the loop delay to 0.05 s for faster reads.
- Harder: Convert counts to g by dividing by 256 (approx for ±2g mode) and print âgâ values.
Microproject 6.2.2 â Tiltâtoâmotion mapping
Goal: Map tilt to motion commands with deadâzones to reduce jitter.
Blocks used:
- Variables: tilt_threshold and dead_zone.
- If / else: Print CMD:FWD/BACK/LEFT/RIGHT based on X/Y.
MicroPython code:
import machine # Import machine for I2C access
import time # Import time for loop delays
# Reuse constants and I2C from previous microproject
ADXL_ADDR = 0x53 # I2C address
REG_POWER_CTL = 0x2D # Power control register
REG_DATA_FORMAT = 0x31 # Data format register
REG_DATAX0 = 0x32 # Axis data start register
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000) # Create I2C bus
i2c.writeto_mem(ADXL_ADDR, REG_POWER_CTL, b'\x08') # Enable measurement
i2c.writeto_mem(ADXL_ADDR, REG_DATA_FORMAT, b'\x00') # Set ±2g range
def read_xyz(): # Helper to read all axes
buf = i2c.readfrom_mem(ADXL_ADDR, REG_DATAX0, 6) # Read 6 bytes for X,Y,Z
x = int.from_bytes(buf[0:2], 'little', signed=True) # Convert X to signed
y = int.from_bytes(buf[2:4], 'little', signed=True) # Convert Y to signed
z = int.from_bytes(buf[4:6], 'little', signed=True) # Convert Z to signed
return x, y, z # Return values
tilt_threshold = 150 # Minimum absolute tilt count to trigger a command
dead_zone = 80 # Range near zero where we print STOP to avoid jitter
print("Microproject 6.2.2: Thresholds -> tilt:", tilt_threshold, "dead:", dead_zone) # Show thresholds
while True: # Continuous mapping loop
x, y, z = read_xyz() # Read axes
print("AXIS:", x, y, z) # Print axes for visibility
if abs(x) < dead_zone and abs(y) < dead_zone: # If inside dead-zone
print("CMD:STOP") # Print stop to avoid jitter
else: # Outside dead-zone
if y > tilt_threshold: # Tilt âforwardâ (positive Y)
print("CMD:FWD") # Forward command
elif y < -tilt_threshold: # Tilt âbackwardâ (negative Y)
print("CMD:BACK") # Backward command
elif x > tilt_threshold: # Tilt ârightâ (positive X)
print("CMD:RIGHT") # Right command
elif x < -tilt_threshold: # Tilt âleftâ (negative X)
print("CMD:LEFT") # Left command
else: # Between thresholds
print("CMD:STOP") # Default stop if no strong tilt
time.sleep(0.1) # Short delay for readability
Reflection: Deadâzones make control smoothâcommands change only when tilt is clear.
Challenge:
- Easy: Print âCMD:DIAG_FWD_LEFT/RIGHTâ when both X and Y exceed the threshold.
- Harder: Create a âspeedâ level from tilt magnitude and print âSPEED:LOW/MID/HIGHâ.
Microproject 6.2.3 â Robot control by tilt (safe command stream)
Goal: Emit a clean stream of commands: only print when the command changes to reduce spam.
Blocks used:
- Variables: last_cmd remembers the last printed command.
- If / else: Only print when current != last.
MicroPython code:
import machine # Import machine for I2C hardware
import time # Import time for loop pacing
# Reuse constants and I2C
ADXL_ADDR = 0x53 # Sensor address
REG_POWER_CTL = 0x2D # Power control register
REG_DATA_FORMAT = 0x31 # Data format register
REG_DATAX0 = 0x32 # Axis data register start
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000) # Create I2C bus
i2c.writeto_mem(ADXL_ADDR, REG_POWER_CTL, b'\x08') # Enable measurement mode
i2c.writeto_mem(ADXL_ADDR, REG_DATA_FORMAT, b'\x00') # ±2g range
def read_xyz(): # Helper to read axes
buf = i2c.readfrom_mem(ADXL_ADDR, REG_DATAX0, 6) # Read 6 bytes
x = int.from_bytes(buf[0:2], 'little', signed=True) # X signed
y = int.from_bytes(buf[2:4], 'little', signed=True) # Y signed
z = int.from_bytes(buf[4:6], 'little', signed=True) # Z signed
return x, y, z # Return values
tilt_threshold = 150 # Tilt threshold for commands
dead_zone = 80 # Dead-zone to avoid jitter
last_cmd = "STOP" # Start with STOP as the last command
print("Microproject 6.2.3: Streaming commands with change-only prints") # Status message
while True: # Continuous control loop
x, y, z = read_xyz() # Read axes
cmd = "STOP" # Default command
if abs(x) < dead_zone and abs(y) < dead_zone: # Inside dead-zone
cmd = "STOP" # Keep stop
else: # Outside dead-zone
if y > tilt_threshold: # Forward tilt
cmd = "FWD" # Forward command
elif y < -tilt_threshold: # Backward tilt
cmd = "BACK" # Backward command
elif x > tilt_threshold: # Right tilt
cmd = "RIGHT" # Right command
elif x < -tilt_threshold: # Left tilt
cmd = "LEFT" # Left command
else: # Weak tilt region
cmd = "STOP" # Stop command
if cmd != last_cmd: # If command changed
print("CMD:", cmd) # Print only the new command
last_cmd = cmd # Update last printed command
time.sleep(0.08) # Short delay for responsive control
Reflection: Changeâonly prints make app parsing and robot reactions more stable.
Challenge:
- Easy: Add a small âCOOLDOWN=200 msâ after any command change.
- Harder: Add âCMD:HOLDâ if the same command stays active for >2 s.
Microproject 6.2.4 â Gestures for commands
Goal: Detect simple gestures (shake, nod, double tilt) and print special commands.
Blocks used:
- Variables: windows and counters.
- If / else: Compare axis magnitudes to thresholds for gesture detection.
MicroPython code:
import machine # Import machine for I2C
import time # Import time for gesture windows
# Reuse constants and I2C
ADXL_ADDR = 0x53 # Sensor address
REG_POWER_CTL = 0x2D # Power control register
REG_DATA_FORMAT = 0x31 # Data format register
REG_DATAX0 = 0x32 # Axis data start
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000) # Create I2C bus
i2c.writeto_mem(ADXL_ADDR, REG_POWER_CTL, b'\x08') # Start measurement
i2c.writeto_mem(ADXL_ADDR, REG_DATA_FORMAT, b'\x00') # ±2g range
def read_xyz(): # Helper to read axes
buf = i2c.readfrom_mem(ADXL_ADDR, REG_DATAX0, 6) # Read 6 bytes
x = int.from_bytes(buf[0:2], 'little', signed=True) # X signed
y = int.from_bytes(buf[2:4], 'little', signed=True) # Y signed
z = int.from_bytes(buf[4:6], 'little', signed=True) # Z signed
return x, y, z # Return values
shake_threshold = 400 # Large magnitude change considered a shake
nod_threshold = 250 # Y-axis magnitude for nod gesture
double_window_ms = 600 # Time window to confirm a double tilt
print("Microproject 6.2.4: Gestures -> shake:", shake_threshold, "nod:", nod_threshold) # Show thresholds
last_event_time = 0 # Store time of the last detected tilt event
last_event = "" # Store type of last tilt event
while True: # Continuous gesture detection loop
x, y, z = read_xyz() # Read axes from sensor
mag = abs(x) + abs(y) + abs(z) # Compute simple magnitude sum
if mag > shake_threshold: # If magnitude suggests shaking
print("GESTURE:SHAKE") # Print shake gesture
time.sleep(0.2) # Debounce after shake
if abs(y) > nod_threshold: # If strong tilt on Y
now = time.ticks_ms() # Record current time in ms
if last_event == "NOD" and time.ticks_diff(now, last_event_time) < double_window_ms: # Check double nod
print("GESTURE:DOUBLE_NOD") # Print double nod gesture
last_event = "" # Reset last event
else: # First nod in sequence
print("GESTURE:NOD") # Print nod gesture
last_event = "NOD" # Save event type
last_event_time = now # Save event time
time.sleep(0.15) # Debounce after nod detection
time.sleep(0.05) # Small loop delay for responsiveness
Reflection: Gestures add personalityâshake and nod let you trigger special actions quickly.
Challenge:
- Easy: Add âGESTURE:TILT_LEFTâ and âGESTURE:TILT_RIGHTâ using X thresholds.
- Harder: Add a âDOUBLE_SHAKEâ detector with a 500 ms window.
Microproject 6.2.5 â Data calibration and filtering
Goal: Calibrate offsets and apply a moving average filter for smooth data.
Blocks used:
- Variables: x_off, y_off, z_off; simple buffers for averaging.
- Loop: Collect samples and compute offsets.
MicroPython code:
import machine # Import machine for I2C access
import time # Import time for sampling and delays
# Reuse constants and I2C
ADXL_ADDR = 0x53 # Sensor address
REG_POWER_CTL = 0x2D # Power control register
REG_DATA_FORMAT = 0x31 # Data format register
REG_DATAX0 = 0x32 # Axis data start
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000) # Create I2C bus
i2c.writeto_mem(ADXL_ADDR, REG_POWER_CTL, b'\x08') # Enable measurement
i2c.writeto_mem(ADXL_ADDR, REG_DATA_FORMAT, b'\x00') # ±2g range
def read_xyz(): # Helper to read raw axes
buf = i2c.readfrom_mem(ADXL_ADDR, REG_DATAX0, 6) # Read 6 bytes for X,Y,Z
x = int.from_bytes(buf[0:2], 'little', signed=True) # X signed
y = int.from_bytes(buf[2:4], 'little', signed=True) # Y signed
z = int.from_bytes(buf[4:6], 'little', signed=True) # Z signed
return x, y, z # Return raw values
print("Microproject 6.2.5: Hold flat for 1 s to calibrate offsets") # Ask user to keep board still
# Offset calibration (average of 20 samples)
x_sum = 0 # Start sum for X
y_sum = 0 # Start sum for Y
z_sum = 0 # Start sum for Z
for i in range(20): # Take 20 samples
x, y, z = read_xyz() # Read raw axes
x_sum += x # Add X to sum
y_sum += y # Add Y to sum
z_sum += z # Add Z to sum
time.sleep(0.05) # Small delay between samples
x_off = int(x_sum / 20) # Compute X offset average
y_off = int(y_sum / 20) # Compute Y offset average
z_off = int(z_sum / 20) # Compute Z offset average
print("CAL:OFFSETS", x_off, y_off, z_off) # Print computed offsets
# Moving average buffers (size 5)
buf_x = [0, 0, 0, 0, 0] # Initialize buffer for X
buf_y = [0, 0, 0, 0, 0] # Initialize buffer for Y
buf_z = [0, 0, 0, 0, 0] # Initialize buffer for Z
idx = 0 # Index for circular buffer
while True: # Continuous filtered output
x, y, z = read_xyz() # Read raw axes
x -= x_off # Apply X offset
y -= y_off # Apply Y offset
z -= z_off # Apply Z offset
buf_x[idx] = x # Store X in buffer
buf_y[idx] = y # Store Y in buffer
buf_z[idx] = z # Store Z in buffer
idx = (idx + 1) % 5 # Move to next index in circular buffer
x_f = int(sum(buf_x) / 5) # Compute average X
y_f = int(sum(buf_y) / 5) # Compute average Y
z_f = int(sum(buf_z) / 5) # Compute average Z
print("AXIS_FILT:", x_f, y_f, z_f) # Print filtered axes
time.sleep(0.08) # Short delay for readability
Reflection: Offsets and averages turn noisy raw data into smooth, trustworthy values.
Challenge:
- Easy: Increase buffer size to 7 samples for stronger smoothing.
- Harder: Add a âCAL:OKâ flag only if offsets stay within a safe range (e.g., |offset| < 200).
Main project â Tilt control with accelerometer
Blocks steps (with glossary)
- I2C + sensor init: Start ADXL345 and set ±2g.
- Calibration: Compute offsets while flat and apply moving average.
- Tilt mapping: Use thresholds and deadâzones to produce commands.
- Gesture layer: Detect shake and doubleânod.
- Changeâonly prints: Reduce spam by printing only when the command changes.
MicroPython code (mirroring blocks)
# Project 6.2 â Tilt Control (Accelerometer)
import machine # Import machine for I2C pins
import time # Import time for delays and windows
# ADXL345 constants and registers
ADXL_ADDR = 0x53 # Sensor I2C address
REG_POWER_CTL = 0x2D # Power control register
REG_DATA_FORMAT = 0x31 # Data format register
REG_DATAX0 = 0x32 # Axis data start register
# I2C setup on pins 22/21
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000) # Create I2C bus
i2c.writeto_mem(ADXL_ADDR, REG_POWER_CTL, b'\x08') # Enable measurement mode
i2c.writeto_mem(ADXL_ADDR, REG_DATA_FORMAT, b'\x00') # Set ±2g range
def read_xyz(): # Read signed X/Y/Z counts
buf = i2c.readfrom_mem(ADXL_ADDR, REG_DATAX0, 6) # Read 6 bytes from axis registers
x = int.from_bytes(buf[0:2], 'little', signed=True) # Convert X bytes to signed int
y = int.from_bytes(buf[2:4], 'little', signed=True) # Convert Y bytes to signed int
z = int.from_bytes(buf[4:6], 'little', signed=True) # Convert Z bytes to signed int
return x, y, z # Return tuple of axes
print("INIT: ADXL345 ready at 0x53") # Confirm sensor init
# Calibration: hold flat to compute offsets
print("CAL: Hold flat 1 s") # Instruct user to hold board flat
x_sum = 0 # Initialize X sum
y_sum = 0 # Initialize Y sum
z_sum = 0 # Initialize Z sum
for i in range(20): # Take 20 samples for calibration
x, y, z = read_xyz() # Read raw axes
x_sum += x # Add X to sum
y_sum += y # Add Y to sum
z_sum += z # Add Z to sum
time.sleep(0.05) # Small delay between samples
x_off = int(x_sum / 20) # Compute X offset
y_off = int(y_sum / 20) # Compute Y offset
z_off = int(z_sum / 20) # Compute Z offset
print("CAL:OFFSETS", x_off, y_off, z_off) # Show offsets
# Filtering buffers
buf_x = [0, 0, 0, 0, 0] # X moving average buffer
buf_y = [0, 0, 0, 0, 0] # Y moving average buffer
buf_z = [0, 0, 0, 0, 0] # Z moving average buffer
idx = 0 # Circular buffer index
# Mapping thresholds and state
tilt_threshold = 150 # Tilt threshold for commands
dead_zone = 80 # Dead-zone to avoid jitter
last_cmd = "STOP" # Start with STOP as last command
shake_threshold = 400 # Shake magnitude threshold
nod_threshold = 250 # Nod threshold on Y
last_event = "" # Last gesture event
last_event_time = 0 # Time of last gesture event
print("RUN: Tilt control active") # Announce program start
while True: # Main control loop
x, y, z = read_xyz() # Read raw axes
x -= x_off # Apply X offset
y -= y_off # Apply Y offset
z -= z_off # Apply Z offset
buf_x[idx] = x # Store X in buffer
buf_y[idx] = y # Store Y in buffer
buf_z[idx] = z # Store Z in buffer
idx = (idx + 1) % 5 # Advance circular buffer index
x_f = int(sum(buf_x) / 5) # Average X
y_f = int(sum(buf_y) / 5) # Average Y
z_f = int(sum(buf_z) / 5) # Average Z
print("AXIS_FILT:", x_f, y_f, z_f) # Print filtered axes
# Gesture layer: shake and double nod
mag = abs(x_f) + abs(y_f) + abs(z_f) # Simple magnitude check
if mag > shake_threshold: # If shaking detected
print("GESTURE:SHAKE") # Print shake gesture
time.sleep(0.2) # Debounce after shake
if abs(y_f) > nod_threshold: # If strong Y tilt detected
now = time.ticks_ms() # Current ms time
if last_event == "NOD" and time.ticks_diff(now, last_event_time) < 600: # Double nod window
print("GESTURE:DOUBLE_NOD") # Print double nod
last_event = "" # Reset last event
else: # First nod
print("GESTURE:NOD") # Print nod
last_event = "NOD" # Save event type
last_event_time = now # Save event time
time.sleep(0.15) # Debounce nod detection
# Tilt mapping with dead-zone
cmd = "STOP" # Default command
if abs(x_f) < dead_zone and abs(y_f) < dead_zone: # Inside dead-zone
cmd = "STOP" # Keep stop
else: # Outside dead-zone
if y_f > tilt_threshold: # Forward tilt
cmd = "FWD" # Forward command
elif y_f < -tilt_threshold: # Backward tilt
cmd = "BACK" # Backward command
elif x_f > tilt_threshold: # Right tilt
cmd = "RIGHT" # Right command
elif x_f < -tilt_threshold: # Left tilt
cmd = "LEFT" # Left command
else: # Weak tilt
cmd = "STOP" # Stop command
if cmd != last_cmd: # If command changed
print("CMD:", cmd) # Print new command
last_cmd = cmd # Update last command
time.sleep(0.08) # Short delay for smooth control
External explanation
- What it teaches: How to configure and read an accelerometer, smooth the data, and convert tilt and gestures into clean commands.
- Why it works: Offsets remove bias, moving average reduces noise, deadâzones prevent jitter, and changeâonly prints keep control stable and readable.
- Key concept: âSense â calibrate â smooth â decide â command.â
Story time
You tilt the board and the robot responds like itâs listening to your hands. A quick nod sends a special action; a shake cancels tasks. Smooth, simple, and surprisingly human.
Debugging (2)
Debugging 6.2.1 â Tilt not detected
Problem: Axis values print, but no CMD:FWD/BACK/LEFT/RIGHT appear.
Clues: AXIS_FILT changes slightly; commands stay âSTOPâ.
Broken code:
tilt_threshold = 500 # Threshold too high for small tilts
Fixed code:
tilt_threshold = 150 # Lower threshold to a realistic value
print("DEBUG: tilt_threshold=", tilt_threshold) # Print current threshold
Why it works: A realistic threshold lets typical tilts cross the decision boundary.
Avoid next time: Tune thresholds while watching AXIS_FILT values.
Debugging 6.2.2 â Erratic movement due to vibrations
Problem: Commands flicker quickly during small shakes or steps.
Clues: AXIS_FILT jumps; CMD alternates fast.
Broken code:
dead_zone = 20 # Dead-zone too small to absorb noise
Fixed code:
dead_zone = 80 # Increase dead-zone to ignore minor changes
print("DEBUG: dead_zone=", dead_zone) # Print dead-zone setting
Why it works: A wider deadâzone prevents microâtilts from triggering new commands.
Avoid next time: Use filtering and deadâzones together for stability.
Final checklist
- ADXL345 initializes and prints AXIS raw/filtered values
- Calibration offsets computed and applied
- Deadâzone + thresholds produce stable CMD outputs
- Gestures (SHAKE, NOD, DOUBLE_NOD) detected reliably
- Changeâonly command stream keeps serial clean
Extras
- đ§ Student tip: Print âSPEED:â from tilt magnitude and use it later to scale robot motion.
- đ§âđ« Instructor tip: Have students record AXIS and CMD logs during tuningâdata makes good thresholds.
- đ Glossary:
- Deadâzone: A range around zero where changes are ignored to avoid jitter.
- Offset (calibration): A correction added/subtracted to remove bias from readings.
- Moving average: A simple filter that smooths values by averaging recent samples.
- đĄ Mini tips:
- Keep wires short on I2C and check grounds to reduce noise.
- Calibrate on a stable surface; reâcalibrate if temperature changes.
- Label your prints consistently so apps can parse them easily.