11 KiB

Enhanced MicroPython for Jumperless V5

This document describes the enhanced MicroPython implementation for Jumperless V5 that adds:

  1. Runtime Module Import - Import .py and .mpy modules from the filesystem
  2. Machine Module Support - Standard MicroPython machine classes integrated with Jumperless hardware
  3. Filesystem Integration - Full filesystem access for scripts and modules
  4. Backward Compatibility - All existing Jumperless functions continue to work

New Features

🔧 Machine Module Support

The standard MicroPython machine module is now available with Jumperless-specific integration:

Pin Class

Control GPIO pins using node numbers or names:

from machine import Pin

# Using node numbers (breadboard nodes)
pin1 = Pin(1, Pin.OUT)       # Breadboard node 1
pin13 = Pin(13, Pin.OUT)     # Breadboard node 13

# Using GPIO numbers directly  
pin_gpio = Pin(26, Pin.IN)   # RP2350 GPIO 26

# Pin operations
pin1.on()                    # Set HIGH
pin1.off()                   # Set LOW
state = pin1.value()         # Read state (0 or 1)
pin1.value(1)                # Set state

# Pin modes
pin_in = Pin(2, Pin.IN)                    # Input
pin_out = Pin(3, Pin.OUT, value=1)         # Output, initially HIGH
pin_pullup = Pin(4, Pin.IN, Pin.PULL_UP)   # Input with pullup

ADC Class

Read analog values from ADC channels:

from machine import ADC

# Create ADC on GPIO 26 (NANO_A0 equivalent)
adc0 = ADC(26)              # GPIO 26 = ADC channel 0
adc1 = ADC(27)              # GPIO 27 = ADC channel 1  
adc2 = ADC(28)              # GPIO 28 = ADC channel 2
adc3 = ADC(29)              # GPIO 29 = ADC channel 3

# Read values
reading = adc0.read_u16()   # Returns 0-65535
voltage = (reading / 65535) * 3.3  # Convert to voltage

PWM Class (for DAC)

Generate PWM signals and control DAC outputs:

from machine import Pin, PWM

# Regular PWM on any pin
pwm = PWM(Pin(2))
pwm.duty_u16(32768)  # 50% duty cycle

# DAC control via PWM (TOP_RAIL and BOTTOM_RAIL)
dac_top = PWM(Pin(16))      # TOP_RAIL (DAC channel 2)
dac_bottom = PWM(Pin(17))   # BOTTOM_RAIL (DAC channel 3)

# Set voltage via duty cycle (-8V to +8V range)
# 0V = duty 32768, 3.3V = duty 46244, -3.3V = duty 19289
duty_3v3 = int(((3.3 + 8.0) / 16.0) * 65535)
dac_top.duty_u16(duty_3v3)  # Set TOP_RAIL to 3.3V

📁 Filesystem and Module Import

Module Search Paths

MicroPython now searches these directories for modules:

import sys
print(sys.path)
# Output:
# ['', '/python_scripts', '/python_scripts/lib', '/python_scripts/modules', '/lib', '/modules']

Creating Custom Modules

Create .py files in python_scripts/lib/ for automatic import:

File: python_scripts/lib/sensors.py

# Custom sensor library
from machine import ADC

def read_all_adcs():
    """Read all 4 ADC channels and return voltages"""
    voltages = []
    for channel in range(4):
        adc = ADC(26 + channel)
        reading = adc.read_u16()
        voltage = (reading / 65535) * 3.3
        voltages.append(voltage)
    return voltages

def temperature_from_adc(adc_channel, r_ref=10000):
    """Convert ADC reading to temperature (assuming thermistor)"""
    adc = ADC(26 + adc_channel)
    reading = adc.read_u16()
    voltage = (reading / 65535) * 3.3
    
    # Simple thermistor calculation (customize for your sensor)
    if voltage > 0:
        resistance = r_ref * (3.3 - voltage) / voltage
        # Steinhart-Hart equation (simplified)
        temp_k = 1.0 / (0.001129148 + 0.000234125 * log(resistance) + 0.0000000876741 * log(resistance)**3)
        temp_c = temp_k - 273.15
        return temp_c
    return 0.0

