01076051Lecture 05

UART — Serial Communication

บทที่ 5: การสื่อสารอนุกรมด้วย UART / USART

เวลาเราอยากให้ไมโครคอนโทรลเลอร์ “คุย” กับคอมพิวเตอร์ หรือกับอุปกรณ์อื่น เช่น โมดูล Bluetooth หรือ GPS เรามักส่งข้อมูลเป็นอนุกรม (Serial) คือส่งทีละบิตต่อกันไปบนสายเส้นเดียว. บทนี้ว่าด้วย UART ซึ่งเป็นการสื่อสารอนุกรมแบบแรกและพื้นฐานที่สุด เราจะดูว่า 1 เฟรมข้อมูลหน้าตาเป็นอย่างไร ทำไมต้องมีบิตเริ่ม-บิตหยุด (start/stop bit) ความต่างระหว่างสัญญาณระดับ TTL กับ RS-232 และวิธีเขียนโปรแกรมรับส่งข้อมูลผ่าน USART3 บนบอร์ด NUCLEO-F767ZI.

ผลการเรียนรู้ (Learning Outcomes)

  • อธิบายได้ว่า UART คืออะไร ต่างจาก USART อย่างไร และทำไมเรียกว่าเป็นการสื่อสารแบบ asynchronous (ไม่มีสายนาฬิกา)
  • อ่านและออกแบบ frame format ของ UART ได้ครบทุกส่วน: start bit, data bits, parity bit, stop bit และบอกได้ว่าส่ง LSB ก่อน
  • แยกความหมายของ bit rate กับ baud rate และคำนวณ overhead ของการส่งแบบ UART ได้
  • เปรียบเทียบสัญญาณระดับ TTL กับ RS-232 ได้ว่าต่างกันเรื่องแรงดันและกลับขั้วลอจิกอย่างไร และรู้ว่าต้องต่อ TX↔RX ไขว้กัน
  • ตั้งค่า (config) UART ได้ครบ: จำนวน data bit, parity (none/odd/even), จำนวน stop bit และเลือก baud rate ที่เหมาะสม
  • เขียนโปรแกรมรับส่งข้อมูลผ่าน USART3 บน NUCLEO-F767ZI ด้วย HAL ได้ โดยเข้าใจบทบาทของ register TDR/RDR

§5.1 UART คืออะไร และ asynchronous หมายถึงอะไร

UART ย่อมาจาก Universal Asynchronous Receiver / Transmitter คือวงจรสำหรับรับและส่งข้อมูลแบบอนุกรม (serial) หนึ่งทิศทางใช้สายเพียงเส้นเดียว. ในโลก Arduino เรามักเรียกการสื่อสารแบบนี้สั้น ๆ ว่า “Serial”.

คำว่า asynchronous (ไม่ประสานเวลา) เป็นหัวใจของ UART. มันแปลว่าเรา “ไม่ได้ส่งสายนาฬิกา (clock) ไปด้วย”. ฝั่งส่งกับฝั่งรับต่างคนต่างใช้นาฬิกาของตัวเอง แล้วตกลงกันไว้ล่วงหน้าว่าจะส่งเร็วเท่าไร ฝั่งรับจึงรู้ว่าต้องไปอ่านค่าบนสายตรงจังหวะไหน.

ถ้าเพิ่มสายนาฬิกาเข้าไปด้วย วงจรจะกลายเป็น USART (Universal Synchronous/Asynchronous Receiver/Transmitter) ซึ่งเลือกได้ว่าจะใช้ clock หรือไม่ใช้. การมี clock ช่วยให้ส่งข้อมูลได้เร็วและแม่นยำขึ้น. ฮาร์ดแวร์ของ STM32 ส่วนใหญ่เป็น USART แต่ถ้าเราไม่ใช้สาย clock มันก็ทำงานเป็น UART ธรรมดา.

