0% found this document useful (0 votes)
149 views7 pages

Unit Testing The MSP430 Within A Desktop Environment: Kris - Dickie@clarius - Me

This document discusses building and running a system for unit testing MSP430 microcontroller code within a desktop environment. It describes a C++ based testing architecture that encapsulates hardware modules as objects. The tests instantiate objects and make calls to their public functions. It also covers working with registers by redefining macros to reference a templated class that notifies threads of register value changes.

Uploaded by

jkkars1
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
149 views7 pages

Unit Testing The MSP430 Within A Desktop Environment: Kris - Dickie@clarius - Me

This document discusses building and running a system for unit testing MSP430 microcontroller code within a desktop environment. It describes a C++ based testing architecture that encapsulates hardware modules as objects. The tests instantiate objects and make calls to their public functions. It also covers working with registers by redefining macros to reference a templated class that notifies threads of register value changes.

Uploaded by

jkkars1
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

Unit Testing the MSP430 Within a Desktop Environment

Kris Dickie – B.Tech


Clarius Mobile Health
[email protected]

I. Introduction designing a proper architecture from the beginning.

The MSP430™ microcontroller (MCU) is a low- The architecture described here is a C++ based
power device designed and produced by Texas system that tries to encapsulate hardware modules
Instruments (TI), and comes in a variety of models. within instantiated objects. Because of the
The purpose of this technical paper is to explain relatively static nature of embedded programs, all
how to build and run a system for unit testing object instances are declared up front and no
MSP430 MCU code within a desktop environment. dynamic memory allocation is used. This is often a
For embedded designs, emulators are often the best good approach to use within environments outside
choice for testing software, however there may be of desktop computing as resources are generally
restrictions such as inability to deploy emulators more optimized.
easily to test systems, cross-platform restrictions,
or the complete non-existence of an emulator. This The tests are designed to instantiate objects at any
paper describes the setup and gives examples of point within the test execution and make
how to write and execute MSP430 MCU code appropriate calls to the public member functions of
within a Linux or Windows desktop environment, each object.
and has been tested using Google’s test suite
Google Test (gtest). It’s worth mentioning that the
examples and framework described make use of
some C++11 and standard thread support library
built into C++, though most native embedded
develop prohibits such use.

II. Testing Architecture


Figure 1. Test Program Migration
The testing architecture will greatly depend on how
your MSP430 MCU code has been structured. For III. Working with Registers
native C applications, a series of function calls
may be required to determine an ultimate result, As the MSP430 MCU primary I/O functionality is
and in a more object-oriented model, a simple driven through registers, it is important that the
constructor initialization and call to one public testing architecture can properly handle both reads
function may yield the results desired. One of the and writes from registers. Since a register is just a
most important ideas in unit testing is to get as memory address, the primary goal of the program
much code coverage as possible, as to run every will be to write peristent values, readback values,
single line of code at least once, and often multiple and get notified when a value is changed.
times with varying inputs. In reality, this is more
difficult to implement, since certain scenarios may Each register is defined in a specific header related
be difficult to replicate at a testing level, however to the MSP430 model (the examples shown are
by thinking about this up-front in the design of a based on the MSP430F5438A MCU), and is either
program, one can help increase code coverage by 8, 16, 20, or 32 bits in size. A typical register

1 Version 1.0 - August 2016