Usage in REPL:

import sensors

# Read all ADC channels
voltages = sensors.read_all_adcs()
print(f"ADC voltages: {voltages}")

# Get temperature from ADC 0
temp = sensors.temperature_from_adc(0)
print(f"Temperature: {temp:.1f}°C")

Importing .mpy Files

Compiled MicroPython bytecode files (.mpy) are also supported for faster loading:

  1. Compile Python to bytecode: mpy-cross your_module.py
  2. Copy your_module.mpy to python_scripts/lib/
  3. Import normally: import your_module

🔗 Backward Compatibility

All existing Jumperless functions continue to work exactly as before:

# Traditional Jumperless functions (still work)
connect(1, 30)
disconnect(1, 30)
dac_set(TOP_RAIL, 3.3)
voltage = adc_get(0)
gpio_set(13, 1)
is_connected(1, 30)

# Node constants still available
TOP_RAIL, BOTTOM_RAIL, DAC0, DAC1, etc.

# All function aliases work
set_dac(TOP_RAIL, 3.3)      # Same as dac_set()
get_adc(0)                  # Same as adc_get()

🎯 Combining Both Approaches

Mix traditional Jumperless functions with machine module:

from machine import Pin, ADC, PWM

# Set up connections using traditional functions
connect(TOP_RAIL, 1)       # Connect top rail to breadboard node 1
connect(5, 30)             # Connect breadboard nodes 5 and 30

# Control hardware using machine module
led = Pin(13, Pin.OUT)     # LED on breadboard node 13
sensor = ADC(26)           # Sensor on ADC channel 0
dac = PWM(Pin(16))         # DAC control for top rail

# Main loop
while True:
    # Read sensor
    reading = sensor.read_u16()
    voltage = (reading / 65535) * 3.3
    
    # Control LED based on sensor
    if voltage > 1.5:
        led.on()
    else:
        led.off()
    
    # Set DAC output based on sensor
    dac_voltage = voltage * 2  # Scale up
    duty = int(((dac_voltage + 8.0) / 16.0) * 65535)
    dac.duty_u16(duty)
    
    time.sleep(0.1)

Pin Mapping Reference

Node to GPIO Mapping

Node Name Node # GPIO Function
1-30 1-30 0-31 Breadboard nodes
NANO_D0 60 0 UART RX
NANO_D1 61 1 UART TX
NANO_D2 62 2 Digital I/O
NANO_D3 63 3 Digital I/O, PWM
NANO_D13 73 13 Digital I/O, LED
NANO_A0 100 26 ADC0
NANO_A1 101 27 ADC1
NANO_A2 102 28 ADC2
NANO_A3 103 29 ADC3
NANO_A4 104 4 I2C SDA
NANO_A5 105 5 I2C SCL
TOP_RAIL 101 16 DAC channel 2
BOTTOM_RAIL 102 17 DAC channel 3
DAC0 106 16 DAC channel 2
DAC1 107 17 DAC channel 3

Communication Interfaces

Interface Pins GPIO
I2C0 SDA=4, SCL=5 Primary I2C
I2C1 SDA=22, SCL=23 Secondary I2C
SPI0 MOSI=23, MISO=20, SCK=22, CS=21 Primary SPI
SPI1 MOSI=27, MISO=24, SCK=26, CS=25 Secondary SPI
UART0 TX=0, RX=1 USB Serial
UART1 TX=24, RX=25 Hardware Serial

Example Projects

from machine import Pin
import time

led = Pin(13, Pin.OUT)

while True:
    led.on()
    time.sleep(0.5)
    led.off()
    time.sleep(0.5)

2. Analog Sensor Monitor

from machine import ADC
import time

sensor = ADC(26)  # ADC0

while True:
    reading = sensor.read_u16()
    voltage = (reading / 65535) * 3.3
    print(f"Sensor: {voltage:.3f}V")
    time.sleep(1)

3. Voltage Controller

from machine import Pin, ADC, PWM
import time

# Set up hardware
sensor = ADC(26)           # Input sensor
output_dac = PWM(Pin(16))  # TOP_RAIL DAC
led = Pin(13, Pin.OUT)     # Status LED

