การพัฒนาเฟิร์มแวร์สำหรับไมโครคอนโทรลเลอร์

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

วิกินี้อธิบายถึงขั้นตอนและตัวอย่างการพัฒนาเฟิร์มแวร์ลงบนบอร์ดไมโครคอนโทรลเลอร์ที่เราได้ประกอบขึ้นมา โดยเนื้อหาครอบคลุมเฉพาะสภาพแวดล้อมการพัฒนาโปรแกรมบนลินุกซ์และ Mac OS X เท่านั้น

ติดตั้งซอฟท์แวร์ที่เกี่ยวข้อง

สำหรับระบบปฏิบัติการ Linux (Debian/Ubuntu/Mint รวมถึง Raspberry Pi)

  • ครอสคอมไพเลอร์ (cross compiler) สำหรับไมโครคอนโทรลเลอร์ตระกูล AVR รวมถึงไลบรารีที่เกี่ยวข้อง
sudo apt install gcc-avr avr-libc
  • AVR toolchain
sudo apt install binutils-avr
  • AVRDUDE (AVR Downloader/UploaDEr) ใช้สำหรับโหลดรหัสภาษาเครื่องลงบนหน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ผ่านพอร์ต USB
sudo apt install avrdude

สำหรับระบบปฏิบัติการ Mac OS X

  • ดาวน์โหลดและติดตั้งชุดโปรแกรม CrossPack-AVR เวอร์ชันล่าสุดจาก Objective Development

การพัฒนาเฟิร์มแวร์ด้วยภาษาซี

ภาษาซีจัดเป็นภาษาระดับสูง (high-level programming language) ที่ถือว่ามีการทำงานใกล้เคียงกับภาษาเครื่องมากโดยที่ผู้พัฒนาโปรแกรมไม่จำเป็นต้องเขียนคำสั่งให้ละเอียดยิบเหมือนกับภาษาแอสเซมบลี้ จึงเป็นภาษาที่ได้รับความนิยมมากในงานที่ต้องเขียนโปรแกรมใกล้ชิดกับฮาร์ดแวร์ดังเช่นงานด้านระบบฝังตัว (embedded system)

โปรแกรมตัวอย่างภาษาซี

ทดลองพิมพ์โปรแกรมตัวอย่างต่อไปนี้ และบันทึกไว้ในชื่อ first.c

#define F_CPU 16000000 // บอกไลบรารีว่า MCU ทำงานที่ 16MHz
#include <avr/io.h>      // โหลดนิยามสำหรับรีจีสเตอร์ที่ควบคุมอินพุท/เอาท์พุท (เช่น PORTD, DDRD)
#include <util/delay.h>  // โหลดนิยามสำหรับฟังก์ชัน _delay_ms()

int main()
{
    PORTD = 0b00000000;  // กำหนดลอจิกขา PD7..0 เป็น 0
    DDRD  = 0b00001000;  // กำหนดให้ขา PD3 ทำหน้าที่เอาท์พุท

    while (1)
    {
        PORTD = 0b00001000;  // ส่งลอจิก 1 ไปที่ขา PD3
        _delay_ms(1000);     // หน่วงเวลารอ 1000 มิลลิวินาที
        PORTD = 0b00000000;  // ส่งลอจิก 0 ไปที่ขา PD3
        _delay_ms(1000);     // หน่วงเวลารอ 1000 มิลลิวินาที
    }
    return 0;
}

รายละเอียดเพิ่มเติมเกี่ยวกับโปรแกรม

  • โปรแกรมข้างต้นเรียกใช้นิยามชื่อรีจีสเตอร์ (PORTD, DDRD ฯลฯ) จากไฟล์เฮดเดอร์ avr/io.h และฟังก์ชันหน่วงเวลา _delay_ms() จากไฟล์เฮดเดอร์ util/delay.h
  • ตัวเลขที่นำหน้าด้วย 0b ในโค้ดภาษาซีเป็นการบอกให้คอมไพเลอร์ตีความค่าตัวเลขที่ตามมาให้เป็นตัวเลขฐานสอง ดังนั้น 0b00001000 จึงมีค่าเท่ากับ 8 (ฐานสิบ) นอกจากเลขฐานสองแล้วภาษาซียังรองรับการระบุค่าจำนวนเต็มคงที่ในรูปฐานแปด (ขึ้นต้นด้วย 0) และฐานสิบหก (ขึ้นต้นด้วย 0x)

