[TH] Bare Metal Cortex-M Ep.1

บทความนี้เป็นชุดบทความเขียนโปรแกรมที่มุ่งเน้นกับ Cortex-M0 ผ่านทาง STM32F030F4P6 หรือไมโครคอนโทรลเลอร์ STM32 รุ่นอื่น ๆ ในแบบที่ใช้ CMSIS ซึ่งเป็นเฟิร์มแวร์ (firmware) ของ ARM ที่เรียบเรียงจากชุดบทความ Bare Metal : STM32 Programming ของ vivonomicon.com โดยไม่ใช้เฟรมเวิร์กของ Arduino และในบทความตอนที่ 1 เป็นเรื่องของการเตรียมความพร้อม ประกอบด้วยการสร้างไฟล์ลิงค์สำหรับเชื่อมโยงส่วนต่าง ๆ ของโค้ดเข้าด้วยกัน และไฟล์ส่วนของการทำงาน หลังจากนั้นนำไฟล์ผลลัพธ์อัพโหลดเข้าไมโครคอนโทรลเลอร์เป็นการเสร็จสิ้นขั้นตอนการพัฒนาโปรแกรม

ภาพที่ 1 บอร์ด STM32F030F4P6

อุปกรณ์ประกอบการเรียนรู้

  1. ARM none eabi tool chain
  2. บอร์ดที่ใช้ไมโครคอนโทรลเลอร์ Cortex-M0 เช่น
    1. STM32F030F4P6
    2. STM32F0 DISCOVERY
    3. หรือ Cortex-M3 เช่น   ET-STM32F103 /Cortex-M4
  3. ST-Link/V2 หรือ USB-RS232 หรือถ้าเป็นบอร์ด ET-STM32F103 ให้ใช้ตัวแปลง USB2RS232 กับสาย RS232
  4. คอมพิวเตอร์และระบบปฏิบัติการที่สามารถติดตั้ง ARM none eabi tool chain ได้ ซึ่งทีมงานเราใช้ Computer Notebook ที่ใช้หน่วยประมวลผลของ Intel N5000 กับ RAM 4GB ติดตั้งระบบปฏิบัติการ Linux MINT 20.2 ดังภาพที่ 2
ภาพที่ 2 เครื่องของทางทีมงาน

ขั้นตอนการพัฒนาโปรแกรม

ขั้นตอนของการพัฒนาโปรแกรมเพื่อใช้กับไมโครคอนโทรลเลอร์ ARM มีดังนี้

  1. เตรียมไฟล์ .ld สำหรับกำหนดการลิงค์เพื่อสร้างโปรแกรม
  2. เขียนโค้ดภาษาแอสเซ็มบลีในไฟล์ .S หรือ .s สำหรับเตรียมส่วนเริ่มต้นทำงานของระบบ
  3. เขียนโค้ดภาษาซี
  4. ทำการคอมไพล์โค้ดโปรแกรมเพื่อสร้าง .obj
  5. ทำการลิงค์เพื่อรวม .obj เข้ากับไลบรารีต่าง ๆ ตามที่กำหนดใน .ld
  6. อัพโหลดโปรแกรมเข้าบอร์ด

องค์ประกอบของไฟล์ตาม CMSIS-Core

ไฟล์ที่ต้องเรียกใช้ในโปรแกรมที่เขียน ประกอบด้วย

  1. device.h
  2. system_device.h
  3. core_cpu.h
  4. cmsis_compiler.h

ไฟล์ที่ใช้ในการลิงค์ประกอบด้วย

  1. โปรแกรมที่พัฒนา (มี main() )
  2. startup_xxx.S หรือ startup_xxx.c
  3. system_xxx.c

องค์ประกอบของไฟล์ตาม CMSIS Embedded Application

ไฟล์สำหรับเรียกใช้ในโปรแกรมที่เขียน ประกอบด้วย

  1. device.h

ไฟล์ที่ใช้ในการลิงค์ประกอบด้วย

  1. startup_<device>.c หรือ .S
  2. system_<device>.c
  3. โปรแกรมที่พัฒนา (มี main( ) )

เตรียมไฟล์สำหรับการลิงค์

