こんにちは、ENGかぴです。
PIC18F14K50 は USB モジュールが搭載されているため USB を使って通信することができますが、USB は MCC が非対応となっているため自作でレジスタの設定が必要です。USB を実装するための初期化についてまとめました。
PIC18F14K50 を実装した基板は AE-USB-I02.0-TYPEC(秋月電子)を使用しています。以下では PIC モジュールとします。
PIC モジュールを使って USB HID を実装する方法をまとめていきますが、本記事は PIC モジュールの外部クリスタルを使用してクロックを生成し、USB が挿入されたことを認識できるまでの初期化までを説明します。
MCC を使って動作確認したことを下記にまとめています。
PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ
MCC で各種機能を実装する
PIC モジュールは外部クリスタルとして 12MHz のクリスタル発振子や USB に関する部品が実装されています。PIC モジュールは PIC18F14K50 が使用されているため MCC を使ってボードの設定を行うことができます。
USB に関するインターフェースは PIC18F14K50 は未対応となっているためレジスタ操作などは自作して組み込む必要があります。
MCC でシステムクロックの設定を行う方法をまとめました。MCC の使い方については下記記事にまとめています。
MPLAB Code Configurator(MCC)の追加と使い方
紹介している記事は MCC Classic(現在サポート外で Melody が推奨される)を使用していますが、使い方はほぼ同じです。
Configuration Bits の設定

MPLAB X IDE を起動し MCC のアイコンをクリックして MCC を有効にします。初期の画面のインターフェースでは設定画面が右隅に表示されて見にくいのでスライドさせて大きく表示するようにしています。
外部のクリスタル発振子を使用する場合は Configuration Bits の CONFIG1H レジスタに関する設定を行う必要があります。Osillator Selection Bits から「HS oscillator」を選択すると Clock Control でシステムクロックが入力できるようになります。
USB クロックは 48MHz を使用するため 4X PLL Enable bit で「Oscillator multiplied by 4」を選択してシステムクロックを 4 逓倍します。
GPIO を DI にした状態でプルアップやプルダウンがない場合、ON, OFF を繰り返して動作する可能性があるため未使用ピンは DO ピンにすることをお勧めします。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
Clock Control の設定

Clock Control を選択し、Clock Source から FOSC を選択します。Configuration Bits で HS を選択している場合は External Clock に外部クリスタルの発信周波数が入力できるので 12,000,000 (12MHz) を入力します。
設定が終わったら左上隅の「Generate」をクリックすると MCC が設定した項目の初期化を含んだソースコードを自動で生成します。
システムクロックの動作確認

USB に使用するクロックは外部クリスタルクロックを PLL で 4 逓倍した 48MHz のクロックを使用するため DO を使ってシステムクロックが正確に生成できているかを確認しました。
int main(void)
{
SYSTEM_Initialize();
while(1)
{
IO_RC0_Toggle();
__delay_ms(2000);//PLLでメインクロックが4倍になっているので×4の値
}
}
上のソースコードで RC0 ピンの DO を 500ms 毎にトグル出力した波形を測定しました。MCC が生成した__delay_ms() 用のクロックの定義は外部クリスタルの周波数 (12MHz) を元に生成されていますが、実際は PLL で 4 逓倍になっているので 4 倍した値を指定して遅延を持たせています。
オシロスコープで確認すると遅延時間に合わせて 500ms のタイミングが生成できているため、システムクロックが 48MHz で動作していることが確認できました。
USB の初期化(レジスタ操作)
以下は、USB モジュールを動作させるための最低限の初期化処理です。システムクロックの 48MHz が生成できている前提で、USB モジュールを有効化し、EP0/EP1 を使える状態にします。
UCON = 0;
UIR = 0;
UIE = 0;
UADDR = 0;
UCFG = 0x14; // FSEN=1, UPUEN=1
UEP0 = 0x16; // EPHSHK=1, EPOUTEN=1, EPINEN=1
UEP1 = 0x16; // EPHSHK=1, EPINEN=1, EPINEN=1
UCONbits.USBEN = 1;
__delay_ms(10);
例に従ってレジスタを設定し、USB モジュールの初期化、エンドポイントの設定、デバイスアドレスの初期化を行っています。
1~3行目は USB モジュールを使う前に制御レジスタ(UCON)・割り込みフラグ(UIR)・割り込み許可(UIE)・USB アドレス (UADDR) をすべてクリアしています。
USB を有効にする前に UCFG レジスタ、UEP0 レジスタ、UEP1 レジスタの設定を行います。
UCFG レジスタの設定

