รับแอปรับแอป

เจาะลึก I2C กับ Nuvoton N76E003: จากทฤษฎีถึงลงมือคุม EEPROM จริง

วรพล ศรีรุ่ง01-31

ภาพรวม: ทำไมต้อง I2C บนไมโครคอนโทรลเลอร์?

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

ในบทความนี้เราจะโฟกัสที่ การใช้ I2C บนไมโครคอนโทรลเลอร์ Nuvoton N76E003 เพื่อต่อกับ EEPROM แล้วลองเขียน–อ่านข้อมูลจริง พร้อมไล่ดูหลักการทำงาน I2C แบบไม่มโน ใครเขียนโค้ดด้วย AI อยู่แล้ว บทนี้คือ blueprint ให้เอาไป feed เข้า AI ช่วย generate code ได้สบาย ๆ

I2C คืออะไร? เข้าใจให้จบในหัวเดียว

คำว่า I2C หรือ IIC มาจาก “Integrated Circuit” หรือบางที่เรียกว่า TWI (Two Wire Interface) ทั้งหมดคือเรื่องเดียวกัน

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

  • SDA – Serial Data

  • SCL – Serial Clock

ทั้งสองเส้นต้องใช้ ตัวต้านทาน pull-up ต่อขึ้นไปที่ VCC เพื่อให้บัสอยู่ที่ระดับ “สูง” เสมอถ้าไม่มีใครดึงลงล่าง ด้วยการออกแบบแบบนี้ บัส I2C หนึ่งเส้นสามารถรองรับอุปกรณ์ได้สูงสุด 127 ตัว (แต่ละตัวมีที่อยู่ไม่ซ้ำกัน)

สถาปัตยกรรม I2C: Master – Slave

การสื่อสาร I2C ถูกออกแบบให้เกิดขึ้นระหว่าง Master และ Slave เสมอ

  • บนบัสเดียวมี Slave ได้หลายตัว

  • แต่ในช่วงเวลาใดเวลาหนึ่งจะมี Master ที่เป็นคนเริ่มสื่อสารเพียงตัวเดียว

การคุยกันทั้งหมดเกิดบน SCL และ SDA:

  • SCL (Serial Clock) – สัญญาณนาฬิกาที่ Master สร้างและแชร์ให้ Slave

  • SDA (Serial Data) – เส้นส่งข้อมูลไป–กลับระหว่าง Master กับ Slave

Master จะอ้างอิงหา Slave แต่ละตัวด้วย I2C Address ที่ไม่เหมือนกัน เมื่อ Master เรียกที่อยู่ใดที่อยู่หนึ่ง Slave ที่ตรงกับ address นั้นจะตอบ ส่วนตัวอื่นจะเงียบ ทำให้เราใช้บัสเดียวต่ออุปกรณ์ได้ทีเดียวหลายตัวโดยไม่ชนกัน

I2C ใช้ที่ไหน เหมาะกับงานแบบใด?

I2C ถูกออกแบบมาเพื่อ การสื่อสารระยะสั้น ภายในบอร์ดหรือภายในอุปกรณ์เดียวกันเป็นหลัก จุดเด่นคือ:

  • ใช้สายไฟน้อยมาก – แค่ 2 เส้นก็เชื่อมหลายอุปกรณ์ได้

  • เหมาะกับการต่อ:
    • เซ็นเซอร์

    • EEPROM / หน่วยความจำเล็ก ๆ

    • โมดูลเสริมที่มี I2C interface

แต่ถ้าโฟกัสด้านอื่น ๆ:

  • ต้องการสื่อสารไกล ๆ → มองไปที่ RS232 หรือโปรโตคอลอื่น

  • ต้องการความเชื่อถือได้สูงและเร็ว → SPI มักจะตอบโจทย์มากกว่า

สรุปสั้น ๆ: I2C = เหมาะกับงานบนบอร์ดเดียวกัน ใช้สายให้น้อยที่สุด แต่ต่ออุปกรณ์เยอะ ๆ ได้สบาย

ฮาร์ดแวร์ I2C บน Nuvoton N76E003

