ผลต่างระหว่างรุ่นของ "การพัฒนาเฟิร์มแวร์สำหรับไมโครคอนโทรลเลอร์"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
 
(ไม่แสดง 50 รุ่นระหว่างกลางโดยผู้ใช้ 4 คน)
แถว 1: แถว 1:
วิกินี้อธิบายถึงขั้นตอนและตัวอย่างการพัฒนาเฟิร์มแวร์ลงบนบอร์ดไมโครคอนโทรลเลอร์ที่เราได้ประกอบขึ้นมา โดยเนื้อหาครอบคลุมเฉพาะสภาพแวดล้อมการพัฒนาโปรแกรมบนลินุกซ์เท่านั้น
+
: ''วิกินี้เป็นส่วนหนึ่งของรายวิชา [[01204223]]''
 +
 
 +
วิกินี้อธิบายถึงขั้นตอนและตัวอย่างการพัฒนาเฟิร์มแวร์ลงบนบอร์ดไมโครคอนโทรลเลอร์ที่เราได้ประกอบขึ้นมา โดยเนื้อหาครอบคลุมเฉพาะสภาพแวดล้อมการพัฒนาโปรแกรมบนลินุกซ์และ Mac OS X เท่านั้น
  
 
== ติดตั้งซอฟท์แวร์ที่เกี่ยวข้อง ==
 
== ติดตั้งซอฟท์แวร์ที่เกี่ยวข้อง ==
 +
=== สำหรับระบบปฏิบัติการ Linux (Debian/Ubuntu/Mint รวมถึง Raspberry Pi) ===
 
* ครอสคอมไพเลอร์ (cross compiler) สำหรับไมโครคอนโทรลเลอร์ตระกูล AVR รวมถึงไลบรารีที่เกี่ยวข้อง
 
* ครอสคอมไพเลอร์ (cross compiler) สำหรับไมโครคอนโทรลเลอร์ตระกูล AVR รวมถึงไลบรารีที่เกี่ยวข้อง
  sudo apt-get install gcc-avr avr-libc
+
  sudo apt install gcc-avr avr-libc
 
* AVR toolchain
 
* AVR toolchain
  sudo apt-get install binutils-avr
+
  sudo apt install binutils-avr
 
