Mô-đun LCD1602 I2C không hiển thị được ký tự

Bảng mạch nhỏ STM32 dùng làm bộ điều khiển chính, mô-đun LCD1602 I2C mua sẵn trên mạng.

Tôi là người mới bắt đầu, mã nguồn do AI viết. Ban đầu hiển thị một hàng các hình vuông, sau khi sửa đổi thì hiển thị hai hàng hình vuông, nhưng vẫn không hiển thị ký tự. Đã điều chỉnh biến trở, điện áp I2C cũng ở mức 5V.

Vẽ sơ đồ đấu dây và đăng mã lên đi, cung cấp thông tin ít như vậy thì ai biết mà trả lời cho bạn được?

Mẫu câu hỏi hay: https://bbs.eeclub.top/t/topic/289
Nghệ thuật đặt câu hỏi: https://bbs.eeclub.top/t/topic/109

1 Lượt thích

Anh em đừng vội, bộ đôi LCD1602+I2C này chắc chắn là “cái bẫy bắt buộc phải dẫm phải” đối với người mới học vi điều khiển, hầu như ai lúc mới bắt đầu đều từng bị kẹt ở đây.

Dựa theo mô tả và ba bức ảnh của bạn, mình xin khẳng định một điều an ủi: Xác suất rất cao là phần cứng của bạn không hỏng, đơn thuần là việc truyền thông chưa thành công.

Màn hình sáng lên và hiển thị hai hàng các ô vuông, điều này chứng tỏ rằng nguồn cấp hoạt động bình thường, đồng thời chiết áp điều chỉnh độ tương phản của bạn đã được chỉnh vừa khít. Việc màn hình LCD1602 hiển thị đầy các ô vuông có nghĩa là: màn hình đã được cấp điện, nhưng chưa nhận được lệnh khởi tạo từ vi điều khiển. Vì code do AI viết nên xác suất tới 99% vấn đề nằm ở phần code và cấu hình truyền thông. Bạn có thể lần lượt kiểm tra theo các bước sau:

1. Lỗi phổ biến nhất: Điền sai địa chỉ thiết bị I2C

Nhìn vào ảnh mặt sau của bạn, bo mạch chuyển đổi màu đen sử dụng chip PCF8574T. Loại module này trên thị trường thường có địa chỉ I2C mặc định là 0x27 hoặc 0x3F.

Nhưng hãy chú ý! Nếu AI đang dùng thư viện HAL cho STM32 để viết code, thì hàm gửi I2C trong HAL yêu cầu phải dịch trái 1 bit địa chỉ 7-bit.

  • Nếu địa chỉ gốc là 0x27, trong code có thể cần điền thành 0x4E (0x27 << 1).
  • Nếu địa chỉ gốc là 0x3F, trong code có thể cần điền thành 0x7E (0x3F << 1).
    AI thường nhầm lẫn ở điểm này, cứ thế điền nguyên 0x27 vào hàm của HAL, dẫn đến hoàn toàn không tìm thấy thiết bị.

2. Dây nối bị ngược (lỗi phổ biến với người mới)

Hãy kiểm tra lại chân kết nối phía STM32. Ví dụ, F103C8T6 dùng I2C1 phần cứng mặc định là PB6 (SCL) và PB7 (SDA). Hãy xác nhận xem dây SDA trên board chuyển đổi đã đúng nối vào PB7 chưa, và SCL đã nối vào PB6 chưa.

3. Mã底层 do AI viết sai về thứ tự xung điều khiển hoặc ánh xạ chân

Thực tế, board chuyển đổi I2C này sẽ chuyển tín hiệu I2C thành 8 cổng IO song song (P0-P7) để điều khiển các chân RS, RW, EN và các chân dữ liệu của LCD1602. Tuy nhiên, tùy loại board mà mỗi chân P cụ thể được nối tới chân nào của LCD có thể khác nhau đôi chút. Mã底层 do AI tự viết “tay chân” thường dễ sai mối quan hệ ánh xạ này.

Gợi ý: Đừng để AI tự viết mã底层 từ đầu nữa. Hãy trực tiếp lên Bilibili (B站) hoặc CSDN tìm kiếm “STM32 HAL库 LCD1602 I2C”, tìm file lcd1602.clcd1602.h đã được người khác kiểm chứng, rồi chép thẳng vào project của bạn — cách này an toàn và hiệu quả nhất.

4. Mức điện áp logic phần cứng (khả năng thấp)

Bạn nói nguồn cấp là 5V, điều này rất tốt, vì LCD1602 bắt buộc phải dùng 5V. Mặc dù chân của STM32 là mức logic 3.3V, nhưng đa số chân I2C (ví dụ PB6, PB7) là loại “chịu được 5V (FT)”, nên nối trực tiếp thường không thành vấn đề. Chỉ cần đảm bảo STM32 và module LCD đã nối chung đất (GND) với nhau là được.

:light_bulb: Gợi ý phá bế tắc (bước tiếp theo nên làm gì):

Đừng vội vàng đòi hiển thị chữ. Trước tiên, hãy yêu cầu AI viết cho bạn một đoạn code quét địa chỉ I2C (STM32 I2C Scanner).