เป้าหมายของโปรเจกต์นี้คือ ทดลองสื่อสาร I2C ด้วย N76E003 โดยใช้ EEPROM (24C02/AT24LC64) เป็นตัวทดสอบ เราจะ:

  • เขียนข้อมูลบางค่าเข้า EEPROM ผ่าน I2C

  • อ่านค่าคืนจาก EEPROM

  • แสดงผลผ่าน UART ไปยัง Serial Monitor

อุปกรณ์ที่ใช้

  • บอร์ดไมโครคอนโทรลเลอร์ N76E003

  • Nu-Link สำหรับโปรแกรมโค้ด

  • EEPROM 24C02

  • ตัวต้านทาน 4.7kΩ จำนวน 2 ตัว (ใช้เป็น pull-up บน SDA/SCL)

  • USB–UART Converter (เช่น CP2102) เพื่อดูผลบน Serial

  • Breadboard และสายจัมเปอร์สำหรับต่อวงจร

นอกจากนั้น ต้องมี บอร์ดพัฒนา N76E003 และชุดสายโปรแกรมจาก Nu-Link เพื่ออัปโหลดโค้ดและดีบัก

การต่อวงจร: AT24LCxx กับ N76E003

EEPROM ถูกต่อเข้ากับบัส I2C ของ N76E003 ผ่าน SDA และ SCL พร้อมตัวต้านทานดึงขึ้น 2 ตัว

ในตัวอย่างนี้

  • EEPROM อยู่บน breadboard

  • ใช้สายจัมเปอร์เชื่อมต่อไปยังบอร์ด Nuvoton พร้อม Nu-Link

ขา I2C บน N76E003 และการตั้งค่าโหมดขา

ดูแผนผังขาของ N76E003 จะเห็นว่าขาแต่ละขามีฟังก์ชันหลายแบบในตัวเดียว

สำหรับ I2C:

  • P1.4 → SDA

  • P1.3 → SCL

เมื่อเลือกใช้ขาเหล่านี้เป็น I2C จะเสียฟังก์ชันอื่น ๆ บนขานั้นไป เช่น PWM แต่ในโปรเจกต์นี้ไม่ใช่ปัญหา

ขา I2C บน N76E003 เป็นขาแบบ GPIO ที่ต้อง ตั้งค่าโหมดให้ถูกต้องก่อนใช้งาน โดย I2C ต้องใช้โหมด Open-Drain ตาม datasheet ซึ่งควบคุมผ่านรีจิสเตอร์ PxM1.n และ PxM2.n

โค้ดตัวอย่างจะใช้มาโครจากไฟล์ `I2C.h` / `I2C.c` เพื่อ:

  • ตั้งค่า P1.3, P1.4 ให้เป็น Open-Drain

  • เลือกให้สองขานี้ทำหน้าที่ SDA/SCL ผ่านบิต I2CPX ในรีจิสเตอร์ I2CON

ถ้าเซ็ต `I2CPX = 0` → ใช้ P1.3, P1.4 เป็น SCL, SDA
ถ้าเซ็ต `I2CPX = 1` → ขา I2C จะย้ายไปใช้ P0.2 (SCL) และ P1.6 (SDA)

อุปกรณ์ต่อพ่วง I2C ใน N76E003

ข้อดีของ N76E003 คือมี ฮาร์ดแวร์ I2C Peripheral มาให้ในตัว ไม่ต้อง bit-banging ด้วยซอฟต์แวร์เหมือน 8051 บางรุ่น

N76E003 รองรับโหมด I2C หลัก ๆ 4 แบบ:

  • Master Transmitter

  • Master Receiver

  • Slave Transmitter

  • Slave Receiver

รองรับทั้งความเร็ว Standard (100 kbps) และ Fast (ถึง 400 kbps) บนบัส I2C

หลักการสำคัญของ I2C: Start, Stop, Address, ACK

1. เงื่อนไข Start และ Stop

ทุกการส่งข้อมูลบน I2C จะเริ่มด้วย Start Condition และจบด้วย Stop Condition

  • Start: SDA เปลี่ยนจาก High → Low ขณะที่ SCL ยัง High

  • Stop: SDA เปลี่ยนจาก Low → High ขณะที่ SCL ยัง High

