ムライボックス (20) --- ソフトウェア (11)

今回は STM32F4xx シリーズのチップを搭載した Nucleo-64 ボードでの話題です。
ハードウェアとしては Arduino 用の「ムライシールド(仮)」(→こちらこちら) を使用し、ソフトウェアとしては CubeF4 HAL ライブラリを使用しています。
デフォルトでは、Arduino 用コネクタに U(S)ART 入出力信号が結線されていないので、(→こちら) のような「改造」を施す必要があります。
Nucleo では、ポートのコネクタへの割り当てに際して、Arduino の SPI、PWM、AD 入力などの機能と同様の機能を持つピンを割り当てることを優先しているため、ポート順やビット番号順には並んでいません。
そのため、"OR" ゲート信号を出力するポートに関しては、出力ポートとビット位置のテーブルをあらかじめ作成しておいて、実行時に、それを参照しながら出力するようにしています。

#define N_PORTS (7)  // Number of MIDI-OUT ports

#define D9_GPIO_Port  GPIOC
#define D9_Pin        GPIO_PIN_7

#define D10_GPIO_Port GPIOB
#define D10_Pin       GPIO_PIN_6

#define D11_GPIO_Port GPIOA
#define D11_Pin       GPIO_PIN_7

#define D12_GPIO_Port GPIOA
#define D12_Pin       GPIO_PIN_6

#define D5_GPIO_Port  GPIOB
#define D5_Pin        GPIO_PIN_4

#define D6_GPIO_Port  GPIOB
#define D6_Pin        GPIO_PIN_10

#define D7_GPIO_Port  GPIOA
#define D7_Pin        GPIO_PIN_8

typedef struct tag_gpio_tab_entry {
  GPIO_TypeDef *port;
  uint16_t      pin;
} gpio_tab_entry_t ; // typedef struct 

const gpio_tab_entry_t gpio_tab[N_PORTS] = {
  {  D9_GPIO_Port,  D9_Pin }, // "OR" port 1
  { D10_GPIO_Port, D10_Pin }, // "OR" port 2
  { D11_GPIO_Port, D11_Pin }, // "OR" port 3
  { D12_GPIO_Port, D12_Pin }, // "OR" port 4
  {  D5_GPIO_Port,  D5_Pin }, // "OR" port 5
  {  D6_GPIO_Port,  D6_Pin }, // "OR" port 6
  {  D7_GPIO_Port,  D7_Pin }  // "OR" port 7
}; // const gpio_tab_entry_t gpio_tab[]

CubeF4 HAL ライブラリの U(S)ART モジュールでは、FIFO キュー (リング・バッファ) を使った入出力はサポートされていないので、入力および出力ともに 1 キャラクタずつ入出力する形でライブラリを利用し、FIFO キューは「自前」で用意することにしました。

#define NFIFO   (32) // number of MIDI-OUT FIFO entries

typedef struct tag_midiout_fifo_entry {
  uint16_t mask;
  uint8_t  midibyte;
  uint8_t  padding;
} midiout_fifo_entry_t;

midiout_fifo_entry_t midiout_fifo[NFIFO];

// read/write index of MIDI-OUT FIFO
struct tag_fifo_ix {
  volatile int rd;
  volatile int wr;
} fifo_ix;    

FIFO キューは、MIDI-IN から入力したデータを「処理」した後と、「出力部」との間に設けています。
受信した MIDI バイトを「処理」したものと、そのバイトに対するマスク・パターンをテーブル参照して得られたビットマスク・データをセットにした構造体を定義し、その構造体の配列に読み書きのインデクスの構造体を組み合わせて FIFO キューを実現しています。
CubeF4 HAL ライブラリでは、割り込みおよび DMA を使用した出力では、ハードウェアの「送信完了割り込み」が使われており、送信完了割り込みハンドラからユーザ定義の「HAL_UART_TxCpltCallback()」関数が呼び出されます。
ここでは「自前」の割り込みハンドラでハードウェアの送信完了割り込みを操作することはせず、HAL ライブラリの「作法」に従ってコールバック関数を定義して使うことにしました。
UART HAL ドライバの割り込み利用のノン・ブロッキング受信関数、つまり実際にキャラクタを受信するまで待たない受信関数「HAL_UART_Receive_IT()」のプロトタイプ定義は

HAL_StatusTypeDef 
  HAL_UART_Receive_IT (UART_HandleTypeDef * huart, 
                       uint8_t            * pData, 
                       uint16_t             Size);

