Communication Interfaces
Communication between boards, shields and sensors need not always be by setting individual digital pins to HIGH or LOW values or through the integers that can be read from an analogue pin. There are a number of standardised serial options that can communicate more complex data and relationships available to an Arduino project designer and programmer. This chapter is intended to explore the main options, not primarily to explore new programming techniques (although there are a few introduced) but to ensure that the range of software and hardware options is understood ready for informed choices to be made.
When we explored some data handling structures and techniques while taking a passing interest in Morse code we created a Morse reading program that took advantage of a serialised data stream arriving on a single digital pin. One of the notional tasks we had to manage was agreeing data timings to establish the “clock” speed of the incoming data. As a general rule, serial data communications interfaces work to a previously agreed data rate.
When we upload a program to an Arduino compiled with the IDE or when a program sends text to the Serial Monitor then we are using serial communications. In almost all instances we will be communicating using the built in serial capability that uses board pins 0 (RX) and 1 (TX). The USB connection on the Arduino board links to that serial interface and facilitates connections to a personal computer through a standardised USB port. [There is a UART in there too, to manage asynchronous communications while the main Arduino process gets on with something else.]
Some Arduino boards have more than one built in serial port. The Mega has an extra three (Serial1 pins 18 & 19, Serial2 16 & 17, Serial3 14 & 15) and the Due also has three extras on the same pin numbers plus an additional USB port on the SAM3X chip. The additional Due USB port can act as a USB host. If your board has multiple serial ports then they can be accessed in just the same way as the default Serial object. Try Serial1.begin(9600) as a good default to get things going once the relevant wire links are in place.
DIY Serial Communications
Many of the programs in this book have used the serial interface connected to the USB/serial adapter for one way and interactive communications with the IDE Serial Monitor. What if we wanted to link two Arduinos together using a serial port? To start, I am going to suggest that unless the need is very pressing we should stay well away from pins 0 and 1. We don’t want to risk compromising the program upload facility in any way. We can though, implement serial communications using any pair of pins (one for RX and one for TX) that support pin change interrupts. There is even a handy library, the SoftwareSerial library to manage things for us.
We could link two Arduinos together as shown in the following diagram. Each connecting wire, other than the ground link, is hooked to pins that will be acting as TX at one end and RX at the other.
My demo program is somewhat whimsical as it recalls my first experience of serial communications over a 300 baud dial up modem connected to a DEC minicomputer. This was my first professional gig and I was learning a language called BASIC+ (I had some COBOL experience). The first lesson in the little book on BASIC I had acquired was something like:
10 PRINT "HELLO WORLD" 20 GOTO 10 30 END
Maybe I have learned a little since then. I thought it might be nice to simulate sending this program to the PDP11/70 in the computer room and then receiving the output on a teletype terminal (no screen!). One Arduino is going to be the PDP and the other my terminal with the Serial Monitor standing duty for the printed output. [Maybe I should have included PWM and a speaker to simulate the zip-zip noise of the dot matrix print head.]
There is a serious point about such serial interfaces that hopefully excuses my personal historical references. There needs to be some sort of agreed protocol that establishes when each partner in the exchange listens or sends. You might imagine an Arduino that has analysed some sensors and now wishes to pass some data to another board. The other board could have been listening constantly but more likely has been occasionally polling the software serial port. The board that wants to send could send a short signal to tell the other board that data is ready. Once the (soon to be) receiving board spots the message it might send an acknowledgement and then switch to constant listening until the data transfer is complete. This might be a good scenario for a state machine when you think about it.
Anyway, here is the program emulating the teletype terminal:
#include <SoftwareSerial.h>
#define RX_PIN 11
#define TX_PIN 13
SoftwareSerial serialT(RX_PIN, TX_PIN); // new serial object
char buff[80];
byte bufPos = 0;
void setup() {
Serial.begin(115200);
serialT.begin(300); // Choose something more sensible
delay(1000);
serialT.println("Ready?");
serialT.println("10 PRINT \"HELLO WORLD\"");
serialT.println("20 GOTO 10");
serialT.println("30 END");
serialT.println("RUN");
}
void loop() { char nChar; if(serialT.available()){ nChar = serialT.read(); if(nChar == '\r' || nChar == '\n') { if(bufPos > 0) { Serial.println(buff); } bufPos = 0; memset(buff, '\0', 80); // clear buffer } else { buff[bufPos] = nChar; bufPos++; } } }
The program sends the fake BASIC program over the software serial link and then starts to listen for a response (the command “RUN” telling the connected Arduino to emulate the program execution).
The other Arduino runs the following program:
#include <SoftwareSerial.h>
#define RX_PIN 11
#define TX_PIN 13
char buff[80];
char greeting[80];
const char command[] = "PRINT \"";
const char run[] = "RUN";
byte bufPos = 0;
SoftwareSerial serialB(RX_PIN, TX_PIN);
void setup() {
serialB.begin(300); // again choose something more sensible
}
void loop() { // Sadly NOT a basic interpreter char nChar; if(serialB.available()){ nChar = serialB.read(); if(nChar == '\n' || nChar == '\r') { if(strstr(buff, run)) { runProg(); } char* inStr = strstr(buff, command); if(inStr != NULL) { // copy print output to greeting[] strncpy(greeting, inStr + strlen(command), 80); char* last = strchr(greeting, '"'); greeting[(int)(last-greeting)] = '\0'; } bufPos = 0; // buffer next input line memset(buff, '\0', 80); // clear buffer } else { buff[bufPos] = nChar; bufPos++; } } } void runProg() { while(true) { serialB.println(greeting); delay(500); } }
This program just processes the incoming bytes from the software serial link. When it spots a “PRINT” command it stores the text to be printed (delimited by double quote marks) in a buffer named greeting[]. When the command “RUN” is received the program switches to repeatedly sending any string in greeting[] to the serial link.
Two takeaway items here. The first is in the line:
strncpy(greeting, inStr + strlen(command), 80);
where the strncpy is passed two char arrays as the first pair of arguments. Of course, what the function gets by default is a pointer to the zero element of each array. There is no reason why the programmer can’t decide to use a pointer to another element of a given array – which is what the program statement in question does.
The next takeaway is that there are some string related functions that have not so far been mentioned in this book. They can be found in the string.h library which is automatically included by the IDE compile process. These additional functions are documented in a later chapter although many of those related to strings are also “wrapped” into the Arduino String class.
The function strstr() returns a pointer to the first occurrence of one string in another string.
The function strchr() returns a pointer to the first occurrence of a specified char in a string.
The function strncpy() copies a set number of bytes from one string to another.
Oh, and the statement greeting[(int)(last-greeting)] = '\0'; shows that a pointer can be used to calculate an array index although I am at a bit of a loss to think why this would be good practice outside of demonstration code. Count it as revision on the relationship between array indexes and pointers to the array data type.
My fake program got me researching small BASICs written in C. There are some, and I rather fancy tackling an Arduino BASIC although I have no idea what it might be useful for. Something you will find on the book website if you care to take a look.
RS232
The RS232 serial interface standard was once extensively used – indeed it enabled the world to connect to the early Internet using dial-up modems. RS232 initially used a 25 pin connection which was later reduced to 9 pins on personal computers. A minimalist RS232 connection (known as a null modem cable) used just 3 of the available pins – TX, RX and ground – just like the Arduino serial connections we just set up and used.
You can’t connect an Arduino directly to an RS232 port as RS232 runs at 12volts. If in need, there are plenty of RS232 to TTL level conversion boards available and many of them include a standard 9 pin female RS232 socket ready to hook up to some legacy device.
I2C
The Inter-Integrated Circuit (I2C) protocol was designed to allow multiple integrated circuits (chips) to communicate with a master chip. This two wire serial communications bus was invented by Philips Semiconductor, primarily to allow multiple peripherals to communicate with one or more microcontroller board.
I2C only requires two wires but can communicate with a great many devices connected simultaneously. One device must take the role of “master” and control the communication “clock” while others are deemed “slaves”. In fact, the protocol allows multiple masters although they have to take it in turns to take control of the clock.
Arduino boards have at least two designated pins for I2C communications. These are marked as SDA (the data pin) and SCL (the clock pin). This role is shared with Analogue pins A4 and A5 on the UNO for instance. The ATMega328 chip includes a hardware TWI (Two Wire Interface) module which handles I2C communications although it is perfectly possible to write an implementation only using software. The Arduino Wire library makes use of the inbuilt ATMega hardware for I2C communications. The I2C communication protocol would be a very convenient way of allowing two (or more) Arduinos to pass data and commands back and forth in a project requiring multiple boards to implement.
The I2C bus consists of two signal lines, SCL and SDA. The SCL line carries the clock signal, and the SDA line the data signal. While the clock signal s always generated by the current master device it is permissible for a slave to pull the clock line LOW to signal a pause in the current communication. The I2C bus drivers can only pull signal lines down which neatly avoids many contention issues but requires “pull-up” resistors to return lines to a HIGH state when no device is pulling them LOW.
I2C communications require devices to stick to the defined protocol. The protocol requires inter device messages to be constructed from two types of “frame”. There is an address frame that specifies the device that is the intended recipient of a message and this is followed by one or more data frame. Within the Arduino world, addresses are 7 bits long and the address is followed by a single bit indicating if this is a read or write operation. Pulses on the clock line are used to signal the presence of individual bits on the data line. The clock line is pulled LOW and a data bit set on the data line. The connected devices sample the data line on the following rising edge on the clock line.
We can control I2C communications most easily through the Wire library. This would normally start with Wire.begin() to join the I2C bus as a master (alternately a device can join as a slave by passing a 7 bit device address as an argument to Wire.begin()). This would initialise the TWI hardware and its connection to the appropriate board pins. It would also set the clock rate to a default value (100khz) although an alternate clock frequency can be set with the Wire.setClock() method.
The Wire.beginTransmission(address) would start a transmission to the slave device with the designated address. Wire.write(data) is then used to send one or more bytes of data to the slave device. Data is written as a sequence of bytes. A transmission is ended with Wire.endTransmission().
#include <Wire.h> byte data = 42; void setup() { Wire.begin(); // join i2c bus as master Wire.beginTransmission(60); // transmit to device address 60 Wire.write(data); // sends data byte Wire.endTransmission(); // stop transmitting }
The Wire.requestFrom() method is used to request data (as bytes) from a specified slave device. Wire.available() will then tell the master device how many bytes have been returned in response to the request. Wire.read() can then be called to read each individual byte from the TWI buffer.
#include <Wire.h> void setup() { Wire.begin(); // join i2c bus as master Serial.begin(115200); Wire.requestFrom(60, 6); // request 6 bytes from slave address 60 while(Wire.available()) // slave may not send 6 bytes { char c = Wire.read(); // read bytes as char Serial.print(c); // print the character } Serial.println(); }
While making I2C communications very straightforward, the Wire library itself does have a couple of downsides. It is a little bit profligate with buffer space and this could be an issue where available SRAM is tight. Plus, the Wire library blocks other processes while transmitting or receiving which is a potential issue in time critical applications. Oddly, the TWI hardware could run without blocking so perhaps this will be dealt with in a future library update.
The I2C interface is wired in a very similar manner to the software serial interface but the two wires connect the same pin at each end. You should also take the precaution of linking the ground pins on each board.
Now a couple of programs to demonstrate an I2C link. The “master” program controls the exchange of data while the “slave” program implements functions to be called by the 2 wire serial interface interrupt to service incoming data and requests to send data.
The “master” program:
#include <Wire.h> #define SLAVE_ADDRESS 42 int reqPin = 1; void setup() { Serial.begin(115200); Wire.begin(); } void loop() { int remoteVal; Wire.beginTransmission(SLAVE_ADDRESS); Wire.write(reqPin); // sends low order byte Wire.endTransmission(); Wire.requestFrom(SLAVE_ADDRESS, 2); while(Wire.available()) { byte bl = Wire.read(); // read low byte byte bh = Wire.read(); // read high byte remoteVal = bh; remoteVal = remoteVal << 8; remoteVal += bl; } Serial.print("Analog pin "); Serial.print(reqPin); Serial.print(" value: "); Serial.println(remoteVal); reqPin++; if(reqPin > 3) {reqPin = 1;} delay(1000); }
The program sends the values 1, 2 and 3 in turn to a slave process on the I2C bus. After sending the data, the master requests a two byte response from the same slave unit. The bytes are expected to represent a 16 bit integer and the low order byte arrives first. The integer is assembled and displayed by the Serial Monitor.
The “slave” program:
#include <Wire.h> #define I2C_ADDRESS 42 int reqVal; void setup() { Serial.begin(115200); Wire.begin(I2C_ADDRESS); Wire.onReceive(receiveData); Wire.onRequest(sendData); } void loop() { // This Arduino can be doing many other things here }
The slave program joins the I2C bus with a specified address (42 in this instance) and sets functions to be called in response to “2 wire” interrupts. Those functions should be treated as ISRs with all that this implies in terms of a speedy execution. As all of the tasks servicing the I2C interface are interrupt driven, the slave Arduino program can concentrate on alternate activities. The demo “bus” service functions follow.
void receiveData(int byteCount){ while(Wire.available()) { reqVal = Wire.read(); } } void sendData() { int retVal = 0; switch(reqVal) { case 1: retVal = analogRead(A1); break; case 2: retVal = analogRead(A2); break; case 3: retVal = analogRead(A3); break; } Serial.println(retVal); Wire.write(retVal); retVal = retVal >> 8; Wire.write(retVal); }
The receiveData() function reads a single byte and stores the value in the int variable reqVal. Only the low order byte is required in this instance as the value range is only 1 to 3. Yes that’s cheating but it keeps the demo concentrating on what matters.
The sendData() function reads what is effectively a random value from one of three unconnected analogue pins. The two bytes of the int variable are then returned to the “master” process in response to the request.
The values being passed back and forth here are pretty meaningless outside of the context of a demonstration program but should clarify nicely just how the I2C interface is controlled by the bus master and how the slave units use interrupts to respond to events on the bus addressed to them.
You might think that passing bytes through the interface using bit shifting could quickly get complicated. Fortunately, a better approach is explored along with the SPI interface that follows.
Serial Peripheral Interface (SPI)
The Arduino built-in library of useful C functions includes two intended to support a parallel shift register or a serial peripheral interface. These are shiftOut() and shiftIn(). here is support for SPI from a standard library (SPI.h) but this library uses specified pins and these two functions assist a programmer developing the same functionality on any available pin set.
The serial peripheral interface is a synchronous serial interface used to communicate with peripheral devices over short distances. There is only one “master” device and this would typically be a microcontroller board such as an Arduino. There are four modes of operation and some variability acknowledged in individual device implementations
One frequent use for this interface is to connect to radio transceiver boards like the nRF24L01+. This particular board is supported by the downloadable RF24 library which implements the serial peripheral interface and provides support for a range of radio protocols. Even if you splash out on a dedicated power supply for the nRF24L01 boards (and I would recommend you do) a project should still be able to add radio communications to an Arduino for something like £2.
The SPI interface is also commonly used to communicate with boards providing read/write access to SD cards. Logging data to an SD card might often prove very attractive for medium and long-term data storage. I picked up a card supporting micro-SD cards for less than £5 and this worked very well with a micro SD card formatted for FAT32 (which is near enough as standard when purchased).
The diagram below shows the default connections from an Arduino Uno although in actual fact I used female/female jumper wires to connect through the ICSP block of 6 male headers at one end of the Uno board. There is no need to connect to pin 10 (or equivalent chip select pin) but the unconnected jumper shown is intended to indicate that this pin can’t be used for anything else while the SPI interface is in use.
The SPI interface is a four wire interface although we are only using three plus 5v and ground to power the SD board. On the Uno, pin 11 is the MOSI connection, pin 12 MIS0 and pin 13 SCK. Those same processor connections are routed through to the ICSP block so it does not matter which you choose to hook up to.
The following program is not intended as a tutorial on using the SD library which makes substantial demands on program and data memory space. What I did think, was that this was a great way to explore the differences between serialising data to pass through an interface and passing data objects in a binary format which we touched on in a previous chapter.
Take a struct like this one:
struct ddata{ float area; unsigned long time; int val; } demo = {22145.679, 45871298, -32766};
If the values defined for the demo instance of the struct were passed through a serial interface using the normal functions such as print() or write() then they would be passed as ascii characters. If we allow a separator between the three fields (comma or space maybe) this would amount to 25 bytes of data. Reconstructing the struct at the other end of the interface would require a process to buffer the maximum possible number of bytes and to implement functionality to identify the individual components, then to convert then to their numeric data types. This sort of process is said to serialise the numeric values in order to pass them over the serial link.
Alternately we could define a union type to contain the struct and a suitably sized byte array.
union dbytes{ struct ddata data; byte bytes[10]; };
If (as in this instance) we were to pass the ten byte array through the serial interface then the magnitude of the individual values stored in the struct would be irrelevant. The binary format of the values would be preserved and the recreation of the struct format is trivial.
The following program demonstrates this process by passing values in a struct twice through an SPI comms. interface. Most of the code is “guard code” to do with managing the SD.h library, so concentrate on the code related to the union instances.
#include <SPI.h> #include <SD.h> struct ddata{ float area; unsigned long time; int val; } demo = {22145.679, 45871298, -32766}; union dbytes{ struct ddata data; byte bytes[10]; } going, coming;
void setup() { Serial.begin(115200); going.data = demo; if (!SD.begin()) { Serial.println("Card failed, or not present"); return; } if(SD.exists("datatest.dat")){ SD.remove("datatest.dat"); // start with a new file } Serial.println("Creating File"); File dataFile = SD.open("datatest.dat", FILE_WRITE); if(dataFile) { dataFile.write(going.bytes, 10); dataFile.close(); } else { Serial.println("Error opening file"); return; } if(SD.exists("datatest.dat")) { dataFile = SD.open("datatest.dat", FILE_READ); if(dataFile) { dataFile.read(coming.bytes, 10); dataFile.close(); if(coming.data.area == demo.area) { Serial.print(coming.data.area, 3); Serial.println(": Area checked"); } if(coming.data.time == demo.time) { Serial.print(coming.data.time); Serial.println(": Time checked"); } if(coming.data.val == demo.val) { Serial.print(coming.data.val); Serial.println(": Int val checked"); } } else { Serial.println("Error reopening File"); } } else { Serial.println("Where is the File?"); } }
If you have the kit to run this demonstration program then you should see ten bytes out and then 10 bytes read back with all numeric values checked on return.
USB
At the start of this chapter it was mentioned that the USB port found on most Arduinos connects to the default serial port on the board. We can see the serial port TX and RX LEDs flashing as data is passed in either direction through the USB connection. As we have seen from the start, the USB port is used to upload programs to the Arduino and to allow a process running on the Arduino to send messages back to the Serial Monitor.
The Arduino can send data through the USB port that allows the device to emulate other peripherals that can connect to a PC. The available IDE examples include emulation of a mouse or keyboard. Based upon these examples it is clear that an Arduino program could emulate other USB connected devices that talk to a USB host. What an Arduino Uno for instance can’t do is directly interact with other USB peripherals connected to itself. This is because the standard USB port is not set up to act as a host. You can though, buy shields that add an extra USB port that can act as a host. The one I have communicates with the attached Arduino using an SPI interface but the port itself can connect to a whole range of devices such as game controllers.
The Arduino Due has two USB ports and one of these is the “native” USB connection for the processor chip while the other (designated the programming port) works in the same way as the USB port on the Uno. The Due “native” port can act as a host.
Clearly, there are a host of interesting things one can do with an Arduino’s USB port but that is true of all of the other connections.
Parallel Interface
A visit to a computer history museum (or my loft) might give you the opportunity to take a look at the rear of a PC still equipped with a couple of RS232 ports. Alongside those, you will very likely see a 25 pin parallel port. These were most often used to connect to a printer back before such devices were network enabled. The parallel port facilitated writing (or reading) eight bits of data at a time. This was an important asset when serial communications were still slow. Nowadays, the venerable USB1 port can deliver 12 megabits a second while USB2 can theoretically transfer 60 megabytes a second and USB3 640 megabytes a second (that’s 5 gigabits a second). Because of this, parallel interfaces with all of their required connections have fallen out of favour.
Ordinarily, if you needed an Arduino to read eight bits at a time from some external source you would use a parallel shift register and a four wire SPI style interface to move data to or from the shift register. As we have not yet made any use of the digital pin port registers it might be fun to link two Arduinos to support parallel communications and see how much of a leg up we can get from ports. Plus, this is an opportunity to take a look at conditional compilation.
The first project design snag is that (on the Uno) only the port managing pins D0 to D7 is 8 bits wide but if we are going to avoid the use of pins D0 and D1 (serial rx and tx) then even that has to be considered as only safe for 6 bits. My plan therefore started by deciding that pins D6 to D13 should be configured to present the 8 bits of a byte through ports D and B. Then the control “lines” could be connected to pins A0 to A2.
The diagram illustrates that the required connections are straightforward with each jumper connecting to the same pin on each Arduino. A ground connection has been added as well.
The initial programming aim is to emulate a parallel printer connection with one Arduino standing in for the printer and the other sending text to be “printed”. This requires three control connections in addition to the eight pins used to pass a byte at a time. The controls are designated “strobe”, “acknowledge” and “busy” and these have been assigned to A0, A1 and A2 respectively.
The program sending text to the printer emulator follows.
#define data0 6 #define data1 7 #define data2 8 #define data3 9 #define data4 10 #define data5 11 #define data6 12 #define data7 13 #define strobe A0 #define ack A1 #define busy A2 char testMessage[] = "HELLO PARALLEL WORLDS";
void setup() { pinMode(strobe, OUTPUT); digitalWrite(strobe, HIGH); pinMode(ack, INPUT_PULLUP); pinMode(busy, INPUT_PULLUP); pinMode(data0, OUTPUT); pinMode(data1, OUTPUT); pinMode(data2, OUTPUT); pinMode(data3, OUTPUT); pinMode(data4, OUTPUT); pinMode(data5, OUTPUT); pinMode(data6, OUTPUT); pinMode(data7, OUTPUT); }
void loop() { for(int i = 0, j = strlen(testMessage); i < j; i++) { writeByte(testMessage[i]); } writeByte('\n'); delay(3000); }
void writeByte(byte b){ while(digitalRead(busy) == HIGH) { // wait a bit } digitalWrite(data0, b & 1); digitalWrite(data1, b & (1 << 1)); digitalWrite(data2, b & (1 << 2)); digitalWrite(data3, b & (1 << 3)); digitalWrite(data4, b & (1 << 4)); digitalWrite(data5, b & (1 << 5)); digitalWrite(data6, b & (1 << 6)); digitalWrite(data7, b & (1 << 7)); digitalWrite(strobe, LOW); while(digitalRead(ack) == HIGH) { //wait for acknowledge } digitalWrite(strobe, HIGH); }
You may have noticed that the code does not seem to be doing anything with ports. Those are coming shortly. This first version is intended to ensure that what the program is doing is totally clear.
The code starts by defining the pin numbers and a test message to send to the print emulator. The setup() function sets the data pins for output along with the pin designated as “strobe”. The other two control pins are set for INPUT_PULLUP to await incoming signals. The loop() function simply sends the bytes in the test char array to the writeByte() function before waiting a bit and then re-sending the message. The writeByte() function will wait if the busy line is high as that would indicate that the print emulator (or a real printer) was not ready for the next character. The next step is to set the individual data pins to the relevant bit in the byte b. Then the strobe pin is pulled LOW until the acknowledge pin is pulled low by the printer emulator.
That’s all that is required and it is likely that this code would operate a physical printer if you made up a cable and connected to the printer parallel port.
[It may be that the strobe signal should be of a short duration for some printers and that the code should not wait for the acknowledge to allow the pin to go HIGH. If you ever need to, try a couple of milliseconds]
Now the printer emulator. Start the same way by defining the pins while the setup() reverses the pin settings from the first program.
#define data0 6 #define data1 7 #define data2 8 #define data3 9 #define data4 10 #define data5 11 #define data6 12 #define data7 13 #define strobe A0 #define ack A1 #define busy A2
void setup() { Serial.begin(115200); pinMode(strobe, INPUT); pinMode(ack, OUTPUT); digitalWrite(ack, HIGH); pinMode(busy, OUTPUT); digitalWrite(busy, HIGH); pinMode(data0, INPUT); pinMode(data1, INPUT); pinMode(data2, INPUT); pinMode(data3, INPUT); pinMode(data4, INPUT); pinMode(data5, INPUT); pinMode(data6, INPUT); pinMode(data7, INPUT); }
void loop() { readByte(); } byte readByte() { digitalWrite(ack, HIGH); while(digitalRead(strobe) == HIGH){ digitalWrite(busy, LOW); } digitalWrite(busy, HIGH); byte b = digitalRead(data0) + (digitalRead(data1) << 1) + (digitalRead(data2) << 2) + (digitalRead(data3) << 3) + (digitalRead(data4) << 4) + (digitalRead(data5) << 5) + (digitalRead(data6) << 6) + (digitalRead(data7) << 7); digitalWrite(ack, LOW); delay(5); // hold ack LOW for a bit Serial.print((char)b); }
The loop() function repeatedly calls readByte(). The readByte() function sets the acknowledge pin high and the busy pin low while it waits for the strobe signal. The busy pin can then go HIGH and the data read from the data pins to be summed together to set the byte b. Once the data pins have been read then the acknowledge pin can be pulled low and the byte received sent to the Serial Monitor after being cast to a char. Of course, there is no reason why the data transferred has to be one or more char variable. Other variable types (including structs) can be transferred a byte at a time.
Code such as this could be used to read data from any device that communicated through a parallel port. You never know what you might turn up at a “car boot” sale.
Just before we get to conditional compilation and Arduino ports, make a note of the program sizes as displayed by the IDE after compilation. We are going to set about reducing those numbers.
Conditional compilation
We have run into the #ifndef directive when looking at C++ classes and libraries. There it was used to check to see if an identifier had not been previously defined and if not went ahead and included the content up until the #endif directive into the compilation. We are now going to use the #ifdef directive to compile some code if the identifier exists and other code if it does not (commented out perhaps). This approach is commonly used to leave debugging code in a program for future use when maintaining the code while excluding the debug statements from a normal compilation.
After adding the definition
#define USE_PORTS
to the top of the program that wrote bytes to the parallel port we can then change the setup() function to optionally use ports.
Refer back to the chapter titled “Input and Output” for a table of ports, pins, registers and constants. Sometimes, below, I have used bit shifts to set one or more register value and other times a binary byte constant.
void setup() { #ifdef USE_PORTS DDRC &= ((1 << DDC1) | (1 << DDC2)); // A1 & A2 INPUT DDRC |= (1 << DDC0); // A0 output PORTC |= ((1 << PORTC1) | (1 << PORTC2)); //A1 & A2 PULLUP DDRB |= B00111111; // D8 -D13 set for OUTPUT DDRD |= B11000000; // D6 & D7 for OUTPUT #else pinMode(strobe, OUTPUT); digitalWrite(strobe, HIGH); pinMode(ack, INPUT_PULLUP); pinMode(busy, INPUT_PULLUP); pinMode(data0, OUTPUT); pinMode(data1, OUTPUT); pinMode(data2, OUTPUT); pinMode(data3, OUTPUT); pinMode(data4, OUTPUT); pinMode(data5, OUTPUT); pinMode(data6, OUTPUT); pinMode(data7, OUTPUT); #endif }
The block of code between #ifdef and #else is functionally equivalent to the original setup() function code that we started with. You can check that by compiling the code and running the program in combination with the Arduino acting as the printer emulator. There should be no change in the output although you should note a reduction in the compiled (sending) program size. Now we can do something similar with the writeByte() function.
Here I have broken the code substitution down into sections to make it easier to follow just what the port register manipulation is doing at each stage. The function gets a bit lengthy but you only need to type in the additions.
void writeByte(byte b){ #ifdef USE_PORTS while(PINC & B00000100){ // while busy HIGH } #else while(digitalRead(busy) == HIGH) { // wait a bit } #endif #ifdef USE_PORTS byte regCpy = (PORTB >> 6); PORTB = (regCpy << 6) | (b >> 2); regCpy = (PORTD << 2); PORTD = (regCpy >> 2) | (b << 6); #else digitalWrite(data0, b & 1); digitalWrite(data1, b & (1 << 1)); digitalWrite(data2, b & (1 << 2)); digitalWrite(data3, b & (1 << 3)); digitalWrite(data4, b & (1 << 4)); digitalWrite(data5, b & (1 << 5)); digitalWrite(data6, b & (1 << 6)); digitalWrite(data7, b & (1 << 7)); #endif #ifdef USE_PORTS PORTC ^= B00000001; // strobe LOW while(PINC & B00000010){ // Wait for ack } PORTC |= B00000001; // strobe HIGH #else digitalWrite(strobe, LOW); while(digitalRead(ack) == HIGH) { //wait for acknowledge } digitalWrite(strobe, HIGH); #endif }
The “fiddly” bit of code here is where we want to set selected bits in a register without upsetting any existing values related to non-involved pins. I hope that the approach that takes a copy of a register and then rolls off the bits that need to be reset before ORing in the new values is as clear as possible.
Once all of this new port related code has been compiled – just how much has the program shrunk? It will be running much faster as well. The only downside (as it suggested in the earlier chapter) is code readability. That’s OK here, where we can compare port manipulation with a more traditional Arduino approach but, well let’s look at the printer emulator without the original code. [You might prefer to edit the first version, inserting the #ifdef lines, for a clearer view.]
void setup() { Serial.begin(115200); DDRC |= ((1 << DDC1) | (1 << DDC2)); // A1 & A2 OUTPUT PORTC |= ((1 << PORTC0) | (1 << PORTC2)); //A0 PULLUP, A2 HIGH } void loop() { readByte(); } byte readByte() { PINC |= B00000010; while(PINC & B00000001){ PINC &= B00000100; } PINC |= B00000100; byte b = (PINB << 2); b |= (PIND >> 6); PORTC ^= B00000010; // ack LOW delay(5); // hold ack LOW for a bit Serial.print((char)b); }
Concise, fast, efficient but not terribly readable even with a smattering of comments.
Just to finish this chapter, a table of conditional compilation directives.
Directive | Format | Description |
#if | #if constant integer expression | Checks if the constant expression is true (non zero) |
#ifdef | #ifdef identifier | Checks to see if the identifier has been defined |
#ifndef | #ifndef identifier | Checks to see if the identifier has not been defined |
#else | #else | Follows #if, #ifdef or #ifndef with code following included in the compilation of the condition was not met |
#elif | #elif constant condition | Shorthand for #else followed by #if |
#endif | #endif | Terminates an entire conditional block |
defined operator | defined(macro) | Used with #if to replace multiple #ifdef or #ifndef directives. (See below.) |
#if defined(USE_PORTS) || defined(WRITER)
// code to conditionally compile
#endif
might replace
#ifdef USEPORTS
//conditional code
#endif
#ifdef WRITER
// more code
#endif