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

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
แถว 158: แถว 158:
 
  make flash
 
  make flash
  
=== โปรแกรมฝั่งเครื่องคอมพิวเตอร์ ===
+
Ya learn somehitng new everyday. It's true I guess!
 
 
* (สำหรับ Ubuntu Linux) ติดตั้ง PyUSB เพื่อติดต่อกับอุปกรณ์ USB ผ่านภาษาไพธอน
 
sudo apt-get install python-usb
 
 
 
* (สำหรับ MacOS X) ดาวน์โหลดซอร์สโค้ดของไลบรารี libusb จาก[http://sourceforge.net/projects/libusb/files/libusb-1.0/ ที่นี่] จากนั้นแตกไฟล์ออกและใช้คำสั่ง cd เข้าไปในไดเรคตอรี่ที่เก็บไฟล์ จึงคอมไพล์และติดตั้งโดยใช้คำสั่งต่อไปนี้
 
./configure
 
make
 
sudo make install
 
 
 
:จากนั้นโหลดซอร์สโค้ดของ PyUSB จาก[http://sourceforge.net/projects/pyusb/ ที่นี่] แตกไฟล์และติดตั้งโดยใช้คำสั่ง
 
sudo python setup.py install
 
 
 
* สร้างไฟล์ชื่อ <code>testusb.py</code> เรียกใช้ไลบรารี usb และนิยามฟังก์ชัน <code>find_board</code> เพื่อค้นหาอุปกรณ์ USB ตัวที่เป็นบอร์ด MCU ของเราจากรายการอุปกรณ์ USB ทั้งหมดที่ต่ออยู่กับเครื่อง
 
import usb
 
 
def find_board():
 
    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> แบบอินเทอแรคทีฟ
 
python -i testusb.py
 
:<span style="color:red;">'''หมายเหตุ:''' หากพบปัญหาเกี่ยวกับสิทธิการเข้าถึงอุปกรณ์ USB ให้ดำเนินตามขั้นตอนที่อธิบายไว้ในเอกสาร [[การแก้ไขสิทธิการเข้าถึงพอร์ท USB ของบอร์ด MCU]]</span>
 
 
 
* สร้าง 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) และให้ฟิลด์ <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
 
>>> 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,)
 
 
 
==== สร้างไพธอนคลาสเพื่อความสะดวกในการใช้งาน ====
 
การส่งคำสั่งจากคอมพิวเตอร์ไปยังบอร์ดไมโครคอนโทรลเลอร์ผ่านเมท็อด controlMsg นั่นนอกจากจะไม่สะดวกและเข้าใจยากแล้วยังจะทำให้เกิดความผิดพลาดได้ง่ายเนื่องจากพารามิเตอร์ต่าง ๆ ที่ส่งไปยังบอร์ดจะถูกเข้ารหัสรวมกันไว้ในฟิลด์ value และ index โปรแกรม <code>board.py</code> ในโค้ดตัวอย่างเป็นการรวมคำสั่งควบคุม LED และอ่านค่าสวิตช์ รวมถึงคำสั่งค้นหาบอร์ดเอาไว้ในคลาสเดียวกันเพื่อความสะดวกในการเรียกใช้ ดังแสดง
 
import usb
 
 
####################################
 
RQ_SET_LED    = 0
 
RQ_GET_SWITCH = 1
 
 
####################################
 
class Board:
 
    def __init__(self):
 
        #print "Looking for MCU board..."
 
        dev = self.find_board()
 
        if not dev: raise "MCU board not found!"
 
        #print "MCU board found"
 
        self.handle = dev.open()
 
 
    def find_board(self):
 
        board = None
 
        for bus in usb.busses():
 
            for dev in bus.devices:
 
                if dev.idVendor == 0x16c0 and dev.idProduct == 0x05dc:
 
                    return dev
 
        return None
 
 
    def set_led(self,pin,val):
 
        reqType = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_OUT
 
        self.handle.controlMsg(reqType, RQ_SET_LED, None, value=val*256+pin)
 
 
    def get_switch(self):
 
        reqType = usb.TYPE_VENDOR | usb.RECIP_DEVICE | usb.ENDPOINT_IN
 
        buf = self.handle.controlMsg(reqType, RQ_GET_SWITCH, 1)
 
        return buf[0]
 
 
 
