การจำลองบอร์ด MCU เป็นอุปกรณ์ USB

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา

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

ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ V-USB (เดิมเรียกว่า AVR-USB) ที่พัฒนาโดยบริษัท Objective Development โดยทำให้บอร์ด MCU ของเราทำงานเป็นอุปกรณ์ที่อยู่ในกลุ่ม custom class device ซึ่งจัดเป็นอุปกรณ์ USB ที่ไม่สังกัดคลาสใด โดยซอฟต์แวร์ฝั่งคอมพิวเตอร์จะอยู่ภายใต้ความควบคุมของเราทั้งหมด

นอกเหนือจาก custom class device แล้ว ไลบรารี V-USB ยังรองรับการโปรแกรมเฟิร์มแวร์ให้สังกัดคลาสอื่นได้อีกหลายคลาส อาทิเช่น HID (Human Interface Device) class device ซึ่งอยู่ในกลุ่มเดียวกับอุปกรณ์จำพวกแป้นพิมพ์ เมาส์ จอยสติ๊ก และเกมแพด

ขั้นตอนการใช้งานไลบรารี V-USB

  • ดาวน์โหลดซอร์สโค้ดจาก Objective Development
  • แตกไฟล์ .tar.gz ที่ดาวน์โหลดมาโดยใช้คำสั่ง
tar zxf vusb-20090822.tar.gz
  • คัดลอกไดเรคตอรี usbdrv/ ที่อยู่ในไดเรคตอรี vusb-2009-0822/ ไปวางในไดเรคตอรีโปรเจ็คของตน
  • ภายในไดเรคตอรีโปรเจ็คของตนเอง ย้ายไฟล์ 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(uchar data[8])
{
    ;
}
ฟังก์ชันนี้จะถูกเรียกทำงานโดยอัตโนมัติจากฟังก์ชัน usbPoll เมื่อทางฝั่งคอมพิวเตอร์ส่งคำร้องขอผ่านมาทางพอร์ท USB หัวข้อ #ตัวอย่างโปรแกรม แสดงตัวอย่างการการเขียนฟังก์ชันนี้เอาไว้

การคอมไพล์และลิ้งค์โปรแกรมรวมกับ V-USB

สร้าง Makefile ต่อไปนี้เพื่อคอมไพล์โปรแกรมและดำเนินการลิ้งค์เข้ากับไลบรารี V-USB ซึ่งตัวอย่าง Makefile นี้สมมติว่าไฟล์หลักของโปรเจ็คคือ main.c

MCU=atmega168
F_CPU=12000000L
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 -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=12000000L -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-example.tgz ซึ่งเก็บไฟล์ทั้งหมดที่ใช้ในตัวอย่างนี้

เพื่อให้เห็นภาพของการใช้งานไลบรารี 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 ให้เป็น Practicum Group <หมายเลขกลุ่ม> โดยเปลี่ยนนิยามของมาโคร USB_CFG_DEVICE_NAME และ USB_CFG_DEVICE_NAME_LEN ตัวอย่างเช่น กลุ่ม 99 ให้แก้ไขมาโครดังนี้
#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_LEN 18

โปรแกรมฝั่งเฟิร์มแวร์

  • เพื่อเป็นแนวปฏิบัติที่ดีในการเขียนโปรแกรม เราจะนิยามคำร้องขอเหล่านี้ไว้ที่ส่วนหัวของ main.c ดังนี้
#define VENDOR_RQ_SET_LED    0
#define VENDOR_RQ_GET_SWITCH 1
  • เราสร้างฟังก์ชันจัดการอินพุท/เอาท์พุทของพอร์ท C เพื่อความสะดวกในการใช้งานไว้ดังนี้
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);
}
  • ภายในฟังก์ชัน 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;   /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */
}
  • คอมไพล์เฟิร์มแวร์และอัพโหลดเข้าแฟลชของไมโครคอนโทรลเลอร์ ซึ่งหากได้สร้าง Makefile ตามที่อธิบายไว้ข้างต้นไว้เรียบร้อยแล้ว ให้เสียบบอร์ดไมโครคอนโทรลเลอร์เข้ากับพอร์ท USB กดสวิตช์เพื่อเข้าสู่ Bootloader แล้วพิมพ์คำสั่ง
make flash

