Embedded C Development Part III : Loader&Startup Code : โหลดลงแล้วส่ง เมรุ()

ไฟล์แบบที่เรียกได้ว่าสามารถนำไปดำเนินการได้ (executable file) ซึ่งเป็นผลพวงต่อเนื่องมาจากการ compile จากการ link อย่างไรก็ตามคำว่าสามารถอาจจะไม่เพียงพอกับคำว่าใช้ได้จริง เนื่องจากมีเงื่อนงำบางอย่างที่ต้องทำให้กระจ่างชัด ก่อนจะลงมือดำเนินการกับแบบจริงจริงจังจัง ข้นตอนต่อมาจึงเป็นการปรับแต่งให้ใช้ได้จริงบนบอร์ดเป้าหมาย

นี่ถือเป็นหน้าที่ของ Loader !!!

การมี Loader สำหรับปรับแต่งไฟล์ Executable (ไฟล์ที่คอมไพล์แล้ว) ในระบบฝังตัวหรือไมโครคอนโทรลเลอร์มีความสำคัญและจำเป็นมาก เนื่องจากหลายเหตุผลที่เกี่ยวข้องกับการจัดการหน่วยความจำ, ความสามารถในการปรับตำแหน่งที่อยู่ในหน่วยความจำ, และการทำให้โปรแกรมทำงานได้อย่างมีประสิทธิภาพในสภาพแวดล้อมที่ทรัพยากรจำกัด สิ่งนี้เป็นปัจจัยสำคัญที่ช่วยให้โปรแกรมทำงานได้อย่างเหมาะสมในระบบที่จำกัดทรัพยากร โดยเฉพาะในกรณีที่โปรแกรมต้องทำงานในหน่วยความจำที่มีข้อจำกัดและจำเป็นต้องใช้พื้นที่อย่างมีประสิทธิภาพ

เหตุผลที่จำเป็นต้องใช้ Loader เพื่อปรับแต่ง Executable File

🚩 การจัดการกับที่อยู่ในหน่วยความจำ (Memory Addressing) โปรแกรมที่คอมไพล์แล้วมักจะมีการอ้างอิงถึงที่อยู่ในหน่วยความจำ เช่น ในไฟล์ ELF (Executable and Linkable Format) หรือ PE (Portable Executable) ที่ระบุที่อยู่ที่เฉพาะเจาะจง ในบางระบบ เช่น ระบบฝังตัวหรือไมโครคอนโทรลเลอร์ ที่หน่วยความจำอาจถูกจัดสรรในลำดับที่แตกต่างจากที่โปรแกรมคาดหวัง จะทำให้โปรแกรมไม่สามารถทำงานได้อย่างถูกต้อง ถ้าไม่มีการปรับตำแหน่งที่อยู่ให้ตรงกัน การปรับตำแหน่งที่อยู่เหล่านี้เรียกว่า relocation โดย Loader จะคำนวณและปรับที่อยู่ต่าง ๆ ในไฟล์ Executable ให้ตรงกับตำแหน่งที่อยู่จริงในหน่วยความจำที่โปรแกรมจะทำงาน เพื่อให้โปรแกรมทำงานได้อย่างถูกต้อง

🚩 การจัดการกับการเริ่มต้นโปรแกรม (Program Initialization) เมื่อโปรแกรมถูกคอมไพล์แล้ว จะมีส่วนต่าง ๆ ที่ต้องได้รับค่าล่วงหน้า เช่น data segment (ข้อมูลที่ต้องการค่าล่วงหน้า) และ bss segment (ข้อมูลที่ไม่ได้รับค่าล่วงหน้าและต้องการตั้งค่าเป็น 0) ก่อนที่โปรแกรมจะเริ่มทำงาน บางครั้งตัวแปรใน data segment อาจจะต้องถูกกำหนดค่าเริ่มต้น หรือข้อมูลใน bss segment อาจจะต้องถูกล้างเพื่อให้โปรแกรมทำงานได้อย่างถูกต้อง ก่อนที่โปรแกรมจะเริ่มทำงานจริง ๆ Loader จะต้องตั้งค่าพื้นที่เหล่านี้ในหน่วยความจำให้ตรงกับที่กำหนดในไฟล์ Executable

🚩 การปรับตำแหน่งของโค้ดและข้อมูล (Code and Data Relocation) ในระหว่างการคอมไพล์ไฟล์ Executable โปรแกรมจะมีการอ้างอิงถึงที่อยู่ในหน่วยความจำ เช่น การเรียกฟังก์ชันหรือการเข้าถึงตัวแปรที่อาจไม่ตรงกับที่อยู่จริงในหน่วยความจำ ระบบที่ไม่มีระบบปฏิบัติการหรือมีการจัดการหน่วยความจำแบบเฉพาะเจาะจงจะต้องมีการปรับที่อยู่ที่ถูกต้องในระหว่างการโหลดโปรแกรมเข้าสู่หน่วยความจำเพื่อให้การอ้างอิงทั้งหมดถูกต้อง Loader จะทำหน้าที่ในการปรับตำแหน่งเหล่านี้ให้ตรงกับที่อยู่ที่โปรแกรมจะทำงาน

🚩 การจัดการกับการสื่อสารกับอุปกรณ์ภายนอก (External Devices Management) ระบบฝังตัวบางระบบที่ทำงานบนไมโครคอนโทรลเลอร์อาจจำเป็นต้องเชื่อมต่อกับอุปกรณ์ภายนอก เช่น เซ็นเซอร์, หน่วยความจำ, หรือพอร์ตการสื่อสารต่าง ๆ เช่น I2C, UART, หรือ SPI ก่อนที่โปรแกรมจะทำงาน Loader จะต้องตั้งค่าพอร์ตการสื่อสารและอุปกรณ์เหล่านี้ให้พร้อมสำหรับการใช้งาน ซึ่งจะทำให้โปรแกรมสามารถทำงานร่วมกับอุปกรณ์ภายนอกได้อย่างมีประสิทธิภาพ

🚩 การปรับตัวโปรแกรมให้ทำงานในสภาพแวดล้อมที่จำกัด (Resource-Constrained Environments) ในระบบฝังตัวที่มีทรัพยากรจำกัด เช่น หน่วยความจำ RAM หรือพื้นที่เก็บข้อมูล Loader จะต้องทำให้แน่ใจว่าโปรแกรมจะถูกโหลดและทำงานในลักษณะที่มีประสิทธิภาพ ซึ่งอาจจะหมายถึงการโหลดเฉพาะส่วนที่จำเป็นของโปรแกรมหรือการบีบอัดข้อมูลที่ไม่จำเป็น เพื่อให้โปรแกรมทำงานได้ภายใต้ข้อจำกัดของระบบ เช่น ลดการใช้ RAM หรือพื้นที่เก็บข้อมูลที่จำกัด

🚩 การตั้งค่าระบบสำหรับการเริ่มต้นโปรแกรม (System Setup for Program Initialization) ในระบบฝังตัวที่ไม่มีระบบปฏิบัติการหรือทำงานในลักษณะ bare-metal การตั้งค่าระบบเพื่อเริ่มต้นโปรแกรมนั้นสำคัญมาก Loader จะต้องทำหน้าที่ในการตั้งค่าระบบเบื้องต้น เช่น การตั้งค่า stackheapprogram counter (PC), และ stack pointer (SP) ให้เหมาะสมก่อนที่โปรแกรมจะเริ่มทำงานเพื่อให้การทำงานของโปรแกรมถูกต้อง

  • Stack เป็นพื้นที่ในหน่วยความจำที่ใช้ในการจัดเก็บข้อมูลที่เกี่ยวข้องกับการทำงานของฟังก์ชัน เช่น ค่าตัวแปร การเก็บที่อยู่ของฟังก์ชันที่เรียกคืน (return address) และตัวแปรภายในฟังก์ชัน ซึ่งต้องตั้งค่าตำแหน่งเริ่มต้นของ Stack Pointer (SP) ให้ถูกต้อง
  • Heap เป็นพื้นที่ในหน่วยความจำที่ใช้สำหรับการจัดสรรหน่วยความจำแบบไดนามิก (เช่น การใช้คำสั่ง malloc() ในภาษา C) เมื่อโปรแกรมทำงาน Loader จะต้องตั้งค่าพื้นที่ heap ให้เหมาะสม เพื่อให้โปรแกรมสามารถใช้พื้นที่นี้ในการจัดสรรหน่วยความจำแบบไดนามิกได้
  • Program Counter (PC) ชี้ไปยังคำสั่งถัดไปที่โปรแกรมจะดำเนินการ ในขณะที่ Stack Pointer (SP) ชี้ไปที่ตำแหน่งปัจจุบันใน stack การตั้งค่าเหล่านี้จะช่วยให้โปรแกรมทำงานได้ถูกต้อง

