chipKIT® Development Platform

Inspired by Arduino™

Timer interrupt & i2c

Created Sat, 13 Sep 2014 15:10:45 +0000 by tcsaba101


tcsaba101

Sat, 13 Sep 2014 15:10:45 +0000

This post connected to my "SD card worst case write time" post (http://chipkit.net/forum/viewtopic.php?f=24&t=3035).

It has been turned out the solution for SD card handling is the separate sensor reading + buffering and writing the buffer to the SD. And the appropriat size of the buffer.

I plan to read the sensor in a 10msec Timer interrupt, fill the buffer, then flush the buffer content to the SD in the loop() task.

I have read somewhere the i2c will not work inside an interrupt cycle. It happened to be true, if I comment out the i2c reading lines from the code the cycling works. If the i2c reading included in the valid code it stucks.

I use Matt's Timer library: https://github.com/majenkotech/Timer

The code:

void __attribute__((interrupt)) isr() 
{
	timer_task_start = micros();

	
	reg3 = ina.readBusVoltage();
	uBat_ms10_tot_mV += reg3;
	uBat_ms10_mV = (int)(reg3*1000.0);
	reg3 = ina.readShuntCurrent();
	iBat_ms10_tot_mA += reg3;
	iBat_ms10_mA = (int)(reg3*1000.0);

/*
 * 	
Here is the buffer filling
 */
	
	ms10_samples++;
	ms10_cnt += 1;
	if(ms10_cnt % 10 == 0) ms100_flag = true;
	if(ms10_cnt % 6000 == 0) min1_flag = true;

	timer_task_delay = micros()- timer_task_start;
		
	clearIntFlag(_TIMER_4_IRQ);
}

What is the solution to handle i2c in interrupt cycles?


majenko

Sat, 13 Sep 2014 15:33:07 +0000

I2C uses interrupts itself to handle the transfers.

It is possible to use interrupts inside interrupts, but only if you do it right. Basically when you are in an interrupt the only interrupts that are allowed to interrupt it are those of a higher priority.

I2C's interrupt runs at priority 3. If you ensure the timer's interrupt runs at a priority than the I2C's interrupt then it should work.

You can specify the priority for the interrupt in the constructor:

Timer4 timer(2); // Configure timer 4 for priority 2.

tcsaba101

Sat, 13 Sep 2014 19:29:22 +0000

Thanks!

It started to work.


tcsaba101

Sun, 14 Sep 2014 15:30:18 +0000

I share the most important things I have learned:

I have two i2c devices: the INA226 what producing the data to be measured and an DS3231 RTC.

All the i2c devices have to be handled inside the interrupt cyle, or the timer interrupt should be disabled during I2c communication. The timer interrupt could interrupt the i2c cycle and begin a new i2c cycle, that makes strange situations.

Using the INA226 library the minimum timer interrupt cycle duration is between 2-3 msec. I need 10ms sampling so this is fine for me at the moment.

The ina226 capable to sample at 140usec, but reaching speed close to that, the i2c bus have to be handled at wire lib level or lower. I didn't experimented.

The card write latencies: I have tried four cards:

  • A: SanDisk 32G Class10
  • B: SanDisk 4G Class6
  • C: Maxell 32G Class10
  • D: Kingston 32G Class 10

Data packets:

  • D1: 1100 bytes
  • D2: 10010 bytes

The maximum write cycle duration (msec): D1_test D2_test A: 91 295 B: 57 325 C: 95 307
D: 50 212

I don't think the figureas are relevant in absolute value, but hopefully somebody can use them in the relative sense. Kingston looks to be the best, but I made only one test round, i could work out different in a longer test.

I havent developed further the code for speed, because it fulfills my requirements. I give it just for reference.

the interrupt task:

void __attribute__((interrupt)) isr() 
{
	timer_task_start = micros();

	reg3 = ina.readBusVoltage();
	uBat_ms10_tot_mV += reg3;
	uBat_ms10_mV = (int)(reg3*1000.0);
	reg3 = ina.readShuntCurrent();
	iBat_ms10_tot_mA += reg3;
	iBat_ms10_mA = (int)(reg3*1000.0);		 
	
  int next = ringNext(head);

		if (next != tail) 
		{
	//	ms10_buffer[head].timestamp = log_timestamp;
			ms10_buffer[head].uBat_samp_mV = uBat_ms10_mV;
			ms10_buffer[head].iBat_samp_mA = iBat_ms10_mA;
	    head = next;
  	}
  	else 
  	{
    over ++;    // overrun the buffer
  	}
  
	ms10_cnt ++;
	if(ms10_cnt % 10 == 0) ms100_flag = true;
	if(ms10_cnt % 100 == 0)
	{
				s1_flag = true;
				dateTime = clock.getDateTime();
	}

	timer_task_delay = micros()- timer_task_start;
	if(timer_task_start > micros()) timer_task_delay = micros()+ (0xFFFFFFFF- timer_task_start); //in case of micros() counter overflov
	timer_del_total += timer_task_delay;
	no_of_cyc++;
	
	clearIntFlag(_TIMER_4_IRQ);
}

the flush task code:

if(recAvailable() > MS10_BUFSIZE/2)	
				{
						log_task_start = millis();
						data_logged	= 0;
						
						ms10FileName.toCharArray(filename, 13);  
					  logFile = SD.open(filename, FILE_WRITE);
					  if (! logFile)
							  {
					    		reason = DATA_FAIL;  	
							    printError("Data write failed");
							  }
						
					  //number of records
						int n = recAvailable();
						log_task_start = millis();
						data_logged	= 0;
									
						for(int i=0; i<n; i++)
						{
					     dataString =  
					//				String(ms10_buffer[i].timestamp) + ", " + 
									String(ms10_buffer[tail].uBat_samp_mV) + ", " + 
									String(ms10_buffer[tail].iBat_samp_mA);

								data_logged += dataString.length();
							  
							  if (logFile)
							  {
							    logFile.println(dataString);
								}
								else
							  {
					    		reason = DATA_FAIL;  	
							    printError("Ms10 write failed");
							  }					
							  tail = ringNext(tail);						  
						}
						logFile.close();
						
						log_elaps_ms = millis() - log_task_start;
						log_elaps_max = log_elaps_max < log_elaps_ms ? log_elaps_ms : log_elaps_max;
}

I list useful links on this topic what found based on pito's post: https://code.google.com/p/beta-lib/downloads/list http://forum.arduino.cc/index.php?topic=109954.0;wap2

Thanks fot the help of matt, pito and the referenced sources to get to this result.


pito

Tue, 16 Sep 2014 18:37:46 +0000

The basic paradigm of using interrupts is to keep them as short as possible.. That is not the case in the above code, imho.. :)


