บทความการพัฒนาโปรแกรมบน Raspberry Pi ด้วย Qt ตอนที่ 7

บทความการพัฒนาโปรแกรมบน Raspberry Pi ด้วย Qt
ตอนที่ 7 ตัวอย่างการเขียนโปรแกรมรับ-ส่งข้อมูลผ่านทางบัส I2C

099

โดยเนื้อหาหลักๆ จะประกอบด้วยหัวข้อดังนี้

  I2C เป็นการสื่อสารแบบอนุกรมแบบ Synchronous ด้วยสายสัญญาณเพียง 2 เส้น คือ สายสัญญาณข้อมูล SDA (Serial Data Line) และสายสัญญาณนาฬิกา SCL (Serial Clock Line) โดย I2C แบ่งการทำงานออกเป็น 4 โหมดตามความเร็วในการรับส่งข้อมูลดังนี้
        1. Normal Mode: 100Kbps
        2. Fast Mode: 400Kbps
        3. Fast Mode Plus: 1Mbps
        4. High Speed Mode: 3.4 Mbps
ตัวอย่างการต่อใช้งาน I2C

094

จากภาพแสดงให้เห็นว่าในการใช้งาน I2C สามารถต่อใช้งานกับอุปกรณ์ได้มากกว่าหนึ่งตัวบนสายสัญญาณ 1 ชุด (2 เส้น) โดยเลือกติดต่อกับอุปกรณ์ใดได้ด้วยการกำหนดแอดเดรสทางฮาร์ดแวร์ให้กับอุปกรณ์ตัวนั้นและต้องมีการ Pull-Up ให้กับสายสัญญาณทั้ง 2 เส้น 

รูปแบบสถานะในการรับ-ส่งข้อมูล

095

 Start เป็นสถานะที่บอกเริ่มต้นการรับ-ส่งข้อมูล โดยการเปลี่ยนสัญญาณของ SDA จาก High ไปเป็น Low โดยที่ SCL ยังคงเป็น High อยู่
       Control Byte ประกอบไปด้วย 3 ส่วน คือ 
       – ID ของอุปกรณ์จำนวน 4 บิต ซึ่งถูกกำหนดมาโดยผู้ผลิต IC หรืออุปกรณ์ I2C
       – Device Address ขนาด 3 บิต สามารถกำหนดได้เองจากการจ่าย Logic หรือต่อขาให้กับ IC
       – Mode ขนาด 1 บิต ใช้กำหนดว่าเป็นการ Read หรือ Write Data กับอุปกรณ์ IC
       ACK หรือ Acknowledge เป็นบิตที่ใช้บอกว่า IC มีการตอบสนองต่อคำสั่งที่ได้รับมาแล้ว
       DATA คือ ข้อมูลที่ต้องการเขียนหรืออ่านออกมาจาก IC ขึ้นกับ Mode ที่เราได้ตั้งค่าเอาไว้
       STOP เป็นสถานะที่บอกให้อุปกรณ์รู้ว่าสิ้นสุดการรับส่งข้อมูลแล้ว โดย SDA จะเปลี่ยนจาก Low เป็น High ในขณะที่ SCL ยังเป็น High อยู่

096

แนะนำโมดูล PCF8591 (EFDV221 PCF8591 Module)
            PCF8591 เป็นชิพที่ใช้สำหรับแปลงสัญญาณ Analog to Digital และ Digital to Analog ทั้ง 2 ทางขนาด 8-bit สั่งงานผ่าน I2C มีรูปแบบการใช้งานที่ไม่ซับซ้อนมากนัก เนื่องจากบนบอร์ด Raspberry Pi ไม่มีขาสัญญาณสำหรับอ่านค่า Analog to Digital โมดูลตัวนี้จึงเหมาะที่จะนำมาทดแทนความสามารถที่ขาดไปนี้ได้ นอกจากนี้บนตัวโมดูลยังมีอุปกรณ์อื่นๆ เช่น Thermister LDR และ Potentiometer ซึ่งให้เอาท์พุตเป็นค่า Analog ทำให้สามารถทดลองอ่านค่าเหล่านี้ได้โดยไม่ต้องไปหาอุปกรณ์เพิ่มเติม

097

ตำแหน่งของ Sensor บน Module

วงจรของโมดูล PCF8591

098

วิธีตั้งค่าจัมพ์เปอร์กับอุปกรณ์บนบอร์ด

099

