Interrupt Driven I2C Assembly on the Atmega328p

Interrupt-driven post here!

The Atmega328 has an I2C interrupt.  This allows I2C processes to interrupt the flow of the code whenever the TWINT bit is toggled by the hardware TWI module.  This means the processor has more clock cycles to deal with the LED display, and I don’t have to read the temperature registers bit by bit while interwoven with LED code.  I did not use interrupts originally because I wanted to do a quick test to make sure everything was hooked up write, and just to see the project work a little to keep me going.  It quickly became apparent that interrupts would be necessary.

The datasheet does a good job explaining exactly what is going on, but code can be really handy for people who are trying to implement this on their own, so I will do an explanation of the important parts.

.org $0000
jmp RESET ; Reset Handler
jmp RESET;EXT_INT0 ; IRQ0 Handler
jmp RESET;EXT_INT1 ; IRQ1 Handler
jmp RESET;PCINT0 ; PCINT0 Handler
jmp RESET;PCINT1 ; PCINT1 Handler
jmp RESET;PCINT2 ; PCINT2 Handler
jmp RESET;WDT ; Watchdog Timer Handler
jmp RESET;TIM2_COMPA ; Timer2 Compare A Handler
jmp RESET;TIM2_COMPB ; Timer2 Compare B Handler
jmp RESET;TIM2_OVF ; Timer2 Overflow Handler
jmp RESET;TIM1_CAPT ; Timer1 Capture Handler
jmp RESET;TIM1_COMPA ; Timer1 Compare A Handler
jmp RESET;TIM1_COMPB ; Timer1 Compare B Handler
jmp RESET;TIM1_OVF ; Timer1 Overflow Handler
jmp RESET;TIM0_COMPA ; Timer0 Compare A Handler
jmp RESET;TIM0_COMPB ; Timer0 Compare B Handler
jmp RESET;TIM0_OVF ; Timer0 Overflow Handler
jmp RESET;SPI_STC ; SPI Transfer Complete Handler
jmp RESET;USART_RXC ; USART, RX Complete Handler
jmp RESET;USART_TXC ; USART, TX Complete Handler
jmp RESET;ADC ; ADC Conversion Complete Handler
jmp RESET;EE_RDY ; EEPROM Ready Handler
jmp RESET;ANA_COMP ; Analog Comparator Handler
jmp TWI_ISR;TWI ; 2-wire Serial Interface Handler
jmp RESET;SPM_RDY ; Store Program Memory Ready Handler

This is the Interrupt Service Routine (ISR) table for the atmega 328p.  Not all megas, tinys and xmegas have the same ISR setup, so check your datasheets if you are not on this exact micro.  Basically interrupts, when enabled (more on this later) cause the program counter to jump to a particular address at the beginning of the program (caveat: bootloaders.  see datasheet, but the ISRs may need be be moved in memory if you have a bootloader).  That address can have any instruction in it.  As you can see, most of mine just jump to reset, except for:

jmp TWI_ISR;TWI ; 2-wire Serial Interface Handler

This jumps to the label TWI_ISR, which is what I creatively called my Two Wire Interface Interrupt Service Routine.  This is a peice of code that deals with whatever has to happen when the TWI interrupt is triggered.

But how does it get triggered?  Well, two main things have to happen to trigger the TWI interrupt.  First, Interrupts have to be enabled.  This is done by setting the global interrupt enable (i) bit in the SREG.  This is SO SPECIAL, that it has its own instruction:


Now, it would be crazy town if all of the interrupts were suddenly enabled without being configured.  Since there are tons of interrupts, and you only want some of them enabled, each one has its own special enable bit somewhere in the registers for the module.  For the TWI module, it is the TWIE bit, which you could set like this:

ldi r16, (1<<TWIE)
sts TWCR, r16

Once those are enabled, the interrupt can be triggered.  Once triggered, other interrupts are turned off, and the address that you jumped from is stored in the stack.  You can turn on interrupts within the interrupt with sei- but be careful if you do.  You can put more and more addresses in the stack (nested interrupts), but first you need to initialize the stack with:

ldi temp, high(RAMEND)
out sph,temp
ldi temp, low(RAMEND)
out spl,temp

SPH and SPL stand for stack pointer high and stack pointer low.  This sets the stack pointer register at the end of the SRAM, which is find for me.  Now, at some point the micro will want to return from the interrupt subroutine.  This can be done with