ข้อสำคัญ: UART เป็นเพียงข้อตกลงเรื่อง “รูปแบบเฟรม (frame format)” เท่านั้น มันไม่ได้กำหนดชั้นกายภาพ (physical layer) ว่าต้องใช้แรงดันหรือสายแบบไหน. ตามแบบจำลอง OSI ถือว่า UART อยู่ใน Data Link layer (เลเยอร์ 2). เพราะไม่ได้ผูกกับชั้นกายภาพ เราจึงนำเฟรมของ UART ไปวิ่งบนสายมาตรฐานอื่น เช่น RS-232 ได้.

เพราะวงจรไม่ซับซ้อน (ใช้สายส่งเส้นเดียวต่อทิศทาง ไม่ต้องมี clock) ไมโครคอนโทรลเลอร์แทบทุกเบอร์จึงมี UART มาให้อย่างน้อย 1 ชุด. UART เหมาะกับงานทั่วไปที่ไม่ต้องการความเร็วสูง ส่วนงานเร็ว ๆ มักไปใช้ SPI หรือ I2C แทน.

VIZ-23Serial vs Parallel / Async vs Sync
แกน 1: Serial — บิตวิ่งทีละตัวบนสายเส้นเดียว
TXRX1 เส้นข้อมูล
แกน 2: Asynchronous — ต่างฝ่ายต่างมี clock ของตัวเอง
TXRXdata

UART = serial + asynchronous → มีแค่สายข้อมูล ไม่มีสาย clock (กำลังแสดงกรณีนี้)

UART = serial + asynchronous: มีแค่สายข้อมูล ไม่มีสาย clock เชื่อมสองฝั่ง. ลองสลับ Async/Sync เพื่อดูสาย clock ปรากฏหรือหายไป.

§5.2 โครงสร้างเฟรม (Frame Format) ของ UART

ข้อมูล 1 เฟรมของ UART ประกอบด้วย 4 ส่วน เรียงกันตามเวลา: บิตเริ่ม (start bit) → บิตข้อมูล (data bits) → บิตพาริตี (parity bit, มีหรือไม่ก็ได้) → บิตหยุด (stop bit).

ตอนไม่มีการส่ง สายจะอยู่สถานะว่าง (idle) ซึ่งเป็นลอจิก 1. เมื่อจะเริ่มส่ง ฝั่งส่งดึงสายลงเป็นลอจิก 0 หนึ่งบิต นี่คือ start bit ที่บอกฝั่งรับว่า “เฟรมใหม่กำลังมา” ให้เริ่มจับเวลาอ่านบิตถัดไป.

data bits: เลือกได้ว่าจะมีกี่บิต เช่น 6, 7, 8 หรือ 9 บิต ขึ้นกับว่าฮาร์ดแวร์ของ MCU รองรับเท่าไร. ที่นิยมที่สุดคือ 8 บิต (ส่ง 1 ไบต์ = 1 ตัวอักษร). UART ส่ง LSB (บิตต่ำสุด) ออกไปก่อน แล้วไล่ไปจน MSB (เรียงแบบ little-endian).

parity bit: เป็น overhead ตัวหนึ่งไว้ตรวจความถูกต้องของข้อมูลแบบง่าย ๆ. ถ้าเลือก even parity ให้เติม parity ให้จำนวนบิต 1 รวมทั้งเฟรมเป็นเลขคู่; ถ้าเลือก odd parity ให้รวมเป็นเลขคี่; ถ้าเลือก none ก็ไม่มี parity เลย. ฝั่งรับนับบิต 1 แล้วเทียบ ถ้าไม่ตรงแปลว่าข้อมูลเพี้ยนระหว่างทาง.

ข้อควรระวังเฉพาะของ STM32: ช่องความกว้าง data ที่ตั้งไว้ (เช่น 8 บิต) จะ “รวม” parity เข้าไปด้วย. ถ้าตั้ง 8 บิตแล้วเปิด parity จะเหลือข้อมูลจริงแค่ 7 บิต + parity 1 บิต. ถ้าต้องการข้อมูลจริง 8 บิตพร้อม parity ต้องตั้งความกว้างเป็น 9 บิต.

