การติดต่อกับบอร์ด MCU ผ่าน USB ด้วย Arduino

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
วิกินี้เป็นส่วนหนึ่งของรายวิชา 01204223

ที่ผ่านมานั้นเราใช้พอร์ท USB เป็นเพียงแหล่งจ่ายพลังงานและโปรแกรมเฟิร์มแวร์เท่านั้น วิกินี้อธิบายถึงขั้นตอนและตัวอย่างการพัฒนาเฟิร์มแวร์ภายใต้สภาพแวดล้อมของ Arduino เพื่อให้บอร์ดไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB ความเร็วต่ำ สำหรับสื่อสารกับแอพลิเคชันที่ทำงานบนเครื่องคอมพิวเตอร์ได้

ไลบรารีและเครื่องมือที่จำเป็น

ให้แน่ใจว่าได้ติดตั้งไลบรารีและเครื่องมือที่จำเป็นตามที่ได้อธิบายไว้ในวิกิด้านล่าง ก่อนเริ่มทำตามขั้นตอนในวิกินี้

การใช้งานไลบรารี V-USB

การเขียนโค้ดเพื่อเรียกใช้งานไลบรารี V-USB ภายใต้สภาพแวดล้อมของ Arduino มีขั้นตอนหลัก ๆ ดังนี้

  • สร้างไฟล์ usbconfig.h เพื่อบอกไลบรารี V-USB ถึงคุณลักษณะของอุปกรณ์ USB ที่เราต้องการให้บอร์ด MCU จำลองตัวเองขึ้นมา เนื่องจากการตั้งค่าต่าง ๆ ถูกระบุไว้ในรูปมาโครเป็นจำนวนมาก วิธีที่สะดวกและเสี่ยงต่อความผิดพลาดน้อยที่สุดคือคัดลอกเนื้อหามาจากไฟล์ usbconfig-prototype.h ที่อยู่ในไดเรคตอรี usbdrv ที่ได้จากการติดตั้ง V-USB ตามขั้นตอนก่อนหน้านี้ การตั้งค่าหลัก ๆ ที่สำคัญได้แก่
    • USB_CFG_VENDOR_ID และ USB_CFG_DEVICE_ID ใช้กำหนดค่า Vendor ID (VID) และ Product ID (PID) ให้กับอุปกรณ์ USB ตัวเลขคู่นี้จะถูกตีความโดยระบบปฏิบัติการว่าเป็นอุปกรณ์ USB ประเภทใด เช่นเครื่องพิมพ์ เมาส์ คียบอร์ด ฯลฯ เพื่อที่ตัวระบบปฏิบัติการจะได้จัดหาตัวขับเคลื่อนอุปกรณ์ (device driver) มาใช้งานได้อย่างเหมาะสม ในตัวอย่างนี้มีการกำหนดค่า VID และ PID ให้เป็น 0x16c0 และ 0x05dc ตามลำดับ ซึ่งเป็นการไม่ระบุประเภทอุปกรณ์ ดูข้อมูลเพิ่มเติมจากหัวข้อ #เกี่ยวกับหมายเลข VID/PID
    • USB_CFG_VENDOR_NAME ใช้กำหนดชื่อผู้ผลิตอุปกรณ์ที่จะปรากฏให้เห็นผ่านระบบปฏิบัติการ ระบุในรูปรายการอักขระคั่นด้วยคอมม่า พร้อมทั้งระบุความยาวชื่อให้กับมาโคร USB_CFG_VENDOR_NAME_LEN ในที่นี้เราจะกำหนดชื่อผู้ผลิตเป็น cpe.ku.ac.th เพื่อให้สอดคล้องกับแนวปฏิบัติของไลบรารี V-USB
    • USB_CFG_DEVICE_NAME ใช้กำหนดชื่อของอุปกรณ์ที่จะปรากฏให้เห็นผ่านระบบปฏิบัติการ ระบุในรูปรายการอักขระคั่นด้วยคอมม่า พร้อมทั้งระบุความยาวชื่อให้กับมาโคร USB_CFG_DEVICE_NAME_LEN ในที่นี้ให้กำหนดชื่อในรูป ID xxxxxxxxxx โดยที่ xxxxxxxxxx แทนรหัสนิสิตของตน
  • สร้าง Arduino Sketch ขึ้นมาใหม่ แล้วพิมพ์คำสั่งต่อไปนี้ที่ส่วนหัวของไฟล์