ไฟล์สำหรับการลิงค์เป็นนามสกุล .ld ใช้สำหรับกำหนดตำแหน่งของหน่วยความจำและขนาดของหน่วยความของไมโครคอนโทรลเลอร์ ซึ่งค่าสำหรับ stm32f030f4p6 เป็นดังนี้ และบันทึกลงไฟล์ชื่อ STM32F030F4P6.ld

/* Label for the program's entry point */
    ENTRY(reset_handler)
    /* End of RAM / Start of stack  (4KB SRAM) */
    _estack = 0x20001000;
    /* กำหนดขปริมาณ ram ที่จองแบบ dynamic */
    _Min_Leftover_RAM = 0x400;
    MEMORY
    {
      FLASH ( rx )      : ORIGIN = 0x08000000, LENGTH = 16K
      RAM ( rxw )       : ORIGIN = 0x20000000, LENGTH = 4K
    }
    SECTIONS
    {
      /* The vector table goes at the start of flash. */
      .vector_table :
      {
        . = ALIGN(4);
        KEEP (*(.vector_table))
        . = ALIGN(4);
      } >FLASH
      /* The 'text' section contains the main program code. */
      .text :
      {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        . = ALIGN(4);
      } >FLASH
      /* The 'rodata' section contains read-only data,
       * constants, strings, information that won't change. */
      .rodata :
      {
        . = ALIGN(4);
        *(.rodata)
        *(.rodata*)
        . = ALIGN(4);
      } >FLASH
      /* The 'data' section is space set aside in RAM for
       * things like variables, which can change. */
      _sidata = .;
      .data : AT(_sidata)
      {
        . = ALIGN(4);
        /* Mark start/end locations for the 'data' section. */
        _sdata = .;
        *(.data)
        *(.data*)
        _edata = .;
        . = ALIGN(4);
      } >RAM
      /* The 'bss' section is similar to the 'data' section,
       * but its space is initialized to all 0s at the
       * start of the program. */
      .bss :
      {
        . = ALIGN(4);
        /* Also mark the start/end of the BSS section. */
        _sbss = .;
        *(.bss)
        *(.bss*)
        *(COMMON)
        . = ALIGN(4);
        _ebss = .;
      } >RAM
      /* Space set aside for the application's heap/stack. */
      .dynamic_allocations :
      {
        . = ALIGN(4);
        _ssystem_ram = .;
        . = . + _Min_Leftover_RAM;
        . = ALIGN(4);
        _esystem_ram = .;
      } >RAM
    }

จากไฟล์จะพบว่าต้องกำหนดให้หน่วยความจำแรมเป็น 4kB รอมเป็น 16kB โดยตำแหน่งเริ่มต้นของ RAM หรือ SRAM จะอยู่ที่ 0x20000000 และ ROM หรือ FLASH ROM อยู่ที่ 0x8000000 ส่วน _estack คือ ตำแหน่งสุดท้ายของหน่วยความจำสแตก (end of stack) ซึ่งกำหนดให้อยู่ที่ 0x20001000 มีส่วนของ SECTIONS เพิ่มเติมเข้ามา โดยแต่ละส่วนใน SECTIONS จะขึ้นต้นด้วยเครื่องหมาย . อันได้แก่

.vector_table สำหรับกำหนดเกี่ยวกับตารางเว็กเตอร์
.text สำหรับกำหนดเกี่ยวกับหน่วยความจำเก็บโปรแกรมซึ่งเป็นหน่วยความจำแฟลช
.data สำหรับกำหนดเกี่ยวกับหน่วยความจำเก็บข้อมูล
.bss สำหรับกำหนดเกี่ยวกับหน่วยความจำสแต็ก
.dynamic_allocations สำหรับกำหนดเกี่ยวกับการจองหน่วยความจำแบบพลวัต

ตารางเว็กเตอร์

ตารางเว็กเตอร์เป็นส่วนของกำหนดการตอบสนองการขัดจังหวะ ใช้สำหรับระบุตำแหน่งของโปรแกรมย่อยที่ถูกเรียกเมื่อเกิดเหตุการณ์ขัดจังหวะเกิดขึ้น โดยข้อมูลของตารางนี้จัดเก็บในไฟล์ชื่อ vector_table.S ซึ่งเป็นไฟล์ภาษาแอสเซมบลี (.s หรือ .S ใช้สำหรับบ่งบอกให้ทราบว่าเป็นไฟล์สำหรับเขียนภาษาแอสเซมบลี) ดังตัวอย่างต่อไปนี้

