ลองนึกภาพตามนะครับ คุณพิมพ์โค้ดสั้นๆ ลงไปในคอมพิวเตอร์ กดปุ่มรัน แล้วไฟ LED ดวงเล็กๆ บนบอร์ดก็ติดขึ้นมา

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

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

ทุกอย่างเริ่มจากไฟฟ้าสถานะเปิด-ปิด

ลึกลงไปในชิปทุกตัว ไม่ว่าจะเป็นคอมพิวเตอร์ มือถือ หรือเครื่องซักผ้า แก่นแท้ของมันคือสวิตช์ไฟฟ้าจิ๋วจำนวนมหาศาล นับเป็นพันล้านตัว สวิตช์แต่ละตัวก็เหมือนสวิตช์ไฟในบ้านเรา คือมีแค่สองสถานะ — เปิด กับ ปิด

เราแทน "เปิด" ด้วยเลข 1 และ "ปิด" ด้วยเลข 0 และเจ้าเลขสองตัวนี้เอง — 0 กับ 1 — คือภาษาเดียวที่คอมพิวเตอร์เข้าใจจริงๆ เราเรียกมันว่าเลขฐานสอง (binary)

คุณอาจสงสัยว่า แค่ 0 กับ 1 จะสื่อความหมายอะไรได้ ลองนึกถึงการส่งสัญญาณด้วยไฟฉายตอนกลางคืนดูครับ ไฟดวงเดียวเปิด-ปิดได้แค่สองแบบ แต่ถ้าเรามีไฟหลายดวงเรียงกัน แล้วกำหนดว่าแต่ละรูปแบบการเปิด-ปิดหมายถึงอะไร เราก็ส่งข้อความที่ซับซ้อนได้ คอมพิวเตอร์ก็ใช้หลักการเดียวกันเป๊ะ เพียงแต่ทำได้เร็วและมีจำนวน "หลอดไฟ" มากกว่ามหาศาล

Machine Code: ภาษาที่คอมพิวเตอร์เข้าใจ แต่มนุษย์ปวดหัว

เมื่อเราเอาเลข 0 กับ 1 มาเรียงต่อกันเป็นชุดตามกติกาที่กำหนดไว้ มันก็กลายเป็น "คำสั่ง" ที่สั่งให้ CPU ทำงานได้ ลองดูชุดเลขนี้ครับ

10110000 01100001

สำหรับเราอาจดูเหมือนเลขมั่วๆ แต่สำหรับ CPU แล้ว นี่คือคำสั่งที่ชัดเจนหนึ่งคำสั่ง ประมาณว่า "เอาค่านี้ไปเก็บไว้ตรงนี้" เราเรียกเลขฐานสองที่เป็นคำสั่งแบบนี้ว่า **Machine Code** หรือภาษาเครื่อง มันคือสิ่งที่ CPU อ่านแล้วลงมือทำได้ทันที ไม่ต้องแปลอะไรอีก

ปัญหาคือ... ลองจินตนาการว่าคุณต้องเขียนโปรแกรมทั้งโปรแกรมด้วยเลข 0 กับ 1 แบบนี้ โปรแกรมจริงๆ มีคำสั่งเป็นแสนเป็นล้านบรรทัด ไม่มีมนุษย์คนไหนเขียนหรือจำไหว แค่พิมพ์ผิดไปตัวเดียวก็หาไม่เจอแล้ว นี่จึงเป็นจุดเริ่มต้นที่มนุษย์คิดค้น "ตัวช่วย" ขึ้นมาทีละชั้น เพื่อให้เขียนโปรแกรมได้ง่ายขึ้น

ISA: เมนูคำสั่งประจำตัวของ CPU แต่ละรุ่น

ก่อนจะไปดูตัวช่วย มีอีกเรื่องที่ต้องเข้าใจก่อน นั่นคือ CPU แต่ละแบบไม่ได้เข้าใจเลขฐานสองเหมือนกันหมด

เปรียบเหมือนร้านอาหารที่มีเมนูของตัวเอง ร้านหนึ่งสั่ง "เมนูเบอร์ 5" ได้ผัดกะเพรา อีกร้านสั่งเบอร์ 5 เดียวกันอาจได้ข้าวมันไก่ CPU ก็เช่นกัน เลขฐานสองชุดเดียวกันอาจหมายถึงคนละคำสั่งบน CPU คนละแบบ

รายการคำสั่งทั้งหมดที่ CPU รุ่นหนึ่งๆ รองรับ — ว่าเลขชุดไหนหมายถึงคำสั่งอะไร มีที่เก็บข้อมูลชั่วคราว (register) อะไรบ้าง — เราเรียกรวมๆ ว่า ISA (Instruction Set Architecture) หรือก็คือ "เมนู" ประจำตัวของ CPU นั่นเอง

ISA ที่เราเจอในชีวิตจริงก็มีหลายแบบ เช่น ARM ที่อยู่ในมือถือและบอร์ด STM32, x86 ที่อยู่ในคอมพิวเตอร์และโน้ตบุ๊ก หรือ AVR ที่อยู่ในบอร์ด Arduino ยอดนิยม

