ページ

2013年11月13日水曜日

STM32で割り込みを使ったシリアル通信をする

newlibを使ったシリアル通信のサンプルはいろんな所にあるのですが、どれも同期的なものが多いです。
しかし、やはり時間のかかる通信は割り込みでやりたいので、ここの方のコードを見ながらC++を用いて作ってみました。

※C言語版はこちら

--

まず、割り込みを使ったシリアル通信部分を実装します。

Serial.hpp
#pragma once

#include "stm32f30x.h"
#include <stdint.h>
#include <boost/utility.hpp>
#include <boost/circular_buffer.hpp>

class Serial : boost::noncopyable
{
  static struct Initializer
  {
    Initializer();
  } _initializer;

public:
  static size_t Read(char* ptr, size_t len = _rbufsize);
  static size_t Write(char* ptr, size_t len);
  static void InterruptHandler();

private:
  static const uint32_t _baudrate = 115200;
  static const size_t _rbufsize = 512;
  static const size_t _wbufsize = 512;

  static boost::circular_buffer<char> _rbuf;
  static boost::circular_buffer<char> _wbuf;

  Serial();
};
Serial.cpp
#include "Serial.hpp"

boost::circular_buffer<char> Serial::_rbuf(Serial::_rbufsize);
boost::circular_buffer<char> Serial::_wbuf(Serial::_wbufsize);

Serial::Initializer Serial::_initializer;
Serial::Initializer::Initializer()
{
  //Interrupt Configuration
  NVIC_InitTypeDef nvic;
  nvic.NVIC_IRQChannel = USART1_IRQn;
  nvic.NVIC_IRQChannelPreemptionPriority = 1;
  nvic.NVIC_IRQChannelSubPriority = 0;
  nvic.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&nvic);

  //Clock Configuration
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

  //USART GPIO Connection
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource9,  GPIO_AF_7);
  GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_7);

  //GPIO Configuration
  GPIO_InitTypeDef gpio;
  gpio.GPIO_Mode = GPIO_Mode_AF;
  gpio.GPIO_Speed = GPIO_Speed_50MHz;
  gpio.GPIO_OType = GPIO_OType_PP;
  gpio.GPIO_PuPd = GPIO_PuPd_UP;
  gpio.GPIO_Pin = GPIO_Pin_9;
  GPIO_Init(GPIOA, &gpio);
  gpio.GPIO_Pin = GPIO_Pin_10;
  GPIO_Init(GPIOA, &gpio);

  //USART Configuration
  USART_InitTypeDef usart;
  usart.USART_BaudRate = _baudrate;
  usart.USART_WordLength = USART_WordLength_8b;
  usart.USART_StopBits = USART_StopBits_1;
  usart.USART_Parity = USART_Parity_No;
  usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  usart.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  USART_Init(USART1, &usart);

  //enable USART
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
  USART_Cmd(USART1, ENABLE);
}

size_t Serial::Read(char* ptr, size_t len)
{
  USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);
  size_t received = 0;
  while (!_rbuf.empty() && received < len)
  {
    *ptr++ = _rbuf.front();
    _rbuf.pop_front();

    ++received;
  }
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

  return received;
} 

size_t Serial::Write(char* ptr, size_t len)
{
  USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
  size_t sent = 0;
  while (sent < len)
  {
    _wbuf.push_back(*ptr++);
    ++sent;
  }
  USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

  return sent;
}

void Serial::InterruptHandler()
{
  //Receive
  if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
  {
    _rbuf.push_back(static_cast(USART_ReceiveData(USART1)));
    return;
  }

  //Send
  if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
  {
    if (!_wbuf.empty())
    {
      USART_SendData(USART1, _wbuf.front());
      _wbuf.pop_front();
    }
    else
      USART_ITConfig(USART1, USART_IT_TXE, DISABLE);

    return;
  }

  //Interrupts which is not permitted raised!
  //This happens when so many packetes are received.
  //Something is wrong... reset USART module.
  USART_Cmd(USART1, DISABLE);
  USART_Cmd(USART1, ENABLE);
}


Read、Writeともにリングバッファに対してのアクセスを行なっています。
割り込みのタイミングでリングバッファに貯められたデータを処理します。

割り込みハンドラでは、このクラスのハンドラを呼び出すようにします。

void USART1_IRQHandler()
{
  Serial::InterruptHandler();
}

--

newlib_stubのほうはこんな感じに書いてみました。

int _read(int file, char *ptr, int len) {
    int num = 0;
    switch (file) {
    case STDIN_FILENO:
        num = Serial::Read(ptr, len);

        if (num < len)
          errno = EAGAIN;
        break;
    default:
        errno = EBADF;
        return -1;
    }
    return num;
}

int _write(int file, char *ptr, int len) {
    int num = 0;
    switch (file) {
    case STDOUT_FILENO: /*stdout*/
        num = Serial::Write(ptr, len);
        break;
    case STDERR_FILENO: /* stderr */
        num = Serial::Write(ptr, len);
        break;
    default:
        errno = EBADF;
        return -1;
    }
    return num;
}

ポイントは、_readをnon blockingで実装するには、len未満の長さしか受信出来なかった場合にしっかりerrnoにEAGAINを入れることです。
これをしないと、システムコールreadを呼び出した際に正常に受信したとみなされ、ゴミがいっぱいついてきます。

--

これで、newlibの標準関数を通してシリアル通信を行うことができます。
readやwrite以外にも、C++らしくstd::coutを使って

std::cout << "Hello World!" << std::endl;

としてみるとカッコイイんじゃないでしょうか。
※std::coutを利用する際の注意点は後記します。

プロジェクトの全体はgithubで公開しています。

なお、このコードはlibstdc++_sとboostに依存しています。
GNUのツールチェインを使った上で、boostのヘッダを/usr/arm-none-eabi/includeに入れればおっけーです。

--

std::coutを用いるときの注意点
std::coutは内部でバッファリングをおこなっており、std::endlやstd::flushが書き込まれない限りフラッシュしません。
フラッシュされない場合には内部バッファにどんどん文字が蓄積され、内部バッファがあふれたタイミング(自分の環境では1024)で、システムコールのwriteが呼び出され、newlib_stubの_write経由で初めてシリアルへの送信が開始されます。
この挙動に不満がある場合には、std::coutを使うのをやめて、自前でwriteするしかなさそうです。
幸い、数値と文字列の変換にはstringstreamがあるので、

std::stringstream ss;
ss << "hoge : " << 3;
std::string str = ss.str();

write(STDOUT_FILENO, str.c_str(), str.size());

で良さそうです。

--

[追記]
どうも、STM32F303のUSART1の割り込み周りにバグがあるようで、大量のデータを受信した場合に、要因不明の繰り返しシリアル割り込みが呼ばれてメインの処理がスタックしてしまう現象が発生しました。
それに対処するために、シリアル割り込みハンドラ内で要因不明の割り込みが検知された場合にはUSART1をリセットする処理を追加しました。
これを追加することで、バグを回避することができたようです。

詳しくは別エントリにまとめました。

0 件のコメント:

コメントを投稿