5
\$\begingroup\$

Most articles I am able to find which are essentially "I2C for dummies on XXX microcontroller" suggests blocking CPU while waiting for confirmation events. For example, (comments and description are in Russian, but that actually doesn't matter). Here is the example of "blocking" I am talking about:

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));

These while loops are there after each operation, essentially. Finally, my question is - how to implement I2C without "hanging" MCU with these while loops? On a very high level I do understand I have to use interrupts instead of whiles, but I can't come up with any code example. Can you please help me with that?

\$\endgroup\$
  • 1
    \$\begingroup\$ Notice that you can also use DMA with I²C to gain some performance benefit and reduce the number of interrupts to handle. \$\endgroup\$ – JimmyB Oct 24 '16 at 15:49
  • \$\begingroup\$ @AlexeyMalev Here's an example of a nonblocking I2C implementation for AVR (though similar principles would apply for STM32): github.com/scttnlsn/avr-twi This particular implementation uses interrupts to manage a state machine and call a callback function when the transmission is finished. \$\endgroup\$ – scttnlsn Apr 10 '17 at 0:21
5
\$\begingroup\$

Sure.

  1. Set up your drivers in terms of an upper and lower pair, separated by a shared buffer. The lower function is an interrupt driven bit of code that responds to interrupting events, takes care of the immediate work needed to serve the hardware, and adds data into the shared buffer (if it is a receiver) or extracts the next bit of data from the shared buffer to continue servicing the hardware (if it is a transmitter.) The upper function is called by your regular code and either accepts data to add to an outgoing buffer or else checks for and extracts data from the incoming buffer. By pairing things up like this, and providing adequate buffering for your needs, this arrangement can run all day long without trouble and it decouples your main code from your hardware servicing.
  2. Use a state machine in your interrupt driver. Each interrupt reads the current state, processes one step, and then either transitions to a different state or stays in the same state, and then exits. This can be as complex or as simple as you need it to be. Often, this is driven off of a timer event. But it doesn't have to be so.
  3. Create a co-operative operating system. This can be just as easy as setting up a few small stacks allocated using malloc() and allowing the functions to cooperatively call a switch() function when they are done with some immediate task for now. You set up a separate thread for your I2C function, but when it decides that there is nothing to do right now, it simply calls switch() to cause a stack change to another thread. This can be done round-robbin until it comes back and returns from the switch() call you made. Then you go back to to your while-condition, which checks again. If still nothing to do, the while calls switch() again. Etc. In this way, your code doesn't get much more complex to maintain and it's simple to insert switch() calls anywhere you feel the need. There is no need to worry about pre-emption of library functions, either, since you are only transitioning between stacks at a function call boundary so it is impossible for a library function to be interrupted. This makes implementation very easy.
  4. Pre-emptive threads. This is much like #3 except there's no need for calling the switch() function. Pre-emption takes place based upon a timer, or a thread can also freely choose to release its time, as well. The difficulty here is dealing with pre-emption of library routines and/or specialized hardware where there may be specific instruction sequences generated by the compiler which cannot be interrupted (back-to-back I/O which must retrieve a high byte followed by a low byte from the same memory mapped address, for example.)

I think the first two options are probably your better bets, though. However, a lot depends on just how much you are depending on library code written by others. It's quite possible that their library code isn't designed to be split into upper and lower level components. And it's also quite possible you can't call them based upon timer events or other state machine based events.

I tend to assume that if I don't like the way the library does the work (busy wait loops only), then I'm stuck writing my own code to do the work. Which means I'm free to use any of the above methods.

But you need to take a close look at the library code and see if it already has features which allow you to avoid the busy wait behavior. It's possible the library has support for something different. So that's the first place to check, just in case. If not, then toy with the idea of using the existing functions as part of either an upper/lower driver split or else as a state machine driven process. It's possible you can work that out, as well.

