Tuesday, April 21, 2015

Implementing Asynchronous (interrupt-driven) I2C communication

Now that we reviewed synchronous (polling-based) I2C communication in the previous post, we should be ready to tackle asynchronous, interrupt-driven components of the I2C library.

The I2C master interrupt is generated on completion of the following master message events:
  • Start condition
  • Stop condition
  • Data transfer byte transmitted/received
  • Acknowledge transmit
  • Repeated Start
  • Detection of a bus collision event

The latter interrupt is a general error condition and should be treated directly in the ISR (Interrupt Service Routine), while the rest of the interrupts are related to the various states of the message protocol. The actual message protocol is dependent on the target slave device, thus it is practically impossible to construct a single ISR capable of communicating with multiple types of slave devices. My approach to addressing this problem was to delegate processing of the message-related interrupts to the client’s callback function, while leaving in the library ISR the code responsible for dealing with generic error conditions and some required housekeeping of the library.

The I2C callback prototype is defined in the I2C.h header file. When requesting asynchronous I2C operation from the I2C library, client is expected to provide I2CAsyncRqst structure (also defined in the I2C.h header file), which should contain the address of the client-provided I2C callback function and an unsigned integer (an opaque value) that will be provided to the callback function to relate the call to the callback function with the respective asynchronous operation request. I2C library passes this client parameter “as-is” back to the callback with no changes or modification. The client program decides on the value and meaning of this parameter. It could be an index, a pointer to some client-defined structure, or completely ignored – this is up to client’s implementation. In the later posts when I will discuss use of the I2C library to communicate with various slave devices, we will look at some possible uses of this “client parameter”.

Besides the client parameter, the I2C callback function will receive pointers to I2C CONTROL and STATUS registers, as well as pointers to I2C TRANSMIT and RECEIVE registers to enable it to read and write I2C data and control I2C message state. It is important to remember that the I2C callback routine will be called directly from the ISR routine and will run at the elevated ISR priority. Thus it is important to make sure that the callback routine is implemented efficiently and concludes its processing in as short time as possible. Typically the callback routine is implemented as a “state machine” and at each state performs a very simple set of operations. In subsequent posts dealing with MPL3115A2 and MPU-6050 clients I will provide examples of the I2C callback routines.

To request support for an asynchronous I2C operation from the library, or, in other words, to subscribe client’s I2C callback routine to receive I2C interrupts, client issues a call to I2CAsyncStart(…) function defined in I2C.h header file and implemented in I2C_Async.c code file. This functions takes two parameters – the numeric ID of the I2C module (1 or 2) where the respective slave device is connected and a pointer to I2CAsyncRqst structure preloaded with the address of the callback routine and client callback parameter.

I2CAsyncStart(…) function verifies initialization of the I2C library, converts index of the I2C module to the pointer to respective I2C control block (_I2C_CB) and retrieves from it pointers to I2C CONTROL and STATUS registers. Then it enters I2C Critical Section by raising its priority to the level of I2C interrupt established in the call to library initialization function I2CInit(…). Critical section guarantees that I2C interrupts will be suspended for the duration of the critical section thus providing exclusive access to I2C-related registers, flags, and control values.

Several tests are performed within the critical section – first we check for the pointer to callback function in the respective _I2C_CB – non-null value indicates that there is an active I2C operation in progress with the registered callback function. In this case we compare the callback address from the I2CAsyncRqst structure with the active callback address – if they are the same the new request is ignored. Otherwise we scan the queue of pending I2C requests – if the callback address from the I2CAsyncRqst is found in the queue, then the request for this device is already scheduled and new request could be ignored.

If this is a new request, the queue is scanned for the empty slot; if an empty slot is found, the new request is put in the queue, otherwise an error return code is provided to the client. Usually we have very few devices sharing I2C bus – due to the relatively slow speed of the bus (400 kHz) and frequent updates from sensors having too many devices on the bus would result in high level of contention and skipped samples. In the case of my board, I have just two sensors sharing the bus, so it is enough to have a queue of depth one, so scanning the queue is very fast operation. The depth of the queue is controlled by the I2CMaxAsyncQueue value defined in I2C_Profile.h.

If there is no active callback routine in the _I2C_CB, then we may try to initiate asynchronous request right away. However first we need to check the status of the I2C bus – it may be busy with some synchronous operation. If that is the case, the asynchronous request is rejected and client receives an error code. This is really a rather seldom condition – usually after slave devices are initialized (in a synchronous mode) I switch all of them to asynchronous mode; however there might be some special cases when one of the devices need to repeat initialization or perform some other synchronous operation.

If the bus is free (most typical case), the request is copied to _I2C_CB as “active” callback, I2C interrupt is enabled, and START command issued on the bus. At the completion of the START sequence by the hardware module, the interrupt will be raised, which will invoke ISR and, if everything is OK, ISR will invoke the client’s callback routine.

Now it is time to look at the implementation of the ISR routine. PIC architecture defines separate interrupt vectors for each (of the two) I2C hardware modules. If both I2C modules requested in defined in I2C_Profile.h, then we will have two interrupt routines, _MI2C1Interrupt and _MI2C2Interrupt (these names are predefined). Interrupt routines are defined in the I2C_ISR.c file. As soon as respective interrupt is raised and control transferred to respective ISR, interrupt routine first clears the interrupt flag (to avoid endless loop). Then it checks for the active callback routine – if there is no one, the interrupt assumed to be spurious and interrupt processing for this module is disabled (as you may recall, it is enabled by the I2CAsyncStart(…) routine when new request comes) and interrupt processing ends at this.

If there is an active callback routine, further interrupt processing is delegated to common (shared between modules) interrupt routine I2CInterrupt(…) implemented in the same code file. To avoid extra overhead of the function call in the interrupt routine I2CInterrupt(…) is defined as “inline” function.

I2CInterrupt(…) routine first checks whether this interrupt is the result of a STOP condition on the bus. If this is the case, then it is an indication that the current asynchronous operation is completed. The Callback pointer in _I2C_CB is nulled (indicating that there is no active asynchronous operation) and the queue is scanned for pending requests. If one is found, it is being promoted to “active” and START command is issued on the I2C bus. Otherwise interrupts on the bus are disabled and function returns.

If the interrupt was not due to the STOP condition, I2CInterrupt(…) routine checks for potential error conditions on the bus. If one is found, it clears the error status and issues STOP on the bus. This would result in abandoned client request – the client code should be ready for the possibility that a request may not run to completion. When we will look at the code for clients, I will indicate how this condition could be dealt with (if necessary) for various slave devices.

Finally, if this is neither the STOP nor error on the bus, the interrupt is being passed to the client’s callback routine for further processing. Later we will look at implementation of callback routines for various slave devices. For now we just need to remember that when the callback routine concludes communication with the slave device (based upon the device and the callback logic) it should indicate this to the I2C library by calling I2CAsyncStop(…) routine defined in I2C.h header file. This is a very small function (again, defined as “inline” to save time on call), which checks for some bus conditions and issues STOP on the bus. Eventually interrupt resulting from the STOP condition will be processed by the ISR as we already discussed above.

Now that we became familiar with the internals of the I2C library, it is time to put our knowledge to test by using the library to facilitate synchronous and asynchronous communication with various sensors. That we will start investigating in the next post.

No comments:

Post a Comment