เงื่อนไขสองอย่างนี้ถูกสร้างโดย Master ทั้งหมด และเป็นสัญญาณบอกว่า “บัสกำลังเริ่มใช้” หรือ “บัสว่างแล้ว”

2. ที่อยู่ 7 บิต + บิต R/W

N76E003 ใช้รูปแบบ 7-bit Address ตามมาตรฐาน เมื่อ Master ส่งข้อมูลครั้งแรกหลัง Start:

  • บิต 7 ตัวแรก = SLA (Slave Address)

  • บิตที่ 8 = R/W bit (0 = เขียน, 1 = อ่าน)

ดังนั้น 1 ไบต์แรกที่ส่งออกไปบนบัสจะมีหน้าตาแบบนี้:

ที่อยู่ 7 บิตของ I2C รองรับได้ 128 รูปแบบ (0–127) แต่ ที่อยู่ 0x00 ถูกจอง เป็น “General Call” ทำให้เหลือใช้จริง 127 Address

ถ้า Master ส่งไปที่ 0x00 คือการ broadcast ถึงทุกอุปกรณ์บนบัส แต่พฤติกรรมตอบสนองจะขึ้นกับการตั้งค่าของแต่ละ Slave

3. บิต Acknowledge (ACK)

หลังจากส่งไบต์ Address + R/W เสร็จ บิตที่ 9 ที่ตามมาคือ บิต ACK

  • ผู้รับ (Master หรือ Slave แล้วแต่เคส) จะ ดึง SDA ให้ Low เพื่อบอกว่า “รับแล้ว” (ACK)

  • ผู้ส่งต้องปล่อย SDA ให้ High เพื่อให้ฝั่งรับมาดึงลง

ถ้าไม่มีอุปกรณ์ตอบกลับ → เสมือนว่าไม่มีใครรับ Address นี้ การสื่อสารจะต้องหยุดหรือเริ่มใหม่ตามโค้ดควบคุม

รีจิสเตอร์ควบคุม I2C: I2CON บน N76E003

รีจิสเตอร์ I2CON คือหัวใจในการควบคุมพฤติกรรม I2C ของ MCU ประกอบด้วยบิตสำคัญหลายตัว เช่น:

  • I2CPX – เลือกชุดขา I2C (P1.3/P1.4 หรือ P0.2/P1.6)

  • AA – Acknowledge Assert Flag

    • ถ้าเซ็ต AA → จะส่ง ACK (ดึง SDA ลง) ในช่วงพัลส์ ACK ของ SCL

    • ถ้าล้าง AA → จะส่ง NACK (ปล่อย SDA สูง)

  • SI – I2C Interrupt Flag

    • บอกว่าเกิดเหตุการณ์ใหม่ใน I2C แล้ว เช่น ส่งไบต์เสร็จ รับไบต์เสร็จ เป็นต้น

    • เมื่อ SI = 1 ผู้ใช้ต้องไปอ่านรีจิสเตอร์ I2STAT เพื่อดูว่าอยู่ในสถานะไหน แล้วตัดสินใจขั้นต่อไป

  • STO – Stop Flag

    • เซ็ตเพื่อสั่งให้ I2C สร้าง Stop Condition

    • ฮาร์ดแวร์จะล้างบิตนี้เองเมื่อ Stop เกิดขึ้นจริงบนบัส

  • STA – Start Flag

    • เซ็ตเพื่อสั่งให้สร้าง Start หรือ Repeated Start เมื่อบัสว่าง

    • ต้องล้างเองจากซอฟต์แวร์หลังจาก Start เสร็จ

  • I2CEN – Enable I2C Bus

    • เปิด/ปิดการทำงานของ I2C ทั้งบัส

ทำความเข้าใจ EEPROM 24C02 บนบัส I2C

24C02 เป็น EEPROM แบบ I2C ขนาดเล็กที่ใช้ง่ายมาก เหมาะกับการทดลอง

