Giao tiếp TWI – I2C bằng kỹ thuật bit-Bangging

 

Giới thiệu

      TWI (Two-Wire Serial Intereafce) là một module truyền thông nối tiếp đồng bộ trên các chip AVR dựa trên chuẩn truyền thông I2C. I2C là viết tắc của từ Inter-Integrated Circuit là một chuẩn truyền thông do hãng điện tử Philips Semiconductor sáng lập và xây dựng thành chuẩn năm 1990. Phiên bản mới nhất của I2C là V3.0 phát hành năm 2007. Để hiểu thêm về I2C bạn có thể tham khảo các tài liệu “I2C Specification” từ trang web của NXP- http /www.nxp.com (lập bởi Philips). Trong phạm vi bài học này tôi chỉ giới thiệu kỹ thuật bit-Bangging, giao tiếp bằng phần mềm có thể áp dụng tương tự cho các họ vi điều khiển khác nhau.

      Bit-bangging là một kỹ thuật thực hiện các chuẩn giao tiếp (I2C, SPI, UART …) bằng các chân GPIO thông thường. Hay nói cách khác, bằng cách define phù hợp thứ tự các mức logic LOW hay HIGH của GPIO ta có thể giao tiếp theo các chuẩn như I2C, SPI, UART…Như vậy, khi phần cứng không hỗ trợ, ta vẫn có thể giao tiếp bình thường bằng phần mềm, chẳng hạn 8051 có thể giao tiếp I2C. Đây rõ ràng là một lợi thế rất lớn.Tuy nhiên, kỹ thuật này cũng gặp phải nhiều bất lợi. Thông thường đối với module phần cứng. Mỗi thao tác chỉ thực hiện trong vòng 1 chu kỳ (chẳng hạn như việc send tín hiệu Start I2C). Trong khi đó, khi dùng Bit-Banging thì phải mất nhiều hơn 1 chu kỳ do cần lần lượt config các chân GPIO (sẽ làm rõ ở phần tiếp theo). Ngoài ra, Bit-Banging còn khiến cho CPU tốn nhiều thời gian để tính toán hơn và như vậy sẽ kém hiệu quả khi cần giao tiếp ở tốc độ cao hay với những dữ liệu lớn.

      TWI (I2C) là một truyền thông nối tiếp đa chip chủ (tạm dịch của cụm từ multi-master serial computer bus). Khái niệm “multi-master” (tôi sẽ dùng từ tiếng anh multi-master thay vì dùng “đa chip chủ”) được hiểu là trong trên cùng một bus có thể có nhiều hơn một thiết bị làm Master, đồng thời một Slave có thể trở thành một Master nếu nó có khả năng. Ví dụ trong một mạng TWI của nhiều AVR kết nối với nhau, bất kỳ một AVR nào đều có thể trở thành Master ở một thời điểm nào đó. Tuy nhiên nếu một mạng dùng một AVR điều khiển các chip nhớ (như EEPROM AT24C1024 chẳng hạn) thì khái niệm “multi-master” không tồn tại vì các chip nhớ được thiết kế sẵn là Slave, không có khả năng trở thành master. TWI (I2C) được thực hiện trên 2 đường SDA (Serial DATA) và SCL (Serial Clock) trong đó SDA là đường truyền/nhận dữ liệu và SCL là đường xung nhịp. Căn cứ theo chuẩn I2C, các đường SDA và SCL trên các thiết bị có cấu hình “cực góp mở” (open-drain hoặc open-collector, tham khảo các mạch số dùng transistor để hiểu thêm), nghĩa là cần có các “điện trở kéo lên” (pull-up resistor) cho các đường này. Ở trạng thái nghỉ (Idle), 2 chân SDA và SCL ở mức cao. Hình 1 mô tả một mô hình mạng TWI (I2C) cơ bản