โปรแกรมฝั่งเครื่องคอมพิวเตอร์

  • ติดตั้ง PyUSB เพื่อติดต่อกับอุปกรณ์ USB ผ่านภาษาไพธอน
sudo apt-get install python-usb
  • สร้างไฟล์ชื่อ testusb.py เรียกใช้ไลบรารี usb และนิยามฟังก์ชัน find_board เพื่อค้นหาอุปกรณ์ USB ตัวที่เป็นบอร์ด MCU ของเราจากรายการอุปกรณ์ USB ทั้งหมดที่ต่ออยู่กับเครื่อง
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 มากกว่าหนึ่งบอร์ดเข้ากับคอมพิวเตอร์เพราะฟังก์ชัน find_board จะคืนอุปกรณ์ที่พบว่ามี VID:PID เป็น 16c0:05dc เป็นตัวแรกเสมอ อย่างไรก็ตาม หากต้องการระบุเอาอุปกรณ์ตัวที่ระบุก็สามารถอาศัยข้อมูลจาก vendor name หรือ device name มากำหนดเงื่อนไขเพิ่ม
  • ติดต่อกับบอร์ด MCU ผ่านทางไพธอนเชลล์ โดยเรียกโปรแกรม testusb.py แบบอินเทอแรคทีฟ
python -i testusb.py
หมายเหตุ: หากพบปัญหาเกี่ยวกับสิทธิการเข้าถึงอุปกรณ์ USB ให้ดำเนินตามขั้นตอนที่อธิบายไว้ในเอกสาร การแก้ไขสิทธิการเข้าถึงพอร์ท USB ของบอร์ด MCU
  • สร้าง handle เพื่อใช้เป็นทางผ่านในการสื่อสารกับบอร์ด MCU โดยใช้คำสั่งดังนี้
>>> board = find_board()
>>> handle = board.open()
  • สร้างคำร้องขอสำหรับคำสั่ง SET_LED ซึ่งระบุชนิดของคำร้องเป็นแบบ Vendor คือผู้สร้างอุปกรณ์กำหนดขึ้นมาเอง และระบุว่าคำร้องนี้มีการไหลของข้อมูลแบบ Host to Device
>>> req = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_OUT
  • ส่งคำร้องออกไปยังบอร์ด MCU โดยกำหนดให้หมายเลขคำร้องเป็น 0 (ซึ่งหมายถึง SET_LED) และให้ฟิลด์ 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
>>> req = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_IN
>>> handle.controlMsg(req, 1, 1)
(0,)
  • เนื่องจากคำร้องนี้เป็นการขอให้ MCU ส่งข้อมูลกลับ จึงต้องมีการระบุในคำร้องโดยใช้ usb.ENDPOINT_IN
  • ในกรณีของการร้องขอข้อมูลจากอุปกรณ์ พารามิเตอร์ buf (พารามิเตอร์ตัวที่สองของเมท็อด controlMsg) เป็นความยาวข้อมูลที่ต้องการร้องขอ ซึ่งเท่ากับหนึ่ง
  • ค่าที่เมทอด controlMsg คืนกลับมาจะเป็น tuple ความยาวหนึ่งเช่นกัน
  • ทดลองกดสวิตช์บนบอร์ดพ่วง แล้วส่งคำร้องไปยังบอร์ด MCU ใหม่ ผลลัพธ์ที่ได้ควรเป็นดังนี้
>>> handle.controlMsg(req, 1, 1)
(1,)

เกี่ยวกับหมายเลข VID/PID

ชุดตัวเลข VID/PID ที่กำหนดให้กับอุปกรณ์ USB ไม่ควรตั้งเอาเองตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกซอฟต์แวร์ไดรเวอร์ที่จะมาควบคุมอุปกรณ์ USB

ข้อมูลเพิ่มเติมเกี่ยวกับการกำหนดค่า VID และ PID ให้กับอุปกรณ์ USB รวมถึงหลักเกณฑ์การปฏิบัติในการผลิตอุปกรณ์ USB สู่สาธารณะ สามารถศึกษาเพิ่มเติมได้จากเนื้อหาในไฟล์ vusb-20090822/USB-ID-FAQ.txt และไฟล์ vusb-20090822/USB-IDs-for-free.txt ที่แจกจ่ายมากับไฟล์ vusb-20090822.tar.gz และเอกสาร How to obtain an USB VID/PID for your project

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