|
Research & Learning Class pages
Shop Admin
ITP Help Pages |
Serial Duplex using an ArduinoOverviewIn the first serial lab, you sent data from one sensor to a personal computer. In this lab, you'll send data from multiple sensors to a program on a personal computer. You'll use the data from the sensors to create a pointing-and-selecting device (i.e. a mouse). Table of Contents (hide) 1. PartsFor this lab you'll need:
2. Connect the sensorsConnect two analog sensors to analog pins 0 and 1 like you did in the analog lab. Connect a switch to digital pin 2 like you did in the digital lab. The photos and schematic in this lab show an accelerometer and a pushbutton. You don't have to use these, though. Use whatever sensors are appropriate to your final application. While you're figuring what sensors to use, use the most convenient sensors you've got in hand; perhaps two potentiometers for the analog sensors and a pushbutton? 3. Read and send the serial dataTo begin with, just send the value from one sensor, the first analog sensor (the first axis of the accelerometer in the photos):
int analogPin = 0;
int analogValue = 0; // outgoing ADC value
void setup()
{
// start serial port at 9600 bps:
Serial.begin(9600);
}
void loop()
{
// read analog input, divide by 4 to make the range 0-255:
analogValue = analogRead(analogPin);
Serial.println(analogValue, DEC);
// pause for 10 milliseconds:
delay(10);
}
When you open the serial monitor, you should see a number between 0 and 1023 scrolling down the debugger pane. That's because the DEC modifier to
Now you get a range of garbage characters. What's going on? The BYTE modifier doesn't format the bytes. It sends out the raw binary value of the byte. The Serial Monitor receives that binary value and assumes it should show you the ASCII character corresponding to that value again. The garbage characters are characters corresponding to the ASCII values the Monitor is receiving.
Here's the tricky part: 4. Send the data in many formatsTry this program and view the results in the Serial Monitor:
int analogPin = 0;
int analogValue = 0; // integer to print
void setup() {
// open serial communications at 9600 bps
Serial.begin(9600);
}
void loop() {
// read the analog input, divide by 4:
analogValue = analogRead(analogPin) /4;
// print in many formats:
Serial.print(analogValue, BYTE); // Print the raw binary value analogValue
Serial.print('\t'); // print a tab
Serial.print(analogValue, BIN); // print the ASCII encoded binary analogValue
Serial.print('\t'); // print a tab
Serial.print(analogValue, DEC); // print the ASCII encoded decimal analogValue
Serial.print('\t'); // print a tab
Serial.print(analogValue, HEX); // print the ASCII encoded hexadecimal analogValue
Serial.print('\t'); // print a tab
Serial.print(analogValue, OCT); // print the ASCII encoded octal analogValue
Serial.println(); // print a linefeed and carriage return
delay(10);
}
You should get output like this: â 11100010 226 E2 342 á 11100001 225 E1 341 á 11100001 225 E1 341 á 11100001 225 E1 341 à 11100000 224 E0 340 à 11100000 224 E0 340 ß 11011111 223 DF 337 ß 11011111 223 DF 337 ß 11011111 223 DF 337 It's printing the raw binary value, then the ASCII-encoded binary value, then the ASCII-encoded decimal, hexadecimal, and octal values. You may never need all of these differnt formats, but you'll likely need at least the decimal and the raw binary versions at some point. 5. Send the values for all three sensorsIn the first serial lab, you sent one byte representing one sensor's value, over and over. When you're sending multiple sensor values, it gets a little more complicated. You need to a way to know which value represents which sensor. For example, imagine if you used the following loop to send your sensor values:
void loop() {
for (int thisSensor = 0; thisSensor < 3; thisSensor++) {
int sensorValue = analogRead(thisSensor);
Serial.print(sensorValue, DEC);
Serial.print(",");
}
}
you'd get a string like this:
You don't know which sensor is which. You could assume that if you start listening when the microcontroller starts sending that the first reading corresponds to the first sensor, but you can't know that for sure. There are two ways to address this. You can use punctuation or you can use a call-and-response or handshaking method. Use whichever makes the most sense to you. 6. Punctuation MethodOne way to send the data such that it can be interpreted clearly is to punctuate each set of data uniquely. Just as a sentence ends with a period, you can end your data with a carriage return and a newline. Here's a modification of the code above that does that:
void loop() {
for (int thisSensor = 0; thisSensor < 3; thisSensor++) {
int sensorValue = analogRead(thisSensor);
Serial.print(sensorValue, DEC);
// if this is the last sensor value, end the line.
// otherwise, print a comma:
if (thisSensor == 2) {
Serial.println();
} else {
Serial.print(",");
}
}
}
From this loop, you'd get output like this: 452,345,416 234,534,417 325,452,231 This is much better. Whenever you get a newline, you know that the next value is the first sensor. Here's a program that reads the two analog sensors on your board and the one digital switch, and prints them out in this format:
/*
Sensor Reader
Language: Wiring/Arduino
Reads two analog inputs and one digital input
and outputs their values.
Connections:
analog sensors on analog input pins 0 and 1
switch on digital I/O pin 2
*/
int analogOne = 0; // analog input
int analogTwo = 1; // analog input
int digitalOne = 2; // digital input
int sensorValue = 0; // reading from the sensor
void setup() {
// configure the serial connection:
Serial.begin(9600);
// configure the digital input:
pinMode(digitalOne, INPUT);
}
void loop() {
// read the sensor:
sensorValue = analogRead(analogOne);
// print the results:
Serial.print(sensorValue, DEC);
Serial.print(",");
// read the sensor:
sensorValue = analogRead(analogTwo);
// print the results:
Serial.print(sensorValue, DEC);
Serial.print(",");
// read the sensor:
sensorValue = digitalRead(digitalOne);
// print the last sensor value with a println() so that
// each set of four readings prints on a line by itself:
Serial.println(sensorValue, DEC);
}
Once you've got a data format, all you have to do is read it in the receiving program. Here's a Processing sketch that reads the data as formatted by the Arduino program above. First, set up the beginning of your program as you did in the first serial lab:
import processing.serial.*; // import the Processing serial library
Serial myPort; // The serial port
void setup() {
// List all the available serial ports
println(Serial.list());
// I know that the first port in the serial list on my mac
// is always my Arduino module, so I open Serial.list()[0].
// Change the 0 to the appropriate number of the serial port
// that your microcontroller is attached to.
myPort = new Serial(this, Serial.list()[0], 9600);
}
Add one extra line at the end of the
// read bytes into a buffer until you get a linefeed (ASCII 10):
myPort.bufferUntil('\n');
The
void draw() {
// twiddle your thumbs
}
The
void serialEvent(Serial myPort) {
// read the serial buffer:
String myString = myPort.readStringUntil('\n');
println(myString);
If you run this, you'll see the serial data print out just like it did in the arduino serial monitor. You'll also see a lot of null values. To clear those out, add an if statement around the println():
if (myString != null) {
println(myString);
}
That makes sure you've got a full string before you do anything with it. The
myString = trim(myString);
// split the string at the commas
// and convert the sections into integers:
int sensors[] = int(split(myString, ','));
Next, print out those three integers using a for() loop, like so:
// print out the values you got:
for (int sensorNum = 0; sensorNum < sensors.length; sensorNum++) {
print("Sensor " + sensorNum + ": " + sensors[sensorNum] + "\t");
}
// add a linefeed after all the sensor values are printed:
println();
Run the sketch now and you should get output like this: Sensor 0: 510 Sensor 1: 499 Sensor 2: 0 Sensor 0: 510 Sensor 1: 498 Sensor 2: 0 Sensor 0: 510 Sensor 1: 498 Sensor 2: 0 Sensor 0: 510 Sensor 1: 497 Sensor 2: 0 Sensor 0: 509 Sensor 1: 498 Sensor 2: 0 Sensor 0: 510 Sensor 1: 497 Sensor 2: 0 Now you've got all the sensor values as integers, and you can do whatever you want with them. Use them to move a circle on the screen. To do that, you'll need a few global variables at the beginning of the sketch: float bgcolor; // Background color float fgcolor; // Fill color float xpos, ypos; // Starting position of the ball Add a line at the beginning of the setup() to set the window size:
Put the following lines in the draw method. These will draw the circle using the variables you just declared: background(bgcolor); fill(fgcolor); // Draw the shape ellipse(xpos, ypos, 20, 20); In order to see anything happen, you need to assign the sensor values to these variables. Do this at the end of the serialEvent():
// make sure there are three values before you use them:
if (sensors.length > 1) {
xpos = sensors[0];
ypos = sensors[1];
// the switch values are 0 and 1. This makes them 0 and 255:
fgcolor = sensors[2] * 255;
}
If you run this, you should see the ball moving onscreen whenever you press the switch and move the analog sensors. If your analog values are greater than 640 or 480, the ball will be offscreen, so you may have to map your sensor range to the screen size. For example, the accelerometer sensor ranges are approximately 430 to 580. Since the screen is 0 to 640 (horizontal) and o tp 480 (vertical), you'd map the ranges like this: xpos = map(sensors[0], 430,580,0,width); ypos = map(sensors[1], 430,580,0,height); Here's the Processing sketch in its entirety:
/*
Serial String Reader
Language: Processing
Reads in a string of characters from a serial port until
it gets a linefeed (ASCII 10). Then splits the string into
sections separated by commas. Then converts the sections to ints,
and prints them out.
created 2 Jun 2005
modified 6 Aug 2008
by Tom Igoe
*/
import processing.serial.*; // import the Processing serial library
Serial myPort; // The serial port
float bgcolor; // Background color
float fgcolor; // Fill color
float xpos, ypos; // Starting position of the ball
void setup() {
size(640,480);
// List all the available serial ports
println(Serial.list());
// I know that the first port in the serial list on my mac
// is always my Arduino module, so I open Serial.list()[0].
// Change the 0 to the appropriate number of the serial port
// that your microcontroller is attached to.
myPort = new Serial(this, Serial.list()[0], 9600);
// read bytes into a buffer until you get a linefeed (ASCII 10):
myPort.bufferUntil('\n');
}
void draw() {
background(bgcolor);
fill(fgcolor);
// Draw the shape
ellipse(xpos, ypos, 20, 20);
}
// serialEvent method is run automatically by the Processing applet
// whenever the buffer reaches the byte value set in the bufferUntil()
// method in the setup():
void serialEvent(Serial myPort) {
// read the serial buffer:
String myString = myPort.readStringUntil('\n');
// if you got any bytes other than the linefeed:
if (myString != null) {
myString = trim(myString);
// split the string at the commas
// and convert the sections into integers:
int sensors[] = int(split(myString, ','));
// print out the values you got:
for (int sensorNum = 0; sensorNum < sensors.length; sensorNum++) {
print("Sensor " + sensorNum + ": " + sensors[sensorNum] + "\t");
}
// add a linefeed after all the sensor values are printed:
println();
if (sensors.length > 1) {
xpos = map(sensors[0], 430,580,0,width);
ypos = map(sensors[1], 430,580,0,height);
fgcolor = sensors[2] * 255;
}
}
}
7. Call and Response (Handshaking) MethodAnother way to keep multiple bytes of serial data in order is to send one set of values at a time, rather than sending repeatedly. If you use this method, the receiving program has to request new data every time it finishes reading what it's got. You can convert the punctuation method shown above to a call-and-response method fairly simply. First, modify the Arduino code. Add a new method at the end of the sketch called
void establishContact() {
while (Serial.available() <= 0) {
Serial.println("hello"); // send a starting message
delay(300);
}
}
To call this, add a line at the end of the
Now, modify the
void loop() {
if (Serial.available() > 0) {
// read the incoming byte:
int inByte = Serial.read();
// read the sensor:
sensorValue = analogRead(analogOne);
// print the results:
Serial.print(sensorValue, DEC);
Serial.print(",");
// read the sensor:
sensorValue = analogRead(analogTwo);
// print the results:
Serial.print(sensorValue, DEC);
Serial.print(",");
// read the sensor:
sensorValue = digitalRead(digitalOne);
// print the last sensor value with a println() so that
// each set of four readings prints on a line by itself:
Serial.println(sensorValue, DEC);
}
}
The rest of the Arduino sketch remains the same. When you run this and open the serial monitor, you'll see: hello hello hello hello Type any character in the output box and click Send. You'll get a string of sensor values at the end of your hellos: 510,497,0 Type another character and click Send. It doesn't matter what character you send, but the loop will always wait for an incoming byte before sending a new set of sensor values. Next modify the Processing program. You'll add a new global variable called Add the following global variable before the
Then modify the
void serialEvent(Serial myPort) {
// read the serial buffer:
String myString = myPort.readStringUntil('\n');
// if you got any bytes other than the linefeed:
if (myString != null) {
myString = trim(myString);
// if you haven't heard from the microncontroller yet, listen:
if (firstContact == false) {
if (myString.equals("hello")) {
myPort.clear(); // clear the serial port buffer
firstContact = true; // you've had first contact from the microcontroller
myPort.write('A'); // ask for more
}
}
// if you have heard from the microcontroller, proceed:
else {
// split the string at the commas
// and convert the sections into integers:
int sensors[] = int(split(myString, ','));
// print out the values you got:
for (int sensorNum = 0; sensorNum < sensors.length; sensorNum++) {
print("Sensor " + sensorNum + ": " + sensors[sensorNum] + "\t");
}
// add a linefeed after all the sensor values are printed:
println();
if (sensors.length > 1) {
xpos = map(sensors[0], 430,580,0,width);
ypos = map(sensors[1], 430,580,0,height);
fgcolor = sensors[2] * 255;
}
}
// when you've parsed the data you have, ask for more:
myPort.write("A");
}
}
If you did everything right, the ball should move in response to the analog sensors, and appear or disappear when you press the button. 8. Advantages of Raw Binary vs. ASCIIAll the examples shown here sent the sensor values as ASCII-encoded strings. As mentioned above, that means you sent three bytes to send a three-digit value. If that same value was less than 255, you could send it in one raw binary byte. So ASCII is definitely less efficient. However, it's more readable for debugging purposes, and if the receiving program is well-suited to convert strings to numbers, then ASCII is a good way to go. If the receiver's not so good at converting strings to numbers (for example, it's more challenging to read a multiple byte string in Arduino than in Processing) then you may want to send your data as binary values. 9. Advantages of Punctuation or Call-and-ResponseThe punctuation method for sending multiple serial values may seem simpler, but it has its limitations. You can't easily use it to send binary values, because you need to have a byte with a unique value for the punctuation. In the example above, you're using the value 10 (ASCII newline) as punctuation, so if you were sending your sensor values as raw bytes, you'd be in trouble when the sensor's value is 10. The receiver would interpret the 10 as punctuation, not as a sensor value. In contrast, call-and-response can be used whether you're sending data as raw binary values or as ASCII-encoded values. Sometimes the receiver reads serial data slower than the sender sends it. For example, if you have a program that does a lot of graphic work, it may only read serial data every few milliseconds. The serial buffer will get full in that case, you'll notice a lag in response time. This is when it's good to switch to a call-and-response method. 10. Get creativeYou just duplicated the basic functionality of a mouse; that is, a device with two analog sensors that affect X and Y, and a digital sensor (mouse button). What applications can you think of that could use a better physical interface for a mouse? A video editor that scrubs forward and back when you tilt a wand? An action game that reacts to how hard you hit a punching bag? An instructional presentation that speeds up if you shift in your chair too much? A music program driven by a custom musical instrument that you design? Create a prototype in Arduino and Processing, or whatever programming environment you choose. Come up with a physical interface that makes it clear what actions map to what movements and actions. Figure out which actions can and should be possible at the same time. Present a working software and hardware model of your idea. |