Written by Pat Hanrahan; edited by Omar Rizwan
Due: Tuesday, April 25, 2017 at 5:00 PM
This assignment involves building a ‘clock’ using a 4-digit 7-segment display.
The goals of this assignment are:
For this assignment, we are introducing
libpi, which contains header files and (binary) reference implementations of the code you will be writing throughout the quarter. You will be creating your own
libpi library through the course assignments. Go to the folder where your cloned
cs107e.github.io repo resides and run
git pull. This should give you a
libpi directory with the relevant files.
From there, go to the parent directory (if you
ls, you should see the
cs107e.github.io folder) and then clone the
assign2 branch from your 107e assignment repo to get the starter code:
$ git clone -b assign2 https://github.com/cs107e/[YOUR-GITHUB-USERNAME]-assignments assign2
For this and future assignments, it will be important that you clone this branch at this location. This means if you are in your
assign2 directory, the new
libpi folder should live at
../cs107e.github.io/libpi/. To check, cd into the
assign2 folder and try:
$ ls ../cs107e.github.io/libpi/
Note: If the response is
../cs107e.github.io/libpi/: No such file or directory then something is wrong.
Familiarize yourself with the starter code. Take a look at the provided Makefile. You’ll notice that the include depends on the relative path described above. The project also includes a few new magic files (
cstart.c) that are necessary for C programming. We will be explaining their purpose in the upcoming lectures.
For the basic part of this assignment, you must implement functions across three different C files:
In the starter code, we have given you stubs for functions you will need to implement. We have also given you the header files (in
cs107e.github.io/libpi/include/) which have prototypes for all these functions.
gpio.c(header file: gpio.h)
timer.c(header file: timer.h)
clock.c(header file: clock.h)
Later in this writeup, we will go into detail about the order in which we recommend you implement these functions, what they should do, and how exactly they build on each other.
In general, assignments will follow this kind of structure: we will give you some function prototypes to implement in some C files.
Each C file is meant to serve as a discrete module in a library of reusable Raspberry Pi functions that you will build up during the quarter.
Some of the functions you implement will take parameters, like a GPIO pin number or a GPIO function type. In those functions, you should check that the arguments are valid; if they are not, your function should just return without doing anything. For example, if someone tries to turn on GPIO pin 300, catch that and ignore the request; don’t blunder on and do something broken. If the function is expected to return a value, simply return 0. (This practice of returning 0 on error isn’t great, but at least we’ve defined the behavior. The C language has no facility for exceptions.)
We have given you the file
main.c which has a
main() that calls
clock_run(). When you
make and boot your program on the Pi normally (as opposed to
make test), it will call
main.c, which then calls into the
You should not modify
main.c at all, because you will implement all functionality in the
You should not modify any header files in
libpi/include/; all your modules should provide exactly the public interface we’ve defined. You shouldn’t expose extra
clock_ functions, for instance. If you want to declare ‘private’ (
static) helper functions to use within a module, declare them at the top of that C file.
Now that you’re writing larger programs, we want to introduce you to more sophisticated ways to test your program than in assignment 1. We hope these ideas will serve you well throughout your programming life.
The starter code for this assignment includes a file
test.c. The file defines an alternate
main() function. Instead of initializing and running the clock, this
main() function makes a series of calls to
assert() in order to validate the basic operation of the functions you have implemented in
timer.c. You can run the tests by running
Recall from lab2 how
assert() uses the red and green LEDs on the Pi as a simple status indicator. If a test fails, the program halts and blinks the red LED of doom. If there are no failures and the program runs to completion, the green LED of happiness will light up. By the time you’re done with this assignment, all the tests should pass. Make that little green light shine!
You can add your own tests here as well. Just make sure that your changes to
test.c are not needed for the current tests to pass (in other words, all functional code should live in the modules).
Wire up your breadboard with a 4-digit 7-segment display.
Hopefully you finished setting it up in lab. Test it with jumper cables so that you know the wiring is correct before you connect it to the Raspberry Pi.
Connect your Pi so that you can control the display.
Connect the Raspberry Pi GPIO pins 20-27 to the 1K current limiting resistors that drive the anodes A, B, C, D, E, F, G on your breadboard.
The 8th pin controls the decimal point, which is optional. Connect GPIO pins 10-13 to the base of the transistors controlling digits 1-4. At this point if you output 3.3V on GPIO 20, and 3.3V on GPIO 10, you should turn on segment A of digit 1.
Implement C functions to set a GPIO pin’s mode.
First, implement this extremely useful function in
gpio.c to set whether a pin is input, output, or some other mode:
void gpio_set_function(unsigned int pin, unsigned int function);
You can find a description of the functions and their parameters just above their declarations in
gpio_set_function, refer to the various functions defined in the documentation for the function select registers. In this assignment, we will be using only INPUT and OUTPUT, but make sure
gpio_set_function works for other GPIO functions. Finally, make sure these functions can be called multiple times with different pins. Each call to
gpio_set_function should change the function of only the pin passed in as an argument; the function of all other pins should be unchanged.
You should also implement
gpio_set_output, which are simple wrappers around
gpio_set_function to set a pin as an input or output pin.
Then you will implement
gpio_get_function. This is used by the testing framework to automatically test whether your
unsigned int gpio_get_function(unsigned int pin);
Now let’s test these gpio operations. Review
test.c and comment out the calls to
test_gpio_read_write() (you will implement the code to make those tests work soon, but not yet). Now run
If the green LED on the Pi turns on, all tests ran successfully – hooray! If you see the red LED blinking, this means you are failing a test. If neither LED lights, your program may be freezing or crashing somewhere during the tests.
If you don’t get the green light, comment out all tests and then add them back one by one to determine which specific test is failing. Use that information to narrow in on your bug, and fix and re-test until you achieve green light nirvana for all tests in
Now review the test cases we gave you and consider what isn’t covered. For example, what about testing alternate GPIO functions or GPIO pins across the full range of 54? Add tests of your own to validate these options. Re-run to verify that you also pass your new tests before moving on.
Implement C functions to turn a GPIO pin on and off.
Next, implement another extremely useful function in
gpio.c for controlling GPIO pins,
void gpio_write(unsigned int pin, unsigned int value);
As in the last step, this function is described in detail above its declarations in
Implement this function and add tests to
test.c to check if
gpio_write works properly (along with the
gpio_set_function from your last part) by turning on and off various segments on different digits on your display.
Then you will implement another function,
gpio_read. This is used by the testing framework to test whether your
gpio_write works. Check the Broadcom BCM2835 peripheral manual: which device register should you read?
unsigned int gpio_read(unsigned int pin);
Now uncomment the call to
test.c. Rerun the tests. You should be passing the read/write tests now! Supplement the starter tests with some test cases of your own to make sure these functions work on a variety of the other GPIO pins.
Implement the logic for displaying a numerical digit on a 7-segment display.
clock.c, create an array with 16 entries, one for each hexadecimal value between 0 and 15. Each entry in the array should be a byte (8-bits). Bit 0 (the least significant) will represent segment A, bit 1 segment B, and so on. If a bit is set, then that segment should be lit. For example, the bit pattern for
0b is what you put in front of a binary number literal, just as you’d put
0x in front of a hexadecimal literal.)
Bit 7 (the most significant) will be used to represent
DP, the dot. Since we won’t be using the dot in this assignment, bit 7 should always be 0.
Create patterns for all the digits 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. You won’t need A-F for this assignment, but they may be useful in the future. Test this part by writing a
clock_run() that displays a number on the display for now. Run
make install (instead of
make test) to run main, which calls
clock_run() for you.
Implement a C library to access the Pi’s system timer.
In order to implement a clock, we’ll need some way to determine the passage of time. Thankfully, the Raspberry Pi includes a “system timer”, an on-board peripheral that is initialized to zero when the Pi powers up and then is continuously incremented once every microsecond behind the scenes.
timer_get_time function in
timer.c that fetches the current system time from the Raspberry Pi. Chapter 12 of the Broadcom BCM2835 Peripherals Manual contains the documentation for the system timer peripheral, and the function’s description is in
timer.c above its declaration.
Note that, for this assignment, we only care about the lower 32-bits of the system timer. Don’t forget that we use ARM physical addresses, not bus addresses (0x7E… for peripherals), so you’ll need to change the 0x7E… prefix in any peripheral address to 0x20.
Now you can uncomment the call
test.c. You should be passing all our library tests now – super! Now you’re ready to implement the clock.
Write the display refresh loop.
Only one set of GPIO pins drives the 7 segments for all of the four digits. These pins are shared by the four digits, and we use another, different set of pins to control which digit is currently active.
We are going to drive all four digits by continuously looping over the digits one-by-one. In the inner loop, we will turn on a digit, wait a short length of time, and then turn off the digit. You might think that turning a digit on and off would cause it to flicker. The key is to sequence through the digits so fast that our eyes cannot see them changing. Good thing computers are fast!
In order to refresh the display, we will use the
timer_get_time function you implemented in the previous step. We have also provided a function
delay_us that uses
timer_get_time to wait for the desired number of microseconds.
Implement the display refresh loop in
clock.c. Loop though all four digits, turning each on for 2500 microseconds (2.5 milliseconds). Do you see any flicker?
Finally, implement the clock.
In the basic assignment, the clock should display the minutes and seconds that have passed since the program began.
The current time should show as zero when the program begins. The inner loop in
clock_run should perform one refresh cycle of the 4-digit display using the current time, and then increment the time by the length of time needed to perform the refresh. Make sure to test that the timer is calibrated correctly so the clock is running at the right rate.
One challenge with a clock is setting the time.
Add two buttons to your clock breadboard and connect them to GPIO pins 2 and 3. Build a user interface that allows you to set the time.
Try to design an interface that is easy to use and that works even after the clock starts running. It can be challenging to build an interface with just a few buttons!
Make sure to document your interface in a README.md file so that your grader can test it out!
The starter code contains additional code files
cstart.c. These files provide the bare minimum of startup needed to set up a C program to run in the bare metal environment. A bit later in the course, we will discuss what this code does and why is necessary. In the meantime, sit tight, and don’t think about it too much. The starter project also includes a file
memmap, which is used by the linker. We will also cover why that file is needed soon.