[TUT][SOFT][C]Understanding ASF4 Atmel Studio 7 code Part1

1 post / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

 

I 'am going to unwind one basic USART function going through  the code line by line to try and understand the new Hardware Abstraction Layers and C code in general. I am no expert in fact I'm guessing a lot feel free to correct my mistakes there will be a lot. With the release of Atmel Studio 7 and ASF4 it looks like they have rewritten much of the code in an attempt to stream line and make the code run faster. To quote the ASF reference manual 

 

ASF4 has a set of fully hardware-abstracted interfaces as a part of the core architecture. These interfaces are use-case driven and supports only a subset of the functionality offered by the hardware. One hardware module is typically supported by multiple interfaces, and START is used to select which interfaces to be included in his project.

Providing common interfaces that are completely abstracted from the hardware makes them easier to use in middleware and software stacks since it is not necessary to add architecture specific code.

If you are interested in manipulating registers without using ASF see this excellent article on that subject https://eewiki.net/display/microcontroller/Getting+Started+with+the+SAM+D21+Xplained+Pro+without+ASF

​When you look at the files in a typical example project in Atmel Studio 7 you see three types. HAL or hardware abstraction layer files; HPL hardware proxy layer files and HRI hardware register interface files.The HRI functions are at root of the registers and you can manipulate the register bits directly through these functions. You will see them in this example.

   The HAL seem to use handles or "descriptors" to abstractly manipulate data and according the the ASF reference manual 

For some of the drivers, there is an I/O system on top of the HAL interface. The I/O system disconnects middleware from any driver dependency because the middleware only needs to rely on the interface that the I/O system provides. The I/O system has a write and a read method that are used to move data in/out of the underlying interface​ I think we will see those read and write methods here.

    When you start Atmel studio you will see the page below. Click on the third line "Open Atmel Start Configurator"

Adding USART0 make sure your Tx and Rx pins match the board your using

 

Export the project and drag and drop it into atmel studio. There you will find an examples folder and in the drivers_example.c file you will find the function

void USART_0_example(void)
{
 struct io_descriptor *io;
 usart_sync_get_io_descriptor(&USART_0, &io);
 usart_sync_enable(&USART_0);

 io_write(io, (uint8_t *)"Hello World!", 12);
}

copy and paste into your main.c file. The USART_0_example function consists of four lines of code that will write the string Hello World over the virtual com port showing up in the data visualizer or any other serial interface like putty. The interesting part is how involved these four lines of code become as you drill down lower.

The first line of code 

struct io_descriptor *io;

​creates a structure object or a pointer to a structure instance called "io" this structure looks like this

struct io_descriptor {
 io_write_t write; /*! The write function pointer. */
 io_read_t  read;  /*! The read function pointer. */
};

​remember anytime you see the word "descriptor" in this code it is a handle. What is a handle ? It is like an abstract label or nickname pointing to an object

in this case " io ". Remember asf is all about abstraction layers so with a handle the user doesn't have to worry what its pointing at or any details attached to

the label the user just has to know how to use it. The descriptor itself is given a descriptive name in that this is an input output object in this case it will be used to output or

write data to the USART. For simplicity we will just work with the write function and ignore the read function.

io_write_t 

typedef int32_t (*io_write_t)(struct io_descriptor *const io_descr, const uint8_t *const buf, const uint16_t length);

​is a typedef declaration this allows us to create our own identifiers like integer or char ... our own custom identifier in this case "io_write_t" is going to

hold all the parameters we need to transmit the "hello world" string over the USART like the buffer that holds the string "buf" how long the string is "length" and what handle

or descriptor "io_descr".

void USART_0_example(void)
{
 struct io_descriptor *io;
 usart_sync_get_io_descriptor(&USART_0, &io);
 usart_sync_enable(&USART_0);

 io_write(io, (uint8_t *)"Hello World!", 12);
}

To review what we covered so far  I will repost the original function above. So the first line created an object with the descriptor or handle name "io_descriptor" in the form of a structure that we will use later in the fourth line of code in the write function. You can see it as the first parameter in the write function.

    We will also use this "io"object in the second line of code the "usart_get_io_descriptor" function.

int32_t usart_sync_get_io_descriptor(struct usart_sync_descriptor *const descr, struct io_descriptor **io)
{
 ASSERT(descr && io);

 *io = &descr->io;
 return ERR_NONE;
}

This function returns a 32 bit integer it has two parameters a descriptor or handle "usart_sync_descriptor" which is a constant pointer so it cant be changed by anything and a double pointer to our "io" descriptor just covered. We will get to the double pointer in a minute. The next line in the function uses the ASSERT keyword which is a sanity check making sure there exists

both a "io" and a usart_sync_descriptor before anything else is done in the function.

  I will try to explain the double pointer but I barely understand it myself, here goes. In C when you pass something as a function parameter you are making a copy of

that parameter and so you cannot change the original value of that parameter just its copy. To change the original value you need to use a pointer so if you use a pointer

for the function parameter and you want to change or affect that pointer inside the function you need to use a double pointer or a pointer to a pointer. So the code line after

the ASSERT statement"

*io = &descr->io;

​wont work unless" io " is a double pointer.

Anyways this line of code sets the "io" pointer to point at the address of the "usart_sync_descriptor"("USART_0" in this case) "io" member. The usart_sync_descriptor structure looks like this

struct usart_sync_descriptor {
 struct io_descriptor      io;
 struct _usart_sync_device device;
};

The function returns an " ERR_NONE;" which is defined as 

#define ERR_NONE 0

in the error codes header file assuming the conditions of the "ASSERT" statement are met.

Reposting the original main function for clarity 

void USART_0_example(void)
{
 struct io_descriptor *io;
 usart_sync_get_io_descriptor(&USART_0, &io);
 usart_sync_enable(&USART_0);

 io_write(io, (uint8_t *)"Hello World!", 12);
}

