SPI แบ่งอุปกรณ์ออกเป็น 2 ฝั่ง คือ Master เป็นตัวควบคุมการรับส่งข้อมูลโดยในที่นี้คือไมโครคอนโทรลเลอร์ กับ Slave เป็นอุปกรณ์ที่รอรับคำสั่งจาก Master โดย Slave มีได้มากกว่า 1 ตัว
SPI ใช้สายสัญญาณทั้งหมด 4 เส้นดังนี้
1. MOSI (Master Out Slave In) Master -> Slave Shared
2. MISO (Master In Slave Out) Slave -> Master Shared
3. SCLK (Clock) Master -> Slave Shared
4. CS (Chip Select) Master -> Slave Not Shared
รูปแบบสัญญาณใน SPI BUS
รูปแบบสัญญาณ SPI มี 4 รูปแบบ แตกต่างกันที่ขอบสัญญาณนาฬิกา (Clock Polarity) และเฟส (Phase)
- เมื่อ CPHA=0 และ CPOL=0 สัญญาณนาฬิกา (Clock) ในสถานะปกติจะเป็น Low และจะรับ-ส่งข้อมูลที่ขอบขาขึ้นของสัญญาณนาฬิกา (Rising Edge Clock)
- เมื่อ CPHA=0 และ CPOL=1 สัญญาณนาฬิกา (Clock) ในสถานะปกติจะเป็น High และจะรับ-ส่งข้อมูลที่ขอบขาลงของสัญญาณนาฬิกา (Falling Edge Clock)
- เมื่อ CPHA=1 และ CPOL=0 สัญญาณนาฬิกา (Clock) ในสถานะปกติจะเป็น Low และจะรับ-ส่งข้อมูลที่ขอบขาลงของสัญญาณนาฬิกา (Falling Edge Clock)
- เมื่อ CPHA=1 และ CPOL=1 สัญญาณนาฬิกา (Clock) ในสถานะปกติจะเป็น High และจะรับ-ส่งข้อมูลที่ขอบขาขึ้นของสัญญาณนาฬิกา (Rising Edge Clock)
ดังนั้น จึงกำหนดเป็น Mode การทำงานได้ 4 โหมด คือ
o Mode 0 = CPOL=0 และ CPHA=0
o Mode 1 = CPOL=0 และ CPHA=1
o Mode 2 = CPOL=1 และ CPHA=0
o Mode 3 = CPOL=1 และ CPHA=1
ทดสอบใช้งาน SPI Driver บนบอร์ด Raspberry Pi
ในการทดสอบนี้เป็นการทดสอบรับ-ส่งข้อมูลผ่าน SPI โดย Loopback ส่งข้อมูลออกจากขา MOSI และรับข้อมูลเข้ามาทางขา MISO
- เปิดโปรแกรม LXTerminal
- ดาวน์โหลดซอร์สโค้ดโปรแกรมทดสอบ Loopback พิมพ์คำสั่ง
wget https://raw.githubusercontent.com/raspberrypi/linux/rpi-3.10.y /Documentation /spi/spidev_test.c
- เมื่อ CPHA=0 และ CPOL=1 สัญญาณนาฬิกา (Clock) ในสถานะปกติจะเป็น High และจะรับ-ส่งข้อมูลที่ขอบขาลงของสัญญาณนาฬิกา (Falling Edge Clock)
- เมื่อ CPHA=1 และ CPOL=0 สัญญาณนาฬิกา (Clock) ในสถานะปกติจะเป็น Low และจะรับ-ส่งข้อมูลที่ขอบขาลงของสัญญาณนาฬิกา (Falling Edge Clock)
- เมื่อ CPHA=1 และ CPOL=1 สัญญาณนาฬิกา (Clock) ในสถานะปกติจะเป็น High และจะรับ-ส่งข้อมูลที่ขอบขาขึ้นของสัญญาณนาฬิกา (Rising Edge Clock)
ดังนั้น จึงกำหนดเป็น Mode การทำงานได้ 4 โหมด คือ
o Mode 0 = CPOL=0 และ CPHA=0
o Mode 1 = CPOL=0 และ CPHA=1
o Mode 2 = CPOL=1 และ CPHA=0
o Mode 3 = CPOL=1 และ CPHA=1
ทดสอบใช้งาน SPI Driver บนบอร์ด Raspberry Pi
ในการทดสอบนี้เป็นการทดสอบรับ-ส่งข้อมูลผ่าน SPI โดย Loopback ส่งข้อมูลออกจากขา MOSI และรับข้อมูลเข้ามาทางขา MISO
- เปิดโปรแกรม LXTerminal
- ดาวน์โหลดซอร์สโค้ดโปรแกรมทดสอบ Loopback พิมพ์คำสั่ง
wget https://raw.githubusercontent.com/raspberrypi/linux/rpi-3.10.y /Documentation /spi/spidev_test.c
- คอมไพล์โปรแกรม พิมพ์คำสั่ง gcc -o spidev_test spidev_test.c
- ทดสอบ Run โปรแกรม พิมพ์คำสั่ง ./spidev_test -D /dev/spidev0.0
โปรแกรมจะแสดงรายละเอียดที่ตั้งค่า SPI และแสดงข้อมูลที่ได้รับจากขา MOSI จากรูปพบว่า Data ทั้งหมดเป็น 0 ทั้งหมดหรืออาจเป็น FF ทั้งหมดเนื่องจากขาของ MOSI ยังไม่ได้ต่อกับอุปกรณ์ใดๆ
- ต่อสัญญาณขา MOSI เข้ากับขา MISO
- ทดสอบ Run โปรแกรมใหม่อีกครั้ง พิมพ์คำสั่ง ./spidev_test -D /dev/spidev0.0
จะมีข้อมูลที่อ่านได้จากขา MOSI ซึ่งตรงกับที่กำหนดไว้ในโค้ดโปรแกรม
-
ในโปรแกรมตัวอย่างนี้ สามารถทดสอบกำหนดตั้งค่าต่างๆให้กับ SPI
ได้โดยกำหนดพารามิเตอร์ซึ่งสามารถดูเมนูวิธีการตั้งค่าโดย พิมพ์คำสั่ง
./spidev_test -x (โดย x คือ ตัวอักษรอะไรก็ได้ที่ไม่มีอยู่ในลิสต์คำสั่ง)
- ทดสอบงาน SPI0 กำหนด Max Speed เป็น 1 MHz เปลี่ยน Mode SPI เป็นโหมด 3 พิมพ์คำสั่ง ./spidev_test -D /dev/spidev0.0 –s 1000000 – H –O
ทดลองใช้งาน SPI กับไลบรารี่ WiringPi บน Qt
ในการทดลองนี้เป็นการนำโมดูล ADXL345 Digital Accelerometer (ESEN067 Triple Axis Accelerometer Breakout - ADXL345) สำหรับวัดค่าความเร่งมาทดลองเขียนโปรแกรมผ่าน SPI ไปสั่งงานและอ่านค่าความเร่งจาก ADXL345
ก่อนทำการทดลองมาทำความรู้จัก ADXL345 สักเล็กน้อยเพื่อให้เข้าใจความหมายของข้อมูลที่รับส่งกับตัวโมดูล โดยเราสามารถติดต่อสื่อสารกับ ADXL345 ได้ 2 รูปแบบ คือ SPI กับ I2C แต่ในการทดลองนี้เลือกใช้แบบ SPI โดยในคู่มือของ ADXL345 ระบุว่าสามารถใช้สัญญาณนาฬิกาเพื่อรับ-ส่งข้อมูลแบบ SPI ได้สูงสุด 5 MHz และใช้ CPOL =1 และ CPHA = 1 คือ Mode 3 นั่นเอง
รูปแบบ Protocol ที่ใช้ใน ADXL 345
เราสามารถสั่งงาน ADXL345 ได้ โดยการระบุตำแหน่งของรีจิสเตอร์ภายในที่เราต้องการเข้าไปเขียนหรืออ่าน ข้อมูลออกมาโดยส่งไปทางขา MOSI โดยข้อมูลไบต์แรกใช้เพื่อระบุว่าเป็นการอ่านหรือเขียนและตำแหน่งที่ต้องการ เข้าถึง หากเป็นการเขียนข้อมูลลงไปในตำแหน่งนั้นไบต์ต่อไปที่ส่งจะเป็นข้อมูลที่ต้อง การเขียน แต่หากเป็นการอ่านข้อมูล ไบต์ของข้อมูลจะถูกส่งออกจากทางขา MISO
ความหมายของข้อมูลไบต์แรกที่ถูกส่งออกไปเพื่อควบคุมการอ่านหรือเขียนและกำหนดตำแหน่ง
- บิตที่ 0 ถึงบิตที่ 5 (A0 - A5) คือ บิตที่ใช้ระบุตำแหน่งรีจิสเตอร์ของ ADXL345 ซึ่งมีขนาด 5 บิต ตั้งแต่ตำแหน่งที่ 0x00 จนถึง 0x39
- บิตที่ 6 คือ MB (Multiple Byte) ให้กำหนดเป็น 1 (High) เมื่อต้องการรับ-ส่งข้อมูลหลายๆ ไบต์ในการรับ-ส่งครั้งเดียว
- บิตที่ 7 (MSB) คือ R/W (Read/Write) หากกำหนดเป็น 0 หมายถึงต้องการเขียนข้อมูลลงไปในตำแหน่งที่ระบุไว้ใน A0 – A5 แต่หากกำหนดเป็น 1 หมายถึงต้องการอ่านข้อมูลจากตำแหน่งนั้น
รีจิสเตอร์ที่ใช้งานในการทดลองนี้
- DATA_FORMAT (Address 0x31)
ในการทดลองนี้กำหนดค่าให้ DATA_FORMAT = 0x01 (ให้ D0 เป็น1) คือ ให้ ADXL345 อ่านค่าที่ + 4g
- POWER_CTL (Address 0x2D)
ในการทดลองนี้กำหนดค่าให้ Address POWER_CTL = 0x08 (ให้ D3 เป็น 1) คือ ให้ทำงานในโหมดการวัดค่า (Measure)
- DATAx0 DATAx1 DATAy0 DATAy1 DATAz0 DATAz1 (Address 0x32 - 0x37)
ตำแหน่ง 0x32 – 0x37 เป็นตำแหน่งที่เก็บค่าความเร่งที่วัดได้แต่ละค่าตามลำดับ
ทดลองเขียนโปรแกรมอ่านค่า ADXL345 ผ่าน SPI
- ต่อโมดูล ADXL345 เข้ากับบอร์ด Raspberry Pi ดังภาพ
- เปิดโปรแกรม LXTerminal และเปิดโปรแกรม Qt Creator พิมพ์คำสั่ง sudo qtcreator
- สร้าง Project ใหม่ชื่อ adxl_345
- เปิดไฟล์ adxl_345.pro แล้วเพิ่มโค้ดลงไปดังนี้
LIBS += -L/usr/local/lib -lwiringPi -lwiringPiDev
INCLUDEPATH += /usr/local/include
- สร้าง Project ใหม่ชื่อ adxl_345
- เปิดไฟล์ adxl_345.pro แล้วเพิ่มโค้ดลงไปดังนี้
LIBS += -L/usr/local/lib -lwiringPi -lwiringPiDev
INCLUDEPATH += /usr/local/include
- ออกแบบหน้าตาโปรแกรมโดยมี Widget ดังนี้
o Pushbutton 3 อัน โดยแสดงข้อความบนปุ่มดังนี้
“INIT SPI”
“Start”
“Stop”
คลิกขวาที่ PushButton ทีละอัน แล้วสร้าง Slot เลือก Signal = clicked ให้ครบทั้ง 3 อัน
o Text Edit
หน้าตาโปรแกรมที่ออกแบบควรจะมีรูปแบบประมาณนี้
- ไปที่ไฟล์ mainwindows.cpp แล้วเพิ่มไลบรารี่ ประกาศตัวแปร และกำหนดค่าต่างๆ ดังนี้
#include <wiringPi.h>
#include <wiringPiI2C.h>
#include <QTimer>
#include <sys/ioctrl.h>
#include <linux/spi/spidev.h>
int fd;
Qtimer *timer;
#define DEVID 0x00
#define POWER_CTRL 0x2D
#define DATA_FORMAT 0x31
#define DATA_X 0x32
#define DATA_Y 0x34
#define DATA_Z 0x36
#define CMD_READ 0x80
#define CMD_MB 0x40
#define CMD_WRITE 0x00
#include <wiringPi.h>
#include <wiringPiI2C.h>
#include <QTimer>
#include <sys/ioctrl.h>
#include <linux/spi/spidev.h>
int fd;
Qtimer *timer;
#define DEVID 0x00
#define POWER_CTRL 0x2D
#define DATA_FORMAT 0x31
#define DATA_X 0x32
#define DATA_Y 0x34
#define DATA_Z 0x36
#define CMD_READ 0x80
#define CMD_MB 0x40
#define CMD_WRITE 0x00
- เพิ่มโค้ดลงในฟังก์ชั่น void MainWindow::on_pushButton_clicked() ดังนี้
บรรทัดที่ 36 เปิดใช้งาน SPI ช่อง 0 และกำหนดความเร็วสัญญาณนาฬิกาที่ 4 MHz
บรรทัดที่ 38 หากเปิดใช้งานไม่สำเร็จให้แสดงข้อความใน Text Edit
บรรทัดที่ 44 เนื่องจากไลบรารี่ WiringPi ไม่ได้ทำฟังก์ชั่นให้เข้าไปปรับ SPI Mode ได้ เราจึงต้องใช้ฟังก์ชั่น ioctrl เข้าไปปรับให้ใช้ SPI ใน Mode 3 ได้
บรรทัดที่ 45 หากตั้งค่า SPI Mode ไม่สำเร็จให้แสดงข้อความใน Text Edit
บรรทัดที่ 51 ฟังก์ชั่น ioctrl อ่านค่า Speed Clock ที่ตั้งเอาไว้มาแสดงใน Text Edit
บรรทัดที่ 53 ฟังก์ชั่น ioctrl อ่านค่า SPI Mode ที่ตั้งเอาไว้มาแสดงใน Text Edit
บรรทัดที่ 55 สืบทอดคลาส Qtimer
บรรทัดที่ 56 สร้าง Slot และ Signal ให้กับ Timer
- สร้างฟังก์ชั่น void write_register(unsigned char reg,unsigned char data) ดังนี้
บรรทัดที่ 63 ประกาศตัวแปร Array unsigned char เพื่อเก็บข้อมูลที่ต้องการส่งให้ ADXL345
บรรทัดที่ 65 นำตำแหน่งของรีจิสเตอร์ที่ต้องการเขียนไปเก็บใน data_out ไบต์ที่ 0 และเนื่องจากคำสั่ง Write ในบิตที่ 7 เป็น 0 อยู่แล้วจึงไม่ต้องกำหนด
บรรทัดที่ 66 นำข้อมูลที่ต้องการเขียนลงรีจิสเตอร์มาเก็บใน data_out ไบต์ที่ 1
บรรทัดที่ 67 ส่งข้อมูลออกไปทาง SPI ด้วยฟังก์ชั่น wiringPiSPIDataRW(int channel , unsigned char *data , int len)
o int channel คือ ช่อง SPI ที่ใช้งาน
o unsigned char *data คือ pointer ของข้อมูลที่ต้องการรับ-ส่ง
o int len คือ จำนวนไบต์ทั้งหมดที่รับ-ส่ง
- สร้างฟังก์ชั่น QByteArray read_register(unsigned char reg , int len) ดังนี้
บรรทัดที่ 73 ประกาศตัวแปร QByteArray dat; เพื่อเก็บข้อมูลสำหรับส่งออกไปเมื่อทำงานจบฟังก์ชั่น
บรรทัดที่ 74 ประกาศตัวแปร Array unsigned char เพื่อเก็บข้อมูลที่ต้องการส่งให้ ADXL345
บรรทัดที่ 75 ตรวจเช็คว่าจำนวนไบต์ของข้อมูลที่ต้องการอ่านกลับมาว่ามีค่ามากกว่า 1 ไบต์หรือไม่
บรรทัดที่ 76 หากจำนวนข้อมูลที่ต้องการอ่านมากกว่า 1 ไบต์ให้เก็บค่าตำแหน่งของรีจิสเตอร์ลงในไบต์ที่ 0 ของ data_out กำหนดบิตที่ 7 เป็น 1 (อ่านข้อมูลจากรีจิสเตอร์) และบิตที่ 6 เป็น 1 (อ่านแบบ Multiple Byte) จาก reg|CMD_READ|CMD_MB หรือ 0xXX|0x80|0x40 ทำให้ data_out[0] มีค่าเท่ากับ 0b11xxxxxx
บรรทัดที่ 78 หากอ่านค่ากลับมาแค่ 1 ไบต์ให้เก็บค่าตำแหน่งของรีจิสเตอร์ลงในไบต์ที่ 0 ของ data_out กำหนดบิตที่ 7 เป็น 1 (อ่านข้อมูลจากรีจิสเตอร์)
บรรทัดที่ 80 ส่งข้อมูลใน data_out ออกไปทาง SPI ด้วยฟังก์ชั่น wiringPiSPIDataRW ซึ่ง ADXL345 จะส่งข้อมูลจากรีจิสเตอร์ที่เราต้องการอ่านค่ากลับมา โดยฟังก์ชั่น wiringPiSPIDataRW จะเขียนข้อมูลที่ได้รับจาก ADXL345 มาเก็บในตัวแปร data_out ต่อจากเดิมที่เราใช้ส่งคำสั่งอ่านข้อมูลออกไป ในที่นี้ เราส่งข้อมูลไปเพียง 1 ไบต์ที่ data_out[0] ดังนั้นข้อมูลที่ ADXL345 ส่งกลับมาจะถูกเก็บตั้งแต่ใน data_out[1] เป็นต้นไป
บรรทัดที่ 81 นำ Data จาก data_out ไปเก็บในตัวแปร QByteArray dat โดยไม่เก็บ data_out[0] ลงไปใน dat ด้วย
บรรทัดที่ 85 ส่งค่า return ตัวแปร dat ออกไป
- ไปที่ไฟล์ mainwindows.h
- ประกาศชื่อฟังก์ชั่น interval() ภายใต้ private slot และฟังก์ชั่น write_register กับ read_register ภายใต้ private
- ไปที่ไฟล์ mainwindows.cpp เพิ่มโค้ดลงในฟังก์ชั่น
void MainWindow::on_pushButton_2_clicked() ดังนี้
บรรทัดที่ 92 เขียนค่า 0x01 ลงในรีจิสเตอร์ DATA_FORMAT เพื่อกำหนดให้ ADXL345อ่านค่าที่ + 4g
บรรทัดที่ 92 อ่านค่ารีจิสเตอร์ DATA_FORMAT ออกมาแสดงใน Text Edit
บรรทัดที่ 95 เขียนค่า 0x08 ลงไปในรีจิสเตอร์ POWER_CTL เพื่อกำหนดให้ADXL ทำงานในโหมดการวัด (Measure)
บรรทัดที่ 96 อ่านค่ารีจิสเตอร์ POWER_CTL ออกมาแสดงใน Text Edit
บรรทัดที่ 99 สั่งให้ Timer ทำงาน มี interval ทุก 200 ms
- เพิ่มโค้ดลงในฟังก์ชั่น void MainWindow::on_pushButton_3_clicked() ดังนี้
บรรทัดที่ 104 สั่งให้ Timer หยุดทำงาน
- สร้างฟังก์ชั่น interval() ดังนี้
บรรทัดที่ 110 เคลียร์ข้อความใน Text Edit
บรรทัดที่ 111 อ่านข้อมูลจากรีจิสเตอร์ตั้งแต่ 0x32 (DATAx0) จำนวน 6 ไบต์หมายถึง การอ่านค่า ADXL345 จาก DATAx0 จนถึง DATAz1 ในครั้งเดียว
บรรทัดที่ 112 ประกาศตัวแปร int ขนาด 16 บิต มาเก็บค่าที่อ่านได้จาก ADXL345
บรรทัดที่ 114 เนื่องจากค่าความเร่งที่อ่านได้ในแต่ละแกนมีขนาด 10 บิต แยกเก็บในตัวแปรขนาด 8 บิต 2 ตัว ก่อนนำไปใช้งานจึงต้องนำข้อมูล 8 บิตทั้ง 2 อันมาเก็บรวมกันก่อนโดย Datax0 จะเป็น LSB และ Datax1 เป็น MSB และทำเช่นเดียวกันกับ Data ของทุกแกน
บรรทัดที่ 118 สร้างตัวแปรแบบ float มาเก็บค่าที่ได้จากทั้ง 3 แกนเมื่อคำนวณเป็นแรง g
บรรทัดที่ 120 ในตอนแรกกำหนดให้รีจิสเตอร์ DATA_FORMAT = 0x01 ทำให้ ADXL345 อ่านค่าที่ +4g และ ADXL345 มีค่าความละเอียด 10 บิท คือ ตั้งแต่ 0 ถึง 1023 ดังนั้นที่ -4g ค่าที่อ่านได้จะเท่ากับ 0 และที่ +4g ค่าที่อ่านได้เท่ากับ 1023 ดังนั้น ถ้าอ่านค่าได้ 1 จะมีค่าเท่ากับ 8/1023 = 0.0078g หากนำค่าในแต่ละแกนที่ ADXL345 อ่านได้มาคูณกับ 0.0078 ก็จะได้ค่าแรง g ที่กระทำต่อแกนนั้นๆ
บรรทัดที่ 124 นำค่า g ที่คำนวณได้มาแสดงใน Text Edit
- ทดลอง Run โปรแกรม
ทดสอบวาง ADXL345 ในทิศทางต่าง
ทดลองนำค่า g ที่วัดได้ในแกน x y z มาคำนวณเป็นค่ามุม Roll / Pitch
Ref: http://developer.nokia.com/community/wiki/How_to_get_pitch_and_roll_from_accelerometer_data_on_Windows_Phone
- เพิ่ม #include <qmath.h> ใน mainwindows.cpp
- เพิ่มโค้ดลงไปในฟังก์ชั่น interval() ดังนี้
- ทดลอง Run โปรแกรม