🚩 การส่งมอบการควบคุมให้กับโปรแกรม (Control Transfer to Program) เมื่อโปรแกรมถูกโหลดลงในหน่วยความจำและการตั้งค่าทั้งหมดเสร็จสิ้น Loader จะส่งมอบการควบคุมให้กับโปรแกรมโดยการส่งการควบคุมไปยัง Entry Point หรือจุดเริ่มต้นของโปรแกรม ซึ่งจะทำให้โปรแกรมเริ่มทำงานจากจุดนี้

🚩 การจัดการกับ Heap และการจัดสรรหน่วยความจำ (Heap Memory Management) เมื่อโปรแกรมทำงานในสภาพแวดล้อมที่มีหน่วยความจำจำกัด การจัดการ heap เป็นสิ่งสำคัญมาก Loader ต้องตั้งค่าพื้นที่ heap ให้พร้อมใช้งาน เพื่อให้โปรแกรมสามารถจัดสรรหน่วยความจำแบบไดนามิกได้อย่างถูกต้อง หากไม่ตั้งค่าให้ดีอาจทำให้เกิด memory leak หรือ heap corruption ซึ่งจะทำให้โปรแกรมทำงานผิดพลาด

🚩 การรองรับการโหลดจาก Flash Memory และการจัดการการเข้าถึงหน่วยความจำที่ช้า ในบางกรณีโปรแกรมอาจถูกเก็บใน Flash Memory ซึ่งมีลักษณะการเข้าถึงที่ช้ากว่า RAM ดังนั้น Loader จะต้องรองรับการโหลดโปรแกรมจาก Flash Memory และทำให้การเข้าถึงข้อมูลเป็นไปอย่างมีประสิทธิภาพ เช่น การใช้ memory mapping หรือการโหลดเฉพาะส่วนของโปรแกรมที่จำเป็น

🚩 การป้องกันข้อผิดพลาดที่อาจเกิดจากการโหลด (Error Handling During Loading) Loader ต้องสามารถตรวจสอบความสมบูรณ์ของไฟล์ Executable ที่จะโหลดและจัดการกับข้อผิดพลาดที่อาจเกิดขึ้นระหว่างการโหลด เช่น การตรวจสอบการสำเร็จของการโหลดข้อมูลหรือการจัดการหน่วยความจำผิดพลาด หากเกิดข้อผิดพลาด ต้องมีการแจ้งเตือนหรือย้อนกลับเพื่อป้องกันไม่ให้โปรแกรมทำงานผิดพลาด

ขั้นตอนการทำงานของ Loader เพื่อทำให้ Executable File สามารถทำงานได้ในสภาพแวดล้อมที่จำกัดและสามารถใช้งานได้อย่างมีประสิทธิภาพในระบบฝังตัวหรือไมโครคอนโทรลเลอร์ ซึ่งจะต้องมีขั้นตอนที่ชัดเจนและเรียงลำดับอย่างมีระเบียบ ดังนี้:

1. การตรวจสอบความสมบูรณ์ของไฟล์ Executable

ก่อนที่ Loader จะเริ่มต้นการโหลดไฟล์ Executable ลงในหน่วยความจำ จำเป็นต้องทำการตรวจสอบความสมบูรณ์ของไฟล์ดังกล่าว เพื่อให้มั่นใจว่าไฟล์นั้นไม่เสียหาย และสามารถทำงานได้อย่างถูกต้องในระบบ:

  • Checksum หรือ Hash Verification: การตรวจสอบค่า checksum หรือ hash ของไฟล์ที่ดาวน์โหลดหรือส่งผ่านจากแหล่งอื่นเพื่อยืนยันว่าไฟล์ไม่ได้ถูกแก้ไขหรือเสียหายระหว่างการส่งข้อมูล ตัวอย่างเช่น การใช้ MD5SHA1, หรือ SHA256 เพื่อคำนวณและเปรียบเทียบกับค่าที่คาดหวังจากแหล่งที่เชื่อถือได้
  • การตรวจสอบฟอร์แมตของไฟล์: ไฟล์ Executable อาจมีฟอร์แมตที่แตกต่างกันไปตามระบบ เช่น ELF (Executable and Linkable Format) สำหรับระบบ Linux หรือ PE (Portable Executable) สำหรับ Windows การตรวจสอบว่าฟอร์แมตของไฟล์นั้นตรงกับระบบปฏิบัติการหรือสถาปัตยกรรมของไมโครคอนโทรลเลอร์
  • การตรวจสอบความเข้ากันได้กับฮาร์ดแวร์: นอกจากการตรวจสอบฟอร์แมตของไฟล์แล้ว Loader ควรตรวจสอบว่าไฟล์สามารถทำงานร่วมกับสถาปัตยกรรมของหน่วยประมวลผล (CPU) หรือไม่ และว่ามีการใช้ชุดคำสั่งหรือทรัพยากรที่เหมาะสมกับระบบของเรา

2. การตั้งค่าเริ่มต้นของระบบ (System Initialization)

เมื่อไฟล์ Executable ได้รับการตรวจสอบแล้ว Loader จะทำการตั้งค่าระบบต่างๆ ที่จำเป็นให้พร้อมใช้งานสำหรับโปรแกรมที่กำลังจะเริ่มทำงาน:

  • Stack Pointer (SP)Stack ใช้สำหรับเก็บข้อมูลที่เกี่ยวข้องกับการเรียกใช้ฟังก์ชันหรือการจัดการข้อผิดพลาด (exceptions) โดย Loader จะตั้งค่าพอยน์เตอร์ของสแตกให้เหมาะสม เพื่อให้ระบบสามารถจัดการหน่วยความจำได้อย่างมีประสิทธิภาพ
  • Program Counter (PC)PC คือพอยน์เตอร์ที่ชี้ไปยังตำแหน่งของคำสั่งถัดไปที่จะทำงาน เมื่อโปรแกรมเริ่มทำงาน Loader จะตั้งค่า PC ไปยังตำแหน่งเริ่มต้นของโปรแกรมที่ในไฟล์ Executable
  • Heap และ Stack: ระบบจำเป็นต้องกำหนดขอบเขตของ heap และ stack ที่ใช้สำหรับการจัดการหน่วยความจำในระหว่างการทำงานของโปรแกรม โดย heap ใช้สำหรับเก็บข้อมูลที่มีขนาดไม่แน่นอนหรือไม่สามารถกำหนดขนาดล่วงหน้าได้
  • Interrupt Vector และ Exception HandlersLoader จะตั้งค่าตำแหน่งของการจัดการการหยุดทำงานหรือข้อผิดพลาดต่างๆ เช่น การตั้งค่าตัวจัดการ interrupts ที่เกิดจากอุปกรณ์ภายนอก เช่น การรับข้อมูลจากเซ็นเซอร์ หรือข้อผิดพลาดที่เกิดจากตัวโปรแกรมเอง

3. การจัดการหน่วยความจำ (Memory Management)

ในระบบฝังตัวหรือไมโครคอนโทรลเลอร์ มักจะมีทรัพยากรหน่วยความจำที่จำกัด ดังนั้น Loader จึงจำเป็นต้องจัดการหน่วยความจำอย่างมีประสิทธิภาพ:

  • Relocation: การปรับตำแหน่งของโค้ดหรือข้อมูลในหน่วยความจำ เพื่อให้โปรแกรมสามารถทำงานได้อย่างถูกต้องในหน่วยความจำที่มีข้อจำกัด
  • ตัวอย่าง: ถ้าหากหน่วยความจำหลัก (RAM) ไม่เพียงพอ โปรแกรมอาจถูกย้ายไปยังตำแหน่งใหม่ในหน่วยความจำอื่น เช่น Flash Memory
  • Memory MappingLoader จะทำการแมปที่อยู่ของโปรแกรมที่อยู่ในไฟล์ Executable ไปยังที่อยู่จริงในหน่วยความจำที่ใช้ โดยการจัดสรรที่อยู่ต่างๆ ให้เหมาะสม เช่น การจัดสรรพื้นที่สำหรับ codedata, และ heap

4. การตั้งค่า Data Segment และ BSS Segment

โปรแกรมที่โหลดเข้ามาจะมีสองส่วนหลักๆ ที่ต้องตั้งค่า ได้แก่ Data Segment และ BSS Segment ซึ่งเป็นที่ที่โปรแกรมเก็บข้อมูลต่างๆ ที่ต้องใช้ในการทำงาน:

  • Data Segment: ส่วนนี้จะใช้เก็บข้อมูลที่ได้รับค่าตั้งต้น เช่น ตัวแปรที่มีค่าเริ่มต้นที่กำหนดไว้ล่วงหน้า
  • ตัวอย่าง: ตัวแปรในโปรแกรมที่กำหนดค่าเริ่มต้น เช่น int a = 5;
  • BSS Segment: ส่วนนี้จะเก็บข้อมูลที่ยังไม่ได้รับค่าตั้งต้น เช่น ตัวแปรที่ไม่ได้กำหนดค่าเริ่มต้น
  • ตัวอย่าง: ตัวแปรที่ถูกกำหนดให้มีค่าเริ่มต้นเป็น 0 เมื่อโปรแกรมเริ่มทำงาน เช่น int b; ซึ่งจะถูกตั้งค่าเป็น 0 โดยอัตโนมัติเมื่อโปรแกรมเริ่ม

