PIC×PICCによるシリアル通信(6)


今回、私は丸一日半(記事としては6日に渡ってしまったが)かけてPIC16F877A(16F887)をターゲットとしたUSARTによるシリアル通信のプログラムをPIC Cを用いて書くという試みを行なった。(検索などで直接この記事に飛んできた人は是非、過去5日分の記事も併せてご覧ください。)



ここまでのことをまとめると…。


・PIC用にCで書かれたサンプルがインターネット上に少なすぎる。(いままでPICの開発はアセンブラがメインだったため)
・PIC CとCSC Cとで細かい点が異なる。どちらの情報も不足している。
・PIC CはコンパイラC++に対応していない。そのため他のC++で書かれたソースをコピペしてくるといろいろ問題を起こす。これではいままで開発してきた資産が流用できない。
・PIC Cしか使っていないが、コンパイラベンダーがどう見ても素人。
CSC Cのほうは体験版のライセンスを要求したが丸二日経っているのにいまだ送られてこない。
・Cで書けるとは言ってもPIC用のアセンブラで書いた経験が無いと一苦労。PICのアーキテクチャを正しく理解していないとハマる。PICのプログラムのサンプルをインターネットで探すとほとんどはアセンブラで書かれたソースなので、PICのアセンブラを読めないとなかなか先に進めない。
・PIC16シリーズはbankなどというとても劣悪なアーキテクチャを採用しているためコンパイラベンダー泣かせ。コンパイラ自体にとても癖がある。また、bank切り替えはコンパイラのコード生成のバグを生みやすい。
・PIC Cで生成されたlstファイルを見たところ、レジスタに直交性が無いためとてもひどいコードが生成されている。
・こんな程度のコンパイラが機能制限のないバージョンは商用ソフトだなんて信じられない。
・MPLAB自体がとても使いにくい。周波数の設定ぐらいプロジェクト設定でさせるべきだし、どうもVisualStudioに慣れている身としては使いにくい。(AVRStudioはVisualStudioっぽくてすぐに使えたのに…)
・ICD2はdebuggerとprogrammerとの切り替えが必要で使いにくい。(MPLABが悪い?)
・MPLABのテキストエディタの操作性が悪い。知らないうちにソースがドラッグされていたりして、必要な部分を消してしまっていたことが何度かあった。どうもバグ持ちのように思う。
・秋月のPICライターが使いにくい。ヒューズビットはhexファイルから読み込んだものを反映させて欲しい。
・秋月のPICライター、書き込みが遅すぎる。たかだか361B書き込むのに(実際はFLASH全域だから8Kwordだが)42秒もかかる。
・PIC16シリーズ自体の設計がいけてない。すべての割り込みが4番地に飛んでくるだとか、割り込み要因の特定に一苦労するだとか、有り得ない。
コンパイラのバグではないかとかPICライターが正しく動いていないのではないかだとか、余計なことに気をとられて開発に集中できなかった。
・私がAVRの時は30分もかからず出来たことが、PICに至っては丸一日使っても出来なかった。


以上が、私のPICを叩き壊してやりたいほどPICが憎らしい17の理由である。毎回PICに触るごとにこのような言いしれぬ憎しみが蓄積かつ増幅されていくのである。


もちろん、その大半はPICに責任があるのではなく私のほうに問題がある(私の能力的な問題とか、私の持っているPICライターの問題とか)のだろうし、PIC24FとかdsPICの時代にいまさらPIC16みたいなアナクロ(←アナログではないよ)デバイスを使うほうが悪いのだが、I/Oが多くて低価格でプログラマブルな石となるとPIC16F887あたりしか候補がないのが現状である。今回のことでいろいろ勉強になったので今後も使っていきたいと思っている。


