ผลต่างระหว่างรุ่นของ "การจำลองบอร์ด MCU เป็นอุปกรณ์ USB"
Chaiporn (คุย | มีส่วนร่วม) |
Chaiporn (คุย | มีส่วนร่วม) |
||
(ไม่แสดง 24 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
แถว 1: | แถว 1: | ||
+ | : ''วิกินี้เป็นส่วนหนึ่งของรายวิชา [[01204223]]'' | ||
+ | |||
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้ | ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้ | ||
แถว 4: | แถว 6: | ||
นอกเหนือจาก custom class device แล้ว ไลบรารี V-USB ยังรองรับการโปรแกรมเฟิร์มแวร์ให้สังกัดคลาสอื่นได้อีกหลายคลาส อาทิเช่น HID (Human Interface Device) ซึ่งอยู่ในกลุ่มเดียวกับอุปกรณ์จำพวกแป้นพิมพ์ เมาส์ จอยสติ๊ก และเกมแพด | นอกเหนือจาก custom class device แล้ว ไลบรารี V-USB ยังรองรับการโปรแกรมเฟิร์มแวร์ให้สังกัดคลาสอื่นได้อีกหลายคลาส อาทิเช่น HID (Human Interface Device) ซึ่งอยู่ในกลุ่มเดียวกับอุปกรณ์จำพวกแป้นพิมพ์ เมาส์ จอยสติ๊ก และเกมแพด | ||
+ | |||
+ | ==ไลบรารีและเครื่องมือที่จำเป็น== | ||
+ | ให้แน่ใจว่าได้ติดตั้งไลบรารีและเครื่องมือที่จำเป็นตามที่ได้อธิบายไว้ในวิกิด้านล่าง ก่อนเริ่มทำตามขั้นตอนในวิกินี้ | ||
+ | * [[การติดตั้งไลบรารี PyUSB]] | ||
== ขั้นตอนการใช้งานไลบรารี V-USB == | == ขั้นตอนการใช้งานไลบรารี V-USB == | ||
− | * ดาวน์โหลดซอร์สโค้ดจาก [http://www.obdev.at/downloads/vusb/vusb- | + | * ดาวน์โหลดซอร์สโค้ดจาก [http://www.obdev.at/downloads/vusb/vusb-20121206.tar.gz Objective Development] |
* แตกไฟล์ .tar.gz ที่ดาวน์โหลดมาโดยใช้คำสั่ง | * แตกไฟล์ .tar.gz ที่ดาวน์โหลดมาโดยใช้คำสั่ง | ||
− | tar zxf vusb- | + | tar zxf vusb-20121206.tar.gz |
− | * คัดลอกไดเรคตอรี <code>usbdrv/</code> ที่อยู่ในไดเรคตอรี <code>vusb- | + | * คัดลอกไดเรคตอรี <code>usbdrv/</code> ที่อยู่ในไดเรคตอรี <code>vusb-20121206/</code> ไปวางในไดเรคตอรีโปรเจ็คของตน |
* ภายในไดเรคตอรีโปรเจ็คของตนเอง ย้ายไฟล์ <code>usbdrv/usbconfig-prototype.h</code> มาวางไว้ด้านนอก และเปลี่ยนชื่อให้เป็น <code>usbconfig.h</code> | * ภายในไดเรคตอรีโปรเจ็คของตนเอง ย้ายไฟล์ <code>usbdrv/usbconfig-prototype.h</code> มาวางไว้ด้านนอก และเปลี่ยนชื่อให้เป็น <code>usbconfig.h</code> | ||
mv usbdrv/usbconfig-prototype.h ./usbconfig.h | mv usbdrv/usbconfig-prototype.h ./usbconfig.h | ||
แถว 43: | แถว 49: | ||
สร้าง Makefile ต่อไปนี้เพื่อคอมไพล์โปรแกรมและดำเนินการลิ้งค์เข้ากับไลบรารี V-USB ซึ่งตัวอย่าง Makefile นี้สมมติว่าไฟล์หลักของโปรเจ็คคือ <code>main.c</code> | สร้าง Makefile ต่อไปนี้เพื่อคอมไพล์โปรแกรมและดำเนินการลิ้งค์เข้ากับไลบรารี V-USB ซึ่งตัวอย่าง Makefile นี้สมมติว่าไฟล์หลักของโปรเจ็คคือ <code>main.c</code> | ||
MCU=atmega168 | MCU=atmega168 | ||
− | F_CPU= | + | F_CPU=16000000L |
TARGET=main.hex | TARGET=main.hex | ||
OBJS=usbdrv/usbdrv.o usbdrv/usbdrvasm.o | OBJS=usbdrv/usbdrv.o usbdrv/usbdrvasm.o | ||
แถว 71: | แถว 77: | ||
หากดูในกฎที่ระบุกลไกการสร้างไฟล์ <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>main.elf</code> นั้นถูกสร้างขึ้นโดยการที่ make เรียกคำสั่งด้านล่างนี้อัตโนมัติ | หากดูในกฎที่ระบุกลไกการสร้างไฟล์ <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>main.elf</code> นั้นถูกสร้างขึ้นโดยการที่ make เรียกคำสั่งด้านล่างนี้อัตโนมัติ | ||
− | avr-gcc -Wall -Os -DF_CPU= | + | avr-gcc -Wall -Os -DF_CPU=16000000L -Iusbdrv -I. -mmcu=atmega168 -o main.elf main.o usbdrv/usbdrv.o usbdrv/usbdrvasm.o |
== โครงสร้างของคำร้องขอ USB ที่รับจากฝั่งคอมพิวเตอร์ == | == โครงสร้างของคำร้องขอ USB ที่รับจากฝั่งคอมพิวเตอร์ == | ||
ในสถาปัตยกรรม USB นั้นการสื่อสารจะถูกเริ่มจากการที่ฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ส่งคำร้องขอ (USB Request) ไปยังฝั่งอุปกรณ์ USB เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ข้อมูลคำร้องขอมีโครงสร้างตามที่นิยามไว้ในไฟล์ <code>usbdrv/usbdrv.h</code> ดังนี้ | ในสถาปัตยกรรม USB นั้นการสื่อสารจะถูกเริ่มจากการที่ฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ส่งคำร้องขอ (USB Request) ไปยังฝั่งอุปกรณ์ USB เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ข้อมูลคำร้องขอมีโครงสร้างตามที่นิยามไว้ในไฟล์ <code>usbdrv/usbdrv.h</code> ดังนี้ | ||
typedef struct usbRequest{ | typedef struct usbRequest{ | ||
− | uchar bmRequestType; /* 1 | + | uchar bmRequestType; /* 1 ไบต์ */ |
− | uchar bRequest; /* 1 | + | uchar bRequest; /* 1 ไบต์ */ |
− | usbWord_t wValue; /* 2 | + | usbWord_t wValue; /* 2 ไบต์ */ |
− | usbWord_t wIndex; /* 2 | + | usbWord_t wIndex; /* 2 ไบต์ */ |
− | usbWord_t wLength; /* 2 | + | usbWord_t wLength; /* 2 ไบต์ */ |
}usbRequest_t; | }usbRequest_t; | ||
* <code>bmRequestType</code> ประกอบด้วยฟิลด์ย่อย 3 ฟิลด์ดังต่อไปนี้ | * <code>bmRequestType</code> ประกอบด้วยฟิลด์ย่อย 3 ฟิลด์ดังต่อไปนี้ | ||
แถว 97: | แถว 103: | ||
::* 3 = Other | ::* 3 = Other | ||
* <code>bRequest</code> ระบุหมายเลขคำร้องขอ คำร้องขอตามมาตรฐานของ USB นั้นมีประเภทเป็น Standard ซึ่งจะถูกประมวลผลจากไลบรารี V-USB อัตโนมัติ เราจึงไม่ต้องสนใจในส่วนนี้ ส่วนที่เราต้องรับผิดชอบคือคำร้องขอแบบ Vendor ซึ่งต้องถูกออกแบบไว้ล่วงหน้าแล้วว่าอุปกรณ์ USB ของเราจะรองรับคำร้องขอหมายเลขอะไรบ้าง โดยในฟังก์ชัน <code>usbFunctionSetup</code> ของเราต้องประมวลผลคำร้องขอเหล่านี้ได้ถูกต้อง | * <code>bRequest</code> ระบุหมายเลขคำร้องขอ คำร้องขอตามมาตรฐานของ USB นั้นมีประเภทเป็น Standard ซึ่งจะถูกประมวลผลจากไลบรารี V-USB อัตโนมัติ เราจึงไม่ต้องสนใจในส่วนนี้ ส่วนที่เราต้องรับผิดชอบคือคำร้องขอแบบ Vendor ซึ่งต้องถูกออกแบบไว้ล่วงหน้าแล้วว่าอุปกรณ์ USB ของเราจะรองรับคำร้องขอหมายเลขอะไรบ้าง โดยในฟังก์ชัน <code>usbFunctionSetup</code> ของเราต้องประมวลผลคำร้องขอเหล่านี้ได้ถูกต้อง | ||
− | * <code>wValue</code> และ <code>wIndex</code> ทั้งคู่เป็นฟิลด์ที่ไม่มีความหมายใดในกรณีที่คำร้องขอเป็นแบบ Vendor ดังนั้นเราจึงมีอิสระเต็มที่ในการใช้งานฟิลด์ทั้งคู่นี้เป็นตัวส่งรายละเอียดของคำร้องขอ ซึ่งส่งได้สูงสุด 4 | + | * <code>wValue</code> และ <code>wIndex</code> ทั้งคู่เป็นฟิลด์ที่ไม่มีความหมายใดในกรณีที่คำร้องขอเป็นแบบ Vendor ดังนั้นเราจึงมีอิสระเต็มที่ในการใช้งานฟิลด์ทั้งคู่นี้เป็นตัวส่งรายละเอียดของคำร้องขอ ซึ่งส่งได้สูงสุด 4 ไบต์ |
* <code>wLength</code> กำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ USB หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์ | * <code>wLength</code> กำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ USB หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์ | ||
แถว 104: | แถว 110: | ||
== ตัวอย่างโปรแกรม == | == ตัวอย่างโปรแกรม == | ||
− | :โหลดไฟล์ [http://www.cpe.ku.ac.th/~cpj/204223/usb-example. | + | :โหลดไฟล์ [http://www.cpe.ku.ac.th/~cpj/204223/usb-example.zip usb-example.zip] ซึ่งเก็บไฟล์ทั้งหมดที่ใช้ในตัวอย่างนี้ (กรณีที่ต้องการใช้งานผ่าน Arduino IDE ให้ดาวน์โหลดไฟล์ [http://www.cpe.ku.ac.th/~cpj/204223/usb-example-arduino.zip usb-example-arduino.zip]) |
เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น เราลองสร้างตัวอย่างเฟิร์มแวร์อย่างง่ายขึ้นมาพร้อมทั้งใช้ภาษาไพธอนทดลองสั่งงานจากฝั่งคอมพิวเตอร์ ในที่นี้เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำร้องขอ ดังนี้ | เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น เราลองสร้างตัวอย่างเฟิร์มแวร์อย่างง่ายขึ้นมาพร้อมทั้งใช้ภาษาไพธอนทดลองสั่งงานจากฝั่งคอมพิวเตอร์ ในที่นี้เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำร้องขอ ดังนี้ | ||
แถว 128: | แถว 134: | ||
#define USB_CFG_VENDOR_NAME_LEN 12 | #define USB_CFG_VENDOR_NAME_LEN 12 | ||
− | * '''Device Name:''' กำหนดค่า USB Device Name ให้เป็น '' | + | * '''Device Name:''' กำหนดค่า USB Device Name ให้เป็น ''ID <รหัสนิสิต>'' โดยเปลี่ยนนิยามของมาโคร <code>USB_CFG_DEVICE_NAME</code> และ <code>USB_CFG_DEVICE_NAME_LEN</code> ตัวอย่างเช่นนิสิตที่มีรหัสประจำตัว 1234567890 ให้แก้ไขมาโครดังนี้ |
− | #define USB_CFG_DEVICE_NAME ' | + | #define USB_CFG_DEVICE_NAME 'I','D',' ','1','2','3','4','5','6','7','8','9','0' |
− | #define USB_CFG_DEVICE_NAME_LEN | + | #define USB_CFG_DEVICE_NAME_LEN 13 |
− | === | + | === เฟิร์มแวร์ฝั่งอุปกรณ์ === |
* เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ <code>main.c</code> ดังนี้ | * เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ <code>main.c</code> ดังนี้ | ||
− | #define | + | #define RQ_SET_LED 0 |
− | #define | + | #define RQ_SET_LED_VALUE 1 |
+ | #define RQ_GET_SWITCH 2 | ||
+ | #define RQ_GET_LIGHT 3 | ||
− | * | + | * ภายในฟังก์ชัน <code>usbFunctionSetup</code> สร้างโค้ดสำหรับประมวลผลคำร้องขอที่รับมาจากโฮสท์ดังนี้ |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | usbMsgLen_t usbFunctionSetup(uint8_t data[8]) | |
− | usbMsgLen_t usbFunctionSetup( | ||
{ | { | ||
− | usbRequest_t *rq = ( | + | usbRequest_t *rq = (usbRequest_t*)data; |
− | static | + | static uint8_t switch_state; /* เป็น static เพื่อค่าในตัวแปรคงอยู่แม้ฟังก์ชัน return ไปแล้ว */ |
/* ประมวลผลตามหมายเลขคำสั่งที่อยู่ใน bRequest */ | /* ประมวลผลตามหมายเลขคำสั่งที่อยู่ใน bRequest */ | ||
− | if (rq->bRequest == | + | if (rq->bRequest == RQ_SET_LED) |
{ | { | ||
− | uint8_t | + | uint8_t led_val = rq->wValue.bytes[0]; |
− | uint8_t | + | uint8_t led_no = rq->wIndex.bytes[0]; |
− | + | ||
− | return 0; | + | if (led_val) |
+ | PORTC |= (1<<led_no); | ||
+ | else | ||
+ | PORTC &= ~(1<<led_no); | ||
+ | |||
+ | return 0; /* ไม่ส่งข้อมูลกลับ */ | ||
} | } | ||
− | else if (rq->bRequest == | + | |
+ | else if (rq->bRequest == RQ_GET_SWITCH) | ||
{ | { | ||
− | + | if ((PINC & (1<<PC3)) == 0) /* switch is pressed */ | |
− | usbMsgPtr = | + | switch_state = 1; |
− | return | + | else |
+ | switch_state = 0; | ||
+ | |||
+ | /* ให้ usbMsgPtr ชี้ไปที่ตำแหน่งหน่วยความจำที่เก็บข้อมูล */ | ||
+ | usbMsgPtr = (uchar*) &switch_state; | ||
+ | |||
+ | /* ส่งข้อมูลกลับ 1 ไบท์ (เท่ากับขนาดของตัวแปร switch_state) */ | ||
+ | return sizeof(switch_state); | ||
} | } | ||
+ | |||
return 0; /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */ | return 0; /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */ | ||
} | } | ||
แถว 185: | แถว 186: | ||
make flash | make flash | ||
− | === | + | === แอพลิเคชันฝั่งโฮสท์ === |
− | |||
− | |||
− | |||
− | + | ขณะที่เฟิร์มแวร์ทำงานอยู่นั้นเราจะมองไม่เห็นผลลัพธ์การทำงานใด ๆ เนื่องจากเฟิร์มแวร์ถูกเขียนไว้ให้ตอบสนองต่อการสั่งงานผ่านคอมพิวเตอร์เท่านั้น ภายในไฟล์ตัวอย่างมีไฟล์ชื่อ <tt>practicum.py</tt> ซึ่งเป็นโมดูลไพทอนที่เราจะนำมาใช้ติดต่อกับบอร์ดไมโครคอนโทรลเลอร์ผ่านภาษาไพทอน ทดลองเปิดไพทอนเชลล์แล้วโหลดโมดูลมาใช้งาน โดยฟังก์ชันหลักที่เรียกใช้จากโมดูลคือ <tt>find_mcu_boards()</tt> ซึ่งคืนค่าเป็นลิสต์ของอุปกรณ์ USB ทุกตัวที่มี VID/PID เป็น 16c0:05dc ที่ต่ออยู่กับคอมพิวเตอร์ ณ ขณะนั้น | |
− | |||
− | |||
− | |||
− | + | $ python | |
− | + | >>> from practicum import find_mcu_boards,McuBoard | |
+ | >>> devices = find_mcu_boards() | ||
+ | >>> devices | ||
+ | [<usb.Device object at 0xd91d70>] | ||
− | + | จากนั้นสร้างอ็อบเจกต์ของคลาส <tt>McuBoard</tt> ขึ้นมาจากอุปกรณ์ตัวแรกในลิสต์ | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | >>> mcu = McuBoard(devices[0]) | |
− | + | >>> mcu.handle.getString(mcu.device.iManufacturer, 256) | |
− | + | 'cpe.ku.ac.th' | |
+ | >>> mcu.handle.getString(mcu.device.iProduct, 256) | ||
+ | 'ID 1234567890' <- ต้องขึ้นเป็นรหัสนิสิตของตน | ||
− | + | ทดลองส่งคำร้องขอหมายเลข 0 (ควบคุมสถานะ LED) เพื่อให้ LED หมายเลข 2 บนบอร์ดพ่วงติดสว่าง ใช้เมท็อต <code>usb_write</code> ในคลาส <code>McuBoard</code> ดังนี้ | |
− | |||
− | |||
− | + | >>> mcu.usb_write(0, index=2, value=1) | |
− | >>> | ||
− | + | คำสั่งด้านล่างมีผลทำให้ LED หมายเลข 2 ดับ และ LED หมายเลข 1 ติดขึ้นมาแทน | |
− | + | >>> mcu.usb_write(0, index=2, value=0) | |
− | + | >>> mcu.usb_write(0, index=1, value=1) | |
− | |||
− | >>> | ||
− | |||
− | |||
− | |||
− | >>> | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | ทดลองอ่านสถานะของสวิตช์โดยส่งคำร้องหมายเลข 2 ไปยังบอร์ด MCU | |
− | + | >>> mcu.usb_read(2, length=1) | |
− | + | array('B', [0]) | |
+ | ค่าที่เมทอด <code>usb_read</code> คืนกลับมาจะเป็นทูเปิลที่มีสมาชิกหนึ่งตัว ตามที่ระบุในเฟิร์มแวร์ | ||
− | + | ทดลองกดสวิตช์บนบอร์ดพ่วงค้างไว้ แล้วส่งคำร้องไปยังบอร์ด MCU ใหม่ ผลลัพธ์ที่ได้ควรเป็นดังนี้ | |
− | >>> | + | >>> mcu.usb_read(2, length=1) |
+ | array('B', [1]) | ||
− | + | ใช้คำสั่ง <tt>help</tt> เพื่อดูรายละเอียดการใช้งานคลาส <tt>McuBoard</tt> | |
− | >>> | + | >>> help(McuBoard) |
− | |||
== เกี่ยวกับหมายเลข VID/PID == | == เกี่ยวกับหมายเลข VID/PID == | ||
− | ชุดตัวเลข VID/PID ที่กำหนดให้กับอุปกรณ์ USB ไม่ควรตั้งเอาเองตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกซอฟต์แวร์ไดรเวอร์ที่จะมาควบคุมอุปกรณ์ USB | + | ชุดตัวเลข VID/PID ที่กำหนดให้กับอุปกรณ์ USB ไม่ควรตั้งเอาเองตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกซอฟต์แวร์ไดรเวอร์ที่จะมาควบคุมอุปกรณ์ โดยทั่วไปการจะได้มาซึ่งเลข VID/PID เพื่อใช้กับอุปกรณ์ที่เราสร้างขึ้นจำเป็นต้องสมัครเป็นสมาชิกของ [http://www.usb.org USB Implementers Forum] (ค่าสมาชิกปีละ 4,000 เหรียญสหรัฐ) หรือซื้อตัวเลข VID มาจากผู้ที่เป็นสมาชิกอีกทีหนึ่ง |
− | + | อย่างไรก็ตาม Object Development ผู้พัฒนาไลบรารี V-USB ได้เตรียมชุดตัวเลข VID/PID ไว้ให้เราใช้งานโดยไม่เสียค่าใช้จ่าย หมายเลข <tt>16C0:xxxx</tt> ที่เราเลือกนำมาใช้งานก็ได้มาจากตัวเลขในชุดดังกล่าว รายละเอียดเพิ่มเติมเกี่ยวกับการกำหนดค่า VID และ PID ให้กับอุปกรณ์ USB รวมถึงหลักเกณฑ์การปฏิบัติในการผลิตอุปกรณ์ USB สู่สาธารณะ สามารถศึกษาเพิ่มเติมได้จากเนื้อหาในไฟล์ <tt>USB-ID-FAQ.txt</tt> และไฟล์ <tt>USB-IDs-for-free.txt</tt> ในไดเรคตอรี <tt>usbdrv</tt> ที่ได้จากการติดตั้งไลบรารี V-USB รวมถึงเอกสาร [http://www.voti.nl/docs/usb-pid.html How to obtain an USB VID/PID for your project] | |
== ข้อมูลเพิ่มเติม == | == ข้อมูลเพิ่มเติม == | ||
* [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] | ||
+ | * [http://usb4java.org/ usb4java] - ไลบรารีภาษาจาวาสำหรับติดต่อกับอุปกรณ์ USB |
รุ่นแก้ไขปัจจุบันเมื่อ 09:09, 3 พฤษภาคม 2562
- วิกินี้เป็นส่วนหนึ่งของรายวิชา 01204223
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้
ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ V-USB (เดิมเรียกว่า AVR-USB) ที่พัฒนาโดยบริษัท Objective Development โดยทำให้บอร์ด MCU ของเราทำงานเป็นอุปกรณ์ที่อยู่ในกลุ่ม custom class device ซึ่งจัดเป็นอุปกรณ์ USB ที่ไม่สังกัดคลาสใด โดยซอฟต์แวร์ฝั่งคอมพิวเตอร์จะอยู่ภายใต้ความควบคุมของเราทั้งหมด
นอกเหนือจาก custom class device แล้ว ไลบรารี V-USB ยังรองรับการโปรแกรมเฟิร์มแวร์ให้สังกัดคลาสอื่นได้อีกหลายคลาส อาทิเช่น HID (Human Interface Device) ซึ่งอยู่ในกลุ่มเดียวกับอุปกรณ์จำพวกแป้นพิมพ์ เมาส์ จอยสติ๊ก และเกมแพด
เนื้อหา
ไลบรารีและเครื่องมือที่จำเป็น
ให้แน่ใจว่าได้ติดตั้งไลบรารีและเครื่องมือที่จำเป็นตามที่ได้อธิบายไว้ในวิกิด้านล่าง ก่อนเริ่มทำตามขั้นตอนในวิกินี้
ขั้นตอนการใช้งานไลบรารี V-USB
- ดาวน์โหลดซอร์สโค้ดจาก Objective Development
- แตกไฟล์ .tar.gz ที่ดาวน์โหลดมาโดยใช้คำสั่ง
tar zxf vusb-20121206.tar.gz
- คัดลอกไดเรคตอรี
usbdrv/
ที่อยู่ในไดเรคตอรีvusb-20121206/
ไปวางในไดเรคตอรีโปรเจ็คของตน - ภายในไดเรคตอรีโปรเจ็คของตนเอง ย้ายไฟล์
usbdrv/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(uint8_t 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 $(MCU) -c usbasp -U flash:w:$(TARGET) %.hex: %.elf avr-objcopy -j .text -j .data -O ihex $< $@ %.elf: %.o $(OBJS) avr-gcc $(CFLAGS) -o $@ $? %.o: %.c avr-gcc -c $(CFLAGS) -o $@ $< %.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 ทั้งหมด นั่นคือไฟล์ main.elf
นั้นถูกสร้างขึ้นโดยการที่ make เรียกคำสั่งด้านล่างนี้อัตโนมัติ
avr-gcc -Wall -Os -DF_CPU=16000000L -Iusbdrv -I. -mmcu=atmega168 -o main.elf main.o usbdrv/usbdrv.o usbdrv/usbdrvasm.o
โครงสร้างของคำร้องขอ 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 หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์
ดูรายละเอียดเพิ่มเติมได้จาก USB in a NutShell
ตัวอย่างโปรแกรม
- โหลดไฟล์ usb-example.zip ซึ่งเก็บไฟล์ทั้งหมดที่ใช้ในตัวอย่างนี้ (กรณีที่ต้องการใช้งานผ่าน Arduino IDE ให้ดาวน์โหลดไฟล์ usb-example-arduino.zip)
เพื่อให้เห็นภาพของการใช้งานไลบรารี 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
เปิดไฟล์ usbconfig.h
เพื่อปรับค่าให้สอดคล้องกับโปรเจ็ค
- VID/PID: อุปกรณ์ USB ทุกตัวจะต้องถูกกำหนดค่า Vendor ID (VID) และ Product ID (PID) ให้ ซึ่งแต่ละตัวเลขมีขนาด 16 บิต ในโปรเจ็คนี้ให้กำหนดค่า VID และ PID ให้เป็น 0x16c0 และ 0x05dc ตามลำดับ โดยดูให้แน่ใจว่าในไฟล์
usbconfig.h
มีบรรทัดเหล่านี้
#define USB_CFG_VENDOR_ID 0xc0, 0x16 /* VID = 0x16c0 */ : #define USB_CFG_DEVICE_ID 0xdc, 0x05 /* PID = 0x05dc */
- อ่านหลักเกณฑ์การตั้งค่า VID/PID เพิ่มเติมได้จากหัวข้อ #เกี่ยวกับหมายเลข VID/PID
- Vendor Name: กำหนดค่า USB Vendor Name ให้เป็น
cpe.ku.ac.th
โดยเปลี่ยนนิยามของมาโครUSB_CFG_VENDOR_NAME
และUSB_CFG_VENDOR_NAME_LEN
ดังนี้
#define USB_CFG_VENDOR_NAME 'c','p','e','.','k','u','.','a','c','.','t','h' #define USB_CFG_VENDOR_NAME_LEN 12
- Device Name: กำหนดค่า USB Device Name ให้เป็น ID <รหัสนิสิต> โดยเปลี่ยนนิยามของมาโคร
USB_CFG_DEVICE_NAME
และUSB_CFG_DEVICE_NAME_LEN
ตัวอย่างเช่นนิสิตที่มีรหัสประจำตัว 1234567890 ให้แก้ไขมาโครดังนี้
#define USB_CFG_DEVICE_NAME 'I','D',' ','1','2','3','4','5','6','7','8','9','0' #define USB_CFG_DEVICE_NAME_LEN 13
เฟิร์มแวร์ฝั่งอุปกรณ์
- เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ
main.c
ดังนี้
#define RQ_SET_LED 0 #define RQ_SET_LED_VALUE 1 #define RQ_GET_SWITCH 2 #define RQ_GET_LIGHT 3
- ภายในฟังก์ชัน
usbFunctionSetup
สร้างโค้ดสำหรับประมวลผลคำร้องขอที่รับมาจากโฮสท์ดังนี้
usbMsgLen_t usbFunctionSetup(uint8_t data[8]) { usbRequest_t *rq = (usbRequest_t*)data; static uint8_t switch_state; /* เป็น static เพื่อค่าในตัวแปรคงอยู่แม้ฟังก์ชัน return ไปแล้ว */ /* ประมวลผลตามหมายเลขคำสั่งที่อยู่ใน bRequest */ if (rq->bRequest == RQ_SET_LED) { uint8_t led_val = rq->wValue.bytes[0]; uint8_t led_no = rq->wIndex.bytes[0]; if (led_val) PORTC |= (1<<led_no); else PORTC &= ~(1<<led_no); return 0; /* ไม่ส่งข้อมูลกลับ */ } else if (rq->bRequest == RQ_GET_SWITCH) { if ((PINC & (1<<PC3)) == 0) /* switch is pressed */ switch_state = 1; else switch_state = 0; /* ให้ usbMsgPtr ชี้ไปที่ตำแหน่งหน่วยความจำที่เก็บข้อมูล */ usbMsgPtr = (uchar*) &switch_state; /* ส่งข้อมูลกลับ 1 ไบท์ (เท่ากับขนาดของตัวแปร switch_state) */ return sizeof(switch_state); } return 0; /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */ }
- คอมไพล์เฟิร์มแวร์และอัพโหลดเข้าแฟลชของไมโครคอนโทรลเลอร์ ซึ่งหากได้สร้าง
Makefile
ตามที่อธิบายไว้ข้างต้นไว้เรียบร้อยแล้ว ให้เสียบบอร์ดไมโครคอนโทรลเลอร์เข้ากับพอร์ท USB กดสวิตช์เพื่อเข้าสู่ Bootloader แล้วพิมพ์คำสั่ง
make flash
แอพลิเคชันฝั่งโฮสท์
ขณะที่เฟิร์มแวร์ทำงานอยู่นั้นเราจะมองไม่เห็นผลลัพธ์การทำงานใด ๆ เนื่องจากเฟิร์มแวร์ถูกเขียนไว้ให้ตอบสนองต่อการสั่งงานผ่านคอมพิวเตอร์เท่านั้น ภายในไฟล์ตัวอย่างมีไฟล์ชื่อ practicum.py ซึ่งเป็นโมดูลไพทอนที่เราจะนำมาใช้ติดต่อกับบอร์ดไมโครคอนโทรลเลอร์ผ่านภาษาไพทอน ทดลองเปิดไพทอนเชลล์แล้วโหลดโมดูลมาใช้งาน โดยฟังก์ชันหลักที่เรียกใช้จากโมดูลคือ find_mcu_boards() ซึ่งคืนค่าเป็นลิสต์ของอุปกรณ์ USB ทุกตัวที่มี VID/PID เป็น 16c0:05dc ที่ต่ออยู่กับคอมพิวเตอร์ ณ ขณะนั้น
$ python >>> from practicum import find_mcu_boards,McuBoard >>> devices = find_mcu_boards() >>> devices [<usb.Device object at 0xd91d70>]
จากนั้นสร้างอ็อบเจกต์ของคลาส McuBoard ขึ้นมาจากอุปกรณ์ตัวแรกในลิสต์
>>> mcu = McuBoard(devices[0]) >>> mcu.handle.getString(mcu.device.iManufacturer, 256) 'cpe.ku.ac.th' >>> mcu.handle.getString(mcu.device.iProduct, 256) 'ID 1234567890' <- ต้องขึ้นเป็นรหัสนิสิตของตน
ทดลองส่งคำร้องขอหมายเลข 0 (ควบคุมสถานะ LED) เพื่อให้ LED หมายเลข 2 บนบอร์ดพ่วงติดสว่าง ใช้เมท็อต usb_write
ในคลาส McuBoard
ดังนี้
>>> mcu.usb_write(0, index=2, value=1)
คำสั่งด้านล่างมีผลทำให้ LED หมายเลข 2 ดับ และ LED หมายเลข 1 ติดขึ้นมาแทน
>>> mcu.usb_write(0, index=2, value=0) >>> mcu.usb_write(0, index=1, value=1)
ทดลองอ่านสถานะของสวิตช์โดยส่งคำร้องหมายเลข 2 ไปยังบอร์ด MCU
>>> mcu.usb_read(2, length=1) array('B', [0])
ค่าที่เมทอด usb_read
คืนกลับมาจะเป็นทูเปิลที่มีสมาชิกหนึ่งตัว ตามที่ระบุในเฟิร์มแวร์
ทดลองกดสวิตช์บนบอร์ดพ่วงค้างไว้ แล้วส่งคำร้องไปยังบอร์ด MCU ใหม่ ผลลัพธ์ที่ได้ควรเป็นดังนี้
>>> mcu.usb_read(2, length=1) array('B', [1])
ใช้คำสั่ง help เพื่อดูรายละเอียดการใช้งานคลาส McuBoard
>>> help(McuBoard)
เกี่ยวกับหมายเลข VID/PID
ชุดตัวเลข VID/PID ที่กำหนดให้กับอุปกรณ์ USB ไม่ควรตั้งเอาเองตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกซอฟต์แวร์ไดรเวอร์ที่จะมาควบคุมอุปกรณ์ โดยทั่วไปการจะได้มาซึ่งเลข VID/PID เพื่อใช้กับอุปกรณ์ที่เราสร้างขึ้นจำเป็นต้องสมัครเป็นสมาชิกของ USB Implementers Forum (ค่าสมาชิกปีละ 4,000 เหรียญสหรัฐ) หรือซื้อตัวเลข VID มาจากผู้ที่เป็นสมาชิกอีกทีหนึ่ง
อย่างไรก็ตาม Object Development ผู้พัฒนาไลบรารี V-USB ได้เตรียมชุดตัวเลข VID/PID ไว้ให้เราใช้งานโดยไม่เสียค่าใช้จ่าย หมายเลข 16C0:xxxx ที่เราเลือกนำมาใช้งานก็ได้มาจากตัวเลขในชุดดังกล่าว รายละเอียดเพิ่มเติมเกี่ยวกับการกำหนดค่า VID และ PID ให้กับอุปกรณ์ USB รวมถึงหลักเกณฑ์การปฏิบัติในการผลิตอุปกรณ์ USB สู่สาธารณะ สามารถศึกษาเพิ่มเติมได้จากเนื้อหาในไฟล์ USB-ID-FAQ.txt และไฟล์ USB-IDs-for-free.txt ในไดเรคตอรี usbdrv ที่ได้จากการติดตั้งไลบรารี V-USB รวมถึงเอกสาร How to obtain an USB VID/PID for your project
ข้อมูลเพิ่มเติม
- V-USB Documentation Wiki
- USB in a NutShell
- usb4java - ไลบรารีภาษาจาวาสำหรับติดต่อกับอุปกรณ์ USB