Thursday, December 27, 2018

I2C with Arduino: Three Mini Examples


Hi there. I noticed that I did not write anything about Arduino except one post in May 2015 where it is mentioned in one sentence. A little bit for this reason but actually to have a documentation for myself, I decided to write about Arduino. This is kind of an interim post because I could not begin to write the article I have in my mind. In this post, I will give three examples for using I2C bus with Arduino.

Arduino has a useful interface for I2C bus. I previously used I2C with PC serial port and with PIC series micro-controllers. After all, I was surprised how it's incredibly easy with Arduino. I2C bus uses two pins for communication. It is quite similar to old Akbils (short for akıllı bilet i.e. smart ticket in touch on memory (TOM) used previously in Istanbul). Two of the ICs, I used in the examples, are actually manufactured by Dallas Semiconductors (bought by Maxim) which has also manufactured TOMs used as Akbil.


Arduino UNO uses A4 and A5 pins for I2C. Their function is SDA (Serial Data) and SCL (Serial Clock) respectively. The communication in I2C happens between two devices called master and slave. Master generates the clock signal and accesses the devices by their addresses. Slave reads and/or writes data on the bus as it receives its own address in the bus. The clock signal, SCL is used for synchronization during the communication and data flows in SDA pin. I will not dig deeper into the details about the protocol (like start and end of transmission frames etc.) to keep the post short. I will start with the examples.

Sources:
https://www.arduino.cc/en/Reference/Board
https://www.arduino.cc/en/Reference/Wire
https://en.wikipedia.org/wiki/I%C2%B2C


External EEPROM with AT2432 IC
Overture: ATmega328P (i.e. Arduino UNO) has an internal EEPROM of 1KB. I learned this after I first met Arduino. I suppose no one uses this internal EEPROM due to its limited rewrite cycles. At least I did not encounter someone who uses it. It is quite easy to access this internal storage using EEPROM library (source) however I prefer an external storage.

I used Atmel's IC AT24C32. This is an 32K EEPROM consisting from 8 x 4096 bytes. Here are the datasheet from official website and same document in my Google Drive. There are three address pins like A0, A1 and A2. This means: I have written above that every slave has a I2C address. This address is 7 bits long and it is 1010XXXb for this chip. Last three bits of the address is left to the designer. If three of them are grounded the address gets 1010000b or if they all are connected to Vcc then it gets 1010111b. Thus, up to eight chips can be used on the same bus.

AT2432 EEPROM
It is obvious that I2C addresses have to be unique. In theory, 128 chips can be connected to the same bus if their addresses are different. Those chips with address pins can be used more than one in the same bus.
WP pin is used for write protection. I have grounded it because I am not going to use it. SCL and SDA pins are connected to A5 and A4 pins of Arduino respectively using 4.7K pull-up resistors. The schematic is in the next figure.
Fritzing is "almost officially" schematics editor of Arduino project. Fritzing schematics can be found in every book or project that has something to do with Arduino. I couldn't get used to it because, usually I create my schematics in PCAD and nowadays in EagleCAD. However there is currently no better option than Fritzing for Arduino schematics because it contains block diagrams of Arduino itself as well as most of its shields. Another possibility is that I could not find block diagrams for EagleCAD. In 'Breadboard' view in Fritzing, schematic can be drawn as easy as plugging the components onto a breadboard. Many Arduino books include those breadboard view schematics to look "more user-friendly". I find these schematics pretty useless, maybe because I am old fashioned. On these schematics, it is very unclear where the cables are connected or where they pass. Therefore I will put here only old style schematics. And btw, I am still learning Fritzing.

The project code is as follows:

 /* AT24C32 I2C EEPROM Example */

#define MX24C32  B1010000    // 7-bit I2C address
#include <Wire.h>

void setup()  {
  Serial.begin(9600);
  Wire.begin();
}

