Complete the Laser Tag System

Table of contents

  1. Overview
    1. Organization of the Detector
    2. General Requirements
  2. Resources
    1. Source Code
  3. Specifications
    1. ADC Buffer
    2. The Complete Detector
  4. Implementation Details
    1. ADC Buffer Sketch
    2. Detecting “Hits”
      1. A Hit-Detection Algorithm
      2. Hit-Detection Algorithm Example
      3. Determining the Fudge-Factor
  5. Pass Off
    1. 1. Detector Isolated Test
      1. Isolated Test Details
    2. 2. System Demonstration

Overview

In this task you will complete the implementation of your laser-tag system and test it by connecting the transmitter output (generated by your transmitter state machine) to the Analog-to-Digital Converter (ADC) input.

You will complete the system in this task by doing the following:

  1. Insert code that reads the output from the ADC and stores it as an integer.
  2. Implement a buffer for storing values read from the ADC.
  3. Finish implementation of the detector. The detector refers collectively to all of the filtering code that you have tested and implemented thus far, plus some additional code that you will write and test in this task (see Organization of the Detector below).
  4. Demonstrate correct operation in continuous mode using the provided test code. In this mode, your system will continuously run the transmitter state machine.
  5. Demonstrate correct operation in shooter mode using provided test code.
  6. The shooter and continuous mode test functions are provided for you.

Organization of the Detector


General Requirements

  1. Implement the code for the detector.
  2. You must demonstrate continuous mode with the provided software (runningModes.c). Your demonstration should be essentially the same as the demonstration video. Make certain that continuous mode works correctly before moving on to shooter mode.
  3. You must demonstrate shooter mode with the provided software (runningModes.c). Your demonstration should be essentially the same as the demonstration video.
  4. You must pass off using the provided main.c, and runningModes.c.
  5. Carefully follow all of the pass off requirements.
  6. Submit your code as follows. In the top-level directory, run ./check_and_zip.py 390m3-3
  7. You must follow the coding standard.

Resources

Source Code

Note that the following files are provided in your ECEN 390 project directory. Refer to the comments above each function for more details.

  • ltag/main/buffer.h
  • ltag/main/detector.h
  • ltag/main/main.c
  • ltag/main/support/bufferTest.h
  • ltag/main/support/bufferTest.c

You are expected to create and implement the following files. See the provided header files (.h) for a description of each function.

  • ltag/main/buffer.c
  • ltag/main/detector.c

Specifications

ADC Buffer

Implement a circular buffer in buffer.c that is dedicated to storing incoming integer data from the ADC. The code is similar to the code needed for queue.c. Why not just define a queue from queue.c and use that? You absolutely cannot under any circumstances use any floating-point data types (double or float) in any function that is invoked by an interrupt routine. The isr_function() is invoked by an interrupt routine, so when the ADC data is read and placed into a buffer, no floating-point types are allowed.

The solution is to create a circular buffer that stores integers of type uint32_t. Implement a set of functions as follows:

  • buffer_init(): this initializes the ADC buffer much like queue_init() does. It would make sense to have isr_init() invoke this function.
  • buffer_pushover(uint32_t value): this is similar to queue_overwritePush() with an uint32_t argument instead of a double. If the circular buffer is full, overwrite the oldest value.
  • buffer_pop(): this is similar to queue_pop() that returns a uint32_t instead of a double.
  • buffer_elements(): this is similar to queue_elementCount().
  • buffer_size(): simply returns the capacity of the buffer in elements.

You will have to write most of this ADC buffer code. See ADC Buffer Sketch below for code to get started. You can also refer to your queue.c code for inspiration (but don’t copy any double data types if you cut and paste).


The Complete Detector