* [http://www.bsdhome.com/avrdude/ AVRDUDE] (AVR Downloader/UploaDEr) ใช้สำหรับโหลดรหัสภาษาเครื่องลงบนหน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ผ่านพอร์ต USB
 
* [http://www.bsdhome.com/avrdude/ AVRDUDE] (AVR Downloader/UploaDEr) ใช้สำหรับโหลดรหัสภาษาเครื่องลงบนหน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ผ่านพอร์ต USB
  sudo apt-get install avrdude
+
  sudo apt install avrdude
 +
 
 +
=== สำหรับระบบปฏิบัติการ Mac OS X ===
 +
* ดาวน์โหลดและติดตั้งชุดโปรแกรม CrossPack-AVR เวอร์ชันล่าสุดจาก [http://www.obdev.at/products/crosspack/download.html Objective Development]
 +
 
 +
== การพัฒนาเฟิร์มแวร์ด้วยภาษาซี ==
 +
ภาษาซีจัดเป็นภาษาระดับสูง (high-level programming language) ที่ถือว่ามีการทำงานใกล้เคียงกับภาษาเครื่องมากโดยที่ผู้พัฒนาโปรแกรมไม่จำเป็นต้องเขียนคำสั่งให้ละเอียดยิบเหมือนกับภาษาแอสเซมบลี้ จึงเป็นภาษาที่ได้รับความนิยมมากในงานที่ต้องเขียนโปรแกรมใกล้ชิดกับฮาร์ดแวร์ดังเช่นงานด้าน[http://en.wikipedia.org/wiki/Embedded_system ระบบฝังตัว (embedded system)]
  
== การพัฒนาเฟิร์มแวร์ด้วยภาษาแอสเซมบลี้ ==
+
=== โปรแกรมตัวอย่างภาษาซี ===
ภาษาแอสเซมบลี้ (assembly language) จัดเป็นภาษาระดับต่ำที่ใช้การแทนรหัสภาษาเครื่อง (machine code) ด้วยสัญลักษณ์ ดังนั้นคำสั่งในภาษาแอสเซมบลี้หนึ่งคำสั่งจึงเทียบเท่ากับคำสั่งในภาษาเครื่องหนึ่งคำสั่งเช่นกัน แม้ภาษาแอสเซมบลี้จะมีความยุ่งยากมากกว่าภาษาชั้นสูงเช่นภาษาซี การเขียนโปรแกรมในภาษาแอสเซมบลี้ก็ยังเป็นที่นิยมในหลาย ๆ สถานการณ์เนื่องจากผู้พัฒนาโปรแกรมสามารถทราบถึงการทำงานของหน่วยประมวลผลได้ในรายละเอียดทุกคำสั่ง โดยเฉพาะอย่างยิ่งในงานที่ต้องมีการกำหนดเวลาในการประมวลผลที่แม่นยำ
+
ทดลองพิมพ์โปรแกรมตัวอย่างต่อไปนี้ และบันทึกไว้ในชื่อ <code>first.c</code>
  
=== ตัวอย่างโปรแกรมภาษาแอสเซมบลี้ ===
+
#define F_CPU 16000000 // บอกไลบรารีว่า MCU ทำงานที่ 16MHz
ใช้โปรแกรมเท็กซ์เอดิเตอร์สร้างโปรแกรมภาษาแอสเซมบลี้ขึ้นมาหนึ่งโปรแกรมตามตัวอย่างด้านล่าง จากนั้นบันทึกโปรแกรมลงในไฟล์ชื่อ <code>first.S</code> (สังเกตว่าใช้นามสกุล .S ตัวใหญ่)
 
  
  .global main
+
  #include <avr/io.h>      // โหลดนิยามสำหรับรีจีสเตอร์ที่ควบคุมอินพุท/เอาท์พุท (เช่น PORTD, DDRD)
  main:
+
#include <util/delay.h>  // โหลดนิยามสำหรับฟังก์ชัน _delay_ms()
     ldi r16,0b00000111 ; ทำให้ PC2,PC1,PC0 เป็นเอาท์พุท
+
     out 0x07,r16
+
int main()
     ldi r16,0b00000100 ; ทำให้ LED สีเขียวติด
+
  {
     out 0x08,r16
+
    PORTD = 0b00000000;  // กำหนดลอจิกขา PD7..0 เป็น 0
loop:
+
     DDRD = 0b00001000; // กำหนดให้ขา PD3 ทำหน้าที่เอาท์พุท
     rjmp loop          ; วนซ้ำอยู่ที่เดิมเพื่อไม่ให้โปรแกรมจบการทำงาน
+
 +
     while (1)
 +
     {
 +
        PORTD = 0b00001000;  // ส่งลอจิก 1 ไปที่ขา PD3
 +
        _delay_ms(1000);    // หน่วงเวลารอ 1000 มิลลิวินาที
 +
        PORTD = 0b00000000; // ส่งลอจิก 0 ไปที่ขา PD3
 +
        _delay_ms(1000);     // หน่วงเวลารอ 1000 มิลลิวินาที
 +
     }
 +
     return 0;
 +
}
  
แต่ละบรรทัดมีความหมายดังนี้
+
รายละเอียดเพิ่มเติมเกี่ยวกับโปรแกรม
* <code>.global main</code> เป็นการประกาศให้ป้ายระบุตำแหน่ง <code>main</code> เป็นที่รู้จักของไลบรารีภายนอก
+
* โปรแกรมข้างต้นเรียกใช้นิยามชื่อรีจีสเตอร์ (<code>PORTD</code>, <code>DDRD</code> ฯลฯ) จากไฟล์เฮดเดอร์ <code>avr/io.h</code> และฟังก์ชันหน่วงเวลา <code>_delay_ms()</code> จากไฟล์เฮดเดอร์ <code>util/delay.h</code>
* <code>main:</code> ระบุว่า ณ ตำแหน่งนี้สามารถถูกอ้างถึงได้โดยใช้ชื่อ <code>main</code>
+
* ตัวเลขที่นำหน้าด้วย <code>0b</code> ในโค้ดภาษาซีเป็นการบอกให้คอมไพเลอร์ตีความค่าตัวเลขที่ตามมาให้เป็นตัวเลขฐานสอง ดังนั้น <code>0b00001000</code> จึงมีค่าเท่ากับ <code>8</code> (ฐานสิบ) นอกจากเลขฐานสองแล้วภาษาซียังรองรับการระบุค่าจำนวนเต็มคงที่ในรูปฐานแปด (ขึ้นต้นด้วย <code>0</code>) และฐานสิบหก (ขึ้นต้นด้วย <code>0x</code>)
* <code>ldi r16,0b00000111</code> โหลดค่า 00000111 ฐานสอง (ซึ่งเท่ากับ 7) ลงไปในรีจีสเตอร์ r16
 
* <code>out 0x07,r16</code> นำค่าใน r16 ไปใส่ไว้ใน I/O รีจีสเตอร์หมายเลข 7 ซึ่งหมายถึงรีจีสเตอร์ DDRC ดังนั้นจึงมีผลทำให้ขา PC0 ถึง PC2 ทำหน้าที่เป็นเอาท์พุท ส่วนขา PC3 ถึง PC7 ทำหน้าที่เป็นอินพุท
 
* <code>ldi r16,0b00000100</code> โหลดค่า 00000100 ฐานสอง (ซึ่งเท่ากับ 4) ลงไปในรีจีสเตอร์ r16
 
* <code>out 0x08,r16</code> นำค่าใน r16 ไปใส่ไว้ใน I/O รีจีสเตอร์หมายเลข 8 ซึ่งหมายถึงรีจีสเตอร์ PORTC จึงมีผลทำให้ขา PC2 มีค่าลอจิกเป็น 1 จึงทำให้ LED สีเขียวติดสว่าง
 
* <code>loop:</code> ระบุว่า ณ ตำแหน่งนี้สามารถถูกอ้างถึงได้โดยใช้ชื่อ <code>loop</code>
 
* <code>rjmp loop</code> กระโดดกลับไปทำงานที่ตำแหน่งหน่วยความจำที่ระบุด้วยป้าย <code>loop</code> ซึ่งก็คือตำแหน่งเดิม
 
  
=== การแอสเซมเบิลเพื่อสร้างรหัสภาษาเครื่อง ===
+
=== การคอมไพล์โปรแกรมให้เป็นภาษาเครื่อง ===
โปรแกรมที่เขียนด้วยภาษาแอสเซมบลี้ต้องผ่านกระบวนการแปลภาษาเพื่อให้อยู่ในรูปรหัสภาษาเครื่องโดยอาศัยโปรแกรมแปลภาษาที่เรียกว่า''แอสเซมเบลอร์ (assembler)'' ในที่นี้เราจะใช้โปรแกรม <code>avr-gcc</code> ซึ่งตัวมันเองจะไปเรียกใช้โปรแกรม <code>avr-as</code> เพื่อแปลโปรแกรมภาษาแอสเซมบลี้เป็นรหัสภาษาเครื่องสำหรับสถาปัตยกรรม AVR อีกทีหนึ่ง
+
กระบวนการแปลโปรแกรมภาษาชั้นสูงให้เป็นภาษาเครื่องนั้นเรียกว่าเป็นการ''คอมไพล์ (compile)'' ซึ่งอาศัยโปรแกรมที่เรียกว่า''คอมไพเลอร์ (compiler)'' เป็นตัวดำเนินการ เราอาศัยโปรแกรม <code>avr-gcc</code> เป็นตัวคอมไพเลอร์โดยเรียกใช้งานดังนี้ (สังเกตว่ามีการใช้อ็อปชัน <code>-O</code> เพิ่มเติมขึ้นมา)
  avr-gcc -mmcu=atmega168 -o first.elf first.S
+
  avr-gcc -mmcu=atmega328p -O -o first.elf first.c
 
อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้
 
อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้
* <code>-mmcu=atmega168</code> เป็นตัวบอกคอมไพเลอร์ว่าไมโครคอนโทรลเลอร์ที่ใช้เป็นเบอร์ ATMega168
+
* <code>-mmcu=atmega328p</code> เป็นตัวบอกคอมไพเลอร์ว่าไมโครคอนโทรลเลอร์ที่ใช้เป็นเบอร์ ATMega328P
 +
* <code>-O</code> ระบุว่าให้คอมไพเลอร์ทำ code optimization ซึ่งจำเป็นต้องใช้เพื่อให้ฟังก์ชัน <code>_delay_ms()</code> ทำงานได้ถูกต้อง
 
* <code>-o first.elf</code> ระบุว่าให้เอาท์พุทถูกเก็บลงในไฟล์ <code>first.elf</code> หากไม่ระบุโปรแกรมจะสร้างไฟล์ชื่อ <code>a.out</code> แทน
 
* <code>-o first.elf</code> ระบุว่าให้เอาท์พุทถูกเก็บลงในไฟล์ <code>first.elf</code> หากไม่ระบุโปรแกรมจะสร้างไฟล์ชื่อ <code>a.out</code> แทน
 +
''หมายเหตุ: <code>avr-gcc</code> ทำหน้าที่เป็นได้ทั้งแอสเซมเบลอร์และคอมไพเลอร์ ขึ้นอยู่กับนามสกุลของไฟล์อินพุทว่าเป็น .S หรือ .c''
  
ผลลัพธ์ที่ได้จะอยู่ในรูปของไฟล์ฟอร์แมต [http://en.wikipedia.org/wiki/Executable_and_Linkable_Format ELF] (Excutable and Linkable Format) ซึ่งประกอบไปด้วยเฮดเดอร์และข้อมูลเสริมอื่น ๆ อีกมากมาย อย่างไรก็ตามเราต้องการเพียงแค่ส่วนที่เป็นรหัสภาษาเครื่องของโปรแกรม ซึ่งสกัดออกมาได้โดยใช้คำสั่ง <code>avr-objcopy</code> ดังนี้
+
=== การสกัดโค้ดภาษาเครื่องและดาวน์โหลดโปรแกรมลงสู่ไมโครคอนโทรลเลอร์ ===
 +
ผลลัพธ์ที่ได้จากการคอมไพล์จะอยู่ในรูปของไฟล์ฟอร์แมต [http://en.wikipedia.org/wiki/Executable_and_Linkable_Format ELF] (Excutable and Linkable Format) ซึ่งประกอบไปด้วยเฮดเดอร์และข้อมูลเสริมอื่น ๆ อีกมากมาย อย่างไรก็ตามเราต้องการเพียงแค่ส่วนที่เป็นรหัสภาษาเครื่องของโปรแกรม ซึ่งสกัดออกมาได้โดยใช้คำสั่ง <code>avr-objcopy</code> ดังนี้
 
  avr-objcopy -j .text -j .data -O ihex first.elf first.hex
 
  avr-objcopy -j .text -j .data -O ihex first.elf first.hex
 
อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้
 
อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้
* <code>-j .text -j .data</code> สกัดข้อมูลจากเซคชัน .text และ .data ซึ่งเป็นเซคชันที่เก็บโค้ดโปรแกรมและข้อมูลเริ่มต้นในไฟล์ ELF
+
* <code>-j .text -j .data</code> สกัดข้อมูลจากเซคชัน .text (ส่วนของโค้ดโปรแกรม) และ .data (ส่วนของข้อมูลที่กำหนดค่าเริ่มต้นให้ตัวแปร) ออกมาจากไฟล์ ELF
* <code>-O ihex</code> ส่งเอาท์พุทในรูปแบบ [http://en.wikipedia.org/wiki/Intel_HEX Intel HEX]
+
* <code>-O ihex</code> บันทึกเอาท์พุทในรูปแบบ [http://en.wikipedia.org/wiki/Intel_HEX Intel HEX]
ผลลัพธ์ที่ได้จะถูกเก็บไว้ในไฟล์ <code>first.hex</code> ซึ่งเป็นไฟล์ ASCII โดยภายในไฟล์จะมีข้อมูลคล้ายคลึงกับที่แสดงในตัวอย่าง
+
ผลลัพธ์ที่ได้จะถูกเก็บไว้ในไฟล์ <code>first.hex</code> ซึ่งเป็นไฟล์ ASCII ที่บันทึกไบท์โค้ดภาษาเครื่องเอาไว้ โดยภายในไฟล์จะมีข้อมูลคล้ายคลึงกับที่แสดงในตัวอย่าง
 
  $ cat first.hex
 
  $ cat first.hex
 
  :100000000C9434000C943E000C943E000C943E0082
 
  :100000000C9434000C943E000C943E000C943E0082
แถว 56: แถว 70:
 
  :100060000C943E000C943E0011241FBECFEFD4E050
 
  :100060000C943E000C943E0011241FBECFEFD4E050
 
  :10007000DEBFCDBF0E9440000C9445000C940000F0
 
  :10007000DEBFCDBF0E9440000C9445000C940000F0
  :0E00800007E007B904E008B9FFCFF894FFCFFE
+
  :0E00800008E00AB900E00BB9FFCFF894FFCFFB
 
  :00000001FF
 
  :00000001FF
  
สังเกตว่าไฟล์รหัสภาษาเครื่องดูจะยาวกว่าต้นฉบับโปรแกรมภาษาแอสเซมบลี้มาก ทดลองแปลงโปรแกรมภาษาเครื่องกลับมาเป็นภาษาแอสเซมบลี้โดยใช้คำสั่ง <code>avr-objdump</code> ดังนี้
+
== การโหลดโปรแกรมลงบนหน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ ==
$ avr-objdump -d first.elf
 
กระบวนการข้างต้นเรียกว่าเป็นการทำ''ดิสแอสเซมเบิล (disassemble)'' ซึ่งอ็อปชัน <code>-d</code> ย่อมาจาก disassemble บนหน้าจอจะแสดงผลลัพธ์ดังนี้
 
 
 
first.elf:    file format elf32-avr
 
 
 
Disassembly of section .text:
 
 
00000000 <__vectors>:
 
    0: 0c 94 34 00 jmp 0x68 ; 0x68 <__ctors_end>
 
    4: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
 
    8: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
 
    :
 
    : ละไว้
 
    :
 
  64: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
 
 
00000068 <__ctors_end>:
 
  68: 11 24      eor r1, r1
 
  6a: 1f be      out 0x3f, r1 ; 63
 
  6c: cf ef      ldi r28, 0xFF ; 255
 
  6e: d4 e0      ldi r29, 0x04 ; 4
 
  70: de bf      out 0x3e, r29 ; 62
 
  72: cd bf      out 0x3d, r28 ; 61
 
  74: 0e 94 40 00 call 0x80 ; 0x80 <main>
 
  78: 0c 94 45 00 jmp 0x8a ; 0x8a <_exit>
 
 
0000007c <__bad_interrupt>:
 
  7c: 0c 94 00 00 jmp 0 ; 0x0 <__vectors>
 
 
00000080 <main>:
 
  80: 07 e0      ldi r16, 0x07 ; 7
 
  82: 07 b9      out 0x07, r16 ; 7
 
  84: 04 e0      ldi r16, 0x04 ; 4
 
  86: 08 b9      out 0x08, r16 ; 8
 
 
00000088 <loop>:
 
  88: ff cf      rjmp .-2      ; 0x88 <loop>
 
 
0000008a <_exit>:
 
  8a: f8 94      cli
 
 
0000008c <__stop_program>:
 
  8c: ff cf      rjmp .-2      ; 0x8c <__stop_program>
 
 
 
ตัวเลขด้านซ้ายมือสุดในแต่ละบรรทัดที่มีคำสั่งภาษาแอสเซมบลี้อยู่ (เช่น 7c:) แสดงตำแหน่งที่อยู่ (address) ของแฟลชโปรแกรมในรูปฐานสิบหก ตัวเลขชุดถัดมาคือรหัสคำสั่งภาษาเครื่อง และส่วนที่เหลือเป็นคำสั่งภาษาแอสเซมบลี้ที่สอดคล้องกัน ดังนั้นบรรทัดที่ระบุว่า
 
  80: 07 e0      ldi r16, 0x07 ; 7
 
จึงมีความหมายว่าให้บรรจุรหัสคำสั่งภาษาเครื่อง 07 และ e0 (สองไบต์) ลงไปในชิปที่ตำแหน่งหน่วยความจำ 80 และ 81 (ฐานสิบหก)
 
 
 
แต่สังเกตว่าคำสั่งภาษาแอสเซมบลี้ที่เราเขียนไว้ในต้นฉบับปรากฏอยู่ที่ตำแหน่งหน่วยความจำที่ 80 ถึง 89 เท่านั้น แต่รหัสภาษาเครื่องที่ดิสแอมเซมเบิลออกมานั้นกลับประกอบไปด้วยอะไรอีกหลายอย่างที่เราไม่ได้เขียน ทั้งนี้เนื่องจากในสถาปัตยกรรม AVR จำเป็นต้องมีการตั้งค่าเริ่มต้นให้กับชิปหลายอย่างก่อนที่จะเริ่มทำงานได้อย่างถูกต้อง ดังนั้น <code>avr-gcc</code> จึงปะส่วนการตั้งค่าเริ่มต้นนี้ให้เราในส่วนหัวของโปรแกรม นอกจากนั้นยังมีการปะโค้ดให้วนซ้ำไว้ในส่วนท้ายโปรแกรมเพื้อป้องกันกรณีที่เราเผลอปล่อยโปรแกรมให้จบการทำงานออกมา
 
 
 
== การเขียนโปรแกรมลงบนหน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ ==
 
 
โดยทั่วไปการนำโปรแกรมลงสู่แฟลชของไมโครคอนโทรลเลอร์นั้นมักอาศัย[http://en.wikipedia.org/wiki/Programmer_(hardware) เครื่องโปรแกรมชิป] (chip programmer) อย่างไรก็ตามชิป
 
โดยทั่วไปการนำโปรแกรมลงสู่แฟลชของไมโครคอนโทรลเลอร์นั้นมักอาศัย[http://en.wikipedia.org/wiki/Programmer_(hardware) เครื่องโปรแกรมชิป] (chip programmer) อย่างไรก็ตามชิป
ATmega168 ที่แจกไปให้นั้นได้ถูกป้อนโปรแกรมพิเศษที่เรียกว่า[http://en.wikipedia.org/wiki/Bootloader#Boot_loader บูทโหลดเดอร์] (boot loader)
+
ATmega328P ที่แจกไปให้นั้นได้ถูกป้อนโปรแกรมพิเศษที่เรียกว่า[http://en.wikipedia.org/wiki/Bootloader#Boot_loader บูทโหลดเดอร์] (boot loader)
 
เอาไว้เพื่อจำลองบอร์ดไมโครคอนโทรลเลอร์เป็นอุปกรณ์โปรแกรมชิปชนิด [http://www.fischl.de/usbasp/ USBasp] ซึ่งจะรอรับรหัสภาษาเครื่องที่ส่งมาทางพอร์ท USB ของเครื่องคอมพิวเตอร์ และเขียนข้อมูลเหล่านั้นลงสู่หน่วยความจำแฟลช
 
เอาไว้เพื่อจำลองบอร์ดไมโครคอนโทรลเลอร์เป็นอุปกรณ์โปรแกรมชิปชนิด [http://www.fischl.de/usbasp/ USBasp] ซึ่งจะรอรับรหัสภาษาเครื่องที่ส่งมาทางพอร์ท USB ของเครื่องคอมพิวเตอร์ และเขียนข้อมูลเหล่านั้นลงสู่หน่วยความจำแฟลช
  
 
บูทโหลดเดอร์ถูกติดตั้งไว้ในตำแหน่งแฟลชที่เป็นบูทเซคเตอร์ของชิป (เริ่มต้นที่แอดเดรส 0x3800 ของหน่วยความจำแฟลช) ซึ่งเป็นจุดแรกที่ไมโครคอนโทรลเลอร์เริ่มต้นทำงาน กระบวนการทำงานของบูทโหลดเดอร์ที่เตรียมไว้ให้เป็นดังรูปด้านล่าง
 
บูทโหลดเดอร์ถูกติดตั้งไว้ในตำแหน่งแฟลชที่เป็นบูทเซคเตอร์ของชิป (เริ่มต้นที่แอดเดรส 0x3800 ของหน่วยความจำแฟลช) ซึ่งเป็นจุดแรกที่ไมโครคอนโทรลเลอร์เริ่มต้นทำงาน กระบวนการทำงานของบูทโหลดเดอร์ที่เตรียมไว้ให้เป็นดังรูปด้านล่าง
[[Image:flow.png|center|500px]]
+
[[Image:flow.png|center]]
จะเห็นว่าเงื่อนไขของการที่จะให้บูทโหลดเดอร์เข้าสู่โหมด USB เพื่อรอรับข้อมูลนั้นคือไมโครคอนโทรลเลอร์ต้องถูกรีเซ็ตด้วยปุ่มรีเซ็ต และขา PD7 ต้องถูกเชื่อมลงกราวนด์
+
จะเห็นว่าเงื่อนไขของการที่จะให้บูทโหลดเดอร์เข้าสู่โหมด USB เพื่อรอรับข้อมูลนั้นคือไมโครคอนโทรลเลอร์ต้องถูกรีเซ็ตด้วยปุ่มรีเซ็ต และขา Bootloader (PD7) ต้องถูกเชื่อมลงกราวนด์
ซึ่งทำได้โดยการเสียบจั๊มเปอร์ไว้ที่อุปกรณ์ JP1 บนบอร์ด จุดสังเกตที่แสดงให้เห็นว่าไมโครคอนโทรลเลอร์กำลังรอรับข้อมูลจากพอร์ท USB คือ LED สีเขียวบนบอร์ดจะกระพริบถี่
+
ซึ่งทำได้โดยการเสียบจั๊มเปอร์เพื่อชอร์ตวงจรในตำแหน่งที่ระบุว่า Boot-loader บนบอร์ด จุดสังเกตที่แสดงให้เห็นว่าไมโครคอนโทรลเลอร์กำลังรอรับข้อมูลจากพอร์ท USB คือ LED สีเขียวบนบอร์ดจะกระพริบสั้น ๆ และถี่
  
ในระหว่างที่ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB หากเรียกคำสั่ง <code>lsusb</code> (อาจต้องเรียกมากกว่าหนึ่งครั้ง) บนเครื่องคอมพิวเตอร์จะต้องปรากฏรายการอุปกรณ์ที่มี VID:PID เป็น 16c0:05dc ดังตัวอย่าง
+
ในระหว่างที่ไมโครคอนโทรลเลอร์จำลองตัวเองเป็นอุปกรณ์ USB หากเรียกคำสั่ง <code>lsusb</code> บนลินุกซ์ บนเครื่องคอมพิวเตอร์จะต้องปรากฏรายการอุปกรณ์ที่มี VID:PID เป็น 16c0:05dc ดังตัวอย่าง
 
  $ lsusb
 
  $ lsusb
  Bus 004 Device 001: ID 0000:0000 
+
  :
  Bus 003 Device 007: ID 16c0:05dc  <-- ต้องปรากฏบรรทัดนี้ (หมายเลข Bus และ Device อาจแตกต่างออกไป)
+
  Bus xxx Device xxx: ID 16c0:05dc Van Ooijen Technische Informatica shared ID for use with libusb
  Bus 003 Device 001: ID 0000:0000  
+
  :
Bus 002 Device 001: ID 0000:0000 
+
 
Bus 001 Device 001: ID 0000:0000
+
สำหรับเครื่องที่ใช้ Mac OS X ให้ใช้คำสั่ง <code>system_profiler</code> ควบคู่กับ <code>grep</code> เพื่อหาบรรทัดที่มีข้อความ 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 นั้นให้ใช้คำสั่ง <code>avrdude</code> ดังแสดง
 
การส่งโปรแกรมไปยังไมโครคอนโทรลเลอร์ผ่านพอร์ท USB นั้นให้ใช้คำสั่ง <code>avrdude</code> ดังแสดง
  avrdude -p atmega168 -c usbasp -U flash:w:first.hex
+
  avrdude -p atmega328p -c usbasp -U flash:w:first.hex
 
อ็อพชันต่าง ๆ ที่ใช้ในคำสั่งข้างต้นมีหน้าที่ดังนี้
 
อ็อพชันต่าง ๆ ที่ใช้ในคำสั่งข้างต้นมีหน้าที่ดังนี้
* <code>-p atmega168</code> ระบุว่าไมโครคอนโทรลเลอร์ปลายทางคือเบอร์ ATmega168
+
* <code>-p atmega328p</code> ระบุว่าไมโครคอนโทรลเลอร์ปลายทางคือเบอร์ ATmega328P
 
* <code>-c usbasp</code> ระบุว่าเครื่องโปรแกรมชิปที่ใช้คือชนิด USBAsp
 
* <code>-c usbasp</code> ระบุว่าเครื่องโปรแกรมชิปที่ใช้คือชนิด USBAsp
 
* <code>-U flash:w:first.hex</code> ระบุว่าให้ดำเนินการเขียนข้อมูลลงสู่หน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ โดยนำเข้าข้อมูลจากไฟล์ <code>first.hex</code>
 
* <code>-U flash:w:first.hex</code> ระบุว่าให้ดำเนินการเขียนข้อมูลลงสู่หน่วยความจำแฟลชของไมโครคอนโทรลเลอร์ โดยนำเข้าข้อมูลจากไฟล์ <code>first.hex</code>
แถว 139: แถว 113:
 
<span style="color:red;">'''หมายเหตุ:''' หากพบปัญหาเกี่ยวกับสิทธิการเข้าถึงอุปกรณ์ USB ให้ดำเนินตามขั้นตอนที่อธิบายไว้ในเอกสาร [[การแก้ไขสิทธิการเข้าถึงพอร์ท USB ของบอร์ด MCU]]</span>
 
<span style="color:red;">'''หมายเหตุ:''' หากพบปัญหาเกี่ยวกับสิทธิการเข้าถึงอุปกรณ์ USB ให้ดำเนินตามขั้นตอนที่อธิบายไว้ในเอกสาร [[การแก้ไขสิทธิการเข้าถึงพอร์ท USB ของบอร์ด MCU]]</span>
  
== การพัฒนาเฟิร์มแวร์ด้วยภาษาซี ==
+
== การสร้างกระบวนการอัตโนมัติด้วย <code>Makefile</code> ==
ภาษาซีจัดเป็นภาษาระดับสูง (high-level programming language) ที่ถือว่ามีการทำงานใกล้เคียงกับภาษาเครื่องมากโดยที่ผู้พัฒนาโปรแกรมไม่จำเป็นต้องเขียนคำสั่งให้ละเอียดยิบเหมือนกับภาษาแอสเซมบลี้ จึงเป็นภาษาที่ได้รับความนิยมมากในงานที่ต้องเขียนโปรแกรมใกล้ชิดกับฮาร์ดแวร์ดังเช่นงานด้านระบบฝังตัว
+
จากที่ผ่านมาจะเห็นว่าการพัฒนาเฟิร์มแวร์สำหรับไมโครคอนโทรลเลอร์นั้นประกอบด้วยการแก้ไขโปรแกรมด้วยเท็กซ์เอดิเตอร์ และเซฟลงในไฟล์ .c จากนั้นจึงดำเนินตามขั้นตอนดังนี้
 
+
# ครอสคอมไพล์โปรแกรมด้วยคำสั่ง <code>avr-gcc</code>
=== โปรแกรมตัวอย่างภาษาซี ===
+
# สกัดรหัสภาษาเครื่องจากไฟล์ ELF ด้วยคำสั่ง <code>avr-objcopy</code>
ทดลองพิมพ์โปรแกรมตัวอย่างต่อไปนี้ และบันทึกไว้ในชื่อ <code>first.c</code>
+
# ส่งรหัสภาษาเครื่องไปยังไมโครคอนโทรลเลอร์ด้วยคำสั่ง <code>avrdude</code>
 
+
ในแต่ละขั้นตอนนั้นมีการเรียกคำสั่งที่ค่อนข้างยาว อย่างไรก็ตาม ยูนิกซ์มีคำสั่ง <code>make</code> ที่ช่วยเรียกคำสั่งเหล่านี้ให้เราอัตโนมัติ ช่วยให้เราไม่ต้องพิมพ์คำสั่งยาว ๆ ทุกครั้งหลังจากแก้ไขโปรแกรม
#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 รีจีสเตอร์จากไฟล์เฮดเดอร์ <code>avr/io.h</code> และฟังก์ชันหน่วงเวลาจากไฟล์เฮดเดอร์ <code>util/delay.h</code>
 
 
 
=== การคอมไพล์โปรแกรมให้เป็นภาษาเครื่อง ===
 
กระบวนการแปลโปรแกรมภาษาชั้นสูงให้เป็นภาษาเครื่องนั้นเรียกว่าเป็นการ''คอมไพล์ (compile)'' ซึ่งอาศัยโปรแกรมที่เรียกว่า''คอมไพเลอร์ (compiler)'' เป็นตัวดำเนินการ เราอาศัยโปรแกรม <code>avr-gcc</code> เป็นตัวคอมไพเลอร์โดยเรียกใช้งานดังนี้ (สังเกตว่ามีการใช้อ็อปชัน <code>-O</code> เพิ่มเติมขึ้นมา)
 
avr-gcc -mmcu=atmega168 -O -o first.elf first.c
 
อ็อพชันต่าง ๆ ที่ระบุในคำสั่งข้างต้นมีหน้าที่ดังนี้
 
* <code>-mmcu=atmega168</code> เป็นตัวบอกคอมไพเลอร์ว่าไมโครคอนโทรลเลอร์ที่ใช้เป็นเบอร์ ATMega168
 
* <code>-O</code> ระบุว่าให้คอมไพเลอร์ทำ code optimization ซึ่งจำเป็นต้องใช้เพื่อให้ฟังก์ชัน <code>_delay_ms()</code> ทำงานได้ถูกต้อง
 
* <code>-o first.elf</code> ระบุว่าให้เอาท์พุทถูกเก็บลงในไฟล์ <code>first.elf</code> หากไม่ระบุโปรแกรมจะสร้างไฟล์ชื่อ <code>a.out</code> แทน
 
''หมายเหตุ: <code>avr-gcc</code> ทำหน้าที่เป็นได้ทั้งแอสเซมเบลอร์และคอมไพเลอร์ ขึ้นอยู่กับนามสกุลของไฟล์อินพุทว่าเป็น .S หรือ .c''
 
  
== การสร้างกระบวนการอัตโนมัติด้วย Makefile ==
+
ในการใช้คำสั่ง <code>make</code> ให้เตรียมไฟล์ชื่อ <code>Makefile</code> ขึ้นมาในไดเรคตอรีเดียวกับ <code>first.c</code> และป้อนคำสั่งดังนี้
จากที่ผ่านมาจะเห็นว่าการพัฒนาเฟิร์มแวร์สำหรับไมโครคอนโทรลเลอร์นั้นประกอบด้วยการแก้ไขโปรแกรมด้วยเท็กซ์เอดิเตอร์ และเซฟลงในไฟล์ .c จากนั้นจึงดำเนินตามขั้นตอนดังนี้
+
<span style="color:red;">(ระวังว่าบรรทัดที่เยื้องเข้าไปนั้นต้องเป็นอักขระ<u>แท็บ</u> ไม่ใช่ช่องว่าง)</span>
# ครอสคอมไพล์โปรแกรมด้วยคำสั่ง avr-gcc
 
# สกัดรหัสภาษาเครื่องจากไฟล์ ELF ด้วยคำสั่ง avr-objcopy
 
# ส่งรหัสภาษาเครื่องไปยังไมโครคอนโทรลเลอร์ด้วยคำสั่ง avrdude
 
ในแต่ละขั้นตอนนั้นมีการเรียกคำสั่งที่ค่อนข้างยาว เราจึงควรสร้าง Makefile ขึ้นมาเพื่อให้คำสั่งเหล่านี้ถูกเรียกใช้งานโดยอัตโนมัติ
 
  
 
  all: first.hex
 
  all: first.hex
 
   
 
   
 
  flash: first.hex
 
  flash: first.hex
    avrdude -p atmega168 -c usbasp -U flash:w:first.hex
+
avrdude -p atmega328p -c usbasp -U flash:w:first.hex
 
   
 
   
 
  first.hex: first.elf
 
  first.hex: first.elf
    avr-objcopy -j .text -j .data -O ihex first.elf first.hex
+
avr-objcopy -j .text -j .data -O ihex first.elf first.hex
 
   
 
   
 
  first.elf: first.c
 
  first.elf: first.c
    avr-gcc -mmcu=atmega168 -O -o first.elf first.c
+
avr-gcc -mmcu=atmega328p -O -o first.elf first.c
  
(ระวังว่าบรรทัดที่เยื้องเข้าไปนั้นต้องเป็นอักขระแท็บ ไม่ใช่ช่องว่าง)
 
  
 
สมมติว่าภายในไดเรคตอรีที่เรียกใช้คำสั่ง <code>make</code> มีเพียงไฟล์ <code>first.c</code> และ <code>Makefile</code> การเรียกคำสั่ง
 
สมมติว่าภายในไดเรคตอรีที่เรียกใช้คำสั่ง <code>make</code> มีเพียงไฟล์ <code>first.c</code> และ <code>Makefile</code> การเรียกคำสั่ง
 
  make
 
  make
จะถือเป็นการสร้างเป้าหมายที่ระบุไว้ตัวแรกสุด ในที่นี้คือ all ซึ่งจะมีผลให้มีการดำเนินการดังนี้
+
จะถือเป็นการสร้างเป้าหมายที่ระบุไว้ตัวแรกสุด ในที่นี้คือ <code>all</code> ซึ่งจะมีผลให้มีการดำเนินการดังนี้
* เป้าหมาย all ระบุไว้ว่าให้สร้างเป้าหมาย <code>first.hex</code> ขึ้นมา
+
* เป้าหมาย <code>all</code> ระบุไว้ว่าให้สร้างเป้าหมาย <code>first.hex</code> ขึ้นมา
 
* <code>make</code> หาไฟล์ <code>first.hex</code> ไม่พบ แต่ทราบว่าสามารถสร้างขึ้นจาก <code>first.elf</code>
 
* <code>make</code> หาไฟล์ <code>first.hex</code> ไม่พบ แต่ทราบว่าสามารถสร้างขึ้นจาก <code>first.elf</code>
 
* <code>make</code> หาไฟล์ <code>first.elf</code> ไม่พบ แต่ทราบว่าสามารถสร้างขึ้นจาก <code>first.c</code>
 
* <code>make</code> หาไฟล์ <code>first.elf</code> ไม่พบ แต่ทราบว่าสามารถสร้างขึ้นจาก <code>first.c</code>
แถว 207: แถว 148:
 
ซึ่งจะมีการดำเนินการสร้างเป้าหมาย <code>first.hex</code> โดยอัตโนมัติหากหาไฟล์นี้ไม่พบหรือไฟล์ที่มีอยู่นั้นเก่ากว่าไฟล์ <code>first.c</code> จากนั้นจึงตามด้วยการเรียกใช้คำสั่ง <code>avrdude</code> เพื่อแฟลชเฟิร์มแวร์ใหม่ให้กับไมโครคอนโทรลเลอร์
 
ซึ่งจะมีการดำเนินการสร้างเป้าหมาย <code>first.hex</code> โดยอัตโนมัติหากหาไฟล์นี้ไม่พบหรือไฟล์ที่มีอยู่นั้นเก่ากว่าไฟล์ <code>first.c</code> จากนั้นจึงตามด้วยการเรียกใช้คำสั่ง <code>avrdude</code> เพื่อแฟลชเฟิร์มแวร์ใหม่ให้กับไมโครคอนโทรลเลอร์
  
หากต้องการนำ Makefile ไปปรับใช้ในโครงงานอื่นได้ง่าย เราสามารถปรับ Makefile ให้ยืดยุ่นขึ้นโดยระบุขั้นตอนด้วยรูปแบบ (pattern) ดังตัวอย่าง
+
หากต้องการนำ <code>Makefile</code> ไปปรับใช้ในโครงงานอื่นได้ง่าย เราสามารถปรับ <code>Makefile</code> ให้ยืดหยุ่นขึ้นโดยนิยามตัวแปรดังตัวอย่าง
  TARGET=first.hex
+
  TARGET=first
  MCU=atmega168
+
  MCU=atmega328p
F_CPU=16000000L
 
CFLAGS=-DF_CPU=$(F_CPU) -mmcu=$(MCU) -O
 
 
all: $(TARGET)
 
 
   
 
   
  flash: $(TARGET)
+
  all: $(TARGET).hex
    avrdude -p atmega168 -c usbasp -U flash:w:$(TARGET)
 
 
   
 
   
  %.hex: %.elf
+
  flash: $(TARGET).hex
    avr-objcopy -j .text -j .data -O ihex $< $@
+
  avrdude -p $(MCU) -c usbasp -U flash:w:$(TARGET).hex
 
   
 
   
  %.elf: %.c
+
  $(TARGET).hex: $(TARGET).elf
    avr-gcc $(CFLAGS) -o $@ $?
+
  avr-objcopy -j .text -j .data -O ihex $(TARGET).elf $(TARGET).hex
 
   
 
   
  %.o: %.c
+
  $(TARGET).elf: $(TARGET).c
    avr-gcc -c $(CFLAGS) $@ $<
+
  avr-gcc -mmcu=$(MCU) -O -o $(TARGET).elf $(TARGET).c
  
การนำ Makefile นี้ไปใช้กับโครงงานอื่นทำได้โดยการเปลี่ยนบรรทัดแรกจาก <code>first.hex</code> เป็ืนชื่อไฟล์สำหรับโครงงานนั้น ๆ เท่านั้น สังเกตว่าเรายังได้ระบุค่าของ <code>F_CPU</code>
+
การนำ <code>Makefile</code> นี้ไปใช้กับโครงงานอื่นทำได้โดยการเปลี่ยนบรรทัดแรกจาก <code>first</code> เป็นชื่อไฟล์สำหรับโครงงานนั้น ๆ เท่านั้น
ไว้ระหว่างการคอมไพล์ ดังนั้นบรรทัด
 
#define F_CPU 16000000L
 
ในไฟล์ .c จึงไม่จำเป็นอีกต่อไป แต่เพื่อความแน่ใจว่าค่าของ F_CPU จะต้องถูกนิยามไว้ที่ใดที่หนึ่ง และต้องไม่ถูกนิยามซ้ำ เราควรใส่เงื่อนไขการนิยามไว้ตอนต้นของโปรแกรมดังนี้
 
#ifndef F_CPU
 
#define F_CPU 16000000L
 
#endif
 
  
 
== บทความที่เกี่ยวข้อง ==
 
== บทความที่เกี่ยวข้อง ==
แถว 242: แถว 172:
 
== ข้อมูลเพิ่มเติม ==
 
== ข้อมูลเพิ่มเติม ==
 
* [http://www.nongnu.org/avr-libc/user-manual/modules.html เอกสารอธิบายการใช้งานไลบรารี AVR Libc]
 
* [http://www.nongnu.org/avr-libc/user-manual/modules.html เอกสารอธิบายการใช้งานไลบรารี AVR Libc]
* [http://kunetlab2.cpe.ku.ac.th/download/mcu/doc8161-pa-series.pdf Datasheet สำหรับ ATmega168]
+
* [http://kunetlab2.cpe.ku.ac.th/download/mcu/doc8161-pa-series.pdf Datasheet สำหรับ ATmega168/ATmega328P]
 
* [http://kunetlab2.cpe.ku.ac.th/download/mcu/doc0856-avr-instr.pdf ชุดคำสั่งของ AVR (ภาษาเครื่อง)]
 
* [http://kunetlab2.cpe.ku.ac.th/download/mcu/doc0856-avr-instr.pdf ชุดคำสั่งของ AVR (ภาษาเครื่อง)]

รุ่นแก้ไขปัจจุบันเมื่อ 23:44, 22 มกราคม 2563

วิกินี้เป็นส่วนหนึ่งของรายวิชา 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 เป็นชื่อไฟล์สำหรับโครงงานนั้น ๆ เท่านั้น

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

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