การคอมไพล์โปรแกรมให้เป็นภาษาเครื่อง

กระบวนการแปลโปรแกรมภาษาชั้นสูงให้เป็นภาษาเครื่องนั้นเรียกว่าเป็นการคอมไพล์ (compile) ซึ่งอาศัยโปรแกรมที่เรียกว่าคอมไพเลอร์ (compiler) เป็นตัวดำเนินการ เราอาศัยโปรแกรม avr-gcc เป็นตัวคอมไพเลอร์โดยเรียกใช้งานดังนี้ (สังเกตว่ามีการใช้อ็อปชัน -O เพิ่มเติมขึ้นมา)

avr-gcc -mmcu=atmega328p -O -o first.elf first.c

อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้

  • -mmcu=atmega328p เป็นตัวบอกคอมไพเลอร์ว่าไมโครคอนโทรลเลอร์ที่ใช้เป็นเบอร์ ATMega328P
  • -O ระบุว่าให้คอมไพเลอร์ทำ code optimization ซึ่งจำเป็นต้องใช้เพื่อให้ฟังก์ชัน _delay_ms() ทำงานได้ถูกต้อง
  • -o first.elf ระบุว่าให้เอาท์พุทถูกเก็บลงในไฟล์ first.elf หากไม่ระบุโปรแกรมจะสร้างไฟล์ชื่อ a.out แทน

หมายเหตุ: avr-gcc ทำหน้าที่เป็นได้ทั้งแอสเซมเบลอร์และคอมไพเลอร์ ขึ้นอยู่กับนามสกุลของไฟล์อินพุทว่าเป็น .S หรือ .c

การสกัดโค้ดภาษาเครื่องและดาวน์โหลดโปรแกรมลงสู่ไมโครคอนโทรลเลอร์

ผลลัพธ์ที่ได้จากการคอมไพล์จะอยู่ในรูปของไฟล์ฟอร์แมต ELF (Excutable and Linkable Format) ซึ่งประกอบไปด้วยเฮดเดอร์และข้อมูลเสริมอื่น ๆ อีกมากมาย อย่างไรก็ตามเราต้องการเพียงแค่ส่วนที่เป็นรหัสภาษาเครื่องของโปรแกรม ซึ่งสกัดออกมาได้โดยใช้คำสั่ง avr-objcopy ดังนี้

avr-objcopy -j .text -j .data -O ihex first.elf first.hex

อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้

  • -j .text -j .data สกัดข้อมูลจากเซคชัน .text (ส่วนของโค้ดโปรแกรม) และ .data (ส่วนของข้อมูลที่กำหนดค่าเริ่มต้นให้ตัวแปร) ออกมาจากไฟล์ ELF
  • -O ihex บันทึกเอาท์พุทในรูปแบบ Intel HEX

ผลลัพธ์ที่ได้จะถูกเก็บไว้ในไฟล์ first.hex ซึ่งเป็นไฟล์ ASCII ที่บันทึกไบท์โค้ดภาษาเครื่องเอาไว้ โดยภายในไฟล์จะมีข้อมูลคล้ายคลึงกับที่แสดงในตัวอย่าง

$ cat first.hex
:100000000C9434000C943E000C943E000C943E0082
:100010000C943E000C943E000C943E000C943E0068
:100020000C943E000C943E000C943E000C943E0058
:100030000C943E000C943E000C943E000C943E0048
:100040000C943E000C943E000C943E000C943E0038
:100050000C943E000C943E000C943E000C943E0028
:100060000C943E000C943E0011241FBECFEFD4E050
:10007000DEBFCDBF0E9440000C9445000C940000F0
:0E00800008E00AB900E00BB9FFCFF894FFCFFB
:00000001FF

การโหลดโปรแกรมลงบนหน่วยความจำแฟลชของไมโครคอนโทรลเลอร์

