<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.bespokerobotsociety.org/index.php?action=history&amp;feed=atom&amp;title=MicroPython_Programming</id>
	<title>MicroPython Programming - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.bespokerobotsociety.org/index.php?action=history&amp;feed=atom&amp;title=MicroPython_Programming"/>
	<link rel="alternate" type="text/html" href="https://wiki.bespokerobotsociety.org/index.php?title=MicroPython_Programming&amp;action=history"/>
	<updated>2026-04-25T10:12:18Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.5</generator>
	<entry>
		<id>https://wiki.bespokerobotsociety.org/index.php?title=MicroPython_Programming&amp;diff=83&amp;oldid=prev</id>
		<title>John: Created page with &quot;{{Tutorial |name=MicroPython Programming |competency=Software |difficulty=Intermediate |time=5-8 hours (split across multiple sessions) |prerequisites=MicroPython Basics, Electronics Fundamentals |materials=Raspberry Pi Pico, breadboard, motors with H-bridge (TB6612FNG), encoders, I2C sensor (optional: MPU6050 IMU) |next_steps=State Machine Design, Behavior:PID Control, SimpleBot advanced implementations }}  &#039;&#039;&#039;MicroPython Programming&#039;&#039;&#039; builds on...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.bespokerobotsociety.org/index.php?title=MicroPython_Programming&amp;diff=83&amp;oldid=prev"/>
		<updated>2025-10-11T20:15:32Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;{{Tutorial |name=MicroPython Programming |competency=&lt;a href=&quot;/wiki/Software&quot; title=&quot;Software&quot;&gt;Software&lt;/a&gt; |difficulty=Intermediate |time=5-8 hours (split across multiple sessions) |prerequisites=&lt;a href=&quot;/wiki/MicroPython_Basics&quot; title=&quot;MicroPython Basics&quot;&gt;MicroPython Basics&lt;/a&gt;, &lt;a href=&quot;/wiki/Electronics_Fundamentals&quot; title=&quot;Electronics Fundamentals&quot;&gt;Electronics Fundamentals&lt;/a&gt; |materials=Raspberry Pi Pico, breadboard, motors with H-bridge (TB6612FNG), encoders, I2C sensor (optional: MPU6050 IMU) |next_steps=&lt;a href=&quot;/index.php?title=State_Machine_Design&amp;amp;action=edit&amp;amp;redlink=1&quot; class=&quot;new&quot; title=&quot;State Machine Design (page does not exist)&quot;&gt;State Machine Design&lt;/a&gt;, &lt;a href=&quot;/wiki/Behavior:PID_Control&quot; title=&quot;Behavior:PID Control&quot;&gt;Behavior:PID Control&lt;/a&gt;, &lt;a href=&quot;/wiki/SimpleBot&quot; title=&quot;SimpleBot&quot;&gt;SimpleBot&lt;/a&gt; advanced implementations }}  &amp;#039;&amp;#039;&amp;#039;MicroPython Programming&amp;#039;&amp;#039;&amp;#039; builds on...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;{{Tutorial&lt;br /&gt;
|name=MicroPython Programming&lt;br /&gt;
|competency=[[Software]]&lt;br /&gt;
|difficulty=Intermediate&lt;br /&gt;
|time=5-8 hours (split across multiple sessions)&lt;br /&gt;
|prerequisites=[[MicroPython Basics]], [[Electronics Fundamentals]]&lt;br /&gt;
|materials=Raspberry Pi Pico, breadboard, motors with H-bridge (TB6612FNG), encoders, I2C sensor (optional: MPU6050 IMU)&lt;br /&gt;
|next_steps=[[State Machine Design]], [[Behavior:PID Control]], [[SimpleBot]] advanced implementations&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;MicroPython Programming&amp;#039;&amp;#039;&amp;#039; builds on [[MicroPython Basics]] to teach advanced techniques for robot control. This tutorial covers interrupts, PWM motor control, I2C communication, timers, and state machines - everything you need to build sophisticated robot behaviors.&lt;br /&gt;
&lt;br /&gt;
By the end of this tutorial, you&amp;#039;ll understand:&lt;br /&gt;
* Hardware interrupts for encoder counting&lt;br /&gt;
* PWM generation for precise motor speed control&lt;br /&gt;
* I2C communication for reading sensors (IMU example)&lt;br /&gt;
* Timers for non-blocking periodic tasks&lt;br /&gt;
* State machines for organizing complex behaviors&lt;br /&gt;
* Asynchronous programming with asyncio&lt;br /&gt;
&lt;br /&gt;
This tutorial is &amp;#039;&amp;#039;&amp;#039;hands-on&amp;#039;&amp;#039;&amp;#039; and assumes you&amp;#039;ve completed [[MicroPython Basics]].&lt;br /&gt;
&lt;br /&gt;
== Part 1: Hardware Interrupts ==&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Interrupts&amp;#039;&amp;#039;&amp;#039; let the microcontroller respond immediately to events without constantly checking (polling).&lt;br /&gt;
&lt;br /&gt;
=== Why Use Interrupts? ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Polling (inefficient):&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
while True:&lt;br /&gt;
    if encoder.value() == 1:  # Constantly checking&lt;br /&gt;
        count += 1&lt;br /&gt;
    # Do other work&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Problem:&amp;#039;&amp;#039;&amp;#039; If encoder pulse is brief, you might miss it while doing other work.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Interrupts (efficient):&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def encoder_callback(pin):&lt;br /&gt;
    count += 1  # Runs immediately when encoder pulses&lt;br /&gt;
&lt;br /&gt;
encoder.irq(trigger=Pin.IRQ_RISING, handler=encoder_callback)&lt;br /&gt;
# Do other work - encoder is counted automatically!&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Interrupt triggers a function&amp;#039;&amp;#039;&amp;#039; when the event occurs, regardless of what the main loop is doing.&lt;br /&gt;
&lt;br /&gt;
=== Basic Interrupt Example: Count Button Presses ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import Pin&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
press_count = 0&lt;br /&gt;
&lt;br /&gt;
def button_callback(pin):&lt;br /&gt;
    global press_count&lt;br /&gt;
    press_count += 1&lt;br /&gt;
