ARCHIVE: (using XPlained Pro board ATOM) Wake up SamD21 Through an Interrupt - BitKnitting/wakey_circuitpython GitHub Wiki
Goal
Understand how to wake up a SamD21 from standby mode using an external interrupt:
- Project: Press a button to wake up the SamD21 from standby mode. We’ll use the SamD21 XPlained Pro.
- Software environment:
- C
- Atom
- GCC toolchain
- Atmel Start
- OpenOCD
By completing this exploration, we gain more context and insight into:
- What the SamD21 datasheet is trying to say about setting up this scenario through configuring registers. Clear as mud.
- How Atmel Start is used to jump-start programming an Atmel Chip - like the SamD21 - in C.
- Atmel’s recommended steps to wake up a SamD21 from standby mode through an interrupt.
Get Going with Atmel Start
I found the easiest way to start SamD21 C source was to use Atmel Start . Atmel Start is a Web User Interface that lets us point-n-click together a C project + files to compile/link into a .elf file (e.g.: Makefile for the gcc toolchain). The C project inserts the ASF4 APIs. From Atmel: ”…the Atmel Software Framework, provides a rich set of proven drivers and code modules developed by experts to reduce customer design-time. It simplifies the usage of microcontrollers by providing an abstraction to the hardware through drivers and high-value middlewares. ASF is a free and open-source code library designed to be used for evaluation, prototyping, design, and production phases. The ASF4 driver architecture is built of three layers; HAL (Hardware Abstraction Layer), HPL (Hardware Proxy Layer), and HRI (Hardware Register Interface).
Get Initial Code
I thought this video I made is the "best" way to walk us through using Atmel Start to get to our initial code.
Getting a project up and running with Atmel Start was a big “OH…THAT’S How it’s done!”
Configure External Interrupt
We need to configure:
- The pin that will generate the interrupt.
- The NVIC.
- The External Interrupt Controller (EIC).
- Our callback to run when the interrupt is fired.
Configure the GPIO Pin
In the video, I set up both the LED0 and BUTTON pins. Atmel Start puts these definitions in the atmel_start_pins.h
file:
#define BUTTON GPIO(GPIO_PORTA, 15)
#define LED0 GPIO(GPIO_PORTB, 30)
The code for configuring the Button is in driver_init.c
:
// Set pin direction to input
gpio_set_pin_direction(BUTTON, GPIO_DIRECTION_IN);
gpio_set_pin_pull_mode(BUTTON,
// <y> Pull configuration
// <id> pad_pull_config
// <GPIO_PULL_OFF"> Off
// <GPIO_PULL_UP"> Pull-up
// <GPIO_PULL_DOWN"> Pull-down
GPIO_PULL_UP);
gpio_set_pin_function(BUTTON, PINMUX_PA15A_EIC_EXTINT15);
Configure the NVIC
As noted in the SamD21 data sheet: An interrupt request will set the corresponding interrupt pending bit in the NVIC interrupt pending registers (SETPEND/CLRPEND bits in ISPR/ICPR). For the NVIC to activate the interrupt, it must be enabled in the NVIC interrupt enable register (SETENA/CLRENA bits in ISER/ICER).
We need to set up the NVIC to accept interrupt pending requests from the EIC peripheral. We see hpl_eic.c uses the CMSIS APIs to do this:
NVIC_DisableIRQ(EIC_IRQn);
NVIC_ClearPendingIRQ(EIC_IRQn);
NVIC_EnableIRQ(EIC_IRQn);
Configure the EIC
Registers
Here we configure the registers:
- WAKEUP
- CONFIGy
- INTENSET, INTENCLR
- INTFLAG
- CTRL
WAKEUP
I changed the goo that was auto-generated for wakup (_ext_irq_init() hpl_eic.c) to simplify/better understand:
/*
* Write a 1 in the EIC’s WAKEUP register in the bit for WAKEUPEN15.
* i.e.: Tell the EIC to wake up the SamD21 when the interrupt triggers and the
* SamD21 is in sleep mode.
*/
uint32_t extint_mask = 1 << PIN_PA15A_EIC_EXTINT15;
EIC->WAKEUP.reg |= extint_mask;
CONFIGy
The CONFIGy register tells the EIC what will trigger our callback. In our case, it will be a change to a LOW logic level. We could also use filtering although I chose not to. Because we are not using filtering and not edge triggering, the EIC does not need any clocks. As with the WAKEUP register, I modified _ext_irq_init() hpl_eic.c CONFIGy setting goo to:
/* Tell the EIC to trigger the interrupt on GPIO PA15/EXTINT[15] when the
* voltage level goes from HIGH to LOW. This is done by figuring out how to
* talk with the CONFIGn register(s).
*/
uint8_t config_index = PIN_PA15A_EIC_EXTINT15 / 8;
uint8_t position = (PIN_PA15A_EIC_EXTINT15 % 8) * 4;
EIC->CONFIG[config_index].reg &=~ (EIC_CONFIG_SENSE0_Msk << position);
EIC->CONFIG[config_index].reg |= EIC_CONFIG_SENSE0_LOW_Val << position;
INTENSET
Before we put the SamD21 into standby
mode, we must enable the interrupt (in our case the BUTTON).
21.6.6 of the SamD21 datasheet notes: "Each interrupt, except NMI, can be individually enabled by setting the corresponding bit in the Interrupt Enable Set register (INTENSET=1), and disabled by setting the corresponding bit in the Interrupt Enable Clear register (INTENCLR=1)."
In our main.c, we add a line to register our callback interrupt:
ext_irq_register(BUTTON,button_pressed);
This function is located in hal_eic.c. It calls _ext_irq_enable()
that is located in hpl_eic.c. In this function, the INTEN register bit is set. The CMSIS code for this would be
EIC->INTENSET.reg = 1 << PIN_PA15A_EIC_EXTINT15;
INTFLAG
The SamD21 datasheet (21.6.3) notes the INTFLAG for the interrupt is set when the interrupt condition is met...."In level-sensitive mode, when interrupt has been cleared, INTFLAG.EXTINT[x] will be set immediately if the EXTINTx pin still matches the interrupt condition." The EIC handler provided in hpl_eic.c handles reading and clearing the INTFLAG.
CTRL
The CTRL starts up the EIC. When setting these bits, the datasheet (21.6.9 Synchronization) notes:
The following bits are synchronized when written:
• Software Reset bit in the Control register (CTRL.SWRST)
• Enable bit in the Control register (CTRL.ENABLE)
I left the Atmel Start code alone that did this step in _ext_irq_init()
:
hri_eic_wait_for_sync(EIC);
if (hri_eic_get_CTRL_reg(EIC, EIC_CTRL_ENABLE)) {
hri_eic_write_CTRL_reg(EIC, 0);
hri_eic_wait_for_sync(EIC);
}
hri_eic_write_CTRL_reg(EIC, EIC_CTRL_SWRST);
hri_eic_wait_for_sync(EIC);
Other Changes
GCLK
driver_init.c (EXTERNAL_IRQ_0_INIT)
has a call to the function:
_gclk_enable_channel(EIC_GCLK_ID, CONF_GCLK_EIC_SRC);
I comment this out because of our scenario:
- Trigger on a level.
- Not using a Filter. Does not require clock configuration.
Configuring NMI and EVCTRL
We are not using NMI or events. I commented out (hpl_eic.c):
hri_eic_write_NMICTRL_reg(
EIC, (CONF_EIC_NMIFILTEN << EIC_NMICTRL_NMIFILTEN_Pos) | EIC_NMICTRL_NMISENSE(CONF_EIC_NMISENSE));
hri_eic_write_EVCTRL_reg(EIC,
(CONF_EIC_EXTINTEO0 << 0) | (CONF_EIC_EXTINTEO1 << 1) | (CONF_EIC_EXTINTEO2 << 2)
| (CONF_EIC_EXTINTEO3 << 3) | (CONF_EIC_EXTINTEO4 << 4) | (CONF_EIC_EXTINTEO5 << 5)
| (CONF_EIC_EXTINTEO6 << 6) | (CONF_EIC_EXTINTEO7 << 7) | (CONF_EIC_EXTINTEO8 << 8)
| (CONF_EIC_EXTINTEO9 << 9) | (CONF_EIC_EXTINTEO10 << 10) | (CONF_EIC_EXTINTEO11 << 11)
| (CONF_EIC_EXTINTEO12 << 12) | (CONF_EIC_EXTINTEO13 << 13)
| (CONF_EIC_EXTINTEO14 << 14) | (CONF_EIC_EXTINTEO15 << 15) | 0);
Configuring The Filter
We are not using a filter. I commented out (hpl_eic.c):
hri_eic_write_CONFIG_reg(EIC,
0,
(CONF_EIC_FILTEN0 << EIC_CONFIG_FILTEN0_Pos) | EIC_CONFIG_SENSE0(CONF_EIC_SENSE0)
| (CONF_EIC_FILTEN1 << EIC_CONFIG_FILTEN1_Pos) | EIC_CONFIG_SENSE1(CONF_EIC_SENSE1)
| (CONF_EIC_FILTEN2 << EIC_CONFIG_FILTEN2_Pos) | EIC_CONFIG_SENSE2(CONF_EIC_SENSE2)
| (CONF_EIC_FILTEN3 << EIC_CONFIG_FILTEN3_Pos) | EIC_CONFIG_SENSE3(CONF_EIC_SENSE3)
| (CONF_EIC_FILTEN4 << EIC_CONFIG_FILTEN4_Pos) | EIC_CONFIG_SENSE4(CONF_EIC_SENSE4)
| (CONF_EIC_FILTEN5 << EIC_CONFIG_FILTEN5_Pos) | EIC_CONFIG_SENSE5(CONF_EIC_SENSE5)
| (CONF_EIC_FILTEN6 << EIC_CONFIG_FILTEN6_Pos) | EIC_CONFIG_SENSE6(CONF_EIC_SENSE6)
| (CONF_EIC_FILTEN7 << EIC_CONFIG_FILTEN7_Pos) | EIC_CONFIG_SENSE7(CONF_EIC_SENSE7)
| 0);
hri_eic_write_CONFIG_reg(EIC,
1,
(CONF_EIC_FILTEN8 << EIC_CONFIG_FILTEN0_Pos) | EIC_CONFIG_SENSE0(CONF_EIC_SENSE8)
| (CONF_EIC_FILTEN9 << EIC_CONFIG_FILTEN1_Pos) | EIC_CONFIG_SENSE1(CONF_EIC_SENSE9)
| (CONF_EIC_FILTEN10 << EIC_CONFIG_FILTEN2_Pos) | EIC_CONFIG_SENSE2(CONF_EIC_SENSE10)
| (CONF_EIC_FILTEN11 << EIC_CONFIG_FILTEN3_Pos) | EIC_CONFIG_SENSE3(CONF_EIC_SENSE11)
| (CONF_EIC_FILTEN12 << EIC_CONFIG_FILTEN4_Pos) | EIC_CONFIG_SENSE4(CONF_EIC_SENSE12)
| (CONF_EIC_FILTEN13 << EIC_CONFIG_FILTEN5_Pos) | EIC_CONFIG_SENSE5(CONF_EIC_SENSE13)
| (CONF_EIC_FILTEN14 << EIC_CONFIG_FILTEN6_Pos) | EIC_CONFIG_SENSE6(CONF_EIC_SENSE14)
| (CONF_EIC_FILTEN15 << EIC_CONFIG_FILTEN7_Pos) | EIC_CONFIG_SENSE7(CONF_EIC_SENSE15)
| 0);
Go To Sleep
I added the function zzz()
in main.c and call it within the while(1) loop:
void zzz(void)
{
__DSB(); // Complete any pending buffer writes.
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
__WFI();
}
Compiling and Linking
I use the gcc toolchain. I wrote a python script - mm.py:
- Took off the .py extension (i.e.: script starts with #!).
- Copied to /usr/local/bin (i.e.: in PATH).
- Copied over a .gdbinit file that helps with gdb debugging. I then run it within the gcc folder where the .elf file is built. I think (OK, I hope) what is going on is self-explanatory.
Debugging
I open two terminal windows. One runs OpenOCD the other runs GDB.
OpenOCD Terminal Window
I run the OpenOCD from the path where atmel_samd21_xplained_pro.cfg is located.
openocd -f atmel_samd21_xplained_pro.cfg
GDB Terminal Window
I run GDB from the gcc path.
arm-none-eabi-gdb AtmelStart.elf
At This Point
Whew. I hope this is helpful. Please find many things to smile about.