โมดูล 1/5 · สัปดาห์ 1–3 · 27 ชม.

เลขฐานและลอจิกดิจิทัล

UAT 105 เทคโนโลยีดิจิทัลและเครือข่าย

เวลาเรียนประมาณ 90 นาทีร่าง รอตรวจปรับปรุงล่าสุด 26 กันยายน 2569

บทเรียน

เมื่อเรียนจบโมดูลนี้ ผู้เรียนจะสามารถ

  1. แปลงเลขระหว่างฐานสิบ ฐานสอง และฐานสิบหก และอ่านไบต์ของข้อความ MAVLink ได้
  2. อธิบายการเก็บจำนวนเต็มติดลบแบบ two's complement และเลขทศนิยมตาม IEEE 754
  3. เขียนตารางความจริงของเกต AND OR XOR NOT และต่อเป็นวงจรบวก
  4. อธิบายการตรวจความผิดพลาดด้วย parity และ CRC และคำนวณ CRC ของเฟรม MAVLink

ความรู้พื้นฐานที่ควรมี: UAT 104 โมดูล 1 และ 4

ทำไมต้องรู้

ทุกอย่างที่โดรนรับรู้และสั่งงาน ไม่ว่าจะเป็นค่าความเร่งจาก IMU พิกัด GNSS หรือคำสั่งให้บินกลับ ถูกเก็บและส่งเป็น บิต คือค่า 0 กับ 1 เท่านั้น ถ้าอ่านบิตเป็น เราจะถอดข้อความ telemetry ได้เอง เข้าใจว่าทำไมค่าความเร็วติดลบจึงกลายเป็นเลข 65286 เมื่ออ่านผิดชนิด และรู้ว่าเครื่องรับจับข้อมูลที่เสียระหว่างทางได้อย่างไร

โมดูลนี้ต่อจาก UAT 104 โมดูล 4 ที่อ่านไฟล์ .tlog ด้วย pymavlink คราวนี้เราจะลงไปดูถึงระดับบิตว่าไลบรารีทำอะไรให้เรา

เลขฐานสองและฐานสิบหก

เลขฐานสอง (binary) ใช้เพียงเลข 0 และ 1 แต่ละหลักเรียกว่า บิต (bit) ค่าประจำหลักเพิ่มทีละสองเท่า เหมือนเลขฐานสิบที่เพิ่มทีละสิบเท่า บิตแปดตัวรวมเป็น ไบต์ (byte) เก็บค่าได้ 0 ถึง 255

เลขฐานสิบหก (hexadecimal) ใช้ 0–9 และ A–F เลขฐานสิบหกหนึ่งหลักแทนบิตได้พอดีสี่ตัว หนึ่งไบต์จึงเขียนเป็นเลขฐานสิบหกสองหลัก โปรแกรมเมอร์นิยมใช้เพราะสั้นกว่าและอ่านง่ายกว่าฐานสองมาก ใน Python เขียนเลขฐานสองขึ้นต้นด้วย 0b และฐานสิบหกขึ้นต้นด้วย 0x

เลข 200 ในฐานสองคือ 11001000 ใต้แต่ละบิตมีน้ำหนัก 128 64 32 16 8 4 2 1 บิตที่เป็น 1 อยู่ที่ 128 64 และ 8 แบ่งเป็นสองกลุ่มสี่บิต 1100 เป็น C และ 1000 เป็น 8 จึงได้ 0xC8
ภาพที่ 1 เลข 200 ในฐานสองและฐานสิบหก
for n in [5, 13, 200, 255]:
    print(f"{n:>3}  bin {n:08b}  hex 0x{n:02X}")
print(int("11001000", 2), int("C8", 16), int("0xFD", 16))
  5  bin 00000101  hex 0x05
 13  bin 00001101  hex 0x0D
200  bin 11001000  hex 0xC8
255  bin 11111111  hex 0xFF
200 200 253

ไบต์แรกของเฟรม MAVLink 2 คือ 0xFD ซึ่งเท่ากับ 253 อย่างที่เห็นในบรรทัดสุดท้าย

ระวังหน่วยของข้อมูล