void loop()  {
  char hello[8] = {'H', 'e', 'l', 'l', 'o', '!', '!'};
  byte addr = 0;
  int X;
  char t;

  // This part writes 'Hello!!' on the EEPROM and it
  // has to run once. Therefore it's commented out
  //for(int i = 0; i < 7; i++)  {
  //  Wire.beginTransmission(MX24C32); // Send write command
  //  Wire.write(0x00);         // Send high addr.
  //  Wire.write(addr++);       // Send low addr. and increase var. for next char
  //  Wire.write(byte(hello[i]));     // Send the byte
  //  Wire.endTransmission();
  //  delay(100);               // wait until EEPROM is ready
  //}

  // This part sends an empty write command (without data) to
  // the address 0x0 of the EEPROM to seek at that address
  Wire.beginTransmission(MX24C32);
  Wire.write(0x00);
  Wire.write(0x00);
  Wire.endTransmission();

  delay(100);

  // 10 bytes will be read
  for(int i = 0; i < 10; i++)  {
    Wire.requestFrom(MX24C32, 1);       // request a byte from the chip
    X = Wire.available();               // is it available on the bus?
    Serial.print("X = "); Serial.println(X);
    if(X >= 1)  {
      t = Wire.read();                  // read the byte if it's available
      Serial.print("t= "); Serial.println(t);
    }
  }

  while(1);
}


I send the data to the IC, using Wire.write() function. Write command is sent by Wire.beginTransmission() function and it is followed by two bytes of address and one byte of data to be written. Wire.endTransmission() ends the I2C frame. Please refer the datasheet p.9: "A write operation requires two 8-bit data word addresses following the device address word and acknowledgment". The chip has an internal pointer, so to say. Reading occurs from where the last write operation left off. In other words, write operation has an address operand but read operation doesn't. To read from a specific address, a seek operation is done by sending a write command with address but no data. This is the general usage. More detailed information can be found on the datasheet.

Here is another similar project:
https://playground.arduino.cc/code/I2CEEPROM


Thermometer using DS1621 IC
DS1621 is an I2C thermometer integrated circuit by Dallas Semiconductors. (Note: Dallas has another chip with thermometer included, in same packaging of Akbil). This IC also supports using up to eight chips on the same bus. Tout is the temperature alarm pin. This pin is set when the temperature goes higher than the given threshold (datasheet, google drive). The schematics is as follows:
DS1621 Thermometer

There is a library for this IC but I did not used it:
https://github.com/martinhansdk/DS1621-temperature-probe-library-for-Arduino

/* DS1621 temperature sensor interfaced by Arduino
 *
 * I2C ID will be 0x48 if A0 = A1 = A2 = GND
 */

#define DS1621 B1001000
#include <Wire.h>

void setup()   {
  Serial.begin(9600);
  Wire.begin();
  Wire.beginTransmission(DS1621);
  Wire.write(0xAC);     // Send Access Config command
  Wire.write(0x02);     // Write 02 to config register
                        // Output polarity bit = 1 => Active High
  delay(10);

  Wire.beginTransmission(DS1621);
  Wire.write(0xEE);     // send "start convert" command
  Wire.endTransmission();       // Stop bit

}

void loop()  {
  byte SH, SL, X;
  // SH: High order byte of temperature
  // SL: Low order byte of temperature

  Wire.beginTransmission(DS1621);
  Wire.write(0xAA);     // Send "Read Temperature" command
  Wire.endTransmission();


  Wire.requestFrom(DS1621, 2);
  // After sending "read temperature" command
  // request two bytes from IC for temperature
  X = Wire.available();
  if(X >= 2)  {        // if 2 bytes were received
    // SH contains the integer part of the temperature
    SH = Wire.read();
    // SL contains 0x80 for 0.5 degree celcius
    SL = Wire.read();
    Serial.print(SH, HEX);
    Serial.print("    ");
    Serial.println(SL, HEX);
  }
  else
    Serial.println(X);
  //Wire.endTransmission();

  delay(500);

}
 
0xEE and 0xAC are the commands specific to this IC. The list of all commands can be found on the tenth page of the datasheet. During the initialization, method of operation is written to the config register. For example 0x02 means One Shot mode = 0 and Polarity = 1. 0xEE command starts the temperature conversion cycle and using 0xAA command, temperature values are pulled to the bus from the IC. Although there is a busy flag in config register, I implemented busy waiting with delay() function for simplicity.

There is an old project where this IC is connected to a PC through the serial port. I will explain this project in one of the following posts.






Real Time Clock (RTC) with IC DS1307

