บทนำ: กำแพงภาษาแห่งสถาปัตยกรรมซิลิคอน

สวัสดีครับน้องๆ วิศวกรและเพื่อนนักพัฒนาชาว www.123microcontroller.com ทุกคน! กลับมาลงสนามกับวิศวกรขอบตาดำๆ กันอีกครั้ง วันนี้เราจะมาคุยขยี้กันถึงเรื่องที่เป็นเสมือน “กำแพงภาษา” กั้นขวางระหว่างฮาร์ดแวร์ต่างสายพันธุ์ ปัญหาโลกแตกแห่งวงการสถาปัตยกรรมคอมพิวเตอร์ นั่นก็คือเรื่องของ Endianness (เอนเดียนเนส) ครับ

รู้หรือไม่ครับว่าชื่อแปลกประหลาดอย่าง Big-endian และ Little-endian นั้น มีรากฐานที่มาจากนิยายเสียดสีสังคมการเมืองเรื่อง Gulliver’s Travels ในปี ค.ศ. 1726 ซึ่งเป็นท่อนที่ประชากรคนสองกลุ่มตั้งป้อมทำสงครามกลางเมืองเข่นฆ่ากันเพียงเพราะเถียงกันเรื่องไร้สาระว่า “เวลาจะเริ่มปอกไข่ต้ม ควรจะเคาะทุบฝั่งป้าน (Big end) หรือฝั่งแหลม (Little end) ก่อนดี”!

ในโลกของการเขียนโปรแกรมระดับล่าง (Low-level programming) การที่ผู้สร้างโปรเซสเซอร์แต่ละค่ายเลือกฝั่งว่าจะเก็บข้อมูลไบต์ไหนลงหน่วยความจำก่อน ก็ถือเป็นการตัดสินใจเชิงสถาปัตยกรรมระดับปรัชญาที่ไม่มีฝั่งไหนถูกหรือผิด 100% ครับ แต่มันจะกลายเป็น “ฝันร้ายสยองขวัญ” ขั้นสุดทันที เมื่อฮาร์ดแวร์สองตัวที่นับถือ “ความเชื่อ” คนละขั้วต้องจับมือมาคุยแลกเปลี่ยนข้อมูลกัน! วันนี้เราจะมาเจาะลึกทะลวงกันว่า แหล่งข้อมูลระดับเซียนอธิบายเรื่องอื้อฉาวนี้ในบริบทของการโต้ตอบกับฮาร์ดแวร์ (Hardware Interaction) ไว้อย่างไรบ้างครับ

เจาะกึ๋นทฤษฎี: Endianness สองขั้วอำนาจในโลกของฮาร์ดแวร์

ในบริบทที่กว้างขวางขึ้นของการทำงานคลุกฝุ่นร่วมกับฮาร์ดแวร์ Endianness คือกฎเกณฑ์การกำหนด “ลำดับการเรียงตัวของกลุ่มไบต์” เวลาที่เราต้องการจัดเก็บหรือส่งข้อมูลไซส์บิ๊กเบิ้มที่มีขนาดหน้ากว้างใหญ่กว่า 1 ไบต์ (ยกตัวอย่างเช่น ตัวแปร int หรือ uint32_t ขนาด 32 บิต หรือ 4 ไบต์) อัดลงไปในตารางหน่วยความจำหรือสาดส่งผ่านสายเคเบิลเครือข่าย แหล่งข้อมูลวิศวกรรมได้อธิบายสถาปัตยกรรมการเรียงข้อมูล 2 รูปแบบหลักๆ ที่ฟาดฟันกันมาอย่างยาวนานดังนี้ครับ:

  • 1. Big-endian (พี่ใหญ่สับหัวเข้าก่อน): ในขนบธรรมเนียมระบบนี้ ไบต์ซีกที่มีความสำคัญ หรือนัยยะสูงสุด (Most Significant Byte - MSB) จะถูกจับยัดหั่นเก็บไว้ที่ตำแหน่ง Address ที่ต่ำและตื้นที่สุดของชั้นหน่วยความจำ (ดันไปไว้ซ้ายสุด) การเรียงร้อยแบบนี้สายตามนุษย์โลกจะอ่านทำความเข้าใจได้ง่ายที่สุด เพราะมันดิ่งตรงตัวเหมือนเวลาเราเขียนอ่านเลขคณิตศาสตร์ปกติ สถาปัตยกรรมทรงพลังที่ดื้อแพ่งใช้แบบนี้ตัวอย่างเช่น ตระกูลโปรเซสเซอร์ Motorola 68K, ชิปเซิร์ฟเวอร์ SPARC และที่ทรงอิทธิพลที่สุดคือ มันถูกกวาดต้อนบังคับใช้เป็นมาตรฐานหลักระดับโลกในการส่งข้อมูลผ่านเครือข่าย (Network Byte Order) เช่น โปรโตคอลอินเทอร์เน็ต TCP/IP และ UDP
  • 2. Little-endian (น้องเล็กแทรกหางทะลวงก่อน): ในมุมกลับกัน ระบบนี้ ไบต์ที่ถือครองความสำคัญหรือมีค่าน้ำหนักต่ำสุด (Least Significant Byte - LSB) จะถูกฉุดกระชากนำมาเก็บแพ็กไว้ที่ Address ที่ต่ำที่สุดก่อนเป็นอันดับแรก ข้อได้เปรียบทางวิศวกรรมที่ซ่อนอยู่คือ ตัวลอจิกฮาร์ดแวร์สามารถสั่งทุบหั่นโยนไบต์ส่วนเกินทิ้ง เพื่อแปลงคาสต์ (Cast) ขนาดตัวแปรให้สั้นลง (เช่น สับทอนจาก 32-bit เหลือแค่ 16-bit) ได้ง่ายและไวปานวอก สถาปัตยกรรมมหาชนยอดฮิตอย่างค่ายเตาผิง Intel/AMD (x86) พีซีที่คุณใช้อยู่ และแกนสมอง ARM บนมือถือสมาร์ตโฟนส่วนใหญ่ ล้วนเทใจเลือกใช้รูปแบบลูกทุ่งนี้ครับ

