Home
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:
- How to go about writing C source that configures the SamD21 such that a GPIO wakes up from standby mode during level detection (high or low).
- How to build a CircuitPython C-module.
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.
- samd21 datasheet
- The micropython documentation has an excellent section on writing a module. If you are following along, you'll want to understand/follow the steps outlined.
- Follow Dan Halbert's "Build CircuitPython" tutorial. This gets a fork of the CircuitPython source on our local machine.
- Note: I did this prior to v 3 was finalized. The build I used was tagged 3.0.0-rc.0.
- Debugging the SAMD21 with GDB
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
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.
make BOARD=itsybitsy_m0_express DEBUG=1
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 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
From Adafruit's documentation:
- 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
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.
grep -r -C3 <word> .
/usr/local/bin/chd.sh
cd {my path goop}/circuitpython/ports/atmel-samd
To run: . chd.sh
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);
.../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 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,
};
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;
}