DS1307 is a RTC chip again by Dallas Semiconductors. This IC does not have any address bits because it is meaningless to use more than one RTC in same circuit. There is a VBAT input pin for battery supply. The clock continues to tick on battery even if Arduino is powered off. I had made a note to myself "do not connect the battery to Vcc" but I cannot remember why. This IC can be thought as counter with 64 byte of RAM. The counter increases seconds, if seconds overflow minutes are increased, if minutes overflow hours are increased and so on.

DS1307 Real Time Clock
There are ready-to-use kits of this IC and even kits with this IC and AT2432 (the IC, I discussed in the first example) on the same board. There is also a library for this IC. I wrote my own code but I borrowed two function from this library.


This IC requires a 32.768 KHz crystal oscillator to run. X1 and X2 pins are connected to this oscillator. Pin 7 is square wave output. It is floating just because it is unused. There are plenty of comments in the code:


/* The code for DS1307 RTC circuit. Do not connect the battery
 * to Vcc. Do not forget the pull-up resistors on SDA and SCL
 */

#include <Wire.h>
#define DS1307 B1101000

// These two functions were borrowed from the source
// code of RTC library of jeelabs:
// https://jeelabs.org/2010/02/05/new-date-time-rtc-library/
static uint8_t bcd2bin (uint8_t val) { return val - 6 * (val >> 4); }
static uint8_t bin2bcd (uint8_t val) { return val + 6 * (val / 10); }

void setup()  {
  Serial.begin(9600);
  Wire.begin();

  // Begin initialization
  Wire.beginTransmission(DS1307);
  Wire.write(0);        // send first "0" as an address byte
  Wire.endTransmission();

  Wire.requestFrom(DS1307, 1);
  int ss = Wire.read(); // read a character
  Serial.println(ss);
  // End initialization
  // If the chip returns a character then it's working


  /*   //  Set the clock first if it is not set yet
  Wire.beginTransmission(DS1307);
  Wire.write(0);
  Wire.write(0x0);    // seconds
  Wire.write(0x21);   // minutes
  Wire.write(0x0);    // hours
  Wire.write(0);      // day of the week
  Wire.write(0x24);   // day
  Wire.write(0x12);   // month
  Wire.write(0x18);   // year
  Wire.write(0x10);   // config register: SQ Wave out @1Hz
  Wire.endTransmission();
  // */

}

void loop()  {
  Wire.beginTransmission(DS1307);
  Wire.write(0);        // send "0" as an address byte
  Wire.endTransmission();

  Wire.requestFrom(DS1307, 7);  // Read 7 bytes from RTC
  /* Those 7 bytes are:
   * 00H: CH Bit + Seconds BCD (Bit 7 of 00H is the Clock Halt
   *      (CH) bit. It stops the oscillator when set.
   *      "Please note that the initial power-on state of all
   *      registers is not defined. Therefore, it is important
   *      to enable the oscillator (CH bit = 0) during initial
   *      configuration.")
   * 01H: Minutes BCD
   * 02H: Hours BCD (Bit 6 of the hours register selects 12H
   *      mode when set and in 12H mode, bit 5 represents AM
   *      when reset and PM when set. If bit 6 is reset, 4.
   *      and 5. bits are tens place of hours in 24H mode.
   * 03H: Week of the day. Unused in this code.
   * 04H: Day of the month BCD
   * 05H: Month (BCD)
   * 06H: Year (BCD)
   * 07H: Control register
   */

  // Read the seconds but discard CH bit:
  uint8_t ss = bcd2bin(Wire.read() & 0x7F);
  uint8_t mm = bcd2bin(Wire.read());   // Read minutes
  uint8_t hh = bcd2bin(Wire.read());   // Read hours in 24H
  Wire.read();                         // ignore the week day
  uint8_t d = bcd2bin(Wire.read());    // day
  uint8_t m = bcd2bin(Wire.read());    // month
  uint16_t y = bcd2bin(Wire.read());   // year

  Serial.print(d); Serial.print(".");
  Serial.print(m); Serial.print(".");
  Serial.print(y); Serial.print("   ");
  Serial.print(hh); Serial.print(":");
  Serial.print(mm); Serial.print(":");
  Serial.println(ss);

  delay(1000);  // read every second.
}

Thursday, November 29, 2018

How to Define Special Characters on LCD


Hi there. This post is somehow a continuation of previous LCD post however not related with hardware but only software. I will address the problems, mentioned in the previous post and mention how to define special characters on LCD. Although, this is really simple to do in high level platforms such as Arduino using libraries, unfortunately there are only few resources on the Internet about that. 