When you invoke detector(), perform the following steps.

  1. Query the ADC buffer to determine how many elements it contains. Use buffer_elements() for this. Call this amount elementCount.
  2. Now, repeat the following steps elementCount times.
    • If interrupts are enabled (check to see if the interruptsEnabled argument == true), briefly disable interrupts by invoking interrupts_disableArmInts(). You must disable interrupts briefly while you pop an element from the ADC buffer. Otherwise, if an interrupt occurs while you are “popping” a value from the buffer, the interrupt routine and your detector() routine may simultaneously access the ADC buffer and may cause indexIn, indexOut, or some other field of the ADC buffer to be miscomputed. This kind of problem can be very difficult to track down because it is hard to reproduce, so it is best to avoid the problem in the first place.
    • Pop a value from the ADC buffer (use buffer_pop() for this). Place this value in a variable called rawAdcValue.
    • If the interruptsEnabled argument was true, re-enable interrupts by invoking interrupts_enableArmInts().
    • Scale the integer value contained in rawAdcValue to a double that is between -1.0 and 1.0. Store this value into a variable named scaledAdcValue. The ADC generates a 12-bit output that ranges from 0 to 4095. 0 would map to -1.0. 4095 maps to 1.0. Values in between 0 and 4095 map linearly to values between -1.0 and 1.0. Note: this is a common source of bugs. Carefully test the code that does this mapping.
    • Invoke filter_addNewInput(scaledAdcValue). This provides a new input to the FIR filter.
    • If filter_addNewInput() has been called 10 times since the last invocation of the FIR and IIR filters, run the FIR filter, IIR filter and energy computation for all 10 channels. Remember to only invoke these filters and energy computations after filter_addNewInput() has been called 10 times (decimation). If you have just run the filters and computed energy, also do the following:
      • if the lockoutTimer is not running, run the hit-detection algorithm. If you detect a hit and the frequency with maximum energy is not an ignored frequency, do the following:
        • start the lockoutTimer.
        • start the hitLedTimer.
        • increment detector_hitArray at the index of the frequency of the IIR-filter output where you detected the hit. Note that detector_hitArray is a 10-element integer array that simply holds the current number of hits, for each frequency, that have occurred to this point.
        • set detector_hitDetectedFlag to true.

You will implement the detector in detector.c. The header file detector.h is already provided with the laser tag project. Here is an overview of the required functions.

  • detector_init(): Initializes the detector module. By default, all frequencies are considered for hits. The function assumes the filter module is initialized previously.
  • detector_setIgnoredFrequencies(bool freqArray[]): You pass it a preinitialized array of 10 booleans. If freqArray[0] is true, for example, no hits should be registered for this frequency when you run the detector() function.
  • detector(bool interruptsCurrentlyEnabled): This runs the entire detector once each time 10 new inputs have been received, including the decimating FIR-filter, all 10 IIR-filters, energy computation, and the previously-described hit-detection algorithm. detector() sets a boolean flag to true if a hit was detected. The interruptsCurrentlyEnabled flag will be set to true if interrupts are enabled and false otherwise.
  • detector_hitDetected(): Simply returns the boolean flag that was set by detector(). Example code provided in runningModes.c calls detector() and then calls detector_hitDetected() to determine if you have been hit.
  • detector_clear(): This function simply clears the aforementioned flag.
  • detector_getHitCounts(): This function simply copies the values from the detector_hitArray into the supplied argument. You provide an array of size 10 as the only argument. After invoking this function, the array will contain the same values as detector_hitArray.
  • detector_runTest(): When invoked, this function will test the functionality of your detector software. This test function is described below.

Implementation Details

ADC Buffer Sketch

A sketch is given below as a starting point for an implementation of buffer.c. You will need to finish the implementation.

#include "buffer.h"

// This implements a dedicated circular buffer for storing values
// from the ADC until they are read and processed by the detector.
// The function of the buffer is similar to a queue or FIFO.

// Uncomment for debug prints
// #define DEBUG

#if defined(DEBUG)
#include <stdio.h>
#include "xil_printf.h" // outbyte
#define DPRINTF(...) printf(__VA_ARGS__)
#else
#define DPRINTF(...)
#endif

#define BUFFER_SIZE 32768

