This is an old revision of the document!
In this task you will implement several state machines and test each of them separately to verify correct operation. You will be guided how to “connect” the state machines as needed later in this task and the next. See the system diagram below to see how the state machines will ultimately work together. You can ignore the “red” connections to the jumper block for now. Click the image to see a full-size version.
isr_init()
.isr_function()
. They may not be called from anywhere else.isr_init()
and isr_function()
in isr.c
. The isr.h
header file is included in the ecen390 project directory.These mappings are noted on the carrier board (see the image above), but they are repeated here for convenience. Test pins have been added for some signals on the carrier board to make probing a bit easier.
The MIO package pins on the processor map to the JF header on the ZYBO board. More details are available in the MIO section.
Note that the following files are provided in your ecen390 project directory. Refer to the comments above each function for more details.
You are expected to create and implement the following files. See the provided header files (.h) for a description of each function.
transmitter_run()
, it must generate the waveform for 200 ms.transmitter_setFrequencyNumber(uint16_t frequencyNumber)
.transmitter_setFrequencyNumber()
. Do not read the slide switches in the transmitter state machine which will be called under interrupt.transmitter_run()
. The transmitter will only respond to a frequency change in between 200 ms bursts based on the last call to transmitter_setFrequencyNumber().transmitter_tick()
must be invoked in isr_function()
. If it is not, pass-off points will be subtracted.trigger_tick()
must be invoked in isr_function()
and nowhere else. Pass-off points will be subtracted if trigger_tick()
is invoked anywhere else.include/leds.h
to find a function that will control the on-board LEDs.hitLedTimer_tick()
must be invoked in isr_function()
. Pass off is not allowed if hitLedTimer_tick()
is invoked anywhere else.lockoutTimer_tick()
must be invoked in isr_function()
. Pass-off points will be subtracted if it is invoked anywhere else.Mechanical switches are imperfect. When pushed or released they often “bounce” between closed and open states for a short period of time (10 milliseconds or so). For example, assume that a push-button is wired such that it is read as a '1' when pressed and read as a '0' when released. If you continually read the switch when it is first pressed, you may read the switch initially as a '1' but then a few milliseconds later, read it as '0', then '1', then '0' and so forth. This process may repeat itself a few times until the output of the switch becomes completely stable. When a switch is initially pressed (or released) and quickly changes values, we say that the switch “bounces”.
We want our laser tag system to deliver exactly one shot for each press/release of the trigger. The easiest way to debounce a switch is to use a state machine that, in turn, uses a delay to determine when the switch has stopped bouncing. Here is a simple algorithm you can implement in a state machine and use to detect when a switch has stopped bouncing. It assumes that it will take no longer than 50 ms for the switch to stop bouncing. This is a reasonable assumption for a switch that is in good working order.
Note that you need to use this process for both the press and release of the switch or push-button.
You access external pins on the JF PMOD connector via the MIO package. By using the functions in this package, you can designate specific pins as inputs and outputs, read input pins, drive values onto output pins and so forth. Here are some guidelines on how to use the mio
package. Also, you can learn a lot about how mio
works by reading the header file (include/mio.h).
mio_init()
before using any of the functions.mio.h
as these are pretty self-explanatory.mio
pin numbers in all mio
functions. Don't use the ZYBO PMOD numbers. For example, JF-1 refers to Pin 1 on the JF PMOD connector. However, it corresponds to mio
pin-number 13. The code below shows how to set up and use the correct mio
pin that connects to Pin 1 on the JF PMOD (JF-1). Note that the provided code is just an example of how to use some of the mio
functions.Here is some example code that you can use to setup the JF-1 pin and to set values on its output. These are just examples, you will need to do more than just what is shown below.
#include "mio.h" #define TRANSMITTER_OUTPUT_PIN 13 #define TRANSMITTER_HIGH_VALUE 1 #define TRANSMITTER_LOW_VALUE 0 void transmitter_init() { mio_init(false); // false disables any debug printing if there is a system failure during init. mio_setPinAsOutput(TRANSMITTER_OUTPUT_PIN); // Configure the signal direction of the pin to be an output. // Other stuff... } void transmitter_set_jf1_to_one() { mio_writePin(TRANSMITTER_OUTPUT_PIN, TRANSMITTER_HIGH_VALUE); // Write a '1' to JF-1. }
A full pin-mapping between the JF PMOD and the mio
pins is given below (this is just a copy and paste from mio.c
).
// The MIO system is the PS GPIO that communicates with the MIO pins on the ZYBO board. // MIO pins: 13, 10, 11, 12, 0, 9, 14, 15 are connected to JF pins: 1, 2, 3, 4, 7, 8, 9, 10.
For testing purposes and for general convenience, you must be able to “fire” the transmitter using either the trigger on the gun or BTN0 on the ZYBO board. Here's a good way to do that.
In general, it is always a good idea to “wrap up” or encapsulate behavior inside functions. For example, in the trigger state machine, I need to know when the trigger is pressed, but I don't necessarily need to know “what” was pressed, e.g., either the gun trigger or the push-button, BTN0. Here is a simple example from my code that shows how I did it. You may also want to take a similar approach.
In the code below, I have written a function called triggerPressed()
that returns '1' (true) whenever the trigger is pressed. As you can see, I take the OR of the gun-trigger and the push-buttons. The nice thing about this approach, is that I can say something like: if (triggerPressed())…
in my state machine code and I have something that is very readable and understandable. However, if I decide to change the triggerPressed()
code for some reason, I don't have to make any changes to my state machine. Note: see the discussion directly below that explains the ignoreGunInput
variable.
// Trigger can be activated by either btn0 or the external gun that is attached to TRIGGER_GUN_TRIGGER_MIO_PIN // Gun input is ignored if the gun-input is high when the init() function is invoked. bool triggerPressed() { return ((!ignoreGunInput & (mio_readPin(TRIGGER_GUN_TRIGGER_MIO_PIN) == GUN_TRIGGER_PRESSED)) || (buttons_read() & BUTTONS_BTN0_MASK)); }
During trigger_init()
, which gets called before triggerPressed()
, I check to see if the “trigger” is currently pressed. If it is, I presume that the gun cable is not connected. This is because pins commonly “float” to a high logic value when they are left unconnected. Otherwise, the OR of the push-button and the actual gun-trigger will always evaluate to '1' when you run the program with the gun-cable unconnected. Note that the variable ignoreGunInput
is a static bool
variable that is initialized to 0 (false). This allows me to perform all of my testing with the BTN0 push button, but it also allows me to test with the gun. I just have to make sure that the gun is connected when I start or download the program. Then I can use either the gun-trigger or BTN0 to signal a trigger press.
// Configure the trigger-pin as an input. // Ignore the gun if the trigger is high at init (means that it is not connected). void trigger_init() { mio_setPinAsInput(TRIGGER_GUN_TRIGGER_MIO_PIN); // If the trigger is pressed when trigger_init() is called, assume that the gun is not connected and ignore it. if (triggerPressed()) { ignoreGunInput = true; } }
#include "interrupts.h" int main() { ... isr_init(); // Calls state machine init functions interrupts_initAll(true); // Initializes but does not start interrupts. interrupts_enableTimerGlobalInts(); // Enable interrupts at the timer. interrupts_startArmPrivateTimer(); // Start the private ARM timer running. interrupts_enableArmInts(); // ARM will now see interrupts after this. // Default interrupt frequency is 100 kHz. }
In your laser-tag system there are two kinds of code: timing-aware code and non-timing-aware code. Examples of non-timing-aware code include: all of your filtering functions, the code that computes power, etc. The functions that implement filtering, power-computation, etc., need not know anything about elapsed time. These functions simply accept numerical input and compute values.
Examples of timing-aware code include all of your state machines: transmitter, trigger, hitLedTimer, and lockoutTimer. To work correctly, the tick()
functions for each of these state machines must be executed at a precise rate. Otherwise, it would be impossible for the transmitter state machine to generate the transmitter outputs at the desired frequency or for the hitLed-timer and lockout-timer to precisely time intervals, for example.
As the laser tag system is currently structured, there are essentially two places where you can insert any of your code: inside (or called from) main()
, or inside isr_function()
. Precise, timing-critical functions such as the “tick” functions for your state machines go inside isr_function()
. See the code-snippet for isr_function()
below. Note that the invocation order of your “tick” functions does not matter unless you have bugs in your code.
isr_function()
is guaranteed to be precisely invoked at 100 kHz, or 100,000 times per second. When it is time for this function to execute, the CPU stops whatever else it is doing, executes isr_function()
and the quickly returns back to whatever else it was doing. You can assume that any functions that are invoked in the body of isr_function()
are guaranteed to execute at the 100 kHz rate.
Note: It is critical that any functions that you insert into isr_function()
execute very quickly or timer interrupts will be missed. You should verify (using the interval_timer
package, for example) that the functions that you put inside isr_function()
collectively complete execution in much less time than 1 period of 100 kHz. If the total execution time of all of your functions exceeds the period, you will miss timer interrupts and introduce noise and jitter into your system. Actually, if you consume too much time in isr_function
there won't be any time to run your filters. Be careful here.
void isr_init() { // Insert initialization code (e.g. state machine init functions) here. } void isr_function() { // Insert timing-critical code (e.g. state machine tick functions) here. }
As a recommendation, declare all of your state machine global variables volatile
, even the current state variable. Otherwise, you will need to analyze each of your global variables for the condition explained below to see if they need to be volatile
. This condition potentially applies to all state machine global variables (uint32_t, etc.), not just boolean variables.
In some cases it is necessary to add the keyword volatile
to a variable declaration. Consider an example with the hitLedTimer state machine. The variable timerStartFlag
, when set to true, starts the hitLedTimer state machine. Later, when the state machine is finished, it will set timerStartFlag
to false. In the partial example shown below, you can see that the keyword, volatile
has been added to the declaration for the variable, timerStartFlag
.
// When true, the timer starts running. volatile static bool timerStartFlag = false; void hitLedTimer_start() { timerStartFlag = true; }
The volatile
keyword essentially tells the compiler to avoid optimizing code that accesses this variable. Here is the principle at work. Specifically, if a variable is written to by a function called from isr_function()
(e.g., your tick functions), and that same variable is read by a function called from main()
, you must use the volatile
keyword in the declaration for that variable. The volatile
keyword informs the compiler that it should not attempt to optimize read access to the variable.
The following DPRINTF macro is a helpful alternative to the normal printf function call since it can be switched off by making sure DEBUG is not defined. If you have many DPRINTFs sprinkled throughout your code, you can turn them all off at once while still leaving them in your code for later use.
The DPCHAR macro is a low-overhead way to print a character to the console and is useful for passing off the trigger state machine. The macros can be left in the code for later test and debug, but all of them can be disabled when running under normal use by not having DEBUG defined (commended out).
Place the following code in your .c file near the top, but below other #include lines.
// Uncomment for debug prints // #define DEBUG #if defined(DEBUG) #include <stdio.h> #include "xil_printf.h" #define DPRINTF(...) printf(__VA_ARGS__) #define DPCHAR(ch) outbyte(ch) #else #define DPRINTF(...) #define DPCHAR(ch) #endif
Note: When hardware interrupts are enabled, make sure debug prints called from your state machines are not enabled. Otherwise, you will get interrupt overruns and unusual behavior. The only exception is when DPCHAR() is selectively used (e.g. when passing off your trigger state machine).
For more ideas on getting visibility into the operation of your code, here's a link to a page describing how to debug state machines.
It may be easier to test/develop the transmitter state machine without using interrupts. Here is one approach I would suggest.
Here is a test-function that I used with my own code. You would need to either modify this code or add a transmitter_enableTestMode()
function to your transmitter code. In my case, transmitter_enableTestMode()
turns on my debug prints and shortens my waveform to 200 so I don't have to wade through too much output. During normal operation, I don't use this test mode.
Once you have verified that your transmitter state machine is working properly, you can disable the test mode, put the transmitter_tick() function into isr_function() and verify proper operation with an oscilloscope.
// Prints out the clock waveform to stdio. Terminates when BTN1 is pressed. // Prints out one line of 1s and 0s that represent one period of the clock signal, in terms of ticks. #define TRANSMITTER_TEST_TICK_PERIOD_IN_MS 10 #define BOUNCE_DELAY 5 void transmitter_runTest() { printf("starting transmitter_runTest()\n"); mio_init(false); buttons_init(); // Using buttons switches_init(); // and switches. transmitter_init(); // init the transmitter. transmitter_enableTestMode(); // Prints diagnostics to stdio. while (!(buttons_read() & BUTTONS_BTN1_MASK)) { // Run continuously until BTN1 is pressed. uint16_t switchValue = switches_read() % FILTER_FREQUENCY_COUNT; // Compute a safe number from the switches. transmitter_setFrequencyNumber(switchValue); // set the frequency number based upon switch value. transmitter_run(); // Start the transmitter. while (transmitter_running()) { // Keep ticking until it is done. transmitter_tick(); // tick. utils_msDelay(TRANSMITTER_TEST_TICK_PERIOD_IN_MS); // short delay between ticks. } printf("completed one test period.\n"); } transmitter_disableTestMode(); do {utils_msDelay(BOUNCE_DELAY);} while (buttons_read()); printf("exiting transmitter_runTest()\n"); }
My printed console output looks as follows (the printed output changes depending upon the current switch value).
starting transmitter_runTest() init_st wait_for_startFlag_st output_low_st 0000000000000000000000000000000000 output_high_st 1111111111111111111111111111111111 output_low_st 0000000000000000000000000000000000 output_high_st 1111111111111111111111111111111111 output_low_st 0000000000000000000000000000000000 output_high_st 1111111111111111111111111111111111 completed one test period. wait_for_startFlag_st output_low_st 0000000000000 output_high_st 1111111111111 output_low_st 0000000000000 output_high_st 1111111111111 output_low_st 0000000000000 output_high_st 1111111111111 output_low_st 0000000000000 output_high_st 1111111111111 output_low_st 0000000000000 output_high_st 1111111111111 output_low_st 0000000000000 output_high_st 1111111111111 output_low_st 0000000000000 output_high_st 1111111111111 output_low_st 0000000000000 output_high_st 1111111111111 completed one test period.
For each of your state machines, you will write a runTest()
function and demonstrate its operation for pass off. You will need to run each of the runTest()
functions separately. One option is to modify and recompile main.c to run each of these functions individually. If you don't like recompiling between each of your tests, you can use Button 3 to signal when to exit each individual test and move to the next test. The required operation for each test is described below.
The goal is to prove that your transmitter state machine accurately and reliably produces the correct waveform at JF-1. You will test both continuous and non-continuous mode. Non-continuous mode should be the default. In non-continuous mode, the transmitter state machine generates a single 200 ms square wave at the selected frequency when transmitter_run() is invoked. In continuous mode (used for testing), the transmitter continuously generates a waveform at the pin until transmitter_setContinuousMode() is invoked with an argument value of false. You must test both non-continuous and continuous modes of operation.
You can access this signal at the “Transmitter Probe” pin on the development board. To test the transmitter state machine, do the following in a while-loop in transmitter_runTest
. In an endless loop in transmitter_runTest()
do the following:
transmitter_setFrequencyNumber()
function.transmitter_run()
.Demonstrate to the TA, using an oscilloscope probe attached to “Transmitter Probe” pin, that the waveform is the correct shape and frequency for each of the 10 slide-switch settings. You should be able to see that the waveform is generated for 200 ms with a “dead time” of about 400 ms where the output goes to 0 between each successive generation of the waveform.
Access and display the transmitter output as described above. In your test function, do the following:
transmitter_init()
, transmitter_setContinuousMode(true)
, transmitter_run()
prior to entering an endless loop.transmitter_setFrequencyNumber()
function.Demonstrate to the TA that the waveform that is displayed is continuous and changes to the correct frequency as the slide switches are changed.
Goal: Demonstrate that the trigger state machine correctly debounces the trigger (BTN0 for this test). To perform this test, use a debug print inside your state machine.
As you transition to the state when the “press” of the trigger has been successfully debounced, print the 'D' character to the console using the DPCHAR() macro shown above. Make sure that you only print the 'D' character once as you enter the state when the trigger switch has been successfully debounced. Similarly, print out a 'U' character when the release of the trigger has been debounced. If you only print out a single 'D' and a single 'U' for each press-release of BTN0, the trigger is probably getting debounced properly.
In trigger_runTest()
, enable the trigger state machine and demonstrate the required behavior to the TA.
Goal: demonstrate that your hitLedTimer state machine illuminates LD0 and drives a '1' onto the JF-3 pin for 1/2 second each time it is activated. You will see LED1 blink on and off at 1/2-second intervals if this is working correctly. LED1 is directly connected to JF-3 and is provided for debug and pass off.
In your hitLedTimer_runTest()
function do the following inside a while-loop:
hitLedTimer_start()
,hitLedTimer_running()
is false (use another while-loop for this).Demonstrate to the TA that LD0 and LED1 is blinking at a rate of 1/2 second (this will be approximate because this will be a visual inspection). Also show the TA that pin “Hit Detected” has the appropriate waveform by attaching an oscilloscope probe to the “Hit Detected” pin.
Goal: prove that the lock out timer is accurate and that the functions work as specified.
In lockoutTimer_runTest()
do the following:
lockoutTimer_start()
,lockoutTimer_running()
is true (another while-loop),lockoutTimer_running()
is false, stop the interval timer,
Show the TA the execution of lockoutTimer_runTest()
.
TAs: please pay attention to the following during pass off and when you are grading source code.