definition would look as follows: return val_;
}
SFR_8BIT(ADC12CTL0_L); testReg& operator=(T val)
{
val_ = val;
The key for register definition in the testing notify_one();
program is to redefine the macros that initialize return *this;
each register; this can be achieved by referencing }
an externally defined type that encapsulates the testReg& operator|=(T val)
idea of a register. In this case, a template works {
val_ |= val;
nicely to capture the different register sizes. notify_one();
return *this;
#define SFR_8BIT(address) / }
extern testReg<unsigned char> address testReg& operator&=(T val)
{
#define SFR_16BIT(address) / val_ &= val;
extern testReg<unsigned int> address notify_one();
return *this;
typedef void (* __SFR_FARPTR)(); }
#define SFR_20BIT(address) / T* operator&()
extern __SFR_FARPTR address {
return &val_;
#define SFR_32BIT(address) / }
extern testReg<unsigned long> address operator T() { return val_; }

Now when including the msp430<model>.h header private:


T val_;
file for register and bitmask definitions, the macros
std::mutex lock_;
will have a proper definintion to reference. };

The testReg reference is a templated class which The testReg class creates two member variables,
allows the program to define registers of different val_ which holds the current register value
sizes (char, int, and long for example), and is (initialized to 0), and lock_ which is used for
derived from the C++ class thread synchronization when an outside value is
std::condition_variable which allows each
waiting for a register value to change.
register to individually notify any thread that is
waiting for it’s value to change. The class also makes use of operator overloading
to handle direct and bitwise assignments, when
The definition of all template classes need to be in these are used from within the MSP430 MCU
a header file in order for them to compile. program, a call to notify_one() will notify any
template <typename T>
thread waiting on that register value to change, so
class testReg : std::condition_variable that it may read back the value or continue it’s
{ execution.
public:
testReg() : val_(0) { } Example:
testReg(T val) : val_(val) { }
void thread1()
T wait() {
{ P1OUT = 0;
std::unique_lock<std::mutex> lk(lock_); // start thread 2
std::condition_variable::wait(lk);

2 Version 1.0 - August 2016


P1OUT.wait(); #include <msp430.h>
int val = P1OUT;
// value should be 1 void __data20_write_char(unsigned long
printf(“P1: %d”, val); addr, unsigned char v)
} {
*((unsigned long*)addr) = v;
void thread2() }
{
P1OUT |= 0x01; void __delay_cycles(unsigned long cycles)
} {
usleep(cycles);
There is one final step needed for a program to }
compile. Since we’ve defined each MSP430
register as an extern testReg, the actual definition In the above functions, we simply implement a
must be created, i.e. the header file is not good write to an address for __data20_write_char(), and
enough, and your program will yield undefined implement an actual wait time for
reference errors. This can be accomplished by __delay_cycles().
writing a simple python script to parse the MSP430
header file that includes the registers definitions One caveat that potentially needs to be addressed is
and create an implementation file (i.e. .cpp file) the use of defined types and address spaces. In the
that define each register. The script needs to parse example __data20_write_char(), unsigned long
out all SFR_XBIT definitions in the MSP430 will be defined as a 32-bit number on the MSP430
header file and create a file that will look similar to MCU, and will likely be on a desktop environment
the following: as well, as unsigned long long may represent a 64-
bit address. Now since __data20_write_char() is
#include <msp430.h> converting the input into an actual pointer,
dereferencing it, and stuffing a value, this could
testReg<unsigned char> ADC12CTL0_lL (0);
potentially lead to issues and crashes during testing
...
testReg<unsigned int> WDTCTL (0); on a 64-bit operating system, where the memory
space is beyond the range of unsigned long. One
Where each platform will have slightly different must remember that a call to __data20_write_char
register configurations. With this file generated, may yield an input address anywhere in memory
and included in the testing project, the registers space on the desktop.
now have proper definitions.
A solution to this problem is to define the
IV. Overriding Intrinsics __data20_write_char() function with a uint64_t
address type; this is easily accomplished for the
It may be important for the testing program to implementation, but the default definition in the
override specific MSP430 MCU intrinsic calls and intrinsics.h would also have to change in order for
is much simpler than trying to get native assembly compilation to succeed. Again, a python script can
to compile and run within a desktop environment. be used to update this definition, or creating a new
intrinsics.h file for testing can be performed as
The simplest approach is to simply create an well, since it’s contents is relatively small.
implementation file (i.e. .cpp file) that defines a
specified operation for the instrinsics that are used V. Flash Memory Testing
within the actual MSP430 MCU program that is
being tested. For example: Testing flash memory reads and writes may be
useful for an application, especially if critical data

3 Version 1.0 - August 2016


is stored within information banks. Depending on ensure the proper bits are set during an operation
the platform, the number of information banks will that would take place during standard MSP430
vary, however it will be important to define new MCU operation through the calls to the namespace
addresses as opposed to using BANKA, BANKB, mspfunc.
etc. which are MSP430 MCU specific. The
MSP430 program should invoke a method to set VI. Communication Buses
variable addresses at runtime, which are then
referenced for all reads and writes. The default Communication buses can sometimes be more
implementation would set addresses to the BANK challenging to test because of their reliance of an
definitions, and the test program would then be outside source generating an interrupt. One design
able to make a subsequent call to set addresses approach that can be used is to write classes with a
from a defined memory space that was setup on the public method to call the interrupt routine, or in the
desktop environment. case of a C program, a global function that can call
the interrupt directly. We can look at the SPI bus as
Since a flash memory write typically requires a starting point.
waiting for register bits (i.e. WAIT or BUSY) to be
set from the device, the test program must ensure class spibus
that these bits can be set when required, otherwise {
public:
the testing will hang. For most cases, writing a void testInterrupt() { onRxData(); }
thread which asserts the WAIT bit, and de-asserts
the BUSY of the register FCTL3 should do the private:
trick, once the LOCK bit is asserted on the same static __interrupt void onRxData();
char buffer[32];
register, the thread can exit. };

As an example: #pragma vector=USCI_A2_VECTOR


__interrupt void spibus::onRxData()
void setFlash() {
{ switch (__even_in_range(UCA2IV, 4)
while (!(FCTL3 & LOCK)) {
{ case 2:
FCTL3 &= BUSY; // fill buffer
FCTL3 |= WAIT; *buffer++ = UCA2RXBUF;
} // exit low power mode to allow
} // buffer to be processed
__bic_SR_register_on_exit(...);
void testFlash() break;
{ }
FCTL3 = 0; }
std::thread t([&]{ setFlash(); });
This now allows the interrupt to be called by an
mspfunc::writeFlash(address, data);
rd = mspfunc::readFlash(address);
outside testing thread.
ASSERT(rd == data);
To take the example further, a thread can now be
t.join(); used to virtually send data through the SPI bus,
} while the test program waits for a buffer to be
filled.
The exact implemenation may differ somewhat,
but the example is meant to show how a thread can spibuf spi;

4 Version 1.0 - August 2016


void sendData(std::vector<char>& buf) std::thread t1([&]{ monitor(1, tx); });
{ std::thread t2([&]{ sendData(rx); });
UCA2IV = 2;
for (auto i = 0u; < buf.size(); i++) // assume the parsing function will
{ // respond internally by sending the
UCA2RXBUF = buf[i]; // battery level as 1 byte over SPI
spi.testInterrupt(); while (spi.parsing());
}
} t1.join();
t2.join();
std::vector<char> buf = “test”;
std::thread t([&]{ sendData(buf); }); ASSERT(tx == 100);

while (!spi.finishedParsingBuffer());
Using similar methods can be used to test the I2C
t.join(); or UART bus, as well as ADCs. Each will have
their own nuances that need to be addressed, for
ASSERT(spi.buffer == buf); instance, I2C tests will likely have to deal with
setting of start and stop flags, and possibly handle
This is just one potential method to try and NACKs sent as an interrupt.
synchronize the SPI read, another could be to
actually wait for the receive buffer to receive a VII. Simulating Timers
value, and call a separate parsing function similar
to the following: As with communication buses, timers can also be
somewhat difficult to test because of their reliance
int count = 0;
while (count++ < buf.size());
on a temporal interrupt.
{
UCA2RXBUF.wait(); There are many C++ constructs for timers, but one
spi.parseNewData(); of the best for high-resolution timing lies within
} the boost::asio libraries, specifically the
deadline_timer.
To test the full response chain on a SPI bus, with
an object waiting for messages, parsing them, and To setup a system that will generate timer
then transmitting them back. For instance if there interrupts, a deadline_timer can be instantiated
was a query for the battery level of a device, we with a specific interval that is based on a
can use the above examples to implement the capture/compare register. An example for
receive side to respond to the request, and then Timer1_A3 running, capture/control register 0.
monitor the SPI transmissions in a separate thread.
class msptimer
void monitor(int sz, std::vector<char>& {
buf) public:
{ void testInterrupt() { onTick(); }
while (buf.size() < sz)
{ void init(int frequency)
UCA2TXBUF.wait(); {
buf.push_back(UCA2TXBUF); // perform setup
} TA1CCR0 = (ACLKFREQ / frequency) + 1;
} }

std::vector<char> tx; void start()


std::vector<char> rx = “battery”; {
spi.setBatteryLevel(100); TA1CTL |= MC__UP;

5 Version 1.0 - August 2016


} if (TA1CTL & MC_3)
void stop() {
{ tmr->expires_at(tmr->expires_at() +
TA1CTL &= ~MC__UPDOWN; ival);
TA1R = 0; tmr->async_wait(...);
} }
}
private:
static __interrupt void onTick(); // create timer running at 1kHz
int ticks; msptimer t;
}; t.init(1000);

#pragma vector=USCI_A2_VECTOR // setup the deadline_timer


__interrupt void msptimer::onTick() std::thread t([&]{ setup(t); });
{
tick++; // run timer, and wait for 1 second
} t.start();
sleep(1);
There is now a class that can run a timer with the t.stop();
implementation details left to be determined,
ASSERT(t.ticks ≈ 1000);
except that the interrupt will increment a counter.
To setup the test system that will actually call the
The assertion of the number of ticks elapsed will
interrupt, the following deadline_timer setup can
be approximate, as the sleep() function and setup
be used.
times are not necessarily exact, but the high
void setup(const msptimer& t) resolution of a boost deadline_timer should make
{ for a relatively precise way of measuring time if
// setup the timer based on the the capture/control registers are used for setting up
// initialization of the the timer interval.
// control/capture register
int val =
(TA1CCR0 / ACLKFREQ) * 1000000; VIII. Thread Synchronizatoin
boost::asio::io_service ios;
boost:posix_time::microseconds One important concept that has been left out of the
ival(val); previous examples, mostly for brevity, is the use of
boost::asio::deadline_timer
tmr(ios, iva);
thread synchronization to ensure that the test
program does not hang and that register values get
// wait for the msp timer to start set at the appropriate times.
TA1CTL.wait();
TA1CTL |= MC_3; In the case of the previous example of setting up a
// start the deadline_timer, call SPI communications bus for testing, there is a
// btick on an expiration worker thread will generate interrupts for the SPI
tmr.async_wait(boost::bind(btick, ..., receive. To ensure that the worker thread does not
t, &tmr, ival); overtake the main thread, without the proper setup
ios.run();
}
being put in place, it is required that proper wait
conditions are used. The example can be rebuilt
void btick(const msptimer& t, tmr, ival) with notations on how synchronization is
{ performed. Specifically, a condition variable, a
t.testInterrupt(); mutex, and a unique lock are typically required in
// if timer still running, then
// reschedule the deadline_timer order to perform proper synchronization.

6 Version 1.0 - August 2016


// define a short-hand wait macro // with data
#define wait_usec(s) / while (!spi.finishedParsingBuffer());
std::this_thread::sleep_for /
(std::chrono::microseconds(s)); // ensure the worker thread is finished
t.join();
// function to send SPI data by loading
// the RX buffer and generating // check result
// test interrupts ASSERT(spi.buffer == buf);
void sendData(std::vector<char>& buf, }
std::condition_variable& cv)
{
Condition variables can be used in a worker thread
// ensure the interrupt vector is set
// for receive as well when both threads need to setup a pre-
UCA2IV = 2; determined state before the full testing loop can
begin; this can be done simply by passing in
// short delay to allow main thread additional information such as the mutex and
// to call it’s wait function
wait_usec(1);
unique lock object into the worker thread as well.
// notify main thread that we’re ready
cv.notify_one(); IX. Conclusion
// send out the data
for (auto i = 0u; < buf.size(); i++) MSP430 MCU programs can be written for a
{
UCA2RXBUF = buf[i]; multitude of applications, and may use simple or
spi.testInterrupt(); complex algorithms and logic, but as for all
// allow time for the interrupt to software development, having a testable
// be processed, typically a monitor application can help ensure reliability and
// of a buffer from the main thread
wait_usec(10);
performance no matter what the application. By
} making use of some basic C++ constructs,
} migrating an application to be tested on the
desktop can be relatively painless and provide a
// main thread and main entry point flexibile environment to help implement tests that
void mainThread()
{ provide more code coverage and execute functions
// setup the synchronization objects that would traditionally only be possile on the
std::condition_variable cv; embedded device itself.
std::mutex mtx;
std::unique_lock<std::mutex> lk(mtx); About the Author
// initialize other items required
// for the comm bus Kris studied Computer Systems at the British
Columbia Institute of Technology, and has since
// start the worker thread to send data been working in the medical device industry for
std::vector<char> buf = “test”;
std::thread t([&]{ sendData(buf,
over 16 years, helping to design and implement
cv); }); real-time imaging devices using a variety of
// ensure the thread is ready platforms and technologies, which include:
cv.wait(lk); Microsoft Windows, Embedded Linux (ARM
based), TI C6X DSPs, TI MSP430, ADI Blackfin
// parse the buffer as it fills up
SoC, and Xilinx FGPAs.

7 Version 1.0 - August 2016

You might also like