Project Design Walkthrough: Airsoft Tracer Unit, Part 3

Get Started with Altium Upverter, Sign Up Now

Upverter Expert - Project Design Walkthrough_ Airsoft Tracer Unit, Part 3

We are back to work on the Airsoft Tracer Unit we started in Part 1 and continued in Part 2. We are building a unit that fits on the end of an airsoft gun and exposes the dark plastic BBs traveling through to UV light, exciting the luminescent material on the BB. This will make the BB glow brightly, making it easy to see during night games, or indoors. In previous articles, we selected our top level components, prototyped, and extensively tested the photogate for detecting the BB. We also ruled out the use of an LED lighting power supply for limiting the current on the UV emitters. In the first article, I mentioned looking at using an SR Latch to hold the state of the photogate after a BB passed through, but hoped that we would not need to do that.

In this article, we’ll be writing some code to run on an LPC11U24. We will be using the MBED online compiler platform to get the basics of our code written for the project, and see if we can do away with the SR latch. Ideally, we should be capturing the edge change of the signal quite rapidly so we can also use the LED unit as a chronograph as well as for ensuring the UV emitter array is only on when the BB is traveling past it.

Microcontroller Test Setup

In Part 2 of this series, we were able to test the photogate in real-world conditions using a 3D printed holder for the light gate components, and using that, we got some traces from the test board with the oscilloscope. We could use the same setup for testing the microcontroller code, however, it’s a little cumbersome to use such a setup for testing purposes. Therefore, I’m setting up a function generator to create a square wave that will closely simulate the timing of the live test I ran. I’m using a 30Hz square wave, which translates to a pulse width of 33.3ms. The square wave is present on two channels, the second of which has a phase of 353° relative to the first. In the airsoft world, 30 rounds per second is considered to be a moderate to high fire rate, which makes this a realistic test speed. The signal going low for 33.33 microseconds (99.9% duty cycle) matches what we measured previously quite well for the highest speed rounds, and the 353° phase of the second channel gives a 613μs gap between the first and second channel firing, simulating a BB traveling through the tracer unit.

Oscilloscope screenshot showing an active low pulse on the yellow channel and similar but delayed pulse on the cyan channel
Generated signal simulating BB entrance (yellow) and exit (cyan) with difference in time between the pulses of 613.6μs 

By using a function generator, we have a ‘best case scenario’ signal that we can use to develop firmware that will prove out the concept and give us reliable timing information to work with.

Using MBED

This code is being developed using MBED, which is an online compiler (which can also be installed on a computer) that uses an open source hardware abstraction layer for ARM Cortex M0 to M4 series devices and is developed by both ARM and the community. This means the code compiled with MBED is portable across many different devices without having to change controller-specific startup code or specific registers for each device’s peripherals. This is similar to the Arduino library, but only for ARM controllers. The online compiler provides a good user experience and makes collaborating with teams easy through built-in source control. There’s a wide range of useful functions built in, as well as many libraries for specific external devices published by the community that are easy to pull into your project.

Because it supports such a diverse range of controllers, my colleagues and I have experienced frustrating bugs that tend to be very platform-specific. For example, on one microcontroller, the timer has a 32-bit implementation, thus overflows after about 35 mins, but on another microcontroller, the timer for the same function has a 64-bit implementation and doesn’t exhibit the same failure. Also, due to the hardware abstraction layer, some portions of code can take significantly longer to execute than if you had used the register/peripheral directly with controller-specific code. The very slight delay this introduces is a common aspect of all hardware abstraction libraries (like Arduino), and is just something you need to work with for the convenience of the library. The platform-specific bugs do eventually get fixed when reported, but it can be a little frustrating when you come across one, and switching to another microcontroller can resolve the issues.

That being said, the bugs are few and far between, and code quality in MBED is generally very high. The platform allows rapid prototyping and evaluation of many microcontrollers to optimize cost, and makes development easy. Although the code for this project is going to be quite simple, timings need to be fairly tight to ensure the UV emitters turn on quickly and don’t stay on too long after the BB has exited the unit. The emitters generate a lot of heat, and we are doing away with current limiting, so to save them from premature failure we need to keep them on for the shortest period possible.