extern "C" {
#include "usbdrv.h"
}
คำสั่งข้างต้นเป็นการเรียกไลบรารี V-USB มาใช้งาน แต่เนื่องจากไลบรารี V-USB ออกแบบไว้ใช้กับภาษา C ในขณะที่โค้ด Arduino เป็นภาษา C++ จึงต้องครอบไว้ด้วยคำสั่ง extern ดังที่เห็น
  • นิยามฟังก์ชัน setup() เพื่อกำหนดหน้าที่ของขาอินพุทเอาท์พุทตามปกติ และเพิ่มโค้ดสำหรับสั่งไลบรารี V-USB ให้เตรียมการเบื้องต้นลงไปด้วยดังนี้
void setup()
{
  // ตั้งค่าอินพุท/เอาท์พุทตามปกติ
  // :

  // สั่งให้ V-USB เตรียมตัวขั้นต้น
  usbInit();
  usbDeviceDisconnect();
  delay(300);
  usbDeviceConnect();
}
  • นิยามฟังก์ชัน loop() ให้มีการเรียกใช้ฟังก์ชัน usbPoll() ของไลบรารี V-USB โดยให้แน่ใจว่าฟังก์ชันนี้ต้องถูกเรียกซ้ำ ๆ ภายในระยะเวลาไม่เกิน 50 มิลลิวินาทีอย่างต่อเนื่อง ไม่เช่นนั้นอุปกรณ์จะตอบสนองต่อคำร้องขอจากโฮสท์ไม่ทันและมีผลทำให้โฮสท์ตัดการเชื่อมต่อในที่สุด
void loop()
{
  // ประมวลผลตามต้องการ แต่ต้องให้แล้วเสร็จภายใน 50 มิลลิวินาที
  // :

  // สั่ง V-USB ให้เฝ้าดูสัญญาณการร้องขอจากโฮสท์
  usbPoll();
}
  • เมื่อพบว่ามีคำร้องขอจากโฮสท์ ไลบรารี V-USB จะเรียกหาฟังก์ชัน usbFunctionSetup() เพื่อประมวลผลคำร้องขอนั้น เป็นหน้าที่ของเราที่ต้องสร้างฟังก์ชันนี้ขึ้นมา
usbMsgLen_t usbFunctionSetup(uint8_t data[8])
{
  usbRequest_t *rq = (usbRequest_t*)data;

  // ประมวลผลข้อมูลภายในคำร้องขอผ่านทางตัวแปร rq
  // :
}
  • เนื่องจาก Arduino IDE ยังไม่สามารถคอมไพล์โค้ดที่เรียกใช้งานไลบรารี V-USB โดยตรงได้ ให้คอมไพล์เฟิร์มแวร์ด้วย Arduino-Makefile โดยการสร้างไฟล์ Makefile ไว้ในที่เดียวกันกับที่เก็บ Arduino Sketch แล้วเรียกใช้คำสั่ง make หรือ make ispload จากเทอร์มินัล

โครงสร้างของคำร้องขอ USB ที่รับจากฝั่งคอมพิวเตอร์

ตามสถาปัตยกรรม USB นั้นการสื่อสารจะถูกเริ่มจากการที่ฝั่งคอมพิวเตอร์ (ฝั่งโฮสท์) ส่งคำร้องขอไปยังฝั่งอุปกรณ์เสมอไม่ว่าจะต้องการอ่านหรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ข้อมูลคำร้องขอมีขนาด 8 ไบต์ ซึ่งมีโครงสร้างดังนี้ (นิยามโครงสร้างนี้เป็นส่วนหนึ่งของไลบรารี V-USB จึงไม่ต้องเขียนขึ้นมาเอง)

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 หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์