tcsaba101

Wed, 17 Sep 2014 04:23:24 +0000

The interrupt cycle time is max 3,5msec, average below 2msec. What looks to be short enough when the timer hits in every 10msec. It will decrease a bit when I remove the delay calculation codes.

I could make it shorter if that would be necessary, but this is why I like PIC32 :D let me to be lazy in some areas: lot of code and Ram space, and speed.

Changing the ina226 library to read raw values from the chip would decrease dramatically the lenght. If I have resources I plan to make a faster logger in this setup. Now I want to finish the present project.

Any idea?


pito

Wed, 17 Sep 2014 18:24:44 +0000

A 1usec long interrupt with pic32 would be considered too long (at least by me) :)


tcsaba101

Wed, 17 Sep 2014 18:55:52 +0000

Please explain your solution in this application.

I appreciate your help, because this led me to solve the case in my way. I am newbie in this area, and I am open to see the professional solutions.

Thanks.


majenko

Wed, 17 Sep 2014 21:19:29 +0000

A 1usec long interrupt with pic32 would be considered too long (at least by me) :)

Things aren't that cut and dried any more, Pito. When you had one, or maybe two, interrupts and many things get routed to it, then yes keeping your interrupts very short is essential.

Nowadays, with multiple interrupt priorities (7 in the case of the PIC32) it's not as critical to ensure that every interrupt is very short. As long as you get your interrupt priorities right. High priority interrupts should be kept very short, but low priority ones, as in the case of that timer interrupt, can be much longer, as other interrupts aren't blocked by it and can still execute at the right time. The lower priority interrupts become a sort of second thread to your process.

