XMEGA ADC and TWI with interrupts

My current project includes an XMEGA microcontroller, specifically an xmega32a4u. This part is supposed to contain the fixed ADC peripheral. The first versions of this part (xmega32a4) had some problems. You can read more about it here.

What I need is to have a TWI (compatible with I2C) interface between two XMEGAs. One will always be a slave and one will always be a master. The master will write a number of bytes to the slave asynchronously and will periodically read a number of bytes from the slave.

Write your own library or use ASF?

Atmel provides “drivers” for the XMEGA peripherals in a large and convoluted mess known as the Atmel Software Library. The argument for it is that once you use the provided drivers you can port your code to any family of XMEGAs. Personally I found that the documentation for it is scattered and a bit vague. To use it you also need to include a lot of things you might not need/want.

As an aside, I think Atmel pretty much screwed the pooch with their transition to the XMEGA line, both in terms of the quality of the peripherals and the documentation for them. Compare and contrast the ATmega datasheets with any of the newer XMEGA ones.

I tried to google for a nice, simple library for the slave TWI interface but every single example looked either too convoluted or incomplete. So I just wrote my own library for a XMEGA slave that uses interrupts.

Prerequisites

The following code and explanation is based on a few assumptions:

The user has at least a pasing familiarity with how I2C works. The I2C specifications are standard across manufacturers so any tutorial will do.

The user knows how to wire up the SCL and SDA lines. These require pull-up resistors. For more info, go here.

The user is familiar with C, avr-gcc and avrdude and has a suitable programmer for PDI. Many tutorials can be found on any of these subjects. I’ll add pastebin links to the full code.

The work environment is Linux (or GAHNOO/Linux). If you’re still using Windows you’re shit out of luck.

If you aren’t interested in the explanations, just use these pastebin links and get the full code for both ADC and TWI. It contains some comments and should be good enough for the more advanced users. Some include protection might be needed, but this works for me.

adc.c

Makefile

xmega_a4u.c

xmega_a4u.h

Configuring the slave TWI

The first function that is called is setup_twi_slave(). This is declared as an inline to save on useless functions calls.

inline void setup_twi_slave(void)
{
  TWIC.CTRL				=	TWI_SDAHOLD_50NS_gc				;
  TWIC.SLAVE.ADDR		=	0x25						;
  TWIC.SLAVE.CTRLA		|=	TWI_SLAVE_PIEN_bm | TWI_SLAVE_DIEN_bm | TWI_SLAVE_APIEN_bm | TWI_SLAVE_ENABLE_bm | TWI_SLAVE_INTLVL_HI_gc;
}

The first line enables a small delay that prevents glitches under certain circumstances. It isn’t strictly necessary and can be ommited.

The second line sets the slave read address to 0x25. This is very important to keep in mind. When a master wants to write to the slave it will use 0x24 (in this example). When a master wants to read from a slave it will use 0x25. This is due to the function of the R/!W bit that is appended to the end of the 7 bit address. Check this link for more details.

The final line configures multiple flags:

PIEN causes the APIF flag to be set when a STOP condition is detected

DIEN enables the interrupt that triggers when data is available

APIEN enables the interrupt that triggers when an address matches the slave address

The final two bits enable the peripheral and enable high level interrupts to be generated.

The level of interrupt that is generated can be modified to suit the individual application. For instance a key press may need to be serviced with a higher priority than a TWI communication. For more details read the PMIC section of the datasheet.

As another aside, if you’re not sure which document to read, go to the Atmel website and search for the PDF called XMEGA [family name] Manual. In this case it is the XMEGA AU manual. If you were interested in say a xmega32e5 part, you would look for XMEGA E manual. The XMEGA A4U manual contains only the pinout, secondary pin functions and peripheral names for the A4U parts. Why this couldn’t be included in a single document for each part is beyond me.

The TWI interrupt service routine