.syntax unified
.cpu cortex-m0
.fpu softvfp
.thumb

.global vtable
.global default_interrupt_handler

.type vtable, %object
.section .vector_table,"a",%progbits
vtable:
  // 0-15
  .word _estack
  .word reset_handler
  .word NMI_handler
  .word hard_fault_handler
  .word 0
  .word 0
  .word 0
  .word 0
  .word 0
  .word 0
  .word 0
  .word SVC_handler
  .word 0
  .word 0
  .word pending_SV_handler
  .word SysTick_handler
  // 16-31
  .word window_watchdog_IRQ_handler
  .word PVD_IRQ_handler
  .word RTC_IRQ_handler
  .word flash_IRQ_handler
  .word RCC_IRQ_handler
  .word EXTI0_1_IRQ_handler
  .word EXTI2_3_IRQ_handler
  .word EXTI4_15_IRQ_handler
  .word 0
  .word DMA1_chan1_IRQ_handler
  .word DMA1_chan2_3_IRQ_handler
  .word DMA1_chan4_5_IRQ_handler
  .word ADC1_IRQ_handler
  .word TIM1_break_IRQ_handler
  .word TIM1_CC_IRQ_handler
  .word TIM2_IRQ_handler
  // 32-47
  .word TIM3_IRQ_handler
  .word 0
  .word 0
  .word TIM14_IRQ_handler
  .word 0
  .word TIM16_IRQ_handler
  .word TIM17_IRQ_handler
  .word I2C1_IRQ_handler
  .word 0
  .word SPI1_IRQ_handler
  .word 0
  .word USART1_IRQ_handler
  .word 0
  .word 0
  .word 0
  .word 0
  // 48
  // (Location to boot from for RAM startup)
  #define boot_ram_base  0xF108F85F
  .word boot_ram_base

  .weak NMI_handler
  .thumb_set NMI_handler,default_interrupt_handler
  .weak hard_fault_handler
  .thumb_set hard_fault_handler,default_interrupt_handler
  .weak SVC_handler
  .thumb_set SVC_handler,default_interrupt_handler
  .weak pending_SV_handler
  .thumb_set pending_SV_handler,default_interrupt_handler
  .weak SysTick_handler
  .thumb_set SysTick_handler,default_interrupt_handler
  .weak window_watchdog_IRQ_handler
  .thumb_set window_watchdog_IRQ_handler,default_interrupt_handler
  .weak PVD_IRQ_handler
  .thumb_set PVD_IRQ_handler,default_interrupt_handler
  .weak RTC_IRQ_handler
  .thumb_set RTC_IRQ_handler,default_interrupt_handler
  .weak flash_IRQ_handler
  .thumb_set flash_IRQ_handler,default_interrupt_handler
  .weak RCC_IRQ_handler
  .thumb_set RCC_IRQ_handler,default_interrupt_handler
  .weak EXTI0_1_IRQ_handler
  .thumb_set EXTI0_1_IRQ_handler,default_interrupt_handler
  .weak EXTI2_3_IRQ_handler
  .thumb_set EXTI2_3_IRQ_handler,default_interrupt_handler
  .weak EXTI4_15_IRQ_handler
  .thumb_set EXTI4_15_IRQ_handler,default_interrupt_handler
  .weak DMA1_chan1_IRQ_handler
  .thumb_set DMA1_chan1_IRQ_handler,default_interrupt_handler
  .weak DMA1_chan2_3_IRQ_handler
  .thumb_set DMA1_chan2_3_IRQ_handler,default_interrupt_handler
  .weak DMA1_chan4_5_IRQ_handler
  .thumb_set DMA1_chan4_5_IRQ_handler,default_interrupt_handler
  .weak ADC1_IRQ_handler
  .thumb_set ADC1_IRQ_handler,default_interrupt_handler
  .weak TIM1_break_IRQ_handler
  .thumb_set TIM1_break_IRQ_handler,default_interrupt_handler
  .weak TIM1_CC_IRQ_handler
  .thumb_set TIM1_CC_IRQ_handler,default_interrupt_handler
  .weak TIM2_IRQ_handler
  .thumb_set TIM2_IRQ_handler,default_interrupt_handler
  .weak TIM3_IRQ_handler
  .thumb_set TIM3_IRQ_handler,default_interrupt_handler
  .weak TIM14_IRQ_handler
  .thumb_set TIM14_IRQ_handler,default_interrupt_handler
  .weak TIM16_IRQ_handler
  .thumb_set TIM16_IRQ_handler,default_interrupt_handler
  .weak TIM17_IRQ_handler
  .thumb_set TIM17_IRQ_handler,default_interrupt_handler
  .weak I2C1_IRQ_handler
  .thumb_set I2C1_IRQ_handler,default_interrupt_handler
  .weak SPI1_IRQ_handler
  .thumb_set SPI1_IRQ_handler,default_interrupt_handler
  .weak USART1_IRQ_handler
  .thumb_set USART1_IRQ_handler,default_interrupt_handler
