UARTを使ってみる(2) Retarget

今回はprintfの出力先をTeraTermなどのターミナルソフトへ変更するプログラムです。RetargetはC言語の標準入出力関数のprintf/scanfなどの入出力先をUART通信(ターミナルソフト)、キャラクタディスプレイ、TFTなどに変更する機能です。ターミナルではキーボードからの入力もできますから標準入出力関数が使えると少し便利になります。


まずCubeMXの設定は前の「UARTを使ってみよう!」と全く同じですので、そちらを参考にしてください。CoIDEのプロジェクトを作成したら、メニューからRepositoryページを表示します。
①Chipをオンにします。
②STM32F401REをクリックします。


Componentsをクリックします。


一番上にあるC_LibraryAddボタンを押してプロジェクトへ追加します。

(※ダウンロードしていない場合はDownloadボタン)


①プロジェクトへ追加されたComponents->C_Library->syscalls.cを表示するとロックが掛かって編集できない状態になっています。
syscalls.cを編集するためにC_Libraryを一旦プロジェクトから削除します。削除しても追加されたsyscalls.cはプロジェクトフォルダ内に残っています。


①プロジェクトツリーのルートをマウスで右クリックします。
Add Filesで削除したsyscalls.cを追加します。syscalls.cファイルはプロジェクトフォルダ内のcomponents\coocox-master\C_libraryフォルダの中にあります。


追加されたsyscalls.cはロックが掛かっていません。syscalls.cをダブルクリックしてオープンしてください。


syscalls.cの初めの方でインクルードファイルと変数を定義します。


#include <stdio.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "stm32f4xx_hal.h"
#include "stm32f4xx_hal_uart.h"
extern UART_HandleTypeDef huart2;



少し下へ行くと_write関数と_read関数があります。中身は未実装で空っぽですので、それぞれ次のプログラムを追加します。


/*Low layer read(input) function*/
__attribute__ ((used))
int _read(int file, char *ptr, int len)
{
 char c;

 if( HAL_UART_Receive( &huart2, (uint8_t*)&c, 1, 0xFFFF ) == HAL_OK )
 {
  HAL_UART_Transmit( &huart2, (uint8_t*)&c, 1, 0xFFFF ); // callback
  *ptr = c;
  return 1;
 }

 return 0;

#if 0
     //user code example
     int i;
     (void)file;

     for(i = 0; i < len; i++)
     {
        // UART_GetChar is user's basic input function
        *ptr++ = UART_GetChar();
     }

#endif

//    return len;
}

/*Low layer write(output) function*/
__attribute__ ((used))
int _write(int file, char *ptr, int len)
{
 if( HAL_UART_Transmit( &huart2, (uint8_t*)ptr, len, 0xFFFF ) != HAL_OK )
 {
  while(1); // Error!!
 }

#if 0
     //user code example

     int i;
     (void)file;

     for(i = 0; i < len; i++)
     {
        // UART_PutChar is user's basic output function
        UART_PutChar(*ptr++);
     }
#endif

    return len;
}


これでC言語の標準入出力関数のいくつかがターミナル(UART通信)へリターゲットされます。

テストの為のmain.cのプログラムは以下の様になります。


#define MEGA (1000*1000)

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();

 puts( "" );
  printf( ">>>>>>>> Welcome to Nucleo World!! <<<<<<<<<<\r\n" );
 puts( "" );
 printf( "System Clock Freq : %d MHz\r\n", HAL_RCC_GetSysClockFreq() / MEGA );
 printf( "HCLK Clock Freq   : %d MHz\r\n", HAL_RCC_GetHCLKFreq()     / MEGA );
 printf( "PCLK1 Clock Freq  : %d MHz\r\n", HAL_RCC_GetPCLK1Freq()    / MEGA );
 printf( "PCLK2 Clock Freq  : %d MHz\r\n", HAL_RCC_GetPCLK2Freq()    / MEGA );
 puts( "" );

 printf( "1~4のキーを押してください。\r\n" );

 while(1)
  {
  switch( getchar() )
  {
   case '1': printf( "Good morning\r\n" );  break;
   case '2': printf( "Good afternoon\r\n" ); break;
   case '3': printf( "Good night\r\n" );    break;
   case '4': printf( "Hello\r\n" );      break;
   }
  }

}


