Author Archives: Cindy Wu

About Cindy Wu

Staff writer and technical support for PMD Way Limited (pmdway.com) and tronixstuff.com

Tutorial – Using DS1307 and DS3231 Real-time Clock Modules with Arduino

We keep getting requests on how to use DS1307 and DS3231 real-time clock modules with Arduino from various sources – so this is the first of a two part tutorial on how to use them. For this Arduino tutorial we have  two real-time clock modules to use, one based on the Maxim DS1307:

DS1307 real-time clock from PMD Way

and another based on the DS3231:

DS3231 real time clock from PMD Way

There are two main differences between the ICs on the real-time clock modules, which is the accuracy of the time-keeping. The DS1307 used in the first module works very well, however the external temperature can affect the frequency of the oscillator circuit which drives the DS1307’s internal counter.

This may sound like a problem, however will usually result with the clock being off by around five or so minutes per month. The DS3231 is much more accurate, as it has an internal oscillator which isn’t affected by external factors – and thus is accurate down to a few minutes per year at the most. If you have a DS1307 module- don’t feel bad, it’s still a great value board and will serve you well.

With both of the modules, a backup battery is not installed when you receive them – it’s a good idea to buy a new CR2032 battery and fit it to the module.

Along with keeping track of the time and date, these modules also have a small EEPROM, an alarm function (DS3231 only) and the ability to generate a square-wave of various frequencies – all of which will be the subject of a second tutorial.

Connecting your module to an Arduino

Both modules use the I2C bus, which makes connection very easy. If you’re not sure about the I2C bus and Arduino, check out the I2C tutorials (chapters 20 and 21), or review chapter seventeen of the book “Arduino Workshop“.

Moving on – first you will need to identify which pins on your Arduino or compatible boards are used for the I2C bus – these will be knows as SDA (or data) and SCL (or clock).

  • On Arduino Uno or compatible boards, these pins are A4 and A5 for data and clock;
  • On the Arduino Mega the pins are D20 and D21 for data and clock;
  • And if you’re using a Pro Mini-compatible the pins are A4 and A5 for data and clock, which are parallel to the main pins.

DS1307 module

If you have the DS1307 module you will need to solder the wires to the board, or solder on some inline header pins so you can use jumper wires. Then connect the SCL and SDA pins to your Arduino, and the Vcc pin to the 5V pin and GND to GND.

DS3231 module

Connecting this module is easy as header pins are installed on the board at the factory. You can simply run jumper wires again from SCL and SDA to the Arduino and again from the module’s Vcc and GND pins to your board’s 5V or 3.3.V and GND. However these are duplicated on the other side for soldering your own wires.

Both of these modules have the required pull-up resistors, so you don’t need to add your own. Like all devices connected to the I2C bus, try and keep the length of the SDA and SCL wires to a minimum.

Reading and writing the time from your RTC Module

Once you have wired up your RTC module. enter and upload the following sketch. Although the notes and functions in the sketch refer only to the DS3231, the code also works with the DS1307.

#include "Wire.h"
#define DS3231_I2C_ADDRESS 0x68
// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
  return( (val/10*16) + (val%10) );
}
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
  return( (val/16*10) + (val%16) );
}
void setup()
{
  Wire.begin();
  Serial.begin(9600);
  // set the initial time here:
  // DS3231 seconds, minutes, hours, day, date, month, year
  // setDS3231time(30,42,21,4,26,11,14);
}
void setDS3231time(byte second, byte minute, byte hour, byte dayOfWeek, byte
dayOfMonth, byte month, byte year)
{
  // sets time and date data to DS3231
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0); // set next input to start at the seconds register
  Wire.write(decToBcd(second)); // set seconds
  Wire.write(decToBcd(minute)); // set minutes
  Wire.write(decToBcd(hour)); // set hours
  Wire.write(decToBcd(dayOfWeek)); // set day of week (1=Sunday, 7=Saturday)
  Wire.write(decToBcd(dayOfMonth)); // set date (1 to 31)
  Wire.write(decToBcd(month)); // set month
  Wire.write(decToBcd(year)); // set year (0 to 99)
  Wire.endTransmission();
}
void readDS3231time(byte *second,
byte *minute,
byte *hour,
byte *dayOfWeek,
byte *dayOfMonth,
byte *month,
byte *year)
{
  Wire.beginTransmission(DS3231_I2C_ADDRESS);
  Wire.write(0); // set DS3231 register pointer to 00h
  Wire.endTransmission();
  Wire.requestFrom(DS3231_I2C_ADDRESS, 7);
  // request seven bytes of data from DS3231 starting from register 00h
  *second = bcdToDec(Wire.read() & 0x7f);
  *minute = bcdToDec(Wire.read());
  *hour = bcdToDec(Wire.read() & 0x3f);
  *dayOfWeek = bcdToDec(Wire.read());
  *dayOfMonth = bcdToDec(Wire.read());
  *month = bcdToDec(Wire.read());
  *year = bcdToDec(Wire.read());
}
void displayTime()
{
  byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
  // retrieve data from DS3231
  readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month,
  &year);
  // send it to the serial monitor
  Serial.print(hour, DEC);
  // convert the byte variable to a decimal number when displayed
  Serial.print(":");
  if (minute<10)
  {
    Serial.print("0");
  }
  Serial.print(minute, DEC);
  Serial.print(":");
  if (second<10)
  {
    Serial.print("0");
  }
  Serial.print(second, DEC);
  Serial.print(" ");
  Serial.print(dayOfMonth, DEC);
  Serial.print("/");
  Serial.print(month, DEC);
  Serial.print("/");
  Serial.print(year, DEC);
  Serial.print(" Day of week: ");
  switch(dayOfWeek){
  case 1:
    Serial.println("Sunday");
    break;
  case 2:
    Serial.println("Monday");
    break;
  case 3:
    Serial.println("Tuesday");
    break;
  case 4:
    Serial.println("Wednesday");
    break;
  case 5:
    Serial.println("Thursday");
    break;
  case 6:
    Serial.println("Friday");
    break;
  case 7:
    Serial.println("Saturday");
    break;
  }
}
void loop()
{
  displayTime(); // display the real-time clock data on the Serial Monitor,
  delay(1000); // every second
}

