บทความนี้เป็นชุดบทความเขียนโปรแกรมที่มุ่งเน้นกับ Cortex-M0 ผ่านทาง STM32F030F4P6 หรือไมโครคอนโทรลเลอร์ STM32 รุ่นอื่น ๆ ในแบบที่ใช้ CMSIS ซึ่งเป็นเฟิร์มแวร์ (firmware) ของ ARM ที่เรียบเรียงจากชุดบทความ Bare Metal : STM32 Programming ของ vivonomicon.com โดยไม่ใช้เฟรมเวิร์กของ Arduino และในบทความตอนที่ 1 เป็นเรื่องของการเตรียมความพร้อม ประกอบด้วยการสร้างไฟล์ลิงค์สำหรับเชื่อมโยงส่วนต่าง ๆ ของโค้ดเข้าด้วยกัน และไฟล์ส่วนของการทำงาน หลังจากนั้นนำไฟล์ผลลัพธ์อัพโหลดเข้าไมโครคอนโทรลเลอร์เป็นการเสร็จสิ้นขั้นตอนการพัฒนาโปรแกรม
อุปกรณ์ประกอบการเรียนรู้
- ARM none eabi tool chain
- บอร์ดที่ใช้ไมโครคอนโทรลเลอร์ Cortex-M0 เช่น
- STM32F030F4P6
- STM32F0 DISCOVERY
- หรือ Cortex-M3 เช่น ET-STM32F103 /Cortex-M4
- ST-Link/V2 หรือ USB-RS232 หรือถ้าเป็นบอร์ด ET-STM32F103 ให้ใช้ตัวแปลง USB2RS232 กับสาย RS232
- คอมพิวเตอร์และระบบปฏิบัติการที่สามารถติดตั้ง ARM none eabi tool chain ได้ ซึ่งทีมงานเราใช้ Computer Notebook ที่ใช้หน่วยประมวลผลของ Intel N5000 กับ RAM 4GB ติดตั้งระบบปฏิบัติการ Linux MINT 20.2 ดังภาพที่ 2
ขั้นตอนการพัฒนาโปรแกรม
ขั้นตอนของการพัฒนาโปรแกรมเพื่อใช้กับไมโครคอนโทรลเลอร์ ARM มีดังนี้
- เตรียมไฟล์ .ld สำหรับกำหนดการลิงค์เพื่อสร้างโปรแกรม
- เขียนโค้ดภาษาแอสเซ็มบลีในไฟล์ .S หรือ .s สำหรับเตรียมส่วนเริ่มต้นทำงานของระบบ
- เขียนโค้ดภาษาซี
- ทำการคอมไพล์โค้ดโปรแกรมเพื่อสร้าง .obj
- ทำการลิงค์เพื่อรวม .obj เข้ากับไลบรารีต่าง ๆ ตามที่กำหนดใน .ld
- อัพโหลดโปรแกรมเข้าบอร์ด
องค์ประกอบของไฟล์ตาม CMSIS-Core
ไฟล์ที่ต้องเรียกใช้ในโปรแกรมที่เขียน ประกอบด้วย
- device.h
- system_device.h
- core_cpu.h
- cmsis_compiler.h
ไฟล์ที่ใช้ในการลิงค์ประกอบด้วย
- โปรแกรมที่พัฒนา (มี main() )
- startup_xxx.S หรือ startup_xxx.c
- system_xxx.c
องค์ประกอบของไฟล์ตาม CMSIS Embedded Application
ไฟล์สำหรับเรียกใช้ในโปรแกรมที่เขียน ประกอบด้วย
- device.h
ไฟล์ที่ใช้ในการลิงค์ประกอบด้วย
- startup_<device>.c หรือ .S
- system_<device>.c
- โปรแกรมที่พัฒนา (มี 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ให้เลือกประเภทของการเชื่อมต่อให้ตรงกับอุปกรณ์ ซึ่งทางทีมงานเราเลือกใช้ UART หรือทำงานผ่านตัวแปลง USB เป็น RS232 และเลือกพอร์ตเชื่อมต่อให้ถูกต้อง โดยก่อนทำการเชื่อมต่อ (ก่อนกดปุ่ม connect) ต้องบูตให้อยู่ในโหลดโปรแกรมชิพ ด้วยการเลือก BOOT0 เป็น VCC แล้วกดสวิตช์ Reset เมื่อมากดปุ่ม Connect ของโปรแกรม STM32CubeProgrammer จะทำการเชื่อมต่อ เมื่อเชื่อมต่อสำเร็จจะแสดงดังภาพที่ 4 มีคำว่า Connected และปุ่ม Connect เปลี่ยนข้อความเป็น Disconnect เพื่อใช้สำหรับกดสั่งยกเลิกการเชื่อมต่อ
หมายเหตุ
กรณีที่ใช้แบบอื่น ๆ มีขั้นตอนเหมือนกัน คือ เลือกประเภทของการเชื่อมต่อ ระบุพอร์ตสื่อสาร ตั้งค่าความเร็วในการสื่อสาร หลังจากนั้นสั่ง connect เพื่อเชื่อมต่อกับอุปกรณ์
จากภาพที่ 4 ให้คลิกไอคอน Erasing & Programming (ภาพที่อยู่ต่อจากภาพคล้ายดินสอหรือปากกา) จะแสดงหน้าจอดังภาพที่ 5 ในขั้นตอนนี้ต้องเลือกไฟล์โปรแกรมที่คอมไพล์แล้วด้วยการคลิกที่ปุ่ม Browse และหน้าจอตามภาพที่ 6 จะแสดง ให้เลือกไฟล์ main.elf และคลิก Open หลังจากนั้นจะแสดงรายชื่อไฟล์ที่เปิดดังภาพที่ 8
เมื่อเปิดไฟล์โปรแกรมที่ใช้สำหรับอัพโหลด (ตัวโปรแกรม STM32CubeProgramming ใช้คำว่า Download) เข้าสู่ไมโครคอนโทรลเลอร์ ให้คลิกที่ปุ่ม Start Program จะเข้าสู่กระบวนการเขียนชิพ (ปกติพวกเรานิยมคลิกที่ปุ่ม Full chip erase ก่อน เพื่อให้มั่นใจว่าข้อมูลในรอมถูกลบไปทั้งหมด) ถ้าสำเร็จเรียบร้อยจะแสดงหน้าต่างดังภาพที่ 7 แล้วให้คลิกที่ปุ่ม Disconnect เพื่อยกเลิกการเชื่อมต่อจะทำให้ปุ่ม Disconnect เปลี่ยนเป็น Connect ดังภาพที่ 8
สรุป
จากบทความนี้จะพบว่าขั้นตอนการเขียนโปรแกรมแบบ Bare Metal หรือเขียนด้วยชุดพัฒนาของชิพโดยไม่ได้ใช้เครื่องมือของ Arduino นั้นมีขั้นตอนที่มากกว่า ทั้งนี้เนื่องจาก ผู้ที่ทำชุดเฟรมเวิร์กครอบ Bare Metal เพื่อให้ใช้งานได้กับ Arduino เขาได้ซ่อนขั้นตอนเหล่านี้เอาไว้ทำให้ผู้เริ่มต้นเขียนโปรแกรมมีความสะดวกมากกว่า ด้วยเหตุนี้จึงพบว่า ความนิยมของการใช้ Arduino จึงมีสูง แต่ถ้าเมื่อไรที่ต้องเขียนโปรแกรมกับไมโครคอนโทรลเลอร์ที่ชุดพัฒนา Arduino ยังไม่สนับสนุน หรือสนับสนุนแต่ไม่ครอบคลุมการทำงานทั้งหมด ทางเลือกที่เป็นไปได้ของผู้พัฒนาคือ กลับมาสู่ Bare Metal ส่วนไมโครคอนโทรลเลอร์ ESP32 ก็คือ การกลับไปใช้ ESP-IDF สุดท้าย ขอให้สนุกกับการเขียนโปรแกรมครับ
ท่านใดต้องการพูดคุยสามารถคอมเมนท์ไว้ได้เลยครับ
แหล่งอ้างอิง
- bare-metal-stm32-programming-part-1-hello-arm @vivonomicon.com
- STM32F030F4 Mainstream Arm Cortex-M0 Value line MCU with 16 Kbytes of Flash memory, 48 MHz CPU
- Startup File startup_<device>.c
- Startup File startup_<device>.s (deprecated)
- System Configuration Files system_<device>.c and system_<device>.h
- Device Header File <device.h>
(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-07-10, 2021-07-11, 2021-07-12, 2021-10-21