ทำไมมันถึงกลายเป็นตัวบั๊กหินบดในงาน Hardware Interaction? ในจักรวาลอันปิดตาย เมื่อคุณเขียนโค้ดรันอยู่บนบอร์ดไมโครคอนโทรลเลอร์เพียงบอร์ดเดียวโดดๆ Endianness แทบจะไร้ตัวตนและไม่มีความสำคัญต่อชีวิตคุณเลยครับ คุณลอยตัวไม่ต้องสนใจด้วยซ้ำว่าสถาปัตยกรรมข้างในมันแอบเก็บข้อมูลเรียงหน้ากระดานแบบไหน แต่มันจะกลายร่างเป็นปัญหาปีศาจ (Critical Issue) ทันทีในสถานการณ์หัวเลี้ยวหัวต่อต่อไปนี้:

  • การคุยข้ามพรมแดนแพลตฟอร์ม: หากคุณเอาบอร์ด x86 (หัวใจ Little-endian) ส่งยิงข้อมูลตัวเลข 32-bit ยาวเหยียดข้ามบัสสื่อสารไปให้บอร์ดดึกดำบรรพ์ฝั่ง SPARC (หัวใจ Big-endian) แผงวงจรบอร์ดปลายทางจะซื่อบื้อตีความลำดับไบต์หน้ากระดานนั้นกลับด้านสลับกันทั้งหมด (Reverse) ส่งผลกระทบรุนแรงทำให้ได้ค่าตัวเลขเศษขยะที่ผิดเพี้ยนบิดเบี้ยวไปเป็นคนละเรื่อง (Garbage data หลักล้าน)
  • วงจรอุบาทว์อ่านเขียนไฟล์ไบนารี: หากโปรแกรม A สั่งเซฟดรอปไฟล์ออกมาเป็นก้อนไบนารีบนสถาปัตยกรรมฝั่งหนึ่ง แล้วมีคนอุตริก๊อปไปดับเบิ้ลคลิกเปิดบนอีกสถาปัตยกรรมฝั่งหนึ่งที่มีความเชื่อ Endianness ต่างกัน ข้อมูลสูตรฟิสิกส์ หรือพิกัดภาพในไฟล์นั้นก็จะพังพินาศสลับหน้าสลับหลังเละเทะเช่นกัน

Endianness Diagram

ตัวอย่างโค้ด: งัดทักษะ Pointer Casting ล้วงตับดูเครื่องใน Endianness

มาชำแหละแงะดูวิธีการเขียนฟังก์ชันโค้ด C ป่าเถื่อน เพื่อ “แอบส่อง” ตรวจดูว่าหน่วยความจำในไมโครคอนโทรลเลอร์ที่กำลังรันโค้ดของเราอยู่นั้น นับถือศาสนา Big-endian หรือ Little-endian โดยใช้เขี้ยวเล็บของการทำตลบตะแลงพอยน์เตอร์ Pointer Casting ครับ (ท่าไม้ตายลวงตานี้ วิศวกรสาย Embedded ยกให้เป็นท่ามาตรฐาน)

#include <stdio.h>
#include <stdint.h>