&lt;br /&gt;
button = Pin(14, Pin.IN, Pin.PULL_UP)&lt;br /&gt;
button.irq(trigger=Pin.IRQ_FALLING, handler=button_callback)&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    print(f&amp;quot;Button pressed {press_count} times&amp;quot;)&lt;br /&gt;
    time.sleep(1)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Explanation:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Pin.IRQ_FALLING&amp;#039;&amp;#039;&amp;#039; - Trigger when signal goes HIGH→LOW (button pressed)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;global press_count&amp;#039;&amp;#039;&amp;#039; - Modify variable from interrupt handler&lt;br /&gt;
* Main loop can do other work while button presses are counted&lt;br /&gt;
&lt;br /&gt;
=== Interrupt-Driven Encoder Counting ===&lt;br /&gt;
&lt;br /&gt;
Optical encoders produce pulses as wheels rotate. Interrupts ensure no pulses are missed.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Single encoder (counts pulses):&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import Pin&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
encoder_count = 0&lt;br /&gt;
&lt;br /&gt;
def encoder_callback(pin):&lt;br /&gt;
    global encoder_count&lt;br /&gt;
    encoder_count += 1&lt;br /&gt;
&lt;br /&gt;
encoder = Pin(15, Pin.IN, Pin.PULL_UP)&lt;br /&gt;
encoder.irq(trigger=Pin.IRQ_RISING, handler=encoder_callback)&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    print(f&amp;quot;Encoder count: {encoder_count}&amp;quot;)&lt;br /&gt;
    time.sleep(0.5)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Quadrature encoder (detects direction):&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
Quadrature encoders have two channels (A and B) that pulse 90° out of phase:&lt;br /&gt;
* A leads B → forward rotation&lt;br /&gt;
* B leads A → backward rotation&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import Pin&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
encoder_count = 0&lt;br /&gt;
encoder_a = Pin(15, Pin.IN, Pin.PULL_UP)&lt;br /&gt;
encoder_b = Pin(16, Pin.IN, Pin.PULL_UP)&lt;br /&gt;
&lt;br /&gt;
def encoder_a_callback(pin):&lt;br /&gt;
    global encoder_count&lt;br /&gt;
    if encoder_b.value() == 1:&lt;br /&gt;
        encoder_count += 1  # Forward&lt;br /&gt;
    else:&lt;br /&gt;
        encoder_count -= 1  # Backward&lt;br /&gt;
&lt;br /&gt;
encoder_a.irq(trigger=Pin.IRQ_RISING, handler=encoder_a_callback)&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    print(f&amp;quot;Position: {encoder_count}&amp;quot;)&lt;br /&gt;
    time.sleep(0.5)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Why quadrature encoding matters:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* SimpleBot&amp;#039;s [[Capability:Optical Odometry]] uses encoder direction to track position&lt;br /&gt;
* Essential for [[Activity:Dead Reckoning]] and precise movement&lt;br /&gt;
&lt;br /&gt;
=== Interrupt Best Practices ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;DO:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* Keep interrupt handlers SHORT and FAST&lt;br /&gt;
* Use global variables to pass data to main loop&lt;br /&gt;
* Use volatile flags to signal events&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;DON&amp;#039;T:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* Use &amp;#039;&amp;#039;&amp;#039;time.sleep()&amp;#039;&amp;#039;&amp;#039; in interrupt handlers (blocks everything!)&lt;br /&gt;
* Print in interrupt handlers (slow, can cause problems)&lt;br /&gt;
* Do complex calculations in interrupt handlers&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Good pattern - minimal interrupt, main loop does work:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
encoder_flag = False&lt;br /&gt;
&lt;br /&gt;
def encoder_callback(pin):&lt;br /&gt;
    global encoder_flag&lt;br /&gt;
    encoder_flag = True  # Just set a flag&lt;br /&gt;
&lt;br /&gt;
encoder.irq(trigger=Pin.IRQ_RISING, handler=encoder_callback)&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    if encoder_flag:&lt;br /&gt;
        # Do complex processing here&lt;br /&gt;
        calculate_position()&lt;br /&gt;
        encoder_flag = False&lt;br /&gt;
&lt;br /&gt;
    # Other work&lt;br /&gt;
    time.sleep(0.01)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Part 2: PWM (Pulse Width Modulation) ==&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;PWM&amp;#039;&amp;#039;&amp;#039; controls motor speed by rapidly switching power on and off.&lt;br /&gt;