ความเร็วของลิงก์นับเป็น บิตต่อวินาที เช่น 20 Mbit/s = 20 000 000 bit/s ส่วนขนาดไฟล์มักนับเป็น ไบต์ และ 1 byte = 8 bit เสมอ นอกจากนี้ kB (กิโลไบต์) = 1000 byte ตามระบบ SI แต่ KiB (คิบิไบต์) = 1024 byte ระบบปฏิบัติการบางตัวแสดงหน่วยหนึ่งแต่ติดป้ายเป็นอีกหน่วย จึงควรดูให้แน่ก่อนคำนวณ

จำนวนเต็มติดลบแบบ two’s complement

คอมพิวเตอร์เก็บจำนวนเต็มติดลบด้วยวิธี two’s complement คือค่า ในพื้นที่ บิตเก็บเป็นเลข ข้อดีคือวงจรบวกลบตัวเดียวใช้ได้ทั้งเลขบวกและเลขลบ บิตซ้ายสุดเป็น 1 เมื่อเลขติดลบ จำนวนเต็ม 16 บิตแบบมีเครื่องหมายจึงเก็บได้ตั้งแต่ −32768 ถึง 32767

ใน MAVLink ข้อความ GLOBAL_POSITION_INT เก็บความเร็วแนวดิ่ง vz เป็นจำนวนเต็ม 16 บิตมีเครื่องหมาย หน่วย cm/s ถ้าโดรนไต่ขึ้นด้วย 2.5 m/s ค่า vz จะเป็น −250 เพราะแกน z ของกรอบ NED ชี้ลงดิน

import struct

raw = struct.pack("<h", -250)
print(raw.hex(" "), f"{int.from_bytes(raw, 'little'):016b}")
print(struct.unpack("<h", raw)[0], struct.unpack("<H", raw)[0])
print((1 << 16) - 250)
06 ff 1111111100000110
-250 65286
65286

รหัส <h บอกให้ struct อ่านเป็นจำนวนเต็ม 16 บิตมีเครื่องหมาย เรียงไบต์แบบ little-endian (ไบต์ค่าน้อยมาก่อน) ซึ่ง MAVLink ใช้ ถ้าอ่านไบต์เดียวกันผิดเป็น <H (ไม่มีเครื่องหมาย) จะได้ 65286 ซึ่งเท่ากับ พอดี ค่าความเร็วแปลก ๆ แบบนี้ในกราฟมักเกิดจากอ่านชนิดข้อมูลผิด

ตัวอย่างที่ 1 เลขทศนิยม 0.1 ในหน่วยความจำ

เลขทศนิยมใช้มาตรฐาน IEEE 754-2019 แบ่งบิตเป็นเครื่องหมาย เลขชี้กำลัง และส่วนทศนิยม ข้อความ ATTITUDE ของ MAVLink เก็บมุมเป็นเลขทศนิยม 32 บิต ซึ่งแทน 0.1 ได้ไม่ตรง เหมือนที่เห็นใน UAT 104 โมดูล 1

print(struct.pack(">f", 0.1).hex(), f"{struct.unpack('>f', struct.pack('>f', 0.1))[0]:.20f}")
3dcccccd 0.10000000149011611938

ค่าที่เก็บจริงคือ 0.100000001490… ไม่ใช่ 0.1 พอดี จึงห้ามเทียบเลขทศนิยมด้วย ==

เกตตรรกะและตารางความจริง

เกตตรรกะ (logic gate) เป็นวงจรที่รับบิตเข้าแล้วให้บิตออกตามกฎหนึ่ง เกตพื้นฐานได้แก่ AND (ออก 1 เมื่อทุกขาเป็น 1), OR (ออก 1 เมื่อมีขาใดเป็น 1), NOT (กลับค่า) และ XOR (ออก 1 เมื่อสองขาต่างกัน) ตารางความจริง แสดงผลของทุกกรณีที่เป็นไปได้ สำหรับสองขาเข้ามี กรณี

from itertools import product

print("A B | AND OR XOR NAND")
for a, b in product([0, 1], repeat=2):
    print(a, b, "|", a & b, " ", a | b, " ", a ^ b, "  ", 1 - (a & b))
A B | AND OR XOR NAND
0 0 | 0   0   0    1
0 1 | 0   1   1    1
1 0 | 0   1   1    1
1 1 | 1   1   0    0

