1. Giới thiệu chung
Trong các hệ thống Embedded nói chung cũng như các hệ thống Linux Embedded nói riêng, chúng đều cần các dữ liệu lấy từ bên ngoài thông qua các cảm biến như: ánh sáng, nhiệt độ, độ ẩm,… Thông thường, các cảm biến này thường không đi liền với SoC mà được tính hợp riêng trong 1 module tương tác với SoC thông qua các giao thức như SPI, I2C, UART,…
Ở bài viết lần này, chúng ta cùng nhau tìm hiểu về giao thức I2C – giao thức được sử dụng phổ biến trong các module cảm biến
Tổng quan giao thức I2C
I2C ( Inter – Integrated Circuit) là 1 giao thức giao tiếp nối tiếp đồng bộ được phát triển bởi Philips Semiconductors, sử dụng để truyền nhận dữ liệu giữa các IC với nhau chỉ sử dụng hai đường truyền tín hiệu.
Các bit dữ liệu sẽ được truyền từng bit một theo các khoảng thời gian đều đặn được thiết lập bởi 1 tín hiệu xung clock.
Bus I2C thường được sử dụng để giao tiếp ngoại vi cho rất nhiều loại IC khác nhau như các loại vi điều khiển, cảm biến, EEPROM, … Và có thể dễ dàng thêm các thiết bị vào mạng bằng việc kết nối trực tiếp thiết bị vào bus
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-9.png)
Nguyên lý hoạt động của I2C
I2C sử dụng 2 đường truyền tín hiệu:
- SCL – Serial Clock Line : Tạo xung nhịp đồng hồ do Master phát đi
- SDA – Serial Data Line : Đường truyền nhận dữ liệu giữa Master – Slave
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-10.png)
Thiết bị Master là 1 vi điều khiển, nó có nhiệm vụ điều khiển đường tín hiệu SCL và gửi nhận dữ liệu hay lệnh thông qua đường SDA đến các thiết bị khác.
Các thiết bị nhận các dữ liệu lệnh và tín hiệu từ thiết bị Master được gọi là các thiết bị Slave. Slave sẽ đáp ứng các yêu cầu của Master và được định danh bởi 7 bit địa chỉ. Khi Master muốn giao tiếp với Slave thì sẽ gửi 7 bits địa chỉ của thiết bị muốn giao tiếp vào đường SDA.
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-11.png)
Master và Slave được kết nối với nhau như hình trên. Hai đường bus SCL và SDA đều hoạt động ở chế độ Open Drain, nghĩa là bất cứ thiết bị nào kết nối với mạng I2C này cũng chỉ có thể kéo 2 đường bus này xuống mức thấp (LOW), nhưng lại không thể kéo được lên mức cao. Vì để tránh trường hợp bus vừa bị 1 thiết bị kéo lên mức cao vừa bị 1 thiết bị khác kéo xuống mức thấp gây hiện tượng ngắn mạch. Do đó cần có 1 điện trờ ( từ 1 – 4,7 kΩ) để giữ mặc định ở mức cao.
Khung truyền dữ liệu
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-12.png)
Start Condition: Bắt đầu một frame truyền nhận giữ liệu
Address: 7 bits địa chỉ của slave mà master muốn giao tiếp
Read/Write: Xác định hướng dữ liệu được truyền của Master đối với Slave ( Master gửi dữ liệu – “0” , Master nhận dữ liệu – “1”).
ACK/NACK (Acknowledged / Not Acknowledged): Nếu có slave trùng với address mà master phát ra thì sẽ tạo ra một bit có giá trị 0 để thông báo, nếu không thì mặc định là “1”
Stop Condion: Kết thúc một frame truyền nhận dữ liệu
Trình tự gửi nhận dữ liệu
- Step 1: Master sẽ gửi Start Condition đến tất cả slave
- Step 2: Master gửi 7 bits address của thiết bị slave mà master muốn giao tiếp cùng với 1 bit Read/Write
- Step 3: Mỗi slave ở trong mạng sẽ so sánh với địa chỉ mà master phát ra, nếu có slave trùng khớp với địa chỉ thì thiết bị đó sẽ kéo SDA xuống LOW để tạo ra ACK/NACK có giá trị bằng 0, ngược lại SDA sẽ ở mức HIGH và tạo ra ACK/NACK có giá trị bằng 1.
- Step 4.1: Nếu Read/Write có giá trị 0, thì master sẽ gửi 8 bits dữ liệu đến slave. Nếu slave nhận được thành công thì sẽ kéo SDA xuống mức LOW để tạo ra ACK/NACK có giá trị bằng 0, ngược lại SDA sẽ ở mức HIGH và tạo ra ACK/NACK có giá trị bằng 1.
- Step 4.2. Nếu Read/Write có giá trị 1, thì slave sẽ gửi 8 bits dữ liệu đến slave. Nếu master nhận được thành công thì sẽ kéo SDA xuống mức LOW để tạo ra ACK/NACK có giá trị bằng 0, ngược lại SDA sẽ ở mức HIGH và tạo ra ACK/NACK có giá trị bằng 1.
- Step 5: Sau khi tất cả dữ liệu được gửi nhận hoàn tất, master sẽ tạo ra một Stop Condition để kết thúc frame truyền
2. I2C trong Linux
Tìm hiểu về ví dụ sử dụng OLED kết nói với board BeagleBone Black thông qua giao thức I2C.
Kiến trúc I2C trong Linux
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-13.png)
Ta cùng nhau tìm hiểu về 3 tầng: User Space, Kernel Space và cuối cùng là Hardware. Cụ thể ở đây là ví dụ với màn hình OLED
OLED Appliaction có nhiệm vụ tương tác trực tiếp với người dùng. Người dùng có thể nhập kí tự in lên màn hình, xóa màn hình,… Application sử dụng các API do OLED Driver (trong tầng Kernel Space) cung cấp.
OLED Driver có nhiệm vụ điều khiển logic cho màn hình OLED. Giả sử với màn hình OLED, muốn xóa hết kí tự thì cần nhận một command “0x00” thông qua SDA hoặc khi muốn hiển thị kí tự “a” lên màn hình cần command “0x40”; chính driver sẽ thực hiện các công việc điều khiển các công việc đó.
OLED Driver sẽ tương tác với I2C Core – framework I2C của Linux. I2C Core có nhiệm vụ cung cấp các API cho OLED Driver sử dụng và quản lý OLED Driver khi điều khiển device cụ thể trong số các thiết bị đang cắm vào bus I2C.
I2C Core tương tác với I2C BeagleBone Driver (ví dụ cụ thể trong bài viết này là BeagleBone Black). Nó có nhiệm vụ tương tác với các thanh ghi của SoC (ở đây là BeagleBone Black) và đăng ký các hàm để I2C Core gọi tới. Ví dụ muốn gửi data lên bus I2C thì cần ghi vào thanh ghi cụ thể nào thì I2C BeagleBone Driver đảm nhận vai trò đó.
Khởi tạo OLED trong Device Tree
Đầu tiên, để viết được device driver cho OLED trên BeagleBone Black, chúng ta cần xác định đường bus I2C muốn sử dụng rồi từ đó khai báo một node trong Device Tree tương ứng với màn hình OLED đó.
Node trên có ý nghĩa khai báo địa chỉ thiết bị kết nối thông qua I2C nằm trên bus I2C mà chúng ta đã xác định trước đó. Ví dụ ta kết nối nối OLED với BBB thông qua bus I2C0, node của OLED sẽ được khai báo với vai trò là 1 node con trong node I2C0 như sau:
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-14.png)
Trường reg trong node oled có nhiệm vụ khai báo địa chỉa của OLED trong không gian đường bus I2C0 mà mình muốn sử dụng. Trường “compatible” và “status” đã được nhắc tới chi tiết trong bài Device Tree.
Triển khai OLED Driver
Cấu trúc cơ bản của một I2C device driver (OLED driver) bao gồm các thành phần như sau:
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-15.png)
struct i2c_driver là một struct mang các thông tin cơ bản của device và các hàm sử dụng trong driver và struct này được sử dụng để đăng kí driver với hệ thống.
probe(): Cấu hình tốc độ, chế độ hoạt động, và khởi tạo OLED … và được gọi ra khi trường “compatible” trong node I2C đã khai báo match với “compatible” trong driver.
remove(): Giải phóng toàn bộ tài nguyên(RAM, I/O, …) mà driver đang sử dụng. Được gọi ra khi rmmod driver.
module_i2c_driver() là macro được sử dụng để đăng ký OLED driver.
Ví dụ: Sử dụng 2 chân P9-18 và P9-17 (I2C1) trên BBB để kết nối với OLED SSD11306
Đầu tiên, ta tra bảng Header của beaglebone black, thu được các thông tin về chân I2C dự kiến sử dụng:
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-16.png)
- Địa chỉ của chân P9_17: 0x95c
- Địa chỉ của chân P9_18: 0x958
- Mode I2C: Mode 2
Sau đó, để tránh việc trùng lặp khai báo node cấu hình, ta cần xác định xem 2 chân P9_17 và P9_18 đã được sử dụng trong Device Tree hay chưa bằng cách dịch ngược file dtb trên beaglebone blak sang file dts để thuận tiện cho việc tìm kiếm. Nếu đã được sử dụng rồi, ta có thể sử dụng chân Pin khác hoặc xóa node cấu hình đã sử dụng 2 chân Pin đó đi và cấu hình 2 Pin với chức năng I2C.
Trong device tree, đầu tiên ta khai báo cấu hình pinmux cho 2 chân 17 18 trên tại node “am33x_pinmux”:
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-17.png)
Tiếp đến là node khai báo chức năng “i2c1” có nhiệm vụ khai báo bus I2C1 sử dụng 2 chân P9_17 và P9_18 làm 2 đường dây SCL và SDA thông qua việc sử dụng node cấu hình pinmux ở trên. Và trong node “i2c1” đó, ta thực hiện khai báo node cho device – ssd1306 OLED:
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-18.png)
Cuối cùng, ta thực hiện nạp lại Deivce Tree vào trong Beaglebone Black. Nếu thành công, ta sẽ thấy trong thư mục /dev sẽ xuất hiện 1 deivce file là i2c-1. Ngoài ra, để kiếm tra xem node device tree khai báo ssd1306 đã được add vào hệ thống thành công hay chưa, ta vào trong thư mục /sys/bus/i2c/devices/ sẽ thấy các device được add vào i2c: ( với ssd1306 đã khai báo tương ứng với 1-003c, có nghĩa là thiết bị trên đường bus i2c1 đã được khâi báo với địa chỉ 0x3c)
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-19.png)
Sau khi đã thực hiện cấu hình thành công thiết bị trong Device Tree, ta thực hiện viết một driver demo đơn giản cho I2C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <linux/module.h> #include <linux/init.h> #include <linux/slab.h> #include <linux/i2c.h> // thư viện cung cấp các hàm tương tác với i2c #include <linux/delay.h> #include <linux/kernel.h> static int i2c_demo_probe(struct i2c_client *client, const struct i2c_device_id *id) { pr_info("Jay: %s, %d\n", __func__, __LINE__); return 0; } static int i2c_demo_remove(struct i2c_client *client) { pr_info("Jay: %s, %d\n", __func__, __LINE__); return 0; } // khai báo trường compatiple để match với device tree static const struct of_device_id i2c_demo_of_match[] = { { .compatible = "b3,ssd1306" }, { }, }; // struct dùng để đăng kí driver i2c với hệ thống static struct i2c_driver i2c_demo_driver = { .probe = i2c_demo_probe, .remove = i2c_demo_remove, // các thông tin của driver .driver = { // các thông tin của driver .name = "ssd1306", .owner = THIS_MODULE, //match driver với node trong device tree .of_match_table = i2c_demo_of_match, }, }; // đăng kí driver với hệ thống module_i2c_driver(i2c_demo_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Jay <huuthinh1603@gmail.com>"); MODULE_DESCRIPTION("SSD1306"); |
Truyền nhận dữ liệu
Linux cung cấp 2 hàm cho phép gửi nhận dữ liệu qua I2C:
1 |
int i2c_master_send(struct i2c_client *client, const char *buf, int count); |
1 |
int i2c_master_recv(struct i2c_client *client, char *buf, int count); |
Trong đó:
- client: tham số được truyền vào hàm probe
- buf: là dữ liệu được gửi/nhận
- count: là số lượng dữ liệu gửi/nhận được tính theo byte
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-20.png)
Khi hàm i2c_master_send() được gọi bằng driver thì lệnh đó sẽ được truyền tới I2C Core – framework của Linux, framework này sau đó sẽ gọi rất nhiều hàm mà cuối cùng là hàm master_xfeer() của I2C BeagleBone driver. Hàm này có nhiệm vụ ghi dữ liệu xuống các thanh ghi của BeagleBone để chuyển dữ liệu ra ngoài các chân Pin.
Tương tự với hàm i2c_master_recv() cũng có cơ chế hoạt động như trên.
Khởi tạo bằng sysfs
Ta thấy việc khởi tạo thiết bị thông qua Device Tree thường rất phức tạp, gây mất thời gian khi build và có thể dễ phát sinh ra các lỗi ngoài mong muốn. Nên thông thường, trong quá trình develop, ta thường sử dụng cơ chế đăng ký device I2C qua sysfs.
Đăng ký một i2c-device:
1 |
echo “name” “address” > /sys/bus/i2c/devices/i2c-%d/new_device |
Hủy đăng kí một i2c-device:
1 |
echo “address” > /sys/bus/i2c/device/i2c-%d/delete_device |
Với %d là số thự tự của i2c mình muốn đăng ký.