Atmega328p Assembly I2C Transactions

Image

The development environment.  Breadboard, Computer, zeptoprog, saleae logic 8

The first of three tasks to get working for the thermal camera build is of course, to read the sensor.  The other two tasks are writing a display driver, and then combining the two.  Since I am doing it in assembly (because why not?), it is a little trickier than just some C snippets from the datasheet or the arduino wire library.

Anyway, here is a practical breakdown and code snipped for the i2c master peripheral on the atmega328/atmegaXX8 series, in assembly.  The datasheet is nice, but it looks like it is copy pasted from every other atmel chip with this peripheral, which means there are some errors in the code they provide.  Specifically, the IN and OUT instructions on page 219 dont work- they only work on registers 0x00-0x3F, which is a tidbit hidden away on page 625.  The solution is to use STS and LDS.  Atmel provides a nice macro for this that lets you write LOAD and STORE, but the whole point of this project is to write some assembly, so I didn’t use them, as they hide some of the details and are not included by default.

To be an I2C master you need to do two things: put data on the bus, and get data from the bus.  This comes out to four things you might want to do to the bus, which are put, get, start, and stop the bus.  You also need to configure the master clock speed.  There are four important addresses for a master- TWBR, TWCR, TWSR, and TWDR.  TWAR and TWAMR are for slave devices, so we wont worry about them.  The below code is based on this hardcoded example, but has been cleaned up into more flexible and portable subroutines.

Setup the Bitrate

The registers to pay attention to in setup is the TWBR, which is the Two Wire Bit Rate register, and TWSR, the Two Wire Status Register.  The last two bits of TWSR set the clock prescaler, and the entire TWBR is used to calculate the I2C clock frequency:

SCL Hz= Clock/(16+2*prescaler*TWBR)

You should refer to the datasheet for more information about TWBR and TWSR if you are trying to implement this.  My clock speed is 8MHz since I set the CLKDIV fuse to 0 and I am running off the internal RC oscillator.  This means the highest selectable speed is 500kHz.  The chip I want to read from is only rated to 400kHz, although I tested and it works at 800kHz.  As you can see in the code below, I ended up choosing the 400kHz speed, although I may increase it later.  Here is the assembly snippet for setting the TWBR/TWSR.  Since TWSR is read only for the top 6 bytes, it is pretty safe to write the whole thing to 0 in the beginning.  It would also be ok to comment out the last two lines since the default TWSR is 0bXXXXXX00.

ldi     r16, 2
sts    TWBR, r16

ldi    temp, 0x00
sts    TWSR , temp

There you go, that’s how you set it up.  Now lets look at some the subroutines that start, stop, put and get.  The code is broken up below, but uninterrupted copies of the subroutines are pasted at the bottom of the document in case you want them.

START

I2CSTART:
ldi         temp, (1<<TWINT)|(1<<TWSTA)|(1<<TWEN)
sts             TWCR,temp

This is the beginning of the subroutine.  First, the data register (which we assume holds the R/W address of the slave) is sent to the Two Wire Data Register (TWDR).  Then, it sets TWINT, TWSTA, and TWEN.  Writing TWINT clears the TWI interrupt flag, which is a little misleading- it doesen’t generate in interrupt unless you have the correct bit masked in TWCR (TWIE, TW interrupt enable).  The other purpose is to let you know that the bus is done doing its stuff, and it is time to check on it.  Writing TWSTA tells the peripheral that it is time to send a start, and TWEN enables the module.

WAIT_START:
lds         temp,TWCR
sbrs         temp,TWINT
rjmp         WAIT_START

Now that we have the start condition in progress, we just check the TWINT flag to see if it is set.  Once it is set, it means that the the start condition is sent, or more generally that the bus is waiting on the controller for some kind of next move.

lds          temp,TWSR
andi        temp,0b11111000
cpi          temp,0x08
breq       PC+2
jmp         errloop
ret

Eventually, TWINT is set and we move on in the code.  We and the value in Two Wire Status Register (TWSR) with 0b11111000 to mask the last three bits, which are the clock prescaler bits and a reserve bit.  This stores the bus status in TWSR- we want it to be 0x08, which means “start condition” on the bus.  It could be something else, which would be an error- in the debug code it goes to errloop, which is just a do-nothing loop.  If it is 0x08, it skips over the jump to the error loop and returns.

STOP

I2CSTOP:
ldi         temp,(1<<TWINT)|(1<<TWSTO)|(1<<TWEN)
sts            TWCR,temp

Much like a car, an I2C transmission needs to be able to stop just as well as it can start.  This snippet should look familiar- it is the same as the start instruction, only instead of 1<<TWSTA it has 1<<TWSTO, which tells the module to send a stop condition.

Check1:
lds            temp,TWCR
andi        temp,0b00010000        ; Check to see that no transmission is going on
brne        Check1
ret