UCFG は USB の動作モードを設定するレジスタです。ここでは UPUEN ビットと FSEN ビットをセットします。
- UPUEN = 1 → D+ ラインの内部プルアップ抵抗を有効化する
- FSEN = 1 → フルスピード(48Mbps)で動作
UPUEN により D+ ラインのプルアップが有効になります。有効になると PC がUSB デバイスが挿入されたことを検出して接続音が鳴るようになります。外部に抵抗を実装している場合はこの設定は不要です。
FSEN ビットは USB の通信速度を指定します。この PIC マイコンは Full-speed と Low-speed に対応していますが、本記事では Full-speed を指定しています。
UPUEN ビットと FSEN ビットをセットするので UCFG = 0x14 になります。
PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール
UEP0/UEP1 の設定

UEPn レジスタは USB の各エンドポイント(EP0~EP7)の動作を設定するレジスタです。IN/OUT の方向、ハンドシェイク、制御転送の可否、STALL 応答などを制御します。
HID デバイスでは EP0 を初期設定(ディスクリプタ応答など)の制御転送用に、EP1 をデータ転送用に使用するため、EP0 は IN/OUT 両方を有効化し、EP1 は必要に応じて IN/OUT を有効化します。
- EPHSHK = 1 → ハンドシェイク行う
- EPOUTEN = 1 → OUT パケットを受信できる
- EPINEN = 1 → IN パケットを送信できる
- EPSTALL= 1 → STALL応答(エラー応答)
EPHSHK ビットは PC と通信する際のハンドシェークを行います。ハンドシェークを行うと PICマイコンと PC 間で確実なデータの転送ができるようになります。HID 通信を行うためのディスクリプタやレポートを確実に送る必要があるためハンドシェークを使用することが推奨されます。
例では EP0 と EP1 をどちらも 0x16(EPHSHK=1、EPOUTEN=1、 EPINEN=1) に設定し、双方向通信ができるようにしています。
UCON レジスタの設定

UCON レジスタは USB モジュールの動作状態を制御するレジスタです。USB の有効化(USBEN)、サスペンド制御(SUSPND)、パケット受信禁止(PKTDIS)などの基本機能が含まれています。
- USBEN = 1 → USB モジュール動作開始
- PKTDIS = 1 → 新規パケット受信禁止
PKTDIS ビットは新しいパケットを受信しないようにするためのビットで、EP0 の SETUP 処理などで一時的に受信を止めたい場合(応答する際の送信優先にする)に使用します。
USBEN を1 にするとUCFG レジスタやUEP0/UEP1 レジスタで設定した条件で USB モジュールが有効化され、PC がデバイスを検出できる状態になります。動作開始時は不安定な状態になることがあるため10ms のウェイトを入れて安定させています。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
BDT(Buffer Descriptor Table)の準備

