This page explains several aspects of the software used in the Boldport project #9 “PissOff”. It also describes the commands of the maintenance mode and how you prepare the SD card for the project.

Table of Contents:

Language and Style

The software is written for the MKE04Z8VWJ4 MCU from NXP. This is a Cortex-M0+ architecture with 8KB flash memory and 1KB SRAM. It is a 32-bit processor which is using a special instruction set called “Thumb”. This instruction set is optimised for size, and has a very good code density.

NXP provides a integrated development environment for this series of MCUs, called Kinetis Studio. There is an integrated assistant, called “Processor Expert”, which automatically generates code for the various peripherals in the MCU. There are several problems with the approach of this IDE.

  • First it generates C based code, even there is a fully functional C++ toolchain available which produces way better results.
  • The generated code from the “Processor Expert” is complicated and wastes resources for nothing. The 8KB flash of the MCU is easily filled with complicated processor expert code and there is no space left for the application.

Therefore I reduced the use of the processor expert tool to the minimum and used a procedural C++ approach to implement the software.

  • I use the initialisation code from the processor expert, which setups some of the peripherals and creates the interrupt vector.
  • To come free from the C code, I implemented a simple C++ wrapper which the compiler optimises away.

Documentation

The whole software is well documented, especially the interfaces. As an experienced software developer, you should not have any problems to understand how the software works. There are also a few comments in the functions itself, but in most cases the code speaks for itself.

Download

The software can be downloaded from the GitHub repository. To use this code in the IDE, you have to generate the processor expert code first.

The C++ Wrapper

The C++ wrapper is defined in the Wrapper.h and Wrapper.cpp file. It is a simple function with a C interface, so it can be integrated into main.c. The function itself calls Application::initialize() to initialise the device, and then calls Application::main() to start the main loop of the application. This call never returns. So all code after lrMain() in main.c is not executed.

Components and Namespaces

The software is built using several logical components. Each component has its own namespace. There is a common namespace lr, which stands for Lucky Resistor, and nested inside of this namespace there are the component namespaces. You will see code like this:

namespace lr {
namespace Component {
// ... function declarations ...
}
}

This code embeds the declaration or implementation in a namespace. In this example in two namespaces, resulting in the namespace lr::Component.

No Class?

Classes combine a data structure with functionality. This is extremely useful if you have multiple instances of this data structure and would make sure, each function accesses the data structure of a certain instance.

If you have functionality, where you know there will be only one single instance of the data structure, usually a C++ programmer will create a singleton class. It is a class which enforces there is only one instance. This instance is usually created at the start of the application and destroyed at the end.

All components of this software are “singletons” because they provide an abstraction for a functionality which only can exist once. Therefore, using a singleton classes wold be a waste of resources. Ideally the compiler would optimise all overhead away, still there would be the challenge to make the right components accessible to each other. Therefore I decided to use a procedural approach with namespaces.

Namespaces

Namespaces are a really important language feature of C++ (and of many other languages). They allow to group names, from variables and functions, into logical “spaces”. These spaces can be nested to create a hierarchy of namespaces. They prevent name collisions: You can use simple and speaking names for variables and functions but do not have to fear name collisions. There can be a function initialize() in namespace Application and in SimpleADC without conflict.

There is another feature called anonymous namespace:

namespace {
}

If you declare a namespace without explicitly set a name, as shown in the example above, you create an anonymous namespace. The compiler automatically generates a random name, which is unique for the compiled unit. You use these usually in cpp files to elegantly prevent any linker problems if you declare internally used global variables.

To address functions or variables declared in namespaces, you simple use the :: operator. It works like a file path. You only have to specify as much as required to resolve the name. The compiler starts its search from the current namespace, then goes up in the hierarchy. If you start a name with ::, then this is like an absolute filename. In this case, the compiler starts at the global namespace. Nice if you want to ensure you use a function declared in the global namespace. If you are new to namespaces, please read more about namespaces here, or search for more detailed description.

Peripheral Abstraction Layer

In OO design, there is a basic and very important rule: “Program to an interface, not an implementation”. In the case of MCUs, this means: Never program peripherals directly via registers, always write/use an abstraction layer. It is true in both bases for the same reasons. Abstractions will let you port, maintain and refactor your code way easier. The compiler also gets a better “understanding” of your peripheral access and can optimise your code for speed or size.