stop bit: ปิดท้ายเฟรมด้วยลอจิก 1 (เหมือนคืนสายกลับสู่ idle) เป็นเวลาอย่างน้อย 1 บิต. UART ส่วนใหญ่ใช้ 1 stop bit แต่เลือกใช้ 1.5 หรือ 2 บิตก็ได้ เพื่อให้วงจรฝั่งรับมีเวลาตั้งตัวก่อนเฟรมถัดไป. การตั้งค่าแบบ 8 data bit, no parity, 1 stop bit เขียนย่อว่า 8N1 เป็นค่าที่พบบ่อยที่สุด.

ส่วนลอจิก/ค่าหน้าที่
Idle1สถานะว่าง ไม่มีการส่ง
Start bit0บอกว่าเฟรมใหม่เริ่มแล้ว (1 บิตเสมอ)
Data bits6/7/8/9 บิตข้อมูลจริง ส่ง LSB ก่อน (little-endian)
Parity bit0 หรือ 1 (เลือกได้)ตรวจความถูกต้อง: even=คู่, odd=คี่, none=ไม่มี
Stop bit1ปิดท้ายเฟรม ≥1 บิต (นิยม 1 บิต)
องค์ประกอบของ 1 เฟรม UART
VIZ-24UART Frame Builder
สัญญาณดิจิทัลของเฟรม (เรียงตามเวลา →)
1idle0START1b00b11b20b31b40b51b60b71STOP1idle↑ LSB ออกก่อน
byte = 0x55 = 01010101 (8-bit)
data = [1 0 1 0 1 0 1 0] (LSB→MSB) · จำนวนบิต 1 = 4
parity = none (ไม่มีบิตตรวจสอบ)

ประกอบเฟรม UART หนึ่งเฟรม: idle(1) → start(0) → data (LSB ออกก่อน) → parity → stop(1). ปรับ config แล้ว waveform กับการคำนวณ parity จะวาดใหม่ทันที.

§5.3 Bit rate, Baud rate และ Overhead

Bit rate = จำนวนบิตที่ส่งได้ใน 1 วินาที หน่วยเป็น bit per second (bps). เช่น 9600 bps หมายถึงส่งได้ 9600 บิตต่อวินาที.

Baud rate = จำนวน “สัญลักษณ์ (symbol)” ที่ส่งได้ใน 1 วินาที. ในระบบ UART ที่ 1 สัญลักษณ์เท่ากับ 1 บิตพอดี ค่าของ baud rate กับ bit rate จึง “เท่ากัน” และใช้หน่วยเดียวกันได้ (คนจึงมักเรียกสลับกัน). แต่ในระบบที่ 1 สัญลักษณ์แทนได้หลายบิต สองค่านี้จะไม่เท่ากัน จึงต้องแยกให้ชัด.

Overhead = ทุกบิตที่เราต้องส่งไปแต่ไม่ใช่ข้อมูลจริง ได้แก่ start bit, parity bit และ stop bit. มันทำให้ความเร็ว “ข้อมูลจริง” ต่ำกว่า baud rate เสมอ.

ตัวอย่าง: ส่งแบบ 8N1 (8 data, no parity, 1 stop) 1 เฟรมมี 1 (start) + 8 (data) + 1 (stop) = 10 บิต แต่เป็นข้อมูลจริงแค่ 8 บิต. overhead = 2/10 = 20%. ที่ baud rate 9600 จึงส่งข้อมูลจริงได้จริงราว 9600 × 8/10 = 7680 bps.

การเลือก baud rate: ยิ่งสูงยิ่งส่งเร็ว แต่ต้องระวังความคลาดเคลื่อนของนาฬิกาสองฝั่ง เพราะเป็น asynchronous ถ้าเร็วเกินไปฝั่งรับอาจอ่านบิตผิดจังหวะ. ค่ายอดนิยมคือ 9600 และ 115200 bps.

ฝั่งรับอ่านค่าด้วยการ oversampling คือสุ่มอ่านที่ความถี่ 16 เท่าของ baud rate เพื่อหาจุดกึ่งกลางของแต่ละบิตให้แม่นยำ หลังจากนั้นจึงตัด start bit กับ stop bit ทิ้ง เหลือแต่ data bits.

