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 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