Bare-metal? Unit-tests? Why not!

Ok, so I’ll try to keep this brief. 

Some context

Unit testing is a good thing, and you should be doing it (don’t worry: that was just me talking to myself, since I miserably fail to follow my own advice). “Well, why?”, you might ask. Simply put, it’s a nice way of ensuring that your software modules/classes/thingies work in the way they should in isolation. Even though coming up with tests takes time, overall this practice tends reduces development complexity and headaches, forces you to write marginally better APIs and serves as a makeshift documentation for your code. 

Stolen from Reddit.

Good. When we’re talking about unit testing for desktop targets, framework options are plentiful. For the C/C++ world, three options pop in mind fairly quickly, but the list is gigantic. Same goes for most of the popular languages that kids use these days. 

However, in the embedded scene, the scenario is different. Computing power tends to be much more limited, and there are often interactions with external hardware that make fully isolated tests hard or unfeasible. If you’re running a *nix box, you can perhaps still profit from the one of the aforementioned frameworks (or try some tailor-made tool), but if you’re dealing with bare-metal targets, you’re pretty much out of luck. On her book, “Making Embedded Systems”, Elicia White skims over some testing practices for embedded systems, but the gist is that there’s no one-size-fits all approach. This thread pretty much echoes this feeling. 

My 2 cents

Getting to the point. As mentioned in another post, I’ve been working with a colleague on a firmware for a VFD. The firmware quickly racked quite a few software components, and keeping track of what worked and what didn’t was getting way to confusing. 

So, I drew some (*ahem*, a lot of) inspiration from Martin Dørum’s unit-test framework Snow – check it out! -, and started working on a bare-bones version of it for the STM32F10x I was using. The result is L_TEST, my attempt at an minimalist unit-test framework for bare-metal targets. The idea behind L_TEST is allowing the user to quickly define test routines and use basic assertion macros to ensure everything is working as it should. Test cases should look straightforward and clear. I really enjoy Dørum’s take on this, so L_TEST employs pretty much the same syntax as Snow does. Take a look at a simple example:

L_TEST uses printf, so you’ll need to implement the _write syscall. The output is color formatted, and should play nice with bash & friends:

Contrary to Dørum’s Snow, L_TEST is unfortunately not header-only, but the implementation is fairly light: 260 bytes for the aforementioned STM32F10x compiled with the -g flag, as shown in the map file below:

The addition of such functions also allowed the macros to be generally simpler, which in turn also reduces code footprint. Still a win, even though you have another source file to add to your list. Speaking of macros, the list still isn’t very plentiful, but here are a few:

  • L_TEST_MODULE(mname, mdesc): Define a test module, with a name and a string description. Test cases should be defined within its body
  • L_TEST_CASE(desc): Define a test case with a description. Test cases can only be defined withen a L_TEST_MODULE body.
  • L_TEST_ASSERT(val): Asserts that the value is non-zero. If the assertion fails, an error message is printed and the test case is aborted.
  • L_TEST_ASSERTEQ_INT(val, ref): Asserts that val == ref, assuming integer values. If the assertion fails, the expected and current values will be printed, and the test case will be aborted.
  • L_TEST_ASSERTEQ_FLT(val, ref): Asserts that the RPD between val and ref is less than 0.1%. If the assertion fails, the expected and current values will be printed, and the test case will be aborted.
  • L_TEST_ASSERTEQ_STR(val, ref): Executes a strcmp between the two supplied strings. If the assertion fails, the expected string and the supplied string are printed.

You can take a look at the full list here.

And thus, 

Unit-testing functionalities like this are just the tip of the iceberg. There’s a lot more that can be done using dummies, mocks, stubs and so on. L_TEST is not, by any stretch of the imagination, an attempt at a full-fledged test framework. However, it did suit my project’s simple needs, so it might suit someone else’s.  You can take a look at L_TEST on GitHub; have your go with it, and let me if you find (or better, when you find) any bugs/issues. Suggestions are welcome! 

’til next time. 

 

AS5030 magnetic encoder: capturing a PWM signal with an ATSAMD21

It seems that I can’t avoid periodically ostracizing this page. Welp, let’s try to make it up.

Context: the AS5030 magnetic encoder IC

In a project I’m currently working on (more about it in later posts, perhaps), I needed a halfway decent way of measuring the angular displacement of a small, manually-turnt wheel. I had originally mounted a small mechanical encoder (Bourns PEC11R) into my solution, which yielded about 96 pulses per-revolution (PPR). However, on a long run, contacting encoders are not the best pick for these applications due to mechanical wear.

