cu-summer-stem-embedded-systems
The Cooper Union - Summer STEM Embedded Systems: C-ing Beyond Arduino
Starting with programming an Arduino Uno Rev3 with the Arduino framework and progressing to bare-metal C on the underlying AVR microcontroller, students will dive headfirst into the embedded world where all the safety is off, and all our favorite abstractions are gone. Students will start with an Arduino and get comfortable interfacing with hardware using digital IO, PWM, ADCs, interrupts, and communications protocols like UART & I2C. Once familiar, the facade will get peeled away, and students will get a rundown of a Unix shell, computer architecture, compilers, build systems, and debuggers. Using this new-found knowledge, students will go through the paces of writing a device driver using one of the available memory-mapped peripherals on an AVR microcontroller to gain familiarity with bare-metal C. Throughout the course, students will get constant exposure to practical embedded applications and by the end, will have to come up with and implement an embedded application from scratch.
Student outcomes:
- Gain familiarity with embedded tools and hardware devices.
- The ability to apply embedded programming techniques.
- Successfully traverse datasheets and sample code.
- Debugging techniques (software and hardware) along with collaboration.
Copyright & Licensing
Copyright (C) 2025 Jacob Koziej <jacobkoziej@gmail.com>
Distributed under the CC-BY-NC-SA-4.0
.
Syllabus
The following is a tentative syllabus for the Summer of 2025. Please check this site frequently for the most up-to-date information.
Course Overview
Instructor: Jacob Koziej (EE ’25)
Teaching Assistants: Marco Chen (EE ’28), Matthew Jeong (EE ’28), Charles Wan (STEM ’25)
Location: 41 Cooper Square, Room 603 (ICE Lab)
Time: Monday–Thursday, 09:00–15:00
Office Hours: On request
Contact: TEAMs message or email (<jacob.koziej@cooper.edu>
)
Prerequisite Skills
Proficiency in any programming language; this could be something as simple as a block-based language1.
Course Goals
The ultimate goal of this course is to familiarize you enough with embedded development that you can continue exploring beyond Arduino or move away from embedded as a more informed engineer. By the end of the summer, you should be able to:
- Identify & establish engineering problem requirements.
- Decompose problems into digestible sub-problems.
- Find and parse datasheets and reference manuals for relevant information.
- Make sense of unfamiliar code.
- Organize a programming project.
- Write clean, effective, and defensive C code.
- Debug with software and hardware.
- Work with others on an engineering problem.
- Understand the social implications of your work.
- Be comfortable with being uncomfortable.
This course aims to address the following ABET student outcomes:
- An ability to identify, formulate, and solve complex engineering problems by applying principles of engineering, science, and mathematics.
- An ability to apply engineering design to produce solutions that meet specified needs with consideration of public health, safety, and welfare, as well as global, cultural, social, environmental, and economic factors.
- An ability to communicate effectively with a range of audiences.
- An ability to recognize ethical and professional responsibilities in engineering situations and make informed judgments, which must consider the impact of engineering solutions in global, economic, environmental, and societal contexts.
- An ability to function effectively on a team whose members together provide leadership, create a collaborative environment, establish goals, plan tasks, and meet objectives.
- An ability to develop and conduct appropriate experimentation, analyze and interpret data, and use engineering judgment to draw conclusions.
- An ability to acquire and apply new knowledge as needed, using appropriate learning strategies.
Policy
Overall, I am a very understanding and flexible person, but please don’t force my hand.
Ask Questions
The man who asks a question is a fool for a minute, the man who does not ask is a fool for life. (Confucius)
At times, my mind moves too fast for my own good, and I may skip steps. If you are confused at any moment for any reason, please stop me. I can guarantee you are not the only person in the room feeling that way. I do understand it may be difficult to come forward with questions in a class environment, so I’ll also be providing a real-time anonymous question board you can take advantage of. Finally, if you’re so confused that you cannot compose a meaningful question, please reach out, and we’ll work together to identify the root of the problem.
Attendance
Attendance is mandatory. We’ll be starting at 09:00 sharp as starting later is unfair to the students who made an effort to get to class on time. If you are running late, please inform me at least 15 minutes before the start of class so that I can take note. In the event of an absence, please inform both me and Dr. Thevenot.
Course Work
I expect all relevant work from the day to be submitted (even if incomplete). Work will not be explicitly graded, however, I plan to return a commented diff by the start of the next class. At the start of each class, we might spend some time reviewing any common errors made in assignments.
Academic Integrity
I do not tolerate cheating of any kind. If I have reasonable suspicion that you have cheated, I will report you to Cooper Union’s administration for a formal review.
It is painfully obvious when I read code that includes code not written by you. That is not to say you cannot use online resources or LLMs, but I do expect proper attribution. If you’re using just a snippet of code, type it out, and if possible, refactor it to fit the flow of your code. I intend for such a practice to get you in the habit of understanding the code you read so that you can grow as a programmer. That said, using larger snippets becomes problematic due to licensing, which could land you in legal trouble!
tldr: write everything yourself!
Course Structure
This course is segmented into three, two-week units. With the exception of the last unit, we’ll try to stick to the following daily schedule:
- 09:00–10:00 - Lecture for background/motivation
- 10:00–12:00 - Interactive lesson
- 12:00–13:00 - Lunch
- 13:00–15:00 - In-class assignment(s)
At the end of each week, we will spend less time on learning and instead focus more on a single-day project. The goal of this project is to encompass everything we covered during the week but also to prepare you for your open-ended final project.
Background
In this unit, we’ll do a run-through of how to interact with Arduinos. The first week will primarily focus on the basics of Arduino and useful parts of the C programming language when working in an embedded setting. In the second week, we’ll set up a debugger to effectively navigate timers, interrupts, and communication peripherals.
C-ing Beyond Arduino
In this unit, we’ll leave the Arduino framework behind and will start programming the ATmega328P on the Arduino Uno Rev3 directly. In the first week, we’ll familiarize ourselves with interacting with the ATmega328P without a GUI or libraries; doing so entails becoming comfortable with the terminal, along with reading datasheets and sample code. In the second week, we’ll explore the peripheral systems of the ATmega328P and learn how to work around its limitations.
Final Project
In this final unit, we’ll work on an open-ended two-week final project in small groups. This project will revolve around using the ATmega328P in a “real world” application where you’ll have the chance to work with sensors and hardware not covered in class; however, do keep in mind that some parts have long lead times. We’ll also have drop-in lessons as necessary to ensure we finish in time for the final showcase.
Student Resources
Accommodations: https://cooper.edu/students/student-affairs/disability
Mental Health Services: https://cooper.edu/students/student-affairs/health/counseling
Title IX: https://cooper.edu/students/student-affairs/sexual-misconduct
Background
Tentative schedule:
- 2025-07-08: Hello, World!
- 2025-07-09: Analog & Serial
- 2025-07-10: Debugging
- 2025-07-14: Bit Manipulation & Lookup Tables
- 2025-07-15: Circular Buffers & Seven Segment Displays
- 2025-07-16: Timers & State Machines
- 2025-07-17: Interrupts
Hello, World!
Getting the ball rolling with Arduino.
Outline
- The basics of Information Theory, in particular, defining entropy
- Working with different numeral systems:
- Decimal, Binary, Octal, & Hexadecimal
- Two’s complement
- Quick conversion between Binary, Octal, & Hexadecimal
- Circuit fundamentals:
- Ohm’s Law
- Series vs Parallel
- Power
- Analog vs Digital
- Arduino basics:
- Using the language reference
setup()
&loop()
- Interfacing with digital IO
- Delaying actions
- Defining variables and constants
- Arithmetic operations and assignment
if
-else
statements
- Debouncing buttons
- Crude debugging with an LED
Exercises
-
In an attempt to absorb as much information as possible during the lesson, you suffer an exceptionally unexpected event and magically become stranded on an island with nothing but a laptop and your trusty Arduino. To make matters worse, it’s night, and a new moon is out.
-
Right as you’re about to start cursing your luck, you remember that the Arduino Uno Rev3 has a built-in LED, and there are some fishers about a mile out from the island. Using your newfound knowledge, write a program to blink SOS so that you can make it back in time for tomorrow’s lesson!
-
Fortunately for you, the fishers noticed your SOS, shot a flare, and are now heading your way. Unfortunately, you notice unsettling noises coming from the bushes not too far from you. Thinking quickly, you decide to change the LED blinking pattern to be irritating and scare away anything from coming out to the beach. Write a program that blinks the built-in LED in a range between 50 and 500 milliseconds. Change the delay by 50 milliseconds every cycle, alternating between increasing and decreasing the delay depending on whether the delay time is saturated.
-
Having been rescued by the fishers, as a token of appreciation (and as a practical joke), you decide to make them a counter that keeps track of the number of fish that got away in a day. They only have four LEDs, some resistors, and two push buttons on board. Being resourceful, you build them a 4-bit, unsigned counter. Write a program that displays in binary the number of fish that got away. One button should increment the counter while the other should reset it back to zero. If the counter overflows, turn on the built-in LED and turn off the remaining LEDs.
-
-
Being soon-to-be broke college students, you’ll need to be resourceful with what you have on hand. To prepare you for this reality, let’s create a dimmable Arduino night light using the built-in LED and a push button. We can achieve this by varying the percentage of time we power the LED. However, we need to perform this switching at a high enough frequency (>1 kHz) so that we cannot perceive the flickering. Utilize a push button to create 10 different LED brightness levels along with an off state. The push button should cycle between increasing the brightness and turning off the LED.
Analog & Serial
More interesting interactions with the world.
Outline
- Quantization
- Pulse-Width Modulation (PWM)
- Potentiometers
- Analog to Digital Conversion (ADC)
map()
function- Hysteresis
Serial
communication:- RX vs TX
- Baud rate
- Parity
- Serialization
- Serial console
- ASCII encoding
- RGB(A) color model
- RGB LEDs
Serial
debugging
Exercises
-
In anticipation of building your own audio amplifier, you decide to create a volume indicator. Using four LEDs, create a 4-LED volume indicator where a potentiometer serves as the volume knob. Ensure that hysteresis is added to each of the volume regions to prevent flickering of volume levels.
-
Since I’m a huge Pink Floyd fan, let’s pick Any Colour You Like. Utilize three potentiometers to change the color of an RGB LED to any color you like. Optionally, add a button to dim the LED brightness by 50%.
-
Since moving our hands from the keyboard is such a hassle, let’s instead utilize the serial console to control the color of the LED. Your code should expect a valid hex RGB value followed by a newline. If this value is malformed, return an error to the user with a meaningful error code; otherwise, return a success code. Optionally, add support for an optional alpha channel. If the user supplies no alpha channel, assume alpha is 100%.
Debugging
Squash one bug and two more may appear.
Outline
- Recursion
- debugWire
- Breakpoints
- Watchpoints
- Mutating memory
- Evaluating expressions
Exercises
-
Implement Fizz Buzz up to a number specified over the serial console.
-
Create a recursive factorial function. This function should be able to at least successfully calculate 10 factorial.
Bit Manipulation & Lookup Tables
Squeezing and pre-computing data.
Outline
- Standard integer types
- Endianness
- Bitwise operations:
- AND
- OR
- XOR
- NOT
- Left shift
- Right shift
- Bit masks
- Bit packing
enum
erations- Lookup tables
sizeof
operatorstatic
storage class qualifier- Seven-segment displays
Exercises
-
You’re working on an embedded system where a service technician needs to know status information at a glance. To fulfill this requirement, you and your team decide that it’d be easiest to display a status code using a hex code. Luckily, there’s only 16 different status states your system can assume, so a single seven segment display appears to be the perfect choice, however there’s only one problem: the hardware team still doesn’t know which pins on the microcontroller are going to be easiest to route to your seven segment display on the multi-layer PCB. Being the brilliant engineer you are, you decide to create a lookup table that allows you to easily map segments to pins. With this lookup table in mind, write code that displays any value between 0 and 15 on a seven-segment display. Make sure to create a bit-packed lookup table for each of the hex digits. To ensure your code works, continuously cycle through 0 and 15.
-
You’re working on a communication protocol, and following some clever math over GF(2), you end up with a
uint64_t
where each bit represents an irrecoverable bit error. Since you’re working on a real-time system, you need to quickly determine if the number of bit errors is still recoverable. Write code that counts the number of bits set in auint64_t
. Make your code interactive by allowing a user to specify any integer over serial and reporting the number of bits set back to the user. -
Since you’re working in an embedded environment, memory is a scarce resource, making logging a headache. During the engineering design process, you and your team decide that you need to keep track of the occurrence of an event over the last eight time steps. Write code that cyclically fills a
uint8_t
where we represent the presence of an event by a set bit. When you press one push button, read the value of another to determine the occurrence of the event. Optionally, add another button that will print the last eight events over serial when pressed. During setup, zero the buffer to assume no events have happened. -
(Challenge) When dealing with a vast amount of binary data, corruption is inevitable. Due to this possibility, we need a method to determine if our data has changed so that we can act accordingly. One method is to utilize a 32-bit Cyclic Redundancy Check (CRC-32). Implement a function that computes a CRC-32 given an array of
uint8_t
’s and the number of bytes using the standard IEEE polynomial of0x04C11DB7
and initial condition of0xFFFFFFFF
. Remember, endianness matters here! You might need to use the reversed version of the polynomial. Create an array with the message “cooper union” and check if your function reports a correct CRC-32.
Circular Buffers
Don’t look too far ahead; you might see what you’ve left behind.
Outline
- Modular arithmetic
- Circular buffers
- Multiplexing
Project
Scrolling quad seven-segment display.
- Create two lookup tables: one that maps seven-segment display segments to pins and another that maps digits to pins.
- Write a function that, given a
uint8_t
bitmask, displays it on a seven-segment digit. - Write code that multiplexes between the different digits of the display. At this point, it should look as if all the digits are on at the same time.
- Write a function that, given ASCII digits, letters, or the space character, returns an index into a lookup table of associated seven-segment bitmasks.
- Implement a circular text buffer that can repeatedly scroll on your display.
Timers & State Machines
We can’t describe everything in terms of finite states, but that sure hasn’t stopped us from trying.
Outline
millis()
µs()
- Super loops
random()
&randomSeed()
struct
urestypedef
initionsswitch
-case
s- State machines
Exercises
-
Blink two LEDs every 389 and 991 milliseconds.
-
I miss the days of bringing quarters to school for a snack at the vending machine, so let’s make one to bring this childhood memory of mine back to life! Create a simple state machine to turn your Arduino into a vending machine! Your state machine should have the following states:
- Collecting quarters
- Item dispensing
- Returning change (if any)
Since this is a “proof of concept” device, let’s make the following simplifications:
- Print status information to the serial console to let the user know what’s going on.
- Pressing a button adds a quarter.
- Pressing a button will toggle between items.
- Pressing a button returns users their change.
And to complicate things a bit, let’s consider the following:
- Have at least three different priced items with a finite stock.
- Add a 30s timeout from the last inserted quarter. On timeout, return all change.
- Randomly reject quarters at a 1% rate and let users know about this failure.
- Blink the built-in LED every second so users know the machine is still on.
Interrupts
Like a crying child, some things just need your attention right away.
Outline
- The Fourier Transform
- Hardware low-pass filter for debouncing
- Ultrasonic sensors
- Interrupts
attachInterrupt()
,detachInterrupt()
, &digitalPinToInterrupt()
- Function prototypes
- Header files
- Multi-file sketches
- Quadrature encoders
Exercises
- Utilize interrupts to keep track of encoder steps.
Create a separate source file that encapsulates your encoder functionality and expose the current encoder steps with a function that has the following signature:
uint32_t encoder_steps(void)
.
Examples
ASCII Remap
static const uint32_t BAUD = 115200;
static const uint8_t DIGITS = 10;
static const uint8_t INVALID_INDEX = 255;
static uint8_t ascii_to_index(const unsigned char c)
{
if ((c >= '0') && (c <= '9'))
return c - '0';
if ((c >= 'A') && (c <= 'Z'))
return c - 'A' + DIGITS;
if ((c >= 'a') && (c <= 'z'))
return c - 'a' + DIGITS;
return INVALID_INDEX;
}
void setup(void)
{
Serial.begin(BAUD);
Serial.println(ascii_to_index('4'));
Serial.println(ascii_to_index('J'));
Serial.println(ascii_to_index('K'));
Serial.println(ascii_to_index('!'));
}
void loop(void)
{
}
Button Debounce
static const uint8_t BUTTON = 2;
static const uint8_t DEBOUNCE_MS = 4;
static bool button_state;
void setup(void)
{
pinMode(BUTTON, INPUT);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, button_state);
}
void loop(void)
{
// get our initial sample of the button
bool button_sample = digitalRead(BUTTON);
// wait for the button to settle
delay(DEBOUNCE_MS);
// if the current sample is the same as the
// previous we assume the new button state
if (digitalRead(BUTTON) == button_sample)
button_state = button_sample;
digitalWrite(LED_BUILTIN, button_state);
}
Circular Buffer
static const char MESSAGE[] = "cooper union ";
static const uint8_t WINDOW_SIZE = 4;
// exclude the NULL character when calculating the size
static const uint8_t MESSAGES_SIZE = sizeof(MESSAGE) - 1;
static const uint32_t BAUD = 115200;
static const uint16_t DELAY_MS = 500;
void setup(void)
{
Serial.begin(BAUD);
}
void loop(void)
{
static uint8_t index;
for (size_t i = 0; i < WINDOW_SIZE; i++)
Serial.print(MESSAGE[(index + i) % MESSAGES_SIZE]);
Serial.print('\n');
index = (index + 1) % MESSAGES_SIZE;
delay(DELAY_MS);
}
Lookup Tables
enum {
RED,
GREEN,
BLUE,
COLORS_TOTAL,
};
static const uint16_t ADC_MAX = (1 << 10) - 1;
static const uint8_t PWM_MAX = (1 << 8) - 1;
static const uint8_t COLOR_TO_PIN[COLORS_TOTAL] = {
[RED] = 9,
[GREEN] = 10,
[BLUE] = 11,
};
static const uint8_t POTENTIOMETER_TO_PIN[COLORS_TOTAL] = {
[RED] = A0,
[GREEN] = A1,
[BLUE] = A2,
};
void setup(void)
{
for (size_t i = 0; i < COLORS_TOTAL; i++) {
pinMode(POTENTIOMETER_TO_PIN[i], INPUT);
pinMode(COLOR_TO_PIN[i], OUTPUT);
digitalWrite(COLOR_TO_PIN[i], LOW);
};
}
void loop(void)
{
for (size_t i = 0; i < COLORS_TOTAL; i++) {
const uint16_t sample = analogRead(POTENTIOMETER_TO_PIN[i]);
analogWrite(COLOR_TO_PIN[i], map(sample, 0, ADC_MAX, 0, PWM_MAX));
}
}
Timer Delay
static const uint32_t DELAY_US = 500000;
static uint32_t previous_time_us;
void setup(void)
{
digitalWrite(LED_BUILTIN, LOW);
pinMode(LED_BUILTIN, OUTPUT);
previous_time_us = micros();
}
void loop(void)
{
uint32_t current_time_us;
if ((current_time_us = micros()) - previous_time_us >= DELAY_US) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
previous_time_us = current_time_us;
}
}
Ultrasonic Interrupt
static const uint8_t ECHO_PIN = 2;
static const uint8_t TRIGGER_PIN = 4;
/*
* In Standard Temperature and Pressure (STP), sound travels at 343 m/s.
* Here we define this constant but in terms of microseconds to make it
* easy for us to convert to meters given our measurement in
* microseconds.
*/
static const float STP_SOUND_METERS_PER_US = 343e-6f;
static const uint32_t BAUD = 115200;
static const uint8_t PULSE_LENGTH_US = 10;
static const uint8_t SAMPLE_DELAY_MS = 10;
static volatile bool pulse_ready;
static volatile uint16_t pulse_time_us;
static void echo_isr(void)
{
/*
* NOTE: On ATmega328P, only one interrupt can execute at a time. This
* becomes problematic when using millis() or micros() inside of an
* interrupt context as the underlying timers which drive these
* functions are only 16-bit! This means the upper 16 bits of the
* returned value is stored independent of the timer value and
* incremented with an overflow interrupt. Since timers don't stop in
* interrupt contexts, it's possible for the timer to overflow between
* the start and end times, causing a possible erroneous reading of
* about (1 << 32) - (1 << 16). To avoid this, we only store the lower
* 16 bits of the return value of millis() or micros(), more
* specifically, the actual timer value, allowing us to avoid this
* issue all together!
*/
static uint16_t pulse_start_us;
// erroneous interrupt
if (pulse_ready)
return;
if (digitalRead(ECHO_PIN)) {
pulse_start_us = micros();
return;
}
const uint16_t pulse_end_us = micros();
pulse_time_us = pulse_end_us - pulse_start_us;
pulse_ready = true;
}
void setup(void)
{
pinMode(ECHO_PIN, INPUT);
digitalWrite(TRIGGER_PIN, LOW);
pinMode(TRIGGER_PIN, OUTPUT);
Serial.begin(BAUD);
attachInterrupt(digitalPinToInterrupt(ECHO_PIN), echo_isr, CHANGE);
}
void loop(void)
{
pulse_ready = false;
digitalWrite(TRIGGER_PIN, HIGH);
delayMicroseconds(PULSE_LENGTH_US);
digitalWrite(TRIGGER_PIN, LOW);
// NOTE: your code can be doing other things while we wait for the
// pulse to come in, like timeout---we just have nothing to do
while (!pulse_ready)
continue;
// since the pulse travels to the object and back, we need to divide
// the total time in half since we measure double the distance
const float distance_m = STP_SOUND_METERS_PER_US * (pulse_time_us / 2);
Serial.print(distance_m);
Serial.println("m");
// if we don't wait here, the HC-SR04 becomes unhappy and doesn't
// ever send the next pulse we request :(
delay(SAMPLE_DELAY_MS);
}
How-tos
debugWire
You’ll need two Arduino Uno Rev3s.
One will serve as our target, which will get debugged, while the other will act as the actual debugger.
To make this possible, we’ll be taking advantage of dw-link
to turn one of the Arduinos into a debugger.
- Grab the latest release of
dw-link
.- (Optionally) Feel free to read the
dw-link
manual.
- (Optionally) Feel free to read the
- Compile and upload the sketch to your designated Arduino debugger.
- Install MiniCore using the board manager.
- Set your board to be the MiniCore ATmega328, disable compiler LTO, and optimize sketches for debugging.
- We have already enabled debugWire on all of the target boards, so it is sufficient to connect pin 8 of the debugger board to the reset pin of the target, along with power and ground to their respective locations.
- Verify your sketch at least once before starting debugging.
Solutions
Scrolling Quad Seven-Segment Display
// SPDX-License-Identifier: MPL-2.0
/*
* scrolling quad seven-segment display
* Copyright (C) 2025 Jacob Koziej <jacobkoziej@gmail.com>
*/
/*
* Here we create a constant array of characters for the message we wish
* to scroll on the seven segment display along with the scrolling delay
* and time each digit is enabled in microseconds.
*
* NOTE: when calculating the message size, we subtract one from the
* result of sizeof(). This is because the string literal we used for
* assignment includes a trailing NULL character. Since this is not a
* printable, we simply exclude it.
*/
static const char MESSAGE[] = "COOPER EE25 ";
static const uint8_t MESSAGE_SIZE = sizeof(MESSAGE) - 1;
static const int32_t SCROLL_DELAY_US = 500000;
static const uint16_t DIGIT_TIME_US = 250;
/*
* Here we map each segment to an array and bit index for a seven-
* segment digit. In this case, we treat the LSB as a decimal point and
* the MSB as segment A.
*/
enum {
SEGMENT_DP,
SEGMENT_G,
SEGMENT_F,
SEGMENT_E,
SEGMENT_D,
SEGMENT_C,
SEGMENT_B,
SEGMENT_A,
SEGMENTS_TOTAL,
};
/*
* Here we create a lookup table that maps segment index to pin number
* on the Arduino Uno Rev3. We explicitly assign with the array index as
* to make this code "self documenting" and reduce the probability of
* incorrect assignments while editing the order of pins.
*/
static const uint8_t SEGMENT_TO_PIN[SEGMENTS_TOTAL] = {
[SEGMENT_DP] = 8,
[SEGMENT_G] = 7,
[SEGMENT_F] = 4,
[SEGMENT_E] = 2,
[SEGMENT_D] = A3,
[SEGMENT_C] = A2,
[SEGMENT_B] = A1,
[SEGMENT_A] = A0,
};
/*
* Since a display can possibly have any number of digits, we don't go
* through the effort of creating an enum for each of the digits (unless
* there's a need to refer to specific digits by name). We also store
* the number of digits using sizeof() so that the constant dynamically
* updates if we were to add/remove digits in the future.
*
* NOTE: we divide the size of the array by the size of the first
* element as there is no guarantee that a uint8_t is the smallest
* integer size on our target architecture unlike a char.
*/
static const uint8_t DIGIT_TO_PIN[] = {9, 6, 5, 3};
static const uint8_t DIGITS_TOTAL
= sizeof(DIGIT_TO_PIN) / sizeof(DIGIT_TO_PIN[0]);
/*
* In our case, the quad seven-segment display digits are active low
* while segments are active high. If we were to change our display in
* the future, we can just change the following constants to adjust the
* logic levels elsewhere in our code as opposed to inverting a bunch of
* HIGH/LOW constants.
*/
static const uint8_t DIGIT_ON = LOW;
static const uint8_t DIGIT_OFF = HIGH;
static const uint8_t SEGMENT_ON = HIGH;
static const uint8_t SEGMENT_OFF = LOW;
/*
* Since we're only concerned with a subset of the printable ASCII
* characters, we create a condensed lookup table with bitmasks we can
* show on the seven-segment display. To facilitate this reduced lookup
* table size, we must also write a function that remaps ASCII values.
*/
static const uint8_t ASCII_DIGIT_OFFSET = 1;
static const uint8_t ASCII_LETTER_OFFSET = 10 + ASCII_DIGIT_OFFSET;
static const uint8_t ASCII_LOOKUP[] = {
[' ' - ' '] = 0b00000000,
['0' - '0' + ASCII_DIGIT_OFFSET] = 0b11111100,
['1' - '0' + ASCII_DIGIT_OFFSET] = 0b01100000,
['2' - '0' + ASCII_DIGIT_OFFSET] = 0b11011010,
['3' - '0' + ASCII_DIGIT_OFFSET] = 0b11110010,
['4' - '0' + ASCII_DIGIT_OFFSET] = 0b01100110,
['5' - '0' + ASCII_DIGIT_OFFSET] = 0b10110110,
['6' - '0' + ASCII_DIGIT_OFFSET] = 0b00111110,
['7' - '0' + ASCII_DIGIT_OFFSET] = 0b11100000,
['8' - '0' + ASCII_DIGIT_OFFSET] = 0b11111110,
['9' - '0' + ASCII_DIGIT_OFFSET] = 0b11110110,
['a' - 'a' + ASCII_LETTER_OFFSET] = 0b11101110,
['b' - 'a' + ASCII_LETTER_OFFSET] = 0b00111110,
['c' - 'a' + ASCII_LETTER_OFFSET] = 0b10011100,
['d' - 'a' + ASCII_LETTER_OFFSET] = 0b01111010,
['e' - 'a' + ASCII_LETTER_OFFSET] = 0b10011110,
['f' - 'a' + ASCII_LETTER_OFFSET] = 0b10001110,
['g' - 'a' + ASCII_LETTER_OFFSET] = 0b11110110,
['h' - 'a' + ASCII_LETTER_OFFSET] = 0b00101110,
['i' - 'a' + ASCII_LETTER_OFFSET] = 0b00100000,
['j' - 'a' + ASCII_LETTER_OFFSET] = 0b01111000,
['k' - 'a' + ASCII_LETTER_OFFSET] = 0b01101110,
['l' - 'a' + ASCII_LETTER_OFFSET] = 0b00011100,
['m' - 'a' + ASCII_LETTER_OFFSET] = 0b10101010,
['n' - 'a' + ASCII_LETTER_OFFSET] = 0b00101010,
['o' - 'a' + ASCII_LETTER_OFFSET] = 0b00111010,
['p' - 'a' + ASCII_LETTER_OFFSET] = 0b11001110,
['q' - 'a' + ASCII_LETTER_OFFSET] = 0b11100110,
['r' - 'a' + ASCII_LETTER_OFFSET] = 0b00001010,
['s' - 'a' + ASCII_LETTER_OFFSET] = 0b10110110,
['t' - 'a' + ASCII_LETTER_OFFSET] = 0b00011110,
['u' - 'a' + ASCII_LETTER_OFFSET] = 0b00111000,
['v' - 'a' + ASCII_LETTER_OFFSET] = 0b01111100,
['w' - 'a' + ASCII_LETTER_OFFSET] = 0b10111000,
['x' - 'a' + ASCII_LETTER_OFFSET] = 0b01101110,
['y' - 'a' + ASCII_LETTER_OFFSET] = 0b01110110,
['z' - 'a' + ASCII_LETTER_OFFSET] = 0b11011010,
};
/*
* Here we create a function that maps ASCII characters to our lookup
* table indices. This function checks ranges of contiguous ASCII
* characters and adds appropriate offsets.
*/
static uint8_t ascii_to_index(const char c)
{
if (c == ' ')
return c - ' ';
if ((c >= '0') && (c <= '9'))
return c - '0' + ASCII_DIGIT_OFFSET;
if ((c >= 'A') && (c <= 'Z'))
return c - 'A' + ASCII_LETTER_OFFSET;
if ((c >= 'a') && (c <= 'z'))
return c - 'a' + ASCII_LETTER_OFFSET;
return 0;
}
/*
* To simplify using the seven-segment display, we create a function,
* that given a bitmask, will write it to the display using the bit
* values as to identify which segments to turn on/off.
*/
static void digit_write(const uint8_t digit)
{
for (size_t i = 0; i < SEGMENTS_TOTAL; i++)
digitalWrite(
SEGMENT_TO_PIN[i],
digit & (1 << i) ? SEGMENT_ON : SEGMENT_OFF);
}
void setup(void)
{
// on reset, we disable the display to get it into a consistent state
for (size_t i = 0; i < DIGITS_TOTAL; i++) {
digitalWrite(DIGIT_TO_PIN[i], DIGIT_OFF);
pinMode(DIGIT_TO_PIN[i], OUTPUT);
};
for (size_t i = 0; i < SEGMENTS_TOTAL; i++) {
digitalWrite(SEGMENT_TO_PIN[i], SEGMENT_OFF);
pinMode(SEGMENT_TO_PIN[i], OUTPUT);
};
}
void loop(void)
{
static uint8_t index;
static int32_t scroll_delay_us = SCROLL_DELAY_US;
for (size_t i = 0; i < DIGITS_TOTAL; i++) {
const uint8_t letter = MESSAGE[(index + i) % MESSAGE_SIZE];
const uint8_t digit = ASCII_LOOKUP[ascii_to_index(letter)];
// set segments before enabling digits to reduce flicker
digit_write(digit);
digitalWrite(DIGIT_TO_PIN[i], DIGIT_ON);
delayMicroseconds(DIGIT_TIME_US);
digitalWrite(DIGIT_TO_PIN[i], DIGIT_OFF);
}
scroll_delay_us -= DIGITS_TOTAL * DIGIT_TIME_US;
if (scroll_delay_us <= 0) {
index = (index + 1) % MESSAGE_SIZE;
scroll_delay_us = SCROLL_DELAY_US;
}
}