👉 Table of Contents
I²C kết hợp các tính năng tốt nhất của SPI và UART. Với I2C, bạn có thể kết nối nhiều Slave với một master duy nhất (như SPI) và bạn có thể có nhiều master điều khiển một hoặc nhiều Slave.
Điều này thực sự hữu ích khi bạn muốn có nhiều hơn một vi điều khiển ghi dữ liệu vào một thẻ nhớ duy nhất hoặc hiển thị văn bản trên một màn hình LCD, hoặc điều khiển đồng thời nhiều màn hình LCD.
Giới thiệu giao tiếp I2C?
Giống như giao tiếp UART, I²C chỉ sử dụng hai dây để truyền dữ liệu giữa các thiết bị:
SDA (Serial Data) - đường truyền cho Master và Slave để gửi và nhận dữ liệu.
SCL (Serial Clock) - đường mang tín hiệu xung nhịp.
I²C là một giao thức truyền thông nối tiếp, vì vậy dữ liệu được truyền từng bit dọc theo một đường duy nhất (đường SDA).
Giống như SPI, I²C là đồng bộ, do đó đầu ra của các bit được đồng bộ hóa với việc lấy mẫu các bit bởi một tín hiệu xung nhịp được chia sẻ giữa master và Slave. Tín hiệu xung nhịp luôn được điều khiển bởi Master.
Cách hoạt động của I2C
Với I2C, dữ liệu được truyền trong các tin nhắn. Tin nhắn được chia thành các khung dữ liệu.
Mỗi tin nhắn có một khung địa chỉ chứa địa chỉ nhị phân của địa chỉ Slave và một hoặc nhiều khung dữ liệu chứa dữ liệu đang được truyền.
Thông điệp cũng bao gồm điều kiện khởi động và điều kiện dừng, các bit đọc / ghi và các bit ACK / NACK giữa mỗi khung dữ liệu:
Các bước truyền dữ liệu I2C
- Master gửi điều kiện khởi động đến mọi Slave được kết nối.
- Master gửi cho mỗi Slave địa chỉ 7 bit hoặc 10 bit của Slave mà nó muốn giao tiếp, cùng với bit đọc / ghi.
- Mỗi Slave sẽ so sánh địa chỉ được gửi từ master với địa chỉ của chính nó.
- Nếu địa chỉ trùng khớp, Slave sẽ trả về một bit ACK bằng cách kéo dòng SDA xuống thấp cho một bit.
- Nếu địa chỉ từ master không khớp với địa chỉ của Slave, Slave rời khỏi đường SDA cao.
- Master gửi hoặc nhận khung dữ liệu.
- Sau khi mỗi khung dữ liệu được chuyển, thiết bị nhận trả về một bit ACK khác cho thiết bị gửi để xác nhận đã nhận thành công khung.
- Để dừng truyền dữ liệu, master gửi điều kiện dừng đến Slave bằng cách chuyển đổi mức cao SCL trước khi chuyển mức cao SDA.
⚙️Điều khiện bắt đầu (Start condition)
Bất cứ khi Master muốn truyền dữ liệu, nó sẽ chuyển mạch SDA từ mức điện áp cao xuống mức điện áp thấp sau đó đườ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ỉ.
Start condition
⚙️Khung địa chỉ:
Về lý thuyết lẫn thực tế I²C sử dụng 7 bit để định địa chỉ, do đó trên một bus có thể có tới 2^7 địa chỉ tương ứng với 128 thiết bị có thể kết nối, nhưng chỉ có 112 thiết bị có thể kết nối , 16 địa chỉ còn lại được sử dụng vào mục đích riêng.
Ngoài ra I²C còn có chế độ 10 bit địa chỉ tương đương với 1024 địa chỉ, tương tự như 7 bit, chỉ có 1008 thiết bị có thể kết nối, còn lại 16 địa chỉ sẽ dùng để sử dụng mục đích riêng.
Bài viết giới thiệu về I²C 10-bit địa chỉ: I²C Ten-bit Addresses
Danh sách địa chỉ I²C 7-bit của các thiết bị phổ biến: All I²C addresses here are in 7-bit format
Một chuỗi 7 (hoặc 10 bit) duy nhất cho mỗi Slave để xác định Slave khi Master muốn giao tiếp với nó.
Tất cả các thiết bị Slave trên bus I²C so sánh các bit địa chỉ này với địa chỉ của chúng.
⚙️Bit Read/Write
Bit này xác định hướng truyền dữ liệu.
- Nếu Master cần gửi dữ liệu đến Slave, bit này được thiết lập là ‘0’.
- Nếu Master cần nhận dữ liệu từ 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.
- Giá trị của bit này được set bởi Slave.
- Nếu địa chỉ vật lý của bất kỳ Slave nào trùng với địa chỉ được 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, Master sẽ buông tín hiệu đường SCL về điện áp cao, tiếp sau đó là giải phóng đường SDA về điện áp cao (2 đường này lên được điện áp cao là nhờ điện trở kéo lên của 2 đường Bus).
Stop condition
Giới thiệu I²C của STM32F407VET?
Ở bài viết này mình sẽ tập trung về giao tiếp I2C nằm trong chip STM32F407VET với Mode Master.
⚙️Sơ đồ khối I²C của STM32F407VET
• SCL
- Serial Clock Line: Tạo xung nhịp đồng hồ do Master phát ra.
• SDA
– Serial Data Line: Đường truyền nhận dữ liệu.
Chip STM32F407VET hỗ trợ cho chúng ta 3 bộ I2C, lần lượt là I2C1, I2C2, I2C3 tương ứng với các chân:
I2C_1 | I2C_2 | I2C_3 | |
---|---|---|---|
SCL | PB6 | PB10 | PA8 |
SDA | PB7 | PB11 | PC9 |
Lập trình Master hoặc Slave ở STM32F407VET:
-
Nếu cấu hình Master: Có thể tạo xung clock và tạo tín hiệu start, stop.
-
Nếu cấu hình Slave: Lập trình địa chỉ thiết bị I2C, chế độ kiểm tra stop bit.
-
Hỗ trợ 2 chuẩn tốc độ 100khz và 400khz.
-
Có tích hợp chế độ DMA
-
Hỗ trợ các ngắt: Ngắt buffer truyền, nhận, ngắt sự kiện, báo lỗi
⚙️Module I²C LCD2004 (I²C PCF8574)
Tài liệu về LCD-I²C: LCD
Tài liệu về HD44780: HD44780
Từ sơ đồ nguyên lý trên, STM32F407VET làm Master sẽ phát tín hiệu truyền dữ liệu qua Bus I2C đến IC PCF8574 làm thiết bị Slave, với output của IC PCF8574 ngõ ra 8 bit từ P7 → P0, các ngõ ra này sẽ kết nối với các chân điều khiển trên màn hình LCD như sau (kết nối theo chuẩn 4-bit của LCD1602 hoặc LCD2004):
⚙️4-bit data
PCF8574 | P7 | P6 | P5 | P4 |
---|---|---|---|---|
LCD | D7 | D6 | D5 | D4 |
⚙️4-bit điều khiển (chân output P3 không sử dụng)
PCF8574 | P3 | P2 | P1 | P0 |
---|---|---|---|---|
LCD | X | E | RW | RS |
Theo datasheet (driver HD44780) của màn hình LCD khi sử dụng chuẩn truyền data 4-bit thì sử dụng 4 chân
D7→ D4
- Các driver cũng gần tương tự cách hoạt động.
Việc đầu tiên, là chúng ta tìm xem giá trị địa chỉ của Slave IC trên module I2C-LCD là bao nhiêu?
⚙️Quét tìm địa chỉ Slave bằng STM32F407VET
Tạo Project bằng phần mềm MXCube: Enable I2C_1, sử dụng mode Speed SM (Standard Mode)
Chúng ta kết nối I²C của LCD với I2C1 của STM32F407VET:
I2C1 GPIO Configuration
PB6 ------> I2C1_SCL
PB7 ------> I2C1_SDA
Trong phần /* USER CODE BEGIN 2 */
, thêm môt đoạn mã để Scan các thiết bị Slave có tồn tại trên Bus I2C_1
Nguyên lý quét 7 bit địa chỉ từ 1→127 tương ứng 000_0001b → 111_1111b (0x01 → 0x7F)
Gửi một tín hiệu yêu cầu tới địa chỉ đó trên Bus SDA I2C_1, nếu tồn tại thì bit ACK sẽ được trả về và báo chúng ta biết tại địa chỉ đó có thiết bị Slave tồn tại.
Sử dụng Mode ITM Debug để in ra printf("HAL_I2C_IsDeviceReady: 0x%X \n",i);
HAL_I2C_IsDeviceReady: 0x27
Giá trị của thiết bị Slave là 0x27
Đây là hình ảnh xung chúng ta kiểm tra trên Bus I2C_1
Chúng ta bắt đầu kiểm tra từ 0x01 → và không có tín hiêu ACK (NAK) → có nghĩa là không có thiết bị Slave tồn tại ở địa chỉ đó.
Chúng ta thấy Master gửi mỗi địa chỉ 3 lần check (3 lần 0x01, 3 lần 0x02, ..) nhằm đảm bảo chắn chắn không xảy ra nhầm lẫn, việc setup 3 lần phụ thuộc vào line code int ret = HAL_I2C_IsDeviceReady(&hi2c1, (uint16_t)(i<<1), 3, 5);
với tham số 3 là số lần thử, và 5 là timeout.
⚙️Tại sao (uint16_t)(i<<1)
, tại sao lại dịch trái 1 bit?
Địa chỉ I²C nó hơi đặc biệt một tí, hãy nhìn vào bảng:
Sau tín hiệu S
- Start: gồm 7 bit địa chỉ và 1 bit R/W, nên chúng ta phải dịch trái một bít để một bit cho bit Read hoặc Write **
Ở hình dưới ta thấy module của chúng ta có dạng 8 bit: 7 bit địa chỉ, 1 bit cuối = 0
(cuối cùng là bit 0
của Slave trả về).
Khi tín hiệu SCL
phát xung cạnh lên
mà lúc đó thấy SDA đang ở mức thấp
thì xác nhận data gửi vào là 0
, ngược lại là 1
Khi quét tới giá trị 0x27
thì chúng ta thấy tín hiệu cuối cùng đã có mức logic = 0 (tín hiệu 0 từ Slave) báo cho Master biết là tôi (Slave) có mặt.
Chúng ta có thể tham khảo thêm ở đây! STM32 Scan I²C bus - Stm32World Wiki
Khởi tạo màn hình LCD?
Dựa vào datasheet của HD44780U và lý thuyết của I²C chúng ta sẽ thiết lập code để khởi tạo và sử dụng màn hình LCD1602 cũng như LCD2004
Figure 24 in HD44780 Datasheet
Hình ảnh trong datasheet cho chúng ta thấy các bước để cấu hình khởi tạo driver sử dụng LCD1602 hoặc LCD2004 .
Chúng ta sẽ sử dụng loại truyền 4 bit data (nibble) theo driver HD44780U (chứ không phải 4bit của I²C nhé - I²C vẫn gửi data frame 8 bit)
Một “
nibble
” (cũng đánh vần là “nybble
”) trong lĩnh vực CNTT cách nói là một tập hợp dữ liệu Bốn-bit tương đương với một nửa của một byte. Điều này cũng đôi khi được gọi là mộtquadbit
, một nửa byte, một tetrade hoặc semi-octet.
⚙️Mô phỏng LCD truyền nhận 4 bit không sử dụng I2C:
⚙️Chân E đóng vai trò là chân chốt (latching): kích chân E (Enable) lên mức cao, sau đó xuống mức thấp để latching 4bit nibble.
Sử dụng I²C từ STM32F407VET truyền 8 bit theo cấu trúc thứ tự bit như sau:
⚙️4 BIT CAO: BIT 7 → BIT 4
⚙️4 BIT THẤP: BIT 3 → BIT 0
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
⚙️Gửi lệnh cmd lên LCD?
⚙️Hàm void lcd_send_cmd (char cmd)
Để thực hiện việc hiển thị LCD thì giao tiếp của STM32 với Driver HD55780U phải có.
Hàm void lcd_send_cmd (char cmd)
cho phép chúng ta gửi yêu cầu tới Driver HD55780U thông qua I²C Bus
Nhìn vào hình bên dưới, để gửi 4 bit thì chúng ta gửi lần lượt 4 bit mỗi lần (4 BIT UPPER: BIT 7 → BIT 4
gửi trước, 4 BIT LOWER: BIT 3 → BIT 0
gửi sau) và tín hiệu E phải chuyển từ trạng thái OFF(0) → ON(1)
Theo thông tin từ datasheet như ảnh trên:
HD44780U có thể gửi dữ liệu theo kiểu gửi hai lần 4 bit (để tổng hợp thành 8 bit). Khi gửi hai lần 4 bit, HD44780U chỉ sử dụng bốn đường bus (DB4 đến DB7) để truyền dữ liệu, còn bốn đường bus còn lại (DB0 đến DB3) không được sử dụng.
Để truyền dữ liệu theo giao diện 4 bit, HD44780U phải nhận được hai lần truyền dữ liệu, mỗi lần truyền bốn bit. Lần truyền đầu tiên chứa bốn bit cao của byte dữ liệu (DB4 đến DB7), còn lần truyền thứ hai chứa bốn bit thấp của byte dữ liệu (DB0 đến DB3). Thứ tự truyền dữ liệu phải tuân theo quy tắc này: bốn bit cao được truyền trước, sau đó là bốn bit thấp.
⚙️Màu đỏ là gửi đi trước, màu xanh lá là gửi đi sau
Ví dụ gửi chữ H lên màn hình theo datasheet hình trên No.6
:
H
→ 0X48
→ GỬI 4 BIT CAO 0100
TRƯỚC, GỬI 4 BIT THẤP 1000
SAU
Các tín hiệu được OR với EN, RS, RW (chúng ta phân tích sau)
0x4D, 0x49
trong đó 4
là ở chuỗi bit cao
0x8D, 0x89
trong đó 8
là ở chuỗi bit thấp
Vậy cmd
là gì?
Chúng ta quan sát tài liêu datasheet của nhà sản xuất về driver HD44780:
Để ý vị trí của những bit
mang giá trị logic = 1
, mỗi vị trí tương ứng với một mode setup
khác nhau.
Table 6: Sự sắp sếp các Mode của nhà sản xuất
Giải thích ý nghĩa các giá trị bit
Tôi tóm gọn lại một bảng với mã cmd
Hex như sau:
No. | Instruction | Hex |
---|---|---|
1 | Function Set: 8-bit, 1 Line, 5x8 Dots | 0x30 |
2 | Function Set: 8-bit, 2 Line, 5x8 Dots | 0x38 |
3 | Function Set: 4-bit, 1 Line, 5x8 Dots | 0x20 |
4 | Function Set: 4-bit, 2 Line, 5x8 Dots | 0x28 |
5 | Entry Mode | 0x06 I/D = 1 S=0 |
6 | Display off Cursor off(clearing display without clearing DDRAM content) | 0x08 |
7 | Display on Cursor on | 0x0E |
8 | Display on Cursor off | 0x0C |
9 | Display on Cursor blinking | 0x0F |
10 | Shift entire display left | 0x18 |
11 | Shift entire display right | 0x1C |
12 | Move cursor left by one character | 0x10 |
13 | Move cursor right by one character | 0x14 |
14 | Clear Display (also clear DDRAM content) | 0x01 |
15 | Set DDRAM address or coursor position on display | 0x80 + address* |
16 | Set CGRAM address or set pointer to CGRAM location | 0x40 + address** |
Chúng ta sẽ có đoạn mã như sau để thực hiện việc gửi lệnh cmd
ở trên:
void lcd_send_cmd (char cmd) // cmd là mã được truyền vào
{
char data_u, data_l; // data_u: 4 bit UPPER NIBBLE; data_l: 4 bit LOWER NIBBLE
uint8_t data_t[4];
data_u = (cmd&0xf0); // &0Xf0 để tách 4 bit UPPER
data_l = ((cmd<<4)&0xf0); // tách 4 bit LOWER
data_t[0] = data_u|0x0C; //en=1, rs=0 ,rw=0
data_t[1] = data_u|0x08; //en=0, rs=0 ,rw=0
data_t[2] = data_l|0x0C; //en=1, rs=0 ,rw=0
data_t[3] = data_l|0x08; //en=0, rs=0 ,rw=0
HAL_I2C_Master_Transmit (&hi2c1, Slave_ADDRESS_LCD << 1,(uint8_t *) data_t, 4, 100);
}
Có 4 data frame (data_t[0]
→data_t[3]
) được tạo sẳn sàng cho I²C truyền từ STM32F407VET qua IC HD447780
Lệnh HAL_I2C_Master_Transmit (&hi2c1, Slave_ADDRESS_LCD << 1,(uint8_t *) data_t, 4, 100);
sẽ gửi tới đĩa chỉ Slave_ADDRESS_LCD
thông qua hi2c1
với 1 frame tìm địa chỉ, 4 frame data_t
tuần tự data_t[0]
→ data_t[1]
→ data_t[2]
→data_t[3]
với timeout là 100
Ví dụ gửi lệnh cmd = 0x28
Function Set: 4-bit, 2 Line, 5x8 Dots
Frame đầu tiên, gửi từ MASTER tới địa chỉ 0x27
→ và có tín hiêu ACK
từ Slave
, nên sẽ được gửi các frame tiếp tục sau.
Frame 1: data_t[0]
= 0x2C
Tương đương: (chú ý bit EN)
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
0 | 0 | 1 | 0 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
1 | 1 | 0 | 0 |
Frame 2: data_t[1]
= 0x28
Tương đương: (chú ý bit EN)
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
0 | 0 | 1 | 0 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
1 | 0 | 0 | 0 |
Frame 3: data_t[2]
= 0x8C
Tương đương: (chú ý bit EN)
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
1 | 0 | 0 | 0 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
1 | 1 | 0 | 0 |
Frame 4: data_t[3]
= 0x88
Tương đương: (chú ý bit EN)
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
1 | 0 | 0 | 0 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
1 | 0 | 0 | 0 |
Vậy là xong quá trình gửi lệnh cmd
⚙️Khởi tạo màn hình LCD?
Để màn hình LCD có thể hoạt động thì chúng ta cần một loạt các lệnh cmd
được gửi từ STM32F407VET tới LCD để khởi động và sử dụng LCD.
Vậy quy trình nó ra sao?
Nhìn vào hình theo datasheet của nhà sản xuất, nếu muốn khởi tạo màn hình theo chuẩn 4-bit thì phải theo các bước như sau:
-
Màu đỏ: gửi 1 lệnh 8 bit
-
Màu xanh lá: gửi 2 lệnh 4 bit
Ví dụ với lệnh màu đỏ đầu tiên:
Ta sắp xếp chuỗi data thành mã hex như sau:
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
0 | 0 | 1 | 1 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
0 | 0 | 0 | 0 |
BIT 3, BIT 2, BIT 1, BIT 0 đều đã được tính toán ở trong lệnh gửi cmd
nên ta cho = 0
Như vậy, ta sẽ có mã gửi vào cmd
là 0x30
Ví dụ với cặp lệnh màu xanh đầu tiên:
Ở cặp lệnh đặc biệt này:
Function set:
- DL = 1; 8-bit interface data → ta chọn DL = 0 (4 bit)
- N = 0; 1-line display → ta chọn N = 1 (2 line)
- F = 0; 5 × 8 dot character font → ta chọn F= 0
Function Set
DL: Sets the interface data length. Data is sent or received in 8-bit lengths (DB7 to DB0) when DL is 1, and in 4-bit lengths (DB7 to DB4) when DL is 0. When 4-bit length is selected, data must be sent or received twice.
N: Sets the number of display lines. F: Sets the character font.
Note: Perform the function at the head of the program before executing any instructions (except for the read busy flag and address instruction). From this point, the function set instruction cannot be executed unless the interface data length is changed.
Dòng lệnh đầu tiên:
Ta sắp xếp chuỗi data thành mã hex như sau:
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
0 | 0 | 1 | 0 (DL) |
Như vậy, ta sẽ có là 0x2
→ setup 4-bit
Dòng lệnh thứ 2:
Ta sắp xếp chuỗi data thành mã hex như sau:
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
D7 | D6 | D5 | D4 |
1 (N) | 0 (F) | 0 | 0 |
Như vậy, ta sẽ có là 0x8
Vậy tổng hợp lại theo nhà sản xuất gửi cmd
là 0x28
Note: cmd
→ 0x28
: Function set (Interface is 4 bits long. Specify the number of display lines and character font.) The number of display lines and character font cannot be changed after this point
Tương tự, các cặp lệnh phía dưới ta có
cmd
→ 0x08
: Display off —> D=0, C=0, B=0 —> display off
cmd
→ 0x01
: Display clear
cmd
→ 0x06
: Entry mode set —> I/D = 1 (increment cursor) & S = 0 (no shift)
cmd
→ 0x0C
: Display on —> D=1, C=0, B=0 —> display off
Tổng kết lại ta sẽ có một hàm khởi tạo lcd:
void lcd_init (void)
{
// 4 bit initialisation
HAL_Delay(50); // wait for >40ms
lcd_send_cmd (0x30);
HAL_Delay(5); // wait for >4.1ms
lcd_send_cmd (0x30);
HAL_Delay(1); // wait for >100us
lcd_send_cmd (0x30);
HAL_Delay(10);
lcd_send_cmd (0x20); // 4bit mode
HAL_Delay(10);
// dislay initialisation
lcd_send_cmd (0x28); // Function set --> DL=0 (4 bit mode), N = 1 (2 line display) F = 0 (5x8 characters)
HAL_Delay(1);
lcd_send_cmd (0x08); // Display on/off control --> D=0,C=0, B=0 ---> display off
HAL_Delay(1);
lcd_send_cmd (0x01); // clear display
HAL_Delay(1);
HAL_Delay(1);
lcd_send_cmd (0x06); // Entry mode set --> I/D = 1 (increment cursor) & S = 0 (no shift)
HAL_Delay(1);
lcd_send_cmd (0x0C); // Display on/off control --> D = 1, C and B = 0. (Cursor and blink, last two bits)
}
Cùng nhìn vào hình ảnh dưới đây để phân tích quá trình khởi tạo khi sử dụng I²C có đúng như chúng ta lập trình hay không?
- Tổng cộng có 9 lệnh
lcd_send_cmd
được gửi đi. - Khoảng cách thời gian chờ giữa các lệnh
lcd_send_cmd
gần đúng như chúng ta setup ( khoảng thời gian chúng ta chờ đểLCD thực thi lệnh cmd
đó)
Hình ảnh chi tiết về 9 lệnh được gửi:
lcd_send_cmd (0x30);
được gửi 3 lần giống nhau nên tôi chụp màn hình 1 lần!
lcd_send_cmd (0x20);
// 4bit mode
lcd_send_cmd (0x28);
// Function set –> DL=0 (4 bit mode), N = 1 (2 line display) F = 0 (5x8 characters)
lcd_send_cmd (0x08);
// Display on/off control –> D=0,C=0, B=0 —> display off
lcd_send_cmd (0x01);
// clear display
lcd_send_cmd (0x06);
//Entry mode set –> I/D = 1 (increment cursor) & S = 0 (no shift)
lcd_send_cmd (0x0C);
// Display on/off control –> D = 1, C and B = 0. (Cursor and blink, last two bits)
⚙️Toạ độ trên LCD?
Làm sao để chúng ta hiển thị một ví trí bất kỳ mà chúng ta muốn trên màn hình LCD?
Set DDRAM Address
Set DDRAM address sets the DDRAM address binary AAAAAAA into the address counter. Data is then written to or read from the MPU for DDRAM.
However, when N is 0 (1-line display), AAAAAAA can be 00H to 4FH. When N is 1 (2-line display), AAAAAAA can be 00H to 27H for the first line, and 40H to 67H for the second line.
⚙️Setting cursor position on LCD
Để đặt vị trí con trỏ trên LCD, chúng ta cần gửi địa chỉ DDRAM…
Màn LCD 2004 có địa chỉ:
Màn LCD 1602 có địa chỉ:
Tuy nhiên, để ghi được giá trị lên vùng nhớ đó thì Bit DB7
phải ở mức logic 1
Cấu trúc mã hex: 1 AD6 AD5 AD4 AD3 AD2 AD1 AD0
Bit thứ bảy luôn là 1 và bit từ AD6 đến AD0 là địa chỉ DDRAM. Vì vậy nếu bạn muốn đặt con trỏ ở vị trí đầu tiên, địa chỉ sẽ là ‘0b0000000
’ (7 số 0 nhé - địa chỉ ở hình phía trên) ở dạng nhị phân và bit thứ 7 là 1.
Vì vậy địa chỉ sẽ là 0x80
, vì vậy đối với DDRAM, tất cả địa chỉ bắt đầu từ 0x80
.
Đối với LCD 2 dòng và 16 ký tự. Địa chỉ từ 0x80 đến 0x8F hiển thị trên dòng đầu tiên và 0xC0 đến 0xCF hiển thị trên dòng thứ hai, phần còn lại của vùng DDRAM vẫn có nhưng không hiển thị trên màn hình LCD (sử dụng cho màn LCD 2004), nếu bạn muốn kiểm tra điều này, chỉ cần đặt một dấu dài lớn hơn 16 ký tự và dịch chuyển toàn bộ màn hình, bạn sẽ thấy tất cả các ký tự bị thiếu xuất hiện từ phía sau.. Bằng cách này, bạn có thể tạo dòng cuộn trên màn hình LCD.
Nên tôi có cú pháp công thức như sau:
Set DDRAM address or coursor position on display | 0x80 + address* |
---|---|
Set CGRAM address or set pointer to CGRAM location | 0x40 + address* |
Ví dụ hiển thị “OK” tại cột 3 hàng 1 có địa chỉ là 0x02
Tương ứng với lcd_send_cmd (0x80 + 0x02);
→ lcd_send_cmd (0x82);
lcd_send_cmd (0x82); // tương ứng vị trí thứ 3, của dòng đầu tiên
lcd_send_string("OK"); // OK sẽ chiếm 2 vị trí ô nhớ trên màn hình
HAL_Delay(1000);
Nên địa chỉ ghi dữ liệu khi sử dụng lệnh cmd
có giá trị hex
như bảng dưới:
Note: Bảng có giá trị khi sử dụng lệnh cmd
Cuối cùng, ta sẽ có function để đưa tới vị trí cần hiển thị như sau:
Trong lập trình, tôi đặt vị trí hàng bắt đầu là 0
- Tương ứng với hàng đầu tiên thì
row = 0
- Tương ứng với hàng tiếp theo thì
row = 1
- …
- Ví trí của cột
cmd_col = LCD_SETDDRAMADDR | (col + 0x00);
(địa chỉ base của hàng đầu tiên) - Ví trí của cột
cmd_col = LCD_SETDDRAMADDR | (col + 0x40);
(địa chỉ base của hàng tiếp theo) - …
#define LCD_SETDDRAMADDR 0x80 void lcd_put_cur(int row, int col) { int cmd_col = 0x80; switch (row) { case 0: cmd_col = LCD_SETDDRAMADDR | (col + 0x00); break; case 1: cmd_col = LCD_SETDDRAMADDR | (col + 0x40); break; case 2: cmd_col = LCD_SETDDRAMADDR | (col + 0x14); break; case 3: cmd_col = LCD_SETDDRAMADDR | (col + 0x54); break; } lcd_send_cmd (cmd_col); }
⚙️Gửi data lên LCD?
Chúng ta đã có hàm gửi lệnh cmd
và khởi tạo màn hình LCD thành công.
Bây giờ cùng xem cách gửi data hiện ký tự lên màn hình LCD nhé!
⚙️Hàm void lcd_send_data (char data)
Tiếp tục, đoạn mã sau đây sẽ làm công việc gửi data
void lcd_send_data (char data) // data là dữ liệu được truyền vào
{
char data_u, data_l; // data_u: 4 bit UPPER NIBBLE; data_l: 4 bit LOWER NIBBLE
uint8_t data_t[4];
data_u = (data&0xf0); // &0Xf0 để tách 4 bit UPPER
data_l = ((data<<4)&0xf0); // tách 4 bit LOWER
data_t[0] = data_u|0x0D; //en=1, rs=1 ,rw=0
data_t[1] = data_u|0x09; //en=0, rs=1 ,rw=0
data_t[2] = data_l|0x0D; //en=1, rs=1 ,rw=0
data_t[3] = data_l|0x09; //en=0, rs=1 ,rw=0
HAL_I2C_Master_Transmit (&hi2c1, Slave_ADDRESS_LCD << 1,(uint8_t *) data_t, 4, 100);
}
Ví dụ ta thực hiện lệnh
// đây là một lệnh cmd yêu cầu trỏ con trỏ tơi địa chỉ 0x00 của DDRAM =>
// tương đương lệnh lcd_send_cmd (0x80);
lcd_put_cur(0,0);
// sau khi có lệnh cmd trỏ địa chỉ (bit D7 = 1) thì thực hiện hiện data 0x48 lên
lcd_send_data(0x48); // Chữ H`
Note: Như vậy, luôn phải có hàm lcd_send_cmd
để xác định hành vi của hàm lcd_send_data
trước khi sử dụng!
Hành vi của hàm
lcd_send_cmd
được xác định ở Table 6 - phần lệnh cmd
Frame đầu tiên, gửi từ MASTER tới địa chỉ 0x27
→ và có tín hiêu ACK
từ Slave
, nên sẽ được gửi các frame tiếp tục sau.
Frame 1: data_t[0]
= 0x4D
Tương đương: (chú ý bit EN, Rs)
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
0 | 1 | 0 | 0 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
1 | 1 | 0 | 1 |
Frame 2: data_t[1]
= 0x49
Tương đương: (chú ý bit EN, RW)
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
0 | 1 | 0 | 0 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
1 | 0 | 0 | 1 |
Frame 3: data_t[2]
= 0x8D
Tương đương: (chú ý bit EN, RW)
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
1 | 0 | 0 | 0 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
1 | 1 | 0 | 1 |
Frame 4: data_t[3]
= 0x89
Tương đương: (chú ý bit EN, RW)
BIT 7 | BIT 6 | BIT 5 | BIT 4 |
---|---|---|---|
D7 | D6 | D5 | D4 |
1 | 0 | 0 | 0 |
BIT 3 | BIT 2 | BIT 1 | BIT 0 |
---|---|---|---|
X | E | RW | RS |
1 | 0 | 0 | 1 |
Vậy là xong quá trình gửi data
Với hàm trên, chúng ta chỉ gửi một lần một ký tự duy nhất, giờ cải tiến thêm một chút nhé!
void lcd_send_string (char *str)
{
while (*str) lcd_send_data (*str++);
}
Hàm lcd_send_string
cho phép chúng ta gửi một chuỗi ký tự.
Ví dụ chúng ta hiển thị chữ “OK
”
lcd_send_string(”OK”);
Chúng ta quan sát I²C Master gửi 2 chuỗi ( mỗi chuỗi 5 frame) tới Slave LCD
- Với ký tự
O
là0x4F
(5 frame: 1 frame địa chỉ, 4 frame để gửi chữO
) - Với ký tự
K
là0x4B
(5 frame: 1 frame địa chỉ, 4 frame để gửi chữK
)
⚙️CGRAM và CGROM
CGRAM (Character Generator RAM) và CGROM (Character Generator ROM) là hai vùng nhớ riêng biệt trong bộ điều khiển màn hình LCD như HD44780, được sử dụng để lưu trữ các mẫu ký tự (font) để hiển thị trên màn hình LCD. Dưới đây là sự khác nhau giữa CGRAM và CGROM:
-
CGRAM (Character Generator RAM):
- CGRAM là một vùng nhớ RAM (Random Access Memory) trong bộ điều khiển LCD.
- Nó được sử dụng để lưu trữ các mẫu ký tự tùy chỉnh mà người dùng có thể định nghĩa.
- CGRAM có dung lượng nhỏ hơn so với CGROM và được sử dụng để lưu trữ các ký tự tùy chỉnh dưới dạng ma trận điểm ảnh (pixel matrix).
- Khi người dùng muốn hiển thị một ký tự tùy chỉnh, dữ liệu về ký tự đó phải được định nghĩa và ghi vào CGRAM trước khi nó có thể xuất hiện trên màn hình LCD.
- Mỗi ô nhớ trong CGRAM lưu trữ dữ liệu của một ký tự tùy chỉnh, và số lượng ký tự tùy chỉnh mà màn hình LCD có thể hỗ trợ phụ thuộc vào kích thước của CGRAM (thường là 8 ô nhớ cho LCD 1602).
-
CGROM (Character Generator ROM):
- CGROM là một vùng nhớ ROM (Read-Only Memory) trong bộ điều khiển LCD.
- Nó được sử dụng để lưu trữ các mẫu ký tự chuẩn (các ký tự thông thường như số, chữ cái, dấu chấm, dấu phẩy và một số ký tự đặc biệt).
- CGROM chứa sẵn các mẫu ký tự chuẩn và không thể được thay đổi bởi người dùng.
- Khi muốn hiển thị một ký tự chuẩn, dữ liệu về ký tự đó được trích xuất từ CGROM và hiển thị trực tiếp trên màn hình LCD.
- Số lượng ký tự chuẩn mà màn hình LCD có thể hiển thị được phụ thuộc vào số lượng mẫu ký tự được lưu trữ trong CGROM và phạm vi hỗ trợ của bộ điều khiển LCD (thường là 208 mẫu ký tự cho LCD 1602).
Tóm lại, CGRAM và CGROM là hai vùng nhớ khác nhau trong bộ điều khiển màn hình LCD, mỗi vùng nhớ có nhiệm vụ riêng biệt trong việc lưu trữ và hiển thị các mẫu ký tự trên màn hình LCD. CGRAM dùng cho các ký tự tùy chỉnh mà người dùng định nghĩa, trong khi CGROM chứa các ký tự chuẩn được cung cấp sẵn.
⚙️CGRAM Creating custom character
Vùng nhớ CGRAM (Character Generator RAM) của màn hình LCD 1602 là một phần của bộ nhớ trong bộ điều khiển LCD (như HD44780) dùng để lưu trữ các mẫu ký tự tùy chỉnh mà người dùng có thể định nghĩa.
Màn hình LCD 1602 có khả năng hiển thị một số ký tự chuẩn như các ký tự số, chữ cái, dấu chấm, dấu phẩy và một số ký tự đặc biệt. Nhưng ngoài các ký tự này, nó cũng cung cấp cho người dùng khả năng tự định nghĩa các ký tự tùy chỉnh.
Trong bộ điều khiển LCD, bit 7 và bit 6 của thanh ghi hướng dẫn là bit CGRAM.
Khi bit 7 là 0 và bit 6 là 1, lệnh địa chỉ CGRAM được tạo ra với địa chỉ bắt đầu từ 0x40. Địa chỉ CGRAM (ACG) có thể có giá trị từ 0x00 đến 0x3F.
Vùng nhớ CGRAM gồm 64 ô nhớ (64 byte), mỗi ô nhớ dành cho một ký tự tùy chỉnh. Mỗi ký tự được biểu diễn bởi một ma trận 5x8 (5 cột và 8 hàng), trong đó mỗi bit đại diện cho một điểm ảnh. Điều này cho phép người dùng tự định nghĩa các ký tự theo ý muốn và lưu trữ chúng vào vùng nhớ CGRAM.
Memory Map | |
---|---|
Pattern No. | CGRAM Address (Acg) |
1 | 0x00 - 0x07 |
2 | 0x08 - 0x0F |
3 | 0x10 - 0x17 |
4 | 0x18 - 0x1F |
5 | 0x20 - 0x27 |
6 | 0x28 - 0x2F |
7 | 0x30 - 0x37 |
8 | 0x38 - 0x3F |
Kết hợp với Bit 7 = 0, Bit 6 = 1 ta có bảng dưới đây để sử dụng lệnh cmd
:
Cấu trúc mã hex: 0_1_AD5_AD4 AD3_AD2_AD1_AD0
Cách định nghĩa ký tự tùy chỉnh và lưu trữ chúng vào vùng nhớ CGRAM thường được thực hiện bằng cách gửi các byte dữ liệu đại diện cho các hàng của ma trận 5x8 vào các ô nhớ CGRAM tương ứng.
Dưới đây là cách định nghĩa và lưu trữ một ký tự tùy chỉnh vào vùng nhớ CGRAM của màn hình LCD 1602:
-
Đầu tiên, cần di chuyển con trỏ lệnh đến vị trí ô nhớ CGRAM mà bạn muốn định nghĩa ký tự. Vị trí các ô nhớ CGRAM được đánh số từ 0 đến 7 (tổng cộng 8 ô nhớ).
-
Tiếp theo, gửi 8 byte dữ liệu liên tiếp cho 8 hàng của ma trận 5x8 của ký tự tùy chỉnh. Mỗi byte đại diện cho một hàng, và mỗi bit trong byte đại diện cho một điểm ảnh (pixel) của hàng đó.
-
Lặp lại quá trình trên nếu bạn muốn định nghĩa nhiều ký tự tùy chỉnh khác.
Sau khi định nghĩa các ký tự tùy chỉnh trong vùng nhớ CGRAM, bạn có thể gọi chúng bằng các mã lệnh tương ứng khi muốn hiển thị chúng trên màn hình LCD.
Lưu ý rằng mỗi màn hình LCD có thể có một số hạn chế về số lượng ký tự tùy chỉnh và cách lưu trữ chúng. Điều này sẽ phụ thuộc vào bộ điều khiển cụ thể được sử dụng trong màn hình LCD.
Hãy lấy một mô hình tùy chỉnh. Tất cả những gì chúng ta phải làm là tạo một bản đồ pixel 7x5 và nhận giá trị hex hoặc thập phân hoặc giá trị hex cho mỗi hàng, giá trị bit là 1 nếu pixel phát sáng và giá trị bit là 0 nếu pixel tắt. 7 giá trị cuối cùng được tải vào CGRAM từng cái một. Như tôi đã nói, có 8 hàng cho mỗi mẫu, vì vậy hàng cuối cùng thường được để trống (0x00) cho con trỏ. Nếu bạn không sử dụng con trỏ thì bạn cũng có thể sử dụng hàng thứ 8 đó. để bạn có được một mô hình lớn hơn.
Dưới đây là một ví dụ khởi tạo hình cái chuông:
Bit: | 4 | 3 | 2 | 1 | 0 | Hex |
Row1: | 0 | 0 | 1 | 0 | 0 | 04 |
Row2: | 0 | 1 | 1 | 1 | 0 | 0E |
Row3: | 0 | 1 | 1 | 1 | 0 | 0E |
Row4: | 0 | 1 | 1 | 1 | 0 | 0E |
Row5: | 1 | 1 | 1 | 1 | 1 | 1F |
Row6: | 0 | 0 | 0 | 0 | 0 | 00 |
Row7: | 0 | 0 | 1 | 0 | 0 | 04 |
Row8: | 0 | 0 | 0 | 0 | 0 | 00 |
#define LCD_SETCGRAMADDR 0x40
// khởi tạo mảng
char cc0[] = {0x00, 0x04, 0x0E, 0x0E, 0x0E, 0x1F, 0x04, 0x00}; // bell
...
lcd_send_cmd(LCD_SETCGRAMADDR | 0x00); // lênh cmd LCD_SETCGRAMADDR
lcd_send_data(0x00); //hàng 1
lcd_send_data(0x04); //hàng 2
lcd_send_data(0x0E); //hàng 3
lcd_send_data(0x0E); //hàng 4
lcd_send_data(0x0E); //hàng 5
lcd_send_data(0x1F); //hàng 6
lcd_send_data(0x04); //hàng 7
lcd_send_data(0x00); //hàng 8
...
lcd_send_data(0x00); // hiển thị giá tại địa chỉ 0x00
Big-Endian Nibble?
Big-Endian Nibble là một cách sắp xếp các nibble (nhóm 4 bit) trong một từ (word) hoặc một dãy bit. Đây là một trong hai kiểu sắp xếp nibble, còn kiểu kia là Little-Endian Nibble.
Trong Big-Endian Nibble, nibble quan trọng nhất (nibble có giá trị cao nhất) được đặt ở đầu từ hoặc dãy bit, trong khi nibble ít quan trọng nhất (nibble có giá trị thấp nhất) nằm ở cuối.
Ví dụ về Big-Endian Nibble:
-
Giả sử chúng ta có một từ 8-bit (1 byte) với giá trị nhị phân là: 1101 0010
-
Theo Big-Endian Nibble, ta chia thành hai nibble:
-
Nibble quan trọng nhất (nibble cao): 1101
-
Nibble ít quan trọng nhất (nibble thấp): 0010
-
Ta có thể thể hiện từ trên dưới dạng Big-Endian Nibble là: 1101 0010
Trong trường hợp từ có độ dài lớn hơn 1 byte (ví dụ: 16-bit, 32-bit, 64-bit), việc sắp xếp các nibble vẫn tuân theo nguyên tắc của Big-Endian Nibble, tức là nibble quan trọng nhất nằm ở vị trí đầu tiên.
Ví dụ lý thuyết về việc tách bit UPPER NIBBLE, LOWER NIBBLE:
Cám ơn mọi người đã theo dõi bài viết!