The software contains the following abstractions:

  • SimpleADC
    This is an abstraction to access the analog digital converter from the MCU.
  • SimpleIO
    This abstraction is for accessing the IO lines of the chip. It is used to control the signal and IR LED, enable the audio amp and control the digital analog converter part of the device.
  • SimpleSPI
    A simple abstraction to use the SPI bus, which is attached to the SD card.
  • SimpleSerial
    This is an abstraction to use the serial line for sending and receiving data. It contains a few methods to convert numbers into a hexadecimal form.
  • SimpleTimer
    An abstraction to use a timer to measure time or wait for a given amount of time.
  • TimedInterrupt
    This abstraction is to control an interrupt which is triggered using a timer. It uses a simple callback mechanism, where you can register a function which is called for each interrupt. In this software it is used to blink the LED in case of an error, to control the detection mechanism while the MCU is in sleep mode and to play the sound samples in the correct intervals.

You can use these simple abstractions to build an own application and you will never get in touch with the hardware details. In case you would like to port the software to a different kind of MCU, you can simple replace the implementations of these abstractions and most likely – everything else will work without any additional code changes.

The SD Card Component

The SD card component (SDCard.h and SDCard.cpp) implements a very minimal access to SD cards using the SPI protocol (using the SimpleSPI abstraction). The component can read single blocks, or multiple sequential blocks from the card. It also implements the Micro Disk format as a very basic file directory.

There is already a detailed documentation about the communication between the MCU and the SD card. The discussed library in this article is very similar to the used one in this software, but meanwhile a few years old. Nevertheless, the basic principle did not change, so you should easily understand how everything works. The component of this software is much simpler with less code as the one described in the article.

The SD card component uses the SimpleSPI abstraction for the communication and the SimpleTimer abstraction to detect time-outs in the communication with the card.

The Audio Player

The audio player component plays sound files from the SD card. The player assumes the sound data is stored as uncompressed, mono, unsigned, 8-bit data on the SD card. If you call playSound() it will start reading from the given block on the SD card and play size bytes of sound.

To reproduce the sound as good as possible, an interrupt is used to play the samples while there is a buffer which is filled in the main process. This guarantees an exact sample frequency, which is key for passable sound output. The precision of the sample output is more important than the bit resolution.

The audio player uses the SimpleIO abstraction to enable the audio amp and output the samples to the digital analog circuit. It also uses the TimedInterrupt abstraction for the interrupt.

The Detector Component

The detector component encapsulates the task of detecting an obstacle in front of the device. This is done using an IR LED and an IR transistor in front of the device (the eyes of the head) which point straight forward. While this is the absolute minimum setup you can have for optical distance sensing, there are a number of disruptive influences you have to consider:

  • The IR transistor is sensible to visible daylight and other IR devices.
  • There is stray light from the side, directly from the IR LED.
  • There is a reflection and stray light from the room.

I choose an approach which works best in most situations, which makes the device flexible  in the range of used IR LEDs and IR transistors. It is a static approach which cannot deal with slowly changing stray light or day light, but it is simple enough to understand easily.

For your own experiments, simple attach two probes of your oscilloscope to the two wires holding the head of the device, where the IR LED signal and the IR transistor signal is transmitted (looking to the “back side” of the head, these are the middle and right wire).

Have a look at the following measurement:

signal-response-1

The yellow line is the IR LED signal. It is inverted, because the MOSFET driving the LEDs is attached at the cathode. So if the LED lights up, this signal will go to ground. In the shown measurement, if the line goes up, the IR LED is enabled.

The blue line shows the measurement from the IR transistor. The middle line of the screen is 0V and each dotted line is 1V.

The detector is sending eight very short light pulses, every 200ms and measures the feedback from the IR transistor. Now have a closer look to the signal and the response from the IR transistor:

signal-response-2

This is the situation in a quite dark room, the head is pointing into the empty room. You can see there is only a minimal base voltage, around 0.2V, and very small spikes from the stray light or reflection from the room.

If there is an obstacle in front of the device, the measurements will look like this:

signal-response-4

You can see the strong response to the signal, up to 2.6V. You also notice, the response has a delay. This is the delay from the IR transistor, how fast it reacts to the light.

This would be an ideal situation, and if this would be the only case, the detection would be similar to the detection of a key press.

If I turn the light on in the room, the situation looks like this:

signal-response-3

Even without any obstacle in front of the sensor, the IR transistor passes 3.1V. You still can see the small spikes from the stray light or background reflection, but it got very subtle.

So how should an algorithm look like to be able to work reliable in any condition?

Measurements

For each signal pulse, three measurements are made. For each measurement, 16 samples are taken from the ADC and an average value is calculated. This reduces the influence from any sampled noise.

