ผลต่างระหว่างรุ่นของ "การจำลองบอร์ด MCU เป็นอุปกรณ์ USB"
Chaiporn (คุย | มีส่วนร่วม) |
Chaiporn (คุย | มีส่วนร่วม) |
||
แถว 1: | แถว 1: | ||
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้ | ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้ | ||
− | |||
− | ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ [http://www.obdev.at/products/vusb/index.html V-USB] (หรือ AVRUSB) ที่พัฒนาโดยบริษัท [http://www.obdev.at/index.html Objective Development] | + | ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ [http://www.obdev.at/products/vusb/index.html V-USB] (หรือ AVRUSB) ที่พัฒนาโดยบริษัท [http://www.obdev.at/index.html Objective Development] โดยทำให้บอร์ด MCU ของเราทำงานเป็นอุปกรณ์ที่อยู่ในกลุ่ม [http://vusb.wikidot.com/usb-device-classes custom class device] ซึ่งจัดเป็นอุปกรณ์ USB ที่ไม่สังกัดคลาสใด โดยซอฟต์แวร์ฝั่งคอมพิวเตอร์จะอยู่ภายใต้ความควบคุมของเราทั้งหมด |
== ขั้นตอนการใช้งานไลบรารี V-USB == | == ขั้นตอนการใช้งานไลบรารี V-USB == | ||
แถว 38: | แถว 37: | ||
: | : | ||
} | } | ||
− | :ฟังก์ชันนี้จะถูกเรียกทำงานโดยอัตโนมัติจากฟังก์ชัน <code>usbPoll</code> | + | :ฟังก์ชันนี้จะถูกเรียกทำงานโดยอัตโนมัติจากฟังก์ชัน <code>usbPoll</code> เมื่อทางฝั่งคอมพิวเตอร์ส่งคำร้องขอผ่านมาทางพอร์ท USB |
== การคอมไพล์และลิ้งค์โปรแกรมรวมกับ V-USB == | == การคอมไพล์และลิ้งค์โปรแกรมรวมกับ V-USB == | ||
แถว 70: | แถว 69: | ||
rm -f *~ | rm -f *~ | ||
− | สังเกตดูในกฎการสร้างไฟล์ <code>main.elf</code> ขึ้นมาจาก <code>main.o</code> บรรทัดที่มีการเรียกใช้ <code>avr-gcc</code> ได้มีการนำเอา <code>$(OBJS)$</code> (ซึ่งได้แก่ไฟล์ | + | สังเกตดูในกฎการสร้างไฟล์ <code>main.elf</code> ขึ้นมาจาก <code>main.o</code> บรรทัดที่มีการเรียกใช้ <code>avr-gcc</code> ได้มีการนำเอา <code>$(OBJS)$</code> (ซึ่งได้แก่ไฟล์ <code>usbdrv/usbdrv.o</code> และ <code>usbdrv/usbdrvasm.o</code>) ลิ้งค์รวมเข้าไปด้วย ในที่นี้ตัวแปรพิเศษ <code>$@</code> แทน target ซึ่งหมายถึง <code>main.elf</code> ส่วนตัวแปรพิเศษ <code>$?</code> แทนรายการของ dependency ทั้งหมด |
− | <code>usbdrv/usbdrv.o</code> และ <code>usbdrv/usbdrvasm.o</code>) ลิ้งค์รวมเข้าไปด้วย ในที่นี้ตัวแปรพิเศษ <code>$@</code> แทน target ซึ่งหมายถึง | ||
− | <code>main.elf</code> ส่วนตัวแปรพิเศษ <code>$?</code> แทนรายการของ dependency ทั้งหมด | ||
− | == | + | == โครงสร้างของคำร้องขอ USB ที่รับจากฝั่งคอมพิวเตอร์ == |
− | ในสถาปัตยกรรม USB | + | ในสถาปัตยกรรม USB นั้นการสื่อสารจะถูกเริ่มจากการที่ฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ส่งคำร้องขอ (USB Request) ไปยังฝั่งอุปกรณ์ USB เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ข้อมูลคำร้องขอมีโครงสร้างตามที่นิยามไว้ในไฟล์ <code>usbdrv/usbdrv.h</code> ดังนี้ |
− | |||
typedef struct usbRequest{ | typedef struct usbRequest{ | ||
uchar bmRequestType; /* 1 ไบท์ */ | uchar bmRequestType; /* 1 ไบท์ */ | ||
แถว 88: | แถว 84: | ||
::* 0 = จากคอมพิวเตอร์ไปอุปกรณ์ USB (Host to Device) | ::* 0 = จากคอมพิวเตอร์ไปอุปกรณ์ USB (Host to Device) | ||
::* 1 = จากอุปกรณ์ USB มายังคอมพิวเตอร์ (Device to Host) | ::* 1 = จากอุปกรณ์ USB มายังคอมพิวเตอร์ (Device to Host) | ||
− | :* บิต 6..5 | + | :* บิต 6..5 ประเภทคำร้องขอ (Type) |
::* 0 = Standard | ::* 0 = Standard | ||
::* 1 = Class | ::* 1 = Class | ||
::* 2 = Vendor | ::* 2 = Vendor | ||
+ | :ฟังก์ชัน <code>usbFunctionSetup</code> ที่เราต้องเขียนขึ้นนั้นจะถูกเรียกใช้เมื่อค่าในฟิลด์ Type นี้มีค่า 2 (Vendor) เท่านั้น | ||
:* บิต 4..0 ผู้รับ (Recipient) | :* บิต 4..0 ผู้รับ (Recipient) | ||
::* 0 = Device | ::* 0 = Device | ||
แถว 97: | แถว 94: | ||
::* 2 = Endpoint | ::* 2 = Endpoint | ||
::* 3 = Other | ::* 3 = Other | ||
− | * <code>bRequest</code> | + | * <code>bRequest</code> ระบุหมายเลขคำร้องขอ คำร้องขอตามมาตรฐานของ USB นั้นมีประเภทเป็น Standard ซึ่งจะถูกประมวลผลจากไลบรารี V-USB อัตโนมัติ เราจึงไม่ต้องสนใจในส่วนนี้ ส่วนที่เราต้องรับผิดชอบคือ คำร้องขอแบบ Vendor ซึ่งต้องถูกออกแบบไว้ล่วงหน้าแล้วว่าอุปกรณ์ USB ของเราจะรองรับคำร้องขอหมายเลขอะไรบ้าง โดยในฟังก์ชัน <code>usbFunctionSetup</code> ของเราต้องประมวลผลคำร้องขอเหล่านี้ได้ถูกต้อง |
− | * <code>wValue</code> และ <code>wIndex</code> | + | * <code>wValue</code> และ <code>wIndex</code> ทั้งคู่เป็นฟิลด์ที่ไม่มีความหมายใดในกรณีที่คำร้องขอเป็นแบบ Vendor ดังนั้นเราจึงมีอิสระเต็มที่ในการใช้งานฟิลด์นี้เป็นตัวส่งรายละเอียดของคำร้องขอได้สูงสุด 4 ไบท์ |
* <code>wLength</code> กำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ USB หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์ | * <code>wLength</code> กำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ USB หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์ | ||
− | |||
− | |||
== ตัวอย่างโปรแกรม == | == ตัวอย่างโปรแกรม == | ||
− | เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น | + | เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น เราลองสร้างตัวอย่างเฟิร์มแวร์อย่างง่ายขึ้นมาพร้อมทั้งใช้ภาษาไพธอนทดลองสั่งงานจากฝั่งคอมพิวเตอร์ ในที่นี้เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำร้องขอ ดังนี้ |
− | + | * '''คำร้องขอ SET_LED''' สั่งให้ LED ดวงที่ระบุติดหรือดับ มีรายละเอียดของคำร้องขอดังนี้ | |
− | + | :* กำหนดให้หมายเลขคำร้องขอ (request number) คือ 0 | |
− | + | :* ส่งรายละเอียดมาให้ 2 ไบท์ ไบท์แรกระบุตำแหน่งของ LED บนบอร์ดพ่วง (0, 1 หรือ 2) ส่วนไบท์ที่สองระบุว่าจะให้ LED ดวงดังกล่าวติดหากมีค่า 0 หรือดับหากมีค่าอื่นที่ไม่ใช่ศูนย์ เนื่องจากข้อมูลมีขนาดเพียงสองไบท์ เราจะใส่ข้อมูลนี้ลงไปในฟิลด์ <code>wValue</code> ที่ถูกส่งไปพร้อมกับคำร้องขอโดยตรง | |
− | + | :* แม้ไม่มีข้อมูลอื่นเพิ่มเติมส่งจากคอมพิวเตอร์ไปยังอุปกรณ์ USB แต่เราจะระบุทิศทางการไหลของข้อมูลไว้เป็น Host to Device | |
− | * ''' | + | * '''คำร้องขอ GET_SWITCH''' สั่งให้บอร์ด MCU รายงานสถานะการกดปุ่มสวิตช์กลับมา มีรายละเอียดของคำร้องขอดังนี้ |
− | :* | + | :* กำหนดให้หมายเลขคำร้องขอ (request number) คือ 1 |
− | :* | + | :* ทิศทางการไหลของข้อมูลเป็น Device to Host |
− | + | :* รับข้อมูลกลับมา 1 ไบท์ บอกสถานะของสวิตช์ (0 คือไม่ถูกกด 1 คือถูกกด) | |
− | * ''' | ||
− | :* | ||
− | :* | ||
− | :* | ||
===การตั้งค่าให้อุปกรณ์ USB=== | ===การตั้งค่าให้อุปกรณ์ USB=== | ||
แถว 122: | แถว 113: | ||
ซึ่งไม่สามารถตั้งเองได้ตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกโปรแกรมไดรเวอร์ที่จะมาควบคุมอุปกรณ์ USB | ซึ่งไม่สามารถตั้งเองได้ตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกโปรแกรมไดรเวอร์ที่จะมาควบคุมอุปกรณ์ USB | ||
http://www.voti.nl/docs/usb-pid.html | http://www.voti.nl/docs/usb-pid.html | ||
− | ตั้งค่า VID ให้เป็น 0x16c0 | + | ตั้งค่า VID ให้เป็น 0x16c0 และ PID ให้เป็น 0x05dc |
===โปรแกรมฝั่งเฟิร์มแวร์=== | ===โปรแกรมฝั่งเฟิร์มแวร์=== | ||
− | เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม | + | * เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ <code>main.c</code> ดังนี้ |
− | #define | + | #define VENDOR_RQ_SET_LED 0 |
− | #define | + | #define VENDOR_RQ_GET_SWITCH 1 |
+ | |||
+ | * เราสร้างฟังก์ชันจัดการอินพุท/เอาท์พุทของพอร์ท C เพื่อความสะดวกในการใช้งานไว้ดังนี้ | ||
+ | uint8_t in_c(uint8_t pin) | ||
+ | { | ||
+ | // read logic level of port-C's pin | ||
+ | DDRC &= ~(1<<pin); | ||
+ | return ((PINC & (1<<pin))>>pin); | ||
+ | } | ||
+ | |||
+ | void out_c(uint8_t pin, uint8_t val) | ||
+ | { | ||
+ | // write logic level to port-C's pin | ||
+ | DDRC |= 1<<pin; | ||
+ | if(val) | ||
+ | PORTC |= 1<<pin; | ||
+ | else | ||
+ | PORTC &= ~(1<<pin); | ||
+ | } | ||
− | ภายในฟังก์ชัน <code>usbFunctionSetup</code> | + | * ภายในฟังก์ชัน <code>usbFunctionSetup</code> สร้างโค้ดสำหรับประมวลผลคำร้องขอที่รับมาจากโฮสท์ดังนี้ |
usbMsgLen_t usbFunctionSetup(uchar data[8]) | usbMsgLen_t usbFunctionSetup(uchar data[8]) | ||
{ | { | ||
− | + | usbRequest_t *rq = (void *)data; | |
− | + | static uchar dataBuffer[1]; /* ข้อมูลนี้ต้องไม่ถูกเขียนทับหลังจาก usbFunctionSetup รีเทิร์น */ | |
− | if(rq->bRequest == | + | /* ประมวลผลตามหมายเลขคำสั่งที่อยู่ใน bRequest */ |
+ | if (rq->bRequest == VENDOR_RQ_SET_LED) | ||
{ | { | ||
− | + | uint8_t led_no = rq->wValue.bytes[0]; | |
− | + | uint8_t led_val = rq->wValue.bytes[1]; | |
− | + | out_c(led_no, led_val); | |
− | + | return 0; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
− | return 0; /* | + | else if (rq->bRequest == VENDOR_RQ_GET_SWITCH) |
+ | { | ||
+ | dataBuffer[0] = !in_c(3); /* กลับลอจิกเพื่อให้ 0 = ปล่อย 1 = กด */ | ||
+ | usbMsgPtr = dataBuffer; /* ระบุว่าข้อมูลส่งกลับอยู่ใน dataBuffer */ | ||
+ | return 1; /* ความยาวข้อมูลส่งกลับเท่ากับ 1 */ | ||
+ | } | ||
+ | return 0; /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */ | ||
} | } | ||
− | |||
+ | == การโปรแกรมฝั่งเครื่องคอมพิวเตอร์ == | ||
− | + | * ติดตั้ง PyUSB เพื่อติดต่อกับอุปกรณ์ USB ผ่านภาษาไพธอน | |
− | ติดตั้ง PyUSB | ||
sudo apt-get install python-usb | sudo apt-get install python-usb | ||
+ | |||
+ | * สร้างไฟล์ชื่อ <code>testusb.py</code> เรียกใช้ไลบรารี usb และนิยามฟังก์ชัน <code>find_device</code> เพื่อค้นหาอุปกรณ์ USB ตัวที่เป็นบอร์ด MCU ของเราจากรายการอุปกรณ์ USB ทั้งหมดที่ต่ออยู่กับเครื่อง | ||
+ | import usb | ||
+ | |||
+ | def find_device(): | ||
+ | board = None | ||
+ | for bus in usb.busses(): | ||
+ | for dev in bus.devices: | ||
+ | if dev.idVendor == 0x16c0 and dev.idProduct == 0x05dc: | ||
+ | return dev | ||
+ | return None | ||
+ | :สังเกตว่าการค้นหาบอร์ด MCU ของเรานั้นอาศัยค่า VID และ PID ที่ตั้งไว้แต่แรก ดังนั้นอาจเกิดปัญหาขึ้นหากมีการเสียบบอร์ด MCU มากกว่าหนึ่งบอร์ดเข้ากับคอมพิวเตอร์เพราะฟังก์ชัน <code>find_device</code> จะคืนอุปกรณ์ที่พบว่ามี VID:PID เป็น 16c0:05dc เป็นตัวแรกเสมอ อย่างไรก็ตาม หากต้องการระบุเอาอุปกรณ์ตัวที่ระบุก็สามารถอาศัยข้อมูลจาก vendor name หรือ device name มากำหนดเงื่อนไขเพิ่ม | ||
+ | |||
+ | * ติดต่อกับบอร์ด MCU ผ่านทางไพธอนเชลล์ โดยเรียกโปรแกรม <code>testusb.py</code> แบบอินเทอแรคทีฟ | ||
+ | python -i testusb.py | ||
+ | |||
+ | * สร้าง handle เพื่อใช้เป็นทางผ่านในการสื่อสารกับบอร์ด MCU โดยใช้คำสั่งดังนี้ | ||
+ | >>> dev = find_device() | ||
+ | >>> handle = dev.open() | ||
+ | |||
+ | * สร้างคำร้องขอสำหรับคำสั่ง SET_LED ซึ่งมีหมายเลขคำร้องเป็น 0 | ||
+ | >>> req = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_OUT | ||
+ | |||
+ | * ส่งคำร้องออกไปยังบอร์ด MCU โดยกำหนดให้ฟิลด์ <code>value</code> มีค่าเป็น 0x0102 ซึ่งเฟิร์มแวร์ของเราจะตีความว่าเป็นการสั่งให้ LED หมายเลข 2 บนบอร์ดพ่วงติด การร้องขอครั้งนี้ไม่ได้ส่งข้อมูลใด ๆ ไปเพิ่มเติม จึงให้พารามิเตอร์ buf ของ handle.ControlMsg เป็น None ไป | ||
+ | >>> handle.controlMsg(req, 0, None, value=0x0102) | ||
+ | |||
+ | * ลองให้ LED หมายเลข 2 ดับ และ LED หมายเลข 1 ติดขึ้นมาแทน | ||
+ | >>> handle.controlMsg(req, 0, None, value=0x0002) | ||
+ | >>> handle.controlMsg(req, 0, None, value=0x0101) | ||
+ | |||
+ | * ทดลองอ่านสถานะของสวิตช์โดยส่งคำร้องหมายเลข 1 ไปยังบอร์ด MCU เนื่องจากคำร้องนี้เป็นการขอให้ MCU ส่งข้อมูลกลับ จึงต้องมีการระบุในคำร้องโดยใช้ usb.ENDPOINT_IN ในกรณีของการร้องขอข้อมูลจากอุปกรณ์ พารามิเตอร์ buf จะเป็นความยาวข้อมูลที่ต้องการร้องขอ ซึ่งเท่ากับหนึ่ง ส่วนค่าที่เมทอด controlMsg คืนกลับมาจะเป็น tuple ความยาวหนึ่งเช่นกัน | ||
+ | >>> req = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_IN | ||
+ | >>> handle.controlMsg(req, 1, 1) | ||
+ | (0,) | ||
+ | * ทดลองกดสวิตช์บนบอร์ดพ่วง แล้วส่งคำร้องไปยังบอร์ด MCU ใหม่ ผลลัพธ์ที่ได้ควรเป็นดังนี้ | ||
+ | >>> handle.controlMsg(req, 1, 1) | ||
+ | (1,) | ||
== ข้อมูลเพิ่มเติม == | == ข้อมูลเพิ่มเติม == | ||
* [http://vusb.wikidot.com/ V-USB Documentation Wiki] | * [http://vusb.wikidot.com/ V-USB Documentation Wiki] | ||
* [http://www.beyondlogic.org/usbnutshell/usb1.htm USB in a NutShell] | * [http://www.beyondlogic.org/usbnutshell/usb1.htm USB in a NutShell] |
รุ่นแก้ไขเมื่อ 06:09, 26 สิงหาคม 2552
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้
ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ V-USB (หรือ AVRUSB) ที่พัฒนาโดยบริษัท Objective Development โดยทำให้บอร์ด MCU ของเราทำงานเป็นอุปกรณ์ที่อยู่ในกลุ่ม custom class device ซึ่งจัดเป็นอุปกรณ์ USB ที่ไม่สังกัดคลาสใด โดยซอฟต์แวร์ฝั่งคอมพิวเตอร์จะอยู่ภายใต้ความควบคุมของเราทั้งหมด
เนื้อหา
ขั้นตอนการใช้งานไลบรารี 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 Request) ไปยังฝั่งอุปกรณ์ USB เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ข้อมูลคำร้องขอมีโครงสร้างตามที่นิยามไว้ในไฟล์ 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
- ฟังก์ชัน
usbFunctionSetup
ที่เราต้องเขียนขึ้นนั้นจะถูกเรียกใช้เมื่อค่าในฟิลด์ Type นี้มีค่า 2 (Vendor) เท่านั้น- บิต 4..0 ผู้รับ (Recipient)
- 0 = Device
- 1 = Interface
- 2 = Endpoint
- 3 = Other
bRequest
ระบุหมายเลขคำร้องขอ คำร้องขอตามมาตรฐานของ USB นั้นมีประเภทเป็น Standard ซึ่งจะถูกประมวลผลจากไลบรารี V-USB อัตโนมัติ เราจึงไม่ต้องสนใจในส่วนนี้ ส่วนที่เราต้องรับผิดชอบคือ คำร้องขอแบบ Vendor ซึ่งต้องถูกออกแบบไว้ล่วงหน้าแล้วว่าอุปกรณ์ USB ของเราจะรองรับคำร้องขอหมายเลขอะไรบ้าง โดยในฟังก์ชันusbFunctionSetup
ของเราต้องประมวลผลคำร้องขอเหล่านี้ได้ถูกต้องwValue
และwIndex
ทั้งคู่เป็นฟิลด์ที่ไม่มีความหมายใดในกรณีที่คำร้องขอเป็นแบบ Vendor ดังนั้นเราจึงมีอิสระเต็มที่ในการใช้งานฟิลด์นี้เป็นตัวส่งรายละเอียดของคำร้องขอได้สูงสุด 4 ไบท์wLength
กำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ USB หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์
ตัวอย่างโปรแกรม
เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น เราลองสร้างตัวอย่างเฟิร์มแวร์อย่างง่ายขึ้นมาพร้อมทั้งใช้ภาษาไพธอนทดลองสั่งงานจากฝั่งคอมพิวเตอร์ ในที่นี้เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำร้องขอ ดังนี้
- คำร้องขอ SET_LED สั่งให้ LED ดวงที่ระบุติดหรือดับ มีรายละเอียดของคำร้องขอดังนี้
- กำหนดให้หมายเลขคำร้องขอ (request number) คือ 0
- ส่งรายละเอียดมาให้ 2 ไบท์ ไบท์แรกระบุตำแหน่งของ LED บนบอร์ดพ่วง (0, 1 หรือ 2) ส่วนไบท์ที่สองระบุว่าจะให้ LED ดวงดังกล่าวติดหากมีค่า 0 หรือดับหากมีค่าอื่นที่ไม่ใช่ศูนย์ เนื่องจากข้อมูลมีขนาดเพียงสองไบท์ เราจะใส่ข้อมูลนี้ลงไปในฟิลด์
wValue
ที่ถูกส่งไปพร้อมกับคำร้องขอโดยตรง - แม้ไม่มีข้อมูลอื่นเพิ่มเติมส่งจากคอมพิวเตอร์ไปยังอุปกรณ์ USB แต่เราจะระบุทิศทางการไหลของข้อมูลไว้เป็น Host to Device
- คำร้องขอ GET_SWITCH สั่งให้บอร์ด MCU รายงานสถานะการกดปุ่มสวิตช์กลับมา มีรายละเอียดของคำร้องขอดังนี้
- กำหนดให้หมายเลขคำร้องขอ (request number) คือ 1
- ทิศทางการไหลของข้อมูลเป็น Device to Host
- รับข้อมูลกลับมา 1 ไบท์ บอกสถานะของสวิตช์ (0 คือไม่ถูกกด 1 คือถูกกด)
การตั้งค่าให้อุปกรณ์ USB
- อุปกรณ์ USB ทุกตัวจะต้องมีการระบุค่า Vendor ID (VID) และ Product ID (PID) อย่างละ 16 บิต
ซึ่งไม่สามารถตั้งเองได้ตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกโปรแกรมไดรเวอร์ที่จะมาควบคุมอุปกรณ์ USB http://www.voti.nl/docs/usb-pid.html ตั้งค่า VID ให้เป็น 0x16c0 และ PID ให้เป็น 0x05dc
โปรแกรมฝั่งเฟิร์มแวร์
- เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ
main.c
ดังนี้
#define VENDOR_RQ_SET_LED 0 #define VENDOR_RQ_GET_SWITCH 1
- เราสร้างฟังก์ชันจัดการอินพุท/เอาท์พุทของพอร์ท C เพื่อความสะดวกในการใช้งานไว้ดังนี้
uint8_t in_c(uint8_t pin) { // read logic level of port-C's pin DDRC &= ~(1<<pin); return ((PINC & (1<<pin))>>pin); } void out_c(uint8_t pin, uint8_t val) { // write logic level to port-C's pin DDRC |= 1<<pin; if(val) PORTC |= 1<<pin; else PORTC &= ~(1<<pin); }
- ภายในฟังก์ชัน
usbFunctionSetup
สร้างโค้ดสำหรับประมวลผลคำร้องขอที่รับมาจากโฮสท์ดังนี้
usbMsgLen_t usbFunctionSetup(uchar data[8]) { usbRequest_t *rq = (void *)data; static uchar dataBuffer[1]; /* ข้อมูลนี้ต้องไม่ถูกเขียนทับหลังจาก usbFunctionSetup รีเทิร์น */
/* ประมวลผลตามหมายเลขคำสั่งที่อยู่ใน bRequest */
if (rq->bRequest == VENDOR_RQ_SET_LED) { uint8_t led_no = rq->wValue.bytes[0]; uint8_t led_val = rq->wValue.bytes[1]; out_c(led_no, led_val); return 0; } else if (rq->bRequest == VENDOR_RQ_GET_SWITCH) { dataBuffer[0] = !in_c(3); /* กลับลอจิกเพื่อให้ 0 = ปล่อย 1 = กด */
usbMsgPtr = dataBuffer; /* ระบุว่าข้อมูลส่งกลับอยู่ใน dataBuffer */
return 1; /* ความยาวข้อมูลส่งกลับเท่ากับ 1 */ } return 0; /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */ }
การโปรแกรมฝั่งเครื่องคอมพิวเตอร์
- ติดตั้ง PyUSB เพื่อติดต่อกับอุปกรณ์ USB ผ่านภาษาไพธอน
sudo apt-get install python-usb
- สร้างไฟล์ชื่อ
testusb.py
เรียกใช้ไลบรารี usb และนิยามฟังก์ชันfind_device
เพื่อค้นหาอุปกรณ์ USB ตัวที่เป็นบอร์ด MCU ของเราจากรายการอุปกรณ์ USB ทั้งหมดที่ต่ออยู่กับเครื่อง
import usb
def find_device(): board = None for bus in usb.busses(): for dev in bus.devices: if dev.idVendor == 0x16c0 and dev.idProduct == 0x05dc: return dev return None
- สังเกตว่าการค้นหาบอร์ด MCU ของเรานั้นอาศัยค่า VID และ PID ที่ตั้งไว้แต่แรก ดังนั้นอาจเกิดปัญหาขึ้นหากมีการเสียบบอร์ด MCU มากกว่าหนึ่งบอร์ดเข้ากับคอมพิวเตอร์เพราะฟังก์ชัน
find_device
จะคืนอุปกรณ์ที่พบว่ามี VID:PID เป็น 16c0:05dc เป็นตัวแรกเสมอ อย่างไรก็ตาม หากต้องการระบุเอาอุปกรณ์ตัวที่ระบุก็สามารถอาศัยข้อมูลจาก vendor name หรือ device name มากำหนดเงื่อนไขเพิ่ม
- ติดต่อกับบอร์ด MCU ผ่านทางไพธอนเชลล์ โดยเรียกโปรแกรม
testusb.py
แบบอินเทอแรคทีฟ
python -i testusb.py
- สร้าง handle เพื่อใช้เป็นทางผ่านในการสื่อสารกับบอร์ด MCU โดยใช้คำสั่งดังนี้
>>> dev = find_device() >>> handle = dev.open()
- สร้างคำร้องขอสำหรับคำสั่ง SET_LED ซึ่งมีหมายเลขคำร้องเป็น 0
>>> req = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_OUT
- ส่งคำร้องออกไปยังบอร์ด MCU โดยกำหนดให้ฟิลด์
value
มีค่าเป็น 0x0102 ซึ่งเฟิร์มแวร์ของเราจะตีความว่าเป็นการสั่งให้ LED หมายเลข 2 บนบอร์ดพ่วงติด การร้องขอครั้งนี้ไม่ได้ส่งข้อมูลใด ๆ ไปเพิ่มเติม จึงให้พารามิเตอร์ buf ของ handle.ControlMsg เป็น None ไป
>>> handle.controlMsg(req, 0, None, value=0x0102)
- ลองให้ LED หมายเลข 2 ดับ และ LED หมายเลข 1 ติดขึ้นมาแทน
>>> handle.controlMsg(req, 0, None, value=0x0002) >>> handle.controlMsg(req, 0, None, value=0x0101)
- ทดลองอ่านสถานะของสวิตช์โดยส่งคำร้องหมายเลข 1 ไปยังบอร์ด MCU เนื่องจากคำร้องนี้เป็นการขอให้ MCU ส่งข้อมูลกลับ จึงต้องมีการระบุในคำร้องโดยใช้ usb.ENDPOINT_IN ในกรณีของการร้องขอข้อมูลจากอุปกรณ์ พารามิเตอร์ buf จะเป็นความยาวข้อมูลที่ต้องการร้องขอ ซึ่งเท่ากับหนึ่ง ส่วนค่าที่เมทอด controlMsg คืนกลับมาจะเป็น tuple ความยาวหนึ่งเช่นกัน
>>> req = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_IN >>> handle.controlMsg(req, 1, 1) (0,)
- ทดลองกดสวิตช์บนบอร์ดพ่วง แล้วส่งคำร้องไปยังบอร์ด MCU ใหม่ ผลลัพธ์ที่ได้ควรเป็นดังนี้
>>> handle.controlMsg(req, 1, 1) (1,)