การตั้งค่าบิตต่อเฟรมข้อมูลจริงoverhead
8N1 (no parity, 1 stop)10820%
8E1 / 8O1 (parity, 1 stop)11827%
8N2 (no parity, 2 stop)11827%
เปรียบเทียบ overhead ตามการตั้งค่า (data bit = 8)
VIZ-25Baud rate, Oversampling & Overhead
Oversampling 16× — จุดกึ่งกลางบิตใช้ตัดสินค่า
จุดตัดสินค่า (กึ่งกลางบิต)1 บิต = 16 sample ticks
สัดส่วนในหนึ่งเฟรม (start + 8 data + 1 stop)
data 8 bitoverhead 2overhead = start + parity + stop
baud = 115200 → เฟรม 10 บิต (ข้อมูลจริง 8 บิต)
overhead = 20.0% · effective bit rate (data) ≈ 92160 bps

baud rate คืออัตราสัญลักษณ์ ไม่ใช่ bit rate ของข้อมูลล้วน. ฝั่งรับ oversample 16× แล้วตัดสินค่าที่จุดกึ่งกลางบิต; start/parity/stop คือ overhead ที่กินแบนด์วิดท์.

§5.4 ระดับสัญญาณ: TTL เทียบกับ RS-232 และการต่อสาย

เฟรมของ UART เป็นแค่ลำดับลอจิก 0/1 แต่เวลาวิ่งบนสายจริง ต้องแปลงเป็นแรงดัน. ที่ขาของ MCU โดยตรงจะเป็นระดับ TTL คือลอจิก 1 = ไฟบวก (เช่น 3.3V) และลอจิก 0 = 0V. สัญญาณ TTL ส่งได้ระยะสั้น เพราะแรงดันต่ำและถูกรบกวนง่าย.

RS-232 เป็นมาตรฐานชั้นกายภาพที่ออกแบบมาให้ส่งได้ไกลขึ้น โดย (1) ใช้แรงดันสูงกว่า และ (2) กลับขั้วลอจิก: ลอจิก 1 เป็นไฟลบ และลอจิก 0 เป็นไฟบวก. เพราะส่งไปไกลสัญญาณจะมีการสูญเสีย (loss) ฝั่งส่งจึงดันแรงดันให้สูง ส่วนฝั่งรับตั้งเกณฑ์ให้ต่ำลงมาเพื่อยังอ่านได้.

ตัวเลขแรงดันของ RS-232 (ค่าที่พบบ่อย): ฝั่งส่งใช้ประมาณ ±3 ถึง ±15 V ส่วนฝั่งรับยอมรับ +3 ถึง +15 V เป็นลอจิก 0 และ −3 ถึง −15 V เป็นลอจิก 1. ช่วง −3 ถึง +3 V ถือเป็นเขตคลุมเครือ (ไม่นับเป็นค่าแน่นอน).

เพราะ TTL กับ RS-232 ใช้แรงดันและขั้วต่างกัน จะต่อตรงกันไม่ได้ ต้องมีวงจรแปลงระดับ (level shifter) เช่นชิป MAX232 คั่นกลาง. สาย RS-232 แบบเก่าใช้หัวต่อ DB9; โน้ตบุ๊กสมัยใหม่ไม่มีพอร์ตนี้แล้ว จึงต้องใช้ตัวแปลง USB-to-Serial (ภายในเป็น IC แปลง) เสียบเข้า USB แทน.

เรื่องต่อสายที่พลาดบ่อย: ต้องต่อ TX ของฝั่งหนึ่งเข้ากับ RX ของอีกฝั่ง และ RX เข้ากับ TX (ต่อไขว้กัน) เพราะขาส่งของเราต้องไปเข้าขารับของเขา. ยกเว้นกรณีมีวงจรสลับให้อัตโนมัติ จึงไม่ต้องไขว้เอง. และอย่าลืมต่อสาย Ground ร่วมกันด้วย.

