Giao thức I2C là một chuẩn truyền thông đồng bộ trong vi xử lý. Giao thức này được sử dụng rất nhiều trong thiết kế vì tính đơn giản và dễ dàng làm việc. Tuy nhiên bù lại tốc độ thường không cao và chỉ sử dụng trong mạch (on board)
Bài 10 trong serie Học lập trình STM32 từ A tới Z
Giao thức I2C là gì
I2C viết tắt của Inter- Integrated Circuit là một phương thức giao tiếp được phát triển bởi hãng Philips Semiconductors. Dùng để truyền tín hiệu giữa vi xử lý và các IC trên các bus nối tiếp.
Đặc điểm:
- Tốc độ không cao
- Thường sử dụng onboard với đường truyền ngắn
- Nối được nhiều thiết bị trên cùng một bus
- Giao tiếp đồng bộ, sử dụng Clock từ master
- Sử dụng 7 bit hoặc 10 bit địa chỉ
- Chỉ sử dụng 2 chân tín hiệu SDA, SCL
- Có 2 tốc độ tiêu chuẩn là Standard mode (100 kb/s)và Low mode (10 kbit/s)
Kết nối vật lý của giao thức I2C
Bus I2C sử dụng 2 dây tín hiệu là SDA (Serial Data Line) và SCL (Serial Clock Line). Dữ liệu truyền trên SDA được đồng bộ với mỗi xung SCL. Đường SCL chỉ master mới có quyền điều khiển.
Tất cả các thiết bị đều dùng chung 2 đường tín hiệu này.
Hai đường bus SDA và SCL hoạt động ở chế độ Open Drain hay cực máng hở. Nghĩa là tất cả các thiết bị trong mạng đều chỉ có thể lái 2 chân này về 0 chứ ko thể kéo lên 1. Để tránh việc sảy ra ngắn mạch khi thiết bị này kéo lên cao, thiết bị kia kéo xuống thấp.
Để giữ mức logic là 1 ở trạng thái mặc định phải mắc thêm 2 điện trở treo lên Vcc (thường từ 1k – 4k7).
Mỗi Bus I2C sẽ có 3 chế độ chính:
- Một Master, nhiều Slave
- Nhiều master, nhiều Slave
- Một Master, một Slave
Tại một thời điểm truyền nhận dữ liệu chỉ có một Master được hoạt động, điều khiển dây SCL và phát tín hiệu bắt đầu tới các Slave.
Tất cả các thiết bị đáp ứng sự điều hướng của Master gọi là Slave. Giữa các Slave với nhau, phân biệt bằng 7bit địa chỉ.
Cách truyền dữ liệu của giao thức I2C
Giao thức (phương thức giao tiếp) là cách các thiết bị đã thống nhất với nhau khi sử dụng một chuẩn nào đó để truyền và nhận tín hiệu.
Dữ liệu được truyền đi trên dây SDA được thực hiện như sau:
- Master thực hiện điều kiện bắt đầu I2C (Start Condition)
- Gửi địa chỉ 7 bit + 1bit Đọc/Ghi (R/W) để giao tiếp muốn đọc hoặc ghi dữ liệu tại Slave có địa chỉ trên
- Nhận phải hồi từ Bus, nếu có một bit ACK (Kéo SDA xuống thấp) Master sẽ gửi dữ liệu
- Nếu là đọc dữ liệu R/W bit = 1, chân SDA của master sẽ là input, đọc dữ liệu từ Slave gửi về. Nếu là ghi dữ liệu R/W = 0, chân SDA sẽ là output ghi dữ liệu vào Slave
- Truyền điều khiện kết thúc (Stop Condition)
Mỗi lần giao tiếp có cấu trúc như sau:
Start condition( Điều khiện bắt đầu)
Bất cứ khi nào một thiết bị chủ / IC quyết định bắt đầu một giao dịch, nó sẽ chuyển mạch SDA từ mức điện áp cao xuống mức điện áp thấp trước khi đường SCL chuyển từ cao xuống thấp.
Khi điều kiện bắt đầu được gửi bởi thiết bị Master, tất cả các thiết bị Slave đều hoạt động ngay cả khi chúng ở chế độ ngủ (sleep mode) và đợi bit địa chỉ.
Bit Read/Write
Bit này xác định hướng truyền dữ liệu. Nếu thiết bị Master / IC cần gửi dữ liệu đến thiết bị Slave, bit này được thiết lập là ‘0’. Nếu IC Master cần nhận dữ liệu từ thiết bị Slave, bit này được thiết lập là ‘1’.
Bit ACK / NACK
ACK / NACK là viết tắt của Acknowledged/Not-Acknowledged. Nếu địa chỉ vật lý của bất kỳ thiết bị Slave nào trùng với địa chỉ được thiết bị Master phát, giá trị của bit này được set là ‘0’ bởi thiết bị Slave. Ngược lại, nó vẫn ở mức logic ‘1’ (mặc định).
Khối dữ liệu
Nó bao gồm 8 bit và chúng được thiết lập bởi bên gửi, với các bit dữ liệu cần truyền tới bên nhận. Khối này được theo sau bởi một bit ACK / NACK và được set thành ‘0’ bởi bên nhận nếu nó nhận thành công dữ liệu. Ngược lại, nó vẫn ở mức logic ‘1’.
Sự kết hợp của khối dữ liệu theo sau bởi bit ACK / NACK được lặp lại cho đến quá trình truyền dữ liệu được hoàn tất.
Điều kiện kết thúc (Stop condition)
Sau khi các khung dữ liệu cần thiết được truyền qua đường SDA, thiết bị Master chuyển đường SDA từ mức điện áp thấp sang mức điện áp cao trước khi đường SCL chuyển từ cao xuống thấp.
Giới thiệu chip thời gian thực DS3231
DS3231 là chip thời gian thực, giao tiếp thông qua giao thức I2C. Làm việc tại dải điện áp từ 2.3 đến 5.5V, tích hợp sẵn thạch anh nội nên rất nhỏ gọn.
Có 2 chế độ hẹn giờ có thể Config từng giây tới ngày trong tháng.
Datasheet các bạn down tại đây: https://datasheets.maximintegrated.com/en/ds/DS3231.pdf
DS3231 có địa chỉ 7bit là 0x68. Cách đọc và truyền được mô tả như trong hình
Bảng sau mô ta địa chỉ lưu các giá trị ngày tháng năm đó là từ 0x00 tới 0x06.
Các byte từ 0x07 tới 0x0D lưu giá trị Hẹn giờ A1M và A2M
Các byte từ 0x0E tới 0x12 là các thanh ghi điều khiển DS3231
Cấu hình giao thức I2C trên STM32 CubeMX
Mở phần mềm, chọn chip STM32F103C8 nhấn start project.
Trong Sys chọn Debug : Serial Wire. Nêu rõ trong Bài 3
Trong Tab Connectivity: Chọn giao thức I2C1
Mode: I2C
Parameter: để mặc định với Speed mode là standard, Clock speed 100khz.
Trong NVIC tick chọn bật ngắt cho I2C event.
Config thêm PC13 là Led để kiểm tra trạng thái I2C
Đặt tên project rồi Gencode
Lập trình giao thức I2C
Trong KeilC chúng ta cấu hình như sau.
Khai báo địa chỉ của DS3231 là 0x68<<1. Các bạn phải dịch trái 1 đơn vị, vì các hàm i2c của hal yêu cầu vậy, bit thứ 8 là R/W sẽ được thêm vào ngay sau giá trị địa chỉ đó.
Khai báo 2 nguyên mẫu hàm là BCD2DEC và DEC2BCD. Hai hàm này có tác dụng chuyển đổi dữ liệu kiểu BCD thành kiểu số thập phân(hay hexa, bin) và ngược lại. Vì dữ liệu đọc ghi vào Ds3231 là kiểu BCD
Hai mảng lưu dữ liệu truyền và nhận từ BCD gồm 7 byte
Một biến Status để đọc phản hồi của giao thức I2C
Định nghĩa biến dữ liệu kiểu Struct để lưu các giá trị thời gian, sau đó khởi tạo 2 biến TimeNow để đọc giá trị thời gian và Time Set để cài đặt thời gian lên DS3231
Sau đó viết hàm thực thi chuển đổi BCD2DEC và ngược lại
Các bạn tìm hàm Call back của giao thức I2C bằng cách tìm từ stm32f1xx_it.c như các bài trước, hoặc sử dụng list functions như sau.
Mở tab Function ở phía work space project, mở file I2C và tìm tới hàm
HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c)
Copy phần thực thi của hàm đó và dán vào phần tiền xử lý trên main(). Sau đó thêm các bước sau.
Điều hướng với lệnh if(hi2c->Instance == I2C1) nếu I2C1 sảy ra ngắt
Lưu các giá trị thời gian vào biến DS3231_TimeNow, với byte đầu tiên là giây, phút, giờ …. Sử dụng hàm chuyển đổi BCD2DEC
Trong phần main(), đầu tiên chúng ta quét thử xem có thiết bị i2c nào kết nối không bằng cách.
Tắt Led PC13.
Cho hàm HAL_I2C_IsDeviceReady vào vòng lặp for, khi có kết nối, sẽ lưu địa chỉ của Slave đó vào Status và bật Led PC13
Trước While(1) chúng ta sẽ ghi giá trị thời gian cho DS3231, hiện tại là 14h42 ngày 17 tháng 7 năm 2020. Ta sẽ ghi như sau. Sau đó ghi vào I2C bằng hàm HAL_I2C_Mem_Write_IT(&hi2c1,DS3231_ADDRESS,0x00,I2C_MEMADD_SIZE_8BIT,u8_tranBuffer,7);
Với 0x00 là địa chỉ bắt đầu,
I2C_MEMADD_SIZE_8BIT: kiểu dữ liệu bộ nhớ là 8bit
Gửi 7 byte: từ 0x00 –> 0x06
Trong While(1) ta sẽ đọc dữ liệu từ DS3231 mỗi 5s một lần, ta làm như sau
Nhấn Build F7 và nạp chương trình. Kết nối DS3231 SDA với PB7, SCL với PB6, sau đó nhấn vào debug .
Trong debug Add 2 biến Time_now và Status vào Watch 1
Ta thấy rằng Status hiển thị 0xD1 nghĩa là 11010001 chính là địa chỉ 0x68 <<1 + R/W bit, với R/W = 1 nghĩa là đọc dữ liệu
Thời gian sẽ hiển thị đúng với giá trị ta nạp vào và sẽ thay đổi mỗi 5S một lần.
Kết
Giao thức I2C rất dễ sử dụng, vì các slave đều phản hồi lại master trước khi truyền, đó là các bit Ack Nack, vậy nên việc truyền sai địa chỉ, truyền sai dữ liệu rất dễ phát hiện.
Hi vọng bạn đã nắm rõ cách thức hoạt động của giao thức I2C, hãy chuyển tới bài tiếp theo để tiếp tục học nhé
Cho mình hỏi ở cái hàm HAl_I2C_MEM_READ_IT cái đoạn gửi địa chỉ 0x00 mà trong khi đó là dữ liệu bộ nhớ từ 0x00-0x06 thì chỉ cần đưa địa chỉ đầu tiên là 0x00 thôi à.
Đúng rồi bạn, bạn chỉ cần xác định địa chỉ đầu tiên, số byte, và kiểu byte là nó tự động đọc từ đầu đến cuối
Ngay chỗ HAl_I2C_MEM_READ_IT while(1) ngay đoạn u8_tranBuffer phải là u8_revBuffer đúng không anh?
anh chưa hiểu ý em là sao
Hình như đoạn đó anh phải đọc dữ liệu từ slave ra vi xử lí, nên anh phải lưu vào buffer là hàm u8_revBuffer , vì hàm callback của anh , anh dùng u8_revBuffer ạ
mình cũng có suy nghĩ giống bạn, u8_tranBuffer là mảng chứa data thời gian thiết lập, u8_revBuffer mới là mảng chứa thời gian trả về của IC, nên mình nghĩ là cần đọc data từ u8_revBuffer. có thể ad bị nhầm lẫn.
mình làm theo nhưng không chạy được, debug không thấy sự thay đổi của biến timenow. À mà mình thắc mắc tại sao biến timeset không có chức năng gì trong code ta?
anh ơi mình hiển thị thời gian lên LCD thì làm sao ạ