(u_u)

stm32 nucleo f446reでI2Cと割り込みを同時に使う方法

Categories: [blog]
Tags: [platformIO], [Mbed], [Nucleo], [mbed os], [rtos], [eventqueue], [I2C], [STM32], [f446re], [InterruptIn], [Ticker]

F446RE、I2Cと割り込みを同時に使うことができません。I2Cの通信をしている最中に割り込みが入ると(InterruptIn, Tickerなど)I2Cの通信はボードをリセットするまで全くできなくなります。これは既知の問題のようで(STM32 I2C fails inside Ticker callback · Issue #3966 · ARMmbed/mbed-os)、解決するには

call the i2c_read/i2c_write from the main thread or another dedicated thread
use the async API as suggested.

とするのが良いようです。
今回私はThreadで分けることを選択しました。

使ったもの
コード

mbedのwebコンパイラでコンパイルし実機で動作を確認しました。platformIOでもコンパイルはできましたが、実機で動くかどうかの確認はしていません。

#include <mbed.h>
#include "mbed_events.h"

#define AQM1602XA_addr (0x7C)

const unsigned char contrast = 32; // -63
DigitalOut led(LED2);
InterruptIn sw(USER_BUTTON);
I2C i2c(D14, D15);
EventQueue queue;
EventQueue timer;
Thread thread;
Thread startTimerThread;
Thread startButtonThread;

// しばらくLCD用のクラス記述が続きます。

class AQM1602
{
public:
  void cmd(char x)
  {
    char data[2];
    data[0] = 0x00; // CO = 0,RS = 0
    data[1] = x;
    i2c.write(AQM1602XA_addr, data, 2);
  }

  void contdata(char x)
  {
    char data[2];
    data[0] = 0xC0; //0b11000000 CO = 1, RS = 1
    data[1] = x;
    i2c.write(AQM1602XA_addr, data, 2);
  }

  void lastdata(char x)
  {
    char data[2];
    data[0] = 0x40; //0b11000000 CO = 0, RS = 1
    data[1] = x;
    i2c.write(AQM1602XA_addr, data, 2);
  }

  void printStr(const char *s)
  {
    while (*s)
    {
      if (*(s + 1))
      {
        contdata(*s);
      }
      else
      {
        lastdata(*s);
      }
      s++;
    }
  }

  void setCursor(unsigned char x, unsigned char y)
  {
    cmd(0x80 | (y * 0x40 + x));
  }

  void CLS(void)
  {
    cmd(0x01); // Clear Display
    wait(0.1);
  }

  void init()
  {
    wait(0.04f);
    // LCD initialize
    cmd(0x38);                           // function set
    cmd(0x39);                           // function set
    cmd(0x04);                           // EntryModeSet
    cmd(0x14);                           // interval osc
    cmd(0x70 | (contrast & 0xF));        // contrast Low
    cmd(0x5C | ((contrast >> 4) & 0x3)); // contast High/icon/power
    cmd(0x6C);                           // follower control
    wait(0.2);
    cmd(0x38); // function set
    cmd(0x0C); // Display On
    cmd(0x01); // Clear Display
    wait(0.2); // need additional wait to Clear Display
  }
};

// 本題はここから

AQM1602 lcd;

void printFirstRow()
{
  static int num = 0;
  char string[20];
  lcd.setCursor(0, 0);
  sprintf(string, "hello! No.%d           ", num);
  lcd.printStr(string);
  num++;
}

void printSecondRow()
{
  static int num = 0;
  char string[20];
  lcd.setCursor(0, 1);
  sprintf(string, "%d seconds          ", num);
  lcd.printStr(string);
  num++;
}

void pushed() //swが押されると呼ばれる
{
  queue.call(&printFirstRow);
}

void called() //1000ms経つと呼ばれる
{
  timer.call(&printSecondRow);
}

void led_toggle()
{
    while(1){ //無限ループ(threadを分ける利点)
        led = !led; //LEDを反転させる(点滅させる)
        wait(0.3);
    }
}

void threadStartButton(){ //ボタン割り込み用EventQueueをDispatchする
    queue.dispatch();
}

void threadStartTimer(){ //タイマー割り込み用EventQueueをDispatchする。
    timer.dispatch();    
}

int main()
{
  lcd.init();
  thread.start(&led_toggle); //メイン処理(今回はLEDの点滅)threadのスタート
  sw.mode(PullUp);
  sw.fall(&pushed);
  timer.call_every(1000, &called); //1000msごとに呼び出される
  startButtonThread.start(&threadStartButton); //ボタン割り込みEventQueue用threadをスタートさせる。
  startTimerThread.start(&threadStartTimer);  //タイマー割り込みEventQueue用threadをスタートさせる。
}
何をしているか

今回の要点は、

です。
EventQueueについてはmbed OS 5でのイベント処理(mbed events libraryの使い方) - 工作とかプログラミングとかのを参考にさせていただきました。
割り込み処理をosを介して行うことで、I2C通信に影響を出さずに済むようです。また、EventQueueを一つにまとめてもこれは動作するのですが、例えばタイマー(call_every)とボタン割り込み(call)が一つのEventQueueに含まれているとき、タイマーだけを止めたいといったときにボタン割り込み側まで無効化されてしまうので、このように2つに分けてみました。
一つのEventQueueのdispatchをしてしまうと、その後に処理が続かないため、それぞれのdispatchはthreadを分けて実行しています。