อุปกรณ์บนบอร์ดสามารถเลือกเชื่อมต่อได้ด้วยการต่อจัมพ์เปอร์ ดังนี้ 
       – P4 ใช้ต่อ Thermistor เข้ากับช่อง AIN1
       – P5 ใช้ต่อ LDR เข้ากับช่อง AIN0
       – P6 ใช้ต่อ Potentiometer เข้ากับช่อง AIN3
ดังนั้น หากต้องการอ่านค่าจากภายนอกที่ช่อง AIN0 AIN1 และ AIN2 ให้ถอด P4 P5 และ P6 ออก

0100

ค่า Device Address ของโมดูล PCF8591
         จากแผนผังวงจรจะเห็นว่าขา A0 A1 และ A2 ของ PCF8591 ต่อลง GND เอาไว้ทั้ง 3 ขา ทำให้ค่าในบิต Device Address ของชิพ คือ 0 ทั้ง 3 บิต และ Datasheet ของ PCF8591 ได้กำหนด ID มาจากผู้ผลิตคือ 0b1001

0101

ดังนั้น Slave Address ของโมดูลถูกกำหนดตายตัวไว้ที่ 0b100100 (0x48 ที่ 7 บิต)
                – หากกำหนดสถานะ Write Data ไปยังโมดูล บิตที่ 0 (LSB) จะถูกกำหนดเป็น 0 ทำให้ Slave address ทั้ง 8 บิต มีค่าเป็น                 0b1001000 (0x90)
                – หากกำหนดสถานะ Read Data จากโมดูล บิตที่ 0 (LSB) จะถูกกำหนดเป็น 1 ทำให้ Slave address ทั้ง 8 บิต มีค่าเป็น                   0b1001001 (0x91)
Bus Protocol สำหรับอ่านและเขียนข้อมูลของ PCF 8591
– Bus Protocol สำหรับ Write Data

0102

  o S = เริ่มติดต่อ I2C
          o Address = Slave Address ที่กล่าวถึงในหัวข้อก่อนหน้า สำหรับโมดูลนี้คือ 0x48(7Bits) จากรูปจะเห็นว่าบิตที่ 0 (LSB) ถูกกำหนดให้เป็นลอจิก 0 คือ Write Data เมื่อรวมกับ 0x48 แล้วจึงมีค่า = 0x90 ตามที่อธิบายไปแล้วข้างต้น
          o A = Acknowledge ที่ PCF8591 ตอบรับ Slave Address ที่เราส่งไป
          o Control Byte = ไบต์ที่ใช้ควบคุมการทำงานของ PCF8591 ซึ่งมีรูปแบบการกำหนดดังนี้

0103

  บิตที่ 0 และ 1 สำหรับกำหนดช่องของ A/D ที่ต้องการ
              บิตที่ 2 กำหนด AUTO-INCREMENT FLAG
              บิตที่ 3 กำหนดเป็น 0 เสมอ
              บิตที่ 4 และ 5 สำหรับเลือกรูปแบบ Analog Input
              Bit ที่ 6 สำหรับเปิดใช้งาน Analog Output
              Bit ที่ 7 กำหนดเป็น 0 เสมอ
          o Data Byte คือ ข้อมูลที่เราต้องการส่งไปให้โมดูล 
          o P/S หมายถึง Stop หรือ Start

0104

– Bus Protocol สำหรับ Read mode

0105

    o S = เริ่มติดต่อ I2C
          o Address = Slave Address ที่กล่าวถึงในหัวข้อก่อนหน้า สำหรับโมดูลนี้คือ 0x48(7Bits) จากรูปจะเห็นว่าบิตที่ 0 (LSB) ถูกกำหนดให้เป็นลอจิก 1 คือ Read Data เมื่อรวมกับ 0x48 แล้วจึงมีค่า = 0x91 ตามที่อธิบายไปแล้วข้างต้น
          o A = Acknowledge ที่ PCF8591 ตอบรับ Slave Address ที่เราส่งไป
          o Data Byte = ข้อมูลที่โมดูลส่งกลับออกมา 
          o P หมายถึง Stop 
การต่อ Module PCF8591 เข้ากับ Board Raspberry Pi

0106

ทดสอบ I2C Driver
           ในขั้นตอนนี้เป็นการทดสอบว่าโมดูลหรืออุปกรณ์ที่สื่อสารผ่าน I2C ที่นำมาเชื่อมต่อกับ Raspberry Pi สามารถติดต่อสื่อสารกันได้จริงโดยสามารถลิสต์ Slave Address ของโมดูลที่ต่ออยู่บนบัส I2C มาแสดงได้ 