I don't have any other suggestions that come quickly to mind, right now.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Please correct me if I got the idea of #1 wrong - let's assume I have a code that reads some data from I2C bus and doing some heavy math, for example calculates factorial of some number. As I have no threads here, my main code that calculates factorial should "pause" sometimes (instead of yielding to interrupt handling thread in some high level language) and check if there is any data in the buffer you mentioned, added there by interrupt handler? \$\endgroup\$ – Alexey Malev Oct 24 '16 at 10:09
  • \$\begingroup\$ @AlexeyMalev That factorial code should probably be in your main function, which calls the upper level code. The lower level code doesn't do stuff that needs a break in the middle of it. If more stuff happens while your main code is calling the upper code or processing after calling it, the interrupt will still be serviced and stuff values into the buffer for you. That doesn't stop even if you are doing calcs. There's no need to check for more buffered data, if you haven't completed the other work. If you aren't able to keep up, you've got a problem anyway. \$\endgroup\$ – jonk Oct 24 '16 at 10:13
  • \$\begingroup\$ @AlexeyMalev The longest computation work should be in your main code. If you have shorter bits that need doing in between times, then you can set those up onto timer events that interrupt your long computation code to get their short tasks done. These timed short tasks can call the upper level half of any driver to get buffered data, too. It's just a matter of laying out a timing diagram. Leave enough time between steps for the longest calcs to get done plus all the interrupt times, too. That's your main process. You can use my #3 option, though, and salt your main code with switch() calls. \$\endgroup\$ – jonk Oct 24 '16 at 10:20
  • 1
    \$\begingroup\$ Sorry, but I have to downvote. This lengthy answer does not seem to provide any useful information as to how to implement an interrupt-driven I²C statemachine on an STM32. \$\endgroup\$ – JimmyB Oct 24 '16 at 15:37
  • \$\begingroup\$ The question was - how to implement nonblocking i2c interaction, I mentioned interrupts as an option. Interrupt-based in only one of possible approaches. \$\endgroup\$ – Alexey Malev Oct 24 '16 at 20:51
3
\$\begingroup\$

There are examples in the STM32Cube libraries. Get the one appropriate for your controller family (e.g. STM32CubeF4 or STM32CubeL1), and look for Examples/I2C/I2C_TwoBoards_ComDMA in the Projects subdir.

| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

Reason

Well, the reason is simple: blocking is just easy, and at first glance it seems to work. Woe to you if you want to do something else, meanwhile.

So, without going into many specifics, as I don't know the STM32, you can generally get out of this problem in two ways, depending on your needs.

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));

Convert to non-blocking

Either you implement a timeout for all your while loops. This means:

I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
/* wait for confirmation */
static unsigned long start = now();
while (!I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT) && now()-start < TIMEOUT);  
if (now()-start >= TIMEOUT) { return ERROR_TIMEOUT; }

(This is pseudocode of course, you get the idea. Feel free to optimize or adjust to your coding preferences as necessary.)

You have to check the return codes when traveling up the stack and pick the correct spot where you do your timeout handling. Note that it helps to also set a global variable i2c_timeout_occured=1 or whatever so you can quickly abort further I2C calls without having to pass too many arguments around.

This change is rather painless, hopefully.

Inside out

If, instead, you really need to do other processing while you wait for that event, then you need to get rid of the inner while loop completely. You do that like this:

void main_loop() {
   do_i2c_stuff();    // must never block
   do_other_stuff();
   ...
}

// Must never block. Assuming all I2C_... functions do not block either.
void do_i2c_stuff() {
  static int state=...;

  if (state==0) {
    I2C_GenerateSTART(HMC5883L_I2C, ENABLE);
    state=1;
  } else if (state==1) {
    if (I2C_CheckEvent(HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT)) 
      state=2;
  } else ...
}

It's not necessarily badly complicated, depending on your other logic. You can do a lot with proper indenting/commenting/formatting so you don't lose track of what you are programming.

The way this works is by creating a state machine. If you look at your original code, it looks like this:

non-blocking code
while (!nonblocking_function_call1());
non-blocking code
while (!nonblocking_function_call2());

