การจำลองบอร์ด MCU เป็นอุปกรณ์ USB
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้
ใช้งานเป็น custom class device ซึ่งเป็นประเภทอุปกรณ์...
ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ V-USB (หรือ AVRUSB) ที่พัฒนาโดยบริษัท Objective Development
เนื้อหา
ขั้นตอนการใช้งานไลบรารี V-USB
- ดาวน์โหลดซอร์สโค้ดจาก [1]
- แตกไฟล์ .tar.gz ที่ดาวน์โหลดมาโดยใช้คำสั่ง
tar zxf vusb-20090822.tar.gz
- คัดลอกไดเรคตอรี
usbdrvที่อยู่ในไดเรคตอรีvusb-2009-0822ไปวางในไดเรคตอรีโปรเจ็คของตน - ภายในไดเรคตอรีโปรเจ็คของตนเอง ย้ายไฟล์
usbconfig-prototype.hมาวางไว้ด้านนอก และเปลี่ยนชื่อให้เป็นusbconfig.h
mv usbdrv/usbconfig-prototype.h ../usbconfig.h
- ไฟล์นี้จะเก็บข้อมูลการตั้งค่าเกี่ยวกับอุปกรณ์ USB ที่จะให้ไมโครคอนโทรลเลอร์จำลองตัวเองขึ้นมา
- ในไฟล์หลักของโปรเจ็ค เรียกใช้คำสั่ง
#includeต่อไปนี้ไว้ที่ตอนต้นของโปรแกรม
#include <avr/io.h> #include <avr/interrupt.h> /* for sei() */ #include <util/delay.h> /* for _delay_ms() */ #include <avr/pgmspace.h> /* required by usbdrv.h */ #include "usbdrv.h"
- ในส่วนของฟังก์ชัน
main()ต้องมีโครงสร้างหลักดังนี้ (สามารถใส่โค้ดอื่นเพิ่มได้ตามที่ต้องการ)
int main()
{
usbInit();
usbDeviceDisconnect();
_delay_ms(300); /* fake USB disconnect for > 250 ms */
usbDeviceConnect();
sei(); /* enable interrupts */
while (1) /* main event loop */
{
usbPoll(); /* คำสั่งนี้ต้องถูกเรียกอย่างน้อยที่สุดทุก ๆ 50ms */
}
return 0;
}
- นิยามฟังก์ชัน
usbFunctionSetupเพื่อประมวลผลข้อมูลที่รับมาจากเครื่องคอมพิวเตอร์ผ่านทางพอร์ท USB
usbMsgLen_t usbFunctionSetup(uchar data[8])
{
:
}
- ฟังก์ชันนี้จะถูกเรียกทำงานโดยอัตโนมัติจากฟังก์ชัน
usbPollเมื่อทางฝั่งคอมพิวเตอร์ส่งคำสั่งผ่านมาทางพอร์ท USB
การคอมไพล์และลิ้งค์โปรแกรมรวมกับ V-USB
สร้าง Makefile ต่อไปนี้เพื่อคอมไพล์โปรแกรมและดำเนินการลิ้งค์เข้ากับไลบรารี V-USB ซึ่งตัวอย่าง Makefile นี้สมมติว่าไฟล์หลักของโปรเจ็คคือ main.c
MCU=atmega168
F_CPU=16000000L
TARGET=main.hex
OBJS=usbdrv/usbdrv.o usbdrv/usbdrvasm.o
CFLAGS=-Wall -Os -DF_CPU=$(F_CPU) -Iusbdrv -I. -mmcu=$(MCU)
all: $(TARGET)
flash: $(TARGET)
avrdude -p atmega168 -c usbasp -U flash:w:$(TARGET)
%.hex: %.elf
avr-objcopy -j .text -O ihex $< $@
%.elf: %.o $(OBJS)
avr-gcc $(CFLAGS) -o $@ $?
%.o: %.c
avr-gcc -c $(CFLAGS) $<
%.o: %.S
avr-gcc $(CFLAGS) -x assembler-with-cpp -c -o $@ $<
clean:
rm -f $(OBJS)
rm -f $(TARGET)
rm -f *~
สังเกตดูในกฎการสร้างไฟล์ main.elf ขึ้นมาจาก main.o บรรทัดที่มีการเรียกใช้ avr-gcc ได้มีการนำเอา $(OBJS)$ (ซึ่งได้แก่ไฟล์
usbdrv/usbdrv.o และ usbdrv/usbdrvasm.o) ลิ้งค์รวมเข้าไปด้วย ในที่นี้ตัวแปรพิเศษ $@ แทน target ซึ่งหมายถึง
main.elf ส่วนตัวแปรพิเศษ $? แทนรายการของ dependency ทั้งหมด
โครงสร้างของคำสั่ง USB ที่รับจากฝั่งคอมพิวเตอร์
ในสถาปัตยกรรม USB นั้นการสื่อสารจะถูกเริ่มจากฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ไปยังฝั่งอุปกรณ์ USB เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม
ข้อมูลคำสั่งที่ถูกส่งจากโฮสท์ เรียกว่า USB Request มีโครงสร้างตามที่นิยามไว้ในไฟล์ usbdrv/usbdrv.h ดังนี้
typedef struct usbRequest{
uchar bmRequestType; /* 1 ไบท์ */
uchar bRequest; /* 1 ไบท์ */
usbWord_t wValue; /* 2 ไบท์ */
usbWord_t wIndex; /* 2 ไบท์ */
usbWord_t wLength; /* 2 ไบท์ */
}usbRequest_t;
bmRequestTypeประกอบด้วยฟิลด์ย่อย 3 ฟิลด์ดังต่อไปนี้
- บิต 7 ทิศทางการส่งข้อมูล (Data Phase Transfer Direction)
- 0 = จากคอมพิวเตอร์ไปอุปกรณ์ USB (Host to Device)
- 1 = จากอุปกรณ์ USB มายังคอมพิวเตอร์ (Device to Host)
- บิต 6..5 ประเภทคำสั่ง (Type)
- 0 = Standard
- 1 = Class
- 2 = Vendor
- บิต 4..0 ผู้รับ (Recipient)
- 0 = Device
- 1 = Interface
- 2 = Endpoint
- 3 = Other
bRequestระบุหมายเลขคำสั่ง ซึ่งต้องถูกออกแบบไว้ล่วงหน้าแล้วว่าอุปกรณ์ USB ของเราจะรองรับคำสั่งอะไรบ้าง โดยเฟิร์มแวร์ต้องประมวลผลคำสั่งเหล่านี้ได้ถูกต้องwValueและwIndexwLengthกำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ USB หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์
อุปกรณ์ USB
ตัวอย่างโปรแกรม
เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น เราลองสร้างตัวอย่างเฟิร์มแวร์อย่างง่ายขึ้นมาพร้อมทั้งโปรแกรมที่ทดลองสั่งงานจากฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ด้วยภาษาไพธอน ในสถาปัตยกรรม USB นั้นการสื่อสารจะถูกกำหนดโดยฝั่งโฮสท์เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ดังนั้นหน้าที่ของเฟิร์มแวร์คือตีความคำสั่งที่ส่งมาจากโฮสท์และเตรียมข้อมูลที่จะส่งกลับไปให้โฮสท์
เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำสั่ง ดังนี้
- คำสั่ง SET_LED สั่งให้ LED ดวงที่ระบุติดหรือดับ มีรายละเอียดของคำสั่งดังนี้
- กำหนดให้หมายเลขคำสั่ง (request number) คือ 0
- การใช้งานจะระบุให้อุปกรณ์ USB เป็นเอาท์พุท
- ส่งพารามิเตอร์มาให้ 2 ไบท์ ไบท์แรกระบุตำแหน่งของ LED (0, 1 หรือ 2) ส่วนไบท์ที่สองระบุว่าจะให้ LED ดวงดังกล่าวติด (หากมีค่า 0) หรือดับ (หากมีค่าอื่นที่ไม่ใช่ศูนย์)
- คำสั่ง GET_SWITCH สั่งให้บอร์ด MCU รายงานสถานะการกดปุ่มสวิตช์กลับมา มีรายละเอียดของคำสั่งดังนี้
- กำหนดให้หมายเลขคำสั่ง (request number) คือ 1
- การใช้งานจะระบุให้อุปกรณ์ USB เป็นอินพุท
- ไม่มีพารามิเตอร์ใดส่งไป แต่ตัวบอร์ด MCU ส่งรายงานกลับมา 1 ไบท์ บอกสถานะของสวิตช์ (0 คือไม่ถูกกด 1 คือถูกกด)
การตั้งค่าให้อุปกรณ์ USB
- อุปกรณ์ USB ทุกตัวจะต้องมีการระบุค่า Vendor ID (VID) และ Product ID (PID) อย่างละ 16 บิต
ซึ่งไม่สามารถตั้งเองได้ตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกโปรแกรมไดรเวอร์ที่จะมาควบคุมอุปกรณ์ USB http://www.voti.nl/docs/usb-pid.html ตั้งค่า VID ให้เป็น 0x16c0
โปรแกรมฝั่งเฟิร์มแวร์
เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำสั่งเหล่านี้ไว้ที่ส่วนหัวของ main.c ดังนี้
#define SET_LED 0 #define GET_SWITCH 1
ภายในฟังก์ชัน usbFunctionSetup สร้างโค้ดสำหรับประมวลผลคำสั่งที่รับมาจากโฮสท์ดังนี้
usbMsgLen_t usbFunctionSetup(uchar data[8])
{
usbRequest_t *rq = (void *)data;
static uchar dataBuffer[4]; /* buffer must stay valid when usbFunctionSetup returns */
if(rq->bRequest == SET_LED)
{
dataBuffer[0] = rq->wValue.bytes[0];
dataBuffer[1] = rq->wValue.bytes[1];
dataBuffer[2] = rq->wIndex.bytes[0];
dataBuffer[3] = rq->wIndex.bytes[1];
usbMsgPtr = dataBuffer; /* tell the driver which data to return */
return 4;
}else if(rq->bRequest == CUSTOM_RQ_SET_STATUS){
if(rq->wValue.bytes[0] & 1){ /* set LED */
LED_PORT_OUTPUT |= _BV(LED_BIT);
}else{ /* clear LED */
LED_PORT_OUTPUT &= ~_BV(LED_BIT);
}
}else if(rq->bRequest == CUSTOM_RQ_GET_STATUS){
dataBuffer[0] = ((LED_PORT_OUTPUT & _BV(LED_BIT)) != 0);
usbMsgPtr = dataBuffer; /* tell the driver which data to return */
return 1; /* tell the driver to send 1 byte */
}
return 0; /* default for not implemented requests: return no data back to host */
}
การโปรแกรมฝั่งเครื่องคอมพิวเตอร์
ติดตั้ง PyUSB เพื่อให้ติดต่อกับอุปกรณ์ USB โดยใช้ภาษาไพธอนได้
sudo apt-get install python-usb