となっており、第 1 引数「huart」に渡す U(S)ART のインスタンスに対し、第 2 引数「pData」でポイントされる受信バッファに、第 3 引数「Size」で指定するバイト数のキャラクタを受信するようになっています。
ノン・ブロッキングなので、必要な設定を終えるとすぐにリターンし、実際にキャラクタを受信するまで待ちません。
「Size」バイトの受信が完了すると、ユーザ定義のコールバック関数「HAL_UART_RxCpltCallback()」が呼び出されます。
ここでは、受信バイト数の指定を「1」とし、1 バイト受信するごとにコールバックが呼ばれるようにします。
コールバック関数を下に示します。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  USART_TypeDef *uart_n;
  uint8_t  c;       // received byte
  uint8_t  midi_ch; // MIDI ch / system message type
  uint16_t bm;      // effective bitmap for port masking
  
  uart_n = huart->Instance;
  if (USART1 == uart_n) { // this instance is "USART1"
// MIDI byte received, LED ON
    HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
    c = midi_in_buf; // get byte from MIDI-IN
    midi_ch = (0x0f & c);  // extract MIDI ch / system msg type
    bm = r_mask; // use "running" port mask
    if (0x80 & c) { // status byte
      if (0xf0 > c) { // channel mode message [0x80..0xef]
        bm     = mask_tab[midi_ch]; // get port mask pattern  
        r_mask = bm; // set "running" port mask pattern 
#if !defined(REMAP_INH)        
        c = ((c & 0xf0) | remap_tab[midi_ch]); // remap MIDI channel
#endif        
      } else { // system common/realtime message [0xf0..0xff]
        bm = sysmsg_tab[midi_ch]; // escaping for system realtime
        if (0xf8 > c) { // system common msg [0xf0..0xf7]
          r_mask = bm; // set "running" port mask pattern
        } // if (0xf8 > c) { ...    
      } // if (0xf0 > c) {} else { ...
    } else { // data byte 
//  no processing required for data byte
    } // if (0x80 & c) { ...
    midiout_fifo[fifo_ix.wr].mask = MASK_POL(bm);  // effective port mask pattern
    midiout_fifo[fifo_ix.wr].midibyte = c;
    fifo_ix.wr++; // advancd FIFO write index
    if (NFIFO <= fifo_ix.wr) { // wap around
      fifo_ix.wr = 0;
    } // if (NFIFO <= fifo_ix.wr) { ...
    if (txc_flag) { // UART TX already idle
// send MIDI byte immediately
      send_midibyte(); 
    } else { // UART TX not idle
// leave it for Tx Complete callback routine
    } // if (txc_flag) { ...
// prepare to receive next char    
    HAL_UART_Receive_IT(&huart1, &midi_in_buf, 1);
  } // if (USART1 == uart_n) { ...
} // void HAL_UART_RxCpltCallback()

HAL_UART_RxCpltCallback() 関数はただひとつだけ定義され、すべての U(S)ART のインスタンスで共通して使われるので、まず、今回の呼び出しが MIDI-IN/OUT に使っている USART1 からのものかどうかを調べます。
USART1 の場合には、指定した入力バッファ「midi_in_buf」から 1 バイト読み出し、「処理」を行います。
この「処理」は他のプロセッサ用のプログラムと同様のものです。
「処理」によって得られた MIDI バイト、およびビットマスク・パターンを出力部へ送る FIFO キューに書き込みます。
その後に、すでに UART 送信ハードウェアがアイドル状態になっていることを示すフラグ「txc_flag」を参照し、すでにアイドル状態なら出力ルーチン「send_midibyte()」を直接呼び出し、まだアイドル状態でなければ送信完了コールバックに任せることにして、何もしません。
その後は、1 バイト入力のための UART 受信関数

    HAL_UART_Receive_IT(&huart1, &midi_in_buf, 1);

を呼び出し、次の MIDI バイトに備えてから受信完了コールバック関数を終了します。
次のバイトが受信されると、再び HAL_UART_RxCpltCallback() 関数が呼び出されるので、プログラムの文面上は無限ループになっていなくても、実質的には無限ループとして作用します。
UART 送信完了コールバック関数「HAL_UART_TxCpltCallback()」を下に示します。

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
  USART_TypeDef *uart_n;
  
  uart_n = huart->Instance;
  if (USART1 == uart_n) { // this instance is "USART1"
    if (fifo_ix.rd == fifo_ix.wr) { // FIFO empty
      txc_flag = 1; // set tx_complete flag
// no more MIDI bytes, LED OFF
      HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
    } else {
      send_midibyte();
    } // if (fifo_ix.rd == ...
  } // if (USART1 == uart_n) { ...
} // void HAL_UART_TxCpltCallbackI()

FIFO キューが「空」なら「txc_flag」をセットし、そうでなければ出力ルーチン「send_midibyte()」を呼び出します。
send_midibyte() を下に示します。

void send_midibyte( void )
{
  uint16_t bm; // bitmask pattern
  uint8_t  c;  // MIDI byte
    
  bm = midiout_fifo[fifo_ix.rd].mask; // bitmask
// setup "OR" mask 
  for (int i = 0; i < N_PORTS; i++) { // scan bitwise
    HAL_GPIO_WritePin(gpio_tab[i].port, 
                      gpio_tab[i].pin,
                      (GPIO_PinState)(0x01 & bm));
    bm >>= 1; // next bit
  } // for (int i = 0; ...    
// send MIDI byte
  c = midiout_fifo[fifo_ix.rd].midibyte, 
  HAL_UART_Transmit_IT(&huart1, 
                       &(midiout_fifo[fifo_ix.rd].midibyte), 
                       1);
  txc_flag = 0; // clear tx_complete flag
  fifo_ix.rd++; // advance FIFO read index
  if (NFIFO <= fifo_ix.rd) { // wrap around
    fifo_ix.rd = 0;
  } // if (N_FIFO <= ...
  sysx_proc(c); // system exclusive processing  
}  // void send_midibyte()

まず、FIFO キューから MIDI バイトとビットマスク・データを取り出し、「gpio_tab[]」を参照しながら "OR" マスク・データを出力します。
MIDI バイトの出力は HAL ライブラリ関数の呼び出し

HAL_UART_Transmit_IT(&huart1, 
                     &(midiout_fifo[fifo_ix.rd].midibyte), 
                     1);

で行なっています。
受信完了コールバック関数で「無限ルーブ」を実現しているので、メイン関数では、各種初期化を終え、1 バイト目の受信開始のための「HAL_UART_Receive_IT()」関数の呼び出しを終えた後の無限ループ部分は「空」になっています。

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  txc_flag = 1; // UART Tx already complete
// kick off UART Rx receiving  
  HAL_UART_Receive_IT(&huart1, 
                      &midi_in_buf, 
                      1);
  while (1)  {
  } // while (1) { ...
} // int main()