To transform that into a state machine, you have one state for each:

state 0: non-blocking code
state 1: nonblocking_function_call1()
state 2: non-blocking code
state 3: nonblocking_function_call2()

Then, as shown in the example above, you call this code in an endless loop (your main loop), and only execute the code matching your current state (tracked in a static state variable). The non-blocking code is trivial, it's unchanged from before. The blocking code is replaced by a variation that does not block, but only updates state when it is finished.

Note that the individual while loops are gone; you have replaced them by the fact that you have your top level main loop anyways, which calls your state machine repeatedly.

This solution can be painful when you have lots of legacy code since you cannot simply adapt the innermost blocking function, like in the first solution. It shines when you start out writing fresh code and go this way from the beginning. Combine it with lots of other things a µC could do (e.g., wait for button presses etc.); if you get used to doing it this way all the time, you get arbitrary multitasking abilities for free.

Interrupts

Frankly, for something like this (i.e., just get rid of infinite blocking) I would try hard to stay away from interrupts unless you have extreme timing needs. Interrupts make it complicated, fast, you may not have enough of them anyway, and it will boil down to quite similar code anyways, as you don't want to do much more inside the interrupt except set some flags.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ nice (suboptimal) solution here -- I thought some kind of interrupt would be always necessary. \$\endgroup\$ – Florian Castellane Oct 24 '16 at 13:49
  • \$\begingroup\$ The whole idea was to get rid of while loops. I have a question about your last code sample - not sure what exactly do you propose. The problem is - I2C_CheckEvent returns true after some time and doesn't block, which means in general it is not enough to call it once, which (as I got it) you suggest. Can you please explain a little bit on that? \$\endgroup\$ – Alexey Malev Oct 24 '16 at 14:02
  • \$\begingroup\$ @AlexeyMalev, my solutions do get rid of infinite while loops. The first one still has them but add a timeout. The second one gets rid of them completely (assuming you have a top level loop that gets run in a perpetual loop, anyways). I have added some explanation, as requested. i2C_CheckEvent is called often, but your method returns immediately, no matter the result, so giving the other parts of your program time to run as well. \$\endgroup\$ – AnoE Oct 24 '16 at 14:23
  • \$\begingroup\$ This is not "non-blocking". Nonblocking means that your CPU goes to sleep when no work is to be done. This is typically done using a scheduler allowing the task to sleep on a semaphore instead of blocking the cpu in a while loop. When i2c even occurs, the interrupt routine gives the semaphore and the task that was waiting on the semaphore wakes up and runs again. THIS is non-blocking. The solutions outlined in this post are not. \$\endgroup\$ – Martin Apr 6 '19 at 3:42
  • \$\begingroup\$ @Martin, there are different aspects of blocking. My answer reflects my understanding of the question, which asks how to avoid blocking the flow of the program when entering an i2c call. OP describes a blocking variant, and my outlined solution is the corresponding non blocking one. My solution solves the immediate problem and is straightforward to generalize to interrupt or semaphore driven if that is needed. \$\endgroup\$ – AnoE Apr 6 '19 at 11:07
3
\$\begingroup\$

Here is a list of \$ \small I^2C \$ interrupt request available on an STM32. As I do not know the exact chip you are using, it is recommended to check yours reference manual if there is any difference, but I doubt that there will be.

enter image description here

To stay at your example code, if a non-blocking version is need you have to enable the Start bit sent (Master) event with the ITEVFEN control bit and check the SB event flag inside the ISR.

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

Considering @BenceKaulics' excerpt from a datasheet, the pseudo-code for an interrupt service routine (ISR) could look like this:

i2c_event_isr() {
  switch( i2c_event ) {

    case master_start_bit_sent: 

      send_address(...);
      break;

    case master_address_sent:
    case data_byte_finished:

      if ( has_more_data() ) {
        send_next_data_byte(); 
      } else {
        send_stop_condition();
      }

      break;
    ...
  }
}
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.