– เปิดโปรแกรม LXTerminal 
– โหลด Linux Kernel I2C Module พิมพ์คำสั่ง gpio load i2c

0107

 คำสั่ง gpio i2c load สามารถใส่พารามิเตอร์กำหนดความเร็วในการรับ-ส่งข้อมูลเพิ่มเติมได้ เช่น gpio i2c load 1000 หมายถึงกำหนดให้รับ-ส่งด้วยความเร็ว 1000 Kbps
– ลิสต์ Slave address ของโมดูลที่ต่ออยู่บนบัส I2C มาแสดง พิมพ์คำสั่ง gpio i2cd

0108

– จากรูปผลการแสดง Slave address จะเห็นว่ามีโมดูลที่ต่ออยู่บนบัส I2C ที่มี Address = 0x48 นั่นก็คือ Address ของ PCF8591 ตามที่ได้อธิบายไว้ในหัวข้อ ค่า Device Address ของโมดูล PCF8591
*** ก่อนใช้งาน I2C ทุกครั้งต้องแน่ใจว่ามีการโหลด Linux Kernel I2C Module แล้วจึงจะใช้งานได้
ทดลองเขียนโปรแกรมติดต่อกับ Module PCF8591 ผ่าน I2C
– เปิดโปรแกรม LXTerminal แล้วเปิดโปรแกรม Qt Creator พิมพ์คำสั่ง sudo qtcreator

0109

– สร้าง Project ใหม่ชื่อ PCF8951 และเปิดไฟล์ PCF8951.pro
– เพิ่มโค้ดเรียกใช้ไลบรารี่ WiringPi ลงไปดังนี้ 
LIBS += -L/usr/local/lib -lwiringPi -lwiringPiDev
INCLUDEPATH += /usr/local/include
– ออกแบบหน้าตาโปรแกรมให้มี Widget ดังนี้ 
           o Pushbutton 2 อัน โดยแสดงข้อความบน Button ดังนี้
                “Start”
                “Stop”
คลิกขวาที่ PushButton ทีละอัน แล้วสร้าง Slot เลือก Signal = clicked ให้ครบทั้ง 2 อัน

0110

o LCD Number
           o Label

0111

ลักษณะหน้าตาโปรแกรมที่ให้ออกแบบ

– ไปที่ไฟล์ mainwindows.cpp เพิ่มไฟล์ include และประกาศตัวแปล
#include <wiringPi.h>
#include <wiringPiI2C.h>
#include <QTimer>
int pcf_add = 0x48;
int fd;
Qtimer *timer;

0112

– เพิ่มโค้ดลงในฟังก์ชั่น MainWindow::MainWindow(QWidget *parent):QmainWindow (parent),ui(new Ui::MainWindow) ดังนี้

0113

 บรรทัดที่ 17 เปิดใช้งาน I2C ด้วยฟังก์ชั่น wiringPiI2CSetup (int devId) โดยส่งค่า Slave Address (0x48) ให้กับฟังก์ชั่นแล้ว     ตรวจสอบการเปิดใช้งาน
           บรรทัดที่ 18 หากไม่สามารถเปิดใช้งาน I2C ได้ ให้แสดงข้อความ “Unable to open I2C Device” ที่ label
           บรรทัดที่ 23 หากเปิดใช้งานสำเร็จให้แสดง “Open I2C Device” ใน Text Labelบรรทัดที่ 24 สืบทอดคลาส Qtimer โดยตั้งชื่อว่า timer
           บรรทัดที่ 25 สร้าง Signal กับ Slot ให้ timer โดยกำหนดให้เมื่อครบเวลาตามกำหนด timeout() จะเกิด Signal ให้เข้าไปทำงานใน Slot และสร้างฟังก์ชั่น interval() มารองรับ เมื่อครบเวลาตามกำหนดจะกระโดดเข้ามาทำงานในฟังก์ชั่น read_ADC() ทุกครั้ง
– สร้างฟังก์ชั่น read_ADC() แล้วเขียนโค้ดการทำงานดังนี้

0114

   บรรทัดที่ 38 ประกาศตัวแปร int adc;
           บรรทัดที่ 39 ส่ง Control Byte ออกไปสั่ง PCF8591 โดยในการทดลองนี้ต้องการอ่านค่า Potentiometer บนโมดูลซึ่งต่ออยู่ที่ช่อง   AIN3 กำหนด Control Byte ให้มีค่าเท่ากับ 0x43