Nạp chương trình vào, mở phần mềm trợ lý cổng nối tiếp (serial assistant), xem thử STM32 có quét thấy module này trên bus hay không.

  • Nếu cổng nối tiếp in ra “không tìm thấy thiết bị”: tức là dây nối sai hoặc I2C của STM32 chưa được khởi tạo đúng.
  • Nếu cổng nối tiếp in ra tìm thấy thiết bị (ví dụ trả về 0x4E): chứng tỏ kết nối phần cứng hoàn hảo, bạn chỉ cần lấy địa chỉ này điền vào code khởi tạo LCD là xong.

Chúc bạn sớm nhìn thấy dòng chữ “Hello World”! Nếu vẫn bí, cứ quay lại hỏi tiếp nhé.

1 Lượt thích

Chỉ cần lưu ý nhanh về phần cứng: Màn hình LCD và mạch I2C backpack yêu cầu 5V, trong khi STM32 của bạn hoạt động ở mức 3.3V. Mặc dù hầu hết các chân I2C trên STM32 chịu được điện áp 5V (FT), bạn nên kiểm tra kỹ bảng dữ liệu (datasheet) để đảm bảo rằng các chân cụ thể mà bạn đang sử dụng thực sự chịu được 5V. Thông thường, các điện trở kéo lên (pull-up) trên module là đủ, nhưng truyền thông có thể thất bại nếu các mức logic không tương thích với nhau.

Bản mạch hệ thống tối thiểu STM32 được gắn vào đầu nối 20 chân, mô-đun LCD1602_I2C gắn vào đầu nối 4 chân.

Dưới đây là file main.c

#include "main.h"
#include "i2c.h"
#include "gpio.h"

#include "lcd1602_i2c.h"

void SystemClock_Config(void);

int main(void)
{

  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_I2C1_Init();


  lcd_init();                // Khởi tạo LCD1602


  lcd_clear();               // Xóa màn hình
  lcd_set_cursor(0, 0);      // Đặt con trỏ về dòng đầu tiên, cột đầu tiên
  lcd_send_string("Hello STM32!"); //

  while (1)
  {

  }

}

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
  {
    Error_Handler();
  }
}

void Error_Handler(void)
{

  __disable_irq();
  while (1)
  {
  }

}

#ifdef USE_FULL_ASSERT

void assert_failed(uint8_t *file, uint32_t line)
{

}
#endif /* USE_FULL_ASSERT */

Dưới đây là file lcd1602_i2c.h

#ifndef __LCD1602_I2C_H
#define __LCD1602_I2C_H

#include "main.h" // Bao gồm thư viện HAL và định nghĩa chân
#define LCD_I2C_ADDRESS 0x4E

void lcd_init(void);
void lcd_send_cmd(char cmd);
void lcd_send_data(char data);
void lcd_send_string(char *str);
void lcd_set_cursor(int row, int col);
void lcd_clear(void);

#endif

Dưới đây là file lcd1602_i2c.c

#include "lcd1602_i2c.h"

// Khai báo biến xử lý I2C bên ngoài, được tạo bởi CubeMX trong main.c
extern I2C_HandleTypeDef hi2c1;

// Hàm nội bộ: Gửi dữ liệu qua I2C
void lcd_send_to_i2c(char data, int rs)
{
  uint8_t data_t[4];
  uint8_t upper_nibble, lower_nibble;


  upper_nibble = data & 0xF0;
  lower_nibble = (data << 4) & 0xF0;

  // Byte điều khiển: Bit 3 là đèn nền (1=bật), Bit 2 là EN, Bit 1 là RW (0=ghi), Bit 0 là RS
  uint8_t backlight = 0x08; 

  data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
  data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
  data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
  data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

  // Gửi 4 byte dữ liệu qua I2C1
  HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 100);


}

// Gửi lệnh
void lcd_send_cmd(char cmd)
{
  lcd_send_to_i2c(cmd, 0); // RS = 0 nghĩa là gửi lệnh
}

// Gửi dữ liệu (ký tự)
void lcd_send_data(char data)
{
  lcd_send_to_i2c(data, 1); // RS = 1 nghĩa là gửi dữ liệu
}

// Xóa màn hình
void lcd_clear(void)
{
  lcd_send_cmd(0x01);
  HAL_Delay(2); // Lệnh xóa màn hình cần thời gian thực thi lâu hơn
}

// Đặt vị trí con trỏ (row: 0-1, col: 0-15)
void lcd_set_cursor(int row, int col)
{
  uint8_t address;
  switch (row)
  {
    case 0:
      address = 0x00;
      break;
    case 1:
      address = 0x40;
      break;
    default:
      address = 0x00;
  }
  address += col;
  lcd_send_cmd(0x80 | address); // Thiết lập địa chỉ DDRAM
}

// Gửi chuỗi ký tự
void lcd_send_string(char *str)
{
  while (*str)
  {
    lcd_send_data(*str++);
  }
}