So I have nothing at all against how that interrupt is being used.


tcsaba101

Thu, 02 Jul 2015 08:59:36 +0000

I got back to this topic, because I had problem on i2c and timer irq conflict.

The SD Card data logger worked well for many hours as inteded and planned using the earlier info in this topic. There were two i2c devices used: INA226, RTC DS3231. I used as it was described earlier: timer on irq priority level 2, and we know i2c is on level 3.

Timer4 timer(2);

then initialized:

timer.setFrequency(PRESCALE);
			timer.attachInterrupt(isr);
			timer.start();

The logger is reading and buffering the INA226 current and voltage sensor data within the timer interrupt task:

void __attribute__((interrupt)) isr() 
{

	reg3 = ina.readBusVoltage();   //read the i2c sensor
	reg4 = ina.readShuntCurrent();

	//calculation and storing in the buffer code here
	
	clearIntFlag(_TIMER_4_IRQ);
}

the execution time of the irq task was max 2.5 ms, and the timer has 10ms interval.

I have had a paralel 20x4 lcd, now I had to change to i2c lcd due to the fact I need more io pins. After installing the i2c LCD the system was freezing on the display operation. If the irq has been disabled, the display was working.

In the earlier version the RTC was read every sec once, there was no other action on the i2c bus outside the irq task. In the new version the LCD refresh time is 100ms, plus the RTC reading as was before. I have to admit, there were sometimes veird characters on the display on the time displaying area, so it has problems also in the earlier version but was not freezing the system.

When I arranged the timer interrupt only to set a flag, and polling the flag in the main loop(), executing the same code when the flag set, then it started to work.

I suppose two i2c cycles are overlaping somehow. Or what is the answer?

In my case the polling solution doesn't make any functional problem, because I am reading averaged value from the INA226, and the PIC32 makes short loop(). So a little sampling jitter probaly makes no or minor difference.

But in applications where the sampling is critical you have to act immediately within the irq task. What could be the solution if you have multiple i2c devices?

Otherwise in this case I don't need the timer interrupt anymore, a micros() polling would be simpler solution.


majenko

Thu, 02 Jul 2015 11:06:41 +0000

Yes, your two I2C operations are overlapping. You start an I2C transaction for the LCD outside of the interrupt routine, then the interrupt fires, and a new i2C transaction is started mid-way through the LCD transaction. That is bad.

You cannot use I2C both in an interrupt and outside the interrupt without having some kind of mutex or semaphore to ensure there is no conflict like this. The simplest solution is, as you have noticed, to not do I2C in the interrupt.

Other options include:

  • Disable the timer interrupt whenever you're doing an LCD operation
  • Use a separate I2C bus for the timer-based I2C
  • Examine the state of the Wire object in the interrupt and don't perform the interrupt transaction if one is already in progress

ElmaWebber

Tue, 17 Nov 2015 16:15:11 +0000

As per my experience I2C uses interrupts itself to handle the transfers. And it is possible to use interrupts inside interrupts, but only if you do it right. Actually when you are in an interrupt the only interrupts that are allowed to interrupt it are those of a higher priority. Also I2C interrupt runs at priority 3. If you ensure the timer's interrupt runs at a priority than the I2C's interrupt then it should work.


majenko

Tue, 17 Nov 2015 18:15:55 +0000

The problem is there is only one I2C transmit buffer per I2C channel. If you start a transmission (which starts filling the buffer) then start another midway through (so the buffer gets corrupted) and then try and send the first transmission (which happens in a block at the endTransmission() call) then you just get gibberish.