Skip to content
BitKnitting edited this page Aug 13, 2018 · 22 revisions

I wrote a low-power module for CircuitPython so that I can put the Itsy Bitsy m0 Express into standby mode and then wake up through an external interrupt. My goal is to extend the battery life of a gardening device I am building. There are several steps I went through:

The lowpower module was coded as a way to learn how to write a CircuitPython module extension. I ended up implementing a sleep() function instead of thinking through implementing a Python class or how sleeping and then waking up through an interrupt or an alarm should fit within CircuitPython's module/class structure.

Resources

The Python Script

GitHub location of code.py
Our goal is to tell the m0 to go to sleep within a CircuitPython script. When this happens, the script stops executing. A pin chosen as the interrupt changes the state of the pin from LOW to HIGH. When this happens, the m0 wakes up and the script continues.

import board
from digitalio import DigitalInOut, Direction
import lowpower
import time
# put the m0 to sleep on pin D12
lowpower.sleep(board.D12)
# This part of the script runs after the D12 pin goes from LOW to HIGH
led = DigitalInOut(board.D13)
led.direction = Direction.OUTPUT
on = True
# Blink on for 200 ms 5 times.
for i in range(10):
    led.value = on
    time.sleep(.2)
    on = not on

The C code

GitHub location of lowpower.c
I break the C code into two aspects:

  • The module extension - this is the goop every CircuitPython module sets up to map the Python object model to C.
  • The sleep() function - the function that sets up the interrupt to wake up when the pin changes state from LOW to HIGH and puts the m0 into standby mode.

Debugging

make Command

make BOARD=itsybitsy_m0_express DEBUG=1

Debugging With Line Numbers

Adding (at the top):

#pragma GCC push_options
#pragma GCC optimize ("O0")  

and (at the bottom):

#pragma GCC pop_options

Turns off the C compiler optimizer. This way I can match up the source code lines with GDB.

.gdbinit

.gdbinit is a time saver when starting up GDB! Thanks to Dan Halbert (Adafruit Discord) for sharing his .gbinit file. I slightly modified and put in the directory where I make the binary.

define jlink
  target extended-remote :2331
end

define jload
  jlink
  mon reset
  load
  mon reset
end

jload

RGB Indicator

From Adafruit's documentation:

Status

  • steady GREEN: code.py (or code.txt, main.py, or main.txt) is running
  • pulsing GREEN: code.py (etc.) has finished or does not exist
  • YELLOW: Circuit Python is in safe mode: it crashed and restarted
  • WHITE: REPL is running
  • BLUE: Circuit Python is starting up

Error

Colors with multiple flashes following indicate a Python exception and then indicate the line number of the error. The color of the first flash indicates the type of error:

  • GREEN: IndentationError
  • CYAN: SyntaxError
  • WHITE: NameError
  • ORANGE: OSError
  • PURPLE: ValueError
  • YELLOW: other error
    These are followed by flashes indicating the line number, including place value. WHITE flashes are thousands' place, BLUE are hundreds' place, YELLOW are tens' place, and CYAN are one's place. So for example, an error on line 32 would flash YELLOW three times and then CYAN two times. Zeroes are indicated by an extra-long dark gap.

Finding a Word in a File

grep -r -C3 <word> .

Useful Shell commands

/usr/local/bin/chd.sh

cd {my path goop}/circuitpython/ports/atmel-samd

To run: . chd.sh

Pin Mappings

I found myself several times wanting to map between the circuitpython pins and the pins in the Atmel datasheet. I found the mappings in ..\circuitpython\ports\atmel-samd\boards\itsybitsy_m0_express\pins.c:

STATIC const mp_rom_map_elem_t board_global_dict_table[] = {
    { MP_ROM_QSTR(MP_QSTR_D0), MP_ROM_PTR(&pin_PA11) },
    { MP_ROM_QSTR(MP_QSTR_RX), MP_ROM_PTR(&pin_PA11) },

    { MP_ROM_QSTR(MP_QSTR_D1), MP_ROM_PTR(&pin_PA10) },
    { MP_ROM_QSTR(MP_QSTR_TX), MP_ROM_PTR(&pin_PA10) },

    { MP_ROM_QSTR(MP_QSTR_D2), MP_ROM_PTR(&pin_PA14) },
    { MP_ROM_QSTR(MP_QSTR_D3), MP_ROM_PTR(&pin_PA09) },
    { MP_ROM_QSTR(MP_QSTR_D4), MP_ROM_PTR(&pin_PA08) },
    { MP_ROM_QSTR(MP_QSTR_D5), MP_ROM_PTR(&pin_PA15) },
    { MP_ROM_QSTR(MP_QSTR_D6), MP_ROM_PTR(&pin_PA20) },
    { MP_ROM_QSTR(MP_QSTR_D7), MP_ROM_PTR(&pin_PA21) },
    { MP_ROM_QSTR(MP_QSTR_D8), MP_ROM_PTR(&pin_PA06) },
    { MP_ROM_QSTR(MP_QSTR_D9), MP_ROM_PTR(&pin_PA07) },
    { MP_ROM_QSTR(MP_QSTR_D10), MP_ROM_PTR(&pin_PA18) },
    { MP_ROM_QSTR(MP_QSTR_D11), MP_ROM_PTR(&pin_PA16) },
    { MP_ROM_QSTR(MP_QSTR_D12), MP_ROM_PTR(&pin_PA19) },

    { MP_ROM_QSTR(MP_QSTR_D13), MP_ROM_PTR(&pin_PA17) },
    { MP_ROM_QSTR(MP_QSTR_L), MP_ROM_PTR(&pin_PA17) },  // a.k.a D13

    { MP_ROM_QSTR(MP_QSTR_A0), MP_ROM_PTR(&pin_PA02) },
    { MP_ROM_QSTR(MP_QSTR_A1), MP_ROM_PTR(&pin_PB08) },
    { MP_ROM_QSTR(MP_QSTR_A2), MP_ROM_PTR(&pin_PB09) },
    { MP_ROM_QSTR(MP_QSTR_A3), MP_ROM_PTR(&pin_PA04) },
    { MP_ROM_QSTR(MP_QSTR_A4), MP_ROM_PTR(&pin_PA05) },
    { MP_ROM_QSTR(MP_QSTR_A5), MP_ROM_PTR(&pin_PB02) },

    { MP_ROM_QSTR(MP_QSTR_MOSI), MP_ROM_PTR(&pin_PB10) },
    { MP_ROM_QSTR(MP_QSTR_MISO), MP_ROM_PTR(&pin_PA12) },
    { MP_ROM_QSTR(MP_QSTR_SCK), MP_ROM_PTR(&pin_PB11) },

    { MP_ROM_QSTR(MP_QSTR_SCL), MP_ROM_PTR(&pin_PA23) },
    { MP_ROM_QSTR(MP_QSTR_SDA), MP_ROM_PTR(&pin_PA22) },

    { MP_ROM_QSTR(MP_QSTR_APA102_MOSI), MP_ROM_PTR(&pin_PA01) },
    { MP_ROM_QSTR(MP_QSTR_APA102_SCK), MP_ROM_PTR(&pin_PA00) },
    { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) },
    { MP_ROM_QSTR(MP_QSTR_SPI), MP_ROM_PTR(&board_spi_obj) },
    { MP_ROM_QSTR(MP_QSTR_UART), MP_ROM_PTR(&board_uart_obj) },
};
MP_DEFINE_CONST_DICT(board_module_globals, board_global_dict_table);  