but ret does not set the i bit in the SREG.  This means that interrupts will not be enabled again until the main code calls sei.  This could be a good thing, or not.  In my case, I mostly want to call


which returns and re-enables interrupts.  You could call


but that would leave a chance of being interrupted by another interrupt in between when sei is processed and when the ret is processed.

Now we will take a look at the interrupt handler/service routine, but first a note about this particular application.

I want to quickly read the temperature data for each pixel from my 8×8 thermal camera.  These registers are stored one after another in memory space- a table can be found here.  Each temperature is stored as a 12 bit value across two sequential registers, with the next two registers being the next pixel.  so reading 0x80, 0x81, 0x82, 0x83 would give me pixel 1 high, pixel 1 low, pixel 2 high, pixel 2 low.  Since they are sequential, it seems like it would make sense to have some way to read them without having to ask for each one by name each time.  Hmm.  There is!

The device actually auto-increments the register you are going to read each time you read from it.  In other words, I can just request 128 bytes and I will get all of the temperature data at once!  This is great, and you will see how this plays out in the handler for the interrupt.

;if you are about to read the last pixel, dont.
;instead send a stop condition
cpi   pixel,128
breq  send_stop

These first lines just check and see if the pixel counter has reached the end.  If it has, it sends a stop condition.  Otherwise, the master would just keep reading the memory of the slave.

lds   temp,TWSR
andi  temp,0b11111000

This masks the top 5 bits of the Two Wire Status Register (TWSR).  This will tell us what is happening on the bus, since the main code is not keeping track of it anymore.  The micro will want to send out different signals depending on what this is.

cpi   temp,start
breq  send_addrR

This compares the masked TWSR value and the value of start, which happens to be 0x08.  If the TWSR is set to 0x08, it means that the micro has just sent a start condition.  For this application, this means we should send the Slave addres+read address, to let the slave device know that it is going to be read from.

cpi   temp,MR_SLA_ACK
breq  read_more

cpi   temp,MR_DATA_ACK
breq  read_more


This compares the masked TWSR values to see if they are MR_SLA_ACK, or MR_DATA_ACK.  The former means the slave has just acknowledged it is going to be read from, and the latter means that the slave has sent a data byte and the master has acknowledged receipt.  The last reti just tells the routine to return to the main code if none of these conditions are met.  Some kind of error handling should be put here for robustness, in case of MR_DATA_NACK or other nasty codes.

ldi            temp,(1<<TWINT) | (1<<TWEN) | (1<<TWEA) |(1<<TWIE)
sts             TWCR,temp
inc pixel

This snippet reads another byte from the bus.  In the future there will be a few more lines for storing data, but for now it just keeps asking for bytes!  The key parts of this are that TWCR has Two Wire Interrupt Enable set to 1, which enables TW interrupts, and that it returns with reti, so no matter where the code returns to it enables interrupts for later.  We also increment pixel counter, but that is implementation specific. You will notice that this is a lot sleeker than the previous code, because right aver the TWCR is set, it returns to the main program and does not wait for TWSR or TWINT statuses like in the previous poll driven code.  This means the processor can do whatever it wants in the main code, instead of waiting for the bus.  By the time it returns to the main code, about 20 clock cycles (2.5 us) will have passed.  Receiving this data will take 20 us, so this frees up about 18us or 144 clock cycles!

ldi         data,0xd1
sts         TWDR,data
ldi         temp,(1<<TWINT) | (1<<TWEN) | (1<<TWIE)
sts         TWCR,temp

This is the new code for sending the slave+read address.  0xd1 (0xd0+R) is loaded into temp, and then stored in TWDR, then the module is re-enabled (TWINT=1).

ldi         temp,(1<<TWINT)|(1<<TWSTO)|(1<<TWEN)
sts            TWCR,temp
ldi pixel,0x00

Sends a stop condition.  Again, no waiting!

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

Sends a start condition.

The main code to read all the registers is then:

rjmp main

rjmp main

this will read all the registers exactly once.  Calling I2C_START_READ allows the TW interrupt to trigger for the first time, once the start condition is set.

As you can see, this code will free up quite a few clock cycles for dealing with the LEDs and storing and reading data, but it will require some code restructuring form last time.

Posted in: ENG

Leave a Reply

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

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

Facebook photo

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

Connecting to %s