5. การโหลดโปรแกรมจาก Flash Memory

ในกรณีที่โปรแกรมต้องทำงานในระบบที่มีหน่วยความจำแบบ Flash (เช่น ใน ROM หรือ Flash StorageLoader จะทำการอ่านข้อมูลจาก Flash ลงไปใน RAM หรือที่หน่วยความจำหลัก:

  • Memory Mapping: โดยการใช้เทคนิค memory-mapped I/O เพื่อให้การเข้าถึงข้อมูลจาก Flash เป็นไปอย่างมีประสิทธิภาพและรวดเร็ว
  • Flash จะถูกจัดสรรให้กับตำแหน่งที่อยู่ใน RAM ที่สามารถใช้งานได้ในการดำเนินงานของโปรแกรม
  • Partial Loading: บางระบบอาจไม่สามารถโหลดโปรแกรมทั้งหมดได้ในคราวเดียว Loader จึงอาจโหลดโปรแกรมบางส่วนเท่านั้นตามลำดับความสำคัญ เพื่อประหยัดพื้นที่หน่วยความจำ

6. การกำหนดค่า External Devices

หากโปรแกรมที่กำลังจะโหลดต้องทำงานร่วมกับอุปกรณ์ภายนอก เช่น เซ็นเซอร์, หน้าจอ LCD, หรืออุปกรณ์การสื่อสาร Loader จะต้องตั้งค่าพอร์ตหรือการเชื่อมต่อให้พร้อมใช้งาน:

  • การตั้งค่าพอร์ต I2CSPI, หรือ UART ซึ่งเป็นโปรโตคอลที่ใช้สำหรับการสื่อสารระหว่างไมโครคอนโทรลเลอร์กับอุปกรณ์ต่างๆ
  • การเตรียมพร้อมอุปกรณ์ให้สามารถรับและส่งข้อมูลกับโปรแกรมได้ โดย Loader จะตั้งค่าตัวควบคุมการสื่อสาร เช่น การตั้งค่า baud rate สำหรับการส่งข้อมูลผ่าน UART

7. การตั้งค่าฟังก์ชันและตัวแปรเริ่มต้น

เมื่อ Loader ทำการเตรียมระบบและตรวจสอบความสมบูรณ์ของไฟล์ Executable แล้ว จะต้องตั้งค่าฟังก์ชันและตัวแปรต่างๆ ที่จำเป็นในโปรแกรม:

  • Data Segment: ตัวแปรที่มีค่าตั้งต้นถูกโหลดเข้าสู่หน่วยความจำ
  • BSS Segment: ตัวแปรที่ยังไม่ได้รับค่าเริ่มต้นจะถูกตั้งค่าเป็น 0

8. การส่งมอบการควบคุมให้กับโปรแกรม (Control Transfer)

เมื่อการตั้งค่าทั้งหมดเสร็จสมบูรณ์ Loader จะส่งมอบการควบคุมให้กับโปรแกรมที่โหลดเข้ามา โดยการตั้งค่า Program Counter (PC) ให้ชี้ไปยังจุดเริ่มต้นของโปรแกรม:

  • เมื่อ PC ถูกตั้งค่าไปยังจุดเริ่มต้น โปรแกรมจะเริ่มทำงานจากคำสั่งแรกที่ได้รับการโหลดเข้าไปในหน่วยความจำ

9. การจัดการข้อผิดพลาดในระหว่างการโหลด (Error Handling During Loading)
การจัดการข้อผิดพลาดเป็นส่วนสำคัญในการทำงานของ Loader เพื่อให้โปรแกรมทำงานได้อย่างราบรื่นและไม่เกิดปัญหาหลังจากการโหลดโปรแกรมแล้ว ข้อผิดพลาดที่อาจเกิดขึ้นได้ระหว่างการโหลด ได้แก่:

  • การตรวจสอบความสมบูรณ์ของไฟล์: ก่อนที่จะเริ่มโหลดโปรแกรม, Loader จะต้องตรวจสอบความสมบูรณ์ของไฟล์ Executable โดยใช้เทคนิคต่างๆ เช่น การตรวจสอบ Checksum, การตรวจสอบฟอร์แมตของไฟล์, หรือการยืนยันว่ารูปแบบไฟล์เหมาะสมกับระบบที่ใช้งาน หากพบข้อผิดพลาดในขั้นตอนนี้, Loader จะต้องหยุดการโหลดและส่งการแจ้งเตือนให้ผู้ใช้ทราบ
  • ข้อผิดพลาดในการจัดการหน่วยความจำ: หากพื้นที่หน่วยความจำไม่เพียงพอสำหรับการโหลดโปรแกรม, หรือมีการขอพื้นที่หน่วยความจำที่ไม่สามารถเข้าถึงได้, Loader จะต้องจัดการกับข้อผิดพลาดนี้ โดยอาจส่งข้อความแสดงข้อผิดพลาดหรือทำการย้อนกลับการกระทำที่เกิดขึ้น
  • ข้อผิดพลาดจากการกำหนดค่าระบบ: หากมีการตั้งค่าที่ไม่ถูกต้องในระบบ เช่น Stack Pointer (SP), Program Counter (PC), หรือการตั้งค่าอุปกรณ์ภายนอกไม่ถูกต้อง, Loader จะต้องตรวจสอบและแก้ไขค่าที่ผิดพลาดให้เหมาะสม ก่อนที่จะส่งมอบการควบคุมให้กับโปรแกรม
  • ข้อผิดพลาดจากการเข้าถึงอุปกรณ์ภายนอก: หากโปรแกรมต้องใช้อุปกรณ์ภายนอกเช่น เซ็นเซอร์หรือการสื่อสารผ่านพอร์ตต่างๆ (เช่น UART, I2C, SPI) และเกิดปัญหาการเชื่อมต่อหรือตั้งค่าอุปกรณ์ผิดพลาด, Loader จะต้องสามารถตรวจจับข้อผิดพลาดนี้ และแจ้งเตือนหรือทำการปรับแต่งพารามิเตอร์ให้เหมาะสม

10. การเริ่มต้นการทำงานของโปรแกรม (Program Startup) หลังจากที่การตั้งค่าทุกอย่างเสร็จสมบูรณ์และไม่มีข้อผิดพลาด, Loader จะทำการส่งมอบการควบคุมไปยังโปรแกรมโดยการเปลี่ยนค่า Program Counter (PC) ไปยัง Entry Point หรือจุดเริ่มต้นของโปรแกรมที่ระบุในไฟล์ Executable ซึ่งจะเป็นจุดที่โปรแกรมเริ่มทำงานจริงๆ

  • เริ่มต้นการทำงานของฟังก์ชันหลัก: เมื่อโปรแกรมเริ่มทำงานจาก Entry Point, ฟังก์ชันแรกที่ทำงานคือฟังก์ชันที่ถูกเรียกเป็นหลัก ซึ่งจะจัดการกับการเริ่มต้นโปรแกรมและการเรียกใช้ฟังก์ชันต่างๆ ภายในโปรแกรมตามลำดับ
  • การตรวจสอบสถานะหลังจากเริ่มทำงาน: เมื่อโปรแกรมเริ่มทำงาน, Loader จะต้องตรวจสอบให้แน่ใจว่าไม่มีข้อผิดพลาดใดๆ เกิดขึ้นในขณะทำงาน และทุกอย่างยังคงทำงานได้ตามที่คาดไว้

11. การจัดการ Heap และ Stack อย่างมีประสิทธิภาพ (Efficient Heap and Stack Management) ในระบบฝังตัวที่มีหน่วยความจำจำกัด, การจัดการพื้นที่ Heap และ Stack เป็นเรื่องที่สำคัญมาก เพราะการจัดการที่ไม่ดีอาจทำให้โปรแกรมล้มเหลวได้:

  • Heap Management: ในกรณีที่โปรแกรมใช้พื้นที่หน่วยความจำแบบไดนามิก (เช่น การใช้ฟังก์ชัน malloc() ในภาษา C), Loader ต้องจัดการการจัดสรรหน่วยความจำใน Heap อย่างมีประสิทธิภาพ เพื่อลดปัญหาเรื่อง memory leaks หรือ heap corruption
  • Stack Management: การตั้งค่าพื้นที่ Stack ให้เหมาะสมก็เป็นเรื่องที่สำคัญเช่นกัน โดยต้องตรวจสอบให้แน่ใจว่า Stack Pointer (SP) ชี้ไปยังพื้นที่ที่ถูกต้องและมีความปลอดภัยจากการเกิด Stack Overflow ที่อาจทำให้โปรแกรมเกิดข้อผิดพลาด

กล่าวโดยสรุป ขั้นตอนทั้งหมดนี้จะช่วยให้ Executable File ถูกโหลดและทำงานได้อย่างมีประสิทธิภาพในระบบฝังตัว โดยเฉพาะในสภาพแวดล้อมที่มีข้อจำกัดด้านทรัพยากร การปรับที่อยู่ในหน่วยความจำ, การตั้งค่าโปรแกรมเริ่มต้น, และการจัดการหน่วยความจำเป็นสิ่งที่สำคัญในการทำให้โปรแกรมทำงานได้อย่างถูกต้องในระบบฝังตัวหรือไมโครคอนโทรลเลอร์

เมื่อสิ่งที่สามารถสั่งให้ดำเนินการได้ ถูกโหลดลงหน่วยความจำของระบบสมองกลฝังตัวจนถูกต้องเรียบร้อยแล้วด้วย Loader ก็ถึงเวลาของการเริ่มต้นการทำงานของสมองกลตามคำสั่งการของโปรแกรม (bootstrapping) แล้ว ซึ่งต้องอาศัยขั้นตอนที่สลับซับซ้อนและต้องจัดการหลายส่วนเพื่อให้แน่ใจว่าอุปกรณ์ทั้งหมดทำงานร่วมกันได้อย่างถูกต้อง ขั้นตอนนี้มีความสำคัญอย่างยิ่ง กระทั่งสามารถพิพากษาความผิดถูกของการทำงานตามโปรแกรมที่มุ่งหวัง

ขั้นตอนแห่งการเตรียมการสู่การดำเนินการคำสั่งแรกของผู้ใช้ที่อยู่ภายใต้นาม main() ตามหลักการแห่งภาษาซี มักปรากฏอยู่ภายใต้นาม startup code และนี่คือเรื่องราวที่มันจะต้องดำเนินการให้จบสิ้น หากหมายให้ระบบเป็นไปตามบทบัญญัติที่มุ่งหมาย

  1. การตรวจสอบ Reset และ Entry Point :

การตรวจสอบ Reset และ Entry Point ในการเริ่มต้นระบบเป็นกระบวนการที่สำคัญเมื่อระบบเริ่มทำงานหลังจากเปิดเครื่องหรือรีสตาร์ท โดยกระบวนการนี้เกี่ยวข้องกับการตรวจสอบตำแหน่งเริ่มต้นของโปรแกรมในหน่วยความจำและการทำให้แน่ใจว่าโปรแกรมเริ่มทำงานได้อย่างถูกต้อง Startup Code จะเริ่มต้นจากการตรวจสอบ reset vector ซึ่งชี้ไปยังตำแหน่งเริ่มต้นที่อยู่ใน Reset Handler ในการเริ่มต้นระบบ นี่คือลำดับแรกที่เกิดขึ้นเมื่อระบบเปิดเครื่องหรือรีสตาร์ทจากสาเหตุใด ๆ โดย reset vector นี้มักจะถูกกำหนดโดย compiler หรือ linker script ซึ่งจะชี้ไปที่ตำแหน่งในหน่วยความจำที่มี startup code และโปรแกรมนี้จะเริ่มทำงานทันที

  • Reset Vector : คือที่อยู่ในหน่วยความจำที่ชี้ไปยัง ตำแหน่งเริ่มต้น ของการทำงานของโปรแกรมเมื่อเกิดการรีเซ็ต (Reset) หรือการเปิดเครื่องใหม่ (Power-on) ระบบจะเริ่มทำงานจากตำแหน่งนี้และดำเนินการตามขั้นตอนที่กำหนดไว้ใน Reset Handler ซึ่งเป็นฟังก์ชันที่ทำงานในช่วงเริ่มต้น

การทำงานของ Reset Vector

  • เมื่อระบบเริ่มต้น (เช่น การเปิดเครื่องใหม่หรือการรีบูต) ตัวประมวลผล (CPU) จะอ่านค่า Reset Vector เพื่อหาตำแหน่งเริ่มต้นในหน่วยความจำ
  • ตัวประมวลผลจะ กระโดด ไปที่ตำแหน่งนี้แล้วเริ่มทำงานจากที่นั้น
  • โดยปกติ Reset Vector จะถูกกำหนดใน compiler หรือ linker script ซึ่งจะระบุตำแหน่งที่ startup code หรือ Reset Handler อยู่

ตัวอย่าง: ในโปรแกรมที่เขียนด้วย C หรือ C++ สำหรับระบบฝังตัว (embedded system) เมื่อเกิดการรีเซ็ต Reset Vector จะชี้ไปที่ Reset Handler ใน startup code ที่ทำการตั้งค่าพื้นฐานของระบบ เช่น การตั้งค่า stack pointer, การตั้งค่า BSS และ Data Sectionเป็นตำแหน่งเริ่มต้นที่ใช้ในการรีเซ็ตระบบ เมื่อเกิดการรีบูตหรือการเริ่มต้นใหม่ของระบบ CPU. เมื่อการรีเซ็ตเกิดขึ้น (เช่น ปิดแล้วเปิดใหม่) ตัวประมวลผลจะเริ่มทำงานจากตำแหน่งนี้ในหน่วยความจำ (memory) และเรียกใช้งานฟังก์ชันที่กำหนดไว้

  • Entry point คือจุดเริ่มต้นที่โปรแกรมจะเริ่มทำงานหลังจากการรีเซ็ตเสร็จสิ้น เมื่อ Reset Handler ทำการตั้งค่าระบบเสร็จแล้ว ระบบจะ “กระโดด” ไปยังจุดที่โปรแกรมหลักเริ่มทำงาน ซึ่งจุดนี้คือ Entry Point ที่มักจะเป็นฟังก์ชัน main() ในโปรแกรม

การตั้งค่า Entry Point

  • หลังจากที่ Reset Handler ทำการตั้งค่าและเตรียมระบบแล้ว โปรแกรมจะเริ่มทำงานจาก Entry Point ซึ่งมักจะเป็นฟังก์ชัน main()
  • main() จะเป็นจุดที่โปรแกรมหลักเริ่มทำงาน และในนั้นจะมีการเรียกใช้ฟังก์ชันต่าง ๆ ที่โปรแกรมต้องการเพื่อให้ระบบทำงานได้ตามที่ต้องการ
  • Entry Point เป็นจุดที่โปรแกรมจะพร้อมใช้งานฟังก์ชันต่าง ๆ ที่ถูกกำหนดไว้ภายหลัง

3. การตั้งค่า System Clocks และ Peripheral Clocks การตั้งค่า System Clocks และ Peripheral Clocks เป็นขั้นตอนที่สำคัญในการทำให้ระบบไมโครคอนโทรลเลอร์ทำงานได้อย่างถูกต้องและมีประสิทธิภาพ การตั้งค่าสัญญาณนาฬิกานี้จะควบคุมความเร็วในการประมวลผลของ CPU และการทำงานของ peripheral devices (อุปกรณ์ภายนอก) เช่น UART, I2C, SPI, GPIO และอื่นๆ ที่เชื่อมต่อกับไมโครคอนโทรลเลอร์ การตั้งค่าเหล่านี้ต้องทำใน startup code ซึ่งจะช่วยให้ระบบพร้อมใช้งานตั้งแต่เริ่มทำงาน

System Clocks คือสัญญาณนาฬิกาหลักที่ควบคุมความเร็วในการทำงานของ CPU โดยการตั้งค่าสัญญาณนาฬิกานี้จะมีผลต่อประสิทธิภาพโดยรวมของระบบ ระบบบางระบบอาจต้องใช้ PLL (Phase-Locked Loop) เพื่อเพิ่มความแม่นยำในการควบคุมสัญญาณนาฬิกา หรือปรับอัตราความเร็วในการทำงานให้เหมาะสมกับการใช้งานที่ต้องการ

ในขณะเดียวกัน Peripheral Clocks จะควบคุมการทำงานของอุปกรณ์ที่เชื่อมต่อกับไมโครคอนโทรลเลอร์ เช่น พอร์ต I/O, UART, SPI, I2C หรือแม้กระทั่งการตั้งค่า GPIO เพื่อให้สามารถเชื่อมต่อและสื่อสารกับอุปกรณ์ต่างๆ ได้อย่างถูกต้อง การตั้งค่า peripheral clocks เหล่านี้ใน startup code จะเปิดใช้งานอุปกรณ์ที่จำเป็นสำหรับระบบ เช่น การตั้งค่า GPIO สำหรับการเชื่อมต่อกับเซ็นเซอร์หรือมอเตอร์ และการตั้งค่า interrupt เพื่อให้ระบบสามารถตอบสนองต่อเหตุการณ์ต่างๆ ได้ทันที

การตั้งค่า clock configuration ที่เหมาะสมจะช่วยให้ CPU และ peripherals ทำงานได้ในอัตราความเร็วที่ถูกต้องและเหมาะสมกับการทำงานในโปรแกรม การตั้งค่าเหล่านี้มักจะถูกจัดการใน startup code เพื่อให้ระบบสามารถเริ่มทำงานได้อย่างมีประสิทธิภาพ และสามารถใช้งานฟังก์ชันต่างๆ ที่ถูกกำหนดไว้ในโปรแกรมหลักได้อย่างครบถ้วน

4. การตั้งค่า Interrupts และ Exception Handlers เป็นขั้นตอนสำคัญในการพัฒนาระบบที่ต้องตอบสนองต่อเหตุการณ์ที่เกิดขึ้นในเวลาไม่แน่นอน เช่น การกดปุ่ม, การรับข้อมูลจากเซ็นเซอร์, หรือเหตุการณ์ที่เกิดจากฮาร์ดแวร์ การตั้งค่าเหล่านี้จะช่วยให้ระบบสามารถทำงานได้อย่างมีประสิทธิภาพและตอบสนองได้ทันท่วงทีเมื่อเกิดเหตุการณ์เหล่านี้ขึ้น

Interrupt Vector Table (IVT)

Interrupt Vector Table (IVT) คือ ตารางที่เก็บข้อมูลเกี่ยวกับตำแหน่ง (address) ของฟังก์ชันที่ต้องเรียกใช้งานเมื่อเกิด interrupt ขึ้น ตารางนี้จะระบุว่าหลังจากที่เกิด interrupt ระบบควรกระโดดไปที่ตำแหน่งใดเพื่อจัดการกับเหตุการณ์ที่เกิดขึ้น เช่น เมื่อมีการกดปุ่ม, การรับข้อมูลจากเซ็นเซอร์ หรือการเกิดเหตุการณ์จากฮาร์ดแวร์อื่นๆ ไฟล์ใน startup code จะช่วยตั้งค่า IVT ให้เรียบร้อย เพื่อให้ระบบสามารถตอบสนองต่อ interrupt เหล่านี้ได้ทันที

Exception Handlers

Exception Handlers คือฟังก์ชันที่ใช้จัดการกับข้อผิดพลาดหรือสถานการณ์ที่ผิดปกติ เช่น การคำนวณหารศูนย์ (divide by zero), การเข้าถึงหน่วยความจำที่ไม่ได้รับอนุญาต (memory access violations), หรือข้อผิดพลาดที่เกี่ยวข้องกับฮาร์ดแวร์ เช่น การพยายามเข้าถึงพอร์ตที่ไม่มีการเชื่อมต่อ การตั้งค่า exception handlers ใน startup code จะช่วยให้ระบบสามารถรับมือกับข้อผิดพลาดเหล่านี้ได้โดยไม่ทำให้โปรแกรมหยุดทำงานทั้งหมด เมื่อข้อผิดพลาดเกิดขึ้น ระบบสามารถกระโดดไปยังฟังก์ชันที่จัดการข้อผิดพลาดได้ทันที

Priority and Masking

ในบางกรณี, อาจมี interrupt หลายตัวที่เกิดขึ้นพร้อมกัน และบางตัวอาจมีความสำคัญมากกว่าตัวอื่น ๆ ดังนั้น การตั้งค่า priority และ masking จึงมีความสำคัญ:

  • Priority: คือการกำหนดลำดับความสำคัญของ interrupt ต่าง ๆ เมื่อเกิด interrupt หลายตัวพร้อมกัน ระบบจะต้องมีวิธีในการตัดสินใจว่าจะจัดการกับ interrupt ไหนก่อน โดยอาศัยลำดับความสำคัญที่ตั้งไว้ เช่น interrupt ที่มีความสำคัญสูงจะต้องได้รับการจัดการก่อน
  • Masking: คือการควบคุมการเปิดหรือปิดการรับ interrupt บางตัวในบางสถานการณ์ เช่น หากกำลังทำงานกับ interrupt ที่สำคัญ ระบบอาจเลือกที่จะ mask หรือปิดการรับ interrupt ที่มีลำดับความสำคัญต่ำกว่า เพื่อหลีกเลี่ยงการถูกรบกวน

การตั้งค่า priority และ masking ใน startup code ช่วยให้ระบบสามารถจัดการกับเหตุการณ์ได้อย่างเหมาะสม โดยให้ลำดับความสำคัญที่เหมาะสมกับแต่ละเหตุการณ์ และไม่ทำให้เกิดการขัดแย้งหรือรบกวนระหว่าง interrupt ต่าง ๆ

5. การจัดการกับ BSS และ Data Initialization

การจัดการกับ BSS (Block Started by Symbol) และ Data Initialization เป็นส่วนสำคัญในกระบวนการเริ่มต้นการทำงานของโปรแกรมในระบบฝังตัว (Embedded System) ซึ่งเกี่ยวข้องกับการจัดการกับตัวแปรที่ถูกกำหนดในโปรแกรมแต่ไม่ได้รับค่าเริ่มต้นหรือได้รับค่าตั้งแต่แรกเริ่มในโค้ด ดังนั้นการจัดการส่วนเหล่านี้จึงเป็นขั้นตอนที่สำคัญใน startup code เพื่อให้ตัวแปรเหล่านี้สามารถทำงานได้อย่างถูกต้องตลอดเวลา

  • BSS (Block Started by Symbol) เป็นส่วนในหน่วยความจำที่ใช้สำหรับเก็บ ตัวแปรที่ไม่ถูกกำหนดค่าเริ่มต้น ในโปรแกรม ตัวแปรในส่วนนี้อาจจะเป็นตัวแปร global หรือ static ที่ไม่ได้รับค่าเริ่มต้นในโค้ด ซึ่งแตกต่างจากตัวแปรที่มีการกำหนดค่าเริ่มต้นไว้ใน data section ตัวแปรใน BSS จะถูก เซตเป็นค่า 0 หรือ null โดยอัตโนมัติในระหว่างการเริ่มต้นของโปรแกรม ตัวอย่างเช่น
int global_var; // ตัวแปรนี้จะถูกเก็บใน BSS และถูกเซตเป็น 0

ใน startup code จะมีขั้นตอนการ กำหนดค่าเริ่มต้น ให้กับตัวแปรใน BSS (โดยการเซตค่าเป็น 0) ซึ่งจะช่วยให้ตัวแปรเหล่านี้สามารถใช้งานได้โดยไม่มีการค้างคา (unclean state) ตัวอย่างของการดำเนินการนี้ใน startup code คือการทำให้แน่ใจว่า memory region ที่ใช้เก็บตัวแปรใน BSS ได้รับการตั้งค่าเป็น 0 ก่อนที่โปรแกรมหลักจะเริ่มทำงาน

  • Data Section เป็นส่วนในหน่วยความจำที่ใช้สำหรับเก็บ ตัวแปรที่ได้รับการกำหนดค่าเริ่มต้น ในโค้ด เช่น ตัวแปรที่ถูกประกาศและกำหนดค่าเริ่มต้นให้กับมันตั้งแต่ตอนที่เขียนโปรแกรม ตัวแปรเหล่านี้จะถูก เก็บใน ROM หรือ Flash Memory และเมื่อโปรแกรมเริ่มทำงาน ข้อมูลจากส่วนนี้จะถูก คัดลอก (Copy) ไปยัง RAM เพื่อให้สามารถใช้งานได้ ตัวอย่างเช่น :
int initialized_var = 10; // ตัวแปรนี้จะถูกเก็บใน Data Section

ในกรณีนี้ initialized_var จะได้รับค่าเริ่มต้นเป็น 10 ดังนั้นในระหว่างการเริ่มต้นโปรแกรม ข้อมูลนี้จะถูกคัดลอกจากพื้นที่เก็บข้อมูลใน ROM หรือ Flash memory ไปยังพื้นที่ RAM เพื่อให้สามารถใช้งานได้

ใน startup code จะมีขั้นตอนการ คัดลอกข้อมูลจาก ROM ไปยัง RAM สำหรับตัวแปรใน Data Section ข้อมูลเหล่านี้จะถูกคัดลอกไปยังที่อยู่ใน RAM ตามที่กำหนดไว้ในโปรแกรม เพื่อให้โปรแกรมสามารถเข้าถึงข้อมูลเหล่านี้ได้ในระหว่างการทำงาน

6. การตรวจสอบสถานะของโปรแกรม (Self-test or Diagnostics) : หลังจากที่การตั้งค่าฮาร์ดแวร์และการเตรียมระบบเสร็จสมบูรณ์แล้ว ขั้นตอนต่อไปคือการทำ self-test หรือ diagnostics เพื่อยืนยันว่าระบบสามารถทำงานได้อย่างถูกต้องและไม่มีปัญหาที่จะเกิดขึ้นในระหว่างการทำงานจริง ซึ่งการทดสอบนี้มีความสำคัญเพื่อให้มั่นใจว่าอุปกรณ์ทั้งหมดทำงานได้ตามที่คาดหวังและไม่มีข้อผิดพลาดที่อาจเกิดขึ้น

การทดสอบเซ็นเซอร์และอุปกรณ์ที่เชื่อมต่อ

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

  • ตรวจสอบว่าเซ็นเซอร์สามารถให้ข้อมูลที่ถูกต้องตามที่คาดไว้
  • ทดสอบว่าเซ็นเซอร์สามารถรับและส่งข้อมูลอย่างมีประสิทธิภาพ
  • ตรวจสอบการตอบสนองของเซ็นเซอร์ในสถานการณ์ต่าง ๆ ที่มีการเปลี่ยนแปลงสภาพแวดล้อม เช่น การเคลื่อนที่ หรือการเปลี่ยนแปลงของปัจจัยภายนอก

การตรวจสอบการทำงานของสัญญาณสื่อสาร

สำหรับระบบที่มีการสื่อสารระหว่างไมโครคอนโทรลเลอร์กับอุปกรณ์อื่น ๆ ผ่านโปรโตคอลต่าง ๆ เช่น UARTI2CSPI หรือ CAN การทดสอบการทำงานของสัญญาณสื่อสารเป็นสิ่งที่สำคัญในการยืนยันว่าไม่มีปัญหาที่อาจเกิดขึ้นระหว่างการส่งข้อมูล เช่น:

  • ตรวจสอบการส่งและรับข้อมูลผ่าน UART หรือ I2C ว่าสัญญาณการส่งข้อมูลมีความถูกต้องและไม่มีการเสียหาย
  • ตรวจสอบว่าโปรโตคอลที่ใช้อยู่สามารถเชื่อมต่อและส่งข้อมูลระหว่างอุปกรณ์ต่าง ๆ ได้อย่างไม่มีข้อผิดพลาด

การทดสอบการทำงานของอุปกรณ์ภายนอก

การทดสอบอุปกรณ์ภายนอกเช่น หน้าจอมอเตอร์ไฟ LED หรือ เซ็นเซอร์อื่น ๆ เป็นการยืนยันว่าอุปกรณ์เหล่านั้นได้รับการตั้งค่าและพร้อมทำงานร่วมกับระบบ เช่น:

  • ตรวจสอบว่าหน้าจอแสดงผลหรือ LCD สามารถแสดงข้อมูลได้ตามที่คาดหวัง
  • ทดสอบการทำงานของ มอเตอร์ ว่าสามารถหมุนหรือหยุดตามคำสั่งได้
  • ตรวจสอบการทำงานของเซ็นเซอร์ที่เชื่อมต่อกับระบบ เช่น เซ็นเซอร์วัดอุณหภูมิ, เซ็นเซอร์ระยะทาง หรือเซ็นเซอร์การเคลื่อนไหว

การตรวจสอบสถานะของหน่วยความจำ

การตรวจสอบหน่วยความจำ (RAM/ROM/Flash) เป็นการยืนยันว่าระบบสามารถเข้าถึงหน่วยความจำได้อย่างถูกต้องและไม่มีข้อผิดพลาด เช่น:

  • ตรวจสอบว่าโปรแกรมสามารถโหลดข้อมูลจาก Flash memory หรือ EEPROM ได้อย่างถูกต้อง
  • ทดสอบว่า RAM สามารถจัดเก็บข้อมูลและอ่านข้อมูลได้ตามที่คาดหวัง
  • ตรวจสอบว่าไม่มีข้อผิดพลาดในการจัดสรรหน่วยความจำ (memory allocation) หรือการเข้าถึงหน่วยความจำที่ไม่ได้รับอนุญาต

หลังจากการตั้งค่าฮาร์ดแวร์และการเตรียมระบบ, ควรทำการทดสอบหรือ self-test บางประการเพื่อยืนยันว่าระบบสามารถทำงานได้อย่างถูกต้อง:

  • การทดสอบว่าเซ็นเซอร์หรืออุปกรณ์ที่เชื่อมต่ออยู่สามารถทำงานได้ตามปกติ
  • ตรวจสอบการทำงานของสัญญาณสื่อสารต่าง ๆ เช่น UART หรือ I2C
  • ทดสอบว่าอุปกรณ์ภายนอกต่าง ๆ เช่น หน้าจอ, มอเตอร์ หรือเซ็นเซอร์ต่าง ๆ ได้รับการตั้งค่าถูกต้องและสามารถทำงานร่วมกับโปรแกรมได้
  • ตรวจสอบสถานะของหน่วยความจำเพื่อให้มั่นใจว่าไม่มีข้อผิดพลาดในการเข้าถึงหน่วยความจำ

7. การเริ่มต้นโปรแกรมหลัก (Starting the Main Program) หลังจากการตั้งค่าฮาร์ดแวร์และทำการตรวจสอบสถานะของระบบเรียบร้อยแล้ว ขั้นตอนถัดไปคือการเริ่มต้นโปรแกรมหลัก ซึ่งโดยปกติจะเริ่มต้นจากฟังก์ชัน main() การเริ่มต้นนี้เป็นจุดที่สำคัญที่สุดในการเริ่มทำงานของระบบเพราะในฟังก์ชันนี้ ระบบจะเริ่มทำงานตามลำดับที่กำหนดไว้

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

กระบวนการเริ่มต้นโปรแกรมหลัก:

⦿ การตั้งค่าค่าพารามิเตอร์เริ่มต้น: เมื่อเข้าสู่ฟังก์ชัน main(), ระบบจะตั้งค่าพารามิเตอร์เริ่มต้นที่จำเป็นสำหรับการทำงานของระบบ เช่น การกำหนดค่าของตัวแปรต่าง ๆ ที่ใช้ในการคำนวณหรือการควบคุมระบบ เช่น การตั้งค่าพอร์ต I/O, การกำหนดค่าภายในโปรแกรม เช่น การเชื่อมต่อกับอุปกรณ์ต่าง ๆ หรือการตั้งค่าโปรโตคอลการสื่อสาร (UART, SPI, I2C)

⦿ การเริ่มทำงานของอุปกรณ์ภายนอก: เมื่อโปรแกรมเข้าสู่ main(), ระบบจะเริ่มทำงานกับอุปกรณ์ภายนอกที่เชื่อมต่อ เช่น การส่งคำสั่งควบคุมมอเตอร์, การรับข้อมูลจากเซ็นเซอร์ หรือการแสดงผลบนหน้าจอ การตั้งค่าการทำงานของ peripherals เช่น การกำหนดให้เซ็นเซอร์สามารถรับข้อมูลได้ หรือการสั่งให้มอเตอร์หมุน

⦿ การจัดการลูปหลัก (Main Loop): ในโปรแกรมที่ทำงานแบบ embedded system, ฟังก์ชัน main() จะมักมีลูปหลัก (main loop) ซึ่งจะทำงานอย่างต่อเนื่องเพื่อให้ระบบทำงานตามที่กำหนด โดยการตรวจสอบข้อมูลจากเซ็นเซอร์หรืออุปกรณ์ต่าง ๆ ลูปนี้จะคอยตรวจสอบสถานะต่าง ๆ เช่น การตรวจสอบข้อมูลจากเซ็นเซอร์, การเช็คการตอบสนองจากการสั่งงานมอเตอร์, หรือการจัดการกับข้อมูลที่รับเข้ามา

⦿ การควบคุมการทำงานร่วมกับ Interrupts และ Events: เมื่อมีเหตุการณ์ที่ต้องการการตอบสนองทันที เช่น การกดปุ่มหรือการเปลี่ยนแปลงของเซ็นเซอร์ ฟังก์ชัน main() จะทำการจัดการกับเหตุการณ์เหล่านั้นโดยการใช้ interrupts หรือ events เพื่อให้ระบบตอบสนองได้อย่างรวดเร็ว ฟังก์ชัน main() จะทำงานร่วมกับ interrupt handlers และ events เพื่อให้ระบบสามารถจัดการกับเหตุการณ์เหล่านั้นได้ตามเวลาที่เหมาะสม

⦿ การจัดการข้อผิดพลาด (Error Handling): หากระบบพบข้อผิดพลาดในระหว่างการทำงาน เช่น การอ่านข้อมูลจากเซ็นเซอร์ไม่สำเร็จหรืออุปกรณ์ภายนอกไม่ได้เชื่อมต่อ ระบบจะทำการจัดการกับข้อผิดพลาดเหล่านั้นใน main() โดยการแสดงข้อความเตือนหรือการดำเนินการอื่น ๆ เช่น การรีบูตระบบหรือการเปิดใช้งานฟังก์ชันสำรอง

⦿ การปิดระบบหรือการเตรียมระบบสำหรับการหยุดทำงาน: เมื่อระบบทำงานเสร็จสิ้นหรือมีคำสั่งให้หยุดการทำงาน ฟังก์ชัน main() จะทำการปิดอุปกรณ์ต่าง ๆ อย่างถูกต้อง เช่น การปิดการทำงานของเซ็นเซอร์หรือการปิดพอร์ตการสื่อสาร ระบบอาจจะทำการเตรียมข้อมูลหรือสถานะให้พร้อมสำหรับการเริ่มต้นใหม่ในรอบถัดไปหรือเมื่อมีการรีเซ็ตใหม่

8. การเตรียมการทำงานจริงของโปรแกรม (Program Ready for Main Execution) เมื่อ startup code เสร็จสิ้นกระบวนการตั้งค่าทั้งหมดที่จำเป็น เช่น การกำหนดค่า system clocks, การตั้งค่า peripherals, การตั้งค่า interrupts และ exception handlers รวมไปถึงการทดสอบอุปกรณ์ต่าง ๆ ระบบก็จะพร้อมสำหรับการทำงานในโปรแกรมหลักที่ถูกเขียนขึ้นเพื่อควบคุมการทำงานของระบบ การทำงานนี้เป็นจุดเริ่มต้นที่ระบบจะเริ่มทำการรับข้อมูล, ประมวลผลข้อมูล และตอบสนองต่อคำสั่งที่ได้รับจากผู้ใช้หรืออุปกรณ์ต่าง ๆ

กระบวนการเตรียมการทำงานจริง :

⦿ เริ่มต้นการทำงานของโปรแกรมหลัก: เมื่อโปรแกรมเริ่มต้นในฟังก์ชัน main(), ระบบจะเริ่มทำงานตามที่ได้ตั้งค่าไว้ใน startup code การทำงานในโปรแกรมหลักจะเริ่มจากการรับข้อมูลจากเซ็นเซอร์หรืออุปกรณ์ภายนอกอื่น ๆ ที่เชื่อมต่อกับระบบ ซึ่งข้อมูลเหล่านี้อาจเป็นข้อมูลจากเซ็นเซอร์อุณหภูมิ, เซ็นเซอร์ความเร็ว, หรือข้อมูลที่ได้รับจากผู้ใช้ผ่านทางอินเตอร์เฟซต่าง ๆ ตัวอย่างเช่น หากโปรแกรมเป็นระบบควบคุมมอเตอร์ ระบบจะรับคำสั่งจากผู้ใช้หรือเซ็นเซอร์เพื่อตัดสินใจว่าเมื่อใดมอเตอร์จะเริ่มหมุนหรือหยุด

⦿ การประมวลผลข้อมูล: เมื่อข้อมูลจากเซ็นเซอร์หรืออุปกรณ์ภายนอกถูกส่งเข้าสู่โปรแกรม, ข้อมูลจะต้องถูกประมวลผลเพื่อให้ได้ผลลัพธ์ที่ต้องการ เช่น การคำนวณค่าที่ได้จากเซ็นเซอร์ หรือการตรวจสอบคำสั่งจากผู้ใช้ ในกรณีของระบบที่ควบคุมอุปกรณ์ เช่น มอเตอร์หรือปั๊มน้ำ, โปรแกรมจะต้องประมวลผลข้อมูลจากเซ็นเซอร์เพื่อคำนวณว่าควรจะควบคุมอุปกรณ์เหล่านั้นอย่างไร ตัวอย่างเช่น ถ้าค่าจากเซ็นเซอร์อุณหภูมิสูงเกินไป, โปรแกรมอาจส่งคำสั่งให้มอเตอร์หยุดทำงาน

⦿ การควบคุมอุปกรณ์ภายนอก: หลังจากประมวลผลข้อมูลแล้ว, ระบบจะต้องทำการควบคุมอุปกรณ์ต่าง ๆ เช่น การส่งคำสั่งไปยังมอเตอร์เพื่อให้ทำงานตามที่ต้องการ หรือการแสดงผลข้อมูลบนหน้าจอแสดงผล การควบคุมอุปกรณ์ในโปรแกรมหลักจะทำให้ระบบสามารถตอบสนองต่อข้อมูลหรือคำสั่งที่ได้รับจากผู้ใช้ได้อย่างมีประสิทธิภาพ ตัวอย่างเช่น การแสดงผลข้อมูลบนจอ LCD หรือ OLED, หรือการใช้สัญญาณ PWM ในการควบคุมความเร็วของมอเตอร์

⦿ การทำงานในลูป (Main Loop): หลังจากที่เริ่มทำงานแล้ว, โปรแกรมจะดำเนินการในลูปหลัก (main loop) ซึ่งจะทำงานซ้ำไปเรื่อย ๆ จนกว่ามีการหยุดทำงานหรือมีเหตุการณ์พิเศษที่ต้องการการตอบสนอง ภายในลูปนี้, ระบบจะคอยตรวจสอบเหตุการณ์ต่าง ๆ เช่น การรับข้อมูลจากเซ็นเซอร์, การตรวจสอบคำสั่งจากผู้ใช้ หรือการสั่งการให้ทำงานกับอุปกรณ์ภายนอก เช่น หากเป็นระบบควบคุมอุณหภูมิ, โปรแกรมอาจจะคอยตรวจสอบค่าของเซ็นเซอร์อุณหภูมิทุกๆ 1 วินาที เพื่อให้แน่ใจว่าอุณหภูมิของระบบอยู่ในค่าที่ปลอดภัย

⦿ การตอบสนองต่อเหตุการณ์ใหม่ ๆ (Event-driven Programming): ในบางระบบ, โปรแกรมหลักจะถูกออกแบบให้ตอบสนองต่อเหตุการณ์ (events) ที่เกิดขึ้น เช่น การกดปุ่ม, การเปลี่ยนแปลงของเซ็นเซอร์ หรือการสั่งงานจากผู้ใช้ โปรแกรมจะคอยตรวจสอบและตอบสนองต่อเหตุการณ์เหล่านี้ได้ทันที โดยไม่ต้องรอให้ลูปหลักดำเนินการจนเสร็จสมบูรณ์ก่อน

⦿ การตรวจสอบสถานะของระบบ (System Status Monitoring): โปรแกรมหลักจะต้องคอยตรวจสอบสถานะของระบบอย่างต่อเนื่อง เช่น การตรวจสอบว่าอุปกรณ์ต่าง ๆ ทำงานได้ตามปกติหรือไม่ หรือระบบมีข้อผิดพลาดที่ต้องแก้ไขหรือไม่ หากพบปัญหา เช่น เซ็นเซอร์ไม่ได้รับข้อมูล, หน่วยความจำเกือบเต็ม หรืออุปกรณ์ภายนอกเกิดข้อผิดพลาด, โปรแกรมจะต้องจัดการกับข้อผิดพลาดเหล่านั้น เช่น การแจ้งเตือนผู้ใช้ หรือการหยุดทำงานของอุปกรณ์

⦿ การตอบสนองต่อคำสั่งจากผู้ใช้: โปรแกรมหลักมักจะต้องตอบสนองต่อคำสั่งจากผู้ใช้ เช่น การควบคุมอุปกรณ์ผ่านปุ่มหรืออินเตอร์เฟซที่ผู้ใช้มี การตอบสนองต่อคำสั่งเหล่านี้สามารถทำได้โดยการตรวจสอบสถานะของอินพุตต่าง ๆ เช่น การกดปุ่ม หรือการเลือกคำสั่งจากเมนูบนหน้าจอ

กล่าวโดยสรุป Startup code เป็นโค้ดที่ถูกเรียกใช้ในระยะแรกสุดหลังจากการรีเซ็ตหรือการเปิดเครื่อง ซึ่งมีหน้าที่สำคัญในการเตรียมการตั้งค่าระบบต่าง ๆ ให้พร้อมก่อนที่โปรแกรมหลักจะเริ่มทำงาน โดยการตั้งค่าเหล่านี้จะครอบคลุมถึงการตั้งค่า stack pointerclock setupperipheral devicesinterruptsmemory allocation, และการเตรียมข้อมูลที่จำเป็นต่าง ๆ ในหน่วยความจำ ก่อนที่จะ call main ซึ่งจะเริ่มต้นการทำงานของโปรแกรมหลักให้สามารถควบคุมระบบต่าง ๆ ได้ตามที่ต้องการ

ตัวอย่างโค้ดของ startup ขั้นต้น เขียนด้วยภาษา Assembly สำหรับ ไมโครคอนโทรลเลอร์ STM32F4

ประกอบด้วยการตั้งค่า Stack Pointer, การคัดลอกข้อมูลจาก .data section ไปยัง RAM, การตั้งค่า .bss section, และการเรียกใช้ฟังก์ชัน main() หลังจากการรีเซ็ต:

    .section .isr_vector, "a", %progbits
.weak __stack_start
.equ __stack_start, __StackLimit

/* Vector Table: */
.long __stack_start /* Initial stack pointer */
.long Reset_Handler /* Reset handler */
.long NMI_Handler /* NMI handler */
.long HardFault_Handler /* Hard fault handler */
/* Other interrupt vectors can be added here... */

/* Reset Handler */
Reset_Handler:
/* Initialize stack pointer, it is already done by the linker script */

/* Copy .data section from ROM to RAM */
ldr r0, =_sidata /* Load start address of .data in ROM */
ldr r1, =_sdata /* Load start address of .data in RAM */
ldr r2, =_edata /* Load end address of .data in RAM */

copy_data:
cmp r1, r2 /* Check if destination equals the end of .data */
ittt eq
ldreq r3, [r0], #4 /* Load word from ROM and increment the source address */
streq r3, [r1], #4 /* Store word to RAM and increment the destination address */
bne copy_data /* Repeat until all data is copied */

/* Zero out .bss section */
ldr r0, =_sbss /* Load start address of .bss */
ldr r1, =_ebss /* Load end address of .bss */

zero_bss:
cmp r0, r1 /* Compare the start and end of .bss */
ittt eq
moveq r2, #0 /* Set register to zero */
streq r2, [r0], #4 /* Store zero to RAM and increment address */
bne zero_bss /* Repeat until all of .bss is zeroed */

/* Call main function */
bl main /* Branch with link to main function */

/* Infinite loop in case of error or return from main */
b . /* Infinite loop */

/* Default interrupt handlers (for unimplemented interrupts) */
NMI_Handler:
b .

HardFault_Handler:
b .

1. Vector Table (ตารางเวกเตอร์)

Vector Table คือตำแหน่งที่บอกให้ไมโครคอนโทรลเลอร์ทราบว่าจะไปที่ไหนเมื่อเกิดเหตุการณ์ต่าง ๆ เช่น รีเซ็ต, interrupt, หรือ exception เช่น HardFault หรือ NMI (Non-Maskable Interrupt). ค่าต่าง ๆ ใน vector table จะถูกกำหนดโดย Linker Script และจะถูกจัดเก็บในหน่วยความจำที่ระบุ

    .section .isr_vector, "a", %progbits
.weak __stack_start
.equ __stack_start, __StackLimit
  • .section .isr_vector: สั่งให้สร้างส่วนของหน่วยความจำที่ใช้เก็บ vector table สำหรับ interrupt.
  • .weak __stack_start: กำหนดให้ __stack_start เป็นตำแหน่งเริ่มต้นของ stack pointer. ค่านี้จะถูกกำหนดใน linker script.
  • .equ __stack_start, __StackLimit: กำหนดให้ __stack_start เท่ากับค่า __StackLimit ที่จะเป็นค่าตำแหน่งที่ stack pointer เริ่มต้น.

จากนั้น เราจะกำหนดค่าตำแหน่งใน vector table:

    .long __stack_start               /* Initial stack pointer */
.long Reset_Handler /* Reset handler */
.long NMI_Handler /* NMI handler */
.long HardFault_Handler /* Hard fault handler */
/* Other interrupt vectors can be added here... */
  • ค่าที่ตำแหน่ง 0x00000000 จะเป็น initial stack pointer ซึ่งจะถูกใช้เพื่อเริ่มต้น stack ของโปรแกรม
  • ค่าตำแหน่ง 0x00000004 จะเป็น Reset Handler ที่จะถูกเรียกเมื่อเกิดการรีเซ็ต
  • ค่าตำแหน่ง 0x00000008 และ 0x0000000C จะเป็นการจัดการกับ interrupt อื่น ๆ เช่น NMI และ HardFault

2. Reset Handler (ตัวจัดการการรีเซ็ต)

เมื่อ STM32F4 เริ่มทำงานหลังจากการรีเซ็ต (power-up หรือ software reset) มันจะกระโดดไปยัง Reset_Handler ใน vector table. โค้ดนี้ทำการตั้งค่าตัวแปรต่าง ๆ และเตรียมความพร้อมให้กับโปรแกรม.

Reset_Handler:
/* Initialize stack pointer, it is already done by the linker script */
  • ส่วนนี้ stack pointer จะถูกตั้งค่าจากตำแหน่งใน vector table โดยที่ไมโครคอนโทรลเลอร์ใช้ค่าของ stack pointer นี้เพื่อระบุว่าข้อมูลใดจะถูกเก็บใน stack.

2.1 การคัดลอก .data จาก ROM ไปยัง RAM

ในขั้นตอนนี้ข้อมูลที่อยู่ใน .data section (ซึ่งถูกตั้งค่าในโปรแกรม) จะถูกคัดลอกจาก ROM (พื้นที่โปรแกรม) ไปยัง RAM (พื้นที่ที่ใช้งานได้). ซึ่งจะช่วยให้โปรแกรมทำงานได้อย่างถูกต้อง เนื่องจากข้อมูลใน .data section ถูกกำหนดไว้ล่วงหน้า แต่ถูกจัดเก็บใน ROM เพื่อประหยัดพื้นที่.

    ldr r0, =_sidata           /* Load start address of .data in ROM */
ldr r1, =_sdata /* Load start address of .data in RAM */
ldr r2, =_edata /* Load end address of .data in RAM */
  • ldr r0, =_sidata: โหลดที่อยู่เริ่มต้นของ .data จาก ROM
  • ldr r1, =_sdata: โหลดที่อยู่เริ่มต้นของ .data ที่ RAM
  • ldr r2, =_edata: โหลดที่อยู่สุดท้ายของ .data ใน RAM
copy_data:
cmp r1, r2 /* Compare if destination equals the end of .data */
ittt eq
ldreq r3, [r0], #4 /* Load word from ROM and increment the source address */
streq r3, [r1], #4 /* Store word to RAM and increment the destination address */
bne copy_data /* Repeat until all data is copied */
  • ในส่วนนี้ เราจะทำการคัดลอกข้อมูลทีละคำ (4 ไบต์) จาก ROM ไปยัง RAM โดยใช้คำสั่ง ldreq (load) และ streq (store)

2.2 การตั้งค่า .bss เป็นศูนย์

หลังจากคัดลอก .data เสร็จแล้ว ฟังก์ชัน Reset_Handler จะทำการตั้งค่าตัวแปรที่ไม่ได้กำหนดค่าเริ่มต้นทั้งหมดใน .bss section ให้เป็นศูนย์.

    ldr r0, =_sbss             /* Load start address of .bss */
ldr r1, =_ebss /* Load end address of .bss */

zero_bss:
cmp r0, r1 /* Compare the start and end of .bss */
ittt eq
moveq r2, #0 /* Set register to zero */
streq r2, [r0], #4 /* Store zero to RAM and increment address */
bne zero_bss /* Repeat until all of .bss is zeroed */
  • ฟังก์ชันนี้จะทำการเซ็ตค่าตัวแปรที่อยู่ใน .bss section ให้เป็นศูนย์ โดยใช้คำสั่ง moveq และ streq.

3. เรียกใช้ฟังก์ชัน main

หลังจากการตั้งค่าเสร็จสิ้น เราจะเรียกใช้ฟังก์ชัน main ซึ่งเป็นจุดเริ่มต้นของโปรแกรมของเรา.

    /* Call main function */
bl main /* Branch with link to main function */
  • คำสั่ง bl main จะกระโดดไปยังฟังก์ชัน main โดยเก็บค่า address ที่จะกลับมาหลังจาก main เสร็จสิ้น

4. การจัดการ Interrupt (Interrupt Handlers)

ในกรณีที่เกิด interrupt ที่ไมโครคอนโทรลเลอร์ไม่สามารถจัดการได้ จะเข้าสู่การจัดการเริ่มต้น (default handlers) เช่น NMI_Handler หรือ HardFault_Handler.

    /* Default interrupt handlers (for unimplemented interrupts) */
NMI_Handler:
b .

HardFault_Handler:
b .
  • ฟังก์ชันนี้จะทำให้ไมโครคอนโทรลเลอร์เข้าสู่ลูปไม่สิ้นสุดในกรณีที่เกิด NMI หรือ HardFault ซึ่งยังไม่ได้กำหนดวิธีการจัดการในระบบ

Linker Script File ที่ช่วยในการกำหนดการจัดสรรหน่วยความจำสำหรับ STM32F4 จะช่วยกำหนดตำแหน่งต่าง ๆ ของข้อมูลและตัวแปรที่เกี่ยวข้องกับหน่วยความจำ

    .section .data
_sidata = 0x08000000 /* ROM start address for .data */
_sdata = 0x20000000 /* RAM start address for .data */
_edata = 0x20010000 /* RAM end address for .data */

.section .bss
_sbss = 0x20020000 /* RAM start address for .bss */
_ebss = 0x20030000 /* RAM end address for .bss */

“ Voluntas agendi celeriter et subito effectum adipisci, non solum methodos aptas, sed etiam experientiam diuturnam postulat, ut omnes obices et hostes superare possit ”

“ The intent to act swiftly and achieve results instantly requires not only the proper techniques but also deep, long-standing experience, so that all obstacles and enemies can be overcome ”

“ หมายจะลงมือให้รวบรัด ได้ผลในฉับพลัน ไม่เพียงกระบวนท่าที่เหมาะสม ยังต้องพึ่งพิงประสบการณ์ที่โชกโชน จึงจะโค้นล่มอุปสรรคทั้งปวงได้ ”

Naturvirtus

XX Decembris MMXXIV