Publish : 21 August 2020
หัวข้อในบทความนี้
- ทบทวนคำสั่ง
- I2C Function ที่ใช้ได้ทั้งฝั่ง Master และฝั่ง Slave
- I2C Function ที่ใช้ในฝั่ง Master
- I2C Function ที่ใช้ในฝั่ง Slave
- Slave ส่งข้อมูลกลับไปให้ Master เมื่อร้องขอ
- ท้ายบท
หลังจากตอนล่าสุด (ที่แอบหายไป 1 สัปดาห์) เราได้เกริ่นและทดลองให้ Arduino สองตัวสื่อสารกันแบบ I2C แล้ว แต่ก็ยังเป็นตัวอย่างง่าย ๆ ที่ทำการส่งตัวแปรเล็ก ๆ และส่งจาก Master ไป Slave เพียงด้านเดียว วันนี้เราก็จะมาต่อกันครับ เพราะ I2C มันสามารถไปกลับได้นี่ Master เรียกข้อมูลจาก Slave ก็ได้ หรือ Slave จะไปขอความมั่นใจจาก Master ก็คงได้เหมือนกัน อย่าช้าเสียเวลา ไปกันเลยครับ แต่ !!! ก่อนจะไปกันต่อขออธิบายคำสั่งซ้ำกันอีกรอบแบบสั้น ๆ ก่อน ตอนที่แล้วอาจจะอธิบายแบบลุยไปเรื่อยตามหน้า Reference ของ Arduino หลายท่านอาจสับสน เดี๋ยวจะขอแยกเป็นกลุ่มดีกว่าครับ เวลาหยิบไปใช้งานจะได้ไม่มึน
I2C Function ที่ใช้ได้ทั้งฝั่ง Master และฝั่ง Slave Wire.write(value) ใช้ส่งข้อมูลออกไปยังตัวอื่น โดยข้อมูลที่ส่งออกไปได้คือ Byte, String และ Array เช่น Wire.write(1), Wire.write("Fitrox"), Wire.write(a, 7)
Wire.available() ใช้สำหรับตรวจสอบว่าข้อมูลต่าง ๆ ที่รับมาใช้งานได้หรือไม่ (มีข้อมูลหรือไม่) โดยจะส่งกลับค่าเป็นจำนวน Byte ที่อ่านได้ และนำข้อมูลไปเก็บไว้ใน Buffer พร้อมใช้งานต่อไป
Wire.read() อ่านค่าข้อมูลที่ได้รับมาอยู่ใน Buffer จำนวน 1 Byte และคืนค่าเป็น Byte ถัดไปที่รับเข้ามา (เราจึงใช้ Read หลาย ๆ บรรทัดต่อกันเพื่อรับข้อมูลแบบต่อเนื่องได้เลย)
I2C Function ที่ใช้ในฝั่ง Master
Wire.begin() แบบโล้น ๆ ในวงเล็บปล่อยว่าง เป็นการเชื่อมต่อเข้าไปใน I2C Bus ในฐานะ Master Device
Wire.beginTransmission(slave address) เป็นการส่งข้อมูลไปยัง Slave ตัวใดตัวหนึ่ง โดยต้องกำหนด Slave address ของ Slave ตัวนั้น เช่น Wire.beginTransmission(39), Wire.beginTransmission(0x27), Wire.beginTransmission(0b0100111) Wire.requestFrom(address, byte) หรือ Wire. requestFrom(address, byte, stop) ใช้เรียกข้อมูลจาก Slave ตัวที่กำหนด Address ไป เป็นจำนวน x Byte เช่น จาก Address 50 จำนวน 7 bytes จะได้ Wire.requestFrom(50, 7) ถ้ามี stop เป็น true จะส่งข้อความหยุดเพื่อปล่อยให้บัสว่าง หากเป็น false จะส่งข้อความ restart เพื่อคงสถานะเชื่อมต่อ(ค่า Default หรือไม่กำหนดจะเป็น true) เช่น Wire.requestFrom(50, 7, true)
Wire.endTransmission() สิ้นสุดการส่งข้อมูล
คำสั่งที่ใช้สำหรับการให้ Master "ส่ง" ข้อมูลอย่างง่าย - เปิด begin ต่อด้วย write และปิดด้วย end Wire.beginTransmission(slave address); Wire.write(x); Wire.endTransmission();
|
I2C Function ที่ใช้ในฝั่ง Slave
Wire.begin(address) ในวงเล็บใส่ Address ประจำตัว เป็นการเชื่อมต่อเข้าไปใน I2C Bus ในฐานะ Slave Device ด้วย Address ที่กำหนด
Wire.onReceive(handler) เรียกฟังก์ชัน handler ขึ้นมาทำงานเมื่อ Master ส่งข้อมูลมาให้ (เราต้องสร้างฟังก์ชัน handler แยก เป็นคำสั่งว่าจะทำงานอะไรบ้าง เช่น read ข้อมูล แล้วนำไป print เป็นต้น)
Wire.onRequest(handler) เรียกฟังก์ชัน handler ขึ้นมาทำงานเมื่อ Master เรียกเข้ามาเพื่อขอข้อมูล (เราต้องสร้างฟังก์ชัน handler แยก เป็นคำสั่งว่าจะทำงานอะไรบ้าง เช่น อ่านข้อมูลจากเซ็นเซอร์ แล้วนำ write เพื่อส่งกลับไปให้ Master เป็นต้น)
หวังว่าอธิบายใหม่จะเข้าใจไม่งงมากกว่าเดิมนะครับ เอาแบบที่ต้องใช้ประจำ อะไรไม่ได้ใช้ก็ไม่หยิบมาแล้ว เอาหล่ะ ทีนี้ได้เวลาไปเริ่มกันแล้วครับSlave ส่งข้อมูลกลับไปให้ Master เมื่อร้องขอ ครั้งที่แล้วเราก็ทดลอง onReceive ซึ่งเป็นการรับข้อมูลจาก Master แล้วนำออกไปใช้งาน ก็คงพอมองออกกันแล้วว่าหลักการมันเป็นแบบไหน คราวนี้เมื่อเราต้องการใช้งานให้ Slave ส่งกลับข้อมูลมาให้ Master หล่ะ คราวนี้ขี้เกียจต่อปุ่มกด ให้ delay แล้วเรียกข้อมูลทุก ๆ 5 วินาทีแทนละกัน เสมือนว่าเรากดปุ่มทุก ๆ 5 วินาที วิธีกรก็คือ...ใช่แล้วครับ เราต้องใช้ onRequest ไปลุยกันเลยครับ ผู้เขียนจะขอออกแบบการทดลองโดยให้ Master ต่ออยู่กับจอ LCD และ delay เรียกข้อมูลทุก ๆ 5 วินาที และ Slave ต่อกับ DHT เพื่อวัดอุณหภูมิ เมื่อ Master ทำการ delay จนถึงเวลาทำงานก็จะไปเรียกข้อมูลอุณหภูมิจาก Slave แล้วจึงส่งค่านำมาแสดงที่จอที่ต่อกับ Master โดยจะสังเกตุได้ว่า Master นั้นจะต่อกับอุปกรณ์ I2C ถึงสองตัว คือจอ LCD และ Arduino ที่เป็น Slave Device สำหรับจอ LCD นั้นเราจะใช้ Wire เขียนเพียว ๆ ก็ได้เหมือนกัน แต่เดี๋ยวจะงงเกินไป เอาทีละเรื่อง จึงจะขอใช้ Library LiquidCrystal_I2C สำเร็จรูปไปก่อน ส่วน Wire นี้จะใช้ติดต่อกับ Slave Device อย่างเดียว (ในโค้ดนี้ก็มีเรื่องให้งงนิดหน่อยแล้วหล่ะครับ ดูก่อนเดี๋ยวค่อยอธิบาย) โปรแกรมสำหรับ Master
โปรแกรมสำหรับ Slave
นั่นไงครับ พอดูโค้ดใครเขียน Arduino อาจไม่คุ้นเท่าไร แต่ถ้าใครรู้ C แบบดั้งเดิมก็อาจพอคุ้นอยู่ กับ toFloat() และ dtostrf() แล้วถามว่ามันมาได้ไง ก็คงจำกันได้นะครับว่า Wire.write นั้นสามารถส่งตัวแปรได้ 3 ชนิด คือ Byte, String และ Array ที่นี้ค่าอุณหภูมิที่เราอ่านออกมามันเป็น Float แน่นอนว่ามันส่งไปไม่ได้ครับ เราจึงต้องทำการแปลง Float -> String แล้วทำการส่งในรูป String ไป เมื่อข้อมูลถึงปลายทางก็แปลงกลับเป็น Float เหมือนเดิม คอนเซ็ปมีแค่นี้เองง่าย ๆ จ้า ทีนี้เรามาแวะดูซักนิดว่าสองอันนี้มันใช้ยังไง
dtostrf(intilize, width, prec, final); |
โดย intilize คือ ตัวแปรตั้งต้น(ค่า Float)ที่ต้องการนำมาแปลง width คือ ขนาดต่ำสุดของจำนวนเต็มรวมจุดทศนิยมและเครื่องหมายบวกลบ(ถ้ามี) เราจะให้ยาวน้อยที่สุดได้กี่ตัวนั่นเอง prec คือ จำนวนหลักหลังจุดทศนิยม final คือ ตัวแปร String ที่จะใช้เก็บข้อมูลหลังจากที่แปลงแล้ว ตัวอย่าง intilize = 1.2456 เราสั่ง dtostrf(intilize, 1, 4, final) ค่า String หลังจากแปลงแล้วจะเป็น "1.2456" เพราะ เรา width ไว้อย่างน้อย 1 หลัก แต่มันมีค่า 2 หลัก (1.) และ prec หลังทศนิยมเรากำหนด 4 มันก็มาครบทั้ง 4 ตัว ตัวอย่าง ถ้าเรา intilize = 541.42572456 dtostrf(intilize, 10, 3, final) ค่า String หลังจากแปลงแล้วจะเป็น " 541.426" เพราเรา width ไว้ว่าอย่างน้อย 10 แต่ข้อมูลมันมีแค่ 4 (541.) มันจึงเติมเว้นวรรคจนครบ 10 และ prec เรากำหนดไว้ 3 หลัก แต่ข้อมูลมีถึง 8 หลัก มันจึงตัดทอนเหลือเพียงแค่ 3 หลักตามที่เราสั่ง ส่วน toFloat() ก็ใช้ไปตรง ๆ ต่อท้ายตัวแปร String ที่ต้องการจะแปลงเลยครับ
ส่วนอีกจุดที่เปลี่ยนคือค่า Address ของ Slave ซึ่งในบทความครั้งก่อนเราจะใช้ 0x22 แต่ผู้เขียนเห็นว่ามันจะใกล้เคียงกับ Address ของจอ LCD ที่ส่วนมากจะเป็น 0x27 หรือ 0x3F จึงหนีมาใช้ Slave Address เป็น 0x53 ซะเลย (ที่จริงไม่มีผลอะไรหรอครับ จอ 0x27 ใครจะตั้ง Slave เป็น 0x28 ก็ได้ เปลี่ยนไม่ให้มันใกล้เคียงเพื่อป้องกันความสับสนเลยหนีมาไกล ๆ ซึ่งเจ้า 7bit Address สำหรับ I2C นี้มีให้เลือกใช้เป็นร้อยเลยครับ คือตั้งแต่ 0x07 ถึง 0x78 หยิบอันไหนมาซ้ำก็ได้ขอแค่ไม่ซ้ำกับตัวอื่นก็พอ โดยเฉพาะเซ็นเซอร์ จอ ที่ส่วนใหญ่จะตั้งค่า Address ไว้ที่ Hardware เราไม่สามารถเปลี่ยนได้ หรือเปลี่ยนก็ยาก บอร์ดเรากำหนดใน Software ได้ก็กำหนดเลี่ยงกันเอง)
เถลไถลไปซะย่อหน้านึง กลับมาเรื่องของเรา ทำการต่ออุปกรณ์เลยครับ
เริ่มการทำงาน Master ก็จะเรียกข้อมูลจาก Slave ทุก ๆ 5 วินาที มาแสดงที่จอ LCD แล้วหล่ะครับ
Tips : ใน UNO R3 ย้ำว่า R3 นะครับ สองขาบนสุดที่ไม่ได้สกรีนเพราะโดนขา ICSP บังอยู่ ตรงนั้นเป็นขา I2C อีกจุดที่สามารถใช้งานได้ครับผม |
ก็หวังว่าสองตอนจะพอเข้าใจและนำไปใช้งานกันได้นะครับ การใช้งานก็จะวนอยู่กับคำสั่ง write, read, onRequest, onReceive, requestFrom แค่นี้ไม่ยุ่งยาก ซึ่ง Library ต่าง ๆ หากไปเปิดดูไฟล์ก็จะพบว่าเขียนขึ้นด้วยคำสั่งพวกนี้เช่นกัน ฉะนั้นไม่มีอะไรยากครับต้องลองพลิกไปพลิกมาดู และขอให้การบ้านคือ กดปุ่มที่ Master -> รีเลย์ที่ Slave เปิดปิด (กดเปิด กดอีกทีปิด) -> Slave ทำงานแล้วส่งสถานะกลับมาที่ Master -> Master แสดงค่าสถานะของรีเลย์ช่องต่าง ๆ ทางจอ LCD น่าจะไม่ยากนะครับลองเล่นกันดู โอกาสหน้า(หากไม่มีบทความจะลง) อาจหยิบโจทย์นี้ขึ้นมาเฉลยก็ได้ หวังว่าทุกคนจะสนุกกับการลองเขียนโปรแกรม ผิดบ้างถูกบ้างแก้ไปแก้มาสนุกดีครับ สำหรับวันนี้ลาไปก่อนแล้วสวัสดี
ท้ายบท
ทำไม 7bit Address ที่ใช้ได้จึงต้องมีค่าระหว่าง 0x07 ถึง 0x78 ทั้งที่ควรเป็น 0x00 ถึง 0x7F ?????
ตอบ - ถูกต้องแล้วครับ 7bit Address มันควรเป็น 000 0000 ถึง 111 1111 แต่ด้วยมีการสำรอง 8 Address แรก และ 8 Address สุดท้ายไว้ในการทำงานของระบบ จึงทำให้ต้องตัดค่าเหล่านั้นออกไปจากการใช้งาน โดยค่าที่สำรองไว้มีดังนี้
Address (binary) |
R/W bit |
Descriptions |
000 0000 |
0 |
General Call Address |
000 0000 |
1 |
START byte |
000 0001 |
X |
CBUS Address |
000 0010 |
X |
Reserved for different bus format |
000 0011 |
X |
Reserved for future purposes |
000 01XX |
X |
Hs-mode master code |
111 10XX |
X |
10-bit slave addressing |
111 11XX |
X |
Reserved for future purposes |
และเหตุที่ค่าปกติของ Address ต้องเป็น 7bit ทำไมไม่ 8bit จะได้เท่ากับ 1 Byte ไปเลย นั่นก็เพราะว่าจะต้องมี Read/Write bit อีก 1 เมื่อมารวมกับ 7bit Address แล้วก็จะครบ 8bit = 1 Byte พอดี