There may be a lot of code, however it breaks down well into manageable parts.

It first includes the Wire library, which is used for I2C bus communication, followed by defining the bus address for the RTC as 0x68. These are followed by two functions that convert decimal numbers to BCD (binary-coded decimal) and vice versa. These are necessary as the RTC ICs work in BCD not decimal.

The function setDS3231time() is used to set the clock. Using it is very easy, simple insert the values from year down to second, and the RTC will start from that time. For example if you want to set the following date and time – Wednesday November 26, 2014 and 9:42 pm and 30 seconds – you would use:

setDS3231time(30,42,21,4,26,11,14);

Note that the time is set using 24-hour time, and the fourth paramter is the “day of week”. This falls between 1 and 7 which is Sunday to Saturday respectively. These parameters are byte values if you are subsituting your own variables.

Once you have run the function once it’s wise to prefix it with // and upload your code again, so it will not reset the time once the power has been cycled or micrcontroller reset.

Reading the time form your RTC Is just as simple, in fact the process can be followed neatly inside the function displayTime(). You will need to define seven byte variables to store the data from the RTC, and these are then inserted in the function readDS3231time().

For example if your variables are:

byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;

… you would refresh them with the current data from the RTC by using:

readDS3232time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);

Then you can use the variables as you see fit, from sending the time and date to the serial monitor as the example sketch does – to converting the data into a suitable form for all sorts of output devices.

Just to check everything is working, enter the appropriate time and date into the demonstration sketch, upload it, comment out the setDS3231time() function and upload it again. Then open the serial monitor, and you should be provided with a running display of the current time and date.

From this point you now have the software tools to set data to and retrieve it from your real-time clock module, and we hope you have an understanding of how to use these inexpensive modules.

You can learn more about the particular real-time clock ICs from the manufacturer’s website – DS1307 and DS3231.

This post brought to you by pmdway.com everything for makers and electronics enthusiasts, with free delivery worldwide.

To keep up to date with new posts at tronixstuff.com, please subscribe to the mailing list in the box on the right, or follow us on twitter @tronixstuff.

Project – Scrolling text clock

The purpose of this project is to build a scrolling text clock that displays the time as it is spoken (for example, “it’s midnight”).

scrolling-clock-pmdway

This is a quick project – we give you enough to get going with the hardware and sketch, and then you can take it further to suit your needs.

Hardware

You’ll need three major items –

You might want an external power supply, but we’ll get to that later on. The first stage is to fit your real-time clock. Click here for the tutorial if you need help with that. By now I hope you’re thinking “how do you set the time?”.

There’s two answers to that question. If you’re using the DS3231 just set it in the sketch (see below) as the accuracy is very good, you only need to upload the sketch with the new time twice a year to cover daylight savings.

Otherwise add a simple user-interface – a couple of buttons could do it. Finally you just need to put the hardware on the back of the DMD. There’s plenty of scope to meet your own needs, a simple solution might be to align the control board so you can access the USB socket with ease – and then stick it down with some Sugru.

With regards to powering the clock – you can run ONE LED display from the Arduino, and it runs at a good brightness for indoor use. If you want the DMD to run at full, retina-burning brightness you need to use a separate 5V 4A DC power supply. If you’re using two DMDs – that goes to 8A, and so on. Simply connect the external power to one DMD’s terminals (connect the second or more DMDs to these terminals):

highpowerss

If you don’t fancy chopping the end of your power supply cable, use a DC socket breakout.

The Arduino Sketch

You will need to install the following two Arduino libraries – TimerOne and DMD. Then upload the sketch:

// for RTC
#include "Wire.h"
#define DS1307_I2C_ADDRESS 0x68 // the DS1307 RTC is 0x68

// for LED display
#include "SPI.h"
#include "DMD.h"
#include "TimerOne.h"
#include "SystemFont5x7.h"
#include "Arial_black_16.h"
#define DISPLAYS_ACROSS 1 // you could have more than one DMD in a row
#define DISPLAYS_DOWN 1
DMD dmd(DISPLAYS_ACROSS, DISPLAYS_DOWN);

String finalString; // used to hold final sentence to display on DMD

void ScanDMD() // required for DMD
{
dmd.scanDisplayBySPI();
}

void setup()
{
// for DMD
Timer1.initialize( 5000 );
Timer1.attachInterrupt( ScanDMD );
dmd.clearScreen(true);

// for RTC
Wire.begin(); // fire up I2C bus
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
// change the variables and uncomment the setDateDs1307 to set the time
// then re-comment out the function and upload the sketch again
second = 0;
minute = 13;
hour = 23;
dayOfWeek = 4;
dayOfMonth = 19;
month = 5;
year = 13;
// setDateDs1307(second, minute, hour, dayOfWeek, dayOfMonth, month, year);
}

// usual RTC functions
// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
return ( (val/10*16) + (val%10) );
}

// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
return ( (val/16*10) + (val%16) );
}

void setDateDs1307(byte second, // 0-59
byte minute, // 0-59
byte hour, // 1-23
byte dayOfWeek, // 1-7
byte dayOfMonth, // 1-28/29/30/31
byte month, // 1-12
byte year) // 0-99
{
Wire.beginTransmission(DS1307_I2C_ADDRESS);
Wire.write(0);
Wire.write(decToBcd(second)); // 0 to bit 7 starts the clock
Wire.write(decToBcd(minute));
Wire.write(decToBcd(hour));
Wire.write(decToBcd(dayOfWeek));
Wire.write(decToBcd(dayOfMonth));
Wire.write(decToBcd(month));
Wire.write(decToBcd(year));
Wire.write(00010000); // sends 0x10 (hex) 00010000 (binary) to control register - turns on square wave
Wire.endTransmission();
}

