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

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
 
(ไม่แสดง 54 รุ่นระหว่างกลางโดยผู้ใช้ 5 คน)
แถว 1: แถว 1:
 +
: ''วิกินี้เป็นส่วนหนึ่งของรายวิชา [[01204223]]''
 +
 
ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมแฟลชเท่านั้น วิกินี้อธิบายถึงขั้นตอนการทำให้ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ เพื่อที่จะสามารถสื่อสารกับโปรแกรมที่ทำงานอยู่บนเครื่องคอมพิวเตอร์ได้
 
ที่ผ่านมานั้นเราใช้พอร์ท 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 ที่ไม่สังกัดคลาสใด โดยซอฟต์แวร์ฝั่งคอมพิวเตอร์จะอยู่ภายใต้ความควบคุมของเราทั้งหมด
 
ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ [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) class device ซึ่งอยู่ในกลุ่มเดียวกับอุปกรณ์จำพวกแป้นพิมพ์ เมาส์ จอยสติ๊ก และเกมแพด
+
นอกเหนือจาก custom class device แล้ว ไลบรารี V-USB ยังรองรับการโปรแกรมเฟิร์มแวร์ให้สังกัดคลาสอื่นได้อีกหลายคลาส อาทิเช่น HID (Human Interface Device) ซึ่งอยู่ในกลุ่มเดียวกับอุปกรณ์จำพวกแป้นพิมพ์ เมาส์ จอยสติ๊ก และเกมแพด
 +
 
 +
==ไลบรารีและเครื่องมือที่จำเป็น==
 +
ให้แน่ใจว่าได้ติดตั้งไลบรารีและเครื่องมือที่จำเป็นตามที่ได้อธิบายไว้ในวิกิด้านล่าง ก่อนเริ่มทำตามขั้นตอนในวิกินี้
 +
* [[การติดตั้งไลบรารี PyUSB]]
  
 
== ขั้นตอนการใช้งานไลบรารี V-USB ==
 
== ขั้นตอนการใช้งานไลบรารี V-USB ==
* ดาวน์โหลดซอร์สโค้ดจาก [http://www.obdev.at/downloads/vusb/vusb-20090822.tar.gz Objective Development]
+
* ดาวน์โหลดซอร์สโค้ดจาก [http://www.obdev.at/downloads/vusb/vusb-20121206.tar.gz Objective Development]
 
* แตกไฟล์ .tar.gz ที่ดาวน์โหลดมาโดยใช้คำสั่ง
 
* แตกไฟล์ .tar.gz ที่ดาวน์โหลดมาโดยใช้คำสั่ง
  tar zxf vusb-20090822.tar.gz
+
  tar zxf vusb-20121206.tar.gz
* คัดลอกไดเรคตอรี <code>usbdrv/</code> ที่อยู่ในไดเรคตอรี <code>vusb-2009-0822/</code> ไปวางในไดเรคตอรีโปรเจ็คของตน
+
* คัดลอกไดเรคตอรี <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
แถว 34: แถว 40:
 
  }
 
  }
 
* นิยามฟังก์ชัน <code>usbFunctionSetup</code> เพื่อประมวลผลข้อมูลที่รับมาจากเครื่องคอมพิวเตอร์ผ่านทางพอร์ท USB
 
* นิยามฟังก์ชัน <code>usbFunctionSetup</code> เพื่อประมวลผลข้อมูลที่รับมาจากเครื่องคอมพิวเตอร์ผ่านทางพอร์ท USB
  usbMsgLen_t usbFunctionSetup(uchar data[8])
+
  usbMsgLen_t usbFunctionSetup(uint8_t data[8])
 
  {
 
  {
     :
+
     ;
 
  }
 
  }
 