ประเด็นสำคัญที่ต้องจำคือ เพราะ CPU คนละ ISA มีเมนูคนละชุด โปรแกรม (machine code) ที่สร้างมาสำหรับชิปแบบหนึ่ง จึงเอาไปรันบนชิปอีกแบบไม่ได้ จำประเด็นนี้ไว้นะครับ เดี๋ยวมันจะกลับมาสำคัญมากตอนท้าย

Assembly: แปลงเลขให้เป็นคำที่พอจำได้

วช่วยชั้นแรกที่มนุษย์คิดขึ้นมา คือการเอา "คำย่อ" มาแทนเลขฐานสองแต่ละคำสั่ง เพื่อให้พออ่านรู้เรื่องและจำได้ เราเรียกภาษาชั้นนี้ว่า "Assembly"

ยกตัวอย่าง machine code ชุดเดิมที่เราเห็นไปก่อนหน้า เขียนเป็น Assembly ได้แบบนี้

MOV AL, 61h    ; เอาค่า 0x61 ไปเก็บใน register ชื่อ AL

MOV มาจากคำว่า move เห็นแบบนี้พออ่านออกแล้วว่ามันกำลัง "ย้าย" ค่าไปไหนสักที่ ดีกว่ามานั่งไล่อ่าน 10110000 01100001 เยอะเลย และที่สำคัญคือ Assembly หนึ่งบรรทัดนี้ แปลตรงกลับไปเป็น machine code ชุดที่เราเห็นตอนแรกได้พอดี

ฟังดูดีขึ้นเยอะ แต่ Assembly ก็ยังมีจุดที่ไม่สะดวกอยู่สองข้อ ข้อแรกคือมันยังต้องสั่งงานทีละขั้นเล็กๆ อย่างละเอียด งานง่ายๆ ในหัวเราอาจต้องเขียน Assembly หลายบรรทัด ข้อสองคือ — จำเรื่อง ISA ที่บอกให้จำไว้ได้ไหมครับ — Assembly ก็ยังผูกติดกับ ISA ของชิปแต่ละแบบอยู่ดี Assembly ของ ARM กับของ x86 ก็เขียนคนละแบบ ถ้าเปลี่ยนชิปก็ต้องเขียนใหม่

ภาษา C: จุดที่ลงตัวที่สุด

แล้วเราก็มาถึงพระเอกของเรา — ภาษา C ซึ่งเป็นตัวช่วยอีกชั้นที่ยกระดับขึ้นไปอีก ใกล้เคียงภาษามนุษย์มากขึ้น คำสั่ง Assembly ที่เราเห็นเมื่อกี้ เขียนเป็นภาษา C ได้สั้นๆ แบบนี้

char al = 0x61

อ่านปุ๊บก็พอเดาออกว่า "เก็บค่า 0x61 ลงในตัวแปรชื่อ al" เราไม่ต้องไปยุ่งกับ register หรือจำเมนูของ ISA ให้ปวดหัวอีกต่อไป เราเขียนสิ่งที่เราอยากให้เกิดขึ้น แล้วมีโปรแกรมตัวหนึ่งที่เรียกว่า คอมไพเลอร์ (compiler) ทำหน้าที่แปลโค้ด C ของเราลงไปเป็น Assembly และ machine code ให้เองโดยอัตโนมัติ

แต่จุดที่ทำให้ C เจ๋งจริงๆ คือเรื่องนี้ครับ — โค้ด C ชุดเดียวกันนี้ เราเอาไปคอมไพล์สำหรับชิป ARM ก็ได้ x86 ก็ได้ AVR ก็ได้ แค่เปลี่ยนคอมไพเลอร์ให้ตรงกับชิป โดยไม่ต้องเขียนโค้ดใหม่ ต่างจาก Assembly ที่ต้องเขียนใหม่ทุกครั้งที่เปลี่ยนชิป

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

ภาพรวมทั้งหมด

ลองมาดูเส้นทางทั้งหมดในภาพเดียวกันครับ ว่าโค้ดที่เราเขียนเดินทางลงไปจนถึงไฟฟ้าจริงได้อย่างไร

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

Machines Labs — From C Code to Electricity
Machines Labs — From C Code to Electricity

สรุป

กลับมาที่คำถามตอนต้น ว่าคอมพิวเตอร์ "เข้าใจ" คำสั่งของเราได้อย่างไร ทั้งที่มันรู้จักแค่ไฟฟ้าเปิด-ปิด

คำตอบก็คือ มันไม่ได้เข้าใจเราโดยตรงเลย แต่มีชั้นของการแปลภาษาซ้อนกันอยู่หลายชั้น ตั้งแต่ภาษา C ที่เราเขียน ถูกแปลลงเป็น Assembly แล้วเป็น Machine Code และสุดท้ายกลายเป็นไฟฟ้าที่ไหลในวงจร แต่ละชั้นทำหน้าที่แปลภาษาให้อีกชั้นที่อยู่ต่ำกว่าเข้าใจ มันไม่ใช่เวทมนตร์ แต่เป็นระบบที่มนุษย์ค่อยๆ สร้างขึ้นมาอย่างชาญฉลาด เพื่อให้เราคุยกับเครื่องจักรได้ง่ายขึ้น

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

แล้วพบกันในบทความถัดไปครับ