Hình 1.Mạng TWI (I2C) với nhiều thiết bị và 2 điện trở kéo lên cho SDA, SCL.

      Tiếp theo chúng ta tìm hiểu một số khái niệm và đặc điểm của TWI. Các khái niệm và đặc điểm tôi đề cập dưới đây được dùng cho cả TWI và I2C, nếu có sự khác biệt tôi sẽ giải thích thêm.

      Master: là chip khởi động quá trình truyền nhận, phát đi địa chỉ của thiết bị cần giao tiếp và tạo xung giữ nhịp trên đường SCL.
Slave: là chip có một địa chỉ cố định, được gọi bởi Master và phục vụ yêu cầu từ Master.
SDA- Serial Data: là đường dữ liệu nối tiếp, tất cả các thông tin về địa chỉ hay dữ liệu đều được truyền trên đường này theo thứ tự từng bit một. Chú ý là trong chuẩn I2C, bit có trọng số lớn nhất (MS  được truyền trước nhất, đặc điểm này ngược lại với chuẩn UART.
SCL –Serial Clock: là đường giữ nhịp nối tiếp. TWI (I2C) là chuần truyền thông nối tiếp đồng bộ, cần có 1 đường tạo xung giữ nhịp cho quá trình truyền/nhận, cứ mỗi xung trên đường giữ nhịp SCL, một bit dữ liệu trên đường SDA sẽ được lấy mẫu (sample). Dữ liệu nối tiếp trên đường SDA được lấy mẫu khi đường SCL ở mức cao trong một chu kỳ giữ nhịp, vì thế đường SDA không được đổi trạng thái khi SCL ở mức cao (trừ START và STOP condition). Chân SDA có thể được đổi trạng thái khi SCL ở mức thấp.

Các function hỗ trợ giao tiếp

      Trong bài viết này mình sẽ cài đặt I2C mềm trên VĐK MSP430 tuy nhiên hoàn toàn có thể sửa lại các hàm set mức logic GPIO để tương thích với VĐK khác.

      Quá trình truyền nhận dữ liệu:

Hình 2. Biểu đồ thời gian tổng quát cho quá trình giao tiếp.

o    Gửi tín hiệu Start (S) từ Master đến slave để bắt đầu quá trình truyền nhận.
o    Gửi từng bit dữ liệu (B1…Bn) từ Master đến slave.
o    Gửi tín hiệu Stop (P) từ Master đến Slave kết thúc quá trình truyền nhận.

Let’s Start

      Việc đầu tiên cần làm là define các thanh ghi và các chân GPIO muốn sử dụng làm SCL, SDA cũng như thời gian delay.

Mã:

#define I2C_PxSEL        P1SEL

#define I2C_PxSEL2        P1SEL2

#define I2C_PxDIR        P1DIR

#define I2C_PxOUT        P1OUT

#define I2C_PxIN        P1IN

#define SCL               BIT6

#define SDA                    BIT7

#define ACK             0x00

#define NACK            0x01

#define TIME_DELAY              100

#define I2C_DELAY() __delay_cycles(TIME_DELAY)

      Các hàm cơ bản cần thiết cho giao tiếp như sau:

Mã:

unsigned char Read_SCL(void); // Set SCL as input and return current level of line, 0 or 1, nomal is 1 because pullup by res

unsigned char Read_SDA(void); // Set SDA as input and return current level of line, 0 or 1, nomal is 0 because pull by res

void Clear_SCL(void); // Actively drive SCL signal Low

void Clear_SDA(void); // Actively drive SDA signal Low

void I2C_Config(void);

void I2C_Start(void);

void I2C_Stop(void);

void I2C_Writebit(unsigned char bit);

unsigned char I2C_Readbit(void);

void I2C_WriteByte(unsigned char Data);

unsigned char I2C_ReadByte(void);

void I2C_WriteData(unsigned char *Data, unsigned char DevideAddr, unsigned char Register, unsigned char nLength);

void I2C_ReadData(unsigned char *Buff, unsigned char DevideAddr, unsigned char Register,   unsigned char nLength);

      Các hàm set Logic GPIO

      I2C sử dụng 2 chân GPIO để giao tiếp. Ứng với mỗi chân này sẽ có các thao tác cơ bản là set xuống mức LOW, set lên mức HIGH và đọc mức logic.

Lưu ý, I2C thường được kéo lên nguồn bằng trở pullup, vì vậy khi set GPIO là chân INPUT thì mặc nếu không có điều khiển, mức logic sẽ là mức cao. Vì vậy có thể chỉ cần thực hiện 1 hàm cho việc set mức HIGH và đọc mức logic. Đây cũng là phần cần chỉnh sửa nếu muốn sử dụng cho VĐK khác.

Phần cài đặt ứng với MSP430 như sau:

Mã:

/*——————————————————————————–

Function : Read_SCL

Purpose : Set SCL như một input và trả về mức logic 0 hoặc 1, bằng 1 khi có trở treo.

Return : mức logic của chân SCL

——————————————————————————–*/

unsigned char Read_SCL(void)

{

I2C_PxDIR &= ~SCL;

return((I2C_PxIN & SCL) != 0);

}

/*——————————————————————————–

Function : Read_SDA

Purpose : Set SDA như một input và trả về mức logic 0 hoặc 1, bằng 1 khi có trở treo.

Return : mức logic của chân SDA

——————————————————————————–*/

unsigned char Read_SDA(void)

{

I2C_PxDIR &= ~SDA;

return((I2C_PxIN & SDA) != 0);

}

 

/*——————————————————————————–

Function : Clear_SCL

Purpose : Set SCL như một Output với mức logic 0

——————————————————————————-*/

void Clear_SCL(void)

{

I2C_PxDIR |= SCL;

}

/*——————————————————————————–

Function : Clear_SDA

Purpose : Set SDA như một Output với mức logic 0

——————————————————————————–*/

void Clear_SDA(void)

{

I2C_PxDIR |= SDA;

       }

      Khởi tạo I2C

     Để khởi tạo I2C, ta cần cấu hình các chân IO là GPIO. Ở trạng thái ban đầu của chuẩn I2C, các chân này có mức logic HIGH. Lưu ý gọi hàm I2C_Init() trước khi muốn sử dụng I2C.

void I2C_Init(void)
{

// Thiết lập SCL và SDA là GPIO.
I2C_PxSEL &= ~(SCL + SDA);
I2C_PxSEL2 &= ~(SCL + SDA);
Thiết lập SCL và SDA là input và ở mức logic 1.
I2C_PxDIR &= ~(SCL + SDA);
I2C_PxOUT &= ~(SCL + SDA);​

}

 

      Tín hiệu Start và Stop

       Để thực hiện start, ta kéo chân SDA xuống thấp trong khi SCL đang ở mức cao.

void I2C_Start(void)
{

Read_SDA(); //set SDA ở mức logic 1
I2C_DELAY();
Clear_SDA(); //set SDA mức logic 0, trong khi SCL là 1
I2C_DELAY();
Clear_SCL(); //set SCL mức logic 0​

}

       Stop thực hiện tương tự start nhưng SDA kéo lên cao trong khi SCL đang ở mức cao.

void I2C_Stop(void)
{

Clear_SDA(); //set SDA to 0
I2C_DELAY();
Read_SCL(); //set SCL to 1
I2C_DELAY();
Read_SDA(); //set SDA to 1​

}

      Ghi 1 byte

      Chia nhỏ việc ghi 1Byte thành thực hiện liên tiếp các hàm ghi 1Bit. Lưu ý, chân SDA sẽ thực hiện thay đổi mức logic trong khi SCL đang ở mức thấp. Khi có sườn lên SCL, dữ liệu sẽ được ghi nhận.

      Lưu ý, phần code này chỉ đơn thuần là đọc ghi dữ liệu trên SDA, chưa có các phần theo chuẩn giao tiếp I2C như gửi địa chỉ thiết bị, địa chỉ thanh ghi… Các công việc trên sẽ cài đặt ở phần wite khối dữ liệu.

void I2C_Writebit(unsigned char bit)
{

if(bit)

Read_SDA();​

else

Clear_SDA();​

I2C_DELAY();
Read_SCL();
I2C_DELAY();
Clear_SCL();​

}

      Gọi liên tiếp các hàm I2C_Writebit để thực hiện write byte. Lưu ý, khi gửi xong 8bit, thiết bị sẽ gửi đến host 1bit ACK để xác nhận việc tryền dữ liệu đã thành công.

Mã:

void I2C_WriteByte(unsigned char Data)

{

unsigned char nBit;

for(nBit = 0; nBit <8; nBit++)

{

I2C_Writebit((Data & 0x80) != 0);

Data <<= 1;

}

I2C_Readbit();

}

       Đọc 1 byte

Thực hiên tương tự đối với hàm ghi 1Byte.

unsigned char I2C_Readbit(void)
{

unsigned char bit;
Read_SDA();
I2C_DELAY();
Read_SCL();
bit = Read_SDA();
I2C_DELAY();
Clear_SCL();
return bit;​

}
unsigned char I2C_ReadByte(void)
{

unsigned char Buff = 0;
unsigned char nBit;
for(nBit = 0; nBit < 8; nBit++)
{

Buff = (Buff << 1) | I2C_Readbit();​

}
return Buff;​

}

       Ghi một khối dữ liệu
Để thực hiện ghi một khối dữ liệu ta cài đặt như sau:

  • Sử dụng một con trỏ để trỏ tới khối dữ liệu cần ghi.
  • Gửi 1byte gồm 7bit địa chỉ và 1bit quy định việc đọc hay ghi dữ liệu (trong trường hợp này là ghi)
  • Gửi 1byte chưa địa chỉ thanh ghi cần tác động
  • Sử dụng liên tiếp các hàm ghi dữ liệu
  • Phát tín hiệu stop

void I2C_WriteData(unsigned char *Data, unsigned char DevideAddr, unsigned char Register, unsigned char nLength)
{

unsigned char nIndex;
I2C_Start();
I2C_WriteByte(DevideAddr << 1); // 7 bit địa chỉ của slave
I2C_WriteByte(Register);
for(nIndex = 0; nIndex < nLength; nIndex++)
{

I2C_WriteByte(*(Data + nIndex));​

}
I2C_Readbit();
I2C_Stop();​

}

       Đọc một khối dữ liệu

Cài đặt tương tự với việc ghi một khối dữ liệu, tuy nhiên có điểm khác biệt là sau khi gửi địa chỉ thiết bị và địa chỉ thanh ghi, ta cần phát tín hiệu reset.

Cụ thể như sau:

  • Sử dụng một con trỏ để trỏ tới vùng bộ nhớ dùng để lưu dữ liệu đọc được từ devide
  • Gửi 1byte gồm 7bit địa chỉ và 1bit yêu cầu ghi
  • Gửi 1byte chưa địa chỉ thanh ghi cần tác động
  • Phát tín hiệu reset
  • Gửi 1byte gồm 7bit địa chỉ và 1bit yêu cầu đọc
  • Gọi liên tiếp các hàm đọc dữ liệu
  • Gửi tín hiệu NACK báo hiệu việc kết thúc đọc dữ liệu và phát tín hiệu stop

void I2C_ReadData(unsigned char *Buff, unsigned char DevideAddr, unsigned char Register, unsigned char nLength)
{

unsigned char nIndex;
I2C_Start();
I2C_WriteByte(DevideAddr << 1);
I2C_WriteByte(Register);
I2C_Stop();
__no_operation(); // Short delay
I2C_Start();
__no_operation(); // Short delay
I2C_WriteByte((DevideAddr << 1) | 1);
for(nIndex = 0; nIndex < nLength; nIndex++)
{

*(Buff + nIndex) = I2C_ReadByte();
if(nIndex > 0)I2C_Writebit(ACK);​

}
I2C_Writebit(NACK);
I2C_Stop();​

}

Toàn bộ source code và library có thể tham khảo ở link sau

PHẠM CÔNG ANH HUY