by gloomyandy » Sun Dec 11, 2011 11:17 pm
Hi,
the problem was with the way that the i2c_port_busy var is modified. It is typically set in the i2cStart method and cleared by the interrupt routine. The problem was that the set code was none atomic and so could end up overwriting the changes made by the interrupt code (which is atomic as it runs at interrupt time). Basically the line...
i2c_port_busy |= 1 << port;
which sets a bit in the i2c_port_busy var is not atomic with gcc on the AT91. Essentially it turns into code that is more like this...
int bitval = 1 << port;
int newval = i2c_port_busy;
newval = newval | bitval;
i2c_port_busy = newval;
The problem occurs when an interrupt happens between the initial assignment to newval and the final assignment to i2c_port_busy. If the interrupt routine makes any changes to i2c_port_busy then the change will be lost. The easy/obvious fix would be to make the above operation atomic by disabling the interrupt while the change is made (and indeed this works fine), but the disable/enable operation is actually pretty tricky with the AT91 timers...
Trying to just mask the timer interrupt did not work very well and it was easy to end up with the timer stopping. To get the AT91 timer module to generate a series of tick events you use a base clock counter and compare it to a set value, when the counter matches the compare value the event is triggered and the counter reset. The problem is that if you disable/enable the interrupt at the "wrong time" then you end up never getting any more events... I'm sure it could be made to work that way but I couldn't figure out exactly what was going on.
My second attempt simply disabled the interrupt using the advanced interrupt controller (aic) and this did indeed work. The problem is that this is considerably more complex than simply setting/clearing a bit (which is what is required to enable/disable the timer interrupt mask) and involves several method calls and disabling all interrupts for a short time. I measured the impact on the overall cpu usage and with all four ports running i2c devices this extra code added a fair bit (about 5%) to the cpu usage. I'd really rather not waste this time if possible and I don't like having to disable interrupts system wide so frequently.
So the 3rd solution which is the one in the test build was to take advantage of the way the i2c code works to retry the change made by the interrupt routine. Basically when in the RELEASE state the code checks to see if the port has been set to no longer be busy, if it has not then it clears the busy bit (but no change is made to the state), if was set to not be busy then the state is set to complete. So in effect the state will remain as RELEASE until the bit has been successfully cleared. This change handles the case of the clear being overwritten, but has a much lower impact on the cpu usage (I was unable to see any increase). It is a bit of a hack, but will be documented in the code and I think it is worth it...
Andy