ในภาษา Python ตัวดำเนินการ & | ^ ทำงานกับบิตโดยตรง เราใช้ตัวดำเนินการเหล่านี้กับ บิตมาสก์ (bitmask) เพื่ออ่านสถานะหลายอย่างจากเลขตัวเดียว เช่น บิตที่ 7 ของ base_mode ใน HEARTBEAT บอกว่าโดรนติดอาวุธมอเตอร์ (armed) หรือยัง

MAV_MODE_FLAG_SAFETY_ARMED = 0b1000_0000
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED = 0b0000_0001
base_mode = 0x51
print(f"{base_mode:08b}", bool(base_mode & MAV_MODE_FLAG_SAFETY_ARMED), bool(base_mode & MAV_MODE_FLAG_CUSTOM_MODE_ENABLED))
armed = base_mode | MAV_MODE_FLAG_SAFETY_ARMED
print(f"{armed:08b}", hex(armed))
01010001 False True
11010001 0xd1

จากเกตสู่วงจรบวก

เอาเกต XOR กับ AND มาต่อกันจะได้ วงจรบวกครึ่ง (half adder) ที่บวกเลขหนึ่งบิตสองตัว ได้ผลบวก และตัวทด ต่อวงจรบวกครึ่งสองตัวจะได้ วงจรบวกเต็ม (full adder) ที่รับตัวทดจากหลักก่อนหน้าได้ และต่อวงจรบวกเต็มหลายตัวเรียงกันจะบวกเลขหลายบิตได้ หน่วยคำนวณ (ALU) ใน CPU สร้างจากหลักการนี้

วงจรบวกครึ่ง ขาเข้า A และ B ต่อเข้าเกต XOR ได้ผลบวก S และต่อเข้าเกต AND ได้ตัวทด C ด้านขวามีตารางความจริง A B S C สี่แถว 0 0 0 0, 0 1 1 0, 1 0 1 0 และ 1 1 0 1
ภาพที่ 2 วงจรบวกครึ่งและตารางความจริง
def half_adder(a, b):
    return a ^ b, a & b


def full_adder(a, b, cin):
    s1, c1 = half_adder(a, b)
    s, c2 = half_adder(s1, cin)
    return s, c1 | c2


def add_4bit(x, y):
    carry, bits = 0, []
    for i in range(4):
        s, carry = full_adder((x >> i) & 1, (y >> i) & 1, carry)
        bits.append(s)
    return sum(b << i for i, b in enumerate(bits)), carry


for x, y in [(5, 6), (9, 7), (15, 1)]:
    total, carry = add_4bit(x, y)
    print(f"{x} + {y} -> sum {total:04b} ({total}), carry {carry}")
5 + 6 -> sum 1011 (11), carry 0
9 + 7 -> sum 0000 (0), carry 1
15 + 1 -> sum 0000 (0), carry 1

ใหญ่เกินกว่าที่ 4 บิตเก็บได้ (สูงสุด 15) ผลบวกจึงวนกลับเป็น 0 และตัวทดเป็น 1 ปรากฏการณ์นี้เรียกว่า ล้น (overflow) ตัวนับลำดับ (sequence) ของ MAVLink ขนาด 8 บิตก็วนจาก 255 กลับเป็น 0 ด้วยเหตุผลเดียวกัน

ตรวจความผิดพลาดด้วย parity และ CRC

คลื่นวิทยุอาจทำให้บิตบางตัวพลิกระหว่างทาง เครื่องรับจึงต้องมีวิธีรู้ว่าข้อมูลเสีย วิธีง่ายที่สุดคือ บิตพาริตี (parity bit) ที่นับว่ามีเลข 1 เป็นจำนวนคู่หรือคี่ แต่จับได้เฉพาะเมื่อบิตพลิกเป็นจำนวนคี่ตัว

วิธีที่แข็งแรงกว่าคือ CRC (cyclic redundancy check) ที่ใช้ XOR และการเลื่อนบิตคำนวณรหัสตรวจสอบจากข้อมูลทั้งเฟรม MAVLink ใช้ CRC-16/MCRF4XX ขนาด 2 ไบต์ ปิดท้ายทุกเฟรม และเติมค่าพิเศษ CRC_EXTRA ของแต่ละชนิดข้อความเข้าไปในการคำนวณด้วย ถ้าผู้ส่งกับผู้รับเข้าใจรูปแบบข้อความไม่ตรงกัน CRC จะไม่ตรง ข้อความนั้นจึงถูกทิ้ง