// Khởi tạo LCD1602
void lcd_init(void)
{
  // Quy trình khởi tạo chuẩn cho chế độ 4 bit
  HAL_Delay(50);
  lcd_send_cmd(0x30);
  HAL_Delay(5);
  lcd_send_cmd(0x30);
  HAL_Delay(1);
  lcd_send_cmd(0x30);
  HAL_Delay(10);
  lcd_send_cmd(0x20);
  HAL_Delay(10);


  // Thiết lập hiển thị
  lcd_send_cmd(0x28); // 4 dây, 2 dòng, font 5x8
  HAL_Delay(1);
  lcd_send_cmd(0x08); // Tắt hiển thị
  HAL_Delay(1);
  lcd_send_cmd(0x01); // Xóa màn hình
  HAL_Delay(2);
  lcd_send_cmd(0x06); // Di chuyển con trỏ sang phải
  HAL_Delay(1);
  lcd_send_cmd(0x0C); // Bật hiển thị, tắt con trỏ


}

Tôi đã viết sơ đồ đấu dây đơn giản hóa và mã code trong phần trả lời rồi, cảm ơn bạn giúp tôi xem thử.

Được rồi, cảm ơn bạn, tôi sẽ thử theo đề xuất của bạn.

Sau khi tôi biên dịch và tải xuống, tôi sử dụng nguồn từ mô-đun nguồn, lúc này điện áp chân VCC và chân GND của I2C là 5V.

Tuyệt vời, việc cung cấp sơ đồ nguyên lý và mã nguồn giúp tăng hiệu suất gỡ lỗi lên gấp trăm lần!

Trước tiên hãy để tôi trấn an bạn: kết nối phần cứng của bạn hoàn toàn chính xác. Trên sơ đồ, PB6 nối với SCL và PB7 nối với SDA là các chân I2C1 tiêu chuẩn của STM32F103. Hơn nữa, trong file header bạn đã viết #define LCD_I2C_ADDRESS 0x4E, điều này chứng tỏ bạn (hoặc AI) đã biết HAL library yêu cầu dịch địa chỉ 0x27 sang trái một bit — như vậy bạn đã thành công tránh được hai lỗi phổ biến nhất mà người mới hay mắc phải.

Lý do thực sự khiến màn hình chỉ hiện các hình vuông là do hàm khởi tạo (lcd_init) do AI viết đã sai ở phần giao tiếp theo chế độ “4 bit”.

:bug: Phân tích lỗi chính: Cái bẫy “khởi tạo bắt buộc” của HD44780

Bộ chuyển đổi I2C bạn dùng (PCF8574) điều khiển màn hình 1602 thông qua 4 dây dữ liệu (D4–D7). Khi vừa cấp điện, màn hình 1602 mặc định đang ở chế độ 8 bit. Để chuyển nó sang chế độ 4 bit, ta phải gửi một vài lệnh theo đúng trình tự quy định trong datasheet — cụ thể là gửi từng nửa byte đơn lẻ (chỉ gửi 4 bit cao, xong ngay lập tức).

Xem xét hàm lcd_send_cmd() của bạn, nó gọi đến lcd_send_to_i2c(). Hàm底层 này rất “siêng năng”, bất kể bạn truyền lệnh gì, nó cũng sẽ tự động chia lệnh ra làm hai nửa (gửi 4 bit cao trước, rồi đến 4 bit thấp), đồng thời kích hoạt hai xung EN.

Khi AI viết lcd_send_cmd(0x30) trong quá trình khởi tạo:

  1. Ý định ban đầu: Chỉ gửi duy nhất 0x3 tới màn hình.
  2. Thực tế xảy ra: lcd_send_to_i2c gửi 4 bit cao là 0x3 (kích hoạt EN lần 1), sau đó lại gửi tiếp 4 bit thấp là 0x0 (lại kích hoạt EN lần 2).
  3. Kết quả: Màn hình nhận thêm xung 0x0 không mong muốn, làm hỏng hoàn toàn thứ tự giao tiếp. Hệ quả là màn hình từ chối khởi tạo, bị treo ở trạng thái khởi động ban đầu — hiện đầy các ô vuông.

:hammer_and_wrench: Giải pháp

Chúng ta cần thêm vào file lcd1602_i2c.c một hàm chuyên dụng để gửi chỉ một nửa byte, dùng riêng cho việc đánh thức màn hình, đồng thời viết lại hàm lcd_init().

Hãy sửa file lcd1602_i2c.c của bạn thành nội dung sau (thay thế phần lcd_init và các hàm liên quan ở trên, giữ nguyên phần còn lại):

#include "lcd1602_i2c.h"

// Khai báo biến extern xử lý I2C
extern I2C_HandleTypeDef hi2c1;

// ====== Thêm mới: Hàm gửi nửa byte dùng riêng cho khởi tạo ======
void lcd_send_cmd_4bit(uint8_t nibble)
{
    uint8_t data_t[2];
    uint8_t backlight = 0x08; // Giữ đèn nền luôn sáng

    // Lưu ý: nibble truyền vào phải là dữ liệu đã căn chỉnh 4 bit cao (ví dụ: 0x30)
    data_t[0] = (nibble & 0xF0) | backlight | 0x04 | 0;  // EN = 1, RS = 0
    data_t[1] = (nibble & 0xF0) | backlight | 0x00 | 0;  // EN = 0, RS = 0
    
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 2, 100);
}