Searching for a better solution I came across ams’ portfolio of magnetic encoders, which features interesting solutions for contactless position measurement. Amongst them, the AS5030 (datasheet) was the one that best met my requirements, providing an improved 256 PPR. The gist of it is simple: mount the IC, power it up and spin a magnet on top of it. The IC will generate a PWM signal with a pulse width proportional to the magnet’s angle in relation to the chip. Spoiler alert: not any magnet will work! You’ll have to get diametric magnets, such as this little guy here.

AS5030 overview: the hall frontend of the chip measures the orientation of the field lines of a diametric magnet and produces both PWM and analog signals accordingly. A SPI interface is also available.

As you may see in the diagram above, the AS5030 also provides a serial interface. The thing, however, is a weird half-duplex SPI that operates on 21-bit long frames, making it odd to use. On top of that, the AS5030 is strictly 5V compatible, which means I would have to level-shift all the signals going to my 3v3 microcontroller. That would just add unnecessary parts and complexity, so I ditched the serial interface and went with PWM (and a resistive divider to shift the voltage for the uC).

The PWM signal frequency is rated at 1.72kHz, but may vary slightly with temperature. The duty cycle encodes the position, going from a spec’d minimum of 2.26us to a spec’d maximum of 578.56us, like shown in the picture below:

AS5030 PWM signal specifictaion.

Getting the angular position accurately can be done via the ratio of the duty cycle (t_{\text{on}}) and the PWM’s period (t_{\text{on}} + t_{\text{off}}), as per the equation supplied in the datasheet:

\text{angle}[{^\circ}] = \frac{360}{256}\large[\large(257\frac{t_{on}}{t_{\text{on}}+t_{\text{off}}}\large)-1\large]

 ATSAMD21: peripheral bureaucracy

Measuring the pulse-width of a PWM signal is a textbook example of input capture. Capture operations allow you to record a signal edge together with a timestamp directly via hardware, without having to employ any CPU cycles. The vast majority of modern microcontrollers support this feature, and the ATSAMD21E18A employed in this project is no exception.

As we see in the ATSAMD21 datasheet (section 31.2), the TCC peripheral not only supports this sort of operation, but also has a dedicated mode for pulse-width capture, where “period T will be captured into CC0 and the pulse-width t_p into CC1″. Unfortunately, there’s a catch. Usually, capture operations occur entirely within a timer peripheral, which detects the signal’s edge on a dedicated pin an stores the timestamp based on its internal counter. However, to increase flexibility, capture operations in the ATSAMD21 use the External Interrupt (EIC) peripheral to generate an event in the internal Event System (EVSYS), which gets relayed via the internal event channels to the Timer/Counter (TCC) peripheral, in turn triggering the capture. Wait, what? Let me sketch that out:

Signal flow on ATSAMD21 capture operation: the PWM signal is routed from the GPIO pin to the EIC peripheral, generating an EVENT (IRQ-esque feature) on high logic. The event edges get relayed via an EVSYS channel, where the EIC acts as a generator, and the TCC timer peripheral is the event user.

This certainly allows for a lot more flexibility, not tying the capture operation to a particular pin, since all pins have EIC Interrupt/Event capability. However, configuring it is quite a mouthful (and quite poorly documented, BTW).

Talk is chip, show me the encode

I suck at puns. Ok, so let’s get a bloatw…I mean, ASF-free setup going. In my particular example, I’ll be using pin PA11, tied to EIC channel 4, but again, any pin can be used. Also, I’ll be using EVSYS’s channel 0, but you can use any of the available channels for any event. This will be done in four steps:

We’ll first configure the timer. It’ll run off the main GCLK (in this case, at 48MHz), counting up from 0 until 0xFFFFFFFF (then wrapping around):

Then, let’s setup the EIC. Sense is set to HIGH, and a tiny helper function configures the channel. Notice how the EVCTRL bit is set, which generates the EIC event:

Let’s then configure the EVSYS: EIC acts as a event generator on channel 0, and TCC1 is the event’s user (akin to a listener):

Last but not least,  let’s configure the pin. The PINMUX_PA11A_EIC_EXTINT11 value should be defined in the samd21.h header:

That’s it! Your device is now running PWM pulse-width capture on pin PA11, with no CPU cycles being used for it at all.

Now what?

Now that everything’s configured, … Well, make sure the PWM signal is getting to the designated pin. The signal’s pulse-width and period are now constantly being fetched and saved into the CC0 and CC1 registers, respectively. To wrap it up with the AS5030, we can now compute the measured angle with the following function:

This function returns angles x10, (i.e. 15.4° = 154), so that no float support needs to be added (which can eat a lot of Flash on smaller devices). It’s also enough to properly deal with the AS5030’s 1.4° resolution.

Best part? It actually works:

Rotating a magnet in front of the AS5030. Very professional test setup.

You can optionaly use the TCC_INTFLAG_MC1 interruption if you don’t want to poll the registers for changes. Yeah.

’til next time.