คำร้องขอนี้จะถูกส่งมายังโค้ดของเราผ่านมาทางฟังก์ชัน usbFunctionSetup() ดังนั้นสิ่งที่เราต้องทำคือตรวจสอบข้อมูลเหล่านี้ภายในคำร้องขอ แล้วตอบสนองไปยังโฮสท์ ซึ่งเป็นไปได้สองกรณีคือ

  1. ไม่มีข้อมูลส่งกลับให้โฮสท์ ให้ใช้คำสั่ง return 0 ออกจากฟังก์ชันตามปกติ
  2. มีข้อมูลส่งกลับให้โฮสท์ ให้ตั้งค่าตัวแปร usbMsgPtr (V-USB ประกาศไว้ให้แล้ว) ให้ชี้ไปยังตำแหน่งหน่วยความจำที่เก็บข้อมูลที่ต้องการส่งคืนโฮสท์ จากนั้นใช้คำสั่ง return len โดยที่ len คือจำนวนไบต์ของข้อมูลที่ต้องการส่งกลับ ซึ่งส่งคืนได้ทีละไม่เกิน 8 ไบต์ ระวังว่าข้อมูลนี้ต้องยังอยู่ในหน่วยความจำแม้จะออกจากฟังก์ชัน usbFunctionSetup ไปแล้ว ตัวแปรที่ใช้เก็บข้อมูลส่งกลับจึงต้องถูกประกาศเป็นแบบโกลบอลหรือแบบสแตติก

ส่วนของโค้ดด้านล่างส่งค่า 12 และ 34 (ฐานสิบ) กลับไปยังโฮสท์เมื่อได้รับคำร้องขอหมายเลข 38 (ฐานสิบ) และไม่ส่งข้อมูลใด ๆ กลับไปหากคำร้องขอเป็นหมายเลขอื่น

usbMsgLen_t usbFunctionSetup(uint8_t data[8])
{
  usbRequest_t *rq = (usbRequest_t*)data;
  static uint8_t value[2];

  if (rq->bRequest == 38)
  {
    value[0] = 12;
    value[1] = 34;
    usbMsgPtr = &value;
    return sizeof(value);
  }
  return 0; // ไม่ส่งข้อมูลกลับโฮสท์หากเป็นคำร้องขออื่น ๆ
}

ตัวอย่างโปรแกรม

ดาวน์โหลดตัวอย่างโปรแกรม usb_generic.tgz แล้วแตกเอาไว้ในไดเรคตอรีที่เก็บ sketch ของ Arduino

เฟิร์มแวร์สำหรับฝั่งดีไวซ์

โค้ดเฟิร์มแวร์อยู่ในไฟล์ usb_generic ซึ่งคอมไพล์ได้แต่ยังทำงานไม่สมบูรณ์ เฟิร์มแวร์ตัวอย่างนี้รองรับคำร้องขอ 2 หมายเลขคือ หมายเลข 0 เป็นการควบคุมสถานะ LED บนบอร์ดพ่วง และหมายเลข 1 เป็นการตรวจสอบสถานะของสวิตช์บนบอร์ดพ่วง ให้ปฏิบัติตามขั้นตอนดังนี้ในการอัพโหลดเฟิร์มแวร์ลงบอร์ด

  • แก้ไขไฟล์ usbconfig.h ในส่วนที่นิยามมาโคร USB_CFG_DEVICE_NAME เอาไว้เพื่อตั้งชื่ออุปกรณ์ให้เป็น ID รหัสนิสิต
