Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / ASM

HWA: Hardware Abstraction for Microcontrollers

4.62/5 (9 votes)
26 Aug 2015CPOL5 min read 18.6K  
This article introduces a hardware abstraction tool for microcontroller programming.

Introduction

Microcontroller programming basically involves writing bits into hardware registers to operate the peripheral controllers a device embeds.

Although numerical constants are usually replaced by more self-speaking symbols, this way of doing things does not address the major issue that makes the porting of the code, or even just the swapping of peripheral controllers, painful: the mapping of the registers is rarely consistent between different devices, even of the same vendor, and sometimes even between different controllers of the same device.

In the last few years, more elaborate hardware programming interfaces have become available. For example, just to name three: the Atmel Software Framework (ASF), the Cortex Microcontroller Software Interface Standard (CMSIS), or the free software libopencm3. I have not tried these tools much, just enough to conclude that they will not satisfy me as they remain too close to the registers and are too verbose.

As to the "Arduino language", if it seems to give a consistent abstracted interface to the hardware, this is at the cost of reduced performances, a (much) larger program memory consumption, and requires a C++ compiler.

These issues have inspired me to try to create a hardware programming tool with the following goals in mind:

  • Provide a consistent interface for the most various hardware possible
  • Help write concise and easy to read code
  • Lead to a binary code free of any penalty
  • Require no external program, only a standard C programming toolchain

HWA achieves these goals by providing the programmer with a generic interface to the hardware made of a small set of generic instructions designed to be applied on various types of objects that represent the peripheral controllers embedded in the target device. This is what is called ad hoc polymorphism.

The key difference with other hardware abstraction tools is that HWA is not a library: it implements the ad hoc polymorphism mechanism using only standard C99 variadic macros, C structures and C inlined functions that are discarded by the compiler's optimizers.

A First Example

Let's start with the famous "Hello World!" of the microcontroller programmer: making a LED blink. This is what is usually written (I borrowed this code somewhere on the web, for an Atmel AVR, I hope the author will not sue me!):

C++
#define F_CPU 1000000UL

#include <avr/io.h>
#include <util/delay.h>

int
main (void)
{
    DDRB |= _BV(DDB0); 
    
    while(1) 
    {
        PORTB ^= _BV(PB0);
        _delay_ms(500);
    }
}

Now the same, using HWA:

C++
//  The device is clocked by its internal RC oscillator.
//  Device configuration must be declared before loading HWA since it will use
//  it to compute the values of the fuse bytes and the system clock frequency (hw_syshz).
//  Note: HWA assumes factory defaults for undefined configuration parameters. That would
//  produce the same result in the present case, resulting in hw_syshz = 1,000,000 Hz.
//
#define HW_DEVICE_CLK_SRC               rc_8MHz

#define HW_DEVICE_CLK_PSC               8

//  Load HWA for this device.
//  Use the full device name so that HWA knows the packaging and pin numbers
//  can be used in addition to pin names.
//
#include <hwa/attiny85_20pu.h>

//  The pin at which the LED is connected
//
#define PIN_LED                         hw_pin_5  //  Is 'hw_pin_pb0' on ATtiny85 PU

int main ( )
{
   hw_config( PIN_LED, direction, output );

   while(1) {
      hw_toggle( PIN_LED );
      hw_delay_cycles( 0.5 * hw_syshz );  //  0.5 s delay
   }
}

No register name, no cryptic symbol. Do you prefer hw_toggle(PIN_LED) or PORTB ^= _BV(PB0) as in the original code that, by the way, should have been PINB = _BV(PB0) ?

A More Complex Example

Let's now make the LED blink using interrupts, and put the device in sleep mode between interrupts:

C++
//  Target device
//
#include <hwa/attiny85_20pu.h>

//  The pin at which the LED is connected
//
#define PIN_LED                 hw_pin_5

//  The blinking period (in seconds)
//
#define PERIOD                  0.5

/*  The counter and its clock prescaling factor
 */
#define COUNTER                 hw_counter0

#define CLKDIV                  64


/*  Service the counter overflow IRQ
 */
