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

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

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

ใช้งานเป็น custom class device ซึ่งเป็นประเภทอุปกรณ์...

ในที่นี้เราจะใช้โอเพนซอร์สไลบรารีชื่อ V-USB (หรือ AVRUSB) ที่พัฒนาโดยบริษัท Objective Development

ขั้นตอนการใช้งานไลบรารี 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 เสมอไม่ว่าจะมีการอ่านข้อมูลจากอุปกรณ์ USB หรือเขียนข้อมูลไปยังอุปกรณ์ USB ก็ตาม ข้อมูลคำสั่งที่ถูกส่งจากโฮสท์ เรียกว่า USB Request มีโครงสร้างตามที่นิยามไว้ในไฟล์ 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
  • บิต 4..0 ผู้รับ (Recipient)
  • 0 = Device
  • 1 = Interface
  • 2 = Endpoint
  • 3 = Other
  • bRequest ระบุหมายเลขคำสั่ง ซึ่งต้องถูกออกแบบไว้ล่วงหน้าแล้วว่าอุปกรณ์ USB ของเราจะรองรับคำสั่งอะไรบ้าง โดยเฟิร์มแวร์ต้องประมวลผลคำสั่งเหล่านี้ได้ถูกต้อง
  • wValue และ wIndex
  • wLength กำหนดขนาดของข้อมูลเพิ่มเติมที่จะส่งจากฝั่งโฮสท์หรือจากอุปกรณ์ USB หากไม่มีข้อมูลเพิ่มเติม ค่านี้จะถูกเซ็ตเป็นศูนย์

อุปกรณ์ USB

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

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

เราจะให้ตัวเฟิร์มแวร์จำลองตัวเป็นอุปกรณ์ USB ที่รองรับการสั่งงานจากโฮสท์ 2 คำสั่ง ดังนี้

  • คำสั่ง SET_LED สั่งให้ LED ดวงที่ระบุติดหรือดับ มีรายละเอียดของคำสั่งดังนี้
  • กำหนดให้หมายเลขคำสั่ง (request number) คือ 0
  • การใช้งานจะระบุให้อุปกรณ์ USB เป็นเอาท์พุท
  • ส่งพารามิเตอร์มาให้ 2 ไบท์ ไบท์แรกระบุตำแหน่งของ LED (0, 1 หรือ 2) ส่วนไบท์ที่สองระบุว่าจะให้ LED ดวงดังกล่าวติด (หากมีค่า 0) หรือดับ (หากมีค่าอื่นที่ไม่ใช่ศูนย์)
  • คำสั่ง GET_SWITCH สั่งให้บอร์ด MCU รายงานสถานะการกดปุ่มสวิตช์กลับมา มีรายละเอียดของคำสั่งดังนี้
  • กำหนดให้หมายเลขคำสั่ง (request number) คือ 1
  • การใช้งานจะระบุให้อุปกรณ์ USB เป็นอินพุท
  • ไม่มีพารามิเตอร์ใดส่งไป แต่ตัวบอร์ด MCU ส่งรายงานกลับมา 1 ไบท์ บอกสถานะของสวิตช์ (0 คือไม่ถูกกด 1 คือถูกกด)

การตั้งค่าให้อุปกรณ์ USB

  • อุปกรณ์ USB ทุกตัวจะต้องมีการระบุค่า Vendor ID (VID) และ Product ID (PID) อย่างละ 16 บิต

ซึ่งไม่สามารถตั้งเองได้ตามใจชอบเนื่องจากระบบปฏิบัติการจะอาศัยตัวเลขคู่นี้ในการเลือกโปรแกรมไดรเวอร์ที่จะมาควบคุมอุปกรณ์ USB http://www.voti.nl/docs/usb-pid.html ตั้งค่า VID ให้เป็น 0x16c0

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

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

#define SET_LED    0
#define GET_SWITCH 1

ภายในฟังก์ชัน usbFunctionSetup สร้างโค้ดสำหรับประมวลผลคำสั่งที่รับมาจากโฮสท์ดังนี้

usbMsgLen_t usbFunctionSetup(uchar data[8])
{
   usbRequest_t *rq = (void *)data;
   static uchar dataBuffer[4];  /* buffer must stay valid when usbFunctionSetup returns */

    if(rq->bRequest == SET_LED)
    {
        dataBuffer[0] = rq->wValue.bytes[0];
        dataBuffer[1] = rq->wValue.bytes[1];
        dataBuffer[2] = rq->wIndex.bytes[0];
        dataBuffer[3] = rq->wIndex.bytes[1];
        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 */
}


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

ติดตั้ง PyUSB เพื่อให้ติดต่อกับอุปกรณ์ USB โดยใช้ภาษาไพธอนได้

sudo apt-get install python-usb

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