Software Requirements

In part 1, we looked at the overall specifications/requirements for the device, but didn’t go into details for the firmware. This is a very simple device, so it’s software requirements are brief.

Required Functionality:

  • Detect first photogate transition to low level signal.
  • Turn on UV LEDs while the BB transits the tracer unit.
  • Detect second photogate transition to low level signal.
  • Turn UV Emitters off when BB exits the tracer unit.
  • If the exit hasn’t been detected after 700μs, turn the UV emitters off.

Future “nice-to-have” functionality would include a Bluetooth chronograph with memory, allowing you to look back at velocity consistency at the end of the game.

Initial Test Code

We can implement this code in one of two ways:

  • Using a loop to poll the input pins to look for a state change.
  • Using interrupts to execute code when the pin experiences the changing edge we are looking for.

I’m going to go down the interrupt route, as the amount of code we need to run when the interrupt fires is very small, so implementation is going to be very easy. My main concern is whether the interrupt will be raised by the LPC11U24 microcontroller, and makes its way through MBEDs code to my user code quickly enough. If the delay is too long, it might make this infeasible.

A polling loop is an effective way to implement this functionality too, but if we have some code that will take a long time to run inside the loop, such as future Bluetooth functionality, it could block loop iteration for long enough to cause the microcontroller to completely miss the signal transition from high to low. On the other hand, with an interrupt, that long-running code will be paused while the interrupt is handled, and then execution will continue once the interrupt service routine is complete. This allows us to still perform any long-running tasks we may need in the main loop without having to worry about losing a pulse as a BB passes through the light gates.

Programming the LPC11U24

I’m starting out my code with the mbed_blinky template. This code just flashes LED1 on the dev board on and off every 200ms.

The first thing I like to do is set up some definitions for the pins I’ll be using on the microcontroller. This makes changing between dev boards easy, and makes it very clear where all the connections go if you pick the project up again at a later date, or are designing the schematic to go with the circuit board.

#define PIN_GATEONE P0_7 // p21
#define PIN_GATETWO P0_17// p22

#define PIN_LED2 p15 // for oscilloscope to watch

#define PIN_LEDOUT P1_8 // this is LED1
#define PIN_STATUSOUT P1_9 // this is LED2

I’ve defined my pins for both gates, a signal for my oscilloscope to watch, a status LED that will be flashed by the main loop, and another that will flash in time with the oscilloscope watch pin.

In a larger project, I’d have all the definitions in a separate header file to make it so I didn’t need to scroll through all the definitions at the start of the file. It makes it a lot easier to move pins around as you’re routing a circuit board to have all the definitions in one place with recognizable names, rather than having to hunt through multiple areas of code to change pin definitions.

Next, we need to define the IO objects. There are two interrupts for the light gates, and 3 digital outputs for the LEDs. That’s all the project requires—it’s nice and simple.

InterruptIn gateOne(PIN_GATEONE);
InterruptIn gateTwo(PIN_GATETWO);

DigitalOut led(PIN_LEDOUT);
DigitalOut led2(PIN_LED2);
DigitalOut status(PIN_STATUSOUT);

After this, we have two very simple functions to toggle led and led2 on or off depending on the interrupt that gets fired.

void triggerIn() 
{
    led = led2 = 1;
}
void triggerOut()

{
    led = led2 = 0;
}

triggerIn is for the first gate, with the BB going into the tracer, and triggerOut is for the second gate, where the BB is going out of the tracer.

Finally, we get to our main()function. This is going to turn all the LEDs off immediately on boot, and then configure the interrupts to trigger on the photogate edges.

int main() 
{
led = 0;
led2 = 0;
status = 0;


// Interrupt configuration
// Rising edge for end of BB transit through the gate
gateOne.rise(&triggerIn);

// Falling edge for start of BB transit into gate.
gateTwo.fall(&triggerOut);


while(1) {
status= !status;
wait(0.2);
}
}