// Hàm nội bộ: Gửi một byte đầy đủ qua I2C (chia thành 2 nửa)
void lcd_send_to_i2c(char data, int rs)
{
    uint8_t data_t[4];
    uint8_t upper_nibble, lower_nibble;

    upper_nibble = data & 0xF0;
    lower_nibble = (data << 4) & 0xF0;

    uint8_t backlight = 0x08; 

    data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
    data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
    data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 100);
}

// ====== Viết lại: Quy trình khởi tạo 4-bit đúng chuẩn ======
void lcd_init(void)
{
    // 1. Giai đoạn khởi tạo bắt buộc (phải gửi đúng từng xung 4-bit đơn lẻ)
    HAL_Delay(50);
    lcd_send_cmd_4bit(0x30); // Đánh thức 1
    HAL_Delay(5);
    lcd_send_cmd_4bit(0x30); // Đánh thức 2
    HAL_Delay(1);
    lcd_send_cmd_4bit(0x30); // Đánh thức 3
    HAL_Delay(10);
    lcd_send_cmd_4bit(0x20); // Báo màn hình: “Chuyển sang chế độ 4 bit!”
    HAL_Delay(10);

    // 2. Giai đoạn cấu hình bình thường (giờ màn hình đã ở chế độ 4-bit, có thể dùng hàm gửi 2 xung)
    lcd_send_cmd(0x28); // Thiết lập chức năng: Dữ liệu 4 dây, hiển thị 2 hàng, font 5x8
    HAL_Delay(1);
    lcd_send_cmd(0x08); // Tắt hiển thị
    HAL_Delay(1);
    lcd_send_cmd(0x01); // Xóa màn hình
    HAL_Delay(2);       // Lệnh xóa mất nhiều thời gian, tối thiểu trì hoãn 2ms
    lcd_send_cmd(0x06); // Thiết lập chế độ: Con trỏ di chuyển sang phải
    HAL_Delay(1);
    lcd_send_cmd(0x0C); // Bật hiển thị, tắt con trỏ
}

// ... Các hàm như lcd_send_cmd, lcd_send_data... giữ nguyên ...

:magnifying_glass_tilted_left: Kiểm tra dự phòng

Nếu bạn đã sửa code và nạp lại chương trình nhưng màn hình vẫn chỉ hiện các ô vuông, thì khả năng duy nhất còn lại là module bạn mua có địa chỉ I2C mặc định không phải 0x27 mà là 0x3F. Bạn chỉ cần vào file lcd1602_i2c.h, đổi #define LCD_I2C_ADDRESS 0x4E thành 0x7E (tức là 0x3F dịch trái 1 bit) rồi thử lại.

Bạn hãy thử sửa phần khởi tạo này trước nhé. Nếu cần, tôi có thể hướng dẫn bạn viết một chương trình nhỏ kiểu “quét địa chỉ I2C” để xác định chính xác 100% địa chỉ giao tiếp của module không?

Bạn có thể dùng máy phân tích logic để kiểm tra xem dạng sóng I2C có xuất hiện hay không, thứ tự thời gian có đúng không. Nếu I2C không có vấn đề gì, tiếp tục dùng máy phân tích logic để kiểm tra tín hiệu song song đầu ra từ mô-đun chuyển đổi I2C sang song song xem có tín hiệu hay không và thứ tự thời gian có chính xác không.

Bạn đã cấu hình STM32CubeMX như thế nào?

Phân tích cốt lõi vấn đề

Màn hình LCD1602 của bạn có đèn nền sáng bình thường nhưng không hiển thị ký tự, 90% khả năng là do giao tiếp I2C bất thường (sai địa chỉ/kết nối chân sai/không phản hồi), kế đến là lỗi về thời gian khởi tạo/độ trễ, hoặc thiếu nguồn/hạn chế kéo lên trên phần cứng. Dưới đây là hướng dẫn kiểm tra và khắc phục đầy đủ theo thứ tự ưu tiên.


Một、Kiểm tra phần cứng (giải quyết các vấn đề cơ bản trước)

1. Kiểm tra lại chân tín hiệu và dây nối

Sơ đồ mạch của bạn định nghĩa như sau:

  • Giao diện LCD H2: 1=+5V, 2=GND, 3=SDA(PB7), 4=SCL(PB6)
  • I2C1 của STM32: SCL phải nối với PB6, SDA phải nối với PB7 — Hãy xác minh lại ánh xạ chân trong CubeMX, tuyệt đối không được đảo ngược SDA/SCL.

2. Nguồn điện phải là 5V

LCD1602 kết hợp board chuyển đổi I2C là thiết bị hoạt động ở mức 5V. Nếu cấp nguồn 3.3V sẽ xuất hiện tình trạng “đèn nền sáng nhưng không giao tiếp I2C được”. Bạn phải nối vào chân 5V của STM32, không được dùng nguồn 3.3V.

