PIC16F1455にはUSBデバイスモジュールが搭載されている一番安価?なPICマイコンで使いやすいです。これを使って、シリアル通信(UART)をUSB変換するモジュールを自作してみます。
USBシリアル変換モジュールはAmazonでいくらでも安いものがあります。買ったほうが早いのは分かっていますが、通信の基礎でもあるし、USBとシリアル通信の基礎に触れておけば、ほかにも応用が利くかもという期待も込めています。
PIC16F1455でUSBシリアル変換モジュールの回路設計
UARTとUSB、それと送受信時のLEDを光らせるようにしています。また送信時の電圧は市販品に合わせて3.3VにするためGPIOの5Vを分圧しています。
最終的にはBluetoothモジュールのHC-05、HC-06を接続することを考えているので、シリアルインタフェースの5番ピンには3.3V電圧を印加しており、ジャンパでON/OFFできるようにしています。HC-05とか限定ならレギュレータ使う意味はなかったかも。3.3V駆動のセンサーに電源供給する場合は使えます。
※R5とR6は実際には送信時にしか点灯しないので、もう少し抵抗値を1/10ぐらいにしたほうが良いです。上図の通りだと暗すぎました。
今回はプリント基板ではなく、ユニバーサル基板を使うので、PCBレイアウトエディタでは部品に配置イメージを検討するだけに留めています。手配線するときに間違えないようにする役割があります。(それでも間違えるけど)
完成予想イメージ
実際に作成したモジュールが以下になります。予想イメージとはやはりズレますが、大体イメージ通りです。KiCADでもユニバーサル基板を使ったレイアウトイメージの検討ができるとありがたいですが、追加レイヤに2.54mmピッチでスルーホールの絵を並べるしかないのかもしれないです。そういった標準機能がほしいです。
MPLABX IDEとMCC(MPLAB Code Configurator)使った開発
まずMCCでUARTとUSBのデバイスを追加します。
USBはCDCデバイスを選択する。注意点としては、VendorやProduct IDはデフォルトのまま変更しません。ベンダIDの0x4D8
は、Microchip社がWindows向けに正式に承認されたベンダ番号になります。このIDに対応したドライバがMicrochip社から正式に提供されているのでこれをそのまま使います。
もし、製品開発する場合であれば、Microchip社からサブベンダとして安いライセンス料で認可してもらうプログラムがありますが、今回は実験目的なのでデフォルトのまま使います。
UARTの設定が以下になります。最終的にはAuto-Band Detection機能をONしてボーレートを可変にしますが、とりあえず固定で行きます。
MCCプラグインのGenerateボタンをクリックして、上記設定からコードを生成します。mcc_generated_files\usb\utilities
フォルダには、上述したWindows向けのUSBドライバもコピーされます。これでPICを使ったUSBデバイスが接続されたときに自動的にMicrochip社から提供されたドライバがロードされてUSB通信できるはずです。
infファイルから右クリックメニューを表示してインストールを実行しておきます。
USB通信できるか確認する
USBの送信ポートに直接文字を書き込んで、Windows側から文字が受信できるか確認
まずは、USBの送信ポートに直接文字を書き込んで、Windows側から文字が受信できるか確認してみます。(UARTはまだこの時点では関係ないです)
/**
main.c
*/
#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/usb/usb.h"
// USB送受信バッファ
#define USB_RS232C_BUFFER_SIZE 64
static uint8_t usb_put_data[USB_RS232C_BUFFER_SIZE];
// USB送信ポート書き込みテスト
void USBSerialTest(void){
if( USBGetDeviceState() < CONFIGURED_STATE ||
USBIsDeviceSuspended()== true ){
return;
}
// "test"固定文字をUSB送信する。送信時にRC2に接続されたLEDをONする
usb_put_data[0] = 't';
usb_put_data[1] = 'e';
usb_put_data[2] = 's';
usb_put_data[3] = 't';
usb_put_data[4] = ' ';
if( USBUSARTIsTxTrfReady() ){
IO_RC2_SetHigh();
putUSBUSART( usb_put_data, 5 );
}
// transmit usb buffer
CDCTxService();
}
void main(void)
{
SYSTEM_Initialize();
INTERRUPT_GlobalInterruptEnable();
INTERRUPT_PeripheralInterruptEnable();
IO_RC2_SetLow();
IO_RC3_SetLow();
// 0.1sec毎にUSBSerialTest()をコールする
while(1){
IO_RC2_SetLow();
__delay_ms(100);
USBSerialTest();
}
return;
}
上記プログラムを書き込んだら、作成したモジュールをWindowsのPCに接続します。接続すると下図のようにデバイスドライバ一覧いCOMポートが追加されます。(自身の環境だとCOM6になっている)
TeraTermを起動し、PICのシリアルポートを選択します。(※上図と整合性取れてないですが、このときはCOM7でした)
0.1secごとに"test"の文字が延々出力されればとりあえず動作していると思います。
入力文字をUSBから受信した後、文字変換してUSBポートへ送信する
次にUSBポートから文字を受信して、その文字を変換して返します。
#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/usb/usb.h"
#define USB_RS232C_BUFFER_SIZE 64
static uint8_t usb_put_data[USB_RS232C_BUFFER_SIZE];
uint8_t usb_put_data_idx = 0;
uint8_t usb_put_data_size = 0;
uint8_t usb_put_data_ready = 0;
void USBSerialTest2(void){
if( USBGetDeviceState() < CONFIGURED_STATE ||
USBIsDeviceSuspended()== true ){
return;
}
//----------------------------------------
// USB (get) => usb_put_data
//----------------------------------------
if (usb_put_data_ready == 0){
usb_put_data_size = getsUSBUSART( usb_put_data, USB_RS232C_BUFFER_SIZE );
if(usb_put_data_size > 0){
for(int i = 0; i < usb_put_data_size; i++ ){
usb_put_data[i] = usb_put_data[i] + 1;
}
usb_put_data_ready = 1;
}
}
//----------------------------------------
// usb_put_data => USB (put)
//----------------------------------------
if( usb_put_data_ready == 1 && USBUSARTIsTxTrfReady() ){
IO_RC2_SetHigh();
putUSBUSART( usb_put_data, usb_put_data_size );
usb_put_data_ready = 0;
}
// transmit usb buffer
CDCTxService();
}
void main(void)
{
SYSTEM_Initialize();
INTERRUPT_GlobalInterruptEnable();
INTERRUPT_PeripheralInterruptEnable();
IO_RC2_SetLow();
IO_RC3_SetLow();
while(1){
IO_RC2_SetLow();
USBSerialTest2();
}
return;
}
"a"を入力すると、アスキーコード+1した"b"が出力される。
分かりづらいけど、ほかの文字も同様に+1して出力される。(in out in out ...)
USB+EUSART通信でUSBシリアル変換モジュールを作成する
市販のUSBシリアル変換モジュールと作成したモジュールを接続して、2つのモジュール間でデータ通信を行ってみる。自身の環境では以下のような構成になる。
うまく作成できれば、2つのTeraTermを起動してそれぞれ2つのCOMポートに接続した後、お互いのウィンドウから文字入力を行うと、もう片方のウィンドウに文字が表示されるはずである。
とりあえず、ボーレートは19200でやってみる。(市販モジュールは自動的にボーレート設定される)
COM7およびCOM3のコンソールからそれぞれ適当な文字を入力した状態。同じ文字列を表示しているので一応うまく動作している模様。ボーレートはどちらも19200で設定している。
TeraTermのボーレートの設定が、2つのコンソールで値が違っていたり、19200以外の値にして入力すると、相手のコンソールに文字化けした値が出力される。
#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/usb/usb.h"
#define USB_RS232C_BUFFER_SIZE 64
static uint8_t rs232c_put_data[USB_RS232C_BUFFER_SIZE];
uint8_t rs232c_put_ready = 0;
uint8_t rs232c_put_size = 0;
uint8_t rs232c_put_idx = 0;
static uint8_t usb_put_data[USB_RS232C_BUFFER_SIZE];
uint8_t usb_put_idx = 0;
void USBSerialEmulator(void){
if( USBGetDeviceState() < CONFIGURED_STATE ||
USBIsDeviceSuspended()== true ){
return;
}
//----------------------------------------
// USB (get) => RS232C (TX)
//----------------------------------------
if (rs232c_put_ready == 0){
rs232c_put_size = getsUSBUSART( rs232c_put_data, USB_RS232C_BUFFER_SIZE );
if(rs232c_put_size > 0){
rs232c_put_ready = 1; // signal buffer full
rs232c_put_idx = 0; // Reset the current position
}
}
if( rs232c_put_ready == 1 && EUSART_is_tx_ready() ){
// write 1byte
EUSART_Write( rs232c_put_data[ rs232c_put_idx ] );
// next pointer
rs232c_put_idx++;
// end of write buffer ?
if( rs232c_put_idx == rs232c_put_size ){
rs232c_put_ready = 0;
}
}
//----------------------------------------
// RS232C(RX) => USB (put)
//----------------------------------------
if( usb_put_idx < ( USB_RS232C_BUFFER_SIZE - 1 ) && EUSART_is_rx_ready() ){
usb_put_data[ usb_put_idx ] = EUSART_Read();
usb_put_idx++;
usb_put_data[ usb_put_idx ] = 0;
}
if( 0 < usb_put_idx && USBUSARTIsTxTrfReady() ){
putUSBUSART( usb_put_data, usb_put_idx );
usb_put_idx = 0;
}
// transmit usb buffer
CDCTxService();
}
void main(void)
{
SYSTEM_Initialize();
INTERRUPT_GlobalInterruptEnable();
INTERRUPT_PeripheralInterruptEnable();
while(1){
USBSerialEmulator();
}
return;
}
Auto-Baud Detectionを試してみる
※最初に断っておくと、2019/05/05現在では、USBシリアル変換モジュールでの自動ボーレート設定はうまく動作できてないです。
まず、MCCでAuto-Baud Detectionをチェックすると、BAUDCONbits.ABDEN
がONになるけど起動した時点ではOFFになっていてMCCでONしても意味がない気がしている。
- 今のところ分かっていること
BAUDCONbits.ABDEN=1
になると、自動ボーレート設定モード状態になる。最初のデータ受信時から立ち上がりエッジを5個目までカウントする。その間のボーレート値カウントしてSPBRGにセットしている。カウントが終了すると、自動的にBAUDCONbits.ABDEN=0
になる。またカウントしたいときに左記をONすればいい。
http://akizukidenshi.com/download/ds/microchip/pic16(l)f1454_1455_1459.pdf
上記のp266「23.4.1 AUTO-BAUD DETECT」を参照
ところが、実際にやってみると、確かにSPBRG(SPBRGH、SPBRGL)の値が更新されるが、期待するような値になってくれないのである。メインループにUSB処理とかいろいろ入っているのでタイミングが合わないのか(そんなわけないと思うが)、とにかくダメである。
ネットで調べてもABDENを使って更新しているようなサンプルが出てこない。使えないからなのか誰も使ってないのか?と思ってしまう。Microchip社も「自動でボーレート設定できます!」と仕様書に書いておきながらサンプルすら用意しないのはどうかと思う。私みたいに色々試しては諦めている人も多い気がして勿体ない。
とりあえずSPBRGに正しいボーレートの値を直接設定すると、正しく動作することは確認しているので、コマンド形式で設定することにする。
- 48MHzクロック時のBaudRate設定
マニュアルには、目標速度と実際の計算速度の乖離が5%未満になるようにと記載がある。下表はUSBを使うためクロック設定が48MHz時のBaudRate設定値になる。SPBRGの値を上位(SPBRGH)、下位(SPBRGL)8bitに分けて整数値を設定すれば、確かにその速度で文字化けせずに通信可能だった。
SYNC | BRG16 | BRGH | 目標BaudRate[bps] | Fosc[Hz] | SPBRG | 計算BaudRate[bps] | Match[%] |
---|---|---|---|---|---|---|---|
0 | 1 | 1 | 2400 | 48000000 | 4999 | 2400 | 100 |
0 | 1 | 1 | 4800 | 48000000 | 2499 | 4800 | 100 |
0 | 1 | 1 | 9600 | 48000000 | 1249 | 9600 | 100 |
0 | 1 | 1 | 14400 | 48000000 | 832 | 14405.76 | 100.04 |
0 | 1 | 1 | 19200 | 48000000 | 624 | 19200 | 100 |
0 | 1 | 1 | 38400 | 48000000 | 312 | 38338.65 | 99.84 |
0 | 1 | 1 | 57600 | 48000000 | 207 | 57692.30 | 100.16 |
0 | 1 | 1 | 115200 | 48000000 | 103 | 115384.61 | 100.16 |
0 | 1 | 1 | 128000 | 48000000 | 93 | 127659.57 | 99.73 |
入力 | ASCIIコード |
---|---|
> | 0x3E |
改行 | 0x0A |