การพัฒนาเฟิร์มแวร์สำหรับไมโครคอนโทรลเลอร์
วิกินี้อธิบายถึงขั้นตอนและตัวอย่างการพัฒนาเฟิร์มแวร์ลงบนบอร์ดไมโครคอนโทรลเลอร์ที่เราได้ประกอบขึ้นมา โดยเนื้อหาครอบคลุมเฉพาะสภาพแวดล้อมการพัฒนาโปรแกรมบนลินุกซ์เท่านั้น
เนื้อหา
ติดตั้งซอฟท์แวร์ที่เกี่ยวข้อง
- ครอสคอมไพเลอร์ (cross compiler) สำหรับไมโครคอนโทรลเลอร์ตระกูล AVR รวมถึงไลบรารีที่เกี่ยวข้อง
sudo apt-get install gcc-avr avr-libc
- AVR toolchain
sudo apt-get install binutils-avr
- AVRDUDE (AVR Downloader/UploaDEr) ใช้สำหรับโหลดรหัสภาษาเครื่องลงบนหน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ผ่านพอร์ต USB
sudo apt-get install avrdude
การพัฒนาเฟิร์มแวร์ด้วยภาษาแอสเซมบลี้
ภาษาแอสเซมบลี้ (assembly language) จัดเป็นภาษาระดับต่ำที่ใช้การแทนรหัสภาษาเครื่อง (machine code) ด้วยสัญลักษณ์ ดังนั้นคำสั่งในภาษาแอสเซมบลี้หนึ่งคำสั่งจึงเทียบเท่ากับคำสั่งในภาษาเครื่องหนึ่งคำสั่งเช่นกัน แม้ภาษาแอสเซมบลี้จะมีความยุ่งยากมากกว่าภาษาชั้นสูงเช่นภาษาซี การเขียนโปรแกรมในภาษาแอสเซมบลี้ก็ยังเป็นที่นิยมในหลาย ๆ สถานการณ์เนื่องจากผู้พัฒนาโปรแกรมสามารถทราบถึงการทำงานของหน่วยประมวลผลได้ในรายละเอียดทุกคำสั่ง โดยเฉพาะอย่างยิ่งในงานที่ต้องมีการกำหนดเวลาในการประมวลผลที่แม่นยำ
ตัวอย่างโปรแกรมภาษาแอสเซมบลี้
ใช้โปรแกรมเท็กซ์เอดิเตอร์สร้างโปรแกรมภาษาแอสเซมบลี้ขึ้นมาหนึ่งโปรแกรมตามตัวอย่างด้านล่าง จากนั้นบันทึกโปรแกรมลงในไฟล์ชื่อ first.S
(สังเกตว่าใช้นามสกุล .S ตัวใหญ่)
.global main main: ldi r16,0b00000111 ; ทำให้ PC2,PC1,PC0 เป็นเอาท์พุท out 0x07,r16 ldi r16,0b00000100 ; ทำให้ LED สีเขียวติด out 0x08,r16 loop: rjmp loop ; วนซ้ำอยู่ที่เดิมเพื่อไม่ให้โปรแกรมจบการทำงาน
การแอสเซมเบลอร์เพื่อสร้างรหัสภาษาเครื่อง
โปรแกรมที่เขียนด้วยภาษาแอสเซมบลี้ต้องผ่านกระบวนการแปลภาษาเพื่อให้อยู่ในรูปรหัสภาษาเครื่องโดยอาศัยโปรแกรมแปลภาษาที่เรียกว่าแอสเซมเบลอร์ (assembler) ในที่นี้เราจะใช้โปรแกรม avr-gcc
ซึ่งทำหน้าที่ได้ทั้งครอสแอสเซมเบลอร์และครอสคอมไพเลอร์ที่แปลโปรแกรมที่เขียนด้วยภาษาแอสเซมบลี้หรือภาษาซีให้เป็นรหัสภาษาเครื่องสำหรับสถาปัตยกรรม AVR
avr-gcc -mmcu=atmega168 -o first.elf first.S
อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้
-mmcu=atmega168
เป็นตัวบอกคอมไพเลอร์ว่าไมโครคอนโทรลเลอร์ที่ใช้เป็นเบอร์ ATMega168-o first.elf
ระบุว่าให้เอาท์พุทถูกเก็บลงในไฟล์first.elf
หากไม่ระบุโปรแกรมจะสร้างไฟล์ชื่อa.out
แทน
ผลลัพธ์ที่ได้จะอยู่ในรูปของไฟล์ฟอร์แมต 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 :0E00800007E007B904E008B9FFCFF894FFCFFE :00000001FF
การเขียนโปรแกรมลงบนหน่วยความจำแฟลชของไมโครคอนโทรลเลอร์
โดยทั่วไปการนำโปรแกรมลงสู่แฟลชของไมโครคอนโทรลเลอร์นั้นมักอาศัยเครื่องโปรแกรมชิป (chip programmer) อย่างไรก็ตามชิป ATmega168 ที่แจกไปให้นั้นได้ถูกป้อนโปรแกรมพิเศษที่เรียกว่าบูทโหลดเดอร์ (boot loader) เอาไว้เพื่อจำลองบอร์ดไมโครคอนโทรลเลอร์เป็นอุปกรณ์โปรแกรมชิปชนิด USBasp ซึ่งจะรอรับรหัสภาษาเครื่องที่ส่งมาทางพอร์ท USB ของเครื่องคอมพิวเตอร์ และเขียนข้อมูลเหล่านั้นลงสู่หน่วยความจำแฟลช
บูทโหลดเดอร์ถูกติดตั้งไว้ในตำแหน่งแฟลชที่เป็นบูทเซคเตอร์ของชิป (เริ่มต้นที่แอดเดรส 0x3800 ของหน่วยความจำแฟลช) ซึ่งเป็นจุดแรกที่ไมโครคอนโทรลเลอร์เริ่มต้นทำงาน กระบวนการทำงานของบูทโหลดเดอร์ที่เตรียมไว้ให้เป็นดังรูปด้านล่าง
จะเห็นว่าเงื่อนไขของการที่จะให้บูทโหลดเดอร์เข้าสู่โหมด USB เพื่อรอรับข้อมูลนั้นคือไมโครคอนโทรลเลอร์ต้องถูกรีเซ็ตด้วยปุ่มรีเซ็ต และขา PD7 ต้องถูกเชื่อมลงกราวนด์ ซึ่งทำได้โดยการเสียบจั๊มเปอร์ไว้ที่อุปกรณ์ JP1 บนบอร์ด จุดสังเกตที่แสดงให้เห็นว่าไมโครคอนโทรลเลอร์กำลังรอรับข้อมูลจากพอร์ท USB คือ LED สีเขียวบนบอร์ดจะกระพริบถี่ ๆ
ในระหว่างที่ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB หากเรียกคำสั่ง lsusb
(อาจต้องเรียกมากกว่าหนึ่งครั้ง) บนเครื่องคอมพิวเตอร์จะต้องปรากฏรายการอุปกรณ์ที่มี VID:PID เป็น 16c0:05dc ดังตัวอย่าง
$ lsusb Bus 004 Device 001: ID 0000:0000 Bus 003 Device 007: ID 16c0:05dc <-- ต้องปรากฏบรรทัดนี้ (หมายเลข Bus และ Device อาจแตกต่างออกไป) Bus 003 Device 001: ID 0000:0000 Bus 002 Device 001: ID 0000:0000 Bus 001 Device 001: ID 0000:0000
อันแสดงว่าไมโครคอนโทรลเลอร์อยู่ในสภาพพร้อมที่จะรับโปรแกรมแล้ว
การส่งโปรแกรมไปยังไมโครคอนโทรลเลอร์ผ่านพอร์ท USB นั้นให้ใช้คำสั่ง avrdude
ดังแสดง
avrdude -p atmega168 -c usbasp -U flash:w:first.hex
อ็อพชันต่าง ๆ ที่ใช้ในคำสั่งข้างต้นมีหน้าที่ดังนี้
-p atmega168
ระบุว่าไมโครคอนโทรลเลอร์ปลายทางคือเบอร์ ATmega168-c usbasp
ระบุว่าเครื่องโปรแกรมชิปที่ใช้คือชนิด USBAsp-U flash:w:first.hex
ระบุว่าให้ดำเนินการเขียนข้อมูลลงสู่หน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ โดยนำเข้าข้อมูลจากไฟล์first.hex
หมายเหตุ: หากพบปัญหาเกี่ยวกับสิทธิการเข้าถึงอุปกรณ์ USB ให้ดำเนินตามขั้นตอนที่อธิบายไว้ในเอกสาร การแก้ไขสิทธิการเข้าถึงพอร์ท USB ของบอร์ด MCU
การพัฒนาเฟิร์มแวร์ด้วยภาษาซี
โปรแกรมตัวอย่างภาษาซี
ทดลองพิมพ์โปรแกรมตัวอย่างต่อไปนี้ และบันทึกไว้ในชื่อ first.c
#define F_CPU 16000000UL // บอกไลบรารีว่า MCU ทำงานที่ 16MHz #include <avr/io.h> #include <util/delay.h> main() { PORTD = 0b00000000; // กำหนดลอจิกขา PD7..0 เป็น 0 DDRD = 0b00001000; // กำหนดให้ขา PD3 ทำหน้าที่เอาท์พุท while (1) { PORTD = 0b00001000; // ส่งลอจิก 1 ไปที่ขา PD3 _delay_ms(1000); PORTD = 0b00000000; // ส่งลอจิก 0 ไปที่ขา PD3 _delay_ms(1000); } }
โปรแกรมข้างต้นเรียกใช้ค่าคงที่สำหรับ I/O รีจีสเตอร์จากไฟล์เฮดเดอร์ avr/io.h
และฟังก์ชันหน่วงเวลาจากไฟล์เฮดเดอร์ util/delay.h
การแปลโปรแกรมให้เป็นภาษาเครื่อง
เรียกใช้คำสั่ง avr-gcc
ดังนี้ (สังเกตว่ามีการใช้อ็อปชัน -O
เพิ่มเติมขึ้นมา)
avr-gcc -mmcu=atmega168 -O -o first.elf first.c
อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้
-mmcu=atmega168
เป็นตัวบอกคอมไพเลอร์ว่าไมโครคอนโทรลเลอร์ที่ใช้เป็นเบอร์ ATMega168-O
ระบุว่าให้คอมไพเลอร์ทำ code optimization ซึ่งจำเป็นต้องใช้เพื่อให้ฟังก์ชัน_delay_ms()
ทำงานได้ถูกต้อง-o first.elf
ระบุว่าให้เอาท์พุทถูกเก็บลงในไฟล์first.elf
หากไม่ระบุโปรแกรมจะสร้างไฟล์ชื่อa.out
แทน
การสร้างกระบวนการอัตโนมัติด้วย Makefile
จากที่ผ่านมาจะเห็นว่าการพัฒนาเฟิร์มแวร์สำหรับไมโครคอนโทรลเลอร์นั้นประกอบด้วยการแก้ไขโปรแกรมด้วยเท็กซ์เอดิเตอร์ และเซฟลงในไฟล์ .c จากนั้นจึงดำเนินตามขั้นตอนดังนี้
- ครอสคอมไพล์โปรแกรมด้วยคำสั่ง avr-gcc
- สกัดรหัสภาษาเครื่องจากไฟล์ ELF ด้วยคำสั่ง avr-objcopy
- ส่งรหัสภาษาเครื่องไปยังไมโครคอนโทรลเลอร์ด้วยคำสั่ง avrdude
ในแต่ละขั้นตอนนั้นมีการเรียกคำสั่งที่ค่อนข้างยาว เราจึงควรสร้าง Makefile ขึ้นมาเพื่อให้คำสั่งเหล่านี้ถูกเรียกใช้งานโดยอัตโนมัติ
all: first.hex flash: first.hex avrdude -p atmega168 -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=atmega168 -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 ให้ยืดยุ่นขึ้นโดยระบุขั้นตอนด้วยรูปแบบ (pattern) ดังตัวอย่าง
TARGET=first.hex MCU=atmega168 F_CPU=16000000L CFLAGS=-DF_CPU=$(F_CPU) -mmcu=$(MCU) -O all: $(TARGET) flash: $(TARGET) avrdude -p atmega168 -c usbasp -U flash:w:$(TARGET) %.hex: %.elf avr-objcopy -j .text -j .data -O ihex $< $@ %.elf: %.c avr-gcc $(CFLAGS) -o $@ $? %.o: %.c avr-gcc -c $(CFLAGS) $@ $<
การนำ Makefile นี้ไปใช้กับโครงงานอื่นทำได้โดยการเปลี่ยนบรรทัดแรกจาก first.hex
เป็ืนชื่อไฟล์สำหรับโครงงานนั้น ๆ เท่านั้น สังเกตว่าเรายังได้ระบุค่าของ F_CPU
ไว้ระหว่างการคอมไพล์ ดังนั้นบรรทัด
#define F_CPU 16000000L
ในไฟล์ .c จึงไม่จำเป็นอีกต่อไป แต่เพื่อความแน่ใจว่าค่าของ F_CPU จะต้องถูกนิยามไว้ที่ใดที่หนึ่ง และต้องไม่ถูกนิยามซ้ำ เราควรใส่เงื่อนไขการนิยามไว้ตอนต้นของโปรแกรมดังนี้
#ifndef F_CPU #define F_CPU 16000000L #endif
บทความที่เกี่ยวข้อง
- การบัดกรีแผงวงจรไมโครคอนโทรลเลอร์
- แผงวงจรพ่วง (Peripheral Board)
- การวัดสัญญาณแอนะล็อกด้วยไมโครคอนโทรลเลอร์