Getting Started with the SAM D21 Xplained Pro without ASF

Introduction

Atmel’s SAM D21 ARM Cortex-M0+ based microcontrollers offer low power and high performance, making them ideal for a wide range of home automation, consumer, metering, and industrial applications. The Atmel SAM D21 Xplained Pro evaluation board allows easy evaluation and prototyping with these chips. This page is intended to provide simple examples of programming this board using direct register access rather than using the Atmel Software Framework (ASF). For more information on the hardware and features, please visit the ATSAMD21 Xplained Pro page.

Background

ASF is an Application Program Interface (API) which provides a layer of abstraction to Atmel’s hardware to simplify the usage of their MCUs. APIs in general have many advantages, the most notable of which is allowing the programmer to write code quickly and easily. They also typically create portable code, come with many examples, and have a large community for support. ASF has all of these attributes while also being free to use and fully integrated into Atmel Studio. However, ASF is not the best solution for those who prefer low-level programming or those who are wishing to learn more about embedded systems. It is also not ideal for those trying to learn a specific programming language because most of the code written with an API will be a series of function calls and only the simplest keywords in the language will be used. What’s more, APIs in general don’t always provide every possible option the programmer might require. Programming with the registers directly ensures total access to every peripheral and their configurations. This can help improve a program’s efficiency as the API may not be providing the most optimized code behind the scenes and it is very likely there will be much unwanted code taking up memory space.

All of the following code was written in Atmel Studio 6.2 using a GCC C Executable Project. Note that the full name of the chip used on the SAM D21 Xplained Pro is the ATSAMD21J18A.

Where to Get Started

In order to start coding with the MCU registers directly, we need to know their addresses so they can be dereferenced and read from and/or written to. Luckily, Atmel’s chain of header files eventually leads to one of them specific to the MCU, i.e., samd21j18a.h . This acts as the root file for all predefined addresses, constants, data types, etc. that are needed to create a GCC Executable Project. This file can be found, along with all other header files used in the project, in the Dependencies folder via the Solution Explorer once the solution is built. From there, the header files can be studied to find the definitions needed for the project. For the most part, Atmel’s naming conventions are intuitive enough that once the definitions for one peripheral are learned, the others can easily be guessed. Combine that with Atmel Studio’s autocomplete capabilities, as shown in Figure 1, and finding the definition required becomes a cinch.

There are many duplicate file names in the Dependencies folder. This is because some files have the same name but are located in different directories. Simply read the file’s brief description in the header block to discern which file is open.

image
Figure 1: Atmel Studio providing suggestions of the complete register definition

Modifying the Registers

The board specific header file contains many important definitions, such as constants and function prototypes necessary for setting up interrupt service routines. However, it’s the #include directives we are interested in. The majority of these are broken into two sections. The instance files are mainly occupied by register definitions for each instance of a peripheral. The component files build cleaver nested structures that can be used to easily access whole registers or their individual bit fields. This section demonstrates both methods.

Register Definitions

The predefined, dereferenced register addresses provided in the instance files are the most straightforward to work with. As an example, we will turn on LED0. Because we are not using ASF, the documentation has to be read more closely. By first reading the SAM D21 Xplained Pro User Guide, we find that pin PB30 needs to be set as output with logic LOW. To accomplish this, we read the datasheet and find that in the PORT peripheral, bit 30 of the DIR register needs to be set and bit 30 of the OUT register needs to be cleared. Figure 2 shows the lines in the instance/port.h file that we are interested in.


Figure 2: Some of the defined registers in the instance/port.h file

Notice that there are two copies of each register. Those followed by ‘0’ are for pins on PORT A (e.g. PA15) and those followed by ‘1’ are for pins on PORT B (e.g. PB30). Therefore, our definitions of interest are REG_PORT_DIR1 and REG_PORT_OUT1. The code shown in figure 3 will illuminate LED0.

image
Figure 3: Illuminate LED0 example

While this code is effective, it will most certainly be difficult for others to understand what is going on. We can use the pio/samd21j18a.h file, which contains the Peripheral I/O definitions to make it much clearer. Figure 4 shows the rewritten code.

image
Figure 4: Illuminate LED0 examples utilizing pio/samd21j18a.h

While coding with these register definitions works well in most instances, there are a few where it becomes clumsy. In these cases, the structures laid out in the component files may be a better option.

While studying the registers in the datasheet, you will come across some that require a “single 32-bit write”. In these cases, be sure to assign values to the register using ‘=’ rather than ‘|=’. This can be a very difficult problem to debug as using ‘|=’ does not cause problems all of the time.

Nested Structures

As an example of when the nested structures in the component files may be preferred, we will write a program that turns LED0 on when the user button (SW0) is pressed and turns LED0 off when released. By again reading the Xplained Pro User Guide, we deduce PA15 needs to be configured as input with a pull-up resistor. To do this, we read the datasheet and find that in the PORT peripheral, bits 2 (PULLEN) and 1 (INEN) in the PINCFG15 register need to be set. The reason we aren’t using register definitions to do this is because the PINCFG15 register is not defined. Rather, Atmel defines the base registers PINCFG0 and PINCFG1 for ports A and B respectively. Using these may get confusing, so we will use the nested structures as shown in Figure 5.


Figure 5: Toggle LED0 example using register access

We see on line 11 that “PORT” is our structure pointer. It, as well as the structure pointers for the rest of the peripherals, are defined in the root samd21j18a.h file. From there, we see the nested structures in action. We start by selecting Group[0] (i.e. Port A) and proceed to chose PINCFG[15] (i.e. PINCFG15). The final .reg means that we are gaining access to the whole register. Another option is .bit, as demonstrated below in Figure 6.