Just like before, we want to wait and check to make sure the command is on the bus before returning to the main code.  Here we check TWCR to see if the right condition is on the bus- this bit in TWCR is the stop condition, which we just wrote.  If that checks out, it returns.

PUT

ldi       r17, your_value

This function assumes the value you want to put on the I2C bus is in r17.  That just means you call this line before calling the put function.

I2CPUT:
NEXT1:
ldi         r16, (0<<TWINT) | (1<<TWEN)
sts        TWCR, r16
sts        TWDR, r17
ldi         r16     , (1<<TWINT) | (1<<TWEN)
sts        TWCR, r16

This should start to look familiar.  Here we turn off the TW peripheral (or kind of pause it with 0<<TWINT).  Then we load r17 into TWDR- this is the thing that gets pushed out of the micro to the slave.  After that is loaded up, we start the peripheral again by writing 1 to TWINT.

WAIT_ACK:
lds           r16 ,TWCR
sbrs         r16 ,TWINT
rjmp         WAIT_ACK
ret

Now we just check TWCR until TWINT is set (which is how the peripheral lets you know you are done) and then return to the program.  If we were fancy, we could also check TWSR to make sure we have the right status.

GET

GET works a lot like put, only the result ends up in the register r17 once it is done.

I2CGETACK:
ldi              r16, (1<<TWINT) | (1<<TWEN) | (1<<TWEA)
sts             TWCR, r16
rjmp I2CGET

Again, are setting TWINT and TWEN, but what is that (1<<TWEA)?  I am glad you asked!  TWEA sends an ack if it is set to 1, or an NACK if it was set to 0.  So if you want an ack, call I2CGETACK, otherwise call the next label:

I2CGETNACK:
ldi              r16 ,(1<<TWINT) | (1<<TWEN)
sts             TWCR, r16

This does exactly what it says on the tin- it gets a byte from the i2c bus and then NAKs.

WAIT_FOR_BYTE:
lds         r16, TWCR
sbrs       r16 ,TWINT
rjmp        WAIT_FOR_BYTE

This snippet waits for the TWCR to tell us it is done, by checking TWINT
lds         r17,TWDR
ret

Then the TWDR is loaded into the data register (r17) to be used in the program later, and the subroutine returns

Reading the AMG8852

 

So to read both the high and low bytes of the AMG8852, the steps you want to take are:

call I2CSTART ;send a start
ldi data,0xD0   ;load the write address into data
call I2CPUT     ;put the data (the slave write address) on the line
ldi data,0x80   ;load the address of the register you want to read in to data
call I2CPUT     ;set the address you want to read on the amg
call I2CSTOP  ;send a stop

Now the grid-eye knows that you want to read address 0x80 (the beginning of the pixels).

call I2CSTART        ;send a start condition
ldi data,0xD1          ;write the slave read address to data
call I2CPUT            ;write the slave read address to the bus
call I2CGETACK    ;get a byte and send an ACK
call I2CGETNACK ;get a byte and send a NACK
call I2CSTOP        ;send a stop condition

Now, after I2CGETACK and I2CGETNACK we can store the data somewhere, but that has not been implemented yet.

Code Paste

After this is a paste of the code.  The commented out sections are additional (not implemented) that catch errors on the i2c bus lines.  some the .equ s are from the reference code from here.  Additionally, there is no error catching right now- if it errors out the code goes straight to the error loop.

/*
* AssemblerApplication5.asm
*
*  Created: 4/21/2014 1:00:03 AM
*   Author: alouie
*/
.device atmega328p
.nolist
.include “C:\Program Files (x86)\Atmel\Atmel Toolchain\AVR Assembler\Native\2.1.39.232\avrassembler\include\m328Pdef.inc”
.include “C:\Users\alouie\Desktop\macros.inc”
.list
; ISR table

.org $0000
rjmp Reset
;—————————————————————–
; DEFINES
;—————————————————————–

.def        temp =r16            ;worker register
.def        data =r17

; Equate statements
.equ        start        = $08        ; Start Condition
.equ        Rep_start    = $10        ; Repeated Start Condition Message
.equ        MT_SLA_ACK    = $18        ; Master Transmitter Slave Address Acknowledge
.equ        MT_DATA_ACK = $28        ; Master Transmitter Data Acknowledge
.equ        MT_DATA_NACK= $30        ; Master Transmitter Data Not Acknowledge
.equ        MR_SLA_ACK    = $40        ; Master Receiver Slave Address Acknowledge
.equ        MR_SLA_NACK    = $48        ; Master Receiver Slave Address Acknowledge
.equ        MR_DATA_ACK = $50        ; Master Receiver Data Acknowlede
.equ        MR_DATA_NACK= $58        ; Master Receiver Data Not Acknowledge
.equ        W            = 0            ; Write Bit
.equ        R            = 1            ; Read Bit
.equ        SLA            = $D0        ; Slave Address of AMG8852
.equ        Inches        = $50        ; Return result in Inches
.equ        CommandReg    = $80        ; SRF08 Command Register
;—————————————————————–
; Reset
;—————————————————————–