def crc16_mcrf4xx(data, crc=0xFFFF):
    for byte in data:
        tmp = byte ^ (crc & 0xFF)
        tmp = (tmp ^ (tmp << 4)) & 0xFF
        crc = ((crc >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4)) & 0xFFFF
    return crc


print(hex(crc16_mcrf4xx(b"123456789")))
0x6f91

ค่า 0x6F91 จากข้อความทดสอบ “123456789” ตรงกับค่าตรวจสอบมาตรฐานของ CRC-16/MCRF4XX แสดงว่าฟังก์ชันเขียนถูก ต่อไปลองกับเฟรม HEARTBEAT จริงที่สร้างด้วย pymavlink

ตัวอย่างที่ 2 ตรวจ CRC ของเฟรม HEARTBEAT

เฟรมนี้ยาว 21 ไบต์ ได้แก่ส่วนหัว 10 ไบต์ ข้อมูล 9 ไบต์ และ CRC 2 ไบต์ ค่า CRC_EXTRA ของ HEARTBEAT คือ 50

frame = bytes.fromhex("fd 09 00 00 00 01 01 00 00 00 04 00 00 00 02 03 51 03 03 7e 22")
header, payload, crc_bytes = frame[:10], frame[10:19], frame[19:]
msg_id = int.from_bytes(header[7:10], "little")
custom_mode = int.from_bytes(payload[0:4], "little")
print(len(frame), msg_id, custom_mode, payload[4], payload[5], hex(payload[6]))

HEARTBEAT_CRC_EXTRA = 50
crc = crc16_mcrf4xx(frame[1:19] + bytes([HEARTBEAT_CRC_EXTRA]))
print(hex(crc), crc == int.from_bytes(crc_bytes, "little"))

damaged = bytearray(frame)
damaged[16] ^= 0b1000_0000
crc_bad = crc16_mcrf4xx(bytes(damaged[1:19]) + bytes([HEARTBEAT_CRC_EXTRA]))
print(hex(crc_bad), crc_bad == int.from_bytes(crc_bytes, "little"))
21 0 4 2 3 0x51
0x227e True
0xf10 False

การคำนวณ CRC เริ่มจากไบต์ที่สอง (ไม่รวม 0xFD) CRC ที่ได้ 0x227E ตรงกับสองไบต์ท้าย 7e 22 ซึ่งเรียงแบบ little-endian เมื่อจำลองให้บิตซ้ายสุดของ base_mode พลิก (ทำให้ดูเหมือนติดอาวุธมอเตอร์แล้ว) CRC เปลี่ยนทันที เครื่องรับจึงทิ้งเฟรมนี้แทนที่จะเชื่อข้อมูลผิด

CRC ไม่ใช่ความปลอดภัย

CRC จับความผิดพลาดที่เกิดโดยบังเอิญได้ แต่ป้องกันผู้ไม่หวังดีไม่ได้ เพราะใครก็คำนวณ CRC ใหม่ให้ข้อความปลอมได้ การยืนยันว่าข้อความมาจากผู้ส่งจริงต้องใช้กุญแจลับ เช่น MAVLink signing ซึ่งจะเรียนในโมดูล 5

ปฏิบัติการประจำโมดูล

ปฏิบัติการ: อ่านบิตของโดรน

  1. เปิดไฟล์ telemetry_sample.tlog จาก UAT 104 อ่าน 40 ไบต์แรกด้วย open(..., "rb") แล้วแยกส่วนหัวของเฟรมแรกด้วยมือ ได้แก่ความยาว ลำดับ system ID และ message ID
  2. เขียนฟังก์ชันแปลง base_mode เป็นรายชื่อธงที่เปิดอยู่ทั้ง 8 บิต โดยใช้ชื่อจากเอกสาร MAVLink common.xml
  3. เขียนตัวบวก 8 บิตจาก full_adder แล้วทดสอบกับทุกคู่ของเลข 0–255 ด้วยการเทียบกับ (x + y) % 256
  4. ใช้ crc16_mcrf4xx ตรวจ CRC ของเฟรมทั้งหมดในไฟล์ .tlog นับว่ามีเฟรมที่ CRC ไม่ตรงกี่เฟรม (ต้องหาค่า CRC_EXTRA ของแต่ละข้อความจาก pymavlink)