/* ประกาศฟังก์ชันสายลับสำหรับตรวจสอบพฤติกรรม Endianness ฮาร์ดแวร์ปัจจุบันที่โค้ดลงไปสิงสู่ */
void check_machine_endianness(void) {
    /* 
     * เสกสร้างกล่องตัวแปรขนาดใหญ่ 32-bit (4 ไบต์) อัดค่า 0x12345678 ลงไป
     * สังเกตว่า MSB (ไบต์ซ้ายสุดค่าเยอะสุด) คือ 0x12 และ LSB (ไบต์ขวาสุดค่าน้อยสุด) คือ 0x78
     */
    uint32_t num = 0x12345678;
    
    /* 
     * บังคับใช้ Pointer ชนิด char (ซึ่งมองภาพกว้างแค่ทีละ 1 ไบต์) ชี้ปักหมุดไปแหย่ที่ Address แรกของตัวแปร num
     * ท่านี้ทำให้เราสามารถสวมแว่นขยายมองสแกนเจาะเข้าไปในด็อกเก็ตหน่วยความจำทีละบล็อก 1 ไบต์ได้ (นักเลงคีย์บอร์ดเรียก Type punning)
     */
    uint8_t *byte_ptr = (uint8_t *)&num;
    
    /* สั่งตรวจเช็กเชือดไบต์แรกสุดที่หมกอยู่ใน Address ตัวเลขต่ำสุด (หน่วยความจำช่องแรก) */
    if (byte_ptr[0] == 0x78) {
        printf("Hardware is Little-Endian (LSB is stored first).\n");
    } else if (byte_ptr[0] == 0x12) {
        printf("Hardware is Big-Endian (MSB is stored first).\n");
    } else {
        printf("Hardware is Mixed/Unknown Endian (Alien Architecture!).\n");
    }
    
    /* เขียนลูปประจาน พิมพ์ค่าบล็อกหน่วยความจำเรียงตัวออกมาดูให้เห็นกับตาทีละไบต์ */
    for (int i = 0; i < sizeof(num); i++) {
        printf("Memory Address Offset %d: contains 0x%02X\n", i, byte_ptr[i]);
    }
}

int main(void) {
    /* จุดพลุเริ่มปฏิบัติการเชือด */
    check_machine_endianness();
    return 0;
}

เกราะคุ้มกัน: วิจิตรศิลป์เคลียร์รอยต่อ ป้องกันขยะจาก Endianness

