Project 3 Building a Stopwatch Using ZYNQ's Triple Timer Counter (TTC) Module
Assembly programming Using Timers and Polling
In this project, you will implement a stopwatch on the Blackboard by writing assembly code to access and control the needed I/O devices (like the pushbuttons and seven-segment display), and to drive the user interface.
When designing projects that use several hardware interfaces (and for more involved project in general), its good practice to identify and use a well-thought design partition. A good partition organizes source code into blocks and subroutines that “modularize” particular functions that can be implemented and tested somewhat independently, and then assembled into the overall solution. This is analogous to some of the larger Verilog designs in the digital logic course, where more complex circuits were built using previously designed Verilog modules. In the requirements, you are encouraged to create subroutines to handle particular tasks, and to organize them in source files according to their functions.
Building a stopwatch requires a more accurate and stable time base than can be attained by counting software instructions. The ZYNQ device includes several timer circuits, and one of them, called the “triple timer counter” (or TTC), will be used in this project.
Going forward, we will use several more of ZYNQ’s built-in peripherals. All of ZYNQ’s peripheral circuits, including those in the FPGA, are accessed through registers at fixed locations the ARM bus. To write drivers for these peripheral circuits, you’ll need to understand how they work, and you’ll need to know the location and function of their registers. The FPGA-based peripherals are described in the resource pages, as are many of the most-used peripherals (like the timers) that are a permanent part of ZYNQ. The “Zynq-7000 SoC Technical Reference Manual” is the definitive source for ZYNQ’s permanent peripherals. You should get familiar with these information sources, and be able to consume and apply the information they contain.
Before you begin, you should:
- Be comfortable working in SDK;
- Be comfortable writing and executing assembly language programs on the Blackboard;
- Be familiar with reading and applying the information in the Blackboard Reference Manual;
- Be familiar with the ARM reference manual.
After you’re done, you should:
- Understand polling and how and when it is applicable;
- Understand ZYNQ’s timer/counter module and how to program it;
- Be more comfortable implementing larger systems in assembly.
Seven Segment Display
The seven segment display was used in the digital logic course stopwatch project, and it is a central part of this project as well. In the digital logic course, a hardware circuit was designed to control the seven segment display; in this project, you will write software to control the display. A display interface circuit that implements control/interface registers and generates display timing signals is provided as a part of the Blackboard configuration file – you must learn how that circuit functions, and then program it appropriately (you can find controller details in the seven-segment controller Reference Manual page).
The ZYNQ chip includes several timer circuits that can be used to establish regular, periodic time bases. One such circuit, called a “triple timer counter” (TTC), will be used to produce a periodic 1KHz signal that can be used as the time base for your stopwatch. In later projects, other hardware timing circuits will be introduced. The TTC can be configured to generate a timing signal that drives one bit in a register to a ‘1’ at a regular and stable interval. The ARM can read that bit, and when it reads as a ‘1’, an exact amount of time will have passed (and reading the bit will automatically clear it back to a 0).
The process of regularly and continuously reading a register value to check for a signal/value change is known as “polling”. Polling is conceptually simple, and it is often used in simple situations. But it is also extremely inefficient – processor time is wasted in continually reading the same register over and over. A better method is to configure the timer circuit to generate a “processor interrupt” at the end of the programmed interval – this is explored in the next project. You can read more about polling and interrupts in the topic document.
Using Multiple files
Most programs developed in industrial settings use a hierarchical design approach, with individual functions located in separate source files. This mirrors the structural Verilog design approach we used in the digital circuits class, and for the same reasons. Placing code in separate files keeps any one file from getting too large and complex; it allows for more general modules that can more readily be reused in later projects; it enhances readability by abstracting away certain details that aren’t so important when trying to grasp the overall design; it helps with debugging and documentation; etc.
In general, code should be partitioned into different source files according to function. For example, the code to drive the seven-segment display might be placed in a source file called “sevensegdriver” or something similar. Then, when all the sub-functions have been defined and created, they can be accessed from the main program (again, this is the same model as is used by structural Verilog).
To use multiple source files, you can simply create additional source files with the “.s” suffix in your project. Any labels you use in any source file can be defined as “.global” so the linker tool can assemble them all into a single, cohesive executable file (see the topic document for more information). Note that all needed source files must be included in the top-level project, but the source file name is not used by the tools. The source file name is useful to human readers, so choose relevant names.
1. Display values on the Seven Segment display
Refer to the resource documents to learn which registers and which bit fields within the registers need to be accessed to interface with the various input and output devices, and write some useful GPIO functions:
- Write a function to display a number on an individual display digit. Your function should input two arguments: the number of the digit to be displayed (1-4), and the value to display. Make sure your subroutine can handle any input given it – even inputs that are out of range (for example, what happens if you specify a digit number higher than 4?);
- Write a function that can read the slide switches and return a 12-bit value;
- Write a function that can read the current value of the pushbuttons.
Use your functions to create a program that reads a 12-bit number from the switches, and a 4-bit number from the pushbuttons. Displays the number from the switches on the first three 7-segment display digits (treat each group of four switches as one hexadecimal digit), and display the number read from the pushbuttons on the fourth digit.
2. Create and test four-digital decimal counter
Write a program that displays a decimal count on the 7-segment display. The counter should start at 0 and count to ‘9999’ then rollover back to zero. Since it’s a decimal counter, each digit should only count to 9 before rolling over to zero and incrementing the next digit (recall this defines a “binary coded decimal”, or BCD counter). The four BCD digits used in the counter counter value will need to be stored somewhere - in this case, we will store the values on the stack (they could be stored in registers, but this provides an excuse to use the stack). In your main program, setup a frame pointer and use 4 stack variables to hold the current value of the counter. Since the stack is in main memory, when you want to modify the counter, you’ll need to load a digit into a register, modify it, then store it back. In your main program header comments, be sure to identify where the counter variables are stored.
Use two pushbuttons to clear and increment the counter. Use slide switches to choose which of the four digits to increment (otherwise, to increment the most significant digit, you’d need to count to 1000!). When the clear button is pressed, reset the four BCD counter digits to zero and update the display. When the increment button is pressed, update the appropriate digit, and update the display (the counter should only increment/add once for each press of the button). Note that when incrementing the least-significant digit, all digits should update at the appropriate time (that is, if the counter is showing “99” and you increment, the counter should show “100”).
Hint: You can’t simply check the increment button for a ‘1’ value in your polling loop - you need to detect a positive edge transition (0 to 1) and increment just once for each such transition. To detect a change in state, you need at least two readings from the buttons.
3. Make a control flow for your stopwatch
Modify your program to add Start, Stop, Increment, and Clear functions (Start and Stop can use the same button). When Start is pressed, increment the count continuously until Stop is pressed. If the counter is stopped and the Clear button is pressed, reset the count to zero. If the counter is stopped, add 1 to the count value each time Increment is pressed.
If you simply execute the program without any built-in delay, the counter will count far too fast to be useful. To slow the counter down, create a delay loop (a “dummy” for-loop) that simply counts to some suitable number (recall in the previous project that a delay loop counting down from 0x30000000 required about 1 second). Try to define a delay as close to 1ms as possible, so that the counter’s LSB increments once per millisecond. Finding the correct delay value will take some experimentation, so it’s a good idea to use a constant at the top of your code that you can find and modify easily.
How you implement the control flow of your stopwatch’s main program is up to you. You could implement it with multiple loops, one for ‘idle’ and one for ‘running’. Or, you could use a single loop with different control structures based on whether the stopwatch is running or not. Experiment!
4. Use ZYNQ’s Triple Timer Counter (TTC) module for the time base
Now that you’ve created a stopwatch that uses a software delay as a timebase, you can modify your program to use a more accurate hardware timer as a timebase.
Configure one of ZYNQ’s Triple-timer counters (TCC) to generate a 1ms signal, and poll that signal to get a more accurate time base. Create a seperate file containing all of your TTC related functions (Make sure you make necessary functions accessible to your main program through the ‘.global’ directive). Check the background documentation for help getting started with the TTC.
After you’ve written configuration functions for the TTC, use your functions in a initialization subroutine that:
- disables the counter
- configures the timer clock
- sets the prescale value
- sets the interval
- enables the interval interrupt flag
- enables the timer in interval mode
Calculating Timer Parameters: When operating in interval mode, the timer has 2 parameters that determine the length of the interval. One parameter defines the prescale value that divides the 111MHz peripheral clock by 2^(prescale+1). Setting this value to 0 divides the pclk by 2, setting it to 1 divides by 4, setting it to 2 divides by 8, etc. The count value will then increment at the divided clock rate, and the second parameter (the value in the interval register) will determine how many clocks pulses are counted in each interval.
The time it takes for the timer to count from 0 to the interval register’s value can be calculated as
itvl*pclk/(2^(div+1)). For a 1ms interval you can divide the clock by 2^10 (by using a prescale value of 9) to get a clock frequency of ~108.5khz. This gives approximately a 1ms interval period (1/108500 * 108)= ~0.995ms, which is close to 1ms.
Modify your TTC subroutine to accept parameters for the interval value and the prescale value.
Using the Timer for Delay through Polling
Now make a function that simply polls the TTC0 ISR (interrupt status register). The function should continously read the appropriate regsiter in a loop, checking for bit 0 to be set (remember the bit is cleared every time it is read). If you have correctly set the prescaler and interval, bit 0 should be set to a ‘1’ about once per millisecond.
Modify your stopwatch program to initialize the timer with the necessary prameters and replace the software delay function from requirement 3 with your new timer delay function.
Bonus: You can acheive an even more precise timebase! Use a bit of math, and find a combination of interval and divider value that gets you as close as possible to 1ms. To get the highest precision, you want the clock to be as fast as possible, but the interval length is limited to it’s register size (16-bit). The largest interval value, then is 65535 ((2^16 == 65536) -1 == 65535)
1. Dynamically change the time base of the timer
Use the slide switches to change the rate at which the timer counts. Have the timer count 1KHz when the switches are 0, 500Hz when the switches are 1, 250Hz when the switches are 2, etc.
2. Create a 7-digit Counter
Modify your counter to hold information for higher count values. Add stack variables for holding seconds * 10, seconds * 100, and seconds * 1000. Since you have 7-digits of precision and 4-digits to display them on, use a switch to change between displaying the higher and lower count value.
3. Create a timer-based game
Create a game that starts with a random number loaded into the display, and then counts down to zero from that random number. The object of the game is to press a “stop" button to stop the timer as close to zero as possible. When the timer stops, the display should show the number of milliseconds before or after the zero-crossing. For maximum points, if the button is pressed after the zero crossing, show the number of elapsed milliseconds with a minus sign in the most significant digit.
The game should start with a button press to load a random number (between about one second and ten seconds) into the counter. One way to generate a psuedo-random number would be to setup a free-running counter in the ZYNQ timer/counter module. Then pressing the start button can read the counter value; since there is no accurate way of timing when an user might hit the start button, the counter value will effectively be a random number. Another acceptable method would use the slide switches to select a number between 0 and 255, and then a simple algorithm to map that input number to the required range.
4. Create a reaction timer
Create a Reaction Time Monitor (RTM) that can indicate how quickly a user can respond to a stimulus. In operation, the RTM is initialized when a “start" button is pressed. Immediately after the start button is pressed, the 7seg display is set to show all 0’s, and then a random time later (between about 1 and 10 seconds), a “react now" LED illuminates and a millisecond timer starts. When the timer starts, the 7seg display shows the timer value as it counts up in milliseconds.
As quickly as possible after the react LED illuminates, the user must press a “react" button to stop the timer. The stopped timer will contain the number of milliseconds between the react LED being illuminated, and the button being pressed, and that time will be shown on the 7seg display. Pressing the start button again will clear the timer and begin a new round.