หัวข้อTTLRS-232
ลอจิก 1ไฟบวก (เช่น 3.3V)ไฟลบ (−3 ถึง −15V)
ลอจิก 00Vไฟบวก (+3 ถึง +15V)
แรงดันต่ำสูง
ระยะส่งสั้นไกลกว่า
ต่อเข้า MCU ตรงได้ต้องมี level shifter (เช่น MAX232)
TTL เทียบกับ RS-232
VIZ-26TTL vs RS-232 Voltage Levels
แรงดันเทียบเวลา (ลำดับบิตเดียวกัน)
TTL+3.30VRS-232 (กลับขั้ว)เขตคลุมเครือ −3..+3V+12−1210110010
Level shifter + สาย TX↔RX ไขว้
MCUMAX232level shifterTXRX
เลื่อนเมาส์/โฟกัสที่คอลัมน์บิตเพื่อดูแรงดัน

RS-232 กลับขั้วลอจิกและสวิงแรงดันกว้างกว่า TTL จึงต้องมี level shifter (เช่น MAX232) และต่อสาย TX↔RX ไขว้. เลื่อนเมาส์หรือโฟกัสที่คอลัมน์บิตเพื่อดูแรงดันทั้งสองมาตรฐาน.

§5.5 UART บน NUCLEO-F767ZI และการเขียนโปรแกรม

ชิป STM32F767ZI มีโมดูล USART/UART ทั้งหมด 8 โมดูล. ช่องที่เราใช้บ่อยที่สุดคือ USART3 เพราะมันถูกเดินสายเชื่อมเข้ากับ ST-LINK บนบอร์ด ซึ่ง ST-LINK ทำหน้าที่เป็น Virtual COM Port (VCP) ให้ — เราจึงคุยกับ PC ได้ผ่านสาย USB เส้นเดียวกับที่โปรแกรมบอร์ด ไม่ต้องต่อสายเพิ่ม.

ขาของ USART3 บน NUCLEO-F767ZI: PD8 ทำหน้าที่เป็นขาส่ง (TX) และ PD9 เป็นขารับ (RX) ซึ่งทั้งสองต่อไปยังฝั่ง VCP ของ ST-LINK เรียบร้อยแล้ว.

ภายในฮาร์ดแวร์ UART มี register สำคัญคือ shift register สำหรับเลื่อนบิตเข้าออก และ data register สำหรับพักข้อมูล. บน STM32 (HAL) data register ฝั่งส่งชื่อ TDR (Transmit Data Register) และฝั่งรับชื่อ RDR (Receive Data Register) พร้อมบิตควบคุม baud rate ต่าง ๆ.

การตั้งค่าใน STM32CubeMX: เลือก baud rate (เช่น 115200), จำนวน word length (8 บิต), parity (none), และ stop bit (1) — รวมเป็น 115200 8N1. จากนั้น generate code.

การส่ง: ใช้ HAL_UART_Transmit โดยระบุ handle ของ UART, ที่อยู่ของข้อมูล, จำนวนไบต์ และ timeout. การรับ: ใช้ HAL_UART_Receive รับมาเก็บในตัวแปร. ฟังก์ชันแบบนี้เป็น blocking คือจะรอจนครบหรือจน timeout. ถ้ารับเป็นตัวอักษร (character) ให้ประกาศ char แล้วส่งที่อยู่ของมัน แต่ต้อง cast เป็น uint8_t* เพราะ HAL รับเป็น pointer ของ uint8_t.

ตัวอย่างการใช้งานจริง: ทำเมนูสั่งหุ่นยนต์ผ่าน UART — รับตัวอักษร 1 ตัวจาก PC แล้ว if เทียบ เช่น 'W' = เดินหน้า, 'S' = ถอยหลัง, 'A' = เลี้ยวซ้าย, 'D' = เลี้ยวขวา.

ส่งและรับข้อมูลผ่าน USART3 ด้วย HALc
// ส่งข้อความออกไปที่ PC (blocking, timeout 100 ms)
char msg[] = "Hello from Nucleo\r\n";
HAL_UART_Transmit(&huart3, (uint8_t *)msg, sizeof(msg) - 1, 100);