ข้อผิดพลาดที่พบบ่อย

ระวัง

  • สับสนบิตกับไบต์ ลิงก์ 20 Mbit/s ส่งได้ 2.5 MB/s ไม่ใช่ 20 MB/s
  • อ่านชนิดข้อมูลผิด อ่านจำนวนเต็มมีเครื่องหมายเป็นไม่มีเครื่องหมาย ค่าติดลบเล็ก ๆ จะกลายเป็นเลขใหญ่มาก
  • ลืมลำดับไบต์ MAVLink เรียงแบบ little-endian ถ้าอ่านกลับด้าน ตัวเลขจะผิดทั้งหมด
  • ไม่ระวังการล้น ตัวนับขนาดจำกัดจะวนกลับเป็นศูนย์ ต้องคำนวณช่องว่างแบบ modulo
  • คิดว่า CRC ป้องกันการปลอมข้อความได้ CRC จับได้แค่ความผิดพลาดโดยบังเอิญ

สรุป

  • บิตแปดตัวเป็นหนึ่งไบต์ ฐานสิบหกหนึ่งหลักแทนสี่บิต จึงนิยมใช้เขียนไบต์
  • จำนวนติดลบเก็บแบบ two’s complement ส่วนเลขทศนิยมเก็บตาม IEEE 754 ซึ่งแทนบางค่าได้ไม่ตรง
  • เกต AND OR XOR NOT ต่อกันเป็นวงจรบวก และบิตมาสก์ใช้อ่านธงสถานะจากเลขตัวเดียว
  • MAVLink ตรวจความผิดพลาดด้วย CRC-16/MCRF4XX ที่รวม CRC_EXTRA แต่ CRC ไม่ได้ป้องกันการปลอมข้อความ

แบบฝึกตรวจความเข้าใจ

  1. 0b10110 มีค่าเท่าใดในฐานสิบ
  2. 0x3F มีค่าเท่าใดในฐานสิบ และเขียนเป็นเลขฐานสองแปดบิตอย่างไร
  3. จำนวนเต็ม 8 บิตมีเครื่องหมายค่า −1 เก็บเป็นไบต์ค่าใดในฐานสิบหก
  4. ถ้า A = 1 และ B = 1 วงจรบวกครึ่งให้ผลบวกและตัวทดเท่าใด
  5. ทำไม MAVLink จึงเติม CRC_EXTRA ลงในการคำนวณ CRC
เฉลย
  1. เขียนเป็น 00111111
  2. คือ 0xFF
  3. ผลบวก และตัวทด (เพราะ 1 + 1 = 10 ในฐานสอง)
  4. เพื่อตรวจว่าผู้ส่งและผู้รับเข้าใจรูปแบบของข้อความชนิดนั้นตรงกัน ถ้าไม่ตรง CRC จะผิดและข้อความถูกทิ้ง

สรุปสูตรสำคัญ

ค่าของเลขฐานสอง n บิต
จำนวนติดลบแบบ two's complement
ช่วงของจำนวนเต็มมีเครื่องหมาย n บิต
วงจรบวกครึ่ง

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

  1. Harris, S. L., & Harris, D. (2021). Digital design and computer architecture: RISC-V edition. Morgan Kaufmann.
  2. IEEE. (2019). IEEE standard for floating-point arithmetic (IEEE Std 754-2019). link
  3. MAVLink Development Team. Packet serialization. MAVLink developer guide. link
  4. MAVLink Development Team. MAVLink common message set (common.xml). link
  5. Python Software Foundation. ipaddress, socket, hashlib and hmac modules. The Python standard library (3.14). link

อ่านเพิ่มเติม

ศึกษาหน่วยความรู้ที่กำหนดล่วงหน้า ดูสื่อประกอบ และทำ quiz ประจำโมดูล

ในชั้นเรียน / ภาคสนาม

ปฏิบัติการในห้องแล็บหรือภาคสนามตามใบงาน พร้อม checklist ความปลอดภัย

หลักฐานการเรียนรู้: ใบงานที่ผ่านการตรวจและผล quiz

แบบทดสอบประจำโมดูล

แบบทดสอบนี้ใช้ตรวจความเข้าใจ (formative) ไม่ใช่การสอบเก็บคะแนน

โดเมนความรู้: การเขียนโปรแกรมและเทคโนโลยีดิจิทัล