Hi,
I’ve been going through and expanding the NRF51/52 HAL, and there are
a couple of things I’ve noticed that I think we should talk about.
I’ll provide a patch for this on the ADC HAL and then slowly work
through the rest, but I wanted to talk about the changes prior to
revising this.
But, first, I think it’s worth talking through ideally what the
layering _should_ look like here. I’ve found it’s often hard to
come to a common definition the differences between the BSP, HAL and
Drivers, and where the separation of concerns should be. In my view,
the layout should be:
+———————————————————————————+
| app |
+———————————————————————————+
| (n)drivers |
+———————————————————————————+
| HAL | BSP |
+—————————————+—————————————+
Where the role of the HAL is _solely_ to abstract MCU peripheral APIs,
in a low layer, simple abstraction.
The role of the BSP is to abstract board specific configurations.
The drivers is where you can have diversity, and interaction with the
rest of the system. ADC may not be the best example of this, but I’ve
been looking at it most closely at the moment.
In the case of an ADC, the HAL should be designed to be as portable _and
low layer_ as possible across the various MCUs that we support. Every
chip has a different set of peripherals for managing an ADC, but there
are a few modes that are quite common across chipsets:
- One shot
- Continuous
- Scan
Beyond that, you can implement continuous and scan pretty easily, on the
few chipsets that don’t have them.
Implementation of these can be fairly different across chipsets.
Sometimes you need to use the ADC + comparator, at other times the ADC
peripheral will do it for you. However, in most cases these modes can
be abstracted with minimal code. There is value in doing this across
processors, because it allows for portability at higher layers.
However, while the HAL is necessary, it is not sufficient, without
information that in most cases comes from the board. Taking the ADC
example here, how do you convert a reading from the ADC without knowing
the reference voltage? I’ll come back to this later.
The role of the BSP is to provide configuration and abstraction of board
level features. This includes things like memory and flash layout, as
well as some form of a pinmux, and abstraction of things like CPU
frequency, input voltage, etc.
Above both of these is the drivers API. While in the ADC HAL there is
only _one_ implementation, for drivers there should be multiple
implementations, and potentially complex helper functions to make it
easy to develop against. So, you might have a very simple/basic driver
that does blocking ADC reads and uses the HAL only. You might have a
more complex driver that can deal with both internal and external ADCs,
and has chip specific support for doing things like DMA’ing ADC reads
into a buffer and posting an event to a task every ’n’ samples. The
drivers are the ones would should register with the kernel’s power
management APIs, and manage turning on and off peripherals and external
chipsets, etc.
And the drivers need not be one-size fit all: it’s beneficial to have
complexity here in some cases, and simplicity in others, and some times
you can’t maintain the balance between the two. It’s the HAL’s
job to be one-size fit all (or as close as possible), so that we can
gain abstraction across the diverse of chip peripherals.
OK, on to changes that I think we should make to how things are done
now…
Right now, we don’t have a drivers interface, and the HAL is
dual-purposing both. The ADC is a good example of this, so I’ll focus
in on that API.
* hal_adc_init() takes a system device id, which is defined by the BSP.
* hal_adc_init() calls bsp_get_hal_adc(), which maps the system device
id to the correct HAL peripheral
* bsp_get_hal_adc() calls the specific MCU HAL ADC create function (e.g.
samd21_adc_create()), which creates and returns a hal_adc structure.
* the various hal functions are then implemented, right now these are:
— hal_adc_read(): blocking read of raw ADC value
- hal_adc_getrefmv(): Get the reference voltage of the ADC read
(using BSP functions)
There are some design decisions made here that I’d like to call out:
* The mapping from the “system device IDs” to peripheral IDs in the
BSP. This is to abstract the definition across boards of which ADCs to
map either to an internal peripheral or an external ADC
* The hal_adc_read() function blocks on read. This is for user
convenience, but does not represent how the underlying hardware
implements an ADC read.
* hal_adc_getrefmv() comes from the BSP, which defines these in
hal_bsp.c. The samd21 specific implementation of getrefmv() then maps
these to peripheral specific APIs.
My suggestions on how to change this API, and the general design
philosophy are as follows:
* A #define should be added HAL_ADC_MAX which indicates the number of
peripherals enabled on a given board
* hal_adc_init() should be renamed to hal_adc_config() and should
configure the peripheral provided, which abstracted (cross-platform)
values for reference voltage, resolution and gain.
* hal_adc_read() should be broken into a set of functions that more
appropriately map to underlying peripheral operation (e.g.
hal_adc_read_once(periph_num, callback)).
* There should be BSP APIs that provide necessary information. For
example, reference voltage will most likely come from the board
configuration, so bsp_get_board_voltage() should be provided, as a
generally implemented function. This function could then be passed into
hal_adc_config() as the reference voltage (as an example.)
* Power APIs will be added to the HAL (hal_adc_on(), hal_adc_suspend(),
hal_adc_off().)
* There should be a drivers interface that runs above this, added to the
base directory of mynewt-core. Mynewt-core should come with a base set
of drivers that can use this. The drivers APIs will be what maps
internal and external ADCs, provides blocking interfaces, etc.
* A default ADC driver will be developed, which uses the HAL to read
peripherals, and provides blocking functions. Over time this will be
evolved (or broken apart if too complex) to support external ADCs or
directly support more complex chip features.
* The driver will be responsible for managing power states.
* The mapping of SYSTEM ID -> specific ADC configuration is an exercise
that is left to the user. This can be done wherever the drivers are
created / initialized. In some cases this will be BSP, in other cases
it can be done directly by the application. This can be everything from
provided a generic sensor API that the drivers plug into, to providing a
factory function that returns the appropriate driver structure per
board.
OK, that was a long email. I’d like people’s thoughts on this —
so if you’ve made it this far, please chime in. :-)
Sterling |