.size vtable, .-vtable

.section .text.default_interrupt_handler,"ax",%progbits
default_interrupt_handler:
  default_interrupt_loop:
    B default_interrupt_loop
.size default_interrupt_handler, .-default_interrupt_handler

จากโค้ดข้างต้นมีการกำหนดใช้หน่วยประมวลผลเป็น cortex-m0 และใช้หน่วยประมวลผลทศนิยมเป็นซอฟต์แวร์ (softvfp) และในตารางเว็กเตอร์ (.type vtable) ได้กำหนดตำแหน่งของสแตกและ reset_handler ซึ่งทั้งสองเป็นค่าตัวเลขจำนวนเต็มขนาด word (.word) หรือ 4 ไบต์

โปรแกรมหลัก

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

ส่วนเริ่มต้นทำงาน

ในส่วนของโปรแกรมหลักเป็นดังนี้ โดยบันทึกลงไฟล์ชื่อ core.S เพื่อใช้สำหรับเริ่มต้นทำงาน และระบุการเรียกใช้ฟังก์ชัน main ที่เขียนในภาษา C และทำการวนรอบไม่รู้จบด้วยการ B หรือกระโดดไปทำที่เลเบิล LoopForever

.syntax unified
.cpu cortex-m0
.fpu softvfp
.thumb

.global reset_handler
.type reset_handler, %function
reset_handler:
  LDR  r0, =_estack
  MOV  sp, r0      
  MOVS r0, #0
// Load the start/end addresses of the data section,
// and the start of the data init section.
  LDR  r1, =_sdata   
  LDR  r2, =_edata
  LDR  r3, =_sidata
  B    copy_sidata_loop

copy_sidata:
  // Offset the data init section by our copy progress.
  LDR  r4, [r3, r0]
  // Copy the current word into data, and increment.
  STR  r4, [r1, r0]
  ADDS r0, r0, #4

copy_sidata_loop:
  // Unless we've copied the whole data section, copy the
  // next word from sidata->data.
  ADDS r4, r0, r1
  CMP  r4, r2
  BCC  copy_sidata

// Once we are done copying the data section into RAM,
// move on to filling the BSS section with 0s.
  MOVS r0, #0
  LDR  r1, =_sbss
  LDR  r2, =_ebss
  B    reset_bss_loop

// Fill the BSS segment with '0's.
reset_bss:
  // Store a 0 and increment by a word.
  STR  r0, [r1]
  ADDS r1, r1, #4

reset_bss_loop:
  // We'll use R1 to count progress here; if we aren't
  // done, reset the next word and increment.
  CMP  r1, r2
  BCC  reset_bss

 // เรียกใช้ main และทำการวนรอบไม่รู้จบเมื่อทำงานใน main เสร็จ
  B    main
LoopForever:
  B    LoopForever
.size reset_handler, .-reset_handler

ภาษา c