&lt;br /&gt;
=== PWM Basics ===&lt;br /&gt;
&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Frequency&amp;#039;&amp;#039;&amp;#039; - How fast it switches (typically 1-20 kHz for motors)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Duty cycle&amp;#039;&amp;#039;&amp;#039; - Percentage of time it&amp;#039;s ON (0% = stopped, 100% = full speed)&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Example:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* 50% duty cycle = motor at half speed&lt;br /&gt;
* 75% duty cycle = motor at 75% speed&lt;br /&gt;
&lt;br /&gt;
=== PWM on Raspberry Pi Pico ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import Pin, PWM&lt;br /&gt;
&lt;br /&gt;
# Create PWM object&lt;br /&gt;
motor_pwm = PWM(Pin(16))&lt;br /&gt;
&lt;br /&gt;
# Set frequency (1 kHz)&lt;br /&gt;
motor_pwm.freq(1000)&lt;br /&gt;
&lt;br /&gt;
# Set duty cycle (50% = 32768 out of 65535)&lt;br /&gt;
motor_pwm.duty_u16(32768)&lt;br /&gt;
&lt;br /&gt;
# Other duty cycles:&lt;br /&gt;
motor_pwm.duty_u16(0)      # 0% - stopped&lt;br /&gt;
motor_pwm.duty_u16(16384)  # 25% - quarter speed&lt;br /&gt;
motor_pwm.duty_u16(49152)  # 75% - three-quarter speed&lt;br /&gt;
motor_pwm.duty_u16(65535)  # 100% - full speed&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Why duty_u16?&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;u16&amp;#039;&amp;#039;&amp;#039; = unsigned 16-bit integer (0-65535)&lt;br /&gt;
* Provides fine control (65536 different speed levels)&lt;br /&gt;
* Alternative: &amp;#039;&amp;#039;&amp;#039;duty_ns(nanoseconds)&amp;#039;&amp;#039;&amp;#039; for precise timing&lt;br /&gt;
&lt;br /&gt;
=== Motor Control with H-Bridge ===&lt;br /&gt;
&lt;br /&gt;
DC motors need an &amp;#039;&amp;#039;&amp;#039;H-bridge&amp;#039;&amp;#039;&amp;#039; to control direction and speed. Common chip: [[TB6612FNG]].&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;H-bridge control signals:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;IN1, IN2&amp;#039;&amp;#039;&amp;#039; - Direction control for motor A (HIGH/LOW, LOW/HIGH)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;PWM&amp;#039;&amp;#039;&amp;#039; - Speed control (duty cycle)&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Motor control class:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import Pin, PWM&lt;br /&gt;
&lt;br /&gt;
class Motor:&lt;br /&gt;
    def __init__(self, in1_pin, in2_pin, pwm_pin):&lt;br /&gt;
        self.in1 = Pin(in1_pin, Pin.OUT)&lt;br /&gt;
        self.in2 = Pin(in2_pin, Pin.OUT)&lt;br /&gt;
        self.pwm = PWM(Pin(pwm_pin))&lt;br /&gt;
        self.pwm.freq(1000)  # 1 kHz&lt;br /&gt;
&lt;br /&gt;
    def forward(self, speed):&lt;br /&gt;
        &amp;quot;&amp;quot;&amp;quot;Drive forward at speed (0-100)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
        self.in1.high()&lt;br /&gt;
        self.in2.low()&lt;br /&gt;
        duty = int(speed * 65535 / 100)&lt;br /&gt;
        self.pwm.duty_u16(duty)&lt;br /&gt;
&lt;br /&gt;
    def backward(self, speed):&lt;br /&gt;
        &amp;quot;&amp;quot;&amp;quot;Drive backward at speed (0-100)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
        self.in1.low()&lt;br /&gt;
        self.in2.high()&lt;br /&gt;
        duty = int(speed * 65535 / 100)&lt;br /&gt;
        self.pwm.duty_u16(duty)&lt;br /&gt;
&lt;br /&gt;
    def stop(self):&lt;br /&gt;
        &amp;quot;&amp;quot;&amp;quot;Stop motor (brake)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
        self.in1.low()&lt;br /&gt;
        self.in2.low()&lt;br /&gt;
        self.pwm.duty_u16(0)&lt;br /&gt;
&lt;br /&gt;
# Example usage&lt;br /&gt;
motor_left = Motor(in1_pin=10, in2_pin=11, pwm_pin=12)&lt;br /&gt;
motor_right = Motor(in1_pin=13, in2_pin=14, pwm_pin=15)&lt;br /&gt;
&lt;br /&gt;
motor_left.forward(75)   # Left motor 75% forward&lt;br /&gt;
motor_right.forward(75)  # Right motor 75% forward&lt;br /&gt;
time.sleep(2)&lt;br /&gt;
motor_left.stop()&lt;br /&gt;
motor_right.stop()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Differential Drive Robot ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Differential drive&amp;#039;&amp;#039;&amp;#039; uses two independently controlled wheels:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
class DifferentialDrive:&lt;br /&gt;
    def __init__(self, motor_left, motor_right):&lt;br /&gt;
        self.left = motor_left&lt;br /&gt;
        self.right = motor_right&lt;br /&gt;
&lt;br /&gt;
    def forward(self, speed=50):&lt;br /&gt;
        self.left.forward(speed)&lt;br /&gt;
        self.right.forward(speed)&lt;br /&gt;
&lt;br /&gt;
    def backward(self, speed=50):&lt;br /&gt;
        self.left.backward(speed)&lt;br /&gt;
        self.right.backward(speed)&lt;br /&gt;
&lt;br /&gt;
    def turn_left(self, speed=50):&lt;br /&gt;
        self.left.backward(speed)&lt;br /&gt;
        self.right.forward(speed)&lt;br /&gt;
&lt;br /&gt;
    def turn_right(self, speed=50):&lt;br /&gt;
        self.left.forward(speed)&lt;br /&gt;
        self.right.backward(speed)&lt;br /&gt;
&lt;br /&gt;
    def stop(self):&lt;br /&gt;
        self.left.stop()&lt;br /&gt;
        self.right.stop()&lt;br /&gt;