3. Bus I2C bắt buộc phải có điện trở kéo lên (pull-up)

I2C là bus dạng drain hở (open-drain), bắt buộc phải có điện trở kéo lên:

  • Phương án 1: Trong CubeMX, cấu hình chế độ GPIO cho PB6/PB7 của I2C1 thành Open Drain Pull-up
  • Phương án 2: Thêm một điện trở 4.7KΩ giữa mỗi chân PB6, PB7 với 5V trên mạch
  • Thiếu điện trở kéo lên sẽ khiến tín hiệu I2C bất thường, thiết bị không phản hồi.

Hai、Sửa lỗi phần mềm (theo thứ tự ưu tiên)

1. 【Lỗi phổ biến nhất】Sửa lại địa chỉ I2C

Trong code bạn đang dùng #define LCD_I2C_ADDRESS 0x4E, tuy nhiên địa chỉ này có thể không khớp với module của bạn:

  • Module chuyển đổi I2C cho LCD1602 chủ yếu dùng chip PCF8574, địa chỉ 7 bit thường gặp là 0x27 hoặc 0x3F. Hàm HAL_I2C_Master_Transmit của HAL Library yêu cầu truyền vào địa chỉ ghi 8 bit (địa chỉ 7 bit dịch trái 1 bit):
    • Địa chỉ 7 bit 0x27 → Địa chỉ ghi 8 bit là 0x4E (đây là địa chỉ bạn đang dùng)
    • Địa chỉ 7 bit 0x3F → Địa chỉ ghi 8 bit là 0x7E (địa chỉ phổ biến khác)

Xác minh nhanh địa chỉ đúng (bắt buộc thực hiện)

Ngay sau MX_I2C1_Init(), thêm đoạn quét địa chỉ I2C để tìm xem thiết bị nào phản hồi:

// Đặt ngay sau MX_I2C1_Init(), trước lcd_init()
uint8_t i2c_addr;
for(i2c_addr=0; i2c_addr<128; i2c_addr++)
{
  if(HAL_I2C_IsDeviceReady(&hi2c1, i2c_addr<<1, 1, 100) == HAL_OK)
  {
    // Dừng tại đây bằng breakpoint/đèn báo, ghi nhận giá trị (i2c_addr<<1), rồi điền vào LCD_I2C_ADDRESS
    break;
  }
}

Nếu không quét thấy địa chỉ nào phản hồi, tức là có lỗi về dây nối hoặc khởi tạo I2C — hãy giải quyết phần cứng trước.

2. Kiểm tra hàm HAL_Delay có hoạt động hay không

Việc khởi tạo LCD rất phụ thuộc vào độ trễ. Nếu HAL_Delay không hoạt động, chuỗi lệnh khởi tạo sẽ sai hoàn toàn, dẫn đến không hiển thị.

  • Cách kiểm tra: Thêm đoạn nháy LED trong vòng lặp chính để kiểm tra độ trễ:
while (1)
{
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // Chân LED tích hợp trên bo mạch của bạn
  HAL_Delay(500);
}

Nếu LED không nhấp nháy đúng 500ms, chứng tỏ cấu hình Clock/SysTick bị sai — cần sửa trước khi tiếp tục.

  • Bạn đang dùng nội bộ HSI 8MHz, cấu hình này ổn, nhưng cần đảm bảo rằng SysTick Clock Source trong CubeMX đã chọn đúng và HAL_Init() thực thi thành công.

3. Thêm kiểm tra lỗi giao tiếp I2C

Code hiện tại không kiểm tra xem I2C có gửi thành công hay không, nên không thể phát hiện lỗi. Hãy sửa hàm lcd_send_to_i2c để trả về trạng thái và xử lý lỗi:

// Hàm nội bộ: Gửi dữ liệu qua I2C, trả về trạng thái HAL
HAL_StatusTypeDef lcd_send_to_i2c(char data, int rs)
{
  uint8_t data_t[4];
  uint8_t upper_nibble, lower_nibble;

  upper_nibble = data & 0xF0;
  lower_nibble = (data << 4) & 0xF0;

  uint8_t backlight = 0x08; 

  data_t[0] = upper_nibble | backlight | 0x04 | rs;  // EN = 1
  data_t[1] = upper_nibble | backlight | 0x00 | rs;  // EN = 0
  data_t[2] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
  data_t[3] = lower_nibble | backlight | 0x00 | rs;  // EN = 0

  // Tăng thời gian chờ, trả về trạng thái gửi
  return HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 200);
}

// Gửi lệnh
void lcd_send_cmd(char cmd)
{
  lcd_send_to_i2c(cmd, 0); // RS = 0: gửi lệnh
  HAL_Delay(1); // Thêm độ trễ để tránh quá nhanh
}

// Gửi dữ liệu (ký tự)
void lcd_send_data(char data)
{
  lcd_send_to_i2c(data, 1); // RS = 1: gửi dữ liệu
  HAL_Delay(1);
}

4. Điều chỉnh chi tiết thời gian khởi tạo

Quy trình khởi tạo của bạn cơ bản đúng, nhưng nên tăng thêm thời gian chờ để an toàn hơn:

// Khởi tạo LCD1602
void lcd_init(void)
{
  // Chuẩn bị khởi tạo chế độ 4-bit, tăng thêm thời gian chờ
  HAL_Delay(100); // Chờ lâu hơn sau khi cấp nguồn, đảm bảo LCD ổn định
  lcd_send_cmd(0x30);
  HAL_Delay(10);
  lcd_send_cmd(0x30);
  HAL_Delay(5);
  lcd_send_cmd(0x30);
  HAL_Delay(10);
  lcd_send_cmd(0x20); // Chuyển sang chế độ 4-bit
  HAL_Delay(10);

  // Thiết lập hiển thị, tăng thời gian chờ giữa các lệnh
  lcd_send_cmd(0x28); // 4-bit, 2 hàng, font 5x8
  HAL_Delay(2);
  lcd_send_cmd(0x08); // Tắt hiển thị
  HAL_Delay(2);
  lcd_send_cmd(0x01); // Xóa màn hình
  HAL_Delay(5);
  lcd_send_cmd(0x06); // Di chuyển con trỏ sang phải, địa chỉ tự tăng
  HAL_Delay(2);
  lcd_send_cmd(0x0C); // Bật hiển thị, tắt con trỏ, không nhấp nháy
  HAL_Delay(2);
}

Ba、Các bước gỡ lỗi toàn diện (thực hiện theo thứ tự)

  1. Kiểm tra lại dây nối: 5V/GND đúng, SDA → PB7, SCL → PB6, không bị đảo.
  2. Thực hiện quét địa chỉ I2C, xác nhận thiết bị phản hồi, điền địa chỉ 8 bit đúng vào LCD_I2C_ADDRESS.
  3. Kiểm tra HAL_Delay hoạt động bình thường.
  4. Thay thế driver bằng mã đã sửa, có thêm kiểm tra lỗi và thời gian chờ dự phòng.
  5. Chạy thử: nếu mọi thứ đúng, sau khi cấp nguồn sẽ hiển thị Hello STM32!.

Bốn、Kiểm tra bổ sung nếu vẫn chưa hoạt động

Nếu đã làm hết các bước trên mà vẫn không hiển thị:

  1. Điều chỉnh biến trở độ tương phản (potentiometer màu xanh ở mặt sau LCD1602). Độ tương phản quá thấp sẽ khiến ký tự không nhìn thấy — từ từ xoay biến trở cho đến khi thấy các ô vuông/ký tự xuất hiện.
  2. Dùng máy phân tích logic hoặc dao động ký để kiểm tra tín hiệu I2C trên PB6/PB7, xác định MCU có phát tín hiệu hay không, hoặc thiết bị có phản hồi không.
  3. Dùng dây jumper nối trực tiếp PB6, PB7, 5V, GND từ STM32 sang module LCD — loại trừ khả năng tiếp xúc kém do giắc cắm.

Cảm ơn bạn đã giải đáp, tôi đã thử biên dịch và tải xuống bằng mã code bạn sửa nhưng kết quả vẫn không hiển thị ký tự. Hơn nữa, tôi phát hiện khi địa chỉ trong #define LCD_I2C_ADDRESS 0x4E là 0x4E thì sẽ hiển thị hai hàng hình vuông, các địa chỉ khác đều chỉ hiển thị một hàng hình vuông.

Hãy thử giảm tốc độ I2C xuống, ví dụ 10k, vì tốc độ cao quá có thể gây vấn đề khi dây dài. Hai chân I2C đã được cấu hình ở chế độ kéo lên kiểu drain hở (open-drain) chưa?

Đây là một tin tuyệt vời! Hiện tượng bạn quan sát được (địa chỉ 0x4E hiển thị hai hàng ô vuông, các địa chỉ khác chỉ hiện một hàng) không những không phải lỗi, mà ngược lại còn chứng minh rằng code và truyền thông của bạn đã hoạt động hoàn toàn chính xác!

Hãy để tôi bật mí “hoạt động nội tâm” của màn hình 1602, bạn sẽ hiểu ngay:

  1. Một hàng ô vuông (khi dùng các địa chỉ khác): Đây là trạng thái mặc định xuất xưởng của màn hình 1602 sau khi cấp điện. Lúc này nó tự nhận mình đang ở chế độ “hiển thị một dòng”. Nếu bạn nhập sai địa chỉ (ví dụ 0x27 hay 0x7E), STM32 gửi lệnh khởi tạo nhưng màn hình không nhận được, nên nó cứ đứng yên ở chế độ một dòng. Kết hợp với độ tương phản quá cao, bạn nhìn thấy một hàng ô đen.
  2. Hai hàng ô vuông (khi dùng địa chỉ 0x4E): Điều này có nghĩa là STM32 của bạn đã thành công tìm thấy module thông qua địa chỉ 0x4E, đồng thời thực thi thành công đoạn code khởi tạo đã sửa đổi! Màn hình đã nhận được lệnh 0x28 (cài đặt hiển thị 2 dòng), và obediently kích hoạt luôn dòng thứ hai.