The first measurement is taken just before the IR LED is enabled. This value is recorded as signal minimum. Now, the IR LED is turned on and the routine waits a certain time to compensate for the IR transistor delay. Then the second measurement is taken. The difference between the signal minimum and this measurement is recorded. The IR LED is turned off and again, the routine waits a certain time to allow for the IR transistor delay. Now the third measurement is taken. The difference between the last and this measurement is recorded.

This is repeated eight times to produce a clear signal, which is hopefully different from any other device using an IR transmitter.

At the end we have the average of all recoded signal minimum values and the average of all signal differences.

Normalisation

The first step is to “remove” the background light from the signal. This is easily done with a normalisation. A head room value is calculated. This is the difference between the absolute possible maximum of the signal and the measured average signal minimum. Any signal will be in this range.

Therefore the signal average is converted into a relative value between 0 and 1000. It is always relative to the current signal headroom. This is not perfect, because most IR transistor are not linear, but it compensates many unwanted effects.

Threshold

Now the normalised signal value is compared with a threshold value. If the signal exceeds this threshold, the a positive counter is increased. If the signal is below the threshold, a negative signal counter is increased. A number of positive signals trigger an alarm, while a number of negative signals will reset the positive signal counter.

The threshold is determined at the start, using a short calibration phase. Here a signal threshold is set from a single measurement. Then this threshold is tested for a longer time span and if there are no false positives, this threshold is set for the device. Otherwise the threshold is increased.

The Application Logic

The application logic is implemented in Application.h and Application.cpp. It is a simple state machine with a loop. The application is always in one of the states defined with the State enumeration. Depending of the current state, different things are processed in the loop.

The Maintenance Mode

There is a maintenance mode built-in the software. If you connect a serial cable (3.3V!) and open a console and connect with 112500 baud, 8-N-1, no flow control. If you start the device, you should see a number of messages and in case of any problem an error message.

To enter the maintenance mode, type main, then press enter. You should get a message that you are in the maintenance mode now. To leave the maintenance mode, type exit and press enter.

There are a number of commands you can use:

  • main: Enter the maintenance mode.
  • exit: Exit the maintenance mode or stop any measurement.
  • dump: Start a sensor dump output. In this mode every second a measurement is made and the current sensor value and head room is displayed in the console. You can start this mode to try different light conditions or use it to compare different LED shields or IR transistors.
  • play: Just plays the next sound from the SD card.
  • cali: Calibrates the sensor and shows the new threshold value and head room.
  • info: Get the version of the firmware.
  • rawd: Start a raw sensor dump. Here you get the raw value from the IR transistor.
  • help: Shows a list with all commands.

Preparing a SD Card

You can download a prepared SD card image with a “dog barking” sound from here:

Do not simple copy this file on the SD Card! Please read below!

Unzip this archive and copy the disk.img exactly as you would install a Raspberry Pi operating system image to this disk. You find the process for your operating system in this article:

Using Own Sounds

You can easily use your own sounds. Follow these steps to prepare an image with your own sounds:

  • Convert your sound files to 1 channel, uncompressed, unsigned 8-bit data. The best tool to do this is sox. Download SOX here. With sox, use this command line to convert demo.wav into demo.raw:
    sox demo.wav -e unsigned-integer -b 8 -c 1 -r 44.1k -S -t raw demo.raw
  • Repeat this until you have all files prepared. If you create a disk with multiple files, the device will cycle through your sounds.
  • Use the Micro Disk tool to create a disk image from the converted files. Download Micro Disk here. Make sure you use very short names for the files. I recommend you use s1, s2, etc. as name.
  • Copy the disk image to the SD card as described above.

Thank You

I hope you enjoyed reading all the details about the software of the PissOff project. Subscribe to this blog or to my Twitter feed to get updates about current and new projects. I would like to encourage you:

  • Play with shields on the LED and IR transistor to reduce the stray light. It will improve the detection quite a lot.
  • Connect a larger speaker! The amplifier can produce very loud sounds – it is only limited by the speaker. A 2W speaker will also increase the sound quality. The only requirement for the speaker is 8Ω impedance.
  • Make changes to the code. All what you need is a SWD programmer. I recommend the one from Segger, they are universal and you can also use them to program the Boldport Touchy project using C2. Segger has a special version of the programmer for hobbyists and students.
  • Implement your own (better) detection algorithm. Let me know what you could achieve with the same device and better software.
  • Try different/better IR LEDs and/or transistors.

Have fun!