実行結果

※TeraTermで日本語を表示するにはメニューからSetup->TerminalでKanji(recieve/transmit)にSJISを指定します。


プログラムを見てもらえば分かりますが、printfだけでなくputsやgetchar関数も使える様になってかなりCライクなコードが書けるようになっています。これでscanfも一応動くのですがscanfではBack SpaceキーやDeleteキーが使えない(押すと暴走します)ので、打ち間違えの修正ができません。Back Spaceだけでも使えれるとかなり文字入力し易くなるので、超簡易ですがBack Spaceの使えるテキスト入力関数を作ってみました。


#define ASCII_BS   0x08
#define ASCII_LF   0x0A
#define ASCII_CR   0x0D
#define ASCII_SPACE  0x20

void text_editor( char* ptr, int len )
{
 const char *top = ptr;
 char c;

 while( 1 )
 {
  if( HAL_UART_Receive( &huart2, (uint8_t*)&c, 1, 0xFFFF ) == HAL_OK )
  {
   if( isalnum( c ) )
   {
    HAL_UART_Transmit( &huart2, (uint8_t*)&c, 1, 0xFFFF );
    *ptr++ = c;
   }
   else
   {
 
   if( c == ASCII_CR )
    {
     uint16_t v = ( ASCII_CR << 8 ) ' ASCII_LF;
     HAL_UART_Transmit( &huart2, (uint8_t*)&v, 2, 0xFFFF );
     *ptr = 0;
     return;
    }
    if( c == ASCII_BS )
    {
     if( top < ptr )
     {
      uint32_t v = ( ASCII_BS << 16 ) ' ( ASCII_SPACE << 8 ) ' ASCII_BS;
      HAL_UART_Transmit( &huart2, (uint8_t*)&v, 3, 0xFFFF );
      --ptr;
      *ptr = 0;
     }
    }
   }
  }
 }
}

void get_int( char* msg, int* v )
{
  char buffer[ 20 ];
 printf( msg );
 text_editor( buffer, sizeof( buffer ) );
 sscanf( buffer, "%d", v );
 puts( "" );
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();


 const char *Week_Days[] = { "月", "火", "水", "木", "金", "土", "日" };
 int year = 0, month = 0, day = 0, hour = 0, minutes = 0, second = 0, week_day = 0;

 while(1)
  {
  get_int( "西暦を入力してリターンキーを押してください。\r\n", &year );
  get_int( "月を入力してリターンキーを押してください(1-12)。\r\n", &month );
  get_int( "日を入力してリターンキーを押してください(1-31)。\r\n", &day );
  get_int( "時間を入力してリターンキーを押してください(0-23)。\r\n", &hour );
  get_int( "分を入力してリターンキーを押してください(0-59)。\r\n", &minutes );
  get_int( "秒を入力してリターンキーを押してください(0-59)。\r\n", &second );
  get_int( "曜日を入力してリターンキーを押してください(1-7)。\r\n(1.月, 2.火, 3.水, 4.木, 5.金, 6.土, 7.日)\r\n", &week_day );

  printf( "入力された日時は %d年 %d月 %d日 %s曜日 %d時 %d分 %d秒 です。\r\n",
    year, month, day, Week_Days[ week_day-1 ], hour, minutes, second );

  puts( "" );
  }

}


実行結果



マイコンでは仮想COMポートを使ったUART通信はprintfデバッグに使われることが多いです。例えばArduinoではデバッガがないので仮想COMポートのprintfデバッグをメインのデバッグ方法としています。Nucleoではデバッガが使えますがprintfデバッグでデータをコールバックする方法も重宝しますのでいつでも使えるようにしておくと便利です。次に新しくプロジェクトを作ったら、Retargetを加えたsyscall.cをプロジェクトへコピーすれば良いfsでしょう。