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

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
แถว 1: แถว 1:
 
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้
 
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้
  
ใช้งานเป็น [http://vusb.wikidot.com/usb-device-classes custom class device] ซึ่งเป็นประเภทอุปกรณ์...
 
  
ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ [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> เมื่อทางฝั่งคอมพิวเตอร์ส่งคำสั่งผ่านมาทางพอร์ท USB
+
:ฟังก์ชันนี้จะถูกเรียกทำงานโดยอัตโนมัติจากฟังก์ชัน <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 เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม
+
ในสถาปัตยกรรม USB นั้นการสื่อสารจะถูกเริ่มจากการที่ฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ส่งคำร้องขอ (USB Request) ไปยังฝั่งอุปกรณ์ USB เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ข้อมูลคำร้องขอมีโครงสร้างตามที่นิยามไว้ในไฟล์ <code>usbdrv/usbdrv.h</code> ดังนี้
ข้อมูลคำสั่งที่ถูกส่งจากโฮสท์ เรียกว่า USB Request มีโครงสร้างตามที่นิยามไว้ในไฟล์ <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 ประเภทคำสั่ง (Type)
+
:* บิต 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> ระบุหมายเลขคำสั่ง ซึ่งต้องถูกออกแบบไว้ล่วงหน้าแล้วว่าอุปกรณ์ USB ของเราจะรองรับคำสั่งอะไรบ้าง โดยเฟิร์มแวร์ต้องประมวลผลคำสั่งเหล่านี้ได้ถูกต้อง
+
* <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 หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์
 
อุปกรณ์ USB
 
  
 
== ตัวอย่างโปรแกรม ==
 
== ตัวอย่างโปรแกรม ==
เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น เราลองสร้างตัวอย่างเฟิร์มแวร์อย่างง่ายขึ้นมาพร้อมทั้งโปรแกรมที่ทดลองสั่งงานจากฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ด้วยภาษาไพธอน
+
เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น เราลองสร้างตัวอย่างเฟิร์มแวร์อย่างง่ายขึ้นมาพร้อมทั้งใช้ภาษาไพธอนทดลองสั่งงานจากฝั่งคอมพิวเตอร์ ในที่นี้เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำร้องขอ ดังนี้
ในสถาปัตยกรรม USB นั้นการสื่อสารจะถูกกำหนดโดยฝั่งโฮสท์เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม
+
* '''คำร้องขอ SET_LED''' สั่งให้ LED ดวงที่ระบุติดหรือดับ มีรายละเอียดของคำร้องขอดังนี้
ดังนั้นหน้าที่ของเฟิร์มแวร์คือตีความคำสั่งที่ส่งมาจากโฮสท์และเตรียมข้อมูลที่จะส่งกลับไปให้โฮสท์
+
:* กำหนดให้หมายเลขคำร้องขอ (request number) คือ 0  
 
+
:* ส่งรายละเอียดมาให้ 2 ไบท์ ไบท์แรกระบุตำแหน่งของ LED บนบอร์ดพ่วง (0, 1 หรือ 2) ส่วนไบท์ที่สองระบุว่าจะให้ LED ดวงดังกล่าวติดหากมีค่า 0 หรือดับหากมีค่าอื่นที่ไม่ใช่ศูนย์ เนื่องจากข้อมูลมีขนาดเพียงสองไบท์ เราจะใส่ข้อมูลนี้ลงไปในฟิลด์ <code>wValue</code> ที่ถูกส่งไปพร้อมกับคำร้องขอโดยตรง
เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำสั่ง ดังนี้
+
:* แม้ไม่มีข้อมูลอื่นเพิ่มเติมส่งจากคอมพิวเตอร์ไปยังอุปกรณ์ USB แต่เราจะระบุทิศทางการไหลของข้อมูลไว้เป็น Host to Device
* '''คำสั่ง SET_LED''' สั่งให้ LED ดวงที่ระบุติดหรือดับ มีรายละเอียดของคำสั่งดังนี้
+
* '''คำร้องขอ GET_SWITCH''' สั่งให้บอร์ด MCU รายงานสถานะการกดปุ่มสวิตช์กลับมา มีรายละเอียดของคำร้องขอดังนี้
:* กำหนดให้หมายเลขคำสั่ง (request number) คือ 0  
+
:* กำหนดให้หมายเลขคำร้องขอ (request number) คือ 1  
:* การใช้งานจะระบุให้อุปกรณ์ USB เป็นเอาท์พุท
+
:* ทิศทางการไหลของข้อมูลเป็น Device to Host
:* ส่งพารามิเตอร์มาให้ 2 ไบท์ ไบท์แรกระบุตำแหน่งของ LED (0, 1 หรือ 2) ส่วนไบท์ที่สองระบุว่าจะให้ LED ดวงดังกล่าวติด (หากมีค่า 0) หรือดับ (หากมีค่าอื่นที่ไม่ใช่ศูนย์)
+
:* รับข้อมูลกลับมา 1 ไบท์ บอกสถานะของสวิตช์ (0 คือไม่ถูกกด 1 คือถูกกด)
* '''คำสั่ง GET_SWITCH''' สั่งให้บอร์ด MCU รายงานสถานะการกดปุ่มสวิตช์กลับมา มีรายละเอียดของคำสั่งดังนี้
 
:* กำหนดให้หมายเลขคำสั่ง (request number) คือ 1  
 
:* การใช้งานจะระบุให้อุปกรณ์ USB เป็นอินพุท
 
:* ไม่มีพารามิเตอร์ใดส่งไป แต่ตัวบอร์ด MCU ส่งรายงานกลับมา 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> ดังนี้
+
* เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ <code>main.c</code> ดังนี้
  #define SET_LED   0
+
  #define VENDOR_RQ_SET_LED   0
  #define GET_SWITCH 1
+
  #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;
+
    usbRequest_t *rq = (void *)data;
    static uchar dataBuffer[4];  /* buffer must stay valid when usbFunctionSetup returns */
+
    static uchar dataBuffer[1];  /* ข้อมูลนี้ต้องไม่ถูกเขียนทับหลังจาก usbFunctionSetup รีเทิร์น */
 
   
 
   
     if(rq->bRequest == SET_LED)
+
/* ประมวลผลตามหมายเลขคำสั่งที่อยู่ใน bRequest */
 +
     if (rq->bRequest == VENDOR_RQ_SET_LED)
 
     {
 
     {
         dataBuffer[0] = rq->wValue.bytes[0];
+
         uint8_t led_no  = rq->wValue.bytes[0];
         dataBuffer[1] = rq->wValue.bytes[1];
+
         uint8_t led_val = rq->wValue.bytes[1];
         dataBuffer[2] = rq->wIndex.bytes[0];
+
         out_c(led_no, led_val);
        dataBuffer[3] = rq->wIndex.bytes[1];
+
         return 0;
        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 */
+
    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 เพื่อให้ติดต่อกับอุปกรณ์ USB โดยใช้ภาษาไพธอนได้
 
 
  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,)

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