0115

      ซึ่งมีความหมายดังนี้ 
                                o บิตที่ 0 และ 1 กำหนดให้อ่าน ADC จาก AIN3
                                o บิตที่ 2 ปิด Auto Increment
                                o บิตที่ 3 เป็นลอจิก 0 เสมอ
                                o บิตที่ 4 และ 5 กำหนดโหมดการใช้งาน Analog Input แบบ Four Single-end
                                o บิตที่ 6 เปิดใช้ Analog Output
                                o บิตที่ 7 เป็นลอจิก 0 เสมอ
           บรรทัดที่ 40 อ่านค่า ADC ครั้งแรก (ไม่ได้ใช้งาน)
           บรรทัดที่ 41 อ่านค่า ADC เก็บในตัวแปร int adc;
           บรรทัดที่ 42 นำค่า ADC ที่อ่านได้มาคำนวณกลับเป็นแรงดัน (Voltage)
           บรรทัดที่ 43 นำค่าที่คำนวณเป็นแรงดันแล้วไปแสดงใน LCD Number
– ไปที่ไฟล์ mainwindows.h แล้วเพิ่มชื่อฟังก์ชั่น void read_ADC(); ไว้ภายใต้ private slots:

0116

– กลับมาที่ไฟล์ mainwindows.cpp
– เพิ่มโค้ดลงในฟังก์ชั่น void MainWindow::on_pushButton_clicked() ดังนี้

0117

  บรรทัดที่ 48 สั่งให้ Qtimer ทำงานมี Interval ทุกๆ 10ms
– เพิ่มโค้ดลงในฟังก์ชั่น void MainWindow::on_pushButton_2_clicked() ดังนี้

0118

    บรรทัดที่ 54 ปิด Qtimer
– ก่อนทดลอง Run โปรแกรมเพื่อให้แน่ใจว่าโหลด Linux Kernel I2C Module แล้วให้สั่งโหลด Linux Kernel I2C Module อีกครั้งโดยเปิดโปรแกรม LXTerminal พิมพ์คำสั่ง gpio load i2c

0119

– ทดลอง Run โปรแกรม
         o คลิกที่ปุ่ม Start แล้วหมุน Potentiometer บนโมดูล PCF8591 ดูการเปลี่ยนแปลง

0120
0121

แนะนำการใช้งาน QProcess 
           จากการทดลองจะเห็นว่าก่อนการใช้งาน I2C ของ WiringPi เมื่อเปิดบอร์ด Raspberry Pi ขึ้นมาใหม่ทุกครั้งจำเป็นจะต้องโหลด Linux Kernel I2C Module ขึ้นมาก่อนจึงจะใช้งาน I2C ได้ จึงอยากแนะนำคลาส QProcess ใน Qt ซึ่งใช้สำหรับเปิดโปรแกรมและติดต่อสื่อสารกับโปรแกรมภายนอกได้ 
ตัวอย่างการใช้งาน
           ในตัวอย่างการใช้งาน QProcess นี้จะขออ้างอิงตัวอย่างการใช้งานโมดูล PCF8591 ก่อนหน้าดังนี้
– เพิ่ม #include<Qprocess> และ #include<qdebug.h>

0122

– แก้ไขโค้ดเพิ่มเติมในฟังก์ชั่น MainWindow::MainWindow(QWidget *parent):Qmain Window(parent),ui(new Ui::MainWindow) ดังนี้

0123

     บรรทัดที่ 20 สืบทอดคลาส QProcress
           บรรทัดที่ 21 โหลด Linux Kernel I2C Module ด้วยคำสั่ง gpio load i2c
           บรรทัดที่ 22 รอจนคำสั่งที่เรียก Process ทำงานเสร็จ
           บรรทัดที่ 24 ส่งคำสั่งไปลิสต์ Slave Address ของโมดูลที่ต่ออยู่บนบัส I2C
           บรรทัดที่ 25 รอจนคำสั่งที่เรียก Process ทำงานเสร็จ
           บรรทัดที่ 27 อ่านค่าตอบกลับจากการสั่งงานผ่าน QProcess นำมาเก็บในตัวแปร QByteArray byteArray
           บรรทัดที่ 28 แปลง byteArray ที่เป็น QByteArray มาเก็บในตัวแปร QStringList โดยแบ่งเป็นบรรทัด
           บรรทัดที่ 29 วนรอบอ่านข้อความจาก strline มาเก็บในตัวแปน Qstring line 
           บรรทัดที่ 31 แสดงผลข้อความออกทาง qDebug
