ผลต่างระหว่างรุ่นของ "การจำลองบอร์ด MCU เป็นอุปกรณ์ USB"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
(หน้าที่ถูกสร้างด้วย 'ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพล…')
 
 
(ไม่แสดง 96 รุ่นระหว่างกลางโดยผู้ใช้ 5 คน)
แถว 1: แถว 1:
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลช เราสามารถสั่งงานไมโครคอนโทรลเลอร์ให้จำลองการทำงานเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้
+
: ''วิกินี้เป็นส่วนหนึ่งของรายวิชา [[01204223]]''
  
ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ [[http://www.obdev.at/products/vusb/index.html V-USB]] (หรือ AVRUSB) ที่พัฒนาโดยบริษัท [[http://www.obdev.at/index.html Objective Development]]
+
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้
 +
 
 +
ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ [http://www.obdev.at/products/vusb/index.html V-USB] (เดิมเรียกว่า AVR-USB) ที่พัฒนาโดยบริษัท [http://www.obdev.at/index.html Objective Development] โดยทำให้บอร์ด MCU ของเราทำงานเป็นอุปกรณ์ที่อยู่ในกลุ่ม [http://vusb.wikidot.com/usb-device-classes custom class device] ซึ่งจัดเป็นอุปกรณ์ USB ที่ไม่สังกัดคลาสใด โดยซอฟต์แวร์ฝั่งคอมพิวเตอร์จะอยู่ภายใต้ความควบคุมของเราทั้งหมด
 +
 
 +
นอกเหนือจาก custom class device แล้ว ไลบรารี V-USB ยังรองรับการโปรแกรมเฟิร์มแวร์ให้สังกัดคลาสอื่นได้อีกหลายคลาส อาทิเช่น HID (Human Interface Device) ซึ่งอยู่ในกลุ่มเดียวกับอุปกรณ์จำพวกแป้นพิมพ์ เมาส์ จอยสติ๊ก และเกมแพด
 +
 
 +
==ไลบรารีและเครื่องมือที่จำเป็น==
 +
ให้แน่ใจว่าได้ติดตั้งไลบรารีและเครื่องมือที่จำเป็นตามที่ได้อธิบายไว้ในวิกิด้านล่าง ก่อนเริ่มทำตามขั้นตอนในวิกินี้
 +
* [[การติดตั้งไลบรารี PyUSB]]
 +
 
 +
== ขั้นตอนการใช้งานไลบรารี V-USB ==
 +
* ดาวน์โหลดซอร์สโค้ดจาก [http://www.obdev.at/downloads/vusb/vusb-20121206.tar.gz Objective Development]
 +
* แตกไฟล์ .tar.gz ที่ดาวน์โหลดมาโดยใช้คำสั่ง
 +
tar zxf vusb-20121206.tar.gz
 +
* คัดลอกไดเรคตอรี <code>usbdrv/</code> ที่อยู่ในไดเรคตอรี <code>vusb-20121206/</code> ไปวางในไดเรคตอรีโปรเจ็คของตน
 +
* ภายในไดเรคตอรีโปรเจ็คของตนเอง ย้ายไฟล์ <code>usbdrv/usbconfig-prototype.h</code> มาวางไว้ด้านนอก และเปลี่ยนชื่อให้เป็น <code>usbconfig.h</code>
 +
mv usbdrv/usbconfig-prototype.h ./usbconfig.h
 +
:ไฟล์นี้จะเก็บข้อมูลการตั้งค่าเกี่ยวกับอุปกรณ์ USB ที่จะให้ไมโครคอนโทรลเลอร์จำลองตัวเองขึ้นมา
 +
* ในไฟล์หลักของโปรเจ็ค เรียกใช้คำสั่ง <code>#include</code> ต่อไปนี้ไว้ที่ตอนต้นของโปรแกรม
 +
#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"
 +
* ในส่วนของฟังก์ชัน <code>main()</code> ต้องมีโครงสร้างหลักดังนี้ (สามารถใส่โค้ดอื่นเพิ่มได้ตามที่ต้องการ)
 +
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;
 +
}
 +
* นิยามฟังก์ชัน <code>usbFunctionSetup</code> เพื่อประมวลผลข้อมูลที่รับมาจากเครื่องคอมพิวเตอร์ผ่านทางพอร์ท USB
 +
usbMsgLen_t usbFunctionSetup(uint8_t data[8])
 +
{
 +
    ;
 +
}
 +
:ฟังก์ชันนี้จะถูกเรียกทำงานโดยอัตโนมัติจากฟังก์ชัน <code>usbPoll</code> เมื่อทางฝั่งคอมพิวเตอร์ส่งคำร้องขอผ่านมาทางพอร์ท USB หัวข้อ [[#ตัวอย่างโปรแกรม]] แสดงตัวอย่างการการเขียนฟังก์ชันนี้เอาไว้
 +
 
 +
== การคอมไพล์และลิ้งค์โปรแกรมรวมกับ V-USB ==
 +
สร้าง Makefile ต่อไปนี้เพื่อคอมไพล์โปรแกรมและดำเนินการลิ้งค์เข้ากับไลบรารี V-USB ซึ่งตัวอย่าง Makefile นี้สมมติว่าไฟล์หลักของโปรเจ็คคือ <code>main.c</code>
 +
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 *~
 +
 
 +
หากดูในกฎที่ระบุกลไกการสร้างไฟล์ <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=16000000L -Iusbdrv -I. -mmcu=atmega168 -o main.elf main.o usbdrv/usbdrv.o usbdrv/usbdrvasm.o
 +
 
 +
== โครงสร้างของคำร้องขอ USB ที่รับจากฝั่งคอมพิวเตอร์ ==
 +
ในสถาปัตยกรรม USB นั้นการสื่อสารจะถูกเริ่มจากการที่ฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ส่งคำร้องขอ (USB Request) ไปยังฝั่งอุปกรณ์ USB เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ข้อมูลคำร้องขอมีโครงสร้างตามที่นิยามไว้ในไฟล์ <code>usbdrv/usbdrv.h</code> ดังนี้
 +
typedef struct usbRequest{
 +
    uchar      bmRequestType;  /* 1 ไบต์ */
 +
    uchar      bRequest;      /* 1 ไบต์ */
 +
    usbWord_t  wValue;        /* 2 ไบต์ */
 +
    usbWord_t  wIndex;        /* 2 ไบต์ */
 +
    usbWord_t  wLength;        /* 2 ไบต์ */
 +
}usbRequest_t;
 +
* <code>bmRequestType</code> ประกอบด้วยฟิลด์ย่อย 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
 +
::ฟังก์ชัน <code>usbFunctionSetup</code> ที่เราต้องเขียนขึ้นนั้นจะถูกเรียกใช้เมื่อค่าในฟิลด์ Type นี้มีค่า 2 (Vendor) เท่านั้น
 +
:* บิต 4..0 ผู้รับ (Recipient)
 +
::* 0 = Device
 +
::* 1 = Interface
 +
::* 2 = Endpoint
 +
::* 3 = Other
 +
* <code>bRequest</code> ระบุหมายเลขคำร้องขอ คำร้องขอตามมาตรฐานของ USB นั้นมีประเภทเป็น Standard ซึ่งจะถูกประมวลผลจากไลบรารี V-USB อัตโนมัติ เราจึงไม่ต้องสนใจในส่วนนี้ ส่วนที่เราต้องรับผิดชอบคือคำร้องขอแบบ Vendor ซึ่งต้องถูกออกแบบไว้ล่วงหน้าแล้วว่าอุปกรณ์ USB ของเราจะรองรับคำร้องขอหมายเลขอะไรบ้าง โดยในฟังก์ชัน <code>usbFunctionSetup</code> ของเราต้องประมวลผลคำร้องขอเหล่านี้ได้ถูกต้อง
 +
* <code>wValue</code> และ <code>wIndex</code> ทั้งคู่เป็นฟิลด์ที่ไม่มีความหมายใดในกรณีที่คำร้องขอเป็นแบบ Vendor ดังนั้นเราจึงมีอิสระเต็มที่ในการใช้งานฟิลด์ทั้งคู่นี้เป็นตัวส่งรายละเอียดของคำร้องขอ ซึ่งส่งได้สูงสุด 4 ไบต์
 +
* <code>wLength</code> กำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ USB หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์
 +
 
 +
ดูรายละเอียดเพิ่มเติมได้จาก [http://www.beyondlogic.org/usbnutshell/usb6.htm USB in a NutShell]
 +
 
 +
== ตัวอย่างโปรแกรม ==
 +
 
 +
:โหลดไฟล์ [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 คำร้องขอ ดังนี้
 +
* '''คำร้องขอ 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===
 +
เปิดไฟล์ <code>usbconfig.h</code> เพื่อปรับค่าให้สอดคล้องกับโปรเจ็ค
 +
* '''VID/PID:''' อุปกรณ์ USB ทุกตัวจะต้องถูกกำหนดค่า Vendor ID (VID) และ Product ID (PID) ให้ ซึ่งแต่ละตัวเลขมีขนาด 16 บิต ในโปรเจ็คนี้ให้กำหนดค่า VID และ PID ให้เป็น 0x16c0 และ 0x05dc ตามลำดับ โดยดูให้แน่ใจว่าในไฟล์ <code>usbconfig.h</code> มีบรรทัดเหล่านี้
 +
#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 ให้เป็น <code>cpe.ku.ac.th</code> โดยเปลี่ยนนิยามของมาโคร <code>USB_CFG_VENDOR_NAME</code> และ <code>USB_CFG_VENDOR_NAME_LEN</code> ดังนี้
 +
#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 <รหัสนิสิต>'' โดยเปลี่ยนนิยามของมาโคร <code>USB_CFG_DEVICE_NAME</code> และ <code>USB_CFG_DEVICE_NAME_LEN</code> ตัวอย่างเช่นนิสิตที่มีรหัสประจำตัว 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
 +
 
 +
=== เฟิร์มแวร์ฝั่งอุปกรณ์ ===
 +
* เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ <code>main.c</code> ดังนี้
 +
#define RQ_SET_LED        0
 +
#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])
 +
{
 +
    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;  /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */
 +
}
 +
 
 +
* คอมไพล์เฟิร์มแวร์และอัพโหลดเข้าแฟลชของไมโครคอนโทรลเลอร์ ซึ่งหากได้สร้าง <code>Makefile</code> ตามที่อธิบายไว้ข้างต้นไว้เรียบร้อยแล้ว ให้เสียบบอร์ดไมโครคอนโทรลเลอร์เข้ากับพอร์ท USB กดสวิตช์เพื่อเข้าสู่ Bootloader แล้วพิมพ์คำสั่ง
 +
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 ที่กำหนดให้กับอุปกรณ์ 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://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

ข้อมูลเพิ่มเติม