Introduction
There were a number of issues related to the Two Digit Timer project that need to be reviewd. Some of the issues relate to the differences between writing code for hardware compared to writing software, and some have to do with problems caused by how I told you to structure the code.
Clock Management
In the past I have designed projects for this course so they would be implemented as monolithic pieces of code, with the use of a library for the delay procedures being a new feature in the course last semester. For this project, I had you divide the application into four modules, with a main program that does nothing other than to start the other three modules running. I wanted to make the three manager modules work somewhat like generic library code and in that spirit I had you pass the clock rate as a parameter to each module’s run routine, much as we did when implementing the msecDelay() procedure.
The problem (I think!) is that I didn’t understand Handel-C’s scoping rules for macro expressions. The clock rate gets set as a value for the preprocessor symbol PAL_ACTUAL_CLOCK_RATE, which in turn got passed either literally or using a macro expr to the manager macro procedures and from there to the msecDelay() macro procedure. Most of the time this approach works just fine. But in certain cases it reliably causes the compiler to emit an error message and to stop compilation. From what the error message says (a constant, always one less than the clock rate) is too big to fit in the counter variable defined in msecDelay(). On the surface, it looks like a bug in the compiler, but it is arguably just a poorly worded error message that is generated when the clock rate variable (preprocessor symbol or macro expression) gets defined in multiple places in the code. Whether it is a compiler bug or not, we need a way to work around the problem.
My workaround is to include the following header file (and no others) in each of the four modules:
The key item at this point is the second to last line, which declares ClockRate as an external macro proc. Then in the main module I put:
If you are not familiar with traditional C language rules it might look strange to declare variables and macros to be extern in the header file and then to immediately define them. In C a symbol may be external in any number of modules but must be defined in exactly one. The language specifically allows a symbol to be declared both extern and to be defined in the same module just so the same header file can be included in all modules.
The next step in the workaround was to eliminate the parameter being passed to each manager’s run procedure. Each module is now sharing the single global definition of ClockRate given in the main module. They can all pass ClockRate as the first argument for all their calls to msecDelay() and neither the compiler nor the linker ever complains.
The Workaround Is Now Perfect!
Updated
A previous version of this web page reported a problem with multiple clocks being defined, requiring you to “select one to follow” when starting a simulation run. With a student’s help (thank you!) I found that it is necessary not to #define either PAL_TARGET_CLOCK_RATE or PAL_TARGET_CLOCK_RATE anywhere except in the main module. So you can think of it as the main module sets up the clock (once) and makes it available to the other modules as a global macro expression, ClockRate. (All global variables must be defined exactly once, and we are using the main module as the place for defining this global macro expression as well as the other four variables that are declared extern in the header file.)
An arguable inelegance remains: two_digit_clock.hch has a #include for pal_master.hch, which is not actually needed in the Timer Module and a #include for delay_procs.hch, which is not actually needed in either the main module or in the display manager module. The argument would be that it would somehow be more proper to include only those header files in two_digit_clock.hch that all the .hcc files in the project actually need, and to have the individual .hcc files include any additional header files they need. But this inelegance is offset by the fact that it saves us having to write a separate lines to include pal_master.hch in three out of four files in the project, and to write lines that include delay_procs.hch in the two files that need it. This design choice does not effect what actually gets produced when the project is built, but it does add some (small) amount to the time it takes to do a build because the C preprocessor is sometimes processing files that are not actually needed. But it makes the overall structure of the project less delicate: all modules get all the “standard” header files for the project and there is no need to think about exactly which one is needed in each module.
Coding Issues
This is a good chance to point out some common coding issues that arise as a result of not yet fully understanding how Handel-C and FPGA hardware work.
-
There is no loop in the main module.
The three manager modules are coded as endless loops, so you have to launch them all in parallel in order to get each one started. But since none of them ever return to main() there is no point in coding a while around them; it will never get to the second iteration. The following code works fine:
extern macro proc buttonManagerRun(); extern macro proc displayManagerRun(); extern macro proc timerManagerRun(); void main(void) { par { displayManagerRun(); buttonManagerRun(); timerManagerRun(); } } -
Do not repeat functionality across modules.
The Button Manager should maintain the states of the two global variables, and nothing else. Think of this manager as having one thread for updating the states of both switches in local variables on every clock cycle and two FSM threads for managing the global variables. The first thread is simply there to avoid the need for scattering calls to PalSwitchRead() throughout the two FSM threads.
For both buttons there are four states: (1) Waiting for the switch to close, (2) Waiting for the switch to bounce, (3) Waiting for the switch to open, and (4) Waiting for the switch to bounce (again). The state of each global variable is updated when going from State 1 to State 2 and, for the fast button only, when going from State 3 to State 4. Here is code for the run button:
while (1) { do { delay; } while (b0 == 0); State 1 run = ~run; msecDelay(ClockRate, 10); State 2 do { delay; } while (b0 == 1); State 3 msecDelay(ClockRate, 10); State 4 }The Display Manager and the Timer Manager modules should not do switch debouncing: that would be redundant.
The Display Manager should do nothing but update the visual outputs: the LEDs and seven segment displays. The seven segment display logic is complicated a little bit by the fact that you want stop updating them as soon as run goes off even though the timer manager will be somewhere in the midst of a timing delay when the user clicks the button:
while (1) { if (run) { par { PalSevenSegWriteDigit(PalSevenSegCT(0), tens, 0); PalSevenSegWriteDigit(PalSevenSegCT(1), units, 0); } } else { delay; } }Note the use of a delay statement in the else clause of the if statement. Every Handel-C statement must take exactly one clock cycle to execute, but without the else clause, the if statement has only combinational logic (testing whether run is true or false) and nothing to execute when the combinational logic evaluates to false. The compiler will complain about “breaking a combinatorial cycle” if you omit the delay statement that gives the hardware something to do when there is nothing to be done.
-
Think Parallel.
For example, in the Timer Module, you have to increment the units and tens variables at the end of each time interval. For the same amount of hardware as would be needed using a sequence of if statements, updating the value modulo 60 can be done entirely in one clock cycle. Like C, C++, and Java (among other languages), Handel-C provides the triadic ?: operator for situations like this. If you are not already familiar with this operator, the following example should show how it works:
par { units = ((units == 9) ? 0 : (units + 1)); tens = ((units == 9) ? ((tens == 5) ? 0 : (tens + 1)) : tens); }Remember, the two assignment statements could have been written in the opposite order without changing anything because they are inside a par block. That is, the references to units on the righthand side of both statements refer to the same value: the value of units before the single clock pulse that executes both assignments.