:ฟังก์ชันนี้จะถูกเรียกทำงานโดยอัตโนมัติจากฟังก์ชัน <code>usbPoll</code> เมื่อทางฝั่งคอมพิวเตอร์ส่งคำร้องขอผ่านมาทางพอร์ท USB หัวข้อ [[#ตัวอย่างโปรแกรม]] แสดงตัวอย่างการการเขียนฟังก์ชันนี้เอาไว้
 
:ฟังก์ชันนี้จะถูกเรียกทำงานโดยอัตโนมัติจากฟังก์ชัน <code>usbPoll</code> เมื่อทางฝั่งคอมพิวเตอร์ส่งคำร้องขอผ่านมาทางพอร์ท USB หัวข้อ [[#ตัวอย่างโปรแกรม]] แสดงตัวอย่างการการเขียนฟังก์ชันนี้เอาไว้
แถว 51: แถว 57:
 
   
 
   
 
  flash: $(TARGET)
 
  flash: $(TARGET)
     avrdude -p atmega168 -c usbasp -U flash:w:$(TARGET)
+
     avrdude -p $(MCU) -c usbasp -U flash:w:$(TARGET)
 
   
 
   
 
  %.hex: %.elf
 
  %.hex: %.elf
     avr-objcopy -j .text -O ihex $< $@
+
     avr-objcopy -j .text -j .data -O ihex $< $@
 
   
 
   
 
  %.elf: %.o $(OBJS)
 
  %.elf: %.o $(OBJS)
แถว 70: แถว 76:
 
     rm -f *~
 
     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 เรียกคำสั่งนี้อัตโนมัติ
+
หากดูในกฎที่ระบุกลไกการสร้างไฟล์ <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
 
  avr-gcc -Wall -Os -DF_CPU=16000000L -Iusbdrv -I. -mmcu=atmega168 -o main.elf main.o usbdrv/usbdrv.o usbdrv/usbdrvasm.o
  
แถว 76: แถว 82:
 
ในสถาปัตยกรรม 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 หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์
 +
 +
ดูรายละเอียดเพิ่มเติมได้จาก [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 คำร้องขอ ดังนี้
 
เพื่อให้เห็นภาพของการใช้งานไลบรารี V-USB มากขึ้น เราลองสร้างตัวอย่างเฟิร์มแวร์อย่างง่ายขึ้นมาพร้อมทั้งใช้ภาษาไพธอนทดลองสั่งงานจากฝั่งคอมพิวเตอร์ ในที่นี้เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำร้องขอ ดังนี้
 
* '''คำร้องขอ SET_LED''' สั่งให้ LED ดวงที่ระบุติดหรือดับ มีรายละเอียดของคำร้องขอดังนี้
 
* '''คำร้องขอ SET_LED''' สั่งให้ LED ดวงที่ระบุติดหรือดับ มีรายละเอียดของคำร้องขอดังนี้
แถว 123: แถว 134:
 
  #define USB_CFG_VENDOR_NAME_LEN 12
 
  #define USB_CFG_VENDOR_NAME_LEN 12
  
* '''Device Name:''' กำหนดค่า USB Device Name ให้เป็น ''Practicum Group <หมายเลขกลุ่ม>'' โดยเปลี่ยนนิยามของมาโคร <code>USB_CFG_DEVICE_NAME</code> และ <code>USB_CFG_DEVICE_NAME_LEN</code> ตัวอย่างเช่น กลุ่ม 99 ให้แก้ไขมาโครดังนี้
+
* '''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    'P','r','a','c','t','i','c','u','m',' ','G','r','o','u','p',' ','9','9'
+
  #define USB_CFG_DEVICE_NAME    'I','D',' ','1','2','3','4','5','6','7','8','9','0'
  #define USB_CFG_DEVICE_NAME_LEN 18
+
  #define USB_CFG_DEVICE_NAME_LEN 13
  
===โปรแกรมฝั่งเฟิร์มแวร์===
+
=== เฟิร์มแวร์ฝั่งอุปกรณ์ ===
 
* เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ <code>main.c</code> ดังนี้
 
* เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ <code>main.c</code> ดังนี้
  #define VENDOR_RQ_SET_LED    0
+
  #define RQ_SET_LED        0
  #define VENDOR_RQ_GET_SWITCH 1
+
  #define RQ_SET_LED_VALUE  1
 +
#define RQ_GET_SWITCH      2
 +
#define RQ_GET_LIGHT      3
  
* เราสร้างฟังก์ชันจัดการอินพุท/เอาท์พุทของพอร์ท C เพื่อความสะดวกในการใช้งานไว้ดังนี้
+
* ภายในฟังก์ชัน <code>usbFunctionSetup</code> สร้างโค้ดสำหรับประมวลผลคำร้องขอที่รับมาจากโฮสท์ดังนี้
uint8_t in_c(uint8_t pin)
 
{
 
    // กำหนดให้ขาที่ระบุของพอร์ท C ทำหน้าที่เป็นอินพุท
 
    DDRC &= ~(1<<pin);
 
    // ดึงให้ขามีลอจิก 1 หากขาถูกปล่อยลอยไว้
 
    PORTC |= (1<<pin);
 
    // อ่านสถานะลอจิกของขา
 
    return ((PINC & (1<<pin))>>pin);   
 
}
 
 
void out_c(uint8_t pin, uint8_t val)
 
{
 
    // กำหนดให้ขาที่ระบุของพอร์ท C ทำหน้าที่เป็นเอาท์พุท
 
    DDRC |= 1<<pin;
 
    // เซ็ตลอจิกของขาเป็น 1 ถ้า val ไม่ใช่ศูนย์ ไม่เช่นนั้นเซ็ตให้เป็น 0
 
    if (val)
 
        PORTC |= 1<<pin;
 
    else
 
        PORTC &= ~(1<<pin);
 
}
 
  
* ภายในฟังก์ชัน <code>usbFunctionSetup</code> สร้างโค้ดสำหรับประมวลผลคำร้องขอที่รับมาจากโฮสท์ดังนี้
+
  usbMsgLen_t usbFunctionSetup(uint8_t data[8])
  usbMsgLen_t usbFunctionSetup(uchar data[8])
 
 
  {
 
  {
     usbRequest_t *rq = (void *)data;
+
     usbRequest_t *rq = (usbRequest_t*)data;
     static uchar dataBuffer[1];  /* ข้อมูลนี้ต้องไม่ถูกเขียนทับหลังจาก usbFunctionSetup รีเทิร์น */
+
     static uint8_t switch_state;  /* เป็น static เพื่อค่าในตัวแปรคงอยู่แม้ฟังก์ชัน return ไปแล้ว */
 
   
 
   
 
     /* ประมวลผลตามหมายเลขคำสั่งที่อยู่ใน bRequest */
 
     /* ประมวลผลตามหมายเลขคำสั่งที่อยู่ใน bRequest */
     if (rq->bRequest == VENDOR_RQ_SET_LED)
+
     if (rq->bRequest == RQ_SET_LED)
 
     {
 
     {
         uint8_t led_no  = rq->wValue.bytes[0];
+
         uint8_t led_val = rq->wValue.bytes[0];
         uint8_t led_val = rq->wValue.bytes[1];
+
         uint8_t led_no  = rq->wIndex.bytes[0];
         out_c(led_no, led_val);
+
         return 0;
+
        if (led_val)
 +
            PORTC |= (1<<led_no);
 +
         else
 +
            PORTC &= ~(1<<led_no);
 +
 +
         return 0; /* ไม่ส่งข้อมูลกลับ */
 
     }
 
     }
     else if (rq->bRequest == VENDOR_RQ_GET_SWITCH)
+
 +
     else if (rq->bRequest == RQ_GET_SWITCH)
 
     {
 
     {
         dataBuffer[0] = !in_c(3); /* กลับลอจิกเพื่อให้ 0 = ปล่อย 1 = กด */
+
         if ((PINC & (1<<PC3)) == 0) /* switch is pressed */
         usbMsgPtr = dataBuffer;   /* ระบุว่าข้อมูลส่งกลับอยู่ใน dataBuffer */
+
            switch_state = 1;
         return 1;                 /* ความยาวข้อมูลส่งกลับเท่ากับ 1 */
+
        else
 +
            switch_state = 0;
 +
 +
        /* ให้ usbMsgPtr ชี้ไปที่ตำแหน่งหน่วยความจำที่เก็บข้อมูล */
 +
         usbMsgPtr = (uchar*) &switch_state;
 +
 +
        /* ส่งข้อมูลกลับ 1 ไบท์ (เท่ากับขนาดของตัวแปร switch_state) */
 +
         return sizeof(switch_state);
 
     }
 
     }
 +
 
     return 0;  /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */
 
     return 0;  /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */
 
  }
 
  }
  
* คอมไพล์เฟิร์มแวร์และอัพโหลดเข้าแฟลชของไมโครคอนโทรลเลอร์ ซึ่งหากได้สร้าง <code>Makefile</code> ตามที่อธิบายไว้ข้างต้นไว้เรียบร้อยแล้ว ให้พิมพ์คำสั่ง
+
* คอมไพล์เฟิร์มแวร์และอัพโหลดเข้าแฟลชของไมโครคอนโทรลเลอร์ ซึ่งหากได้สร้าง <code>Makefile</code> ตามที่อธิบายไว้ข้างต้นไว้เรียบร้อยแล้ว ให้เสียบบอร์ดไมโครคอนโทรลเลอร์เข้ากับพอร์ท USB กดสวิตช์เพื่อเข้าสู่ Bootloader แล้วพิมพ์คำสั่ง
 
  make flash
 
  make flash
  
=== โปรแกรมฝั่งเครื่องคอมพิวเตอร์ ===
+
=== แอพลิเคชันฝั่งโฮสท์ ===
 +
 
 +
ขณะที่เฟิร์มแวร์ทำงานอยู่นั้นเราจะมองไม่เห็นผลลัพธ์การทำงานใด ๆ เนื่องจากเฟิร์มแวร์ถูกเขียนไว้ให้ตอบสนองต่อการสั่งงานผ่านคอมพิวเตอร์เท่านั้น ภายในไฟล์ตัวอย่างมีไฟล์ชื่อ <tt>practicum.py</tt> ซึ่งเป็นโมดูลไพทอนที่เราจะนำมาใช้ติดต่อกับบอร์ดไมโครคอนโทรลเลอร์ผ่านภาษาไพทอน ทดลองเปิดไพทอนเชลล์แล้วโหลดโมดูลมาใช้งาน โดยฟังก์ชันหลักที่เรียกใช้จากโมดูลคือ <tt>find_mcu_boards()</tt> ซึ่งคืนค่าเป็นลิสต์ของอุปกรณ์ USB ทุกตัวที่มี VID/PID เป็น 16c0:05dc ที่ต่ออยู่กับคอมพิวเตอร์ ณ ขณะนั้น
  
* ติดตั้ง PyUSB เพื่อติดต่อกับอุปกรณ์ USB ผ่านภาษาไพธอน
+
$ python
  sudo apt-get install python-usb
+
  >>> from practicum import find_mcu_boards,McuBoard
 +
>>> devices = find_mcu_boards()
 +
>>> devices
 +
[<usb.Device object at 0xd91d70>]
  
* สร้างไฟล์ชื่อ <code>testusb.py</code> เรียกใช้ไลบรารี usb และนิยามฟังก์ชัน <code>find_board</code> เพื่อค้นหาอุปกรณ์ USB ตัวที่เป็นบอร์ด MCU ของเราจากรายการอุปกรณ์ USB ทั้งหมดที่ต่ออยู่กับเครื่อง
+
จากนั้นสร้างอ็อบเจกต์ของคลาส <tt>McuBoard</tt> ขึ้นมาจากอุปกรณ์ตัวแรกในลิสต์
import usb
 
 
def find_board():
 
    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_board</code> จะคืนอุปกรณ์ที่พบว่ามี VID:PID เป็น 16c0:05dc เป็นตัวแรกเสมอ อย่างไรก็ตาม หากต้องการระบุเอาอุปกรณ์ตัวที่ระบุก็สามารถอาศัยข้อมูลจาก vendor name หรือ device name มากำหนดเงื่อนไขเพิ่ม
 
  
* ติดต่อกับบอร์ด MCU ผ่านทางไพธอนเชลล์ โดยเรียกโปรแกรม <code>testusb.py</code> แบบอินเทอแรคทีฟ
+
>>> mcu = McuBoard(devices[0])
  python -i testusb.py
+
  >>> mcu.handle.getString(mcu.device.iManufacturer, 256)
:<span style="color:red;">'''หมายเหตุ:''' หากพบปัญหาเกี่ยวกับสิทธิการเข้าถึงอุปกรณ์ USB ให้ดำเนินตามขั้นตอนที่อธิบายไว้ในเอกสาร [[การแก้ไขสิทธิการเข้าถึงพอร์ท USB ของบอร์ด MCU]]</span>
+
'cpe.ku.ac.th'
 +
>>> mcu.handle.getString(mcu.device.iProduct, 256)
 +
'ID 1234567890' <- ต้องขึ้นเป็นรหัสนิสิตของตน
  
* สร้าง handle เพื่อใช้เป็นทางผ่านในการสื่อสารกับบอร์ด MCU โดยใช้คำสั่งดังนี้
+
ทดลองส่งคำร้องขอหมายเลข 0 (ควบคุมสถานะ LED) เพื่อให้ LED หมายเลข 2 บนบอร์ดพ่วงติดสว่าง ใช้เมท็อต <code>usb_write</code> ในคลาส <code>McuBoard</code> ดังนี้
>>> board = find_board()
 
>>> handle = board.open()
 
  
* สร้างคำร้องขอสำหรับคำสั่ง SET_LED ซึ่งระบุชนิดของคำร้องเป็นแบบ Vendor คือผู้สร้างอุปกรณ์กำหนดขึ้นมาเอง และระบุว่าคำร้องนี้มีการไหลของข้อมูลแบบ Host to Device
+
  >>> mcu.usb_write(0, index=2, value=1)
  >>> req = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_OUT
 
  
* ส่งคำร้องออกไปยังบอร์ด MCU โดยกำหนดให้หมายเลขคำร้องเป็น 0 (ซึ่งหมายถึง SET_LED) และให้ฟิลด์ <code>value</code> มีค่าเป็น 0x0102 ซึ่งเฟิร์มแวร์ของเราจะตีความว่าเป็นการสั่งให้ LED หมายเลข 2 บนบอร์ดพ่วงติด การร้องขอครั้งนี้ไม่ได้ส่งข้อมูลใด ๆ ไปเพิ่มเติม จึงให้พารามิเตอร์ buf ของ handle.ControlMsg เป็น None ไป
+
คำสั่งด้านล่างมีผลทำให้ LED หมายเลข 2 ดับ และ LED หมายเลข 1 ติดขึ้นมาแทน
  >>> handle.controlMsg(req, 0, None, value=0x0102)
+
>>> mcu.usb_write(0, index=2, value=0)
 +
  >>> mcu.usb_write(0, index=1, value=1)
  
* ลองให้ LED หมายเลข 2 ดับ และ LED หมายเลข 1 ติดขึ้นมาแทน
+
ทดลองอ่านสถานะของสวิตช์โดยส่งคำร้องหมายเลข 2 ไปยังบอร์ด MCU
  >>> handle.controlMsg(req, 0, None, value=0x0002)
+
  >>> mcu.usb_read(2, length=1)
  >>> handle.controlMsg(req, 0, None, value=0x0101)
+
  array('B', [0])
 +
ค่าที่เมทอด <code>usb_read</code> คืนกลับมาจะเป็นทูเปิลที่มีสมาชิกหนึ่งตัว ตามที่ระบุในเฟิร์มแวร์
  
* ทดลองอ่านสถานะของสวิตช์โดยส่งคำร้องหมายเลข 1 ไปยังบอร์ด MCU
+
ทดลองกดสวิตช์บนบอร์ดพ่วงค้างไว้ แล้วส่งคำร้องไปยังบอร์ด MCU ใหม่ ผลลัพธ์ที่ได้ควรเป็นดังนี้
  >>> req = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_IN
+
  >>> mcu.usb_read(2, length=1)
>>> handle.controlMsg(req, 1, 1)
+
  array('B', [1])
  (0,)
 
:* เนื่องจากคำร้องนี้เป็นการขอให้ MCU ส่งข้อมูลกลับ จึงต้องมีการระบุในคำร้องโดยใช้ usb.ENDPOINT_IN
 
:* ในกรณีของการร้องขอข้อมูลจากอุปกรณ์ พารามิเตอร์ buf (พารามิเตอร์ตัวที่สองของเมท็อด controlMsg) เป็นความยาวข้อมูลที่ต้องการร้องขอ ซึ่งเท่ากับหนึ่ง
 
:* ค่าที่เมทอด controlMsg คืนกลับมาจะเป็น tuple ความยาวหนึ่งเช่นกัน
 
  
* ทดลองกดสวิตช์บนบอร์ดพ่วง แล้วส่งคำร้องไปยังบอร์ด MCU ใหม่ ผลลัพธ์ที่ได้ควรเป็นดังนี้
+
ใช้คำสั่ง <tt>help</tt> เพื่อดูรายละเอียดการใช้งานคลาส <tt>McuBoard</tt>
  >>> handle.controlMsg(req, 1, 1)
+
  >>> help(McuBoard)
(1,)
 
  
 
== เกี่ยวกับหมายเลข VID/PID ==
 
== เกี่ยวกับหมายเลข VID/PID ==
ชุดตัวเลข VID/PID ที่กำหนดให้กับอุปกรณ์ USB ไม่ควรตั้งเอาเองตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกซอฟต์แวร์ไดรเวอร์ที่จะมาควบคุมอุปกรณ์ USB
+
ชุดตัวเลข VID/PID ที่กำหนดให้กับอุปกรณ์ USB ไม่ควรตั้งเอาเองตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกซอฟต์แวร์ไดรเวอร์ที่จะมาควบคุมอุปกรณ์ โดยทั่วไปการจะได้มาซึ่งเลข VID/PID เพื่อใช้กับอุปกรณ์ที่เราสร้างขึ้นจำเป็นต้องสมัครเป็นสมาชิกของ [http://www.usb.org USB Implementers Forum] (ค่าสมาชิกปีละ 4,000 เหรียญสหรัฐ) หรือซื้อตัวเลข VID มาจากผู้ที่เป็นสมาชิกอีกทีหนึ่ง
  
ข้อมูลเพิ่มเติมเกี่ยวกับการกำหนดค่า VID และ PID ให้กับอุปกรณ์ USB รวมถึงหลักเกณฑ์การปฏิบัติในการผลิตอุปกรณ์ USB สู่สาธารณะ สามารถศึกษาเพิ่มเติมได้จากเนื้อหาในไฟล์ <code>vusb-20090822/USB-ID-FAQ.txt</code> และไฟล์ <code>vusb-20090822/USB-IDs-for-free.txt</code> ที่แจกจ่ายมากับไฟล์ <code>vusb-20090822.tar.gz</code> และเอกสาร [http://www.voti.nl/docs/usb-pid.html How to obtain an USB VID/PID for your project]
+
อย่างไรก็ตาม 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

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