ATMEL's ARM programming without ASF
The ARM architecture is substantially different than AVR, but you still have access to any internal registers by operating on their address.

The AVR Way

With AVR, you would have set the direction of a pin using the DDRx register, which was defined for you as long as you included "io.h" and defined your MCU name prior to this inclusion (or passed it as a compiler option).

Behind the scenes, "io.h" then included whatever you needed for that MCU. For the ATmega328p, this would have been "iom328p.h" where you can find definitions for each register, such as...

#define DDRB _SFR_IO8(0x04)
#define DDB0 0
#define DDB1 1
#define DDB2 2
// ... and so on


If you keep digging, you'd find that the _SFR_IO8() macro (defined in "sfr_defs.h") adds an offset to this address, and that's it. You now have a nice macro DDRB which grants you access to the Data Direction Register of Port B.

// Set Pin B1 as an output
DDRB = _BV(PB1);


The (Atmel) ARM Way

Chips from numerous vendors contain ARM processor cores. The samd21 (product listing) is a family of chips from Atmel featuring an ARM Cortex-M0+ CPU. This same CPU could be in chips from Microchip (who now owns Atmel), TI, STMicro, etc. Each CPU will share some things like NVIC, SCB, and MTB, and each vendor can build around the core however they want.

In Atmel's case, they have a set of packs containing definitions, scripts, and startup code for each of their chips, similar to those used with AVR, but a lot more complicated. If you use Atmel Studio, it will automatically include the correct header files for your MCU; however, you can also download these packs and manually include what you need. Be warned, it's a maze of duplicated code, repeated file names, and oddly nested directories.

Once the correct root header is included (such as "samd21.h"), you again have access to the registers, but they are arranged a bit differently. Taking a look at the SAMD21 datasheet, we see that Pins are handled with the PORT peripheral. From the PORT root, registers such as DIR, OUT, and IN can be used to set the direction, set the output, or read the input. 

Navigating Atmel ARM Registers

So how do we access these registers? Remember, in C code, these registers are really just pointers to some address. Atmel provided two ways, the first uses a series of structures, unions, and enumerations:

// Set Pin A1 as an output
PORT-&gt;Group[0].DIR.reg = PORT_PA01;


You can also address individual bits in a register using the bitfields:

// Set Drive Strength to max on Pin A1
PORT-&gt;Group[0].PINCFG[1].bit.DRVSTR = 1;


If you don't like this method, you can still directly access the registers like this:

// Set Pin A1 as an output
REG_PORT_DIR0 = PORT_PA01;


Note 1 - just like a typical AVR chip, there are multiple PORTs. In the case of the SAMD21, there are ports A and B. Port A is designated as Group 0 while Port B is designated as Group 1; hence:

PORT-&gt;Group[0].DIR.reg = PORT_PA01;   // Port A: Group[0].DIR


or

REG_PORT_DIR0 = PORT_PA01;   // PortA : DIR0


Note 2 - Rather than set the DIR register directly, Atmel provides DIRSET, DIRCLR, and DIRTGL to allow immediate updates to this register without requiring the typical read-modify-write operations (|= , &amp;=, etc). Similar shortcuts are available for many peripheral registers.

Note 3 - All of this information is found in the datasheet, but it can be overwhelming. Even if you don't want to use ASF or Atmel Studio (I personally don't) it can still be very helpful to open a basic project with the chip you want to use to see examples of accessing the registers or configuring peripherals. Atmel Studio also has a nice auto-complete feature for the definitions, so if you start typing "PORT" it will pop up options for the next valid bit of statement. There's a lot of naming to remember here, so this is extremely valuable.

A Practical Example

The SAMD21 has peripherals known as SERCOM which can be configured as USART, I2C, or SPI. The samd21g18 has 6 of them! That's great, but the default way to access the registers is like this:

SERCOM0-&gt;USART.CTRLA.reg |= SERCOM_USART_CTRLA_ENABLE;
SERCOM1-&gt;SPI.CTRLA.reg |= SERCOM_SPI_CTRLA_ENABLE;


or

REG_SERCOM0_USART_CTRLA |= SERCOM_USART_CTRLA_ENABLE;
REG_SERCOM1_SPI.CTRLA |= SERCOM_SPI_CTRLA_ENABLE;


This is easy enough, but makes it difficult to create a generic library for using the SERCOM without using weird macros to assign a number [0,5] to the SERCOM you want to use.

To get around this, you can create an array of SERCOMs using a couple of defined values Atmel provided in the "samd21.h" headers.

Sercom * const SERCOM[SERCOM_INST_NUM] = SERCOM_INSTS;


Now, we can access the SERCOMs using generic code and functions which pass the SERCOM instance number as a variable:

static inline void
USART__enable (uint_fast8_t instance) {
   SERCOM[instance]-&gt;USART.CTRLA.reg |= SERCOM_USART_CTRLA_ENABLE;
}
// ...
USART__enable(0);
USART__enable(1);


Conclusion

To really make sense of these things (especially if you want to write a guide for others) you will need to spend some time reading the datasheet for a few Atmel ARM chips, reviewing the generated code in Atmel Studio using ASF, and deep diving through the component, pio, and instance header files included for a chip in the Atmel Packs.

Note 4 - Atmel's documentation is not complete or easy to initially understand. For example, did you know you can access the PORT peripheral directly for some actions such as setting or clearing the OUT bits by using the PORT_IOBUS? It's mentioned, but not really discussed in the datasheet. If you understand MCU bus architecture, you would have gotten this information from the block diagrams. Here's an example of why it matters:



The point of this example is to show how much faster the PORT_IOBUS operations take place than the PORT operations. There are no delays in the code, just consecutive calls to OUTTGL. Some things you can only find out by experimenting or learning from those who have already done so.

Here's a great online post about programming Atmel ARM without ASF.