The main part of the code is the ISR(). This is the function that is called when an interrupt is generated. In this case it is called ISR(TWIC_TWIS_vect). TWIC is a the peripheral name specific to the A4U parts, TWIS means TWI slave and _vect is appended just like it was for the ATmega parts. The more astute reader will see that this code is not portable to a device that has the TWI peripheral called something else. The even more astute reader will see that a simple search and replace will do the trick nicely.

  
  // HANDLE ADDRESS MATCH  
  if(TWIC.SLAVE.STATUS & TWI_SLAVE_APIF_bm){
    if(TWIC.SLAVE.STATUS & TWI_SLAVE_AP_bm){
      TWIC.SLAVE.CTRLB		=	TWI_SLAVE_CMD_RESPONSE_gc			;
      recv_bf_cnt			=	0x00						;
      send_bf_cnt			=	0x00						;
    }else{
      TWIC.SLAVE.CTRLB		=	TWI_SLAVE_CMD_COMPTRANS_gc			;
      slave_process_master_write()								;
      recv_bf_cnt			=	0x00						;
      send_bf_cnt			=	0x00						;
    }
  }else{
    //HANDLE DATA MATCH
    if(TWIC.SLAVE.STATUS & TWI_SLAVE_DIF_bm){  
      //MASTER READ
      if(TWIC.SLAVE.STATUS & TWI_SLAVE_DIR_bm){
	if((send_bf_cnt > 0) && (TWIC.SLAVE.STATUS & TWI_SLAVE_RXACK_bm)) {
	  send_bf_cnt			=	0x00						;
	  TWIC.SLAVE.CTRLB 		= 	TWI_SLAVE_ACKACT_bm | TWI_SLAVE_CMD_COMPTRANS_gc;
	}else{
	  if(send_bf_cnt < buff_size){
	    uint8_t data		=	twi_send_buffer[send_bf_cnt]			;
	    TWIC.SLAVE.DATA 	= 	data						;
	    send_bf_cnt++										;
	    /* Send data, wait for data interrupt. */
	    if(send_bf_cnt == buff_size){
	      /* End transaction, reset buffer index */
	      send_bf_cnt		=	0x00						;
	      TWIC.SLAVE.CTRLB 	= 	TWI_SLAVE_CMD_COMPTRANS_gc			;
	    }else{
	      TWIC.SLAVE.CTRLB	= 	TWI_SLAVE_CMD_RESPONSE_gc			;
	    }
	  }else{
	    /* End transaction, reset buffer index */
	    TWIC.SLAVE.CTRLB 		= 	TWI_SLAVE_ACKACT_bm | TWI_SLAVE_CMD_COMPTRANS_gc;
	    send_bf_cnt			=	0x00						;
	  }	
	}      
	//MASTER WRITE  
      }else{
	if(recv_bf_cnt < buff_size){
	  twi_recv_buffer[recv_bf_cnt]	=	TWIC.SLAVE.DATA					;
	  recv_bf_cnt++										;
	  if(recv_bf_cnt == buff_size){
	    /* End transaction, reset buffer index */
	    TWIC.SLAVE.CTRLB 		= 	TWI_SLAVE_CMD_COMPTRANS_gc			;
	    send_bf_cnt			=	0x00						;
	    slave_process_master_write()								;
	    
	  }else{
	    TWIC.SLAVE.CTRLB 		= 	TWI_SLAVE_CMD_RESPONSE_gc			; 
	  }
	}else{
	  /* End transaction, reset buffer index */
	  TWIC.SLAVE.CTRLB 		= 	TWI_SLAVE_ACKACT_bm | TWI_SLAVE_CMD_COMPTRANS_gc;
	  recv_bf_cnt			=	0x00						;
	  slave_process_master_write()								;
	  
	}
      }
    }
  }

I’m not going to go through every single line of code because that take up too much space. The gist of it is this:

Check if this is an address match (a master sent our address over the I2C bus)

Is this a STOP condition?

Yes – end the transmission (COMPTRANS)

No – send ACK;

Check if this is a data match (a master sent a byte of data)

Is it a master READ?

Yes – send data from the buffer until master sends NACK or we finish reading the entire buffer

No – receive data and copy it into a buffer until we get a STOP condition or the buffer is full

I’ve tested this code with a xmega32a4u, compiled under Linux with avr-gcc. The TWI part was tested with a bus pirate.