// รับ 1 ตัวอักษรจาก PC มาเก็บใน rx
char rx;
if (HAL_UART_Receive(&huart3, (uint8_t *)&rx, 1, 1000) == HAL_OK) {
  // สั่งงานตามตัวอักษรที่รับมา (เมนูหุ่นยนต์)
  if (rx == 'W') go_forward();
  else if (rx == 'S') go_backward();
  else if (rx == 'A') turn_left();
  else if (rx == 'D') turn_right();
}
VIZ-27USART3 ↔ ST-LINK VCP Data Path
TX →← RXโค้ดUSART3(TDR/RDR)PD8 TXPD9 RXST-LINK(VCP)USBPC terminalสาย USB เส้นเดียวกับที่ใช้โปรแกรมบอร์ด
PC terminal (ข้อมูลที่ TX ส่งออก)
—
RDR (ข้อมูลที่ RX รับเข้ามา)
—

ไบต์เดินทาง: โค้ด → USART3 (TDR/RDR) → PD8/PD9 → ST-LINK (Virtual COM Port) → USB → PC. สาย USB เส้นเดียวกับที่ใช้โปรแกรมบอร์ดก็คุยกับ PC ได้.

แบบฝึกหัด

12 ข้อ — แต่ละข้อมีชนิดและระดับความยากกำกับ กดปุ่ม "ดูเฉลย" เพื่อดูคำตอบและคำอธิบาย

  1. 1.ปรนัยง่าย

    คำว่า asynchronous ใน UART หมายความว่าอะไร

  2. 2.ปรนัยง่าย

    ในเฟรม UART ส่วนใดที่บอกฝั่งรับว่าเฟรมใหม่กำลังเริ่ม

  3. 3.ปรนัยกลาง

    ความต่างหลักระหว่าง UART กับ USART คืออะไร

  4. 4.ปรนัยกลาง

    UART ส่งบิตข้อมูลออกไปตามลำดับใด

  5. 5.ปรนัยยาก

    บน STM32 ถ้าตั้ง word length = 8 บิตและเปิด parity ด้วย ข้อมูลจริง (data) จะเหลือกี่บิต

  6. 6.ถูก/ผิดกลาง

    ในระบบ UART ค่า baud rate กับ bit rate มีค่าเท่ากัน

  7. 7.ถูก/ผิดกลาง

    ใน RS-232 ลอจิก 1 แทนด้วยแรงดันไฟบวก และลอจิก 0 แทนด้วยไฟลบ

  8. 8.ถูก/ผิดง่าย

    การต่อ UART ระหว่างสองอุปกรณ์ต้องต่อ TX ของฝั่งหนึ่งเข้ากับ RX ของอีกฝั่ง (ต่อไขว้)

  9. 9.เติมคำกลาง

    การตั้งค่า 8 data bit, no parity, 1 stop bit เขียนย่อว่า ____ และ 1 เฟรมมีทั้งหมด ____ บิต

  10. 10.เติมคำง่าย

    บน NUCLEO-F767ZI ช่อง USART ที่ต่อกับ ST-LINK เป็น Virtual COM Port คือ USART____ โดยใช้ขา PD8 เป็น ____ และ PD9 เป็น ____

  11. 11.วิเคราะห์ยาก

    ส่งข้อมูลแบบ 8N1 ที่ baud rate 9600 bps ถ้าต้องส่งข้อความ 100 ตัวอักษร (100 ไบต์) จะใช้เวลาประมาณเท่าใด และ overhead เป็นกี่เปอร์เซ็นต์ อธิบายวิธีคิด

  12. 12.วิเคราะห์ยาก

    ต่อ UART ระหว่างบอร์ด STM32 (ขา TTL 3.3V) กับอุปกรณ์ที่มีพอร์ต RS-232 โดยตรงโดยไม่ผ่านอะไรเลย ผลจะเป็นอย่างไร และต้องแก้ด้วยอะไร