​we have gone over the first two lines of code of the main function in general terms.The third line which is the "usart_sync_enable" function looks like this 

int32_t usart_sync_enable(struct usart_sync_descriptor *const descr)
{
 ASSERT(descr);
 _usart_sync_enable(&descr->device);

 return ERR_NONE;
}

The function returns a 32 bit integer (zero) if the "ASSERT" function finds an instance of the "usart_sync_descriptor" (USART_0 in this case)

and enables the usart interface through the second line of code in the function " _usart_sync_enable " as shown below 

void _usart_async_enable(struct _usart_async_device *const device)
{
 hri_sercomusart_set_CTRLA_ENABLE_bit(device->hw);
}

This function enables the SERCOM USART module through the  "hri_sercomusart_set_CTRLA_ENABLE_bit" function shown below which is just basically setting the CTRLA register ENABLE bit to one to turn the USART on. Note we are in the hardware register interface files and getting further away from the abstract layers and closer to the registers themselves. 

static inline void hri_sercomusart_set_CTRLA_ENABLE_bit(const void *const hw)
{
 SERCOM_CRITICAL_SECTION_ENTER();
 hri_sercomusart_wait_for_sync(hw, SERCOM_USART_SYNCBUSY_SWRST | SERCOM_USART_SYNCBUSY_ENABLE);
 ((Sercom *)hw)->USART.CTRLA.reg |= SERCOM_USART_CTRLA_ENABLE;
 SERCOM_CRITICAL_SECTION_LEAVE();
}

This is a static inline function with no return. The "static" refers to the scope of this function limiting it to its own translational unit in other

words this function cant be used in just any location in the main program code but is used locally. "Inline" tells the compiler to optimize this function

for performance reasons or make the code run faster. The compiler can choose to ignore this command although it often optimizes code with out being asked

anyways. Note the "void pointer" in the parameter of this function. A void pointer is allowed to point to any "type" variable  ie integer,char,float or a custom made "type" like "Sercom" in this case.

A void pointer does however need to be explicitly cast to dereference it. Note the explicit casting of this void pointer in the third line of this function 

((Sercom *)hw)->USART.CTRLA.reg |= SERCOM_USART_CTRLA_ENABLE;

We will return to this line of code shortly.

The first line of code inside this function is the SERCOM_CRITICAL_SECTION_ENTER() macro.  Notice there is also a matching SERCOM_CRITICAL_SECTION_LEAVE(); macro at the end of the function. This is an apparent use of memory barrier instructions where there is a critical need to implement instructions in a certain order without cpu or compiler optimizations. For example speculative reads ,out of order executions or data accesses by processor optimizations can cause undesirable program behavior. 

hri_sercomusart_wait_for_sync(hw, SERCOM_USART_SYNCBUSY_SWRST | SERCOM_USART_SYNCBUSY_ENABLE);
static inline void hri_sercomusart_wait_for_sync(const void *const hw, hri_sercomusart_syncbusy_reg_t reg)
{
 while (((Sercom *)hw)->USART.SYNCBUSY.reg & reg) {
 };
}

Note the above function also is using a void pointer pointing at the hardware instance of USART_0. Also note the explicit casting of Sercom. All peripherals are made up of one digital bus interface running off the main clock domain and one peripheral core running off the peripheral generic clock. Communications between these two clock domains needs to be synchronized. All registers in the bus interface are accessible without synchronization. Peripheral registers however need to be write synchronized.​ The following bits need to be synchronized when written to or read.  Software Reset bit in the CTRLA register (CTRLA.SWRST) and the enable bit in the CTRLA register (CTRLA.ENABLE). The user can poll the  STATUS.SYNCBUSY to check when synchronization is complete which is what this function is doing.

The next line writes a one to the CTRLA.ENABLE register bit which enables the USART

((Sercom *)hw)->USART.CTRLA.reg |= SERCOM_USART_CTRLA_ENABLE;

 Reposting the original function for clarity 

void USART_0_example(void)
{
 struct io_descriptor *io;
 usart_sync_get_io_descriptor(&USART_0, &io);
 usart_sync_enable(&USART_0);

 io_write(io, (uint8_t *)"Hello World!", 12);
}

We are now on the final line of the usart example function which is the write function 

io_write(io, (uint8_t *)"Hello World!", 12);

Here we will take the string of characters "Hello World" that are held in a buffer and transmit them out the virtual serial port to the data visualizer. We must remember that even though this is a write function it is an abstract function and that the actual transmission of each character from the buffer out the hardware instance Sercom3 is handled in the "usart_sync_write" function found in the hpl(hardware proxy layer) in a usart initialization function. The actual hardware transmission of the bytes out of the mcu happens on Sercom3. Lets take a look at that in part2. In the main function you can see the USART_0_example function we just covered. Above that function is the atmel_start_init() function that also containes a USART_0_init(); function if you right click on it a couple of times.

int main(void)
{
 /* Initializes MCU, drivers and middleware */
 atmel_start_init();
 USART_0_example();
 /* Replace with your application code */
 
 while (1) {
 }
}

 

 

int32_t io_write(struct io_descriptor *const io_descr, const uint8_t *const buf, const uint16_t length)
{
 ASSERT(io_descr && buf);
 return io_descr->write(io_descr, buf, length);
}

Here in the code above we have another sanity check in the first line checking to make sure we have a handle to the USART and a buffer (something to write!).The function returns the number of bytes written. The next line inserts the parameters of our handle "io" our buffer "Hello World" and the number of characters in our buffer into the descriptor structure.This ends the coverage of the USART_0 example function.See part2 for further analysis.

Last Edited: Sat. Aug 26, 2017 - 09:14 PM