– ทดลอง Run โปรแกรม ในส่วนเอาท์พุตจะแสดงผลการโหลดและลิสต์ I2C แล้วรันโปรแกรมขึ้นมา

0124

PCF8591 Extention Module (EFDV221 PCF8591 Module)
            WiringPi ได้สร้าง Extention Module เอาไว้แล้วสำหรับ PCF8591 เพื่อเป็น ADC และ DAC ให้กับ Raspberry Pi ช่วยใช้งานได้ง่ายขึ้น มีชุดคำสั่งที่สามารถควบคุม Analog Output และ Analog Input ได้ด้วยชุดคำสั่งที่สั้นและใช้งานง่าย
ตัวอย่างการใช้งาน
– ต่อโมดูล PCF8591 เข้ากับบอร์ด Raspberry Pi 
– ถอดจัมพ์เปอร์ P6 ออก
– บนโมดูล PCF8591 ต่อสายที่ AOUT เข้ากับ AIN3

0125

– การทดลองนี้เป็นการสั่งงาน Analog Out ต่อกลับเข้ามาที่ Analog Input เพื่ออ่านค่า Analog
– เปิดโปรแกรม LXTerminal แล้วเปิดโปรแกรม Qt Creator ด้วยคำสั่ง sudo qtcreator
– สร้าง Project ใหม่ชื่อ PCF8591_Ext
– เปิดไฟล์ PCF8591_Ext.pro
– เพิ่มโค้ดเรียกใช้ไลบรารี่ WiringPi ลงไปดังนี้ 
LIBS += -L/usr/local/lib -lwiringPi -lwiringPiDev
INCLUDEPATH += /usr/local/include
– ออกแบบหน้าตาโปรแกรมโดยมี Widget ดังนี้ 
      o Pushbutton โดยแสดงข้อความบน Button “Set Output” แล้วคลิกขวาที่ PushButton แล้วสร้าง Slot เลือก Signal = clicked 
      o LCD Number
      o Line Edit
– ไปที่ไฟล์ mainwindows.cpp
– เพิ่มโค้ดเรียกใช้ไลบรารี่และประกาศตัวแปลดังนี้
#include <wiringPi.h>;
#include <pcf8591.h>;
#include <QTimer>;
#include <QProcess>;
Qtimer *timer;
int input_base = 100;
int i2caddress = 0x48;

0126

– เพิ่มโค้ดลงในฟังก์ชั่น MainWindow::MainWindow(QWidget *parent) :QmainWindow (parent),ui(new Ui::MainWindow) ดังนี้

0127

   บรรทัดที่ 18 สืบทอดคลาส QProcress
             บรรทัดที่ 19 โหลด Linux Kernel I2C Module ด้วยคำสั่ง gpio load i2c
             บรรทัดที่ 20 รอจน Process คำสั่งเสร็จ
             บรรทัดที่ 21 เริ่มต้นใช้งาน PCF 8591 ด้วยฟังก์ชั่น pcf8591Setup (int pinBase, int i2cAddress) 
                              o int pinBase คือค่า pinbase ซึ่งเราสามารถกำหนดเป็นค่าอะไรก็ได้ แต่ต้องเป็นค่าที่มากกว่า 64 (ในที่นี้กำหนด                                      เป็น 100)
                              o int i2cAddress คือค่า Slave address
             บรรทัดที่ 22 สืบทอดคลาส QTimer
             บรรทัดที่ 23 สร้าง Signal กับ Slot ให้ timer โดยกำหนดให้เมื่อครบเวลาตามกำหนด timeout() จะเกิด Signal ให้เข้าไปทำงาน   ใน Slot และสร้างฟังก์ชั่น interval() มารองรับ เมื่อครบเวลาตามกำหนดจะกระโดดเข้ามาทำงานในฟังก์ชั่น read_ADC() ทุกครั้ง
             บรรทัดที่ 24 สั่งให้ Qtimer ทำงาน interval ทุกๆ 10ms
– สร้างฟังก์ชั่น read_ADC() และเขียนโค้ดการทำงานดังนี้

0128

บรรทัดที่ 33 อ่านค่า Analog ด้วยฟังก์ชั่น analogRead(int pin) โดยค่า Pin จะอ้างอิงจากค่า int pinBase จากฟังก์ชั่น                                             pcf8591Setup ดังนี้
                              o pinBase = AIN0
                              o pinBase+1 = AIN1
              &n