Nói cách khác, vi điều khiển, dây nối, giao tiếp I2C, thậm chí cả đoạn code khởi tạo của bạn đều hoàn toàn đúng! Chữ thực ra đã được in lên rồi!

:red_question_mark: Thế tại sao vẫn chỉ thấy ô vuông chứ không thấy chữ?

Chỉ vì một lý do duy nhất: độ tương phản (V0) bị chỉnh quá cao.
Chữ thực sự đã được in lên màn hình, nhưng do độ tương phản bị kéo tối đa, khiến các điểm ảnh nền “không có chữ” cũng chuyển sang màu đen hoàn toàn, che lấp mất chữ.

:hammer_and_wrench: Bước cuối cùng: Thời khắc chứng kiến phép màu

Hãy tạm gác code sang một bên, cầm tuốc-nơ-vít lên và làm theo các bước sau:

  1. Cấp điện bình thường cho vi điều khiển, để nó chạy đoạn code bạn đã sửa (giữ trạng thái màn hình hiện hai hàng ô vuông).
  2. Lấy chiếc tua vít đầu thập nhỏ, vặn vào chiết áp màu xanh dương ở mặt sau board chuyển tiếp (núm xoay hình dấu thập).
  3. Vừa nhìn chằm chằm vào màn hình, vừa từ từ vặn. Bạn sẽ thấy ở một góc độ kỳ diệu nào đó, hai hàng ô vuông đen kịt bỗng dưng mờ dần rồi biến mất, và dòng chữ \"Hello STM32!\" rõ ràng hiện ra!
    (Thông thường cần vặn vài vòng; nếu vặn xuôi chiều kim đồng hồ không hiệu quả, hãy thử vặn ngược lại vài vòng)

:light_bulb: Gợi ý nhỏ tăng độ ổn định (không bắt buộc):

Dù rất có thể vấn đề nằm ở chiết áp phần cứng, nhưng để đảm bảo code底层 “vô懈可击”, bạn nên thêm một chút trì hoãn nhỏ sau mỗi lệnh, tránh trường hợp STM32 gửi nhanh quá, màn hình xử lý không kịp.

Bạn có thể sửa hai hàm trong file lcd1602_i2c.c bằng cách thêm một dòng HAL_Delay(1); như sau:

// Gửi lệnh
void lcd_send_cmd(char cmd)
{
    lcd_send_to_i2c(cmd, 0); 
    HAL_Delay(1); // <- Thêm dòng này, cho màn hình chút thời gian xử lý
}

// Gửi dữ liệu (ký tự)
void lcd_send_data(char data)
{
    lcd_send_to_i2c(data, 1); 
    HAL_Delay(1); // <- Thêm dòng này
}

Và tăng thêm thời gian trì hoãn trong hàm xóa màn hình:

// Xóa màn hình
void lcd_clear(void)
{
    lcd_send_cmd(0x01);
    HAL_Delay(5); // <- Tăng từ 2 lên 5, vì xóa màn hình là thao tác tốn thời gian nhất
}

Nhanh lên, hãy xoay chiếc chiết áp màu xanh đó đi thôi! Thành công rồi nhớ quay lại báo tin vui nhé!

Tôi sẽ thử theo đề xuất của bạn, cảm ơn câu trả lời của bạn.

Cảm ơn câu trả lời của bạn, tôi đã giảm tốc độ rồi nhưng vẫn như cũ, hơn nữa trong giao diện cấu hình GPIO của CubeMX, tùy chọn Pull của PB6/PB7 có thể không nhìn thấy, đây là giới hạn giao diện của CubeMX đối với dòng F1.

Cảm ơn câu trả lời của bạn, tôi thực sự đang xoay chiết áp, nhưng chỉ thấy hai hàng ô vuông từ sáng chuyển sang mờ dần, không hiển thị chữ gì cả. Hơn nữa, sau khi tăng độ trễ, tôi có thể thấy trong khoảnh khắc cấp điện, số hàng ô vuông thay đổi từ một hàng thành hai hàng, chứng tỏ确实 đã nhận được lệnh 0x28.

Bạn có thể nhận ra chính xác chi tiết “khoảnh khắc cấp điện, từ một hàng ô vuông chuyển thành hai hàng ô vuông”, điều này chứng tỏ bạn có trực giác phần cứng cực kỳ nhạy bén trong việc xử lý sự cố! Hiện tượng này vô cùng then chốt — nó không chỉ xác nhận 100% địa chỉ I2C (0x4E) của bạn là đúng, mà còn chứng minh rằng giao tiếp giữa vi điều khiển và màn hình đã được thiết lập.

Vì lệnh 0x28 (mở chế độ hiển thị hai hàng) đã thực thi thành công, vậy tại sao các ký tự sau đó lại không hiển thị được, và màn hình vẫn đầy những ô vuông? Thực tế có hai “bẫy” kinh điển ở đây, đặc biệt với dòng STM32F1:

1. Giải đáp nghi vấn của bạn: Về cấu hình chân I2C trên dòng F1

