1. Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.
    Dismiss Notice
Ian Rogers

Basic 8051 Tutorial 6

SPI and I2c

  1. Ian Rogers
    We start with the SPI bus... The main idea behind serial buses is to save on micro-controller pins... SPI requires three pins or more if there are multiple devices.. The serial transfer on an SPI bus is circular.. The inputs and outputs of each buffer are connected and the shift is synchronized.
    upload_2015-4-17_18-5-20.png

    As data is shifted out of the SDO pin of a micro, the data is shifted into the SDI pin of the device...The data in the device is simultaneously shifted back into the micro. When large data transfers are made after the first (dummy) data is read, the next byte will be called data, this means you can swap chunks of data back and forth relativity quickly... Some SPI buses can run at 10Mbit per second...

    Multiple devices are addressed using a Chip Select (CS) pin.... There are certain devices that are software addressable making the need of the CS pin obsolete, but not many! For the examples I use hardware addressing which usually means supplying a CS pin per device.

    The protocol for talking to SPI devices is fairly simple.. 8 clocks to shift the buffers round whilst holding the CS pin low...

    There are 4 modes of operation and most devices will work with at least two, the modes are determined by the clock signal...

    Normally Low with Rising Clock Mode 0,0 / Mode 0
    Normally Low with Falling Clock Mode 0,1 / Mode 1
    Normally High with Rising Clock Mode 1,0 / Mode 2
    Normally High with Falling Clock Mode 1,1 / Mode 3

    Each device has a datasheet ( somewhere on the net ) that will describe the operation... Micro's with hardware SPI modules have a register where this can be set. However! We are going to implement a software bit banged one so we'll have to take it into consideration.

    The devices I'm going to use are the ADC and the DAC. For the DAC I use the MCP4921 and the MCP3001 for the ADC..

    The MCP4921 is a 12 bit DAC.... So our output has 4096 resolutions, that means for a 0v ~ 5v span we will see changes a little as 1.2mV... To output a voltage we have to load the DAC via the SPI pins... There is 12bits for the DAC itself and 4 bits for control.

    Here is the register inside the DAC as displayed in the relevant datasheet..

    upload_2015-4-17_18-7-26.png

    From this we can see we need 2 x 8 bit data transfers..

    Bit 15 is only used on the dual MCP4922.
    Bit 14 is for the internal buffer, this is used when updating the DAC causes loading issues.
    Bit 13 is the gain control, it set it uses the ref pin as its span if unset it uses the ref pin and doubles( if it can ) the output.... If the DAC runs on 5v this will be the cap.. It cannot be used as a voltage doubler.. Most people use a precision 1.2v or 2.5v reference... This means they can double the output to 2.4v and 5v respectively..
    Bit 12 is the shutdown control.

    When we write to the DAC we need to add 0x3000 to the DAC value so the chip will be enabled and we can use a 5v reference.

    The MCP3001 has a slightly different protocol than the DAC... This being a 10bit ADC there are only 11 bits to shift out.. 1 NULL bit and then the 10 ADC result bits.. There are no control bits..


    If we read 16 clocks the first two clocks receive nothing... The 3rd clock receives the NULL bit and the following 10 clocks receive the data.. The last few clocks can be discarded... As we will be using a software SPI implementation for both chips then the full 16 bits will be being used. This way if an upgrade to hardware SPI is required, little software changes will be needed..

    Ok! We need a circuit.
    upload_2015-4-17_18-8-15.png

    I haven't included( as usual ) the connections to the bootloader.. This is an addon to the basic circuit shown in the first tutorial... Also as we are dabbling with ADC's and DAC's we need to ensure decoupling caps are on all chips... 0.1uf is the usual cap to use... These must be placed as close to the power pins as possible...

    I'll show the ASM code first so we can see it in action... I'm going to read a pot connected to the MCP3001 and watch a voltmeter connected to the MCP4921...

    Code 17
    Code (asm):

    $MOD51

    ResultL    equ   31H     ; Low byte
    ResultH    equ   30H     ; High byte

       org    0
       ljmp   Start     ; jump over vectors

       org    30H
    Start:   call   SPIInit     ; Ready for SPI
       
    Main:
       call    GetVal     ; Read ADC
       call   SendVal     ; Convert to 12 bit
       sjmp    Main     ; And send to DAC

    ; The MCP3001 is a 10 bit ADC but the split over the two
    ; registers is weird
    ; x,x,x,null,D9,D8,D7,D6,D5,D4,D3,D2,D1,D0,D1,D2,D3

    GetVal:
       clr   P1.4     ; CS enable
       mov   A,#0
       call   doSPI     ; Clock in ADC
       mov   ResultH,A   ; Get ADC high
       mov   A,#0
       call   doSPI     ; Clock in ADC
       mov   ResultL,A   ; Get ADC low
       setb   P1.4     ; CS disable
       mov   R0,#2     ; Shift out 2 bits

    ; Next bit removes all rubish and makes a 12 bit result..
       
    shift:   clr   C     ; clear carry before shift
       mov   A,ResultH   ; shift High into Acc
       rrc   A     ; rotate with carry
       mov   ResultH,A   ; store back
       mov   A,ResultL   ; low byte into Acc
       rrc   A     ; rotate with carry
       mov   ResultL,A   ; store back
       djnz   R0,shift   ; All 4 bits
       mov   A,ResultH   ; Get High byte
       anl   A,#0fh     ; Trim to 12 bits
       mov   ResultH,A   ; Store back
       ret

    ; Send 12 bit result to the MCP4921..

    SendVal:
       clr   P1.3     ; CS enable
       mov   A,ResultH   ; get High byte
       add   A,#30h     ; set control bits
       call   doSPI     ; send out
       mov   A,ResultL   ; get low byte
       call   doSPI     ; send out
       setb   P1.3     ; CS dissable
       ret

    ; Ready the pins for SPI transfers

    SPIInit:
       mov   P1,#0ffH   ; Port 1 as input
       clr   P1.6     ; Clock low idle
       ret

    ; Do the SPI transfer

    doSPI:   clr   C     ; Clear carry
       mov   R0,#8     ; 8 clocks
    LpIn:   clr   P1.5
       jnb   Acc.7,BBit   ; bit 7? high / low
       setb   P1.5     ; high
    BBit:   call   Clock     ; low and clock
       jnb   P1.7, None   ; Whats come in???
       setb   C     ; High
       jmp   ABit
    None:   clr   C     ; or Low
    ABit:   rlc   A     ; next bit
       djnz    R0,LpIn     ;
       ret

    ; Clock the SCL line
       
    Clock:
       setb   P1.6     ; Clock
       nop       ; Wait
       clr   P1.6     ; Scl pin
       ret

       end
     
    It works as expected...

    Okay! The C version...

    Code18
    Code (c):

    /* Main.c file generated by New Project wizard
     *
     * Created:  Thu Apr 16 2015
     * Processor: AT89C51RB2
     * Compiler:  SDCC for 8051
     */


    #include <mcs51reg.h>
    #define   SDO P1_5
    #define   SDI P1_7
    #define   SCL P1_6
    #define   CSA P1_4
    #define   CSD P1_3

    void SPIinit(void)
      {
      P1 = 0xBF;      // Clock idle low  
      }
       
    unsigned char doSPI(unsigned char CH)
      {
      char Idx;
       
      for (Idx = 0; Idx<8;Idx++)
      {
      SDO = 0;
      if(CH & 0x80) SDO = 1;
      SCL = 1;
      SCL = 0;
      CH <<= 1;
      CH &= 0xFE;
      if(SDI == 1) CH++;
      }
      return CH;  
      }
       
    int ReadADC(void)
      {
      int tmp = 0;
      CSA = 0;
      tmp = (int)doSPI(0);
      tmp <<= 8;
      tmp += (int)doSPI(0);
      CSA = 1;
      tmp>>= 2;
      return tmp & 0xFFF;
      }

    void WriteDAC(int x)
      {
      x += 0x3000;
      CSD = 0;
      doSPI(x >> 8);
      doSPI(x & 0xFF);
      CSD = 1;
      }
       
    void main(void)
     {
      // Write your code here
      while (1)
      {
      WriteDAC(ReadADC());  
      }  
     }
     
    So you can see SPI isn't rocket science DATA is shifted out by the most significant bit whilst DAT is shifted in on the least significant bit.. This really speeds up the operation.

    The other major contender for inter device communication is I2C..

    Inter IC Communication. Shortened to.. I squared C ( I2C )..

    I2C is pretty much the same as SPI but has only two wire hookup, hence why some call it two wire communication.. The master controls up to 128 devices.. Each device has an internal address... This allows us to leave out the Chip Select pins....

    If an I2C bus has several similar devices they must have external code switches...

    An 24LC256 eeprom has an internal address of 0x50... But! As the address has to include a R/W bit we need to shift the address into a byte so the address becomes 0xA0 for writing and 0xA1 for reading..

    For our example, I'll use a 24LC256 eeprom coupled with DS1307 RTC... We will have to bring back the LCD routines so we can see stuff on the screen ( otherwise nothing will look like its happening ).

    First thing as before.. A schematic.. We need to know how to connect it up.. I normally hook stuff up to port 1, but I have the LCD on there.... I have use Port 0 pins 6 and 7..

    As we only have one 24lc256 the three address lines are grounded...
    upload_2015-4-17_18-16-18.png

    I have included a file or two... This is helpful when making large programs.. Take all your code and bundle it in a file.... Call it MyLibrary.C or something else that fits the bill... Name all your function definitions and constants and place it in a file called MyLibrary.H or similar... Now when you include any functions from the library, then only these will be included in the build..

    But before the version I'll start with ASM as usual..

    Code19
    Code (asm):


    sec   equ   30h
    min   equ   31h
    hour   equ   32h
    day   equ   33h
    date   equ   34h
    mon   equ   35h
    year   equ   36h

       org   0
       sjmp   start
       org   30h

    start:   acall   i2cinit
       acall   LcdInit
       acall   settime
       mov   R3,#1
       mov   R2,#0
       mov   R1,#0AAh
       acall   writeEEprom
       acall   delay
       mov   R1,#0
       acall   readEEprom

    main:
       acall   gettime
       acall   printRTC
       sjmp   main

    t_data:   db   0h,23h,13h,0h,17h,4h,15h

    printRTC:
       acall   LcdGotoline1
       mov   R0,#hour
       mov   A,@R0
       dec   R0
       acall   printhex
       mov   A,#3Ah
       call    lcdData
       mov   A,@R0
       dec   R0
       acall   printhex
       mov   A,#3Ah
       call    lcdData
       mov   A,@R0
       acall   printhex
       acall   LcdGotoline2
       mov   R0,#date
       mov   A,@R0
       inc   R0
       acall   printhex
       mov   A,#2fh
       call    lcdData
       mov   A,@R0
       inc   R0
       acall   printhex
       mov   A,#2fh
       call    lcdData
       mov   A,@R0
       acall   printhex
       mov   A,#20h
       call    lcdData
       ret
       
    printhex:
       push   Acc
       swap   A
       anl   A,#0fh
       add   A,#30h
       call    lcdData
       pop   Acc
       anl   A,#0fh
       add   A,#30h
       call    lcdData
       ret

    gettime:
       mov   R0,#7
       mov   R2,#0
       mov   R1,#sec
    idx2:   acall   readRTC
       mov   A,R3
       mov   @R1,A
       inc   R2
       inc   R1
       djnz   R0,idx2
       ret

    settime:
       mov   R0,#7
       mov   R2,#0
       mov   dptr,#t_data
    idx1:   mov   A,#0h
       movc   A,@a+dptr
       mov   R3,A
       acall   writeRTC
       inc   R2
       inc   dptr
       djnz   R0,idx1
       ret
       
    $include   (I2C.inc)
       end
     

    The include file (I2C,inc) has all the low level code... I will stick it in the appendix and include it on the forum.


    Code20

    Code (c):

    /* Main.c file generated by New Project wizard
     *
     * Created:  Fri Apr 17 2015
     * Processor: AT89C51RB2
     * Compiler:  SDCC for 8051
     */


    #include <mcs51reg.h>
    #include <stdio.h>
    #include "MyLibrary.H"

    unsigned char output[17];
    unsigned char time[] = {0x0,0x3,0x17,0x0,0x17,0x4,0x15};

    void WriteClock(char *array)  // Not used
      {
      char x;
      for(x=0;x<7;x++)
      {
      WriteRTC(x,array[x]);
      }  
      }  

    void ReadClock(char *array)
      {
      char x;
      for(x=0;x<7;x++)
      array[x] = ReadRTC(x);  // No need to BCD-BIN
      }  

    void main(void)
     {
      I2Cinit();
      lcd_init();
      WriteClock(&time[0]);
      while (1)
      {
      lcd_command(0x80);
      ReadClock(&time[0]);
      sprintf(output,"%02x:%02x:%02x",time[2],time[1],time[0]);
      lcd_print(output);
      lcd_command(0xC0);
      sprintf(output,"%02x/%02x/%02x",time[4],time[5],time[6]);
      lcd_print(output);
      }  
     }
     
    And there in lies software SPI and I2C routines
    I have included the two files... I2C.inc for the ASM code ( rename from I2C.h)
    and the MyLibrary.H for the C code ( My compiler wont allow multiple C files so I have done it as a header!!! Same difference)..

    I'm on with the Graphical display now.... I'll try and get it posted soon..

    Attached Files:

    • I2C.h
      File size:
      4.6 KB
      Views:
      286
    absf likes this.

Recent Reviews

  1. RonaldB
    RonaldB
    5/5,
    This tutorial is a great reference. I'll surely get back on these articles once I get my board. Thanks for sharing. Hope to see more of your posts...thanks