// Gets the date and time from the ds1307
void getDateDs1307(byte *second,
byte *minute,
byte *hour,
byte *dayOfWeek,
byte *dayOfMonth,
byte *month,
byte *year)
{
// Reset the register pointer
Wire.beginTransmission(DS1307_I2C_ADDRESS);
Wire.write(0);
Wire.endTransmission();

Wire.requestFrom(DS1307_I2C_ADDRESS, 7);

// A few of these need masks because certain bits are control bits
*second = bcdToDec(Wire.read() & 0x7f);
*minute = bcdToDec(Wire.read());
*hour = bcdToDec(Wire.read() & 0x3f); // Need to change this if 12 hour am/pm
*dayOfWeek = bcdToDec(Wire.read());
*dayOfMonth = bcdToDec(Wire.read());
*month = bcdToDec(Wire.read());
*year = bcdToDec(Wire.read());
}

void drawText(String oldString)
{
dmd.clearScreen(true);
dmd.selectFont(Arial_Black_16);
char newString[256];
int sLength = oldString.length();
oldString.toCharArray(newString, sLength+1);
dmd.drawMarquee(newString,sLength,(32*DISPLAYS_ACROSS)-1,0);
long start=millis();
long timer=start;
long timer2=start;
boolean ret=false;
while(!ret){
if ((timer+20) < millis()) {
ret=dmd.stepMarquee(-1,0);
timer=millis();
}
}
}

void createTextTime(int hh, int mm)
// this mashes up all the time data into text as one sentence
{
finalString=" "; // wipe the sentence out for special cases (below)
finalString=finalString+"It's ";

// now add the hour
if (hh==1 || hh==13) {
finalString=finalString+"one ";
}
if (hh==2 || hh==14) {
finalString=finalString+"two ";
}
if (hh==3 || hh==15) {
finalString=finalString+"three ";
}
if (hh==4 || hh==16) {
finalString=finalString+"four ";
}
if (hh==5 || hh==17) {
finalString=finalString+"five ";
}
if (hh==6 || hh==18) {
finalString=finalString+"six ";
}
if (hh==7 || hh==19) {
finalString=finalString+"seven ";
}
if (hh==8 || hh==20) {
finalString=finalString+"eight ";
}
if (hh==9 || hh==21) {
finalString=finalString+"nine ";
}
if (hh==10 || hh==22) {
finalString=finalString+"ten ";
}
if (hh==11 || hh==23) {
finalString=finalString+"eleven ";
}

// now add the minutes
switch(mm){
case 1:
finalString=finalString+"oh one ";
break;
case 2:
finalString=finalString+"oh two ";
break;
case 3:
finalString=finalString+"oh three ";
break;
case 4:
finalString=finalString+"oh four ";
break;
case 5:
finalString=finalString+"oh five ";
break;
case 6:
finalString=finalString+"oh six ";
break;
case 7:
finalString=finalString+"oh seven ";
break;
case 8:
finalString=finalString+"oh eight ";
break;
case 9:
finalString=finalString+"oh nine ";
break;
case 10:
finalString=finalString+"ten ";
break;
case 11:
finalString=finalString+"eleven ";
break;
case 12:
finalString=finalString+"twelve ";
break;
case 13:
finalString=finalString+"thirteen ";
break;
case 14:
finalString=finalString+"fourteen ";
break;
case 15:
finalString=finalString+"fifteen ";
break;
case 16:
finalString=finalString+"sixteen ";
break;
case 17:
finalString=finalString+"seventeen ";
break;
case 18:
finalString=finalString+"eighteen ";
break;
case 19:
finalString=finalString+"nineteen ";
break;
case 20:
finalString=finalString+"twenty ";
break;
case 21:
finalString=finalString+"twenty one ";
break;
case 22:
finalString=finalString+"twenty two ";
break;
case 23:
finalString=finalString+"twenty three ";
break;
case 24:
finalString=finalString+"twenty four ";
break;
case 25:
finalString=finalString+"twenty five";
break;
case 26:
finalString=finalString+"twenty six";
break;
case 27:
finalString=finalString+"twenty seven";
break;
case 28:
finalString=finalString+"twenty eight ";
break;
case 29:
finalString=finalString+"twenty nine ";
break;
case 30:
finalString=finalString+"thirty ";
break;
case 31:
finalString=finalString+"thirty one ";
break;
case 32:
finalString=finalString+"thirty two";
break;
case 33:
finalString=finalString+"thirty three ";
break;
case 34:
finalString=finalString+"thirty four";
break;
case 35:
finalString=finalString+"thirty five ";
break;
case 36:
finalString=finalString+"thirty six";
break;
case 37:
finalString=finalString+"thirty seven";
break;
case 38:
finalString=finalString+"thirty eight ";
break;
case 39:
finalString=finalString+"thirty nine ";
break;
case 40:
finalString=finalString+"forty ";
break;
case 41:
finalString=finalString+"forty one ";
break;
case 42:
finalString=finalString+"forty two ";
break;
case 43:
finalString=finalString+"forty three ";
break;
case 44:
finalString=finalString+"forty four ";
break;
case 45:
finalString=finalString+"forty five ";
break;
case 46:
finalString=finalString+"forty six ";
break;
case 47:
finalString=finalString+"forty seven ";
break;
case 48:
finalString=finalString+"forty eight ";
break;
case 49:
finalString=finalString+"forty nine ";
break;
case 50:
finalString=finalString+"fifty ";
break;
case 51:
finalString=finalString+"fifty one ";
break;
case 52:
finalString=finalString+"fifty two ";
break;
case 53:
finalString=finalString+"fifty three ";
break;
case 54:
finalString=finalString+"fifty four ";
break;
case 55:
finalString=finalString+"fifty five ";
break;
case 56:
finalString=finalString+"fifty six ";
break;
case 57:
finalString=finalString+"fifty seven ";
break;
case 58:
finalString=finalString+"fifty eight ";
break;
case 59: finalString=finalString+"fifty nine "; break;
}

// midday?
if (hh==12 && mm==0) {
finalString=finalString+"midday ";
}
// midnight?
if (hh==00 && mm==0) {
finalString=finalString+"midnight ";
}

}

