การสื่อสารระหว่าง SPI Master และ Slave โดยใช้บอร์ด Arduino
บทความนี้กล่าวถึงการเขียนโปรแกรมเพื่อทำให้บอร์ด Arduino จำนวน 2 บอร์ด สามารถสื่อสารกันด้วยบัส SPI โดยที่บอร์ดหนึ่งทำหน้าที่เป็นอุปกรณ์ SPI Master และอีกบอร์ดหนึ่งทำหน้าที่เป็น SPI Slave
คำสำคัญ / Keywords: Arduino, SPI Interfacing, SPI Master / Slave, RC Servo Output
| |||||||||||||||||||||
SPI Bus (Serial Peripheral Interface Bus) เป็นรูปแบบหนึ่งของการสื่อสารข้อมูลระหว่างอุปกรณ์แบบดิจิทัลที่พบเห็นได้บ่อย และใช้กับอุปกรณ์ได้มากกว่าสองขึ้นไปและนำมาต่อกันเป็นบัส (Bus) บัส SPI ส่งและรับข้อมูลทีละบิต (Bit Serial) และใช้สัญญาณ Clock เป็นตัวกำหนดจังหวะการทำงาน (ดังนั้นจึงเรียกว่า Synchronous, Bit-Serial Data Communication) มีการกำหนดบทบาทในการทำงานของอุปกรณ์ในระบบบัส แบ่งเป็น SPI Masterและ SPI Slave โดยที่ SPI Master เป็นฝ่ายเริ่มการสื่อสารข้อมูล และสร้างสัญญาณ Clock (มักใช้ชื่อสัญญาณว่า SCK) มากำหนดจังหวะการส่งและรับข้อมูล และด้าน SPI Slave จะเป็นฝ่ายคอยตอบสนอง และในระบบบัส SPI อาจมีอุปกรณ์ที่เป็น SPI Slaveได้มากกว่าหนึ่ง (Single-Master, Multi-Slave)
SPI ใช้สัญญาณ 4 เส้น (ใช้งานในแบบที่เรียกว่า 4-Wire SPI) ได้แก่
เมื่อจะส่ง-รับข้อมูลผ่านบัส SPI (เรียกว่า SPI Data Transfer) สัญญาณ Slave Select (/SS) จะต้องเปลี่ยนจาก HIGH เป็น LOW จากนั้นข้อมูลหนึ่งไบต์จะถูกเลื่อนบิตและส่งออกไปทีละบิตจาก SPI Master ตามจังหวะของ SCK และเลือกได้ว่าจะให้บิต MSB (Most-Significant Bit) หรือ LSB (Least-Significant Bit) ถูกส่งออกไปก่อน และในขณะเดียวกันก็จะรับข้อมูลทีละบิตจาก SPI Slave จนได้ครบหนึ่งไบต์ (หรือกล่าวได้ว่า Data Frame = 8 บิต) ดังนั้นเมื่อ SPI Master ส่งข้อมูลจำนวนหนึ่งไบต์ไปยัง SPI Slave ก็จะได้ข้อมูลหนึ่งไบต์จาก SPI Slave เช่นกัน ในช่วงเวลาที่สัญญาณ /SS เป็น LOW อาจมีการส่ง-รับข้อมูลได้มากกว่าหนึ่งไบต์ (Multi-byte SPI transfer)
การทำงานของ SPI มี 4 โหมด จำแนกตามพารามิเตอร์สองตัวที่เรียกว่า CPOL (Clock Polarity) และ CPHA (Clock Phase) ซึ่งจะเป็นตัวกำหนดลักษณะการทำงานอย่างเช่น จะส่ง-รับบิตที่ขอบขาขึ้นหรือลงของสัญญาณ CLK และสัญญาณ CLK จะอยู่ที่ลอจิก HIGH หรือ LOW เมื่อไม่อยู่ในช่วงของการส่งข้อมูลใดๆในบัส SPI (ช่วงที่เรียกว่า Idle) แต่โดยทั่วไปจะใช้ SPI Mode 0
การเปรียบเทียบระหว่าง SPI และ I2C ในการใช้งาน
I2C เป็นอีกรูปแบบการสื่อสารข้อมูลแบบดิจิทัลในประเภทที่เรียกว่า Synchronous, Bit-Serial Data Communication นิยมใช้งานอย่างแพร่หลายเช่นเดียวกับ SPI ลองมาดูตัวอย่างการเปรียบเทียบประเด็นในการใช้งานของบัสทั้งสองแบบ
ถ้าลองสำรวจอุปกรณ์หรือไอซีบางประเภท ก็จะพบว่า สามารถสื่อสารข้อมูลได้ทั้งแบบ SPI และ I2C อย่างเช่น ไอซีเซนเซอร์วัดความเร่งหรือความเร็วเชิงมุม
แหล่งข้อมูลอ้างอิงและศึกษาเพิ่มเติม:
| |||||||||||||||||||||
Arduino Sketch สำหรับตัวอย่างแรก
ตัวอย่างแรกสาธิตการเขียนโปรแกรมให้บอร์ด Arduino (เช่น Arduino Uno) ให้ทำหน้าที่เป็น SPI Master และอีกบอร์ดหนึ่งเป็น SPI Slave และสาธิตการส่ง-รับข้อมูลหลายไบต์ในช่วงที่สัญญาณ /SS เป็น LOW เมื่อ SPI Master ได้ส่งข้อมูลหนึ่งไบต์ไปยัง SPI Slave ได้แล้ว ข้อมูลไบต์นี้จะถูกส่งกลับไปยัง SPI Master ในการส่งและรับข้อมูลไบต์ครั้งถัดไป ในตัวอย่างนี้ได้เลือกใช้ความเร็ว SCK เท่ากับ 2MHz (โดยการเลือกตัวหารความถี่เท่ากับ 8 และบอร์ด Arduino ใช้ความถี่เท่ากับ 16MHz)
[Wiring Diagram for Arduino Uno boards]
Arduino Master Arduino Slave
D13 = SCK ---- SCK = D13
D12 = MISO ---- MISO = D12
D11 = MOSI ---- MOSI = D11
D10 = SS ---- SS = D10
GND ---- GND
การเชื่อมต่อระหว่างบอร์ด Arduino ทั้งสองบอร์ดให้สื่อสารกแบบ SPI ได้นั้น ให้ต่อสายสัญญาณตาม Wiring Diagram ที่ให้ไว้ (สำหรับ Arduino Uno, 328P, 5V / 16MHz) ได้แก่สัญญาณ SCK, MISO, MOSI, SS และอย่าลืมต่อขา GND ร่วมกัน
ถ้าดูจากโค้ดตัวอย่าง Arduino ที่เป็น SPI Master จะส่งข้อมูลเป็นชุดๆละ 6 ไบต์ โดยไบต์แรกจะเป็นค่าของตัวนับขนาด 8 บิต (เพิ่มค่าทีละหนึ่ง) แล้วตามด้วยไบต์ 0x55, 0x3C, 0xC3, 0xFF, 0x00 ตามลำดับ และเมื่อส่งจนครบ 6 ไบต์ ก็จะได้ข้อมูลจำนวน 6 ไบต์จาก SPI Slave ตามลำดับต่อไปนี้ (?? หมายถึง ไบต์ที่เป็นค่าของตัวนับขนาด 8 บิต)
Master to Slave: [??][55][3C][C3][F0][00] Slave to Master: [00][??][55][3C][C3][FF] Sourcecode: spi-test-master.ino ////////////////////////////////////////////////////////////////////////
// Author: RSP @ Embedded Systems Lab (ESL), KMUTNB
// Date: 22-Apr-2014
// Target Board: Arduino Uno (ATmega328P, 5V, 16MHz)
// Arduino IDE: version 1.0.5
// Description: This Arduino sketch shows how to use an Arduino Uno board
// as an SPI master device which sends and receives a block of data bytes
// periodically through the SPI. The SCK frequency is set to 2MHz.
////////////////////////////////////////////////////////////////////////
// SPI Master
#include <SPI.h> // use the Arduino SPI library
/*
SPI Pins for Arduino Uno:
D13 = SCK, D12 = MISO, D11 = MOSI, D10 = SS
SPI Pins for Arduino Mega:
D50 = MISO, D51 = MOSI, D52 = SCK, D53 = SCK
*/
#define SS (10)
#define BUTTON_PIN (2)
#define LED_PIN (3)
void setup() {
Serial.begin( 115200 ); // use Serial, baudrate = 115200
pinMode( BUTTON_PIN, INPUT );
digitalWrite( BUTTON_PIN, HIGH ); // enable internal pull-up for button input
pinMode( LED_PIN, OUTPUT );
digitalWrite( LED_PIN, LOW );
SPI.begin();
SPI.setDataMode( SPI_MODE0 );
SPI.setBitOrder( MSBFIRST );
// set clock divider for SCK
// use SPI_CLOCK_DIVx, where x=4,8,16,32,64,128
SPI.setClockDivider( SPI_CLOCK_DIV8 ); // -> 16MHz/8 = 2MHz
digitalWrite( SS, HIGH );
Serial.println( "SPI Master..." );
delay( 1000 );
}
#define SPI_TX_DELAY (10) // in microseconds
void spi_transferx( uint8_t *wdata, uint8_t *rdata, uint8_t num ) {
digitalWrite( SS, LOW );
for ( uint8_t i=0; i < num; i++ ) {
rdata[i] = SPI.transfer( wdata[i] );
delayMicroseconds( SPI_TX_DELAY ); // give the SPI slave some time to process
}
digitalWrite( SS, HIGH );
}
boolean state = false;
const int BUF_SIZE = 6; // set the buffer size
const uint8_t TEST_DATA_BYTES[] = { 0x55, 0x3C, 0xC3, 0xF0 };
uint8_t data_buf[ BUF_SIZE ];
uint8_t count = 0;
char sbuf[32]; // used for sprintf()
void loop() {
if ( digitalRead( BUTTON_PIN ) == LOW ) { // check whether the button is pressed
while ( digitalRead( BUTTON_PIN ) == LOW ) ; // wait until the button is released
// write data bytes to the data buffer
memset( data_buf, 0x00, BUF_SIZE );
data_buf[0] = count++;
memcpy( data_buf+1, TEST_DATA_BYTES, sizeof( TEST_DATA_BYTES ) );
// write/read the SPI bus
spi_transferx( data_buf, data_buf, BUF_SIZE );
Serial.print( "Read: " );
for ( uint8_t i=0; i < BUF_SIZE; i++) { // show the received data bytes
sprintf( (sbuf+4*i), "%02Xh ", data_buf[i] );
}
Serial.println( sbuf );
state = !state; // toggle state for LED
}
digitalWrite( LED_PIN, state ); // update LED output
delay(10);
}
///////////////////////////////////////////////////////////////////////
Sourcecode: spi-test-slave.ino ////////////////////////////////////////////////////////////////////////
// Author: RSP @ Embedded Systems Lab (ESL), KMUTNB
// Date: 22-Apr-2014
// Target Board: Arduino Uno (ATmega328P, 5V, 16MHz)
// Arduino IDE: version 1.0.5
// Description:
// This Arduino Sketch shows how to use Arduino Uno as an SPI
// slave device which receives/sends a block of data bytes
// through the SPI bus. Each received data byte will be sent back
// to the SPI master in the next-byte transfer.
////////////////////////////////////////////////////////////////////////
// SPI Slave
#include <SPI.h> // use the Arduino SPI library
#include <Servo.h>
#define SS (10) // specify the SPI Slave Slect pin
volatile uint8_t data = 0;
ISR(SPI_STC_vect) { // ISR for 'SPI reception complete'
data = SPDR;
SPDR = data;
}
void setup() {
Serial.begin( 115200 ); // use Serial port and baudrate=115200
pinMode( MISO, OUTPUT ); // configure the MISO pin as output
digitalWrite( SS, HIGH ); // enable pull-up on the SS pin
SPCR |= _BV(SPE); // set SPE bit in SPCR to enable SPI
//SPI.attachInterrupt(); // enable SPI interrupt
SPCR |= _BV(SPIE);
// In Slave mode the MSTR bit in SPCR is clear.
Serial.println( "SPI Slave..." );
}
void loop() {
if ( digitalRead( SS ) ) {
SPDR = 0x00;
}
delay(10);
}
////////////////////////////////////////////////////////////////////////
รูปแสดงตัวอย่างการต่อวงจรโดยใช้บอร์ด Arduino จำนวน 2 บอร์ด เชื่อมต่อเข้าด้วยกันแบบ SPI
| |||||||||||||||||||||
Arduino Sketch สำหรับตัวอย่างที่สอง
ตัวอย่างที่สองสาธิตการทำให้บอร์ด Arduino ทำหน้าที่เป็น SPI Slave และสร้างสัญญาณ R/C Servo จำนวน 4 ช่อง (50Hz คงที่) สัญญาณ R/C Servo ทุกช่องมีค่า Duty Cycle เริ่มต้นเป็น 1500μsec แต่สามารถปรับเปลี่ยนค่าได้ โดยรับคำสั่งจากบอร์ด Arduino อีกบอร์ดหนึ่งที่ทำหน้าที่เป็น SPI Master เพื่อกำหนดความกว้างช่วงที่เป็น HIGH ได้ในหน่วยเป็นไมโครวินาที (μsec) นอกจากนั้นยังสามารถอ่านค่า Duty Cycle ของแต่ละช่องได้เช่นกัน
รูปแบบของคำสั่งและข้อมูลมีดังนี้ กรณีแรกเป็นการกำหนดค่า Duty Cycle (12 บิต) ให้ช่องสัญญาณที่เลือก จะต้องส่งข้อมูลไบต์จำนวน 2 ไบต์ และไม่สนใจข้อมูลไบต์ที่ได้รับกลับมา
Write Operation (Bytes sent by SPI Master):
First Byte:
Bit 7 = R/W bit = '1'
Bit 6-5 = Channel Number
Bit 4 = don't care
Bit 3-0 = DutyCycle (Bit 11..8)
Second Byte:
Bit 7-0 = DutyCycle (Bit 7..0)
กรณีที่สองเป็นการอ่านค่า Duty Cycle ของช่องสัญญาณที่เลือก จะต้องส่งข้อมูลไบต์ทั้งหมด 3 ไบต์ ไม่สนใจไบต์แรกที่ได้รับมา และค่า Duty Cycle ที่ได้จะอยู่ในข้อมูลไบต์ 2 ไบต์สุดท้าย
Read Operation (Bytes sent by SPI Master):
First Byte:
Bit 7 = R/W bit = '0'
Bit 6-5 = Channel Number
Bit 4-0 = don't care
Second Byte:
Bit 7-0 = don't care
Third Byte:
Bit 7-0 = don't care
โค้ดตัวอย่างนี้สาธิตการเปลี่ยนค่า Duty Cycle ของสัญญาณ R/C Servo 4 ช่อง เมื่อมีการกดปุ่มแต่ละครั้ง และสลับไปมาระหว่างสองกรณี โดยที่กรณีแรก ให้ทุกช่องมีค่า 1500 μsec และในอีกกรณีให้สัญญาณ 4 ช่อง มีค่า 1000,1250,1750,2000 μsec ตามลำดับ
Sourcecode: spi-servo-master.ino ////////////////////////////////////////////////////////////////////////
// Author: RSP @ Embedded Systems Lab (ESL), KMUTNB
// Date: 22-Apr-2014
// Target Board: Arduino Uno (ATmega328P, 5V, 16MHz)
// Arduino IDE: version 1.0.5
// Description: This Arduino sketch shows how to use the Arduino Uno
// as SPI master to communicate with another Arduino Uno which
// operates as SPI slave. The SPI slave device also generates four
// R/C servo signals with adjustable duty cycles.
// The SPI master can set/get the duty cycle of the selected R/C signal
// via the SPI bus.
////////////////////////////////////////////////////////////////////////
// SPI Master
#include <SPI.h> // use the Arduino SPI library
/*
SPI Pins for Arduino Uno:
D13 = SCK, D12 = MISO, D11 = MOSI, D10 = SS
SPI Pins for Arduino Mega:
D50 = MISO, D51 = MOSI, D52 = SCK, D53 = SCK
*/
#define SS (10)
#define BUTTON_PIN (2)
#define LED_PIN (3)
#define NUM_CHANNELS (4)
const uint16_t DEFAULT_PULSE_WIDTHS[ NUM_CHANNELS ] = { 1500,1500,1500,1500, };
const uint16_t TEST_PULSE_WIDTHS[ NUM_CHANNELS ] = { 1000,1250,1750,2000, };
const uint16_t *pulse_widths;
void set_pulse_widths();
void setup() {
Serial.begin( 115200 ); // use Serial, baudrate = 115200
pinMode( BUTTON_PIN, INPUT );
digitalWrite( BUTTON_PIN, HIGH ); // enable internal pull-up
pinMode( LED_PIN, OUTPUT );
digitalWrite( LED_PIN, LOW );
SPI.begin();
SPI.setDataMode( SPI_MODE0 );
SPI.setBitOrder( MSBFIRST );
// set clock divider for SCK -> use 16MHz/8 = 2MHz
SPI.setClockDivider( SPI_CLOCK_DIV8 ); // SPI_CLOCK_DIVx, where x=4,8,16,32,64,128
digitalWrite( SS, HIGH );
pulse_widths = DEFAULT_PULSE_WIDTHS;
set_pulse_widths();
Serial.println( "SPI Master..." );
delay( 100 );
}
#define SPI_WAIT_DELAY (16) // in microseconds
void set_pulse_widths() {
for ( uint8_t i=0; i < NUM_CHANNELS; i++ ) {
uint16_t pulse_width = pulse_widths[i];
digitalWrite( SS, LOW );
// write operation the i-th PWM channel
SPI.transfer( 0x80 | (i << 5) | ((pulse_width >> 8) & 0x0f) );
delayMicroseconds( SPI_WAIT_DELAY );
SPI.transfer( pulse_width & 0xff ); // write high byte
delayMicroseconds( SPI_WAIT_DELAY );
digitalWrite( SS, HIGH ); // write low byte
delayMicroseconds( 30 ); // give the SPI slave some time to process
}
}
void get_pulse_widths( uint16_t *values ) {
uint8_t lo_byte, hi_byte;
for ( uint8_t i=0; i < NUM_CHANNELS; i++ ) {
digitalWrite( SS, LOW );
SPI.transfer( (i << 5) ); // read operation for i-th PWM channel
delayMicroseconds( SPI_WAIT_DELAY );
hi_byte = SPI.transfer( 0x00 );
delayMicroseconds( SPI_WAIT_DELAY );
lo_byte = SPI.transfer( 0x00 );
delayMicroseconds( SPI_WAIT_DELAY );
digitalWrite( SS, HIGH );
values[i] = (hi_byte & 0x0f);
values[i] = (values[i] << 8) | lo_byte;
delayMicroseconds( 30 ); // give the SPI slave some time to process
}
}
boolean state = false;
char sbuf[32]; // used for sprintf()
void loop() {
if ( digitalRead( BUTTON_PIN ) == LOW ) { // check whether the button is pressed
while ( digitalRead( BUTTON_PIN ) == LOW ) ; // wait until the button is released
uint16_t values[ NUM_CHANNELS ];
pulse_widths = (state) ? TEST_PULSE_WIDTHS : DEFAULT_PULSE_WIDTHS;
set_pulse_widths(); // update the pulse widths of all servos
get_pulse_widths( values );
Serial.print( "Pulse widths (usec): " );
for ( uint8_t i=0; i < NUM_CHANNELS; i++ ) {
sprintf( sbuf+i*5, "%5u", values[i] );
}
Serial.println( sbuf );
state = !state; // toggle LED state
}
digitalWrite( LED_PIN, state );
delay(10);
}
///////////////////////////////////////////////////////////////////////
Sourcecode: spi-servo-slave.ino ////////////////////////////////////////////////////////////////////////
// Author: RSP @ Embedded Systems Lab (ESL), KMUTNB
// Date: 22-Apr-2014
// Target Board: Arduino Uno (ATmega328P, 5V, 16MHz)
// Arduino IDE: version 1.0.5
// Description: This Arduino sketch is an example of how to
// make an Arduino Uno board an SPI slave which generates
// four R/C servo signals and allows the SPI master to change
// the duty cycle of each R/C signal.
////////////////////////////////////////////////////////////////////////
// SPI Slave
#include <SPI.h> // use the Arduino SPI library
#include <Servo.h>
#define SS (10)
#define LED_PIN (3)
const uint8_t PWM_PINS[4] = {5,6,7,8};
Servo servos[4];
uint16_t servo_pulse_widths[4];
volatile uint8_t byte_count = 0;
volatile uint8_t channel = 0;
volatile uint16_t pulse_width = 0;
volatile boolean update_flag = false;
ISR(SPI_STC_vect) { // SPI reception complete
static uint8_t rw_bit = 0;
static uint8_t data;
digitalWrite( LED_PIN, HIGH );
data = SPDR;
if ( byte_count == 0 ) {
rw_bit = (data >> 7) & 1;
channel = (data >> 5) & 3;
if ( rw_bit == 1 ) { // write operation
pulse_width = (uint16_t)(data & 0x0f);
SPDR = 0x00; // don't care
} else { // read operation
pulse_width = servo_pulse_widths[ channel ];
SPDR = (pulse_width >> 8) & 0x0f;
}
byte_count++;
}
else if ( byte_count == 1 ) {
if ( rw_bit == 1 ) { // write operation
pulse_width = (pulse_width << 8) | (((uint16_t)data) & 0x00ff);
SPDR = 0x00; // don't care
update_flag = true;
byte_count = 0;
} else { // read operation
SPDR = pulse_width & 0xff;
byte_count++;
}
}
else {
byte_count = 0;
SPDR = 0x00;
}
digitalWrite( LED_PIN, LOW );
}
void setup() {
pinMode( LED_PIN, OUTPUT );
digitalWrite( LED_PIN, LOW );
Serial.begin( 115200 ); // use Serial port and baudrate=115200
pinMode( MISO, OUTPUT ); // configure the MISO pin as output
digitalWrite( SS, HIGH ); // enable pull-up on the SS pin
for ( uint8_t i=0; i < 4; i++) {
servo_pulse_widths[i] = 1500;
servos[i].attach( PWM_PINS[i] );
servos[i].writeMicroseconds( servo_pulse_widths[i] );
}
SPCR |= _BV(SPE); // set SPE bit in SPCR to enable SPI
//SPI.attachInterrupt(); // enable SPI interrupt
SPCR |= _BV(SPIE);
// In Slave mode the MSTR bit in SPCR is clear.
Serial.println( "SPI Slave..." );
}
void loop() {
static uint8_t _channel;
static uint16_t _pulse_width;
if ( update_flag ) { // update
_pulse_width = pulse_width;
_channel = channel;
byte_count = 0;
update_flag = false;
servo_pulse_widths[ _channel ] = _pulse_width;
servos[ _channel ].writeMicroseconds( servo_pulse_widths[ _channel ] );
}
if ( digitalRead( SS ) ) {
byte_count = 0;
update_flag = false;
}
}
////////////////////////////////////////////////////////////////////////
รูปแสดงข้อความที่ปรากฏใน Serial Monitor ของ Arduino IDE หลังจากที่ได้กดปุ่มหลายครั้งเพื่อลองเปลี่ยนและอ่านค่า Duty Cycle ของสัญญาณ |
ไม่มีความคิดเห็น :
แสดงความคิดเห็น