ภาพรวม: ทำไมต้อง 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
ขั้นตอนการเขียนโดยย่อ:
ส่ง Start
ส่ง Control Byte (0xA0)
ส่ง Address ภายใน EEPROM (1 ไบต์)
ส่งข้อมูล 1 ไบต์ที่ต้องการเขียน
ส่ง 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 เขียนให้ ก็จะตรวจและดีบักได้อย่างมั่นใจ เพราะเรารู้ว่าแต่ละบิตบนบัสกำลังบอกอะไรอยู่จริง ๆ