typedef struct {
    uint32_t indexIn; // Points to the next open slot.
    uint32_t indexOut; // Points to the next element to be removed.
    uint32_t elementCount; // Number of elements in the buffer.
    buffer_data_t data[BUFFER_SIZE]; // Values are stored here.
} buffer_t;

volatile static buffer_t buf;


// Initialize the buffer to empty.
void buffer_init(void)
{
}

// Add a value to the buffer. Overwrite the oldest value if full.
void buffer_pushover(buffer_data_t value)
{
}

// Remove a value from the buffer. Return zero if empty.
buffer_data_t buffer_pop(void)
{
}

// Return the number of elements in the buffer.
uint32_t buffer_elements(void)
{
}

// Return the capacity of the buffer in elements.
uint32_t buffer_size(void)
{
}

Detecting “Hits”

Now that you have completed the previous milestones, you have code that can compute the total energy that passes through each of your IIR-based bandpass filters. Now what? Here are some considerations.

  1. You could always select the band-pass filter output that contains the maximum energy relative to the others. Unfortunately this simple scheme won’t work because it would always detect a “hit” of some sort, even when no one is shooting at you. Due to various noise sources, there will always be some energy in at least some of the frequencies, even when no one is shooting at you.
  2. You could select the frequency that contains a energy value that is above some threshold. This “kind of” works; however, it is difficult or impossible to come up with a threshold that works in enough situations. For example, let’s say you perform some experiments in indoor light with the two guns spaced apart by about 10 feet. In continuous mode, you can see the energy values that are being captured and so you set the threshold so that “hits” are only detected when the computed energy is above the threshold. OK so far. Now you move the guns so that they are 20-feet apart. Now, when you press the trigger, nothing happens because the energy numbers across all frequencies are not above the threshold. You can lower the threshold to increase sensitivity and detect “hits” at greater distances. However, at some point you will begin to detect noise (from your amplifier circuitry, from the ambient lighting, etc.) as a “hit”. Clearly, just comparing the energy in the band-pass filter outputs to a fixed threshold won’t work very well.

A Hit-Detection Algorithm

We will use an algorithm that adjusts the threshold based upon the current energy contained in the outputs from all 10 band-pass filters.

The detection algorithm consists of the following steps:

  1. After running all of the filters and computing the energy for each band-pass filter output, sort the energy values in ascending order according to their magnitude. Just use an insertion sort or a selection sort algorithm to do the sorting. Here is a very simple insertion-sort tutorial based on playing cards. Pro Tip: sort a separate array containing the indices of the original array.
  2. Select the median value. Selecting the median value is simple once you have sorted the energy values – the median value is simply the value “in the middle” of the set of sorted values. For our system, we have 10 energy values; once they are sorted in ascending order, the median value is either the 5th or 6th element according to the sorted order. Everyone should sort values in ascending order and select the 5th value for consistency.
  3. Multiply the median value with a “fudge-factor” to compute a threshold. This computed threshold should be high enough to reject noise and avoid false “hits” but should be low enough to detect hits from a distance of 40 feet or so. You will compute the fudge-factor through experimentation. Why use a fudge-factor? The fudge-factor makes it possible to accommodate the different characteristics for everyone’s analog board. For this task, the fudge factor can be pretty high, say 1000 or so. Once you connect the guns in the next task, you will need to lower the fudge factor to maximize your range.
  4. Find the band-pass filter that contains the maximum energy (this is easy to do once you have sorted the values). If the maximum energy exceeds the threshold (median-value * fudge-factor), you have detected a hit.

Hit-Detection Algorithm Example

Here is an example of the hit-detection algorithm in operation. Assume the fudge-factor = 5. Let’s say that we retrieve the current energy values for all 10 frequencies using the previously-implemented function: filter_getCurrentEnergyValue(uint16_t filterNumber). The retrieved energy values for the 10 frequencies for this example are:

  • energy[0]: 150
  • energy[1]: 20
  • energy[2]: 40
  • energy[3]: 10
  • energy[4]: 15
  • energy[5]: 30
  • energy[6]: 35
  • energy[7]: 15
  • energy[8]: 25
  • energy[9]: 80