I will try to follow the sequence of events in the previous post.


Problem with the Pins
I could not write any character to the display initially. It came to my mind that the characters were actually written on the display but I couldn't see them. I had experienced this a long time ago. It's simple enough to connect the LCD contrast pin (3) to the ground over a resistor with 4.7K or with 2.2K value. I was going to do like that. I was still thinking like "it is working without any problem but I could not see it", therefore I changed the resistor with a potentiometer in order to adjust the contrast. Normally, backlight is also not needed in green LCDs. Therefore, I chose a green one. Anyway, I connected pin 15 of LCD to Vcc and pin 16 of LCD to GND.

The problem was still not solved, although I connected everything without any shortcuts. I have changed the line outb(0, BASE) at the end of the code to outb(255, BASE). I checked the voltage on the data pins but everything was fine with them. I increased the delay parameter to 3 seconds in order to read signals with multimeter and checked E and RS pins. E was flipping between high and low as expected but there was nothing on RS except plain high signal. Pin connections I used, are as follows:

PP Signal
DB25 Pin
Centronics Pin
IC In
IC Out
LCD Signal (Pin)
nStrobe(C0)
1
1
IC2_17
IC2_3
E (6)
nSelect (C3)
17
36
IC2_15
IC2_5
RS (4)
Data0 (D0)
2
2
IC1_2
IC1_18
D0 (7)
Data1 (D1)
3
3
IC1_4
IC1_16
D1 (8)
Data2 (D2)
4
4
IC1_6
IC1_14
D2 (9)
Data3 (D3)
5
5
IC1_8
IC1_12
D3 (10)
Data4 (D4)
6
6
IC1_17
IC1_3
D4 (11)
Data5 (D5)
7
7
IC1_15
IC1_5
D5 (12)
Data6 (D6)
8
8
IC1_13
IC1_7
D6 (13)
Data7 (D7)
9
9
IC1_11
IC1_9
D7 (14)