USB モジュールは、受信(OUT)と送信(IN)のバッファを管理するために BDT(Buffer Descriptor Table) を使用します。
BDT は RAM 上に配置される構造体で、各エンドポイントの IN/OUT バッファの状態を管理します。
PIC18F14K50 の USB モジュールでは、各エンドポイントに対して BDT が必要です:
例では EP0 の OUT/IN、EP1 の OUT/IN を使用するため 4 つの BDT を準備します。BDT の構成は以下の通りです。
typedef struct {
uint8_t STAT;
uint8_t CNT;
uint8_t ADDR_L;
uint8_t ADDR_H;
} BDT_ENTRY;
STAT はエンドポイントの状態を示すもので受信/送信で同じものを参照します。CNT はバッファサイズで ADDR はエンドポイント用の送受信データを格納する RAM アドレスになります。
PIC18F14K50 の USB モジュールは、BDT を 特定の RAM アドレス範囲に置く必要があります。アドレスが 200h~280h までが BDT 及び USB Data or User Data、280h~2FFh までが USB Data or User Data の範囲になります。
BDT は 200h~280h の範囲に配置する必要がありますが、ユーザーが受信/送信するデータは任意に配置することができます。以下は配置の例です。
// BDTを0x200に配置
volatile BDT_ENTRY bdt[16] __at(0x200);
uint8_t ep0_out_buffer[8] __at(0x280);
uint8_t ep0_in_buffer[8] __at(0x288);
uint8_t ep1_out_buffer[32] __at(0x290);
uint8_t ep1_in_buffer[32] __at(0x2B0);
宣言した変数の直下に __at(0x200) をつけることで XC8 コンパイラーにおいて RAM の領域を指定することができます。BDT に登録した情報を使って USB モジュールがデータの送受信を行います。
bdt[EP0_OUT].ADDR_L = (uint8_t)((uint16_t)ep0_out_buffer);
bdt[EP0_OUT].ADDR_H = (uint8_t)(((uint16_t)ep0_out_buffer) >> 8);
bdt[EP0_IN].ADDR_L = (uint8_t)((uint16_t)ep0_in_buffer);
bdt[EP0_IN].ADDR_H = (uint8_t)(((uint16_t)ep0_in_buffer) >> 8);
EP0 で使用する ep0_ou_buffer、ep0_in_buffer のアドレスの登録は 16 ビットのアドレスを 8 ビットのデータに分けて登録します。
USB モジュール(SIE)はこのアドレスを参照して OUT パケットを RAM(ep0_out_buffer[]) に書き込みます。IN パケットの場合は ep0_in_buffer[] の内容を送信します。
BDT の STAT は SIE モードと CPU モードで同じ場所を参照しますがモードによってレジスタの意味合いが異なります。
SIE モードは USB モジュールが所有権を持つ状態で、PC からのリクエストを待機している状態です。
CPU モードは PIC マイコンが所有権を持つ状態で、BDT を操作してデータを送信する準備している状態です。
PR:キャリア形成でお悩みのあなたへ!初回無料相談で相談できる[coachee]
BDT の SIE モードの初期化

BDnSTAT は、USB モジュールが各バッファの状態を管理するためのステータスレジスタです。SIE モードは PC からの受信待ちをどのように条件で行うかを設定します。
- UOWN = 1 → SIE が所有権を持つ(OUTパケットの受信待機)
- PID は SIE がリクエストを受けると更新する
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _UOWN;
CNT は受信するバイトの数を指定しますが、ep0_out_buffer[] のサイズを越えないように指定します。USB 専用の RAM 領域は範囲が狭いためエンドポイントの数や受信/送信の数によって調整が必要になります。
この設定により、USB モジュールは EP0 OUT に届く SETUP パケットを受信できる状態になります。
BDT の CPU モードの初期化

CPU モードは USB モジュールにデータを送信する条件を設定します。USB モジュール(SIE)にバッファの所有権を渡したり、DATA0/1 の指定を行ったり、STALL 応答を設定するなど、USB 転送の制御に直接関わります。
- UOWN = 0 → CPU が所有権を持つ( IN パケットの送信待機)
- DTS = 0 → Data0 パケット(USB のデータトグル同期に使用される)
- DTSEN = 0 → SETUP パケットは例外的に同期が無視されるため 0
- BSTALL = 0 → 通常動作(STALL 応答)を使用しない
bdt[EP0_IN].STAT = 0;
EP0 は SETUP パケットなど初期設定の制御転送用に使用するため DTSEN はセットする必要がないため 0 にしています。
EP0 IN はリクエストを受けてから送信するデータが決まるので、初期化時は STAT を 0 にして CPU が所有権を持った状態にしておきます。CNT も同様に送信するデータのサイズによって後で設定します。
USB モジュールに送信させるタイミングで、 後から UOWN を立てて所有権を USB に渡すことになります。
動作確認
PIC モジュールにソフトを書き込んで USB を接続します。通常動作では PC と PICマイコンの間で SETUP のやり取りをしますが、USB モジュールを有効にしているだけなので応答しません。
PC が数回リトライしながら応答を確認しますが、一定の回数を経過すると通信をあきらめて「不明な USB デバイス(デバイス記述子要求の失敗)」としてデバイスマネージャーに表示します。