สังเกตว่าเมท็อด <code>__init__</code> จะดำเนินการค้นหาบอร์ด (ที่รันเฟิร์มแวร์จำลองให้ตัวเองเป็นอุปกรณ์ USB แล้ว) โดยอัตโนมัติหากมีการสร้างอ็อปเจ็กต์จากคลาส Board
 
>>> from board import Board
 
>>> b = Board()
 
 
 
ซึ่งเราสามารถควบคุมการติดดับของ LED ผ่านอ็อปเจ็กต์นี้ เช่นการสั่งให้ LED ดวงแรก (ดวงที่ 0) ติด ทำได้โดยใช้คำสั่ง
 
>>> b.set_led(0,1)
 
 
 
หรือหากต้องการตรวจสอบสถานะของสวิตช์ ทำได้โดยใช้คำสั่ง
 
>>> b.get_switch()
 
0
 
  
 
== เกี่ยวกับหมายเลข VID/PID ==
 
== เกี่ยวกับหมายเลข VID/PID ==

รุ่นแก้ไขเมื่อ 23:25, 14 กุมภาพันธ์ 2555

ที่ผ่านมานั้นเราใช้พอร์ท 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-20100715.tar.gz
  • คัดลอกไดเรคตอรี usbdrv/ ที่อยู่ในไดเรคตอรี vusb-20100715/ ไปวางในไดเรคตอรีโปรเจ็คของตน
  • ภายในไดเรคตอรีโปรเจ็คของตนเอง ย้ายไฟล์ 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=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 $(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=12000000L -Iusbdrv -I. -mmcu=atmega168 -o main.elf main.o usbdrv/usbdrv.o usbdrv/usbdrvasm.o

These topics are so cnfuosing but this helped me get the job done.

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

โหลดไฟล์ 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
  • เราสร้างฟังก์ชันจัดการอินพุท/เอาท์พุทของพอร์ท D เพื่อความสะดวกในการใช้งานไว้ดังนี้
uint8_t in_d(uint8_t pin)
{
    // กำหนดให้ขาที่ระบุของพอร์ท D ทำหน้าที่เป็นอินพุท
    DDRD &= ~(1<<pin);
    // ดึงให้ขามีลอจิก 1 หากขาถูกปล่อยลอยไว้ 
    PORTD |= (1<<pin);
    // อ่านสถานะลอจิกของขาที่ระบุ
    return ((PIND & (1<<pin))>>pin);    
} 

void out_d(uint8_t pin, uint8_t val)
{
    // กำหนดให้ขาที่ระบุของพอร์ท D ทำหน้าที่เป็นเอาท์พุท
    DDRD |= 1<<pin;
    // เซ็ตลอจิกของขาเป็น 1 ถ้า val ไม่ใช่ศูนย์ ไม่เช่นนั้นเซ็ตให้เป็น 0
    if (val)
        PORTD |= 1<<pin;
    else
        PORTD &= ~(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_d(led_no, !led_val); /* กลับลอจิกเพื่อให้ 1 = ติด 0 = ดับ */
        return 0;
    }
    else if (rq->bRequest == VENDOR_RQ_GET_SWITCH)
    {
        dataBuffer[0] = !in_d(3); /* กลับลอจิกเพื่อให้ 0 = ปล่อย 1 = กด */
        usbMsgPtr = dataBuffer;   /* ระบุว่าข้อมูลส่งกลับอยู่ใน dataBuffer */
        return 1;                 /* ความยาวข้อมูลส่งกลับเท่ากับ 1 */
    }
    return 0;   /* ไม่รู้จักคำร้องขอ ไม่ส่งข้อมูลกลับ */
}
  • คอมไพล์เฟิร์มแวร์และอัพโหลดเข้าแฟลชของไมโครคอนโทรลเลอร์ ซึ่งหากได้สร้าง Makefile ตามที่อธิบายไว้ข้างต้นไว้เรียบร้อยแล้ว ให้เสียบบอร์ดไมโครคอนโทรลเลอร์เข้ากับพอร์ท USB กดสวิตช์เพื่อเข้าสู่ Bootloader แล้วพิมพ์คำสั่ง
make flash

Ya learn somehitng new everyday. It's true I guess!

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

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

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

Frnkaly I think that's absolutely good stuff.