target_voltage = 2.0  # Target 2V output

while True:
    # Read input
    reading = sensor.read_u16()
    input_voltage = (reading / 65535) * 3.3
    
    # Calculate output (simple proportional control)
    error = target_voltage - input_voltage
    output_voltage = target_voltage + (error * 0.5)  # Simple P controller
    
    # Clamp output to valid range (-8V to +8V)
    output_voltage = max(-8, min(8, output_voltage))
    
    # Set DAC output
    duty = int(((output_voltage + 8.0) / 16.0) * 65535)
    output_dac.duty_u16(duty)
    
    # Status indicator
    if abs(error) < 0.1:
        led.on()   # Close to target
    else:
        led.off()  # Adjusting
    
    print(f"In: {input_voltage:.2f}V, Out: {output_voltage:.2f}V, Error: {error:.2f}V")
    time.sleep(0.1)

Development Tips

1. Module Development

  • Place reusable code in python_scripts/lib/
  • Use descriptive module names
  • Include docstrings and comments
  • Test modules in REPL before using in scripts

2. Debugging

# Enable verbose output
import sys
print(f"Python version: {sys.version}")
print(f"Module paths: {sys.path}")

# Check available modules
import os
print("Available modules:")
for path in sys.path:
    try:
        files = os.listdir(path)
        print(f"  {path}: {files}")
    except:
        print(f"  {path}: (not accessible)")

3. Performance

  • Use .mpy files for large modules (faster loading)
  • Avoid complex imports in tight loops
  • Cache frequently-used objects

4. Memory Management

import gc

# Check memory usage
print(f"Free memory: {gc.mem_free()} bytes")
print(f"Allocated memory: {gc.mem_alloc()} bytes")

# Force garbage collection
gc.collect()

Migration Guide

From Legacy Jumperless Code

  1. Keep existing code - it will continue to work
  2. Gradually adopt machine module for new features
  3. Create modules for commonly-used functions
  4. Mix approaches as needed

Example Migration

Before:

# Legacy approach
gpio_set(13, 1)
voltage = adc_get(0)
dac_set(TOP_RAIL, voltage)

After (mixed approach):

# Mixed approach - use what's most convenient
from machine import Pin, ADC, PWM

led = Pin(13, Pin.OUT)
sensor = ADC(26)
# Still use legacy DAC function (simpler for this case)
voltage = sensor.read_u16() / 65535 * 3.3
led.on() if voltage > 1.5 else led.off()
dac_set(TOP_RAIL, voltage)  # Legacy function still works

Troubleshooting

Common Issues

1. Module not found

ImportError: No module named 'my_module'
  • Check file is in python_scripts/lib/
  • Verify filename matches import name
  • Check for typos

2. Machine module not available

ImportError: No module named 'machine'
  • Ensure you're using enhanced MicroPython build
  • Check board configuration includes machine module

3. Pin name not recognized

ValueError: Unknown pin name
  • Use GPIO numbers instead of names in current implementation
  • Refer to pin mapping table above

4. Out of memory

MemoryError: memory allocation failed
  • Use gc.collect() to free memory
  • Reduce module imports
  • Use .mpy files instead of .py

Getting Help

  1. Check examples - Run simple_machine_example.py
  2. Use help() - help(Pin), help(ADC), etc.
  3. Check memory - gc.mem_free()
  4. Verify paths - print(sys.path)

Technical Implementation

Files Modified/Added

  • lib/micropython/boards/JUMPERLESS_V5/mpconfigboard.h - Board configuration
  • lib/micropython/boards/JUMPERLESS_V5/pins.csv - Pin definitions
  • lib/micropython/port/mpconfigport.h - Port configuration
  • lib/micropython/port/machine_jumperless.c - Machine module implementation
  • src/Python_Proper.cpp - Filesystem and path setup

Build System Integration

  • Machine module automatically registered via MP_REGISTER_MODULE
  • Board definitions include peripheral mappings
  • Filesystem mounted and paths configured at startup
  • QSTR definitions auto-generated during build

This enhanced MicroPython implementation provides a powerful, flexible platform for Jumperless development while maintaining complete backward compatibility with existing code.