The source article, I used during the assembly of the circuit, shows pin 13 for nSelect signal. I misunderstood this part, because pin 13 is actually an input pin. In another source [ https://www.lammertbies.nl/comm/cable/parallel.html ], which I usually used, port directions were also drawn. I was trying to get an output from pin 13 not pin 36 and since pin 13 is an input pin it was always set. BTW, the width of the plastic part on the both ends of jumper cables (colored ones) is bigger than the distance of the pins in Centronics port. Therefore, I used wires in even numbered ports and jumper cables in odd numbered ports (example). First, I suspected a contact problem but if this was the case, the code would produce different results on each execution. I will also mention the relationship of signals and ports in further chapters.

By the way, there is a video on LCDs, that there is no upper limit for DELAY parameter:


The Problem with the Function lcdKomut()
This function was initially written as follows: 

void lcdKomut(unsigned char veri)    {
    outb(veri, BASE);
    outb(8   , CTRL);    // RS = 0; E = 1
    usleep(DELAY);
    outb(9   , CTRL);    // RS = 0; E = 0
    usleep(DELAY);
}

For some reason, each time after I sent a command with this function, following data was printed twice. For example, as I tried to print 'Testing' in first line and '123' in the second, it was actually printed 'TTesting' and '1123'. I thought, this caused because the parallel port controller cannot reset the pin fast enough. Even E pin is still not zero, the CPU was too fast compared to the controller and sends the data to the bus immediately. But I have observed the same behavior even with bigger values of DELAY parameter. I changed the value 9 to 1 in the code and problem was fixed. There is an image, I like about these situations:


Since the problem was solved, I didn't want to investigate further. Maybe I should examine incoming signals with an oscilloscope but as I said, I didn't want to deal with it.


Status and Control Signals of Parallel Port
Speaking of signals, I need to briefly mention status and control signals. Parallel port has three groups of signals: data, status and control. These signals are controlled by the BASE, BASE+1 and BASE+2 I/O ports, respectively. Data signals are simple: The parallel port pins between 2 and 9 are set/reset according to the byte written to the BASE port and they remain. Status pins can be used for input because they are designed to read the printers status, i.e. printer generates an interrupt request, runs out of paper or has a paper jam etc.

nStrobe and nSelect pins are in control signal group. These control signals exist to control the printer and they are "active low" signals. Therefore, the name of the pin is prefixed by "n" or "~". nStrobe is used as a clock signal when computer is transmitting data to printer. The computer has to set this signal each time the data pins are changed. nSelect is set when the printer is selected (to print). nStrobe and nSelect pins were controlled using 0. and 3. bits of the control register, which is mapped to 0x378 + 2 = 0x37A I/O port. nStrobe is reset by writing 1 to the control port and nSelect is reset by writing 8 to the control port.

LCD Commands and Defining Special Characters
I used some of the LCD commands without mentioning all of them. 3-4 commands are really enough while working on LCDs. I usually use this image working on LCDs: https://goo.gl/images/QHtKee. Many similar results can be found in Google images by searching "lcd commands". The most proper way, is always relying on the datasheet of LCDs however reading an 60 pages document to build some device quickly, is impossible. I uploaded a generic datasheet to my Google drive and will use it to program CGRAM and create my own character set in LCD.

LCD fonts are stored CGRAM and CGROM (character generator RAM). There is a DDRAM, which stores the codes of the characters shown in the LCD cells. When a data is written to DDRAM, CGRAM/CGROM is used as a look-up table to demonstrate the characters. Since CGRAM is writable, it allows the users to create their own character set. There are 6 bits reserved for addressing CGRAM according to the datasheet but I am not sure that all of these bits are used in a standard LCD. CGRAM address starts with the zero.
On the page 24 of datasheet, table of the LCD commands can be found on Table 6. I first need to specify with a command, which character I will write. To address CGRAM, I need to issue 0b01XX XXXX command. Those Xs are the address bits of the character. Therefore the command is 0x40 for the zeroth address. According to page 31, the data can be send with lcdVeri() function. The character to be defined, must be sent as 5-bit-wide bitmap data. This information is on the previous pages in datasheet. One important thing is, LCD has to be disabled while writing CGRAM.

I slightly changed my old code to define characters. Definitions, lcdVeri() and lcdKomut() functions are same. I have pasted main() below:

int main(int argc, char* argv[])    {
    int i;

    if(ioperm(BASE, 3, 1))    {
        fprintf(stderr, "Access denied to %x\n", BASE);
        return 1;
    }

    lcdKomut(0x38);    // 8 bit, 2 lines, 5x7 px
    lcdKomut(0x08);    // disable lcd

    const unsigned char specialchars[] = {
        // something like smiley
        0B01110, 0B10001, 0B11011, 0B10001,
        0B11011, 0B10101, 0B10001, 0B01110,
        // inverse of smiley
        0B10001, 0B01110, 0B00100, 0B01110,
        0B00100, 0B01010, 0B01110, 0B10001,
        // spades
        0B00100, 0B01110, 0B11111, 0B11111,
        0B10101, 0B00100, 0B01110, 0B00000,
        // clubs
        0B00000, 0B01110, 0B10101, 0B11111,
        0B10101, 0B00100, 0B01110, 0B00000,
        // heart
        0B00000, 0B00000, 0B01010, 0B11111,
        0B11111, 0B01110, 0B00100, 0B00000,
        // tile (diamond)
        0B00000, 0B00100, 0B01110, 0B11111,
        0B11111, 0B01110, 0B00100, 0B00000
    };



    lcdKomut(0x40);   // 0. CGRAM address
    for (i = 0; i <= 47 ; i++)
      lcdVeri(specialchars[i]);

    lcdKomut(0x01);    // clear the screen
    //lcdKomut(0x80);    // linefeed
    lcdKomut(0x0F);    // enable screen, cursor blink

    lcdVeri('D'); lcdVeri('e'); lcdVeri('n');
    lcdVeri('e'); lcdVeri('m'); lcdVeri('e');
    lcdKomut(0xC0);    // second line
    lcdVeri('1'); lcdVeri('2'); lcdVeri('3');
    lcdVeri(' '); lcdVeri(' '); lcdVeri(' ');

    // special characters
    lcdVeri(0x00); lcdVeri(0x01); lcdVeri(0x02);
    lcdVeri(0x03); lcdVeri(0x04); lcdVeri(0x05);
 
    outb(0, BASE);
    return 0;
}

With this code above, special characters are defined and printed on the display: