ATmega328pだけでUSBデバイスをつくる
Arduino UNOはUSBでパソコンと接続できるし、Arduino UNOに載っているマイコンはATmega328pだが、ATmega328pだけではUSBでパソコンとつなぐようなデバイスは作れない。Arduino UNOにはUSBドライバのためのハードウェアが別途搭載されていて、そのおかげでArduino UNOでは、直接はUSBをしゃべれないATMega328pのようなマイコンにUSBで気軽にプログラムを転送したり、USBでパソコンを操作するデバイスを開発したりできるようになっている。
Arduino UNOがないとUSBデバイスのマイコンとして使うのが大変そうなATmega328pだけど、実は、その上のソフトウェアとして仮想的にUSBプロトコルを実装したV-USBというファームウェアがある。これを導入すれば、ハードウェアを設計することなく、ATmega328pだけで動くUSBデバイスが作れてしまう。
最低限必要な工作
ハードウェアの設計がいらないといっても、マイコンである以上、どうしても避けられない部分はある。USBの場合は、供給される電圧は5V前後なのに対してデバイスから送り出すデータのプラス側、つまりUSBのD+の電圧を3.3Vにしないといけないので、その調整が必要になる。ATmega328pは5Vでも3.3Vでも動作するけど、パソコン側のUSBホストはD+は3.3Vであることを期待するので、5Vで返すとまずUSBデバイスとして認識してもらえない。
電圧の調整方法として、V-USBのサイトには2通りのアプローチが紹介されている。1つは供給される5Vの電圧を3.3Vに下げて回路の動作電圧も3.3Vにする方法。もうひとつは、D+を3.3Vに下げる方法。後者のほうが簡単なので、後者でいいとおもう。
USBケーブルからのデータ線をマイコンのどのポートに接続すればいいかは、 usbdrv/usbconfig.h
で設定する。D-をD4ポート、D+をD2ポートに接続する場合はこうなる(これがデフォルト)。
#define USB_CFG_IOPORTNAME D
#define USB_CFG_DMINUS_BIT 4
#define USB_CFG_DPLUS_BIT 2
デバイスの書き方
ざっくりとしたノリは、次のような main
関数を書くこと。
int main() {
usbInit();
usbDeviceConnect();
sei();
while(1) {
usbPoll();
// ここに書いた処理が、USBデバイスによる割り込み処理になる
}
return 0;
}
usbInit
は、usbconfig.h
をみていろいろ設定を決めるのに使われる。usbDeviceConnect
は、その設定に応じてホストとの接続を手当たりしだいに試して実行するためのマクロ。このあとで呼ばれている sei
という関数により割り込み処理が始まる("start enable interrupt" の略?)ので、usbPoll
という関数を定期的に呼び出しつつ、好きな処理を書けばいい(書かなくてもいい)。
ここに必ずしも処理を書かなくていいのは、ホストとやり取りするデータの構築や、そのデータを使ってポートをオンオフしたりする仕事は、usbFunctionSetup
という関数を定義することで実現するから。
usbFunctionSetup 関数の定義例
というわけで、main
はしばらく忘れて、 usbFunctionSetup
関数を定義していく。この関数は usbdrv/usbdrv.h
では次のように規定されている。
USB_PUBLIC usbMsgLen_t usbFunctionSetup(uchar data[8]);
引数の data
は、実はパソコンからUSBプロトコルの「SETUPトランザクション」としてやってくるデータを表していて、以下のように規定される usbRequest_t
型にキャストできるようになっている。
typedef struct usbRequest{
uchar bmRequestType;
uchar bRequest;
usbWord_t wValue;
usbWord_t wIndex;
usbWord_t wLength;
}usbRequest_t;
この定義だけ睨んでても各要素の意味がよくわからないんだけど、実はこれ、パソコン側のプログラムを書くときに使うことになるlibusbでデバイスにデータを送信するときに使う libusb_control_transfer
という関数の引数と比べるといい感じに対応している。つまりV-USB側では、「データを libusb_control_transfer
で送ったときに引数にしたやつと同じ名前の要素を usbRequest_t
から取り出せばいい」という理解でよさそう。
int libusb_control_transfer (libusb_device_handle * dev_handle,
uint8_t bmRequestType,
uint8_t bRequest,
uint16_t wValue,
uint16_t wIndex,
unsigned char * data,
uint16_t wLength,
unsigned int timeout
)
したがって、たとえば「libusbで libusb_control_transfer
の3つめの引数を使って送った値にもとづいてマイコンのポートB0をオンオフしたい!」なら、こんな感じに定義することになる。
USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) {
usbRequest_t *rq = (void*) data;
if(rq->bRequest == 1) {
PORTB = PORTB | (1 << PB0)
return 0;
} else {
PORTB &= ~1;
return 0;
}
}
実際にマイコンのポートB0にLEDをつないで、冒頭で触れたUSBケーブルとの接続をやってあげれば、これだけで「libusb_control_transfer
でLEDをオンにするUSBデバイス」が完成する。
コンパイル
コーディングに必要なものはV-USBのソースの usbdrv
ディレクトリにすべて揃っているので、そこにあるアセンブリとCのソースから usbdrv/usbdrv.o
と usbdrv/usbdrvasm.o
と usbdrv/oddebug.o
がコンパイルされるようにして、それらが自分のgcc-avrでプログラムにリンクされるようにすればいい。
まず、include
まわりを含めたコードの全体はこんな感じ。
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include "usbdrv/usbdrv.h"
#define F_CPU 16000000
#include <util/delay.h>
USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) {
usbRequest_t *rq = (void*) data;
if(rq->bRequest == 1) {
PORTB |= 1;
// same as
// PORTB = PORTB | 0b00000001
// PORTB = PORTB | (1 << PB0)
return 0;
} else {
PORTB &= ~1;
return 0;
}
}
int main() {
DDRB = 1; // setup port B0 as output
usbInit();
usbDeviceConnect();
sei();
while(1) {
usbPoll();
}
return 0;
}
上記では、前提として、 usbdrv
ディレクトリをそっくりこのソースと同じディレクトリに置き、avr-gcc
を実行するときにそのディレクトリを -I usbdrv
で指定するものとする。ATmega328pであれば、こんな感じのMakefileでいいはず。
CC = avr-gcc
OBJCOPY = avr-objcopy
CFLAGS = -Wall -Os -I usbdrv -mmcu=atmega328p
OBJFLAGS = -j .text -j .data -O ihex
OBJECTS = usbdrv/usbdrv.o usbdrv/oddebug.o usbdrv/usbdrvasm.o main.o
all: main.hex
%.hex: %.elf
$(OBJCOPY) $(OBJFLAGS) $< $@
main.elf: $(OBJECTS)
$(CC) $(CFLAGS) $(OBJECTS) -o $@
$(OBJECTS): usbdrv/usbconfig.h
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
%.o: %.S
$(CC) $(CFLAGS) -x assembler-with-cpp -c $< -o $@
libusbでクライアントを作る
libusbについては昔かいた記事のほうを参照。V-USBデバイスはデフォルトで名前が "Template"
になるので、それを見つけたらled_control
という関数で libusb_control_transfer
の3つめの引数をオンオフするだけ。
#include <stdio.h>
#include <string.h>
#include <libusb-1.0/libusb.h>
#include <unistd.h>
libusb_device **list;
libusb_device_handle *handle;
struct libusb_device_descriptor desc;
unsigned char buffer[512];
int cnt, i;
void led_control (int);
int main() {
libusb_init(NULL);
cnt = libusb_get_device_list(NULL, &list);
for (i = 0; i < cnt; i++) {
libusb_device *device = list[i];
libusb_get_device_descriptor(device, &desc);
if (libusb_open(device, &handle) == 0) {
libusb_get_string_descriptor_ascii(
handle, desc.iProduct, buffer, sizeof(buffer));
if (strcmp("Template", (char const *)buffer) == 0) {
led_control(1);
sleep(2);
led_control(0);
break;
}
libusb_close(handle);
}
}
libusb_free_device_list(list, 1);
libusb_exit(NULL);
return 0;
}
void led_control (int on_off) {
libusb_control_transfer(handle,
LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE | LIBUSB_ENDPOINT_IN,
on_off, 0, 0, buffer, sizeof(buffer), 5000);
}
デバイスからホストに対するアクションを送るパターン
ここまでで紹介したのは、ホスト、つまりパソコンからデバイスの挙動をコントロールするタイプ。反対に、デバイスを操作してパソコンでなにかのアクションが発生するやつを作りたいこともある。基本的には同じ考え方でV-USB側とlibusb側をプログラムすればいいんだけど、この場合は、デバイスからホストに伝えたい情報をデバイス側のバッファにためて、その場所を usbMsgPtr
という名前のポインタで指してあげると、その情報をホストで取り出して使えるようになっている。下記は、スイッチを押すとパソコンのコンソールに文字を表示するというプロジェクトの例。
Discussion