ขาที่เกี่ยวข้อง:

  • A0, A1, A2 – ขาเลือกที่อยู่ (Hardware Address)

  • WP – Write Protect (ต่อ GND เพื่ออนุญาตให้เขียนได้)

ในตัวอย่างนี้ A0, A1, A2 ถูกต่อกับ VSS ทั้งหมด → ค่าทั้งสามเป็น 0 ทำให้ Address พื้นฐานของ EEPROM อยู่ที่แพทเทิร์น `1010 000`

รูปแบบการเขียน 1 ไบต์ (Byte Write) จะเป็นตามลำดับนี้:

ไบต์ควบคุม (Control Byte) มีโครงสร้าง:

  • 4 บิตบน = `1010` (ค่าคงที่ของ EEPROM Family)

  • 3 บิตถัดมา = A2, A1, A0 (ตามการต่อจริง)

  • บิตสุดท้าย = R/W

ในกรณีนี้ A0, A1, A2 = 0 ทั้งหมด ดังนั้น:

  • สำหรับเขียน → Control Byte = 0xA0

  • สำหรับอ่าน → Control Byte = 0xA1

ขั้นตอนการเขียนโดยย่อ:

  1. ส่ง Start

  2. ส่ง Control Byte (0xA0)

  3. ส่ง Address ภายใน EEPROM (1 ไบต์)

  4. ส่งข้อมูล 1 ไบต์ที่ต้องการเขียน

  5. ส่ง Stop

ไอเดียการเขียนโปรแกรม I2C บน N76E003

การตั้งค่าเริ่มต้น I2C และขา I/O

ในโค้ดจะมีฟังก์ชัน `I2C_init()` ที่ทำงานประมาณนี้:

  • ตั้งขา P1.3, P1.4 เป็น Open-Drain

  • เลือกใช้ขา P1.3, P1.4 เป็น SCL/SDA (ล้างบิต I2CPX)

  • เปิดใช้งาน I2C ผ่าน I2CEN

ภายในไฟล์ `I2C.h` และ `I2C.c` จะมีมาโครประเภท:

  • `P13_OpenDrain_Mode;`

  • `P14_OpenDrain_Mode;`

  • `clr_I2CPX;`

เพื่อทำงานตามนี้แบบอ่านง่าย

โครงสร้างฟังก์ชัน I2C พื้นฐาน

การใช้งาน I2C ถูกห่อหุ้มเป็นฟังก์ชันหลัก ๆ ดังนี้:

  • `I2C_start()` – สร้างเงื่อนไข Start

  • `I2C_stop()` – สร้างเงื่อนไข Stop

  • `I2C_write(value)` – ส่งไบต์หนึ่งไบต์ออกบน SDA

  • `I2C_read(ack_mode)` – อ่านไบต์หนึ่งไบต์จาก SDA พร้อมเลือกว่าจะส่ง ACK หรือ NACK กลับ

ในแต่ละฟังก์ชันจะใช้แนวคิด:

  • เซ็ต/เคลียร์บิต STA, STO, AA, SI ใน I2CON

  • รอให้บิต SI = 1 แปลว่าเหตุการณ์นั้นสำเร็จแล้ว

  • มีตัวแปร timeout เช่น `timeout_count = 1000` เพื่อกันโค้ดค้าง

ตัวอย่างโครงสร้าง:

  • `I2C_start()`

    • `set_STA;`

    • `clr_SI;`

    • รอจน SI = 1 หรือหมดเวลา

  • `I2C_stop()`

    • `clr_SI;`

    • `set_STO;`

    • รอจน STO กลับเป็น 0 หรือหมดเวลา

  • `I2C_write(value)`

    • ใส่ค่าใน `I2DAT`

    • `clr_STA;`

    • `clr_SI;`

    • รอ SI = 1 หรือ timeout

  • `I2C_read(ack_mode)`

    • เซ็ต AA ถ้าต้องการส่ง ACK, หรือเคลียร์ AA เพื่อตอบ NACK

    • `clr_SI;`

    • รอ SI = 1 แล้วอ่านค่าใน `I2DAT`

การเขียน/อ่าน EEPROM ด้วย I2C