PC が何かしらの USB が挿入されたことを認識しているので USB クロックなどハード周りの設定がうまくいっていることが確認できました。
ディスクリプタや HID 用のコンフィグレーションの通信を実装すると「HID 準拠ベンダー定義デバイス」として認識されることができます。
PR:外資系・IT業界などハイクラスの転職に強い【AXIS Agent】
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
main.c のソースコード:
#include "mcc_generated_files/system/system.h"
#include "usbmain.h"
/*
Main application
*/
int main(void)
{
SYSTEM_Initialize();
USB_Init(); // USB 初期化
while(1)
{
//IO_RC0_Toggle();
//__delay_ms(2000);//PLLでメインクロックが4倍になっているので×4の値
//USB_Task(); // USB 処理ループ
}
}
IO_RC0_Toggle() と__delay_ms(2000) のコメントアウトを外すとクロックの波形が確認できます。
usbmain.c のソースコード:
#include "mcc_generated_files/system/system.h"
#include "usbmain.h"
typedef struct {
uint8_t STAT;
uint8_t CNT;
uint8_t ADDR_L;
uint8_t ADDR_H;
} BDT_ENTRY;
// BDTを0x200に配置
volatile BDT_ENTRY bdt[16] __at(0x200);
// バッファを少し離して配置 (安全のため0x280から開始)
uint8_t ep0_out_buffer[8] __at(0x280);
uint8_t ep0_in_buffer[8] __at(0x288);
uint8_t ep1_out_buffer[32] __at(0x290);
uint8_t ep1_in_buffer[32] __at(0x2B0);
#define EP0_OUT 0
#define EP0_IN 1
#define EP1_OUT 2
#define EP1_IN 3
#define _UOWN 0x80
#define _DTS 0x40
#define _DTSEN 0x08
#define _BSTALL 0x04
#define _DATA0 (_DTSEN)
#define _DATA1 (_DTS | _DTSEN)
void USB_Init(void) {
UCON = 0;
UIR = 0;
UIE = 0;
UADDR = 0;
UCFG = 0x14; // FSEN=1, UPUEN=1
UEP0 = 0x16; // EPHSHK=1, EPOUTEN=1, EPINEN=1
UEP1 = 0x16; // EPHSHK=1, EPINEN=1, EPINEN=1
UCONbits.USBEN = 1;
__delay_ms(10); // 念のためのウェイト
for(uint8_t i = 0; i < 16; i++)
{
bdt[i].STAT = 0;
bdt[i].CNT = 0;
bdt[i].ADDR_L = 0;
bdt[i].ADDR_H = 0;
}
bdt[EP0_OUT].ADDR_L = (uint8_t)((uint16_t)ep0_out_buffer);
bdt[EP0_OUT].ADDR_H = (uint8_t)(((uint16_t)ep0_out_buffer) >> 8);
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _UOWN;
bdt[EP0_IN].ADDR_L = (uint8_t)((uint16_t)ep0_in_buffer);
bdt[EP0_IN].ADDR_H = (uint8_t)(((uint16_t)ep0_in_buffer) >> 8);
bdt[EP0_IN].STAT = 0;
// EP1 OUT 初期化
bdt[EP1_OUT].ADDR_L = (uint8_t)((uint16_t)ep1_out_buffer);
bdt[EP1_OUT].ADDR_H = (uint8_t)(((uint16_t)ep1_out_buffer) >> 8);
bdt[EP1_OUT].CNT = 32;
bdt[EP1_OUT].STAT = _UOWN;
bdt[EP1_IN].ADDR_L = (uint8_t)((uint16_t)ep1_in_buffer);
bdt[EP1_IN].ADDR_H = (uint8_t)(((uint16_t)ep1_in_buffer) >> 8);
bdt[EP1_IN].CNT = 32;
bdt[EP1_IN].STAT = 0; // 最初はCPU所有
UIEbits.TRNIE = 1; // トランザクション割り込みを許可
UIEbits.URSTIE = 1; // リセット割り込みを許可
PIR2bits.USBIF = 0; // USB割り込みフラグをクリア
PIE2bits.USBIE = 1; // 周辺機能割り込み(USB)を有効化
}
void USB_Task(void)
{
//EP0でSETUPの処理を実装
}
usbmain.h のソースコード:
#ifndef USBMAIN_H
#define USBMAIN_H
void USB_Init(void);
void USB_Task(void);
#endif
usbmain.c と usbmain.h をプロジェクトに追加する必要があります。
本ソースコードは MPLAB X IDE に MCC のプラグインをインストールしていることが前提となります。MCC をインストールする方法は下記記事を参考にしてください。
MPLAB Code Configurator(MCC)の追加と使い方
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
最後まで、読んでいただきありがとうございました。