After sorting in ascending order, we get the following:

  • energy[3]: 10 (#1)
  • energy[7]: 15 (#2)
  • energy[4]: 15 (#3)
  • energy[1]: 20 (#4)
  • energy[8]: 25 (#5)
  • energy[5]: 30 (#6)
  • energy[6]: 35 (#7)
  • energy[2]: 40 (#8)
  • energy[9]: 80 (#9)
  • energy[0]: 150 (#10)

The median value (sorted element #5) is 25 from the band-pass filter for frequency 8. For this example set of data that would mean that you would only detect hits for values that are over the threshold value of 25 (median value) * 5 (fudge-factor) = 125. The band-pass filter for frequency 0 has the maximum energy value (150) which is greater than 125 so we would detect a hit.

Let’s run the detector again with another set of data. After sorting, the energy values from the band-pass filter outputs are as follows:

  • energy[2]: 10 (#1)
  • energy[1]: 25 (#2)
  • energy[4]: 30 (#3)
  • energy[7]: 30 (#4)
  • energy[8]: 45 (#5)
  • energy[6]: 50 (#6)
  • energy[5]: 55 (#7)
  • energy[3]: 65 (#8)
  • energy[9]: 70 (#9)
  • energy[0]: 150 (#10)

Our median value (element #5 in sorted order) = 45. We compute the new threshold by multiplying 45 * 5 (fudge-factor) = 225. Our maximum energy value (150) is less than our computed threshold (225) so no hit is detected. The reason no hit is detected is because the maximum energy is not sufficiently greater than the energy contained in the outputs of the other band-pass filters.

These numbers are provided solely for example. The actual numbers you will encounter in your system will be quite different.

Determining the Fudge-Factor

A wide range of fudge-factor values will work when using the loop-back circuitry on the board for this task. I have successfully tested systems with fudge factors as high as 10,000. However, such high values won’t work when you attach the guns. For this task, you can probably just select a value between 200 and 1,000. If you note false detections during shooter mode, raise the fudge-factor until they are eliminated.

The general idea behind this detection approach is that the threshold tracks the current background noise to some degree. Thus if the frequency channels all have energy values that are a little high, the computed threshold also tracks higher. Vice versa, if the frequency channels all have energy values that are lower, the computed threshold also tracks lower. In practice this detection strategy has worked quite well, often achieving distances of 100’ in daylight.

You will implement this hit-detection algorithm in this task.


Pass Off

To pass off your system to the TAs, you must do the following:

  1. Demonstrate an isolated test of your hit detection algorithm (see instructions for the isolated test below).
  2. Demonstrate a system test of the entire laser-tag system as implemented thus far (see instructions below regarding shooter and continuous modes).
  3. Run “./check_and_zip.py 390m3-3” to create a .zip file of your project. Submit only one .zip file per group. The TAs will give credit to both members of the group.

1. Detector Isolated Test

detector_runTest() will test your hit detection algorithm following the steps described below. As a suggestion, organize your hit detection algorithm into a local sub function named hit_detect() so it can be called by the test. This sub function should also be called by detector().

Isolated Test Details

For this test, simply provide energy data for your hit detect function by calling filter_setCurrentEnergyValue() for each of the 10 frequencies. Then call your hit detect function, which should retrieve the energy values. The isolated test must perform the test upon two sets of data as described below:

  1. Create a first set of energy data (10 calls to filter_setCurrentEnergyValue()) and provide a fudge-factor value that will detect a hit when you invoke hit_detect(). After calling hit_detect(), you would invoke detector_hitDetected() to determine if a hit occurred.
  2. Create a second set of energy data that will not detect a hit when you invoke hit_detect(). Use the same fudge-factor as you used for the first set of data. Again, invoke detector_hitDetected() to determine if a hit occurred.

2. System Demonstration

Enable the shooter and continuous mode test functions provided in main.c. Demonstrate your system in both shooter and continuous mode and show that your system runs.