จากมุมมองระดับสูง ฟังก์ชันของ EEPROM จะถูกห่อออกมาเป็น interface ง่าย ๆ:

  • `EEPROM_write(address, value)` – เขียนค่า 1 ไบต์ไปยัง address

  • `EEPROM_read(address)` – อ่านค่า 1 ไบต์จาก address เดียวกัน

ตัวอย่างโครงสร้าง `EEPROM_write`:

  • Start

  • Write Control Byte 0xA0 (เขียน)

  • Write Address ภายใน EEPROM

  • Write Data

  • Stop

ฟังก์ชันอ่านจะใช้ลำดับอ่านมาตรฐาน I2C ที่เกี่ยวข้องกับ EEPROM คือ:

  • Start

  • ส่ง Control Byte เขียน (0xA0)

  • ส่ง Address ที่ต้องการอ่าน

  • Repeated Start

  • ส่ง Control Byte อ่าน (0xA1)

  • อ่านข้อมูล 1 ไบต์ (พร้อมส่ง NACK)

  • Stop

ฟังก์ชันหลักและลูป while(1)

โครงร่างของ `main()` จะทำงานประมาณนี้:

  • ตั้งค่า UART ด้วย `InitialUART0_Timer3(115200);`

  • เซ็ต `TI = 1;` เพื่อให้ `printf` ใช้งานได้

  • เรียก `I2C_init();`

  • วนลูปไม่รู้จบ:
    • เขียนค่า `0x55` ไปยัง Address 1 ของ EEPROM

    • อ่านค่าจาก Address 1 กลับมาในตัวแปร `c`

    • ใช้ `printf("
      ค่าที่อ่านได้คือ %x", c & 0xff);` เพื่อแสดงค่าบน Serial Monitor ในรูปแบบ Hex

แนวคิดคือ ทดสอบทั้ง Write และ Read แบบวนซ้ำ เพื่อยืนยันว่าฮาร์ดแวร์และโค้ด I2C ทำงานถูกต้อง

ผลลัพธ์บน Serial Monitor

เมื่อคอมไพล์โค้ดด้วย Keil และแฟลชลงบอร์ดผ่าน Nu-Link จะได้ log การ build ที่เคลียร์ดี:

  • 0 Error

  • 0 Warning

  • ขนาดโค้ดราว ๆ 2.4 KB

เมื่อรันจริงต่อวงจรบน breadboard ค่าที่เราเขียนเข้า EEPROM จะถูกอ่านกลับมาและแสดงบน Serial Monitor ตามที่คาดหวัง

สรุปภาพรวม:

  • N76E003 มีฮาร์ดแวร์ I2C ในตัว ใช้งานสะดวกกว่าการ bit-banging

  • การต่อกับ EEPROM อย่าง 24C02/AT24LC64 ใช้แค่ SDA, SCL และตัวต้านทาน pull-up

  • โค้ดถูกแบ่งเลเยอร์ดี: ชั้น I2C low-level (start/stop/read/write) + ชั้น EEPROM API (EEPROM_read/EEPROM_write)

สรุปส่งท้าย: เอาไปต่อยอดกับ AI และโปรเจกต์จริง

ถ้าคุณเขียนโค้ดด้วย AI อยู่แล้ว โครงสร้างทั้งหมดนี้คือ prompt ชั้นดี สำหรับให้ AI Generate โค้ด I2C เพิ่มเติม เช่น:

  • เพิ่มฟังก์ชันอ่าน/เขียนหลายไบต์ (Page Write / Sequential Read)

  • ทำไลบรารี I2C Generic สำหรับเซ็นเซอร์ตัวอื่นที่ใช้ Address คนละตัว

  • อัปเกรดให้รองรับ error handling และ retry อัตโนมัติ

แกนหลักคือ เข้าใจ Start / Stop / Address / R/W / ACK ให้ชัด จากนั้นจะเขียนเอง หรือให้ AI เขียนให้ ก็จะตรวจและดีบักได้อย่างมั่นใจ เพราะเรารู้ว่าแต่ละบิตบนบัสกำลังบอกอะไรอยู่จริง ๆ