&lt;br /&gt;
# Create robot&lt;br /&gt;
robot = DifferentialDrive(motor_left, motor_right)&lt;br /&gt;
&lt;br /&gt;
# Drive in a square&lt;br /&gt;
robot.forward(50)&lt;br /&gt;
time.sleep(1)&lt;br /&gt;
robot.turn_left(50)&lt;br /&gt;
time.sleep(0.5)&lt;br /&gt;
robot.stop()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the foundation of [[Capability:Differential Drive]]!&lt;br /&gt;
&lt;br /&gt;
== Part 3: I2C Communication ==&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;I2C&amp;#039;&amp;#039;&amp;#039; (Inter-Integrated Circuit) is a two-wire protocol for communicating with sensors.&lt;br /&gt;
&lt;br /&gt;
=== I2C Basics ===&lt;br /&gt;
&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;SDA&amp;#039;&amp;#039;&amp;#039; - Serial Data (bidirectional data line)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;SCL&amp;#039;&amp;#039;&amp;#039; - Serial Clock (clock signal from controller)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Addresses&amp;#039;&amp;#039;&amp;#039; - Each device has a unique 7-bit address (e.g., 0x68)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Multi-device&amp;#039;&amp;#039;&amp;#039; - Multiple sensors on same two wires&lt;br /&gt;
&lt;br /&gt;
=== I2C on Raspberry Pi Pico ===&lt;br /&gt;
&lt;br /&gt;
Pico has two I2C buses (I2C0, I2C1):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import I2C, Pin&lt;br /&gt;
&lt;br /&gt;
# I2C0 on pins 0 (SDA) and 1 (SCL)&lt;br /&gt;
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)&lt;br /&gt;
&lt;br /&gt;
# Scan for devices&lt;br /&gt;
devices = i2c.scan()&lt;br /&gt;
print(f&amp;quot;I2C devices found: {[hex(d) for d in devices]}&amp;quot;)&lt;br /&gt;
# Example output: [&amp;#039;0x68&amp;#039;] (MPU6050 IMU)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Reading an IMU (MPU6050) ===&lt;br /&gt;
&lt;br /&gt;
The MPU6050 is a popular 6-axis IMU (3-axis accelerometer + 3-axis gyroscope) used for [[Capability:IMU Sensing]].&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Basic register reading:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import I2C, Pin&lt;br /&gt;
import struct&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
MPU6050_ADDR = 0x68&lt;br /&gt;
PWR_MGMT_1 = 0x6B&lt;br /&gt;
ACCEL_XOUT_H = 0x3B&lt;br /&gt;
&lt;br /&gt;
# Initialize I2C&lt;br /&gt;
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)&lt;br /&gt;
&lt;br /&gt;
# Wake up MPU6050 (it starts in sleep mode)&lt;br /&gt;
i2c.writeto_mem(MPU6050_ADDR, PWR_MGMT_1, bytes([0]))&lt;br /&gt;
&lt;br /&gt;
def read_accel():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Read accelerometer data (X, Y, Z)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    data = i2c.readfrom_mem(MPU6050_ADDR, ACCEL_XOUT_H, 6)&lt;br /&gt;
    ax, ay, az = struct.unpack(&amp;#039;&amp;gt;3h&amp;#039;, data)  # Big-endian signed 16-bit&lt;br /&gt;
&lt;br /&gt;
    # Convert to g (gravity units)&lt;br /&gt;
    # MPU6050 default range: ±2g, scale: 16384 LSB/g&lt;br /&gt;
    ax_g = ax / 16384.0&lt;br /&gt;
    ay_g = ay / 16384.0&lt;br /&gt;
    az_g = az / 16384.0&lt;br /&gt;
&lt;br /&gt;
    return ax_g, ay_g, az_g&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    ax, ay, az = read_accel()&lt;br /&gt;
    print(f&amp;quot;Accel X: {ax:.2f}g, Y: {ay:.2f}g, Z: {az:.2f}g&amp;quot;)&lt;br /&gt;
    time.sleep(0.1)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Complete MPU6050 class:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
class MPU6050:&lt;br /&gt;
    def __init__(self, i2c, addr=0x68):&lt;br /&gt;
        self.i2c = i2c&lt;br /&gt;
        self.addr = addr&lt;br /&gt;
        # Wake up MPU6050&lt;br /&gt;
        self.i2c.writeto_mem(self.addr, 0x6B, bytes([0]))&lt;br /&gt;
&lt;br /&gt;
    def read_accel(self):&lt;br /&gt;
        data = self.i2c.readfrom_mem(self.addr, 0x3B, 6)&lt;br /&gt;
        ax, ay, az = struct.unpack(&amp;#039;&amp;gt;3h&amp;#039;, data)&lt;br /&gt;
        return ax / 16384.0, ay / 16384.0, az / 16384.0&lt;br /&gt;
&lt;br /&gt;
    def read_gyro(self):&lt;br /&gt;
        data = self.i2c.readfrom_mem(self.addr, 0x43, 6)&lt;br /&gt;
        gx, gy, gz = struct.unpack(&amp;#039;&amp;gt;3h&amp;#039;, data)&lt;br /&gt;
        # Gyro scale: 131 LSB/°/s&lt;br /&gt;
        return gx / 131.0, gy / 131.0, gz / 131.0&lt;br /&gt;
&lt;br /&gt;
    def read_temp(self):&lt;br /&gt;
        data = self.i2c.readfrom_mem(self.addr, 0x41, 2)&lt;br /&gt;
        temp_raw = struct.unpack(&amp;#039;&amp;gt;h&amp;#039;, data)[0]&lt;br /&gt;
        return temp_raw / 340.0 + 36.53  # Convert to °C&lt;br /&gt;
&lt;br /&gt;
# Usage&lt;br /&gt;
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)&lt;br /&gt;
imu = MPU6050(i2c)&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    ax, ay, az = imu.read_accel()&lt;br /&gt;
    gx, gy, gz = imu.read_gyro()&lt;br /&gt;
    temp = imu.read_temp()&lt;br /&gt;
    print(f&amp;quot;Accel: {ax:.2f}, {ay:.2f}, {az:.2f} | Gyro: {gx:.1f}, {gy:.1f}, {gz:.1f} | Temp: {temp:.1f}°C&amp;quot;)&lt;br /&gt;
    time.sleep(0.1)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Why I2C matters for robotics:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* Many sensors use I2C: IMU, magnetometer, time-of-flight, OLED display&lt;br /&gt;
* Two wires connect many devices (versus 1 wire per device with digital I/O)&lt;br /&gt;
* Libraries abstract complexity (you don&amp;#039;t need to know every register)&lt;br /&gt;
&lt;br /&gt;
== Part 4: Timers and Non-Blocking Code ==&lt;br /&gt;
&lt;br /&gt;
=== The Problem with time.sleep() ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Blocking code:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
while True:&lt;br /&gt;
    motor.forward(50)&lt;br /&gt;
    time.sleep(2)  # Blocks for 2 seconds - can&amp;#039;t do anything else!&lt;br /&gt;
    motor.stop()&lt;br /&gt;
    time.sleep(1)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Problem:&amp;#039;&amp;#039;&amp;#039; Can&amp;#039;t read sensors or respond to events during sleep.&lt;br /&gt;
&lt;br /&gt;
=== Solution 1: Time-Based Switching ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
state = &amp;quot;forward&amp;quot;&lt;br /&gt;
state_start_time = time.ticks_ms()&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    current_time = time.ticks_ms()&lt;br /&gt;
    elapsed = time.ticks_diff(current_time, state_start_time)&lt;br /&gt;
&lt;br /&gt;
    if state == &amp;quot;forward&amp;quot;:&lt;br /&gt;
        motor.forward(50)&lt;br /&gt;
        if elapsed &amp;gt; 2000:  # 2 seconds&lt;br /&gt;
            state = &amp;quot;stopped&amp;quot;&lt;br /&gt;
            state_start_time = current_time&lt;br /&gt;
&lt;br /&gt;
    elif state == &amp;quot;stopped&amp;quot;:&lt;br /&gt;
        motor.stop()&lt;br /&gt;
        if elapsed &amp;gt; 1000:  # 1 second&lt;br /&gt;
            state = &amp;quot;forward&amp;quot;&lt;br /&gt;
            state_start_time = current_time&lt;br /&gt;
&lt;br /&gt;
    # Can read sensors and do other work here!&lt;br /&gt;
    sensor_value = sensor.value()&lt;br /&gt;
    print(f&amp;quot;Sensor: {sensor_value}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    time.sleep(0.01)  # Small delay to avoid maxing CPU&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Key functions:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;time.ticks_ms()&amp;#039;&amp;#039;&amp;#039; - Milliseconds since boot (wraps around after ~12 days)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;time.ticks_diff(new, old)&amp;#039;&amp;#039;&amp;#039; - Time difference handling wraparound&lt;br /&gt;
&lt;br /&gt;
=== Solution 2: Hardware Timers ===&lt;br /&gt;
&lt;br /&gt;
Hardware timers trigger a callback periodically without blocking:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import Timer&lt;br /&gt;
&lt;br /&gt;
led_state = False&lt;br /&gt;
&lt;br /&gt;
def timer_callback(timer):&lt;br /&gt;
    global led_state&lt;br /&gt;
    led_state = not led_state&lt;br /&gt;
    led.value(led_state)&lt;br /&gt;
&lt;br /&gt;
# Create timer that fires every 500ms&lt;br /&gt;
timer = Timer(period=500, mode=Timer.PERIODIC, callback=timer_callback)&lt;br /&gt;
&lt;br /&gt;
# Main loop is free to do other work!&lt;br /&gt;
while True:&lt;br /&gt;
    sensor_value = sensor.value()&lt;br /&gt;
    print(f&amp;quot;Sensor: {sensor_value}&amp;quot;)&lt;br /&gt;
    time.sleep(0.1)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;When to use timers:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* Periodic sensor readings (sample IMU every 10ms)&lt;br /&gt;
* Blinking status LEDs without blocking&lt;br /&gt;
* PID control loops (run every 20ms)&lt;br /&gt;
&lt;br /&gt;
== Part 5: State Machines ==&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;State machines&amp;#039;&amp;#039;&amp;#039; organize complex behaviors into discrete states with transitions.&lt;br /&gt;
&lt;br /&gt;
=== Line Following State Machine ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;States:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;FORWARD&amp;#039;&amp;#039;&amp;#039; - Both sensors on line&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;TURN_LEFT&amp;#039;&amp;#039;&amp;#039; - Right sensor lost line&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;TURN_RIGHT&amp;#039;&amp;#039;&amp;#039; - Left sensor lost line&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;SEARCH&amp;#039;&amp;#039;&amp;#039; - Both sensors lost line&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Code:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import Pin&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
# Sensors&lt;br /&gt;
sensor_left = Pin(14, Pin.IN, Pin.PULL_UP)&lt;br /&gt;
sensor_right = Pin(15, Pin.IN, Pin.PULL_UP)&lt;br /&gt;
&lt;br /&gt;
# State machine&lt;br /&gt;
state = &amp;quot;FORWARD&amp;quot;&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    left = sensor_left.value()&lt;br /&gt;
    right = sensor_right.value()&lt;br /&gt;
&lt;br /&gt;
    # State transitions&lt;br /&gt;
    if left == 0 and right == 0:&lt;br /&gt;
        state = &amp;quot;FORWARD&amp;quot;&lt;br /&gt;
    elif left == 0 and right == 1:&lt;br /&gt;
        state = &amp;quot;TURN_LEFT&amp;quot;&lt;br /&gt;
    elif left == 1 and right == 0:&lt;br /&gt;
        state = &amp;quot;TURN_RIGHT&amp;quot;&lt;br /&gt;
    else:&lt;br /&gt;
        state = &amp;quot;SEARCH&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # State actions&lt;br /&gt;
    if state == &amp;quot;FORWARD&amp;quot;:&lt;br /&gt;
        robot.forward(50)&lt;br /&gt;
        print(&amp;quot;State: FORWARD&amp;quot;)&lt;br /&gt;
    elif state == &amp;quot;TURN_LEFT&amp;quot;:&lt;br /&gt;
        robot.turn_left(40)&lt;br /&gt;
        print(&amp;quot;State: TURN_LEFT&amp;quot;)&lt;br /&gt;
    elif state == &amp;quot;TURN_RIGHT&amp;quot;:&lt;br /&gt;
        robot.turn_right(40)&lt;br /&gt;
        print(&amp;quot;State: TURN_RIGHT&amp;quot;)&lt;br /&gt;
    elif state == &amp;quot;SEARCH&amp;quot;:&lt;br /&gt;
        robot.turn_left(30)&lt;br /&gt;
        print(&amp;quot;State: SEARCH&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    time.sleep(0.01)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Advanced State Machine with Timing ===&lt;br /&gt;
&lt;br /&gt;
Add state entry/exit actions and timing:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
class StateMachine:&lt;br /&gt;
    def __init__(self):&lt;br /&gt;
        self.state = None&lt;br /&gt;
        self.state_start_time = 0&lt;br /&gt;
&lt;br /&gt;
    def change_state(self, new_state):&lt;br /&gt;
        if new_state != self.state:&lt;br /&gt;
            self.exit_state(self.state)&lt;br /&gt;
            self.state = new_state&lt;br /&gt;
            self.state_start_time = time.ticks_ms()&lt;br /&gt;
            self.enter_state(new_state)&lt;br /&gt;
&lt;br /&gt;
    def enter_state(self, state):&lt;br /&gt;
        print(f&amp;quot;Enter state: {state}&amp;quot;)&lt;br /&gt;
        # State entry actions&lt;br /&gt;
&lt;br /&gt;
    def exit_state(self, state):&lt;br /&gt;
        print(f&amp;quot;Exit state: {state}&amp;quot;)&lt;br /&gt;
        # State cleanup actions&lt;br /&gt;
&lt;br /&gt;
    def time_in_state(self):&lt;br /&gt;
        return time.ticks_diff(time.ticks_ms(), self.state_start_time)&lt;br /&gt;
&lt;br /&gt;
# Usage&lt;br /&gt;
sm = StateMachine()&lt;br /&gt;
sm.change_state(&amp;quot;FORWARD&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    # Determine next state based on sensors&lt;br /&gt;
    if obstacle_detected:&lt;br /&gt;
        sm.change_state(&amp;quot;AVOID&amp;quot;)&lt;br /&gt;
    elif line_detected:&lt;br /&gt;
        sm.change_state(&amp;quot;FOLLOW&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # Execute state behavior&lt;br /&gt;
    if sm.state == &amp;quot;FORWARD&amp;quot;:&lt;br /&gt;
        robot.forward(50)&lt;br /&gt;
    elif sm.state == &amp;quot;AVOID&amp;quot;:&lt;br /&gt;
        robot.turn_left(50)&lt;br /&gt;
        if sm.time_in_state() &amp;gt; 1000:  # Turn for 1 second&lt;br /&gt;
            sm.change_state(&amp;quot;FORWARD&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
See [[State Machine Design]] for more advanced patterns.&lt;br /&gt;
&lt;br /&gt;
== Part 6: Asynchronous Programming with asyncio ==&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;asyncio&amp;#039;&amp;#039;&amp;#039; allows concurrent tasks without threads (lightweight, cooperative multitasking).&lt;br /&gt;
&lt;br /&gt;
=== Basic asyncio Example ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import asyncio&lt;br /&gt;
from machine import Pin&lt;br /&gt;
&lt;br /&gt;
led1 = Pin(16, Pin.OUT)&lt;br /&gt;
led2 = Pin(17, Pin.OUT)&lt;br /&gt;
&lt;br /&gt;
async def blink_led1():&lt;br /&gt;
    while True:&lt;br /&gt;
        led1.toggle()&lt;br /&gt;
        await asyncio.sleep(0.5)  # Non-blocking sleep&lt;br /&gt;
&lt;br /&gt;
async def blink_led2():&lt;br /&gt;
    while True:&lt;br /&gt;
        led2.toggle()&lt;br /&gt;
        await asyncio.sleep(0.3)  # Different rate&lt;br /&gt;
&lt;br /&gt;
async def main():&lt;br /&gt;
    # Run both tasks concurrently&lt;br /&gt;
    await asyncio.gather(&lt;br /&gt;
        blink_led1(),&lt;br /&gt;
        blink_led2()&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
# Start event loop&lt;br /&gt;
asyncio.run(main())&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Both LEDs blink at different rates simultaneously!&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
=== Robot with Concurrent Sensor Reading and Motor Control ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import asyncio&lt;br /&gt;
from machine import Pin, I2C&lt;br /&gt;
&lt;br /&gt;
sensor_data = {&amp;quot;distance&amp;quot;: 0, &amp;quot;imu&amp;quot;: (0, 0, 0)}&lt;br /&gt;
&lt;br /&gt;
async def read_distance_sensor():&lt;br /&gt;
    while True:&lt;br /&gt;
        # Read I2C distance sensor&lt;br /&gt;
        distance = read_distance()  # Hypothetical function&lt;br /&gt;
        sensor_data[&amp;quot;distance&amp;quot;] = distance&lt;br /&gt;
        await asyncio.sleep(0.1)  # 10 Hz&lt;br /&gt;
&lt;br /&gt;
async def read_imu():&lt;br /&gt;
    while True:&lt;br /&gt;
        # Read I2C IMU&lt;br /&gt;
        ax, ay, az = imu.read_accel()&lt;br /&gt;
        sensor_data[&amp;quot;imu&amp;quot;] = (ax, ay, az)&lt;br /&gt;
        await asyncio.sleep(0.01)  # 100 Hz&lt;br /&gt;
&lt;br /&gt;
async def motor_control():&lt;br /&gt;
    while True:&lt;br /&gt;
        # Use sensor data to control motors&lt;br /&gt;
        if sensor_data[&amp;quot;distance&amp;quot;] &amp;lt; 20:&lt;br /&gt;
            robot.stop()&lt;br /&gt;
        else:&lt;br /&gt;
            robot.forward(50)&lt;br /&gt;
        await asyncio.sleep(0.02)  # 50 Hz control loop&lt;br /&gt;
&lt;br /&gt;
async def main():&lt;br /&gt;
    await asyncio.gather(&lt;br /&gt;
        read_distance_sensor(),&lt;br /&gt;
        read_imu(),&lt;br /&gt;
        motor_control()&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
asyncio.run(main())&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Why asyncio is useful:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
* Multiple sensors at different rates (IMU at 100 Hz, distance at 10 Hz)&lt;br /&gt;
* Motor control loop independent of sensor reading&lt;br /&gt;
* No blocking delays - everything runs &amp;quot;simultaneously&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Part 7: Practical Example - Encoder-Based Odometry ==&lt;br /&gt;
&lt;br /&gt;
Combine interrupts, PWM, and state machines for [[Activity:Dead Reckoning]]:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from machine import Pin, PWM&lt;br /&gt;
import time&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
class Odometry:&lt;br /&gt;
    def __init__(self, encoder_left_pin, encoder_right_pin, wheel_diameter, wheel_base, counts_per_rev):&lt;br /&gt;
        self.wheel_diameter = wheel_diameter  # mm&lt;br /&gt;
        self.wheel_base = wheel_base  # mm&lt;br /&gt;
        self.counts_per_rev = counts_per_rev&lt;br /&gt;
&lt;br /&gt;
        # Position&lt;br /&gt;
        self.x = 0.0&lt;br /&gt;
        self.y = 0.0&lt;br /&gt;
        self.theta = 0.0  # Heading in radians&lt;br /&gt;
&lt;br /&gt;
        # Encoder counts&lt;br /&gt;
        self.left_count = 0&lt;br /&gt;
        self.right_count = 0&lt;br /&gt;
&lt;br /&gt;
        # Setup encoders with interrupts&lt;br /&gt;
        self.encoder_left = Pin(encoder_left_pin, Pin.IN, Pin.PULL_UP)&lt;br /&gt;
        self.encoder_right = Pin(encoder_right_pin, Pin.IN, Pin.PULL_UP)&lt;br /&gt;
&lt;br /&gt;
        self.encoder_left.irq(trigger=Pin.IRQ_RISING, handler=self.left_callback)&lt;br /&gt;
        self.encoder_right.irq(trigger=Pin.IRQ_RISING, handler=self.right_callback)&lt;br /&gt;
&lt;br /&gt;
    def left_callback(self, pin):&lt;br /&gt;
        self.left_count += 1&lt;br /&gt;
&lt;br /&gt;
    def right_callback(self, pin):&lt;br /&gt;
        self.right_count += 1&lt;br /&gt;
&lt;br /&gt;
    def update(self):&lt;br /&gt;
        # Calculate distance traveled by each wheel&lt;br /&gt;
        mm_per_count = (math.pi * self.wheel_diameter) / self.counts_per_rev&lt;br /&gt;
        left_distance = self.left_count * mm_per_count&lt;br /&gt;
        right_distance = self.right_count * mm_per_count&lt;br /&gt;
&lt;br /&gt;
        # Reset counts&lt;br /&gt;
        self.left_count = 0&lt;br /&gt;
        self.right_count = 0&lt;br /&gt;
&lt;br /&gt;
        # Calculate robot movement&lt;br /&gt;
        distance = (left_distance + right_distance) / 2.0&lt;br /&gt;
        delta_theta = (right_distance - left_distance) / self.wheel_base&lt;br /&gt;
&lt;br /&gt;
        # Update position&lt;br /&gt;
        self.theta += delta_theta&lt;br /&gt;
        self.x += distance * math.cos(self.theta)&lt;br /&gt;
        self.y += distance * math.sin(self.theta)&lt;br /&gt;
&lt;br /&gt;
    def get_position(self):&lt;br /&gt;
        return self.x, self.y, self.theta&lt;br /&gt;
&lt;br /&gt;
# Usage&lt;br /&gt;
odom = Odometry(&lt;br /&gt;
    encoder_left_pin=15,&lt;br /&gt;
    encoder_right_pin=16,&lt;br /&gt;
    wheel_diameter=65,  # mm&lt;br /&gt;
    wheel_base=120,     # mm&lt;br /&gt;
    counts_per_rev=20   # Pulses per revolution&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
while True:&lt;br /&gt;
    odom.update()&lt;br /&gt;
    x, y, theta = odom.get_position()&lt;br /&gt;
    print(f&amp;quot;Position: ({x:.1f}, {y:.1f}) mm, Heading: {math.degrees(theta):.1f}°&amp;quot;)&lt;br /&gt;
    time.sleep(0.1)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This implements [[SimpleBot:Dead Reckoning Implementation]]!&lt;br /&gt;
&lt;br /&gt;
== Part 8: Skills Checklist ==&lt;br /&gt;
&lt;br /&gt;
By now, you should be able to:&lt;br /&gt;
&lt;br /&gt;
* ☐ Use hardware interrupts for encoder counting&lt;br /&gt;
* ☐ Generate PWM signals for motor speed control&lt;br /&gt;
* ☐ Control motor direction with H-bridge (TB6612FNG)&lt;br /&gt;
* ☐ Implement differential drive robot class&lt;br /&gt;
* ☐ Communicate with I2C sensors (read IMU)&lt;br /&gt;
* ☐ Use timers for non-blocking periodic tasks&lt;br /&gt;
* ☐ Write non-blocking code with time.ticks_ms()&lt;br /&gt;
* ☐ Design state machines for robot behaviors&lt;br /&gt;
* ☐ Use asyncio for concurrent tasks&lt;br /&gt;
* ☐ Implement encoder-based odometry&lt;br /&gt;
&lt;br /&gt;
If you can check most of these boxes, you can build advanced robot behaviors!&lt;br /&gt;
&lt;br /&gt;
== Next Steps ==&lt;br /&gt;
&lt;br /&gt;
=== Apply to Real Robots ===&lt;br /&gt;
* [[SimpleBot:Dead Reckoning Implementation]] - Encoder odometry&lt;br /&gt;
* [[SimpleBot:Line Following Implementation]] - State machine line follower&lt;br /&gt;
* Add IMU to SimpleBot for heading stabilization&lt;br /&gt;
&lt;br /&gt;
=== Learn Advanced Techniques ===&lt;br /&gt;
* [[State Machine Design]] - Hierarchical state machines, event-driven design&lt;br /&gt;
* [[Behavior:PID Control]] - Closed-loop control for speed and position&lt;br /&gt;
* [[ROS Basics]] - Distributed robotics framework&lt;br /&gt;
&lt;br /&gt;
=== Build New Capabilities ===&lt;br /&gt;
* [[Capability:Encoder Sensing]] - Precise wheel rotation measurement&lt;br /&gt;
* [[Capability:IMU Sensing]] - Orientation and acceleration&lt;br /&gt;
* [[Capability:Time-of-Flight Sensing]] - I2C distance measurement&lt;br /&gt;
&lt;br /&gt;
== Common Intermediate Mistakes ==&lt;br /&gt;
&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Modifying variables in interrupts without &amp;quot;global&amp;quot;&amp;#039;&amp;#039;&amp;#039; - Forgot global declaration&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Long interrupt handlers&amp;#039;&amp;#039;&amp;#039; - Causes missed interrupts and timing issues&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Ignoring PWM frequency effects&amp;#039;&amp;#039;&amp;#039; - Too low = audible whine, too high = inefficient&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Not debouncing switches&amp;#039;&amp;#039;&amp;#039; - Mechanical switches bounce, causing multiple counts&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Forgetting I2C pull-up resistors&amp;#039;&amp;#039;&amp;#039; - I2C needs 4.7kΩ pull-ups on SDA and SCL&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Race conditions in asyncio&amp;#039;&amp;#039;&amp;#039; - Shared variables need careful handling&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Integer overflow in encoder counts&amp;#039;&amp;#039;&amp;#039; - Use 32-bit integers or reset periodically&lt;br /&gt;
&lt;br /&gt;
== Debugging Advanced Programs ==&lt;br /&gt;
&lt;br /&gt;
=== Logic Analyzer for I2C/SPI ===&lt;br /&gt;
&lt;br /&gt;
Use a logic analyzer ($10-60) to visualize I2C communication:&lt;br /&gt;
* See SDA and SCL signals&lt;br /&gt;
* Decode I2C addresses and data&lt;br /&gt;
* Identify timing issues and protocol errors&lt;br /&gt;
&lt;br /&gt;
=== Oscilloscope for PWM ===&lt;br /&gt;
&lt;br /&gt;
Visualize PWM signals:&lt;br /&gt;
* Verify frequency and duty cycle&lt;br /&gt;
* Check for glitches or noise&lt;br /&gt;
* Measure rise/fall times&lt;br /&gt;
&lt;br /&gt;
=== Print Timing Analysis ===&lt;br /&gt;
&lt;br /&gt;
Measure execution time:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
start = time.ticks_us()&lt;br /&gt;
# Code to measure&lt;br /&gt;
expensive_function()&lt;br /&gt;
elapsed = time.ticks_diff(time.ticks_us(), start)&lt;br /&gt;
print(f&amp;quot;Execution time: {elapsed} µs&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Tools and Resources ==&lt;br /&gt;
&lt;br /&gt;
=== Hardware ===&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Logic analyzer&amp;#039;&amp;#039;&amp;#039; - $10-60 (Saleae-compatible clones work well)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Oscilloscope&amp;#039;&amp;#039;&amp;#039; - $100-400 (DSO150 or Rigol DS1054Z)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;I2C sensors&amp;#039;&amp;#039;&amp;#039; - MPU6050 IMU ($3), VL53L0X distance ($5)&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Motor driver&amp;#039;&amp;#039;&amp;#039; - TB6612FNG breakout board ($5-10)&lt;br /&gt;
&lt;br /&gt;
=== Software ===&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;MicroPython I2C library&amp;#039;&amp;#039;&amp;#039; - Built-in, no installation needed&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Pulseview&amp;#039;&amp;#039;&amp;#039; (Free) - Logic analyzer software&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;mpremote&amp;#039;&amp;#039;&amp;#039; (Free) - Command-line tool for MicroPython&lt;br /&gt;
&lt;br /&gt;
=== External Resources ===&lt;br /&gt;
* [https://docs.micropython.org/en/latest/library/machine.html MicroPython machine module]&lt;br /&gt;
* [https://docs.micropython.org/en/latest/library/asyncio.html MicroPython asyncio]&lt;br /&gt;
* [https://learn.sparkfun.com/tutorials/i2c SparkFun I2C Tutorial]&lt;br /&gt;
* [https://invensense.tdk.com/products/motion-tracking/6-axis/mpu-6050/ MPU6050 Datasheet]&lt;br /&gt;
&lt;br /&gt;
== See Also ==&lt;br /&gt;
&lt;br /&gt;
* [[Software]] - Full software competency overview&lt;br /&gt;
* [[MicroPython Basics]] - Prerequisites for this tutorial&lt;br /&gt;
* [[State Machine Design]] - Advanced behavior organization&lt;br /&gt;
* [[SimpleBot]] - Apply these techniques to a real robot&lt;br /&gt;
* [[Behavior:PID Control]] - Closed-loop motor control&lt;br /&gt;
&lt;br /&gt;
[[Category:Tutorials]]&lt;br /&gt;
[[Category:Software]]&lt;br /&gt;
[[Category:Intermediate]]&lt;/div&gt;</summary>
		<author><name>John</name></author>
	</entry>
</feed>