void loop()
{
// get the time from the RTC
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);

// convert the time into a sentence string
createTextTime(hour,minute);

// now send the text to the DMD
drawText(finalString);
}

The sketch has the usual functions to set and retrieve the time from DS1307/3232 real-time clock ICs, and as usual with all our clocks you can enter the time information into the variables in void setup(), then uncomment setDateDs1307(), upload the sketch, re-comment setDateDs1307, then upload the sketch once more. Repeat that process to re-set the time if you didn’t add any hardware-based user interface.

Once the time is retrieved in void loop(), it is passed to the function createTextTime(). This function creates the text string to display by starting with “It’s “, and then determines which words to follow depending on the current time. Finally the function drawText() converts the string holding the text to display into a character variable which can be passed to the DMD.

And here it is in action:

Conclusion

This was a quick project, however we hope you found it either entertaining or useful – and another random type of clock that’s easy to reproduce or modify yourself.

This post brought to you by pmdway.com – offering everything for makers and electronics enthusiasts, with free delivery worldwide.

To keep up to date with new posts at tronixstuff.com, please subscribe to the mailing list in the box on the right, or follow us on twitter @tronixstuff.

Tutorial: Maximising your Arduino’s I/O ports with MCP23017

In this article we discuss how to use the Microchip MCP23017 16-bit serial expander with I2C serial interface. This 28-pin IC offers sixteen inputs or outputs – and up to eight of the ICs can be used on one I2C bus… offering a maximum of 128 extra I/O ports.

A few people may be thinking “Why not just get an Arduino Mega2560?” – a good question. However you may have a distance between the Arduino and the end-point of the I/O pins – so with these ICs you can run just four wires instead of a lot more; save board space with custom designs, and preserve precious digital I/O pins for other uses. Plus we think the I2C bus is underappreciated! So let’s get started…

Here is our subject of the article in DIP form:

mcp23017ss

You can order these in through-hole, surface-mount and also mounted on a breakout board. At this point you should also download yourself a copy of data sheet – it will be referred to several times, and very useful for reference and further reading.

Furthermore if you are not familiar with Arduino and the I2C bus, please familiarise yourself with the I2C tutorials parts one and two. The MCP23017 can be quite simple or complex to understand, so the goal of this article is to try and make it as simple as possible. After reading this you should have the knowledge and confidence to move forward with using a MCP23017.

First, let’s look at the hardware basics of this IC. Consider the pinouts:

pinouts

