業務で組み込みをするうちに、UART等の通信を自分で作ってみたいという話。知ってる人からすれば「馬鹿じゃねーの!!」っていう話かもしれないが、参考になればぜひ。
最後にソースを添付するので、少々お付き合いをお願いします。
環境
- Rasberrypi
- 今回の要。筆者はめんどくさいのでsshでメインPCから飛ばせるように設定した
- あと、MiniUART有効化もお忘れないよう。
手順
今回のミソになる点は以下の通り。
- ボーレートの計算(ボーレートとは?とかそのへん)
- GPIOでいかに波形をうまく作るか(次のエッジからその次のエッジまでの時間精度を高めるにはどうしたらいいか)
- 物理アドレスの取得方法
以上3点がミソ。順番に解決していこう。
その前に
UARTって何ぞや?っていう説明をしておかなければならない。
UART (Universal Asynchronous Receiver/Transmitter, ユーアート) は、調歩同期方式によるシリアル信号をパラレル信号に変換したり、その逆方向の変換を行うための集積回路である。本機能のみがパッケージングされたICで供給されるものと、マイクロプロセッサのペリフェラルの一部として内蔵されるものとがある。マキシムのMAX232のような、RS-232C規格に準拠する信号レベルに変換するICと組み合わせて、外部機器とのインタフェースとして利用されるのが一般的である。(wikipediaより)
…?なにいってんだ?
というわけで自分なりの解釈をする。
UARTは基本的に1:1の通信方式。Tx線とRx線があり、それぞれ名前の通りTx線で相手側にデータを送り、Rx線で相手のデータを受け取る。
キモになるのは、ボーレートとスタート・ストップビット(ガチのUARTならパリティとかあるが、今回は触れない)。
- ボーレートは、「このくらいの速さで通信するよ~」という値。正確には「1Bitが変調する時間(間隔)」。なので、4800ボーレートというと、だいたい1/4800秒に1Bit送るというイメージでよい。
- スタートビット・ストップビットは、「ここからデータ読みだすよ」というのと、「データ読みだすのはここで終わりだよ」という合図。下記図のStaとStoがまさにスタートビットとストップビット。
上記図のように、スタートビットで始まり、0からデータ「H=1、L=0」で1Byte読みだす。ここでは、読みだした結果C1となる。そして、ストップビットという構成になる。
…さて本題に戻る。
ボーレートの計算
ペリフェラルリストを見ると、以下のように記載がある(p11を参照)
If the system clock is 250 MHz and the baud register is zero the baudrate is 31.25 Mega
baud. (25 Mbits/sec or 3.125 Mbytes/sec). The lowest baudrate with a 250 MHz system
clock is 476 Baud.
When writing to the data register only the LS 8 bits are taken. All other bits are ignored.
When reading from the data register only the LS 8 bits are valid. All other bits are zero.
…多分これは誤りかと考える。ボーレートは1秒で変調できる回数なので、31.25Mbaud = 1/31.25M(s/bit)となるかと思う。そのため1/31.25 * 10 ^6秒が、波形間の時間と考える。
GPIO波形の作り方
ボーレートは115200baudといった、かなり秒間隔が短いものもある。そのため、1Bit間の波形を作る際には、正確な精度が求められる。
ラズパイにはSystemTimerとARM Timerが乗っている。SystemTimerは1MHz、ARM Timerは250MHzのクロックである。タイマにはフリーランカウンタと言って、ただカウントアップし、オーバーフローする際に0xFFFFFFFF→0x00000000と繰り返されるカウンタが乗っている。今回はARM Timerのほうが精度が良いので、ARM Timerのフリーランカウンタを利用する。
ラズパイのペリフェラルリストの196pの14.2に、ベースアドレスは0x7E00B000と記載がある。フリーランカウンタはベースアドレスから+0x0420の4Byteが確保されている。ベースアドレスからのオフセットは良いが、ベースアドレスは実は異なる。このベースアドレスはVC CPU Bus Addressのアドレスである。物理アドレスに変換するためには、0x3Fnnnnnnとしてやる。これは6pの1.2.2を参照するとわかる。(ペリフェラルリストでは0x20nnnnnnだが、実際に調べると0x3Fnnnnnnがペリフェラルの物理アドレスとなる。)
物理アドレスの取得
我々が作成するソースコードはユーザ領域で、物理アドレスにはアクセスできないようになっているため、Linuxのmap関数を利用し、マッピングを行う。
※ただし、実行にはroot権限が必要
実装
上記を考慮して、実装する。以下の通り。(使っていない関数もあるが、後で整備します)
# include <stdio.h>
# include <stdint.h>
# include <unistd.h>
# include <stdlib.h>
# include <wiringPi.h>
# include <bcm_host.h>
# include <sys/mman.h>
# include <fcntl.h>
/* ユーザ定義値 */
# define PIN 4 /* PIN番号 GPIO番号とは異なる */
# define BAUDRATE 4800 /* ボーレート */
/* DEFINE */
# define D_CLOCK_FRQ ( 250000000 )
# define D_SEC2NANOSEC ( 1000000000 )
# define D_TIMER_UNIT ( D_CLOCK_FRQ / D_SEC2NANOSEC )
# define D_ALL_BIT ( 10 )
# define D_ARMTIMER_ADD ( 0x3F00B000 )
# define D_ARMTIMER_OFFSET ( 0x00000420 )
/* MACRO */
# define _US (unsigned long)1000000 /* 秒→ナノ秒の変換 */
# define _SLEEP_US2S(x) ( usleep( (unsigned long)x * _US ) )
/* 秒単位でナノ単位のスリープ */
# define _BPS(x) ( _SLEEP_US2S( 1/x ) ) /* ボーレート再現(没になるかも) */
/* プロトタイプ宣言 */
static void testPeriodicChange( void ); /* H/Lを繰り返すだけのプログラム */
static void sendUart1Byte( unsigned char string ); /* UARTもどきで1Byte送信する関数 */
static int openMapping( int* fd , unsigned int address );
/* ArmTimer開始処理 */
static unsigned char closeMapping( int *fd ); /* ArmTimer終了処理 */
static unsigned char readSystemTimer( void );
/* システムタイマの物理アドレス読み出し*/
static unsigned char isNextBit( void );
static unsigned char isStopBit( void );
/* グローバル変数宣言 */
static unsigned int L_next_timer ; /* 前回フリーランカウンタ保存 */
static int L_fd_systimer; /* FD ArmTimer */
static int L_add_systimer; /* Armtimer アドレス */
static const unsigned int L_unit_timer = ( D_SEC2NANOSEC / BAUDRATE ) / 4 ;
/* 1BITあたりの長さ */
int main(void){
/* GPIO初期設定 */
wiringPiSetupGpio();
/* 入力/出力切り替え */
pinMode( PIN , OUTPUT );
/* 波形H設定 */
digitalWrite( PIN , HIGH );
delay( 500 );
L_add_systimer = openMapping( &L_fd_systimer , D_ARMTIMER_ADD );
L_add_systimer += D_ARMTIMER_OFFSET;
sendUart1Byte( 0xC1 );
return 0;
}
/* H/Lを繰り返すだけのプログラム */
/* 後でデッドコードとしたい */
void testPeriodicChange( void ){
/* 無限ループ */
while(1){
/* 波形H設定 */
digitalWrite( PIN , HIGH );
delay( 500 );
/* 波形L設定 */
digitalWrite( PIN , LOW );
delay( 500 );
}
}
/* 1Byteを送信するコード */
void sendUart1Byte( unsigned char string ){
unsigned int count = 0; /* カウンタ */
L_next_timer = (unsigned int)( *( volatile unsigned int *)L_add_systimer ) ;
/* StartBit */
digitalWrite( PIN , LOW );
while(count < 8){
if( isNextBit() ){
if( ( string >> count ) & 0x01 == 0x01 ){
/* 波形H設定 */
digitalWrite( PIN , HIGH );
}else{
/* 波形L設定 */
digitalWrite( PIN , LOW );
}
count ++;
}
}
while( 1 ){
if( isNextBit() ){
/* ENDBit */
digitalWrite( PIN , LOW );
break;
}
}
while( 1 ){
if( isStopBit() ){
/* Hに戻しておく */
digitalWrite( PIN , HIGH );
break;
}
}
}
static unsigned char isNextBit( void ){
if( (unsigned int)( *( volatile unsigned int *)L_add_systimer - L_next_timer ) > L_unit_timer ){
L_next_timer = (unsigned int)( *( volatile unsigned int *)L_add_systimer ) ;
return 1;
}
else{
return 0;
}
}
static unsigned char isStopBit( void ){
if( (unsigned int)( *( volatile unsigned int *)L_add_systimer - L_next_timer ) > L_unit_timer / 2 ){
L_next_timer = (unsigned int)( *( volatile unsigned int *)L_add_systimer ) ;
return 1;
}
else{
return 0;
}
}
static int openMapping( int* fd , unsigned int address ){
int mapping_address;
if( ( *fd = open("/dev/mem" , O_RDWR | O_SYNC ) ) < 0 ){
printf("open error:%d\n" , fd );
exit -1;
}
mapping_address = (int)mmap(NULL , 4096 , PROT_READ | PROT_WRITE , MAP_SHARED , *fd , address );
if( address == MAP_FAILED ){
closeMapping( fd );
printf("map error\n");
exit -1;
}
return mapping_address;
}
static unsigned char closeMapping( int *fd ){
close( (int)(*fd) );
}
簡単に処理を説明すると、
- 例えば「0xAA」なら、「b10101010」なので、「HLHLHLHL」とGPIOを出力してやるプログラム
- ただ、GPIOの出力する時間を一定にするため、ArmTimerのアドレスにアクセスして、指定時間超過していれば、切り替えて次の処理に行く、というのを8Bit分行っている。(正確にはスタートビットとストップビットも含めるので、10Bitないし9.5Bit
といった簡単なシステム。
受信側はpythonで、Serial関数を用いている(ガチのUARTで取得できていることを確認したかったので、作りこみはしなかった)
あとがき
- GPIOって意外にus単位で動いてくれるんだなと感心しました。
- やる気があれば受信も作りたい。