เพื่อสกัดกั้นป้องกันไม่ให้เกิดฝันร้ายบั๊กสยองขวัญในการโยนเพย์โหลดข้อมูลข้ามสายระหว่างฮาร์ดแวร์ มาตรฐานความดิบ Secure Coding และวิศวกรผู้เชี่ยวชาญรุ่นเดอะได้ประทับตราแนะนำแนวทางปฏิบัติเอาตัวรอดขั้นเด็ดขาดไว้ดังนี้ครับ:

  1. บูชา Macro สลับไบต์เป็นสร้อยพระ เมื่อต้องเหยียบเครือข่าย: มาตรฐานเหล็กดัดของโปรโตคอลเครือข่ายอินเทอร์เน็ต (TCP/IP) ล็อกคอบังคับให้แพ็กเกจข้อมูลที่วิ่งไปมาต้องเรียงแบบ Big-endian (Network byte order) เสมอไร้ข้อโต้แย้ง หากฮาร์ดแวร์ตัวเก่งของเราดันเป็นเผ่าพันธุ์ Little-endian เราจะถูกบังคับให้ต้องสลับไบต์กลับหัวทุกครั้งก่อนปล่อยของ โชคดีที่คลังภาษา C มีชุดเกราะมาตรฐานแถมมาอย่างฟังก์ชัน htons() (Host to Network Short), htonl() (Long), ตลอดจนขากลับ ntohs(), และ ntohl() ให้เรียกใช้งาน เพื่อสับรางแปลงลำดับไบต์หน้า-หลังสลับไปมาให้ถูกต้องชัวร์ก่อนส่งอัด หรือรับสูบข้อมูลจากบัส/Socket เสมอครับ ท่องไว้ให้ขึ้นใจ!
  2. ระวังหลุมพราง Union ในการขยี้แยกไบต์ดิบๆ: โปรแกรมเมอร์มือบอนหลายคนชอบความง่าย มักมักง่ายใช้โครงสร้าง union เพื่อจับตัวแปรใหญ่ 32-bit มารีดแยกส่วนกรีดออกเป็นอาร์เรย์ก้อน 8-bit จำนวน 4 ช่องเนียนๆ แม้คอมไพเลอร์ที่ใจดีส่วนใหญ่จะยอมหลับตาให้คอมไพล์ผ่าน แต่ตามรัฐธรรมนูญมาตรฐาน C กฎเหล็กระบุว่าการกระทำนี้ถือเป็น “พฤติกรรมที่ไม่ระบุชัดเจน (Unspecified behavior)” และผลลัพธ์การหั่นไบต์จะกระจัดกระจายเปลี่ยนสลับไปตาม Endianness ของชิปอย่างหลีกเลี่ยงไม่ได้ หากคุณต้องการฟันฉลามดึงไบต์ที่ 2 หรือ 3 ออกมาชัวร์ๆ ให้หันไปกราบใช้ Bitwise Operators (เช่น การกด Shift >> และครอบ Mask AND &) จะให้ผลลัพธ์ที่แข็งแกร่งเป็นอมตะและไม่ผูกปิ่นโตขึ้นกับฮาร์ดแวร์หน้าไหนครับ (Hardware Independent 100%)
  3. กฎห้ามบิน: ห้ามโยน Struct ดิบๆ ลอยข้ามระบบเด็ดขาด: การมักง่ายโยนก้อนก้อน struct ตันๆ ข้ามระบบเครือข่ายด้วยพอยน์เตอร์บอดทื่อๆ เป็นความผิดพลาดระดับฆาตกรรมที่หน้าใหม่พบบ่อยมาก เพราะนอกจากคุณจะต้องเผชิญหน้าต่อสู้กับเรื่อง Endianness ของแต่ละฝั่งที่อาจเรียงสวนทางไม่ตรงกันแล้ว คุณยังต้องกระอักเลือดเจอปัญหาเรื่อง Memory Alignment และหลุมพราง Padding (ช่องว่างแฝงที่คอมไพเลอร์เจ้าเล่ห์แอบยัดไส้เติมขยับให้ใน Struct) อีกด้วย หากไฟลต์บังคับต้องส่งข้ามพอร์ต I2C, SPI หรือยัดเขียนลงไฟล์ไบนารี ให้กัดฟันทำการ Serialization (เช่น การไล่แพ็กเรียงพับสาดส่งทีละไบต์ตามกฎรูปแบบที่ตกลงเซ็นสัญญาแพตเทิร์นกันไว้เป๊ะๆ) จะปลอดภัยและ Portable พกไปบอร์ดไหนก็ทำงานรอดตายที่สุดครับ
  4. สูดความล้ำฟีเจอร์ใหม่ของ C23 (ถ้าบารมีคอมไพเลอร์ถึง): ในมาตรฐานดราฟต์ใหม่เอี่ยมของภาษา C (C23) ได้มีการปูพรมเพิ่มเฮดเดอร์ไฟล์ <stdbit.h> ซึ่งแอบซ่อนอาวุธ Macro ศักดิ์สิทธิ์อย่าง __STDC_ENDIAN_LITTLE__ และ __STDC_ENDIAN_NATIVE__ มาให้ด้วย ทำให้เราสามารถร่ายมนต์เขียน #if เช็กจับหน้า Endianness ของคอมไพเลอร์ตั้งแต่ตอน Compile-time (ก่อนเบิร์นลงชิป) ได้เลยทันที ทำให้ชีวิตวิศวกรสายฮาร์ดแวร์อย่างพวกเรายืนจิบกาแฟง่ายขึ้นอีกเป็นกองครับ

สรุป (Conclusion)

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

หากเพื่อนๆ เสพติดลุ่มหลงชอบบทความผ่าตัดเจาะลึกทะลวงถึงโครงสร้างการประมวลผลและการจัดการไบต์ข้อมูลระดับล่าง Bare-metal ห่ามๆ แบบนี้ หรือใครมีแผลในใจเคยเจอบั๊กนั่งงมเครื่องสโคปดมโค้ดหาตั้งนานสุดท้ายร้องอ๋อจบที่ข้อมูลกลับหัวกลับหาง MSB/LSB สลับฝั่ง มาเล่าสู่กันฟังและแชร์โปรเจกต์สนุกๆ เขย่าบอดเขย่าชิปกันต่อได้ที่ศาลาพักใจเว็บ www.123microcontroller.com ของพวกเรานะครับ การควบเขียนคำสั่งโปรแกรม C บีบเค้นรีดประสิทธิภาพคุมฮาร์ดแวร์เป็นสถาปัตยกรรมศาสตร์ที่สนุก ดิบ และกระตุ้นอะดรีนาลีนท้าทายมาก แล้ววอร์มคีย์บอร์ดพบกันใหม่กระแทกโค้ดในบทความหน้า Happy Embedded Coding ครับทุกคน!