The main loop is simply going to flash the status LED (LED2) on and off every 200ms.

Compiling the code is all done online, and programming is as simple as dropping the downloaded hex file onto the LPC11U24’s USB flash drive and hitting the reset button.

Oscilloscope screenshot showing the two pulses from the previous figure and the microcontroller response on the magenta channel
Introducing channel 3 (magenta) with the microcontroller response

It looks like it’s doing what it’s meant to do. There is a bit of a delay as we’d expect, but what’s most worrying to me is it seems to be jittering around a lot.

Oscilloscope screenshot showing pulse width and delay overlay on top of the previous figure
The oscilloscope provides us with pulse width data and delay data. The microsecond deviation is worrying.

Using the oscilloscope to measure this jitter, we can see from the falling edge of channel 1 (yellow) to the rising edge of channel 3 (purple) that we have an average delay of 19.73μs. However, the deviation is unacceptably high at 7.79μs. The total deviation of the pulse width is only 3.38μs, which I find interesting. The code should have highly repeatable results, so the delay and pulse width should be rock solid. The oscilloscope is in high-resolution mode, so we can detect as small as 97.14ns of deviation from the function generator pulse.

After some investigation, I removed the status LED flashing, and more importantly the 200ms wait call. Even though the code should essentially be paused during the wait call, it appears the wait state perhaps takes longer to interrupt than executing code does!

Similar to the figure above, but with more consistent delays and nanosecond deviation levels
All the delay and width measurements are now in nanoseconds!

Simply removing the wait call from the code gives us an absolutely stable pulse width and delay between the edge transitions. It’s actually significantly more repeatable than my new function generator is on average, which is quite encouraging for the code.

Improving the Code

We now have something that can switch on and off if the timing is right, but it’s fairly easy to get this code into a bad state. While the code is reading the signal reliably, messing with the function generator outputs (such as turning one off) makes it unhappy. It’s a valid proof of concept to show the interrupts are stable, however the code wrapping them is not, and it needs a few extra checks and balances in place.

bool triggeredState = 0;

I’m going to introduce a global variable to determine if the LEDs are active or not. I could check the led/led2 variable, but a global bool is going to execute a few nanoseconds faster, as well as make the code more readable and maintainable.

bool triggeredState = 0;

I’m also going to need to implement some code to ensure we don’t have the LEDs on for more than 700 microseconds, so we’ll need a Timer and a couple of timestamp objects.

Timer sysTime;
us_timestamp_t gateStartTime;

The Timer and gateStartTime are global. We’ll also add a local timeCheck variable to main(), which would also make the code more legible and maintainable even though we don’t strictly need it given the implementation.

us_timestamp_t timeCheck;

Now that we have the variables set up, we need to start the sysTime timer in the main() function, so I’ll do that right after we set the LED outputs to be off.

sysTime.start();

Next, I’ll relocate the contents of the interrupt routines into new functions so we can call them safely from other locations in the code. Because of the way MBED implements the interrupt service routine, we could call the ISR directly. Nevertheless, I feel it looks cleaner to have the ISR serve the single purpose of just handling the interrupt. If we were implementing the ISR directly with the microcontroller GPIO peripheral, it would need to be in it’s own single purpose function too.

void ledActivate()
{
led = led2 = triggeredState = 1;

gateStartTime = sysTime.read_high_resolution_us();
}

void ledDeactivate()
{
led = led2 = triggeredState = 0;
}

The ISR then gets changed to first check the triggeredState variable before calling these functions.

void triggerIn() 
{
if (!triggeredState)
ledActivate();
}

void triggerOut()
{
if (triggeredState)
ledDeactivate();
}

Finally, our main loop gets changed to be a check to see if the UV emitters have been on for longer than they are allowed.