โดยทั่วไปการนำโปรแกรมลงสู่แฟลชของไมโครคอนโทรลเลอร์นั้นมักอาศัยเครื่องโปรแกรมชิป (chip programmer) อย่างไรก็ตามชิป ATmega328P ที่แจกไปให้นั้นได้ถูกป้อนโปรแกรมพิเศษที่เรียกว่าบูทโหลดเดอร์ (boot loader) เอาไว้เพื่อจำลองบอร์ดไมโครคอนโทรลเลอร์เป็นอุปกรณ์โปรแกรมชิปชนิด USBasp ซึ่งจะรอรับรหัสภาษาเครื่องที่ส่งมาทางพอร์ท USB ของเครื่องคอมพิวเตอร์ และเขียนข้อมูลเหล่านั้นลงสู่หน่วยความจำแฟลช

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

Flow.png

จะเห็นว่าเงื่อนไขของการที่จะให้บูทโหลดเดอร์เข้าสู่โหมด USB เพื่อรอรับข้อมูลนั้นคือไมโครคอนโทรลเลอร์ต้องถูกรีเซ็ตด้วยปุ่มรีเซ็ต และขา Bootloader (PD7) ต้องถูกเชื่อมลงกราวนด์ ซึ่งทำได้โดยการเสียบจั๊มเปอร์เพื่อชอร์ตวงจรในตำแหน่งที่ระบุว่า Boot-loader บนบอร์ด จุดสังเกตที่แสดงให้เห็นว่าไมโครคอนโทรลเลอร์กำลังรอรับข้อมูลจากพอร์ท USB คือ LED สีเขียวบนบอร์ดจะกระพริบสั้น ๆ และถี่ ๆ

ในระหว่างที่ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB หากเรียกคำสั่ง lsusb บนลินุกซ์ บนเครื่องคอมพิวเตอร์จะต้องปรากฏรายการอุปกรณ์ที่มี VID:PID เป็น 16c0:05dc ดังตัวอย่าง

$ lsusb
:
Bus xxx Device xxx: ID 16c0:05dc Van Ooijen Technische Informatica shared ID for use with libusb
:

สำหรับเครื่องที่ใช้ Mac OS X ให้ใช้คำสั่ง system_profiler ควบคู่กับ grep เพื่อหาบรรทัดที่มีข้อความ USBasp และพิมพ์ผลลัพธ์ที่ตามมาอีก 10 บรรทัด (ด้วยตัวเลือก -A 10) ดังแสดง

$ system_profiler SPUSBDataType | grep -A 10 USBasp
       USBasp:

          Product ID: 0x05dc
          Vendor ID: 0x16c0
          Version:  1.02
          Speed: Up to 1.5 Mb/sec
          Manufacturer: www.fischl.de
          Location ID: 0xfd130000
          Current Available (mA): 500
          Current Required (mA): Unknown (Device has not been configured)

แสดงว่าไมโครคอนโทรลเลอร์อยู่ในสภาพพร้อมที่จะรับโปรแกรมแล้ว

การส่งโปรแกรมไปยังไมโครคอนโทรลเลอร์ผ่านพอร์ท USB นั้นให้ใช้คำสั่ง avrdude ดังแสดง

avrdude -p atmega328p -c usbasp -U flash:w:first.hex

อ็อพชันต่าง ๆ ที่ใช้ในคำสั่งข้างต้นมีหน้าที่ดังนี้

  • -p atmega328p ระบุว่าไมโครคอนโทรลเลอร์ปลายทางคือเบอร์ ATmega328P
  • -c usbasp ระบุว่าเครื่องโปรแกรมชิปที่ใช้คือชนิด USBAsp
  • -U flash:w:first.hex ระบุว่าให้ดำเนินการเขียนข้อมูลลงสู่หน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ โดยนำเข้าข้อมูลจากไฟล์ first.hex

หมายเหตุ: หากพบปัญหาเกี่ยวกับสิทธิการเข้าถึงอุปกรณ์ USB ให้ดำเนินตามขั้นตอนที่อธิบายไว้ในเอกสาร การแก้ไขสิทธิการเข้าถึงพอร์ท USB ของบอร์ด MCU