おまけとして、PIC Cの生成するプログラムサイズの制限を緩和する方法を発見したので書いておく。上位コンパチと思われるdeviceを選択して開発するのである。上位のdeviceのほうがもともと搭載されているメモリが大きいので、サイズ制限も甘いのである。注意しながらプログラムを書けば、この上位コンパチのdevice用に生成したバイナリは、そのままターゲットdeviceでも動くだろう。(ライセンス的に問題があるのかどうかは知らないが)


以下、今回私が書いたシリアル通信のソース。私が検索したとき、PIC Cでinterruptを用いてring bufferを用意して書かれているソースは皆無だったので資料的価値はたぶんあると思う。このソースは自由に使ってもらって構わない。16F877,16F887で動作確認したが、USART付きのPIC16シリーズならどれにでも簡単に移植できるだろう。


#ifndef __USART_16F877A__H__
#define __USART_16F877A__H__

// このシンボルをdefineしなければ送信関係はコンパイルされない。
#define __SEND_ENABLE__

// 同じく、このシンボルをdefineしなければ受信関係はコンパイルされない。
#define __RECEIVE_ENABLE__

// byteを定義しておく。
typedef unsigned char byte;

/* sio設定 */
void sio_init(unsigned int baud,int bits)
// one Start bit, eight or nine data bits and one Stop bit
// 16F87XAは、スタートbitは1bitで、8,9bitのdata bits,stop bitは1bit固定のようだ。
{

// 条件 BRGHビット=0 BRGHビット=1
// SYNCビット=0 通信速度=Fosc/(64(X+1)) 通信速度=Fosc/(16(X+1))
// SYNCビット=1 通信速度=Fosc/(4(X+1)) なし

unsigned int ubrr = ( (F_CPU>>4)+(baud>>1) )/baud;
// UBRRを設定するときに丸め処理をしておく。

if (ubrr > 256)
{ // (ubrr-1)が8bitに収まっていないのでBRGHを0にするほうの計算式を適用
ubrr >>= 2;
BRGH = 0;
} else {
BRGH = 1;
}
SPBRG = (byte)(ubrr-1); // ボーレート設定
SYNC = 0; /* 非同期通信 */

#ifdef __RECEIVE_ENABLE__
CREN = 1; /* 受信許可 */
RCIE = 1; /* 受信割り込み許可 */
#else
CREN = 0; /* 受信不許可 */
RCIE = 0; /* 受信割り込み不許可 */
#endif
#ifdef __SEND_ENABLE__
TXEN = 1; /* シリアル送信許可 */
// TXIE = 1; /* 送信割り込み許可*/
TXIE = 0; // 送信割り込みは送信するときにのみ許可すれば良い。(いまは許可しない)
SPEN = 1; /* シリアル送信ピン有効 */
#else
TXEN = 0; /* シリアル送信不許可 */
TXIE = 0; /* 送信割り込み不許可*/
SPEN = 0; /* シリアル送信ピン無効 */
#endif
PEIE = 1; /* 送受信関係の割り込み許可 */

TX9 = (bits==9)?1:0; /* 8- or 9-bit 送信 */
RX9 = (bits==9)?1:0; /* 8- or 9-bit 受信 */
}

// フロー制御をしないので32 bytesの送受信bufferを自前で用意する
#ifdef __RECEIVE_ENABLE__
bank1 volatile char usart_recvData[32]; // USARTで受信したデータ。ring buffer
volatile byte usart_recv_write; // 現在のwrite位置(usart_recvDataのindex)
byte usart_recv_read; // 現在のread位置(usart_recvDataのindex)
#endif
#ifdef __SEND_ENABLE__
bank1 volatile char usart_sendData[32]; // USARTで送信するデータ。ring buffer
byte usart_send_write; // 現在のwrite位置(usart_sendDataのindex)
volatile byte usart_send_read; // 現在のread位置(usart_sendDataのindex)
#endif

#ifdef __RECEIVE_ENABLE__

// データを受信しているかのチェック。受信しているなら非0。
int is_received(void)
{
return (usart_recv_write != usart_recv_read) ? 1 : 0;
// read位置とwrite位置が異なるならば受信データがあるはず
}

