It's time to make the call... The call from your communicator to another communicator.
We need you to build a serial communication device capable of sending and receiving messages via UART. For inputs, you'll have buttons, a potentiometer, and optionally any other components you've used in OPS (IR sensor?). Each time you send an input, your communicator will send a message corresponding to that input to a partner's communicator via UART. For outputs, you'll have LEDs, a servo, and optionally any other components you've used in OPS (buzzer?). Each time you receive a message from your partner, you'll adjust your outputs accordingly. You're required to at least implement one button, one LED, the potentiometer, and the servo.
When one communicator's potentiometer is turned, the servo on the other communicator should rotate. When one communicator's button is pressed, the corresponding LED on the other communicator should toggle its state (if it was on, turn off; if it was off, turn on).
If you don't want to or aren't able to partner up with another student to test your communicator, that's totally fine. You can simulate another communicator using the serial monitor within the Arduino IDE. This is because the UART Tx and Rx pins on the Arduino are also connected to the USB-to-serial chip that connects the Arduino to your computer. Every time you use the Serial object to send or receive data, you're sending it via those Tx/Rx pins and via the USB interface, which the serial monitor on your computer can hear and talk to.
Serial and Parallel Communication Protocols
Communication protocols are standardized methods of sending and receiving data between devices. There are several types of protocols and physical connectors that exist to serve different purposes. All communication protocols we use today fall into two categories: serial and parallel.
Serial protocols have only one wire and send data over that wire one bit at a time (similar to series circuits). Serial is the most widely used protocol, and you’re probably familiar with many of its physical connectors, such as USB types, HDMI, and RS-232.
Parallel protocols, like parallel circuits, have several wires and can send multiple bits of data at the same time. However, having several cables leads to cable design complexity, inter-wire crosstalk, and an overall larger physical size. The digital systems that use parallel communication must also be more complex to handle sending/receiving multiple bits of data at once. These factors have led to serial protocols becoming more widely used over parallel.
Synchronous vs Asynchronous Serial
When communicating over a serial connection, how can we tell when we should be reading data? What if we send two 1 bits in a row? We need some way to tell when a new bit should be read.
In a synchronous serial connection, we use a clock signal to tell when the next bit should be read. A second wire, called the clock wire, alternates between 1 and 0. We can then send or receive a bit when our clock signal is high and ignore values when the clock is low!
What if we don’t want to use a clock wire? In asynchronous serial, the sender and receiver agree on how fast to send data rather than using a clock signal. When we use Serial.begin(baud) in our programs, that speed, or baud rate, is what we are setting! But, we also need a way to tell the receiver when to start reading data.
First, we need an Idle state to start the serial data line (this state is HIGH in most protocols). Then, we send a start bit that is the opposite state of Idle (usually LOW). This start bit tells the receiving device that new data is being sent. The receiver waits half a clock cycle, then begins reading data one bit per cycle.
When should the receiver stop reading data? If we tell the receiver in advance how many bits are in each data packet, then it will count to that many bits, then check for a stop bit. The stop bit has the same state as Idle and tells the receiver when the transmission is complete. If our stop bit doesn’t have the same state as Idle, then we know we’ve misread our data and have an error. Programmers must write code to handle this condition.
UART
The protocol we’ll be using in Project 4 is an asynchronous serial protocol called Universal Asynchronous Receiver/Transmitter (UART). UART is a very common and simple protocol found in most microcontrollers. It has 3 wires: Tx, Rx, and Ground. The Tx wire is for transmitting data out of your device, and the Rx wire is for receiving data from another device. The ground wire is a shared ground between the two devices. Remember that the data we’re sending is just voltage on a wire, and we can’t read a voltage without a common reference point!
Keep in mind that UART shares data from mouth to ear, meaning Tx of one device always goes with Rx of the other device, and vice versa!
What does this data look like as it’s being sent? Each unit of information we send is called a data frame. Since UART is an asynchronous protocol, it has a pre-set baud rate and begins with a start bit. Next, it receives the data bits (this is the message we want to read). The number of data bits is commonly fixed at 8 bits, or one byte.
After receiving the data, we may want to check that we received the correct number of bits. We can use a parity bit to verify that the data received was valid. If parity is enabled, it is set to either a 1 or 0. The parity bit is set to 1 if it’s an even parity and set to 0 if it’s an odd parity. Even parity is when it (being a 1) plus the number of 1s in the data adds to an even number. For example, parity is set to 1 if the data contains three 1s (because 3 + 1 = 4 is even)! Odd parity is when the parity bit (being a 0) plus the number of 1s in the data adds to an odd number. So, if the data had three 1s and parity is 0, then (3 + 0 = 3) is odd!
The receiver can then check the parity bit to verify the parity is correct for the number of 1s it received. If parity is invalid, we can execute code to handle the misread. We could ask the sender to retransmit, throw out the frame, or try to guess which bits held the correct data. Note that parity checking will only detect odd bit-flip errors, meaning that if an even number of bits are flipped, invalid data will still pass (you'll learn more reliable methods of error checking if you take ECE 306).
Our data frame is completed with a stop bit, signaling that the frame has completed its transmission. The stop bit is typically only one bit but can be set to use two. This was more common with older systems that needed a second bit of idle time to finish accepting a data frame
Using UART with your Arduino
You’ve already been using the UART connection on your Arduino for projects 1-3! Serial commands (like Serial.print and Serial.println) have been sending data to the serial monitor over UART. Pins 0 and 1 on your Arduino are the UART Tx and Rx pins, which is why we have avoided using them in previous projects. Using these pins for anything other than communication can interfere with the serial connection and may prevent any serial commands from executing properly.
Our Arduino, like most microcontrollers, doesn’t directly support USB. Instead, the Tx and Rx pins are internally connected to a USB to Serial chip on the back of the Arduino. Data leaves the MCU as UART data frames and enters this chip. The chip translates data frames to USB protocol before sending the data frames through the USB cable to your computer, where the Arduino IDE’s serial monitor decodes it.
In the background, our Arduino’s Rx pin is always listening for UART data, either from the serial monitor or from another device via the Rx pin. Whenever data arrives, it is stored in a buffer that acts like a bucket for data. We can check if any data is in the buffer using Serial.available, which returns the number of bits currently in the buffer. Your Arduino’s Serial Receive buffer holds 64 bytes maximum. If you exceed this limit before clearing the buffer, all new data will be discarded, like a bucket overflowing. To get things out of the buffer, we use Serial.read, which returns the next available byte in the buffer. When you call Serial.read, that byte is removed from the buffer; if you don’t store it somewhere, that data will be gone from your program.
ASCII
ASCII is the American Standard Code for Information Interchange. It is a standard for translating letters, numbers, and other symbols into binary. ASCII is the standard implemented in the C language. C has its own data type for ASCII characters, called char. Each ASCII character takes up 8 bits of data.
Using the ASCII table, we can easily store a letter as a char using one of two methods. We can either type the character in single quotes, or we can type the character’s ASCII equivalent decimal integer. For example, the character ‘a’ is the same as the character 97. This is important distinction to remember. For example, char myNum = '3' and char myNum = 3 are different values.
We can convert numbers to and from characters using a simple conversion: add the character ‘0’ (this is equivalent to adding the character 48) to your character to get the equivalent number, or subtract ‘0’ (or 48) from a number to get the equivalent character.
Software Button Debounce
Remember that the contacts inside our buttons bounce when pressed, and our microcontroller may read these bounces as multiple button presses. In Project 3, we used hardware to debounce our buttons. For Project 4, we’ll be implementing software debounce.
Here's the general idea: we’ll watch for the first change in button state from the button being pressed. Then, we’ll delay some time for the contacts to stop bouncing, ignoring any button presses in this time. After the delay, we’ll check if the button is still in a pressed state. If so, then we count that as one button press and execute any code associates with the press.
We could program a debounce circuit using delay(), but there’s a problem: when the delay function runs, no other part of the program will run until the delay has ended. This is called blocking code. One way around this is to use the Arduino’s timer that always runs in the background. This timer counts every millisecond that passes, and that count can be read using the millis() function.
Because your Arduino’s timer is always counting, the value returned by the millis function will often exceed the 16 bits allotted to an int variable. We need a new data type capable of storing larger values. The unsigned long type can store 32 bits and should be used for any call to millis().
The Communicator: Software Structure
You are required to use at least one (or up to 3) button/s and the potentiometer as inputs. These inputs correspond to LED/s and your servo, respectively. In order for you and your partner’s serial messages to align with each other, you must follow the following format.
Example:
We want to toggle our partner's first LED. The sending device will press their first button to send message T0. The 'T' here is the message type, and the '0' corresponds to which output LED should be toggled. When the receiver gets this message, their first LED should toggle.
What if the sender has more LEDs and buttons than the receiver? If the receiver doesn't have enough LEDs, then the message should toggle all the receiver's LEDs.
New Functions and Loops
while(condition)
While loops are a type of conditional loop that will always run while the condition is true. If the condition is true, the loop is entered, code is executed, then the condition is tested again. If it is still true, the loop will repeat. Only when the condition is false will the next section of the program run.
Example:
int buttonTimer = 0;
while(digitalRead(buttonPin) == HIGH) {
Serial.print("Button pressed for ");
Serial.print(buttonTimer);
Serial.println(" seconds.");
delay(1000);
buttonTimer += 1;
}
for(iterator; condition; iterator step)
For loops, like while loops, will run as long as their condition is true. But unlike while loops, a for loop keeps track of how many times the loop has run using an iterator. For loops are used over while loops when we know how many times we want the loop to run.
Example:
Serial.println("Counting by fives from 1 to 100!");
for(int count = 0; count < 100; count += 5) {
Serial.print("Count is: ");
Serial.println(count);
delay(1000);
}
break; and continue;
Both are ways to exit a loop early. continue will skip the code for the rest of that iteration of a loop and check the condition. If still true, the next iteration of the loop will begin. break will skip the rest of the loop code and exit the loop entirely, moving on to the next section of code.
Serial.write(val)
Transmits a single byte of data (one data frame) via UART. Because you are limited to only 8 bits, any numbers you send must be less than 2^8 (between 0 and 255). Anything larger will be lost in transmission.
Serial.available()
Returns the number of bytes currently in the receive buffer. The buffer cannot hold more than 64 bytes.
Serial.read()
Returns the next available byte from the receive buffer. The values you receive will be in ASCII; you can read these characters directly into a char (shown below). Keep in mind that once something is read from the buffer, it’s gone for good, so don’t forget to store it to a variable!
char myData = Serial.read();
Caution!
Remember that you and your partner's Arduinos should share a common ground, but they DO NOT share 5V! Each Arduino has its own power supply. Feeding one power supply into another can cause serious damage!
If you use Serial.print() for debugging purposes, make sure to comment out those statements before testing your project with another Arduino. Anything you send with Serial commands will be sent over UART to your partner's Arduino. Sending random debugging statements will confuse the receiving Arduino!
When using millis(), always use <= or >= comparators, NOT the == comparator. If your code runs too slowly, the millis function could skip over the number you're looking for. If you are checking only for equivalency, you could miss the exact value you needed!
Here's pseudocode which provides much of the project's functionality ready-made for you. We're providing more code this semester than in the past because we want this project to be easy so you can finish it and get back to preparing for final exams!
Treat this pseudocode as a fill-in-the-blank to get your communicator working, then add additional features onto it if you want. The code commends prompt you when you need to add code as well as some ideas for adding optional features!
As always, remember to work on just one thing at a time, then test your code and make sure that part works before moving on to the next part of your program. Printing messages and variables out to the serial console can help a lot with your testing, especially when it comes to signal filtering and making sure your filtering code / math is working the way you want it to.
You can use the Arduino IDE Serial Monitor to simulate being another communicator; simply type a message (like P512 or T0) into the text box and press enter to send it to your communicator. And, like we've seen before, your communicator's messages will be displayed in the serial monitor window for you to read.