while(1) 
{
// check for missed second gate
if (triggeredState)
{
timeCheck = sysTime.read_high_resolution_us() - gateStartTime;

if (timeCheck > LED_TIMEOUT)
{
ledDeactivate();
triggerTransitionRead = 1;
}
}
}

This will check the definition LED_TIMEOUT which is set to 700. If the LED outputs are on for longer than 700μs, the LEDs will be deactivated.

Oscilloscope screenshot showing active low pulse on the yellow channel and no pulse at all on the cyan channel and the finite response we expect on the magenta channel
Response is as we expect with the exit signal on channel 2 (cyan) removed.

With the device programmed, and the second channel on the function generator disabled (simulating the second gate missing the BB entirely), we now have a 716.8μs pulse. If we needed exactly 700μs, we could definitely optimize this further, however the code is doing exactly what it needs to at this point.

Oscilloscope screenshot showing no pulse on the yellow and magenta channels and an active low pulse on the cyan channel
It’s also as we expect with the entrance gate pulse (yellow) removed.

As we’d expect from the code, with no start pulse on channel 1, there is no reaction from the microcontroller.

Oscilloscope screenshot showing _ on the yellow channel and _ on the cyan channel and _ on the magenta channel

Finally, with both function generator channels enabled, our pulse is as we’d expect to see—switching on with the first pulse and off with the second.

Long Term Testing

I wanted to make sure we wouldn’t run into any overflows anywhere, so I left the setup running for around 9.5 hours, or 1,000,000 pulses. At the end of the period, the scope looked exactly as I left it, with everything functioning nominally. For the next 24 hours, I would switch on or off a channel on the function generator every time I walked by it, and at the end of the 24 hour period the device was still functioning well.

It appears that, for this project, MBED is going to be the perfect mix of functionality, ease of use, and speed of development. We also ended up not needing any latching of the signal, as the microcontroller is very capable of catching the pulse from the light gate, which will simplify the electronics further.

One Final Change

In the first article in this series I mentioned that I wanted to use the LPC11U12FHN33 microcontroller due to its compact size and low cost. The code I’ve compiled for the LPC11U24 *should* work on the LPC11U12 just fine, however, MBED have mentioned in several forum posts that you should really only use firmware for the exact chip configuration that is on the dev board, including the packaging, or weird things can happen. A friend of mine had this issue recently, in which he had compiled code that worked great on his dev board, but exhibited some strange behavior in his product which used a different package. I certainly can’t blame MBED for this, as most compilers would want you to select the exact part number you are compiling for. That being said, the LPC11U24 in the /401 package on the dev kit has a fairly large footprint—larger than I’d like to use on the circuit board for this.

As we’re only making a couple of these, the extra cost per unit is fairly negligible for a microcontroller upgrade when we are installing over hundreds of dollars (USD) worth of LEDs on the boards. I also have the MBED supported Embedded Artists LPC11U35 breakout board, which features a QFN package microcontroller instead of the QFP on the LPC11U24 dev board. This package change saves a significant amount of board real estate.

So after a quick change of target board in MBED and a switch of the input/output pins, the code is compiled and running on the LPC11U35 instead.

Oscilloscope image showing the response of an LPC11U35 instead of the LPC11U24
The response of the LPC11U35 instead of the LPC11U24

What I find very interesting about the code running on the LPC11U35 is that somehow, we have a much shorter period between the interrupt and the output pin state change. Both microcontrollers offer a 50MHz max speed and are running on a 12MHz crystal oscillator and, ostensibly, have the same architecture internally.

Well, whatever the cause, I’ll happily use this as an excuse to switch to the LPC11U35! A 24 hour test run with the LPC11U35 was also highly successful.

Next Time

We now have the test firmware completed and working reliably. We’ve selected our final microcontroller, and all of our electronics are tested (other than the basic linear regulator used to drive the microcontroller, which doesn’t need any prototyping). We’re finally ready to start on the PCB design in Upverter.

In the next article, we’ll be tackling the schematic capture, PCB layout, and output generation of the manufacturing files to send off to a PCB fabricator.

Leave a comment