การสร้างกระบวนการอัตโนมัติด้วย Makefile

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

  1. ครอสคอมไพล์โปรแกรมด้วยคำสั่ง avr-gcc
  2. สกัดรหัสภาษาเครื่องจากไฟล์ ELF ด้วยคำสั่ง avr-objcopy
  3. ส่งรหัสภาษาเครื่องไปยังไมโครคอนโทรลเลอร์ด้วยคำสั่ง avrdude

ในแต่ละขั้นตอนนั้นมีการเรียกคำสั่งที่ค่อนข้างยาว อย่างไรก็ตาม ยูนิกซ์มีคำสั่ง make ที่ช่วยเรียกคำสั่งเหล่านี้ให้เราอัตโนมัติ ช่วยให้เราไม่ต้องพิมพ์คำสั่งยาว ๆ ทุกครั้งหลังจากแก้ไขโปรแกรม

ในการใช้คำสั่ง make ให้เตรียมไฟล์ชื่อ Makefile ขึ้นมาในไดเรคตอรีเดียวกับ first.c และป้อนคำสั่งดังนี้ (ระวังว่าบรรทัดที่เยื้องเข้าไปนั้นต้องเป็นอักขระแท็บ ไม่ใช่ช่องว่าง)

all: first.hex

flash: first.hex
	avrdude -p atmega328p -c usbasp -U flash:w:first.hex

first.hex: first.elf
	avr-objcopy -j .text -j .data -O ihex first.elf first.hex

first.elf: first.c
	avr-gcc -mmcu=atmega328p -O -o first.elf first.c


สมมติว่าภายในไดเรคตอรีที่เรียกใช้คำสั่ง make มีเพียงไฟล์ first.c และ Makefile การเรียกคำสั่ง

make

จะถือเป็นการสร้างเป้าหมายที่ระบุไว้ตัวแรกสุด ในที่นี้คือ all ซึ่งจะมีผลให้มีการดำเนินการดังนี้

  • เป้าหมาย all ระบุไว้ว่าให้สร้างเป้าหมาย first.hex ขึ้นมา
  • make หาไฟล์ first.hex ไม่พบ แต่ทราบว่าสามารถสร้างขึ้นจาก first.elf
  • make หาไฟล์ first.elf ไม่พบ แต่ทราบว่าสามารถสร้างขึ้นจาก first.c
  • make พบไฟล์ first.c ในไดเรคตอรี จึงเรียกคำสั่ง avr-gcc เพื่อสร้างไฟล์ first.elf
  • เมื่อได้ first.elf มาแล้วจึงเรียก avr-objcopy เพื่อสร้างไฟล์ first.hex เป็นอันเสร็จกระบวนการ

หากต้องการให้มีการเขียนแฟลชไมโครคอนโทรลเลอร์ ให้เรียกคำสั่ง make โดยระบุเป้าหมาย flash ดังนี้

make flash

ซึ่งจะมีการดำเนินการสร้างเป้าหมาย first.hex โดยอัตโนมัติหากหาไฟล์นี้ไม่พบหรือไฟล์ที่มีอยู่นั้นเก่ากว่าไฟล์ first.c จากนั้นจึงตามด้วยการเรียกใช้คำสั่ง avrdude เพื่อแฟลชเฟิร์มแวร์ใหม่ให้กับไมโครคอนโทรลเลอร์

หากต้องการนำ Makefile ไปปรับใช้ในโครงงานอื่นได้ง่าย เราสามารถปรับ Makefile ให้ยืดหยุ่นขึ้นโดยนิยามตัวแปรดังตัวอย่าง

TARGET=first
MCU=atmega328p

all: $(TARGET).hex

flash: $(TARGET).hex
 	avrdude -p $(MCU) -c usbasp -U flash:w:$(TARGET).hex

$(TARGET).hex: $(TARGET).elf
 	avr-objcopy -j .text -j .data -O ihex $(TARGET).elf $(TARGET).hex

$(TARGET).elf: $(TARGET).c
 	avr-gcc -mmcu=$(MCU) -O -o $(TARGET).elf $(TARGET).c

การนำ Makefile นี้ไปใช้กับโครงงานอื่นทำได้โดยการเปลี่ยนบรรทัดแรกจาก first เป็นชื่อไฟล์สำหรับโครงงานนั้น ๆ เท่านั้น

บทความที่เกี่ยวข้อง

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