การสื่อสารระหว่าง 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 ของสัญญาณ |
ไม่มีความคิดเห็น :
แสดงความคิดเห็น