เมื่อเตรียมครบทุกไฟล์ ตอนนี้เหลือเพียงส่วนของภาษา C ที่ต้องเขียนขึ้นมา ตัวอย่างสำหรับเริ่มต้นเป็นดังนี้ จะพบว่าเราไม่ทำอะไรเลย เนื่องจากการเชื่อมต่อกับอุปกรณ์ภายนอกหรือ Peripheral นั้นต้องเรียกใช้เฟิร์แวร์ (firmware) หรือไลบรารีของบริษัทที่ผลิตชิพมาเรียกใช้งานร่วมกับโค่ดที่เราเขียน

int main(void) {

}

คอมไพล์โปรแกรม

ทดสอบคอมไพล์โปรแกรมประกอบด้วยการคอมไพล์ไฟล์ core.S, vector_table.S และ main.c

arm-none-eabi-gcc \
-x assembler-with-cpp -c -O0 \
-mcpu=cortex-m0 -mthumb -Wall \
core.S -o core.o
arm-none-eabi-gcc \
-x assembler-with-cpp -c -O0 \
-mcpu=cortex-m0 -mthumb -Wall \
vector_table.S -o vector_table.o
arm-none-eabi-gcc -c \
-mcpu=cortex-m0 -mthumb -Wall -g \
-fmessage-length=0 --specs=nosys.specs \
main.c -o main.o 

เมื่อสำเร็จจะได้ไฟล์ core.o สำหรับกำหนดการทำงานเบื้องต้นเมื่อโปรแกรมถูกเรียกใช้งาน และทำหน้าที่โหลดฟังก์ชัน main ที่เขียนในภาษา C ส่วนไฟล์ vector_table.o เป็นส่วนของการ call back ไปยังฟังก์ชันตอบสนองการขัดจังหวะ และ main.o เป็นไฟล์ท่ได้จากการคอมไพล์โค้ดภาษา C ที่เขียนขึ้น เมื่อได้ไฟล์ทั้ง 3 โดยไม่มีความผิดพลาดใด ขั้นตอนถัดไป คือ การรวมไฟล์ทั้งสามภายใต้เงื่อนไขของลิงค์สคริปต์ โดยสั่งงานดังต่อไปนี้

arm-none-eabi-gcc \
core.o vector_table.o main.o \
-mcpu=cortex-m0 \
-mthumb -Wall --specs=nosys.specs \
-nostdlib -lgcc -T./STM32F030F4P6.ld \
-o main.elf

อัพโหลดโปรแกรม

เมื่อสำเร็จจะได้ไฟล์ main.elf ซึ่งเป็นไฟล์ของโปรแกรมทั้งหมดที่เขียนขึ้น และพร้อมสำหรับนำไปอัพโหลดเข้าไมโครคอนโทรลเลอร์ ด้วยโปรแกรม STM32CubeProgrammer เป็นอันจบการเริ่มต้นชีวิตการเขียนโปรแกรมกับ STM32F030F4P6 เป็นดังภาพที่ 3, 4, 5, 6, 7 และ 8

ภาพที่ 3 เปิดโปรแกรม และเลือกรูปแบบการเชื่อมต่อ

จากภาพที่ 3ให้เลือกประเภทของการเชื่อมต่อให้ตรงกับอุปกรณ์ ซึ่งทางทีมงานเราเลือกใช้ UART หรือทำงานผ่านตัวแปลง USB เป็น RS232 และเลือกพอร์ตเชื่อมต่อให้ถูกต้อง โดยก่อนทำการเชื่อมต่อ (ก่อนกดปุ่ม connect) ต้องบูตให้อยู่ในโหลดโปรแกรมชิพ ด้วยการเลือก BOOT0 เป็น VCC แล้วกดสวิตช์ Reset เมื่อมากดปุ่ม Connect ของโปรแกรม STM32CubeProgrammer จะทำการเชื่อมต่อ เมื่อเชื่อมต่อสำเร็จจะแสดงดังภาพที่ 4 มีคำว่า Connected และปุ่ม Connect เปลี่ยนข้อความเป็น Disconnect เพื่อใช้สำหรับกดสั่งยกเลิกการเชื่อมต่อ

ภาพที่ 4 รายงานการเชื่อมต่อกรณีการเชื่อมต่อสำเร็จ