As you can see from the pastebin above, I use the TWI to send a value to the DAC which is then connected on my dev board to PIN0 of the ADC channel 0 (these pins can be found in the A4U manual mentioned above). I also use the bus pirate to read the values that the ADC generates. This is done again with an interrupt.

Configuring the ADC

This was considerably more difficult because the datasheet is a bit of a mess. What I did was I used the ADC with the following settings:

  • single-ended mode CH0
  • internal 1V reference
  • 12 bits resolution
  • aprox 250KHz clock
  • in freerunning mode
  • with an interrupt set to trigger on completion
  • no sweep

This means that the output will be between 0x000 and 0xFFF. In practice there will be an offset and some other errors. The internal reference is not suitable for anything else except these kinds of quick experiments. Special care must be taken when using any ADC above 8 bits. That will be the topic of another article.

The code for configuring the ADC with the above settings is below.

inline void setup_adc(void){

  ADCA.CTRLA = ADC_ENABLE_bm;
  ADCA.CTRLB = ADC_FREERUN_bm/* | ADC_CONMODE_bm*/;
  ADCA.PRESCALER = ADC_PRESCALER2_bm;
  ADCA.CH0.INTCTRL = ADC_CH_INTMODE_COMPLETE_gc | ADC_CH_INTLVL1_bm | ADC_CH_INTLVL0_bm;
  PORTA.DIRCLR = PIN0_bm;

  ADCA.CH0.CTRL = ADC_CH_INPUTMODE_SINGLEENDED_gc;
  ADCA.CH0.MUXCTRL = ADC_CH_MUXPOS_PIN0_gc/* | ADC_CH_MUXNEG_GND_MODE3_gc*/;//PB1 
  ADCA.REFCTRL = ADC_REFSEL_INT1V_gc; //VCC 1V; bandgap is enabled because the DAC uses it; otherwise enable it separately


}

There are several commented lines in the above function. If you need to use the ADC in differential mode (11 bits) without gain, uncomment the ADC_CONMODE_bm and ADC_CH_MUXNEG_GND_MODE3_gc lines.

The first bit sets signed mode, the second bit sets the negative input of the ADC to the pad ground. I had to dig up that last bit in the definitions file because the datasheet doesn’t note it correctly. If you aren’t sure what the bit mask name is, you can look it up inside the include file that comes with the avr c libraries. In my case, on a Debian 8 with everything installed from the default repositories it is under:

/usr/lib/avr/include/avr/iox32a4u.h

If your distro doesn’t place it in that retarded location you can search for it with this handy command:

find /usr -type f | grep iox32a4u.h

Now the ADC is enabled and configured. All that’s left is to start the first conversion (after global interrupts are enabled). In my case I did that in the main function:

 

int main(void)
{
  setup_clk_32M_int();

  PORTB.DIRSET = PIN1_bm;
  PORTB.OUTSET = PIN1_bm;
  
  init_dac();
  enable_dac_out();
 
  uint16_t data0=0x0000;
  uint16_t data1=0x0000;
  
  slave_process_master_read();
  dac_word(data0, data1);
  setup_twi_slave();
  setup_adc();
  setup_inttr_hl();
  sei();

  ADCA.CH0.CTRL |= 0b10000000; // start conversion
  
  while(1){

  }

  return 0; 
}

The various function bodies can be found in the pastebin links at the start of the article.

You can see that after I enabled global interrupts I started the ADC conversion with a different type of bitmask. When doing it this way make sure to apply the mask with the bitwise OR operator |. Otherwise you’ll overwrite anything that’s in the register. If you’re using the _bm or _gc type of masks you don’t need to worry about that.

Conclusions

The code presented here is what I’ve come up with to use the TWI, ADC and DAC peripherals of the XMEGA. It’s worth repeating that this code will not work with other families of micros. Look up the peripheral names in their respective manual and replace the names. In some cases the bit masks may also need to be adjusted.

This code is just a quick and dirty way to get up and running. More checks and optimizations can be made to it. In a future article I’ll add DMA between these peripherals and talk about how to properly drive the ADC.

Leave a Reply

Your email address will not be published.