// データを受信するまで待機する
void wait_for_receiving(void)
{
while(!is_received())
;
}

// 受信したデータを返す。受信したデータがない場合は受信するまで待機。
int getReceivedData(void)
{
wait_for_receiving();
return usart_recvData[usart_recv_read++ & 0x1f];
}

#endif


#ifdef __SEND_ENABLE__

// 送信バッファにデータがあれば、そこから1バイト送信するルーチン。
// 内部的に使用しているだけなのでユーザーは呼び出さないで。
void private_send_char(void)
{
if (usart_send_write != usart_send_read)
{
TXREG = usart_sendData[usart_send_read++ & 0x1f];// 送信バッファのデータを送信
TXIE = 1; // 次のデータを送信しないといけないかも知れないので送信割り込み許可に。
} else {
// データがないので送信レジスタが空き(TXIF==1)だからと言って割り込みがかかり続けられては困る。
// これ以上割り込みがかからないように送信割り込みを禁止してやる必要がある。
TXIE = 0;
}
}

// 1バイト送信
void sendChar(int c)
{
// 送信バッファがいっぱいなら待つ
while(!(((usart_send_write + 1) ^ usart_send_read) & 0x1f))
// usart_send_write+1 と usart_send_readの下位6bitが一致する間、ループする
;

// 何はともあれ送信バッファにデータを積む。
usart_sendData[usart_send_write++ & 0x1f] = c;

// 送信レジスタがセットされている == 送信できる状態 ならば、
// 一度だけ送信しておく。
if (TXIF)
private_send_char();
}


// 文字列の送信
void sendString(const char* p)
{
while(*p)
sendChar(*p++);
}
#endif


// 送受信のための割り込みハンドラ
void interrupt serial_isr(void)
{
#ifdef __RECEIVE_ENABLE__
if(RCIE & RCIF)
{
// RCIF = 0; // 割り込み要因の解除
// RCIFのソフト的なクリアは出来ない(?)

// 割り込みによる受信
usart_recvData[usart_recv_write++ & 0x1f] = RCREG; // 受信データを受信バッファに格納
}
#endif
#ifdef __SEND_ENABLE__
if(TXIE & TXIF)
{
// TXIF = 0; // 割り込み要因の解除
// TXIFのソフト的なクリアは出来ない(?)

// 割り込みによる送信
private_send_char();
}
#endif
}

#endif // #ifndef __USART_16F877A__H__


#include <pic.h>

// クロックの設定
#define F_CPU (4000000UL)
#include "usart16f877a.h"

#define XTAL_FREQ F_CPU
#include "../samples/delay/delay.h"

// 16F877/16F877A
__CONFIG(PROTECT & PWRTEN & XT & WRTEN & DEBUGDIS & LVPDIS & BORDIS & WDTDIS);
// コードプロテクトなし、パワーアップタイマ(電源投入後72msリセットし続ける)を使う
// 外部発信器 , FLASHメモリ書き込みプロテクト , Low Power Program無効
// デバッガ無効 , ブラウンアウトリセット無効 , watch dog timer無効

// 16F887用
//__CONFIG(CP /*PROTECT*/ & PWRTEN & XT & /*WRTEN &*/ DEBUGDIS & LVPDIS & BORDIS & WDTDIS);


void port_init(void)
{
TRISA=0xff; //入出力設定 A,Bポートすべて入力に
TRISB=0xff;
TRISC=0b10111111; // RC6 = TxDなので出力に。RC7はRxDなので入力にする必要あり。
TRISD = 0xff;
}

void main()
{
int i,d;

di(); // 全割り込み不許可

for(i=0;i<2000;++i)
; //動作安定のためのウェイト

sio_init(19200,8); // SIO設定 19200bps , 8bit
port_init();

ei(); // 全割り込み許可

sendString("Hello!! PIC USART world!!");

for(;;)
{
// debug用にecho back
d = getReceivedData();
sendChar(d); // echo back
}

}