หมายเหตุ
กรณีที่ใช้แบบอื่น ๆ มีขั้นตอนเหมือนกัน คือ เลือกประเภทของการเชื่อมต่อ ระบุพอร์ตสื่อสาร ตั้งค่าความเร็วในการสื่อสาร หลังจากนั้นสั่ง connect เพื่อเชื่อมต่อกับอุปกรณ์

จากภาพที่ 4 ให้คลิกไอคอน Erasing & Programming (ภาพที่อยู่ต่อจากภาพคล้ายดินสอหรือปากกา) จะแสดงหน้าจอดังภาพที่ 5 ในขั้นตอนนี้ต้องเลือกไฟล์โปรแกรมที่คอมไพล์แล้วด้วยการคลิกที่ปุ่ม Browse และหน้าจอตามภาพที่ 6 จะแสดง ให้เลือกไฟล์ main.elf และคลิก Open หลังจากนั้นจะแสดงรายชื่อไฟล์ที่เปิดดังภาพที่ 8

ภาพที่ 5 หน้าจออัพโหลดโปรแกรม
ภาพที่ 6 หน้าจอเลือกไฟล์สำหรับอัพโหลดลงไมโครคอนโทรลเลอร์

เมื่อเปิดไฟล์โปรแกรมที่ใช้สำหรับอัพโหลด (ตัวโปรแกรม STM32CubeProgramming ใช้คำว่า Download) เข้าสู่ไมโครคอนโทรลเลอร์ ให้คลิกที่ปุ่ม Start Program จะเข้าสู่กระบวนการเขียนชิพ (ปกติพวกเรานิยมคลิกที่ปุ่ม Full chip erase ก่อน เพื่อให้มั่นใจว่าข้อมูลในรอมถูกลบไปทั้งหมด) ถ้าสำเร็จเรียบร้อยจะแสดงหน้าต่างดังภาพที่ 7 แล้วให้คลิกที่ปุ่ม Disconnect เพื่อยกเลิกการเชื่อมต่อจะทำให้ปุ่ม Disconnect เปลี่ยนเป็น Connect ดังภาพที่ 8

ภาพที่ 7รายงานเมื่ออัพโหลดสำเร็จ
ภาพที่ 8 ผลลัพธ์หลังจากยกเลิกการเชื่อมต่อ

สรุป

จากบทความนี้จะพบว่าขั้นตอนการเขียนโปรแกรมแบบ Bare Metal หรือเขียนด้วยชุดพัฒนาของชิพโดยไม่ได้ใช้เครื่องมือของ Arduino นั้นมีขั้นตอนที่มากกว่า ทั้งนี้เนื่องจาก ผู้ที่ทำชุดเฟรมเวิร์กครอบ Bare Metal เพื่อให้ใช้งานได้กับ Arduino เขาได้ซ่อนขั้นตอนเหล่านี้เอาไว้ทำให้ผู้เริ่มต้นเขียนโปรแกรมมีความสะดวกมากกว่า ด้วยเหตุนี้จึงพบว่า ความนิยมของการใช้ Arduino จึงมีสูง แต่ถ้าเมื่อไรที่ต้องเขียนโปรแกรมกับไมโครคอนโทรลเลอร์ที่ชุดพัฒนา Arduino ยังไม่สนับสนุน หรือสนับสนุนแต่ไม่ครอบคลุมการทำงานทั้งหมด ทางเลือกที่เป็นไปได้ของผู้พัฒนาคือ กลับมาสู่ Bare Metal ส่วนไมโครคอนโทรลเลอร์ ESP32 ก็คือ การกลับไปใช้ ESP-IDF สุดท้าย ขอให้สนุกกับการเขียนโปรแกรมครับ

ท่านใดต้องการพูดคุยสามารถคอมเมนท์ไว้ได้เลยครับ

แหล่งอ้างอิง

  1. bare-metal-stm32-programming-part-1-hello-arm @vivonomicon.com
  2. STM32F030F4 Mainstream Arm Cortex-M0 Value line MCU with 16 Kbytes of Flash memory, 48 MHz CPU
  3. Startup File startup_<device>.c
  4. Startup File startup_<device>.s (deprecated)
  5. System Configuration Files system_<device>.c and system_<device>.h
  6. Device Header File <device.h>

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-07-10, 2021-07-11, 2021-07-12, 2021-10-21