#define USB_CFG_DEVICE_NAME     'I','D',' ','1','2','3','4','5','6','7','8','9','0'  <-- แก้เป็นรหัสนิสิตของตน
#define USB_CFG_DEVICE_NAME_LEN 13
  • (สำหรับบางคน) แก้ไขไฟล์ Makefile เพื่อระบุไดเรคตอรีต่าง ๆ ให้สอดคล้องกับเครื่องที่ตนใช้งาน
  • เปิดเทอร์มินัลเพื่ออัพโหลดเฟิร์มแวร์โดยใช้คำสั่ง
make ispload
  • เมื่อเฟิร์มแวร์เริ่มต้นทำงาน ระบบปฏิบัติการจะมองเห็นบอร์ดไมโครคอนโทรลเลอร์เป็นอุปกรณ์ USB ทันที จะเห็นได้จากเอาท์พุทของคำสั่ง lsusb บน Ubuntu ที่ปรากฏรายการของอุปกรณ์ที่มี VID/PID เป็น 16c0:05dc แม้จะไม่ได้อยู่ในโหมดบูทโหลดเดอร์ (สำหรับ Mac OS X ให้ใช้คำสั่ง system_profiler แทน)

แอพลิเคชันฝั่งโฮสท์

ขณะที่เฟิร์มแวร์ทำงานอยู่นั้นเราจะมองไม่เห็นผลลัพธ์การทำงานใด ๆ เนื่องจากเฟิร์มแวร์ถูกเขียนไว้ให้ตอบสนองต่อการสั่งงานผ่านคอมพิวเตอร์เท่านั้น ภายในไฟล์ตัวอย่างมีไฟล์ชื่อ practicum.py ซึ่งเป็นโมดูลไพทอนที่เราจะนำมาใช้ติดต่อกับบอร์ดไมโครคอนโทรลเลอร์ผ่านภาษาไพทอน ทดลองเปิดไพทอนเชลล์แล้วโหลดโมดูลมาใช้งาน โดยฟังก์ชันหลักที่เรียกใช้จากโมดูลคือ findBoards() ซึ่งคืนค่าเป็นลิสต์ของอุปกรณ์ USB ทุกตัวที่มี VID/PID เป็น 16c0:05dc ที่ต่ออยู่กับคอมพิวเตอร์ ณ ขณะนั้น

$ python
>>> from practicum import findBoards,McuBoard
>>> devices = findBoards()
>>> devices
[<usb.Device object at 0xd91d70>]

จากนั้นสร้างอ็อบเจกต์ของคลาส McuBoard ขึ้นมาจากอุปกรณ์ตัวแรกในลิสต์

>>> b = McuBoard(devices[0])
>>> b.getVendorName()
'cpe.ku.ac.th'
>>> b.getDeviceName()
'ID 1234567890'  <- ต้องขึ้นเป็นรหัสนิสิตของตน

ทดลองส่งคำร้องขอหมายเลข 0 (ควบคุมสถานะ LED) เพื่อให้ LED หมายเลข 2 บนบอร์ดพ่วงติดสว่าง ใช้เมท็อต usbWrite ในคลาส McuBoard ดังนี้

>>> b.usbWrite(0, index=2, value=1)

คำสั่งด้านล่างมีผลทำให้ LED หมายเลข 2 ดับ และ LED หมายเลข 1 ติดขึ้นมาแทน

>>> b.usbWrite(0, index=2, value=0)
>>> b.usbWrite(0, index=1, value=1)

ทดลองอ่านสถานะของสวิตช์โดยส่งคำร้องหมายเลข 1 ไปยังบอร์ด MCU

>>> b.usbRead(1, length=1)
(0,)

ค่าที่เมทอด usbRead คืนกลับมาจะเป็นทูเปิลที่มีสมาชิกหนึ่งตัว ตามที่ระบุในเฟิร์มแวร์

ทดลองกดสวิตช์บนบอร์ดพ่วงค้างไว้ แล้วส่งคำร้องไปยังบอร์ด MCU ใหม่ ผลลัพธ์ที่ได้ควรเป็นดังนี้

>>> b.usbRead(1, length=1)
(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