The sixteen I/O ports are separated into two ‘ports’ – A (on the right) and B (on the left. Pin 9 connects to 5V, 10 to GND, 11 isn’t used, 12 is the I2C bus clock line (Arduino Uno/Duemilanove analogue pin 5, Mega pin  21), and 13 is the I2C bus data line (Arduino Uno/Duemailnove analogue pin 4, Mega pin 20).

External pull-up resistors should be used on the I2C bus – in our examples we use 4.7k ohm values. Pin 14 is unused, and we won’t be looking at interrupts, so ignore pins 19 and 20. Pin 18 is the reset pin, which is normally high – therefore you ground it to reset the IC. So connect it to 5V!

Finally we have the three hardware address pins 15~17. These are used to determine the I2C bus address for the chip. If you connect them all to GND, the address is 0x20. If you have other devices with that address or need to use multiple MCP23017s, see figure 1-2 in the datasheet.

You can alter the address by connecting a combination of pins 15~17 to 5V (1) or GND (0). For example, if you connect 15~17 all to 5V, the control byte becomes 0100111 in binary, or 0x27 in hexadecimal.

Next, here is a basic schematic illustrating how to connect an MCP23017 to a typical Arduino board. It contains the minimum to use the IC, without any sensors or components on the I/O pins:

mcp20317_schemss

Now to examine how to use the IC in our sketches.

As you should know by now most I2C devices have several registers that can be addressed. Each address holds one byte of data that determines various options. So before using we need to set whether each port is an input or an output. First, we’ll examine setting them as outputs. So to set port A to outputs, we use:

Wire.beginTransmission(0x20);
Wire.write(0x00); // IODIRA register
Wire.write(0x00); // set all of port A to outputs
Wire.endTransmission();

Then to set port B to outputs, we use:

Wire.beginTransmission(0x20);
Wire.write(0x01); // IODIRB register
Wire.write(0x00); // set all of port B to outputs
Wire.endTransmission();

So now we are in void loop()  or a function of your own creation and want to control some output pins. To control port A, we use:

Wire.beginTransmission(0x20);
Wire.write(0x12); // address port A
Wire.write(??);  // value to send
Wire.endTransmission();

To control port B, we use:

Wire.beginTransmission(0x20);
Wire.write(0x13); // address port B
Wire.write(??);  // value to send
Wire.endTransmission();

… replacing ?? with the binary or equivalent hexadecimal or decimal value to send to the register.

To calculate the required number, consider each I/O pin from 7 to 0 matches one bit of a binary number – 1 for on, 0 for off. So you can insert a binary number representing the status of each output pin. Or if binary does your head in, convert it to hexadecimal. Or a decimal number.

So for example, you want pins 7 and 1 on. In binary that would be 10000010, in hexadecimal that is 0x82, or 130 decimal. (Using decimals is convenient if you want to display values from an incrementing value or function result).

If you had some LEDs via resistors connected to the outputs, you would have this as a result of sending 0x82:

0x82

For example, we want port A to be 11001100 and port B to be 10001000 – so we send the following (note we converted the binary values to decimal):

Wire.beginTransmission(0x20);
Wire.write(0x12); // address port A
Wire.write(204); // value to send
Wire.endTransmission();
Wire.beginTransmission(0x20);
Wire.write(0x13); // address port B 
Wire.write(136);     // value to send
Wire.endTransmission();

… with the results as such (port B on the left, port A on the right):

0xcc0x88

Now let’s put all of this output knowledge into a more detailed example. From a hardware perspective we are using a circuit as described above, with the addition of a 560 ohm resistor followed by an LED thence to ground from on each of the sixteen outputs. Here is the sketch:

/*
 Example 41.1 - Microchip MCP23017 with Arduino
*/
// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
void setup()
{
 Wire.begin(); // wake up I2C bus
// set I/O pins to outputs
 Wire.beginTransmission(0x20);
 Wire.write(0x00); // IODIRA register
 Wire.write(0x00); // set all of port A to outputs
 Wire.endTransmission();
Wire.beginTransmission(0x20);
 Wire.write(0x01); // IODIRB register
 Wire.write(0x00); // set all of port B to outputs
 Wire.endTransmission();
}
void binaryCount()
{
 for (byte a=0; a<256; a++)
 {
 Wire.beginTransmission(0x20);
 Wire.write(0x12); // GPIOA
 Wire.write(a); // port A
 Wire.endTransmission();
Wire.beginTransmission(0x20);
 Wire.write(0x13); // GPIOB
 Wire.write(a); // port B
 Wire.endTransmission();
 }
}
void loop()
{
 binaryCount();
 delay(500);
}

And here is the example blinking away:

Although that may have seemed like a simple demonstration, it was created show how the outputs can be used. So now you know how to control the I/O pins set as outputs. Note that you can’t source more than 25 mA of current from each pin, so if switching higher current loads use a transistor and an external power supply and so on.

Now let’s turn the tables and work on using the I/O pins as digital inputs. The MCP23017 I/O pins default to input mode, so we just need to initiate the I2C bus. Then in the void loop() or other function all we do is set the address of the register to read and receive one byte of data.

For our next example, we have our basic sketch as described at the start of this article using four normally-open buttons which are connected to port B inputs 0~3. Consider the first five lines of void loop() in the following example:

/*
 Example 41.2 - Microchip MCP23017 with Arduino
 */
// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
byte inputs=0;
void setup()
{
 Serial.begin(9600);
 Wire.begin(); // wake up I2C bus
}
void loop()
{
 Wire.beginTransmission(0x20);
 Wire.write(0x13); // set MCP23017 memory pointer to GPIOB address
 Wire.endTransmission();
 Wire.requestFrom(0x20, 1); // request one byte of data from MCP20317
 inputs=Wire.read(); // store the incoming byte into "inputs"
 if (inputs>0) // if a button was pressed
 {
 Serial.println(inputs, BIN); // display the contents of the GPIOB register in binary
 delay(200); // for debounce
 }
}

In this example void loop() sends the GPIOB address (0x13) to the IC. Then using Wire.requestFrom() it asks for one byte of data from the IC – the contents of the register at 0x13. This byte is stored in the variable inputs. Finally if inputs is greater than zero (i.e. a button has been pressed) the result is sent to the serial monitor window and displayed in binary. We display it in binary as this represents the state of the inputs 0~7. Here is an example of pressing the buttons 1, 2, 3 then 4 – three times:

ex41p2smon

And as we are reading eight inputs at once – you can detect multiple keypresses. The following is an example of doing just that:

ex41p2smon2

As you can see pressing all four buttons returned 1111, or the first and third returned 101. Each combination of highs and lows on the inputs is a unique 8-bit number that can also be interpreted in decimal or hexadecimal. And if you wanted to read all sixteen inputs at once, just request and store two bytes of data instead of one.

For our last example – a demonstration of using port A as outputs and port B as inputs. Four LEDs with matching resistors are connected to port A outputs 0~3, with the buttons connected as per example 41.2. Here is the sketch:

/*
 Example 41.3 - Microchip MCP23017 with Arduino
*/

// pins 15~17 to GND, I2C bus address is 0x20
#include "Wire.h"
byte inputs=0;

void setup()
{
  Serial.begin(9600);
  Wire.begin(); // wake up I2C bus

  Wire.beginTransmission(0x20);
  Wire.write(0x00); // IODIRA register
  Wire.write(0x00); // set all of bank A to outputs
  Wire.endTransmission();
}

void loop()
{
  // read the inputs of bank B
  Wire.beginTransmission(0x20);
  Wire.write(0x13);
  Wire.endTransmission();
  Wire.requestFrom(0x20, 1);
  inputs=Wire.read();

  // now send the input data to bank A
  Wire.beginTransmission(0x20);
  Wire.write(0x12); // GPIOA
  Wire.write(inputs);    // bank A
  Wire.endTransmission();
  delay(200); // for debounce
}

By now there shouldn’t be any surprises in the last example – it receives a byte that represents port B, and sends that byte out to port A to turn on the matching outputs and LEDs. For the curious, here it is in action:

So there you have it… another way to massively increase the quantity of digital I/O pins on any Arduino system by using the I2C bus. You can get the MCP23017 from PMD Way with free delivery worldwide.

This post brought to you by pmdway.com – everything for makers and electronics enthusiasts, with free delivery worldwide.

To keep up to date with new posts at tronixstuff.com, please subscribe to the mailing list in the box on the right, or follow us on twitter @tronixstuff.

Tutorial: Arduino and the SPI bus part II

This is part two of investigating the SPI data bus, and how we can control devices using it with our Arduino systems. If you have not done so already, please read part one of the SPI articles. Again we will learn the necessary theory, and then apply it by controlling a variety of devices. As always things will be kept as simple as possible.

First on our list today is the use of multiple SPI devices on the single bus. We briefly touched on this in part one, by showing how multiple devices are wired, for example:

sspiss2

Notice how the slave devices share the clock, MOSI and MISO lines – however they both have their own chip select line back to the master device. At this point a limitation of the SPI bus becomes prevalent – for each slave device we need another digital pin to control chip select for that device. If you were looking to control many devices, it would be better to consider finding I2C solutions to the problem.

To implement multiple devices is very easy. Consider the example 34.1 from part one – we controlled a digital rheostat. Now we will repeat the example, but instead control four instead of one. For reference, here is the pinout diagram:

mcp4162pinout

Doing so may sound complex, but it is not. We connect the SCK, MOSI and  MISO pins together, then to Arduino pins D13, D11, D12 respectively. Each CS pin is wired to a separate Arduino digital pin. In our example rheostats 1 to 4 connect to D10 through to D7 respectively. To show the resistance is changing on each rheostat, there is an LED between pin 5 and GND and a 470 ohm resistor between 5V and pin 6. Next, here is the sketch:

/*
Multiple SPI bus device demo using four Microchip MCP4162s 
*/

#include "SPI.h" // necessary library
int del=3; // used for various delays

int led1=10; // CS lines for each SPI device
int led2=9;
int led3=8;
int led4=7;

void setup()
{
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  pinMode(led4, OUTPUT);
  digitalWrite(led1, HIGH);
  digitalWrite(led2, HIGH);
  digitalWrite(led3, HIGH);
  digitalWrite(led4, HIGH);
  SPI.begin(); // wake up the SPI bus.
  SPI.setBitOrder(MSBFIRST);
  // our MCP4162s requires data to be sent MSB (most significant byte) first
}

void setValue(int l, int value)
// sends value 'value' to SPI device on CS digital out pin 'l'
{
  digitalWrite(l, LOW);
  SPI.transfer(0); // send command byte
  SPI.transfer(value); // send value (0~255)
  digitalWrite(l, HIGH);
}

void allOff()
// sets all pots to max resistance
{
     setValue(led1,255);
     setValue(led2,255);
     setValue(led3,255);
     setValue(led4,255);
}

void pulse(int l)
{
  allOff();
  for (int a=255; a>=0; --a)
  {
    setValue(l,a);
    delay(del);
  }
  for (int a=0; a<256; a++)
  {
    setValue(l,a);
    delay(del);
  }
}

void pulseAll()
{
  allOff();
  for (int a=255; a>=0; --a)
  {
    setValue(led1,a);
    setValue(led2,a);
    setValue(led3,a);
    setValue(led4,a);
    delay(del);
  }
  for (int a=0; a<256; a++)
  {
    setValue(led1,a);
    setValue(led2,a);
    setValue(led3,a);
    setValue(led4,a);
    delay(del);
  }
}

void loop()
{
  pulse(led1);
  pulse(led2);
  pulse(led3);
  pulse(led4);
  pulseAll();
}

Although the example sketch may be longer than necessary, it is quite simple. We have four SPI devices each controlling one LED, so to keep things easy to track we have defined led1~led4 to match the chip select digital out pins used for each SPI device.

Then see the first four lines in void setup(); these pins are set to output in order to function as required. Next – this is very important – we set the pins’ state to HIGH.

You must do this to every chip select line! Otherwise more than one CS pins may be initially low in some instances and cause the first data sent from MOSI to travel along to two or more SPI devices. With LEDs this may not be an issue, but for motor controllers … well it could be.

The other point of interest is the function

void setValue(int l, int value)

We pass the value for the SPI device we want to control, and the value to send to the device. The value for l is the chip select value for the SPI device to control, and ranges from 10~7 – or as defined earlier, led1~4. The rest of the sketch is involved in controlling the LED’s brightness by varying the resistance of the rheostats. Now to see example 36.1 in action via the following video clip:


Next on the agenda is a digital-to-analogue converter, to be referred to using the acronym DAC. What is a DAC? In simple terms, it accepts a numerical value between zero and a maximum value (digital) and outputs a voltage between the range of zero and a maximum relative to the input value (analogue).

One could consider this to be the opposite of the what we use the function analogRead(); for. For our example we will use a Microchip MCP4921 (data sheet.pdf):

mcp4921ss

(Please note that this is a beginners’ tutorial and is somewhat simplified). This DAC has a 12-bit resolution. This means that it can accept a decimal number between 0 and 4095 – in binary this is 0 to 1111 1111 1111 (see why it is called 12-bit) – and the outpout voltage is divided into 4096 steps. The output voltage for this particular DAC can fall between 0 and just under the supply voltage (5V). So for each increase of 1 in the decimal input value, the DAC will output around 1.221 millivolts.

It is also possible to reduce the size of the voltage output steps by using a lower reference voltage. Then the DAC will consider the reference voltage to be the maximum output with a value of 4095. So (for example) if the reference voltage was 2.5V, each increase of 1 in the decimal input value, the DAC will output around 0.6105 millivolts. The minimum reference voltage possible is 0.8V, which offers a step of 200 microvolts (uV).

The output of a DAC can be used for many things, such as a function generator or the playback of audio recorded in a digital form. For now we will examine how to use the hardware, and monitoring output on an oscilloscope. First we need the pinouts:

mcp4921pinout

By now these sorts of diagrams shouldn’t present any problems. In this example, we keep pin 5 permanently set to GND; pin 6 is where you feed in the reference voltage – we will set this to +5V; AVss is GND; and Vouta is the output signal pin – where the magic comes from 🙂 The next thing to investigate is the MCP4921’s write command register:

mcp4921wcr

Bits 0 to 11 are the 12 bits of the output value; bit 15 is an output selector (unused on the MPC4921); bit 14 controls the input buffer; bit 13 controls an inbuilt output amplifier; and bit 12 can shutdown the DAC. Unlike previous devices, the input data is spread across two bytes (or a word of data).

Therefore a small amount of work needs to be done to format the data ready for the DAC. Let’s explain this through looking at the sketch for example 36.2 that follows. The purpose of the sketch is to go through all possible DAC values, from 0 to 4095, then back to 0 and so on.

First. note the variable outputvalue – it is a word, a 16-bit unsigned variable. This is perfect as we will be sending a word of data to the DAC. We put the increasing/decreasing value for a into outputValue. However as we can only send bytes of data at a time down the SPI bus, we will use the function highbyte() to separate the high side of the word (bits 15~8) into a byte variable called data.

We then use the bitwise AND and OR operators to set the parameter bits 15~12. Then this byte is sent to the SPI bus. Finally, the function lowbyte() is used to send the low side of the word (bits 7~0) into data and thence down the SPI bus as well.

Now for our demonstration sketch:

/*
SPI bus device demo using a Microchip MCP4921 DAC 
 */

#include "SPI.h" // necessary library
int del=0; // used for various delays
word outputValue = 0; // a word is a 16-bit number
byte data = 0; // and a byte is an 8-bit number
void setup()
{
  //set pin(s) to input and output
  pinMode(10, OUTPUT);
  SPI.begin(); // wake up the SPI bus.
  SPI.setBitOrder(MSBFIRST);
}

void loop()
{
  for (int a=0; a<=4095; a++)
  {
    outputValue = a;
    digitalWrite(10, LOW);
    data = highByte(outputValue);
    data = 0b00001111 & data;
    data = 0b00110000 | data;
    SPI.transfer(data);
    data = lowByte(outputValue);
    SPI.transfer(data);
    digitalWrite(10, HIGH);
    delay(del);
  }
  delay(del+25);
  for (int a=4095; a>=0; --a)
  {
    outputValue = a;
    digitalWrite(10, LOW);
    data = highByte(outputValue);
    data = 0b00001111 & data;
    data = 0b00110000 | data;
    SPI.transfer(data);
    data = lowByte(outputValue);
    SPI.transfer(data);
    digitalWrite(10, HIGH);
    delay(del);
  }
  delay(del+25);
}

And a quick look at the DAC in action via an oscilloscope:

By now we have covered in detail how to send data to a device on the SPI bus. But how do we receive data from a device?

Doing so is quite simple, but some information is required about the particular device. For the rest of this chapter, we will use the Maxim DS3234 “extremely accurate” real-time clock. Please download the data sheet (.pdf) now, as it will be referred to many times.

The DS3234 is not available in through-hole packaging, so we will be using one that comes pre-soldered onto a breakout board:

ds3234ss

 

It only takes a few moments to solder in some header pins for breadboard use. The battery type is CR1220 (12 x 2.0mm, 3V); if you don’t have a battery you will need to short out the battery holder with some wire otherwise the IC will not work. Readers have reported that the IC doesn’t keep time if the USB and external power are both applied to the Arduino at the same time.

A device will have one or more registers where information is read from and written to. Look at page twelve of the DS3234 data sheet, there are twenty-three registers, each containing eight bits (one byte) of data.

Please take note that each register has a read and write address. An example – to retrieve the contents of the register at location 08h (alarm minutes) and place it into the byte data we need to do the following:

digitalWrite(10, LOW); // select the DS3234 that has its CS line on digital 10
SPI.transfer(0x08); // tell the DS3234 device we're requesting data from the register at 08h
data=SPI.transfer(0); // the DS3234 sends the data back and stores it in the byte data
digitalWrite(10, HIGH);  // deselect the DS3234 if finished with it

Don’t forget to take note of  the function SPI.setBitOrder(MSBFIRST); in your sketch, as this also determines the bit order of the data coming from the device. To write data to a specific address is also quite simple, for example:

digitalWrite(10, LOW);
SPI.transfer(0x80); // tells the device which address to write to
SPI.transfer(b00001010);   // you can send any representation of a byte
digitalWrite(10, HIGH);

Up to this point, we have not concerned ourselves with what is called the SPI data mode. The mode determines how the SPI device interprets the ‘pulses’ of data going in and out of the device.

For a well-defined explanation, please read this article. With some devices (and in our forthcoming example) the data mode needs to be defined. So we use:

SPI.setDataMode(SPI_MODE1);

to set the data mode, within void(setup);. To determine a device’s data mode, as always – consult the data sheet. With our DS3234 example, the mode is mentioned on page 1 under Features List.

Finally, let’s delve a little deeper into SPI via the DS3234. The people at Sparkfun have already written a good demonstration sketch for the DS3234, so let’s have a look at that and deconstruct it a little to see what is going on. You can download the sketch below from here, then change the file extension from .c to .pde.

#include "SPI.h"
const int  cs=8; //chip select 

void setup() {
  Serial.begin(9600);
  RTC_init();
  //day(1-31), month(1-12), year(0-99), hour(0-23), minute(0-59), second(0-59)
  SetTimeDate(11,12,13,14,15,16);
}

void loop() {
  Serial.println(ReadTimeDate());
  delay(1000);
}
//=====================================
int RTC_init(){
	  pinMode(cs,OUTPUT); // chip select
	  // start the SPI library:
	  SPI.begin();
	  SPI.setBitOrder(MSBFIRST);
	  SPI.setDataMode(SPI_MODE1); // both mode 1 & 3 should work
	  //set control register
	  digitalWrite(cs, LOW);
	  SPI.transfer(0x8E);
	  SPI.transfer(0x60); //60= disable Osciallator and Battery SQ wave @1hz, temp compensation, Alarms disabled
	  digitalWrite(cs, HIGH);
	  delay(10);
}
//=====================================
int SetTimeDate(int d, int mo, int y, int h, int mi, int s){
	int TimeDate [7]={s,mi,h,0,d,mo,y};
	for(int i=0; i<=6;i++){
		if(i==3)
			i++;
		int b= TimeDate[i]/10;
		int a= TimeDate[i]-b*10;
		if(i==2){
			if (b==2)
				b=B00000010;
			else if (b==1)
				b=B00000001;
		}
		TimeDate[i]= a+(b<<4);

		digitalWrite(cs, LOW);
		SPI.transfer(i+0x80);
		SPI.transfer(TimeDate[i]);
		digitalWrite(cs, HIGH);
  }
}
//=====================================
String ReadTimeDate(){
	String temp;
	int TimeDate [7]; //second,minute,hour,null,day,month,year
	for(int i=0; i<=6;i++){
		if(i==3)
			i++;
		digitalWrite(cs, LOW);
		SPI.transfer(i+0x00);
		unsigned int n = SPI.transfer(0x00);
		digitalWrite(cs, HIGH);
		int a=n & B00001111;
		if(i==2){
			int b=(n & B00110000)>>4; //24 hour mode
			if(b==B00000010)
				b=20;
			else if(b==B00000001)
				b=10;
			TimeDate[i]=a+b;
		}
		else if(i==4){
			int b=(n & B00110000)>>4;
			TimeDate[i]=a+b*10;
		}
		else if(i==5){
			int b=(n & B00010000)>>4;
			TimeDate[i]=a+b*10;
		}
		else if(i==6){
			int b=(n & B11110000)>>4;
			TimeDate[i]=a+b*10;
		}
		else{
			int b=(n & B01110000)>>4;
			TimeDate[i]=a+b*10;
			}
	}
	temp.concat(TimeDate[4]);
	temp.concat("/") ;
	temp.concat(TimeDate[5]);
	temp.concat("/") ;
	temp.concat(TimeDate[6]);
	temp.concat("     ") ;
	temp.concat(TimeDate[2]);
	temp.concat(":") ;
	temp.concat(TimeDate[1]);
	temp.concat(":") ;
	temp.concat(TimeDate[0]);
  return(temp);
}

Don’t let the use of custom functions and loops put you off, they are there to save time. Looking in the function SetTimeDate();, you can see that the data is written to the registers 80h through to 86h (skipping 83h – day of week) in the way as described earlier (set CS low, send out address to write to, send out data, set CS high).

You will also notice some bitwise arithmetic going on as well. This is done to convert data between binary-coded decimal and decimal numbers.

Why? Go back to page twelve of the DS3234 data sheet and look at (e.g.) register 00h/80h – seconds. The bits 7~4 are used to represent the ‘tens’ column of the value, and bits 3~0 represent the ‘ones’ column of the value.

So some bit shifting is necessary to isolate the digit for each column in order to convert the data to decimal. Finally here is another example of reading the time data from the DS3234:

/*
SPI bus device demo using a Maxim IC DS3234 Accurate RTC
 */
#include "SPI.h" // necessary library
// store hours, minutes, seconds, day of month, month, year
byte h,m,s,d,mo,y;
void setup() 
{
 pinMode(10, OUTPUT);
 SPI.begin(); // wake up the SPI bus.
 SPI.setBitOrder(MSBFIRST);
 SPI.setDataMode(SPI_MODE1); 
 digitalWrite(10, LOW); 

 SPI.transfer(0x8E); // write to control register 8Eh (page 14 data sheet)
 SPI.transfer(0x60); // oscillator on, 1Hz, alarms off (b01100000)
 digitalWrite(10, HIGH);
 Serial.begin(9600);
}

void readDS3234() 
{
 byte data=0;
 int a,b=0;
 digitalWrite(10, LOW);

 // get seconds
 SPI.transfer(0x00); // get seconds data from register 00h
 data=SPI.transfer(0x00);
 a = data & B00001111; // isolates 1's column
 b = (data & B01110000)>>4; // isolates 10's digit
 s=(b*10)+a; // calculate seconds!

 // get minutes
 SPI.transfer(0x01); // get minutes data from register 00h
 data=SPI.transfer(0x00);
 a = data & B00001111; // isolates 1's column
 b = (data & B01110000)>>4; // isolates 10's digit
 m=(b*10)+a; // calculate minutes!

 // get hours
 SPI.transfer(0x02); // get minutes data from register 00h
 data=SPI.transfer(0x00);
 a = data & B00001111; // isolates 1's column
 b = (data & B00110000)>>4; // isolate upper nibble
 if (b==B00000010) // 24 hr time
 { 
 b=20;
 } else
 if (b==B00000001) // 12 hr time
 {
 b=10;
 }
 h = a + b; // calculate hours

 digitalWrite(10, HIGH);
}
void loop()
{
 readDS3234();
 Serial.print(h, DEC);
 Serial.print(":");
 if (m<10)
 {
 Serial.print("0");
 }
 Serial.print(m, DEC);
 Serial.print(":");
 if (s<10)
 {
 Serial.print("0");
 }
 Serial.print(s, DEC);
 Serial.println();
 delay(500);
}

So there you have it – more about the world of the SPI bus and how to control the devices within.

This post brought to you by pmdway.com – everything for makers and electronics enthusiasts, with free delivery worldwide.

To keep up to date with new posts at tronixstuff.com, please subscribe to the mailing list in the box on the right, or follow us on twitter @tronixstuff.