การจำลองบอร์ด 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
และwIndex
wLength
กำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ 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