Reset:
;—–Setting Stackpointer—————————————-
ldi        temp,low(RAMEND)            ; Set stackptr to ram end
out        SPL,temp
ldi     temp, high(RAMEND)
out     SPH, temp

;—————————————————————–
;setup DDR/IO
;—————————————————————–
;set pullups, DDRC output
ldi temp, 0xff
out DDRC,temp
out    PORTC,temp

;—————————————————————–
;setup speed etc. of i2c port
;—————————————————————–
;400Khz=8MHz/(16+2*TWBR*CLKPRS) TWBR and clock presecaler set
;i2c freq.  clock prescaler default is 1, and is set in
;TWSR
ldi    temp,2
sts    TWBR,temp

ldi    temp,0x00
sts    TWSR,temp

rjmp I2CLOOP

I2CLOOP:
call I2CSTART
ldi data,0xD0
call I2CPUT
ldi data,0x80
call I2CPUT
call I2CSTOP

call I2CSTART
ldi data,0xD1
call I2CPUT
call I2CGETACK
call I2CGETNACK
call I2CSTOP
;take a little nop
;good for debugging
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
rjmp I2CLOOP

;—————-SEND I2C START—————————————————————–
;—————-This will send a DATA out as the address—————————————
;—————-sends start condition and address———————————————-
I2CSTART:
ldi         temp, (1<<TWINT)|(1<<TWSTA)|(1<<TWEN)
sts             TWCR,temp

;wait for start condition to be sent.  when TWINT in TWCR is cleared, it is sent
WAIT_START:
lds         temp,TWCR
sbrs         temp,TWINT
rjmp         WAIT_START

;check TWSR for bus status.
;andi masks last three bits, which are 2=? 1:0prescaler value
lds         temp,TWSR
andi        temp,0b11111000
cpi         temp,START
breq        PC+2
jmp            errloop
ret

;————–PUT I2C————————————————————————–
;————–bytes is stored in r17 aka data————————————————–
;use this by putting the address or data to put on the i2c line in data (r17)
;then this function will disable the TW int, write data to TWDR, and wait for an ack
I2CPUT:
NEXT1:
ldi         temp,(0<<TWINT) | (1<<TWEN)
sts             TWCR,temp
;ldi         temp,SLA+W
sts             TWDR,data
ldi         temp,(1<<TWINT) | (1<<TWEN)
sts             TWCR,temp

;another wait for the TWINT flag, which lets us know if ACK/NACK is back (received
;but I couldnt help myself with that rhyme)
WAIT_DONE:
lds         temp,TWCR
sbrs         temp,TWINT
rjmp         WAIT_DONE

;check and see if TWSR is ACK, or not.  if it is, keep going
;lds         temp,TWSR
;andi        temp,0b11111000
;cpi         temp,MT_SLA_ACK
;breq        SEND_REG
;jmp            errloop
ret

;—————-GET I2C ———————————————————————
;—————-received data is stored in r17 aka data————————————–
;
I2CGETACK:
;enable TWCR, then wait for TWINT
ldi            temp,(1<<TWINT) | (1<<TWEN) | (1<<TWEA)
sts             TWCR,temp
rjmp I2CGET

I2CGETNACK:
ldi            temp,(1<<TWINT) | (1<<TWEN)
sts             TWCR,temp

I2CGET:
WAIT_FOR_BYTE:
lds         temp,TWCR
sbrs         temp,TWINT
rjmp         WAIT_FOR_BYTE

lds         data,TWDR

;check and see if TWSR is ACK, or not.  if it is, keep going
;lds         temp,TWSR
;andi        temp,0b11111000
;cpi         temp,MT_SLA_ACK
;breq        SEND_REG
;jmp            errloop

ret

;—————-SEND I2C STop—————————————————————–
I2CSTOP:
ldi         temp,(1<<TWINT)|(1<<TWSTO)|(1<<TWEN)
sts            TWCR,temp

;check TWCR to see if there is still a transmission- if not, stop bit has been sent
Check1:
lds            temp,TWCR
andi        temp,0b00010000        ; Check to see that no transmission is going on
brne        Check1
ret

;error hell
errloop:
rjmp errloop

Posted in: ENG

3 thoughts on “Atmega328p Assembly I2C Transactions

  1. Livio says:

    Thank you very much, your explanation was very useful for making experiments in assembler with Arduino ATmega and clock module.
    Livio, from Rome, Italy

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s