👉 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?

Untitled

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đ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:

Untitled

Các bước truyền dữ liệu I2C

  1. Master gửi điều kiện khởi động đến mọi Slave được kết nối.
  2. 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.
  3. 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.
  4. Master gửi hoặc nhận khung dữ liệu.
  5. 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.
  6. Để 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?

image-1

Ở 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.

image-1

⚙️Sơ đồ khối I²C của STM32F407VET

image-1

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:

Untitled

  I2C_1 I2C_2 I2C_3
SCL PB6 PB10 PA8
SDA PB7 PB11 PC9

Lập trình Master hoặc SlaveSTM32F407VET:

  • 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)

Untitled

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)

Untitled

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ị Slavetồ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);

image-1

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

image-1 image-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ỉ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 **

https://stm32world.com/images/thumb/6/61/I%C2%B2C_7-bit_Addresses.png/600px-I%C2%B2C_7-bit_Addresses.png

Ở 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

image-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

Untitled

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ột quadbit, 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.

image-1

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)

image-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

Untitled

Ví dụ gửi chữ H lên màn hình theo datasheet hình trên No.6:

H0X48 → 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

image-1

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

image-1

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

Untitled

Ví dụ với lệnh màu đỏ đầu tiên:

Untitled

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 cmd0x30

image-1

Ví dụ với cặp lệnh màu xanh đầu tiên:

Untitled

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 cmd0x28

Note: cmd0x28: 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ó

cmd0x08 : Display off —> D=0, C=0, B=0 —> display off

cmd0x01 : Display clear

cmd0x06 : Entry mode set —> I/D = 1 (increment cursor) & S = 0 (no shift)

cmd0x0C : 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 đó)

Untitled

Untitled

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!

image-1

lcd_send_cmd (0x20);

// 4bit mode

image-1

lcd_send_cmd (0x28);

// Function set –> DL=0 (4 bit mode), N = 1 (2 line display) F = 0 (5x8 characters)

image-1

lcd_send_cmd (0x08);

// Display on/off control –> D=0,C=0, B=0 —> display off

image-1

lcd_send_cmd (0x01);

// clear display

image-1

lcd_send_cmd (0x06);

//Entry mode set –> I/D = 1 (increment cursor) & S = 0 (no shift)

image-1

lcd_send_cmd (0x0C);

// Display on/off control –> D = 1, C and B = 0. (Cursor and blink, last two bits)

image-1

⚙️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ỉ:

Untitled

Màn LCD 1602 có địa chỉ:

Untitled

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ên0xC0 đế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

image-1

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);

Untitled

Nên địa chỉ ghi dữ liệu khi sử dụng lệnh cmd có giá trị hex như bảng dưới:

image-1

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

image-1

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

Untitled

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ự O0x4F (5 frame: 1 frame địa chỉ, 4 frame để gửi chữ O)
  • Với ký tự K0x4B (5 frame: 1 frame địa chỉ, 4 frame để gửi chữ K)

image-1

Untitled

⚙️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:

  1. 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).
  2. 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.

set-CGRAM.png

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:

  1. Đầ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ớ).

  2. 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 đó.

  3. 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

custom-bell.png

#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:

Untitled

Untitled

Project Github i2c-LCD2004

Cám ơn mọi người đã theo dõi bài viết!