Pin Data Structures

.../circuitpython/ports/atmel-samd/common-hal/digitalio/DigitalInOut.h:

typedef struct {
    mp_obj_base_t base;
    const mcu_pin_obj_t * pin;
    bool output;
    bool open_drain;
} digitalio_digitalinout_obj_t;

.../circuitpython/ports/atmel-samd/common-hal/microcontroller/Pin.h:
typedef struct {
    mp_obj_base_t base;
    qstr name;
    uint8_t pin;
    bool has_extint:1;
    uint8_t extint_channel:7;
    bool has_touch:1;
    uint8_t touch_y_line:7; // 0 - 15. Assumed to be Y channel.
    uint8_t adc_input[NUM_ADC_PER_PIN];
    pin_timer_t timer[NUM_TIMERS_PER_PIN];
    pin_sercom_t sercom[NUM_SERCOMS_PER_PIN];
} mcu_pin_obj_t;

The Module Extension

The core of the module extension:

/*
* lowpower module
*/
// Dictionary of globals
STATIC const mp_map_elem_t LowPower_modules_globals_table[] = {
    { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_lowpower) },
    { MP_OBJ_NEW_QSTR(MP_QSTR_sleep), (mp_obj_t)&sleep_obj },
};
STATIC MP_DEFINE_CONST_DICT(lowpower_module_globals, lowpower_module_globals_table);
// Define the module
const mp_obj_module_t lowpower_module = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t*)&lowpower_module_globals,
};  

The sleep() Function

STATIC void go_to_sleep() {

  __DSB(); // Complete any pending buffer writes.
  SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
  __WFI();
}


STATIC mp_obj_t  sleep(mp_obj_t pin_in) {
  // What other things should we do to lower current draw by peripherals?
  new_status_color(BLACK);
  // Get the pin that was passed in (e.g.: D12)
  assert_pin(pin_in,false);
  mcu_pin_obj_t *pin = MP_OBJ_TO_PTR(pin_in);
  assert_pin_free(pin);
  digitalio_digitalinout_obj_t int_pin;
  common_hal_digitalio_digitalinout_construct(&int_pin, pin);
  // Tell NVIC we'll be using an external interrupt.
  // We also gave it a priority of 0. We did this
  // because "most" examples did this (e.g.: rtc_zero)
  NVIC_DisableIRQ(EIC_IRQn);
  NVIC_ClearPendingIRQ(EIC_IRQn);
  NVIC_SetPriority(EIC_IRQn, 0);
  NVIC_EnableIRQ(EIC_IRQn);
  // Enable EIC
  EIC->CTRL.bit.ENABLE = 1;
  while (EIC->STATUS.bit.SYNCBUSY != 0) {}
  // Enable wakeup capability on pin
  uint8_t extint_channel = int_pin.pin->extint_channel;
  uint32_t extint_mask = 1 << extint_channel;
  EIC->WAKEUP.reg |= extint_mask;
  // Set the pin to LOW
  gpio_set_pin_pull_mode(int_pin.pin->pin,GPIO_PULL_DOWN);
  // Set the PMUX pin function to "EIC"
  gpio_set_pin_function(int_pin.pin->pin, GPIO_PIN_FUNCTION_A);
  // Configure the pin to trigger when pin goes from LOW to HIGH
  uint8_t config_index = extint_channel / 8;
  uint8_t position = (extint_channel % 8) * 4;
  // Reset sense mode
  EIC->CONFIG[config_index].reg &=~ (EIC_CONFIG_SENSE0_Msk << position);
  // Configure to trigger when 3.3V applied to pin.
  // Testing led me to use EIC_CONFIG_SENSE0_LOW_VAL.
  EIC->CONFIG[config_index].reg |= EIC_CONFIG_SENSE0_LOW_Val << position;
  // Enable the interrupt
  EIC->INTENSET.reg = EIC_INTENSET_EXTINT(extint_mask);

  // Safe and restful sleep...
  go_to_sleep();
  return mp_const_none;
}