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
Thank you very much, your explanation was very useful for making experiments in assembler with Arduino ATmega and clock module.
Livio, from Rome, Italy
I’m glad you liked it! Good luck with assembly
This was very, very helpful (most such walk-throughs are for C). Thank you!