HW_ISR( COUNTER, overflow )
{
  static uint8_t n ;
  n++ ;
  if ( n >= (uint8_t)(0.5 + PERIOD / 0.001 / 2) ) {
    n = 0 ;
    hw_toggle( PIN_LED );
  }
}


int main ( )
{
  /*  Create a HWA context to collect the hardware configuration.
   *  Preload this context with RESET values.
   */
  hwa_begin_from_reset();

  /*  Configure the LED pin.
   */
  hwa_config( PIN_LED, direction, output );

  /*  Have the CPU enter idle mode when the 'sleep' instruction is executed.
   */
  hwa_config( hw_core0,
              sleep,      enabled,
              sleep_mode, idle );

  /*  Configure the counter to overflow every 0.001 s.
   *
   *  The compare unit `compare0` of the counter (OCRxA) is used to store the top value.
   *  Unless otherwise stated, the overflow will be automatically set to occur
   *  when the 'count' value equals the 'top' value in 'loop_up' counting mode.
   */
  hwa_config( COUNTER,
              clock,     prescaler_output(CLKDIV),
              countmode, loop_up,
              bottom,    0,
              top,       compare0
              );

  /*  Set the TOP value of the counter.
   */
  hwa_write( hw_rel(COUNTER, compare0), 0.001 * hw_syshz / CLKDIV );

  /*  Enable overflow IRQs.
   */
  hwa_turn_irq( COUNTER, overflow, on );

  /*  Write this configuration into the hardware.
   */
  hwa_commit();

  hw_enable_interrupts();

  /*  Keep the device in sleep mode between interrupts.
   */
  for(;;)
    hw_sleep();
}

This shows that:

  • hwa_config() is a generic instruction (the same is used to configure the counter, the device core, or the I/O pin) that accepts a variable length list of key/value pairs following the object name
  • HWA instructions and object names all begin with hw_ or hwa_
  • The code is really concise, without redundancy; no register, no complicated structure or symbol name is mentioned, the wanted result is described with simple words.

and what is not shown:

  • The exact same code compiles if you use another counter (hw_counter1, hw_counter2... provided that your device has it, of course);
  • The exact same code compiles for ATtinyX4, ATtinyX5 and ATmegaX8, the only devices HWA supports for the moment, and should compile for any other Atmel AVR that HWA will support in the future;
  • If not the same, a very similar code should compile for any other microcontroller that HWA will support in the future;
  • The binary code produced does not suffer any drawback.

The HWA Context

This second example has instructions that begin with hwa_. These instructions are equivalent to the ones that begin with hw_ but they do not act on the hardware immediately: they only store information in a hidden structure, called the HWA context, until a hwa_commit() instruction is met.

The hwa_commit() instruction computes all the information stored in the context to determine the values that must be written into the hardware registers (or it tells the programmer that the configuration he wants cannot be obtained) and writes them efficiently.

The use of the HWA context is necessary on the Atmel AVR devices to get the best possible binary code because it minimizes the accesses to the hardware registers that are shared by several controllers and because the configuration of one of the controllers is sometimes tied to the configuration of some of its relative peripherals. A typical example is the timer/counters. HWA implements these timer/counters through several objects:

  • One counting unit, whose clock source can be a system clock prescaler (that makes the counter become a timer)
  • Usually 2 compare units, each having one (or two) output pins
  • Possibly 1 capture unit, one dead time generator...

All these objects are said "relatives" since there exist a relationship between them and the configuration of one can have an impact on the configuration of the others. Typically, the values of the WGM bits of the counter are influenced by the configuration of the compare outputs.

The hw_rel() instruction gives the name of a relative object of another (or produces an error). This also helps writing code that makes peripheral swapping easier.

Status of the Project

HWA is hosted on Github.

Although HWA now supports three different Atmel AVR devices for which it can compile about 20 different examples, it should be considered as "work in progress" until it has enough users to test it and tell whether it can be declared "stable", at least concerning the Atmel AVR family.

Regarding the roadmap of HWA, as I really started it in 2010 as I tried to program a 32 bit microcontroller for the first time, the STM32F103 family is on the starting blocks for HWA support.

History

  • Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)