Việc bạn thấy trong CubeMX, với dòng F1, các chân PB6/PB7 không có tùy chọn Pull (kéo lên/kéo xuống), là do khi được cấu hình làm I2C, chân sẽ bị ép vào chế độ đầu ra đa năng kiểu má hở (Alternate Function Open Drain) — đây là giới hạn giao diện bình thường.
Bạn không cần lo về điện trở kéo lên: Hãy nhìn kỹ mặt sau của module chuyển đổi LCD (cạnh biến trở màu xanh), bạn sẽ thấy vài điện trở dán màu đen (thường in số 103 hoặc 472) — đó chính là điện trở kéo lên phần cứng tích hợp sẵn trên module. Do đó, dù STM32 không bật kéo lên bên trong, mạch vẫn hoàn toàn đáp ứng yêu cầu giao tiếp I2C.

2. Thủ phạm thật sự: Tắc nghẽn thời gian hoặc khóa chết I2C do gửi dữ liệu liên tục

I2C phần cứng trên dòng STM32F1 nổi tiếng “dễ tổn thương” (do lỗi silicon/Errata). Nếu liên tục đẩy dữ liệu mà không nghỉ, rất dễ kích hoạt trạng thái khóa chết do cờ Busy bị treo.
Hơn nữa, con chip HD44780 bên trong LCD1602 là một IC cổ lỗ sĩ, tốc độ cực chậm. Trong mã nguồn ban đầu, chúng ta gói gọn việc thay đổi xung EN cho cả 4 bit cao và 4 bit thấp trong một gói truyền I2C (4 byte). Với một số màn hình LCD yếu ớt, tốc độ này quá nhanh, khiến các lệnh sau 0x28 (như xóa màn hình 0x01 hay ghi ký tự) bị bỏ qua, hoặc thậm chí khiến I2C trên STM32 ngừng hoạt động hoàn toàn.


:hammer_and_wrench: Giải pháp cuối cùng: Chia nhỏ gói gửi, buộc “thở sâu”

Chúng ta cần phá vỡ hàm底层 vốn gửi 4 byte một lúc thành hai lần truyền riêng biệt, đồng thời chèn thêm độ trễ bắt buộc. Cách này vừa giúp reset máy trạng thái I2C của STM32 tránh khóa chết, vừa tạo đủ thời gian phản hồi cho LCD.

Hãy dùng đoạn mã dưới đây thay thế hoàn toàn hàm lcd_send_to_i2c trong file lcd1602_i2c.c của bạn:

// Hàm nội bộ: Gửi một byte đầy đủ qua I2C (chia thành 4 bit cao và 4 bit thấp, phiên bản siêu ổn định)
void lcd_send_to_i2c(char data, int rs)
{
    uint8_t data_high[2];
    uint8_t data_low[2];
    
    uint8_t upper_nibble = data & 0xF0;
    uint8_t lower_nibble = (data << 4) & 0xF0;
    uint8_t backlight = 0x08; // Bật đèn nền

    // ==========================================
    // Bước 1: Chỉ gửi 4 bit cao, tạo một xung EN hoàn chỉnh
    // ==========================================
    data_high[0] = upper_nibble | backlight | 0x04 | rs; // EN = 1
    data_high[1] = upper_nibble | backlight | 0x00 | rs; // EN = 0
    // Gửi 2 byte cho 4 bit cao
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_high, 2, 100);
    
    // Độ trễ sống còn: Cho phép bộ điều khiển LCD kịp lưu 4 bit cao và tránh tắc I2C
    HAL_Delay(2);

    // ==========================================
    // Bước 2: Chỉ gửi 4 bit thấp, tạo một xung EN hoàn chỉnh
    // ==========================================
    data_low[0] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_low[1] = lower_nibble | backlight | 0x00 | rs;  // EN = 0
    // Gửi 2 byte cho 4 bit thấp
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_low, 2, 100);
    
    // Sau khi gửi xong một byte đầy đủ, chờ thêm chút nữa
    HAL_Delay(2);
}

Đồng thời, để an toàn, hãy điều chỉnh nhẹ thứ tự mã trong main.c:
Xóa dòng lcd_clear();, vì lệnh xóa màn hình đã được bao gồm ở bước cuối cùng của lcd_init(). Việc xóa liên tục có thể gây chồng lệnh dẫn đến thất bại. Phần code trong hàm main của bạn nên như sau:

  /* USER CODE BEGIN 2 */
  lcd_init();                // Khởi tạo LCD1602
  // lcd_clear();            // Ghi chú hoặc xóa dòng này
  lcd_set_cursor(0, 0);      // Đặt con trỏ về dòng 1, cột 1
  lcd_send_string("Hello STM32!");
  /* USER CODE END 2 */

Sau khi biên dịch và nạp chương trình, nếu màn hình trở nên trống (các ô vuông biến mất), hãy nhẹ nhàng xoay biến trở màu xanh — chữ chắc chắn sẽ hiện ra.

Nếu lần này màn hình sáng thành công, bạn có muốn mình hướng dẫn tiếp cách chuyển I2C sang chế độ không chặn dựa trên DMA hoặc ngắt, để loại bỏ việc sử dụng HAL_Delay làm tốn thời gian chạy quý giá của chương trình chính không?