Figure 6: Toggle LED0 example using bit access

Notice that when utilizing bit access, we subsequently have to choose the desired bit field. Also worth noting is the use of the OUTSET and OUTCLR registers, as they make our code more efficient by avoiding a read operation.

Yet another option for modifying the PINCFG and PMUX registers is the WRCONFIG register. It has the advantage of being the most efficient method of configuring the PORT peripheral. However, it is also the most confusing method both for the programmer and anyone else trying to read their code. To learn more about the WRCONFIG register, see page 400 of the datasheet.

Interrupt Service Routines

Another question one may have is how enable interrupts and set up their corresponding Interrupt Service Routine (ISR). To do this, we have to consider the Nested Vector Interrupt Controller (NVIC), which is a peripheral of the Cortex-M0+ Processor. Each of the SAM D21’s peripheral instances has an interrupt line connected to the NVIC. The core_cm0plus.h file provides the functions shown in Figure 7 allowing the programmer to enable/disable interrupts, set/clear their pending state, and prioritize them.

image
Figure 7: Available NVIC functions

As previously mentioned, all the interrupt handler functions are predefined in the samd21j18a.h file. However, there is only one function for each peripheral instance. This means that if one enables multiple interrupts for a particular peripheral, they must check the INTFLAG register in the ISR to see which interrupt was triggered. As a quick example, we will blink LED0 using two interrupts in the TC3 peripheral.

We start by initializing one of the Timer/Counter peripheral instances. For this example, most of the defaults will work fine. All that was changed is the clock setup, the prescaler, and the desired interrupts were enabled. The LED will be toggled every time the counter overflows, hence the overflow interrupt was enabled. Also, in order to demonstrate the use of multiple interrupts from the same peripheral, the error interrupt has been enabled as well. The complete function is shown below in Figure 8.


Figure 8: Initialization code for Timer/Counter 3

Since we have enabled individual interrupts in the TC3 peripheral, we now must enable the TC3 line in the NVIC. I prefer to place all similar NVIC functions in one function as shown in Figure 9.

image
Figure 9: Enable interrupts for peripheral instances.

Finally, we must write the function for the TC3 ISR. This function will be called every time one of the individually enabled interrupts is triggered. In order to determine which interrupt caused the exception, the INTFLAG register must be checked. In this simple example, if the overflow interrupt was triggered, the LED is toggled. If the error interrupt is triggered, a global error flag is set. In the case of these particular interrupts, once the interrupt has been serviced, the interrupt flag must be cleared by writing a one to it. Figure 10 shows the handler function in which we define the ISR.

When writing ISRs, the datasheet must be read to learn how the interrupt flag is cleared. Often times is must be done manually (as in our example) while other times is is done automatically depending on how the interrupt is serviced. Not clearing the interrupt flag leads to an infinite loop because every time the program leaves the ISR, another interrupt request will be generated.

image
Figure 10: ISR for TC3

Do not use the following method to clear an interrupt flag: “TC3->COUNT16.INTFLAG.bit.OVF = 1”. This method may be appealing, but it will cause problems if more than one interrupt is used per peripheral. Rather than clear the flag for the one interrupt, it will clear the flags of all interrupts that have been triggered. Be sure to use one of the register access methods as shown in Figure 10 rather than bit access.

The complete code for this example is provided below in Listing 1.

Listing 1: Blink LED0 example

#include "sam.h"
 
#define LED0 PORT_PB30;
 
void init_TC3();
void enable_interrupts();
 
// Global error flag for TC3
volatile uint8_t TC3_error = 0;
 
int main(void)
{
    SystemInit(); // Initialize the SAM system
    enable_interrupts();
    init_TC3();
     
    // Configure LED0 as output
    REG_PORT_DIRSET1 = LED0;
 
    while (1)
    {
         
    }
}
 
 
void init_TC3()
{
    /* Configure Timer/Counter 3 as a timer to blink LED0 */
    // Configure Clocks
    REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID_TCC2_TC3;
    REG_PM_APBCMASK |= PM_APBCMASK_TC3; // Enable TC3 bus clock
     
    // Configure TC3 (16 bit counter by default)
    REG_TC3_CTRLA |= TC_CTRLA_PRESCALER_DIV8;
     
    // Enable interrupts
    REG_TC3_INTENSET = TC_INTENSET_OVF | TC_INTENSET_ERR;
     
    // Enable TC3
    REG_TC3_CTRLA |= TC_CTRLA_ENABLE;
    while ( TC3->COUNT16.STATUS.bit.SYNCBUSY == 1 ){} // wait for TC3 to be enabled
}
 
 
void TC3_Handler()
{   
    // Overflow interrupt triggered
    if ( TC3->COUNT16.INTFLAG.bit.OVF == 1 )
    {
        REG_PORT_OUTTGL1 = LED0;
        REG_TC3_INTFLAG = TC_INTFLAG_OVF;
    }
     
    // Error interrupt triggered
    else if ( TC3->COUNT16.INTFLAG.bit.ERR == 1 )
    {
        TC3_error = 1;
        REG_TC3_INTFLAG = TC_INTFLAG_ERR;
    }
}
 
 
void enable_interrupts()
{
    NVIC_EnableIRQ( TC3_IRQn );
}

Conclusion

Clearly, coding with the registers directly requires more time and effort to obtain a working program. However, the main benefit is a much greater understanding of embedded systems and what is going on behind the scenes in ASF. This knowledge can lead to more efficient code, superior hardware selection for future projects, more flexibility as a designer, etc.