Hướng dẫn sử dụng ngoại vi I2C của vi điều khiển TM4C123GH6PM – Lập trình từ thanh ghi và cách đọc Data Sheet. Thử nghiệm giao tiếp I2C giữa Tiva-C Launchpad và Arduino UNO.
Phần đầu, chúng ta sẽ sử dụng phương pháp bare-metal để cấu hình các module I2C trực tiếp bằng các thanh ghi ngoại vi (peripheral register). CCS IDE hoặc Keil uvision IDE có thể được sử dụng để viết chương trình cho cấu hình I2C Master và Slave.
Cuối bài sẽ là một ví dụ Tiva C (Master) giao tiếp I2C với board Arduino UNO (Slave).
Giới thiệu giao tiếp I2C
I2C còn được gọi là inter-integrated circuit hoặc IIC hoặc I square C. Nó là một giao thức truyền thông nối tiếp bán song đồng bộ (synchronous half-duplex serial communication protocol). Hơn nữa, nó là một giao thức multi-master bus chỉ yêu cầu hai dây là SCL và SDA để truyền nhận dữ liệu nối tiếp (serial).
- SDA (Bidirectional data line)
- SCL (Bidirectional clock line)
Kết nối Bus I2C
Mỗi thiết bị được kết nối với bus I2C có thể ở chế độ Master hoặc chế độ Slave. Nhưng chỉ một thiết bị Master mới có thể khởi tạo quá trình truyền dữ liệu. Thông thường, mô hình gồm có một thiết bị Master và một thiết bị Slave hoặc nhiều thiết bị Slave kết nối với cùng một bus I2C thông qua các điện trở kéo lên (pull-up resistor). Mỗi Slave có một địa chỉ duy nhất gồm 7 bit.
Ví dụ: nếu chúng ta cấu hình TM4C123G Tiva Launchpad là một thiết bị master, chúng ta có thể kết nối nhiều thiết bị slave với cùng bus.
Để biết thêm thông tin về Giao tiếp I2C, bạn có thể đọc bài này: https://microcontrollerslab.com/i2c-bus-communication-protocol-tutorial-applications/
Module giao tiếp I2C của TM4C123G
Như chúng ta đã đề cập trước đó, vi điều khiển TM4C123 có bốn module I2C bên trong chip. Hơn nữa, mỗi module có thể được cấu hình hoạt động ở chế độ Master, Slave, hoặc đồng thời cả Master và Slave.
Vi điều khiển TM4C123GH6PM được tích hợp trên bo mạch Tiva-C Launchpad gồm có bốn kênh (channel) I2C bên trong chip như I2C0, I2C1, I2C2 và I2C3.
Các chân I2C của TM4C123
Bảng sau mô tả chân truyền clock (clock line pin) và chân truyền tín hiệu (data line pin) của mỗi module I2C trên các chân ra (pinout) của TM4C123.
I2C Module | SCL Pin | SDA Pin |
I2C0 | PB2 | PB3 |
I2C1 | PA6 | PA7 |
I2C2 | PE4 | PE5 |
I2C3 | PD0 | PD1 |
Như bạn đã biết, mỗi chân GPIO của vi điều khiển là đa chức năng. Các chân này được dùng chung bởi các ngoại vi (peripheral) khác nhau bằng kỹ thuật ghép kênh (multiplexing technique). Mỗi chân có thể được sử dụng cho một ngoại vi hoặc cho một chức năng tại một thời điểm. Một thanh ghi chọn ngoại vi được sử dụng để bật (enable) hoặc tắt (disable) chức năng thay thế của các chân.
Lưu ý: chân I2CSDA cần được set thành open-drain bằng thanh ghi GPIOODR.
Tất cả các module I2C này đều hỗ trợ bốn tốc độ truyền dữ liệu.
- Standard (100 Kbps)
- Fast-mode (400 Kbps)
- Fast-mode plus (1 Mbps)
- High-speed mode (3.33 Mbps)
Lưu đồ giải thuật
Master TRUYỀN một byte
(Xem Data sheet TM4C123GH6PM Microcontroller trang 1008)
Master NHẬN một byte
(Xem Data sheet TM4C123GH6PM Microcontroller trang 1009)
Master TRUYỀN nhiều byte
(Xem Data sheet TM4C123GH6PM Microcontroller trang 1010)
Master NHẬN nhiều byte
(Xem Data sheet TM4C123GH6PM Microcontroller trang 1011)
Các bước cấu hình module TM4C123 I2C làm Master
Như đã đề cập trước đó, mỗi module I2C có thể được cấu hình trở thành một Master hoặc một Slave. Trong hướng dẫn này, chúng ta sẽ hiểu các bước để cấu hình module I2C3 thành một module Master. Tuy nhiên, bạn cũng có thể dựa theo những bước này để tự cấu hình các module I2C khác thành Master hoặc Slave.
Bước 1: Enable Clock
- Kích hoạt (enable) I2C clock cho module I2C3 bằng thanh ghi RCGCI2C. (Địa chỉ thanh ghi RCGCI2C là 0x400FE620, set giá trị bit 3 lên 1).
- Kích hoạt clock cho các chân GPIO được dùng làm chân SDA và SCL bằng thanh ghi RCGCGPIO. (Địa chỉ thanh ghi RCGCGPIO là 0x400FE608, set giá trị bit 3 lên 1)
- Cho phép chân PD0 và PD1 được điều khiển bằng ngoại vi I2C thay thế bằng thanh ghi GPIOAFSEL. (Địa chỉ thanh ghi GPIOAFSEL port D là 0x40007420, set giá trị bit 0 và bit 1 lên 1).
- Chọn open-drain operation cho chân I2CSDA (chính là chân PD1) bằng thanh ghi GPIOODR. (Địa chỉ thanh ghi GPIOODR port D là 0x4000750C, set giá trị bit 0 và bit 1 lên 1).
- Chọn chức năng I2C3 thay thế bằng thanh ghi GPIOPCTL. (Địa chỉ thanh ghi GPIOPCTL port D là 0x4000752C, encoding chức năng I2C của PD0 và PD1 lần lượt là PMC0 = 0x03, PMC1 = 0x03 – giá trị encoding I2C tra Table 23-5 bên dưới).
(Xem Data sheet TM4C123GH6PM Microcontroller trang 1351)
- Kích hoạt chế độ Master bằng cách set giá trị thanh ghi I2CMCR là 0x0000.0010. (Địa chỉ thanh ghi I2CMCR module 3 là 0x40023020, giá trị bit 4 (MFE) = 1 là master).
Mã giả như sau:
RCGCI2C |= 0x00000008; RCGCGPIO |= 0x00000008; GPIOAFSEL |= 0x00000003; GPIOODR |= 0x00000002; GPIOPCTL |= 0x00000033; I2CMCR = 0x00000010;
Bước 2: Tính toán SCL Clock Speed
Ví dụ chúng ta muốn I2C sử dụng ở tốc độ 100kbps. Sử dụng công thức:
SCL_PERIOD = 2 × (1 + TIMER_PRD) × (SCL_LP + SCL_HP) × CLK_PRD
=> TIMER_PRD = (SystemClock / (2 × (SCL_LP + SCL_HP) × SCL_CLK)) – 1
Ví dụ:
SCL_PERIOD = 10 μs (SCL_CLK = 100 kbps)
CLK_PRD = 62.5 ns (SystemClock = 16 MHz)
SCL_LP = 6
SCL_HP = 4
(giá trị SCL_LP và SCL_HP mặc định với chế độ Standard 100kbps, Fast 400 kbps và Fast Plus 1Mbps. Ở chế độ High-Speed, SCL_LP=2 và SCL_HP=1)
Ta sẽ được:
1/100000 = 2 × (1 + TIMER_PRD) × (6 + 4) × 1/16000000
TIMER_PRD = 7
Đặt giá trị time period trên vào thanh ghi I2CMTPR (Địa chỉ thanh ghi I2CMTPR thuộc module 3 là 0x4002300C)
Bảng sau giới thiệu một số giá trị ví dụ của TIMER_PRD. Bạn có thể tham khảo các ví dụ cho những trường hợp khác.
Mã giả như sau:
I2CMTPR = 7;
Bước 3: Truyền dữ liệu Master
- Chỉ định địa chỉ Slave cho Master và thao tác (operation) tiếp theo là một lệnh Truyền (Transmit) bằng cách ghi (write) vào thanh ghi I2CMSA với giá trị 0x0000.0076 (ví dụ này chọn địa chỉ slave là 0x3B). (Địa chỉ của thanh ghi I2CMSA module 3 là 0x40023000, bit [7:1] lưu địa chỉ slave, clear giá trị bit 0 để I2C truyền dữ liệu, 0:Transmit , 1:Receive).
- Nạp dữ liệu (1 byte) cần truyền vào thanh ghi dữ liệu I2CMDR. (Địa chỉ thanh ghi I2CMDR module 3 là 0x40023008).
- Khởi động một lệnh truyền (transmit) dữ liệu từ Master sang Slave bằng cách ghi giá trị 0x0000.0007 (STOP, START, RUN) lên thanh ghi I2CMCS (Địa chỉ thanh ghi I2CMCS module 3 là 0x40023004).
- Chờ đến khi quá trình truyền hoàn tất, bằng chạy vòng lặp (polling) theo dõi giá trị bit BUSBSY (bit 6) của thanh ghi I2CMCS đến khi nào nó bị clear.
- Check bit ERRROR (bit 1) của thanh ghi I2CMCS để đảm bảo quá trình truyền đã được xác nhận (acknowledge).
Mã giả như sau:
R_S_bit = 0; I2CMSA = (0x3B << 1) + R_S_bit; I2CMDR = data; while(I2CMCS & 0x40); I2CMCS = 0x00000007; if(I2CMCS & 0x02) error_serivce(); return I2CMCS & 0x0E;
Bạn cũng có thể dựa theo các bước trên để cấu hình I2C Master của Tiva-C để NHẬN (receive) dữ liệu từ Slave.
Example: Tiva-C TM4C123G and Arduino UNO with I2C Communication
Trong ví dụ này, chúng ta sẽ khởi tạo module I2C3 của vi điều khiển TM4C123G làm một Master. Chương trình ví dụ này transmit và receive byte qua I2C module 3. Chân PD0 và PD1 được dùng làm chân tín hiệu của module I2C3.
Chúng ta sẽ dùng một board Arduino UNO để giao tiếp I2C với board Tiva C TM4C123G.
Định nghĩa macro thanh ghi của TM4C123GH6PM
Đầu tiên chúng ta cần định nghĩa địa chỉ các thanh ghi cần dùng. Ví dụ như thanh ghi điều khiển System, thanh ghi GPIO port D, thanh ghi điều khiển I2C module 3, v.v. May thay những thanh ghi và bit-mask cần thiết của vi điều khiển này đã được định nghĩa sẵn trong file header tm4c123gh6pm.h bởi hãng sản xuất – Texas Instrument. Bạn có thể download file hoặc tự thêm những định nghĩa macro vào code của bạn như dưới đây.
#define SYSCTL_RCGCGPIO_R (*((volatile uint32_t *)0x400FE608)) #define SYSCTL_RCGCGPIO_R3 0x00000008 // GPIO Port D Run Mode Clock // Gating Control #define SYSCTL_RCGCI2C_R (*((volatile uint32_t *)0x400FE620)) #define SYSCTL_RCGCI2C_R3 0x00000008 // I2C Module 3 Run Mode Clock // Gating Control #define GPIO_PORTD_DEN_R (*((volatile uint32_t *)0x4000751C)) #define GPIO_PORTD_AFSEL_R (*((volatile uint32_t *)0x40007420)) #define GPIO_PORTD_ODR_R (*((volatile uint32_t *)0x4000750C)) #define GPIO_PORTD_PCTL_R (*((volatile uint32_t *)0x4000752C)) #define GPIO_PCTL_PD0_I2C3SCL 0x00000003 // I2C3SCL on PD0 #define GPIO_PCTL_PD1_I2C3SDA 0x00000030 // I2C3SDA on PD1 #define I2C3_MCR_R (*((volatile uint32_t *)0x40023020)) #define I2C_MCR_MFE 0x00000010 // I2C Master Function Enable #define I2C3_MTPR_R (*((volatile uint32_t *)0x4002300C)) #define I2C3_MSA_R (*((volatile uint32_t *)0x40023000)) #define I2C_MSA_RS 0x00000001 // Receive not send #define I2C3_MDR_R (*((volatile uint32_t *)0x40023008)) #define I2C3_MCS_R (*((volatile uint32_t *)0x40023004)) #define I2C_MCS_DATACK 0x00000008 // Acknowledge Data #define I2C_MCS_ADRACK 0x00000004 // Acknowledge Address #define I2C_MCS_ERROR 0x00000002 // Error #define I2C_MCS_START 0x00000002 // Generate START #define I2C_MCS_RUN 0x00000001 // I2C Master Enable #define I2C_MCS_STOP 0x00000004 // Generate STOP #define I2C_MCS_ACK 0x00000008 // Data Acknowledge Enable #define I2C_MCS_BUSY 0x00000001 // I2C Busy #define I2C_MCS_BUSBSY 0x00000040 // Bus Busy
Hàm khởi tạo I2C Master
Đầu tiên, ta cần viết hàm khởi tạo I2C module 3. Hàm này sẽ thực hiện việc cấp xung clock cho ngoại vi GPIOD và ngoại vi I2C3 hoạt động, tiếp theo cấu hình chân GPIO port D để thay đổi thành chức năng I2C – chân tín hiệu SCL và SDA. Cuối cùng, hàm cấu hình I2C làm vai trò Master và cài đặt tốc độ clock (tần số) của chân SCL.
/** * I2C intialization and GPIO alternate function configuration * Configure Port D pins 0 and 1 as I2C module 3 * PD0 -- I2C3_SCL * PD1 -- I2C3_SDA **/ void I2C3_Init ( void ) { SYSCTL_RCGCGPIO_R |= SYSCTL_RCGCGPIO_R3; // Enable and provide a clock to GPIO Port D SYSCTL_RCGCI2C_R |= SYSCTL_RCGCI2C_R3 ; // Enable and provide a clock to I2C module 3 GPIO_PORTD_DEN_R |= 0x00000003; // Enable digital functions for PD0, PD1 pins. GPIO_PORTD_AFSEL_R |= 0x00000003; // PD0 and PD1 function is controlled by the alternate hardware function GPIO_PORTD_ODR_R |= 0x00000002; // Enable the I2C3SDA (PD1) pin for open-drain operation GPIO_PORTD_PCTL_R &= ~0x000000FF; // Clear encoded value of PD0 and PD1 GPIO_PORTD_PCTL_R |= GPIO_PCTL_PD0_I2C3SCL + GPIO_PCTL_PD1_I2C3SDA; // Assign the I2C signals to PD0 and PD1 pins I2C3_MCR_R = I2C_MCR_MFE; // Enable I2C3 Master mode /* Configure I2C clock rate to 100Kbps for Standard Mode, with System Clock is 16MHz */ // TIMER_PRD = (SystemClock / (2 × (SCL_LP + SCL_HP) × SCL_CLK)) - 1 // TIMER_PRD = 16,000,000 / (2 × (6 + 4) × 100000)) - 1 // TIMER_PRD = 7 I2C3_MTPR_R = 0x07 ; }
Hàm Truyền/Nhận 1 byte
Code các hàm để master truyền 1 byte và nhận 1 byte được viết như sau.
/** * Wait until I2C master is not busy and return error code * If there is no error, return 0 **/ static int I2C_wait_till_done(void) { while(I2C3_MCS_R & I2C_MCS_BUSY); // wait until I2C master is not busy return I2C3_MCS_R & (I2C_MCS_DATACK + I2C_MCS_ADRACK + I2C_MCS_ERROR); // return I2C error code } /** Write single byte **/ int I2C3_Write_Byte(int slave_address, char data) { char error; I2C3_MSA_R = slave_address << 1; I2C3_MDR_R = data; // write first byte I2C3_MCS_R = I2C_MCS_START + I2C_MCS_RUN + I2C_MCS_STOP; // 0x0000.0007 error = I2C_wait_till_done(); // wait until write is complete if(error) return error; while(I2C3_MCS_R & I2C_MCS_BUSBSY); // wait until bus is not busy error = I2C3_MCS_R & 0xE; // I2C_MCS_DATACK + I2C_MCS_ADRACK + I2C_MCS_ERROR if(error) return error; return 0; // no error } /** Read single byte **/ int I2C3_Read_Byte(int slave_address, char *data) { char error; I2C3_MSA_R = slave_address << 1; I2C3_MSA_R |= I2C_MSA_RS; // receive direction I2C3_MCS_R = I2C_MCS_START + I2C_MCS_RUN + I2C_MCS_STOP; // 0x0000.0007 error = I2C_wait_till_done(); // wait until read is complete if(error) return error; *data = I2C3_MDR_R; // read byte while(I2C3_MCS_R & I2C_MCS_BUSBSY); // wait until bus is not busy error = I2C3_MCS_R & 0xE; // check error: I2C_MCS_DATACK + I2C_MCS_ADRACK + I2C_MCS_ERROR if(error) return error; return 0; // no error }
Hàm Truyền/Nhận nhiều byte
Code các hàm để master truyền 1 byte và nhận nhiều byte được viết như sau.
/** I2C Master transfer multiple bytes **/ int I2C3_Write_Buffer(int slave_address, int length, char* data) { char error; if (length <= 0) return -1; /* send slave address and starting address */ I2C3_MSA_R = slave_address << 1; I2C3_MDR_R = *data++; // write first byte I2C3_MCS_R = I2C_MCS_START + I2C_MCS_RUN; error = I2C_wait_till_done(); // wait until write is complete if(error) return error; length--; /* send data one byte at a time */ while (length > 1) { I2C3_MDR_R = *data++; // write the next byte I2C3_MCS_R = I2C_MCS_RUN; error = I2C_wait_till_done(); if (error) return error; length--; } /* send last byte and a STOP */ I2C3_MDR_R = *data++; //write the last byte I2C3_MCS_R = I2C_MCS_RUN + I2C_MCS_STOP; error = I2C_wait_till_done(); if(error) return error; while(I2C3_MCS_R & I2C_MCS_BUSBSY); // wait until bus is not busy return 0; } /** I2c Master read multiple bytes **/ int I2C3_Read_Buffer(int slave_address, int length, char *data) { char error; if(length <= 0) return -1; I2C3_MSA_R = slave_address << 1; I2C3_MSA_R |= I2C_MSA_RS; // receive direction if(length == 1) I2C3_MCS_R = I2C_MCS_ACK + I2C_MCS_START + I2C_MCS_RUN + I2C_MCS_STOP; else I2C3_MCS_R = I2C_MCS_ACK + I2C_MCS_START + I2C_MCS_RUN; error = I2C_wait_till_done(); // wait until operation is complete if(error) return error; *data++ = I2C3_MDR_R; // read first byte if(length > 0) { /* read the remain bytes */ while(length > 1) { I2C3_MCS_R = I2C_MCS_ACK + I2C_MCS_RUN; // RUN error = I2C_wait_till_done(); // wait until operation is complete if(error) return error; *data++ = I2C3_MDR_R; // read bytes length--; } I2C3_MCS_R = I2C_MCS_RUN + I2C_MCS_STOP; // RUN+STOP error = I2C_wait_till_done(); // wait until operation is complete if(error) return error; *data++ = I2C3_MDR_R; // read last byte length--; } while(I2C3_MCS_R & I2C_MCS_BUSBSY); // wait until bus is not busy return 0; }
Hàm Main
Chương trình main gọi các hàm I2C để test chức năng truyền và nhận theo byte và theo buffer.
/** * System Clock: 16MHz * SCL rate: 100Kbps * PD0 -- SCL * PD1 -- SDA * Slave Address: 0x3B **/ int main(void) { const int slave_address = 0x3B; // 0x3B = 0011.1011 I2C3_Init(); I2C3_Write_Byte(slave_address, 'H'); I2C3_Write_Byte(slave_address, 'e'); I2C3_Write_Byte(slave_address, 'l'); I2C3_Write_Byte(slave_address, 'l'); I2C3_Write_Byte(slave_address, 'o'); I2C3_Write_Byte(slave_address, '.'); I2C3_Write_Buffer(slave_address, 7, "HELLO"); char received_bytes_1[5] = {0x55, 0x55, 0x55, 0x55, 0x55}; char received_bytes_2[5] = {0}; I2C3_Read_Byte(slave_address, &received_bytes_1[0]); I2C3_Read_Byte(slave_address, &received_bytes_1[1]); I2C3_Read_Byte(slave_address, &received_bytes_1[2]); I2C3_Read_Byte(slave_address, &received_bytes_1[3]); I2C3_Read_Byte(slave_address, &received_bytes_1[4]); I2C3_Read_Buffer(slave_address, 5, received_bytes_2); for(;;) { } }
Arduino UNO I2C Slave
Sử dụng Arduino IDE để tạo một sketch mới. Copy và paste đoạn code sau vào sketch mà bạn vừa tạo. Build và upload lên board Arduino UNO của bạn.
Chương trình này cấu hình board Arduino UNO của bạn thành một thiết bị Slave. Cấu hình I2C Slave với địa chỉ 0x3B, tốc độ 100Kbps. Arduino UNO sẽ nhận rồi hiển thị những dữ liệu được truyền từ Tiva C và phản hồi những yêu cầu (request) từ Tiva C bằng cách gởi về một chuỗi “hello”.
#include <Wire.h> void setup() { Wire.begin(0x3B); // join i2c bus with address 0x3B Wire.onRequest(requestEvent); // register event when master request to send Wire.onReceive(receiveEvent); // register event when arduino receives Serial.begin(115200); // start serial for output Serial.println("I2C Slave Sender + Receiver - addr:0x3B, baud:100000(Hz)"); } void loop() { delay(100); } // function that executes whenever data is requested by master // this function is registered as an event, see setup() void requestEvent() { Wire.write("hello "); // respond with message of 6 bytes //Serial.println("send to master"); // debug only, print in even handler can miss data // as expected by master } // function that executes whenever data is received from master // this function is registered as an event, see setup() void receiveEvent(int howMany) { while (Wire.available()) { // loop through all but the last char c = Wire.read(); // receive byte as a character Serial.print(c); // print the character } }
Kết nối chân tín hiệu SCL và SDA của Arduino UNO với board Tiva-C TM4C123G của bạn để test chức năng I2C.
Tài liệu liên quan
- Data sheet TM4C123GH6PM Microcontroller – spms376e.pdf
- Download file header tm4c123gh6pm.h từ SW-TM4C – https://github.com/TaLucGiaHoang/SW-TM4C.git
- Download example code from my github repository. - https://github.com/TaLucGiaHoang/tiva-c-i2c-arduino-uno.git