Модуль lcd1602 i2c не отображает символы

Основная плата управления — минимальная плата stm32, модуль lcd1602 i2c куплен в интернете готовый.

Я новичок, код писал ИИ. Сначала отображалась одна строка квадратов, после изменения — две строки квадратов, но символы так и не появились. Я регулировал подстроечный резистор, напряжение i2c тоже 5 В.

Нарисуйте схему подключения и разместите код — как вы можете задавать вопросы с такой малой информацией, заставляя других гадать?

Пример правильного вопроса: https://bbs.eeclub.top/t/topic/289
Искусство задавать вопросы: https://bbs.eeclub.top/t/topic/109

1 лайк

Брат, не волнуйся — комбинация LCD1602 + I2C обязательно станет «обязательной ловушкой» для новичков в микроконтроллерах. Практически все начинающие сталкиваются с этой проблемой.

Исходя из твоего описания и трёх приложенных фотографий, могу тебя успокоить: скорее всего, с твоим оборудованием всё в порядке, просто не установилось соединение.

Тот факт, что экран загорелся и отображает два ряда квадратиков, говорит о том, что питание подано правильно, а контрастность, выставленная с помощью потенциометра, настроена как раз. Когда 1602-й дисплей показывает сплошные квадраты, это значит: экранный модуль получает питание, но не получил команду инициализации от микроконтроллера. Поскольку код написан ИИ, то с вероятностью 99% проблема кроется именно в программе или настройках связи. Проверь следующие пункты:

1. Самая распространённая ошибка: неправильно указан I2C-адрес устройства

Посмотри на заднюю часть платы — чёрная переходная плата использует микросхему PCF8574T. У таких модулей на рынке стандартный I2C-адрес обычно 0x27 или 0x3F.

Но внимание! Если ИИ писал код для библиотеки HAL для STM32, учти, что функция отправки данных по I2C в HAL требует, чтобы 7-битный адрес был сдвинут влево на один бит.

  • Если исходный адрес 0x27, в коде может потребоваться указать 0x4E (0x27 << 1).
  • Если исходный адрес 0x3F, в коде может быть нужно 0x7E (0x3F << 1).

ИИ часто путается в этом моменте и просто передаёт 0x27 в функцию HAL, из-за чего устройство так и не обнаруживается.

2. Перепутаны провода (распространённая ошибка у новичков)

Проверь подключение пинов на стороне STM32. Например, у микроконтроллера F103C8T6 аппаратный I2C1 по умолчанию использует PB6 (SCL) и PB7 (SDA). Убедись, что SDA на переходной плате подключена именно к PB7, а SCL — к PB6.

3. Неверные временные параметры или распиновка в нижнем уровне драйвера, написанного ИИ

Эта I2C-переходная плата фактически преобразует сигнал I2C в 8 параллельных выводов (P0–P7), которые управляют контактами RS, RW, EN и шиной данных 1602-го дисплея. Однако на разных платах конкретное соответствие между P-выводами и контактами может немного отличаться. Поэтому самописный драйвер, написанный ИИ с нуля, зачастую содержит ошибки в этих соответствиях.

Рекомендация: Не заставляй ИИ писать драйвер с нуля. Просто зайди на Bilibili или CSDN, найди готовые проверенные файлы lcd1602.c и lcd1602.h по запросу «STM32 HAL библиотека LCD1602 I2C», скачай их и добавь в свой проект — это самый надёжный способ.

4. Логические уровни напряжения (маловероятно, но возможно)

Ты упомянул, что питание 5 В — отлично, потому что LCD1602 должен работать именно от 5 В. Хотя выводы STM32 работают на логическом уровне 3.3 В, большинство I2C-пинов (например, PB6, PB7) являются «выдерживаемыми при 5 В (FT)», поэтому прямое подключение обычно не вызывает проблем. Главное — обеспечить общую землю (GND) между STM32 и модулем LCD.

:light_bulb: Совет по дальнейшим действиям (что делать дальше):

Не торопись сразу выводить символы. Сначала попроси ИИ написать простой код «I2C Scanner для STM32» (поиск устройств по шине I2C).

Прошей контроллер этим кодом и открой терминал последовательного порта, чтобы проверить, может ли STM32 обнаружить этот модуль на шине.

  • Если в терминале пишет, что устройство не найдено — значит, провода подключены неверно или I2C на STM32 не был корректно инициализирован.
  • Если в терминале появился найденный адрес (например, 0x4E) — значит, аппаратное подключение в порядке, и тебе остаётся лишь использовать этот адрес в коде инициализации LCD.

Желаю тебе скорее увидеть «Hello World»! Если зависнешь — возвращайся, спрашивай.

1 лайк

Краткое предупреждение по поводу аппаратного обеспечения: ЖК-дисплей и плата I2C работают при напряжении 5 В, тогда как ваш STM32 работает при 3,3 В. Хотя большинство выводов I2C на STM32 допускают напряжение 5 В (FT), стоит проверить техническую документацию, чтобы убедиться, что используемые вами конкретные выводы действительно допускают 5 В. Обычно подтягивающих резисторов на модуле достаточно, но обмен данными может не выполняться, если логические уровни не согласованы.

На 20-контактной разъемной колодке расположена минимальная система STM32, на 4-контактной — модуль lcd1602_i2c.

Ниже приведён файл 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();                // Инициализация LCD1602
    lcd_clear();               // Очистка экрана
    lcd_set_cursor(0, 0);      // Установка курсора в первую строку, первый столбец
    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 */

Ниже приведён файл lcd1602_i2c.h:

#ifndef __LCD1602_I2C_H
#define __LCD1602_I2C_H

#include "main.h" // Подключение библиотеки HAL и определений выводов
#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

Ниже приведён файл lcd1602_i2c.c:

#include "lcd1602_i2c.h"

// Объявление внешнего дескриптора I2C, который создаётся CubeMX в main.c
extern I2C_HandleTypeDef hi2c1;

// Внутренняя функция: отправка данных по 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;

    // Байт управления: бит 3 — подсветка (1=вкл), бит 2 — EN, бит 1 — RW (0=запись), бит 0 — 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

    // Передача 4 байт через I2C1
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 100);
}

// Отправка команды
void lcd_send_cmd(char cmd)
{
    lcd_send_to_i2c(cmd, 0); // RS = 0 означает передачу команды
}

// Отправка данных (символа)
void lcd_send_data(char data)
{
    lcd_send_to_i2c(data, 1); // RS = 1 означает передачу данных
}

// Очистка экрана
void lcd_clear(void)
{
    lcd_send_cmd(0x01);
    HAL_Delay(2); // Команда очистки требует больше времени на выполнение
}

// Установка позиции курсора (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); // Установка адреса DDRAM
}

// Отправка строки
void lcd_send_string(char *str)
{
    while (*str)
    {
        lcd_send_data(*str++);
    }
}

// Инициализация LCD1602
void lcd_init(void)
{
    // Стандартная последовательность инициализации для 4-битного режима
    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);

    // Настройка отображения
    lcd_send_cmd(0x28); // 4-линейный режим, 2 строки, шрифт 5x8
    HAL_Delay(1);
    lcd_send_cmd(0x08); // Выключить отображение
    HAL_Delay(1);
    lcd_send_cmd(0x01); // Очистить экран
    HAL_Delay(2);
    lcd_send_cmd(0x06); // Сдвиг курсора вправо
    HAL_Delay(1);
    lcd_send_cmd(0x0C); // Включить дисплей, выключить курсор
}

Я написал упрощённую схему подключения и код в области ответов, пожалуйста, помогите проверить

Хорошо, спасибо вам, я попробую ваше предложение.

После компиляции и загрузки я использую модуль питания, при этом напряжение на выводах VCC и GND интерфейса I2C составляет 5 В.

Отлично, предоставление схемы и исходного кода повышает эффективность отладки в сотни раз!

Сразу успокою: подключение вашего оборудования абсолютно правильное. На схеме PB6 подключён к SCL, PB7 — к SDA, что соответствует стандартным выводам I2C1 микроконтроллера STM32F103. Кроме того, в заголовочном файле вы указали #define LCD_I2C_ADDRESS 0x4E, что означает, что вы (или ИИ) уже знаете, что HAL-библиотека требует сдвига адреса 0x27 на один бит влево — таким образом вы успешно обошли две самые распространённые ошибки новичков.

Настоящая причина отображения только квадратиков — ошибка в функции инициализации (lcd_init), написанной ИИ, связанная с последовательностью согласования режима “4-битный интерфейс”.

:bug: Анализ основной ошибки: ловушка «принудительной инициализации» HD44780

Ваша плата преобразования I2C (PCF8574) управляет дисплеем 1602 по четырём линиям данных (D4–D7).
После включения питания дисплей изначально находится в 8-битном режиме. Чтобы перевести его в 4-битный режим, необходимо строго следовать спецификации и отправить несколько команд по одному полубайту (только старшие 4 бита, без младших).

Теперь обратите внимание на вашу функцию lcd_send_cmd(). Она вызывает lcd_send_to_i2c() — «трудолюбивую» нижележащую функцию, которая автоматически разбивает любую команду на два полубайта: сначала передаёт старшие 4 бита (с одним импульсом EN), затем младшие 4 бита (с другим импульсом EN).

Когда ИИ записал lcd_send_cmd(0x30) при инициализации:

  1. Замысел: отправить только 0x3 (старший полубайт).
  2. Что происходит на самом деле: lcd_send_to_i2c отправляет сначала 0x3, а затем — лишний 0x0 (младший полубайт), генерируя второй импульс EN.
  3. Результат: экран получает лишнюю команду 0x0, последовательность сбивается, инициализация не проходит, экран зависает в начальном состоянии с полным заполнением символами-квадратиками.

:hammer_and_wrench: Решение

Нам нужно добавить в файл lcd1602_i2c.c специальную функцию для отправки одного полубайта, предназначенную исключительно для этапа пробуждения экрана, и переписать lcd_init().

Замените соответствующие части в lcd1602_i2c.c следующим кодом (замените всё до lcd_init и саму lcd_init, остальное оставьте как есть):

#include "lcd1602_i2c.h"

// Объявление внешнего хэндла I2C
extern I2C_HandleTypeDef hi2c1;

// ====== НОВОЕ: Специальная функция для отправки одного полубайта при инициализации ======
void lcd_send_cmd_4bit(uint8_t nibble)
{
    uint8_t data_t[2];
    uint8_t backlight = 0x08; // Подсветка включена

    // Примечание: входящий nibble должен быть выровнен по старшим 4 битам (например, 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);
}

// Внутренняя функция: отправка полного байта через 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;

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

// ====== ПЕРЕПИСАННАЯ: правильная процедура инициализации в 4-битном режиме ======
void lcd_init(void)
{
    // 1. Этап принудительной инициализации (необходимо строго отправлять только по одному полубайту)
    HAL_Delay(50);
    lcd_send_cmd_4bit(0x30); // Пробуждение 1
    HAL_Delay(5);
    lcd_send_cmd_4bit(0x30); // Пробуждение 2
    HAL_Delay(1);
    lcd_send_cmd_4bit(0x30); // Пробуждение 3
    HAL_Delay(10);
    lcd_send_cmd_4bit(0x20); // Сообщаем экрану: "Переходи в 4-битный режим!"
    HAL_Delay(10);

    // 2. Этап нормальной настройки (экран теперь в 4-битном режиме, можно использовать обычную функцию)
    lcd_send_cmd(0x28); // Настройка функций: 4-битный интерфейс, 2 строки, шрифт 5x8
    HAL_Delay(1);
    lcd_send_cmd(0x08); // Выключить дисплей
    HAL_Delay(1);
    lcd_send_cmd(0x01); // Очистить экран
    HAL_Delay(2);       // Команда очистки экрана медленная — задержка минимум 2 мс
    lcd_send_cmd(0x06); // Установка режима: перемещение курсора вправо
    HAL_Delay(1);
    lcd_send_cmd(0x0C); // Включить дисплей, скрыть курсор
}

// ... Остальные функции, такие как lcd_send_cmd, lcd_send_data и т.д., остаются без изменений ...

:magnifying_glass_tilted_left: Дополнительная точка проверки

Если после изменения кода и повторной прошивки экран по-прежнему показывает квадратики, возможно, у вашего модуля I2C-адрес по умолчанию — 0x3F, а не 0x27. В этом случае откройте файл lcd1602_i2c.h и замените строку:

#define LCD_I2C_ADDRESS 0x4E

на

#define LCD_I2C_ADDRESS 0x7E  // 0x3F << 1

и попробуйте снова.

Попробуйте сначала внести эти изменения в код инициализации. Если понадобится, могу помочь написать простой «сканер I2C-адресов», чтобы точно определить адрес устройства?

Можно проверить с помощью логического анализатора, появляется ли форма сигнала I2C и правильная ли у неё последовательность. Если с I2C всё в порядке, далее следует проверить с помощью логического анализатора наличие параллельного сигнала на выходе модуля преобразования I2C в параллельный интерфейс и правильность его последовательности.

Как вы настраиваете STM32CubeMX?

Анализ сути проблемы

Если подсветка LCD1602 работает, но символы не отображаются, в 90% случаев причина — ошибка связи по I2C (неправильный адрес/ошибки в настройке выводов/отсутствие ответа), далее идут ошибки временной диаграммы инициализации или задержек, а также проблемы с питанием и подтягивающими резисторами. Ниже приведено полное руководство по диагностике и устранению неполадок в порядке приоритета.


Первый этап: Проверка аппаратной части (начните с базовых проблем)

1. Проверка ключевых контактов и подключения

Согласно вашей схеме:

  • Разъём H2 для подключения LCD: 1=+5V, 2=GND, 3=SDA(PB7), 4=SCL(PB6)
  • Для I2C1 микроконтроллера STM32: SCL должен быть подключён к PB6, SDA — к PB7. Проверьте назначение выводов в CubeMX, крайне важно не перепутать SDA и SCL.

2. Питание должно быть 5 В

Модуль LCD1602 с платой преобразования I2C рассчитан на 5 В. При питании 3.3 В подсветка может работать, но не будет работать связь по I2C. Подключайте модуль строго к линии 5 В микроконтроллера, а не к 3.3 В.

3. На шину I2C необходимо установить подтягивающие резисторы

Шина I2C работает по принципу «открытый сток», поэтому требует подтягивания:

  • Способ 1: В CubeMX настройте режим GPIO для PB6 и PB7 как «открытый сток с подтяжкой» (Open Drain Pull-up)
  • Способ 2: Установите внешние подтягивающие резисторы 4.7 кОм между PB6, PB7 и линией 5 В
  • Отсутствие подтяжки приводит к сбоям сигнала I2C и невозможности получения ответа от устройства.

Второй этап: Решение основных программных проблем (по приоритету)

1. 【Самая частая ошибка】Исправление адреса I2C

В вашем коде указано #define LCD_I2C_ADDRESS 0x4E, однако этот адрес может не совпадать с реальным адресом вашего модуля:

  • Модуль I2C для LCD1602 основан на микросхеме PCF8574, чей 7-битный адрес обычно равен 0x27 или 0x3F. Функция HAL HAL_I2C_Master_Transmit требует передачи 8-битного адреса записи (7-битный адрес, сдвинутый влево на 1 бит):
    • 7-битный адрес 0x27 → 8-битный адрес записи 0x4E (текущий адрес в вашем коде)
    • 7-битный адрес 0x3F → 8-битный адрес записи 0x7E (очень распространённый альтернативный адрес)

Быстрая проверка правильности адреса (обязательно выполните!)

Добавьте после MX_I2C1_Init() код сканирования адресов I2C, чтобы определить, какой адрес отвечает:

// Поместите после MX_I2C1_Init(), до вызова 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)
  {
    // Поставьте точку останова или мигните светодиодом, чтобы зафиксировать найденный адрес.
    // Запишите значение (i2c_addr<<1) в LCD_I2C_ADDRESS
    break;
  }
}

Если ни один адрес не найден — есть проблемы с подключением или инициализацией I2C. Сначала решите их.

2. Проверьте корректность работы HAL_Delay

Инициализация LCD сильно зависит от задержек. Если HAL_Delay не работает, последовательность инициализации нарушится, и дисплей не запустится.

  • Проверка: добавьте мигание светодиода в главный цикл:
while (1)
{
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // Укажите пин, соответствующий светодиоду на вашей плате
  HAL_Delay(500);
}

Если светодиод мигает не раз в 500 мс — ошибка в настройке системной частоты или SysTick. Сначала исправьте конфигурацию тактирования.

  • Вы используете внутренний источник HSI 8 МГц — это допустимо, но убедитесь, что в CubeMX правильно выбран источник тактирования SysTick и что функция HAL_Init() выполняется без ошибок.

3. Добавьте проверку ошибок при передаче по I2C

Ваш код не проверяет успешность отправки данных по I2C, из-за чего невозможно диагностировать сбои. Измените функцию lcd_send_to_i2c, добавив возврат статуса и обработку ошибок:

// Внутренняя функция: отправка данных по I2C, возвращает статус 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

  // Увеличен таймаут, возвращается статус передачи
  return HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_t, 4, 200);
}

// Отправка команды
void lcd_send_cmd(char cmd)
{
  lcd_send_to_i2c(cmd, 0); // RS = 0 — команда
  HAL_Delay(1); // Добавлена задержка для выполнения команды
}

// Отправка данных (символа)
void lcd_send_data(char data)
{
  lcd_send_to_i2c(data, 1); // RS = 1 — данные
  HAL_Delay(1);
}

4. Уточнение временных параметров инициализации

Последовательность инициализации в целом верна, но можно добавить дополнительные задержки для надёжности:

// Инициализация LCD1602
void lcd_init(void)
{
  // Стандартная процедура инициализации 4-битного режима, с увеличенными задержками
  HAL_Delay(100); // Длительная задержка после включения, чтобы LCD стабилизировался
  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); // Переход в 4-битный режим
  HAL_Delay(10);

  // Настройка отображения, с задержками после каждой команды
  lcd_send_cmd(0x28); // 4-линии, 2 строки, шрифт 5x8
  HAL_Delay(2);
  lcd_send_cmd(0x08); // Выключить отображение
  HAL_Delay(2);
  lcd_send_cmd(0x01); // Очистить экран
  HAL_Delay(5);
  lcd_send_cmd(0x06); // Курсор вправо, автоматическое увеличение адреса
  HAL_Delay(2);
  lcd_send_cmd(0x0C); // Включить отображение, курсор выключен, мерцание отключено
  HAL_Delay(2);
}

Третий этап: Полная процедура отладки (выполнять по порядку)

  1. Проверьте подключение: +5 В, GND, SDA → PB7, SCL → PB6, без перепутанных проводов.
  2. Выполните сканирование адресов I2C, убедитесь, что устройство отвечает, и подставьте правильный 8-битный адрес в LCD_I2C_ADDRESS.
  3. Убедитесь, что HAL_Delay работает корректно.
  4. Обновите код драйвера с учётом проверки ошибок и увеличенных задержек.
  5. Протестируйте: если всё работает, после включения должно появиться сообщение Hello STM32!.

Четвёртый этап: Дополнительные меры на случай крайнего случая

Если ничего не помогло:

  1. Отрегулируйте переменный резистор контрастности на обратной стороне LCD1602 (синий регулировочный элемент). Слишком низкий контраст сделает символы невидимыми. Аккуратно поворачивайте его, пока не появятся прямоугольники или символы.
  2. Проверьте сигналы I2C на PB6/PB7 с помощью логического анализатора или осциллографа, чтобы понять — отправляет ли МК сигналы, или устройство просто не отвечает.
  3. Подключите модуль LCD напрямую к STM32 с помощью перемычек (PB6, PB7, 5 В, GND), исключив плохой контакт в разъёмах.

Спасибо за ваш ответ. Я уже попробовал скомпилировать и загрузить с изменённым вами кодом, но символы всё равно не отображаются. Кроме того, я обнаружил, что при адресе #define LCD_I2C_ADDRESS 0x4E отображаются два ряда квадратов, а при других адресах — только один ряд квадратов.

Попробуйте уменьшить скорость I2C, например, до 10 кГц. При высокой скорости и слишком длинных линиях могут возникнуть проблемы. Установлены ли выводы I2C в режим с открытым стоком с подтяжкой?

Это великолепная новость! Наблюдаемое вами явление (когда по адресу 0x4E отображаются два ряда квадратов, а по другим адресам — только один) — вовсе не ошибка, а наоборот, прямое доказательство того, что ваш код и связь полностью работают!

Позвольте раскрыть вам «внутренний мир» экрана 1602 и объяснить, почему так происходит:

  1. Один ряд квадратов (при других адресах): это стандартное заводское состояние экрана 1602 после включения питания. По умолчанию он находится в режиме «однострочного отображения». Если вы указываете неправильный адрес (например, 0x27 или 0x7E), микроконтроллер STM32 не может установить с ним связь, инициализационные команды не доходят, поэтому экран остаётся в однострочном режиме. А из-за слишком высокой контрастности вы видите просто сплошной ряд чёрных квадратов.

  2. Два ряда квадратов (по адресу 0x4E): это значит, что ваш STM32 успешно нашёл модуль по адресу 0x4E и успешно выполнил ваш изменённый код инициализации! Экран получил команду 0x28 (настройка отображения в 2 строки) и послушно активировал вторую строку.

Иными словами, ваш микроконтроллер, провода, I2C-связь и даже код инициализации — всё работает правильно! Текст уже записан на экран!

:red_question_mark: Тогда почему мы видим только квадраты, а не текст?

Только одна причина: слишком высокая контрастность (регулировка V0).
Текст действительно уже напечатан на экране, но из-за максимальной контрастности фоновые пиксели также становятся чёрными и “заливают” собой символы, скрывая их.

:hammer_and_wrench: Последний шаг: момент истины

Отложите код в сторону, возьмите отвёртку и выполните следующие действия:

  1. Подайте питание на микроконтроллер, чтобы он запустил ваш обновлённый код (экран должен показывать два ряда квадратов).
  2. Возьмите маленькую крестовую отвёртку и начните медленно поворачивать синий потенциометр (регулятор с крестообразной головкой) на обратной стороне переходной платы.
  3. Не отводя взгляда от экрана, аккуратно вращайте. В какой-то момент вы увидите, как два ряда чёрных квадратов внезапно станут светлее или исчезнут, а на их месте ясно проявится надпись \"Hello STM32!\"!
    (Обычно требуется несколько оборотов. Если по часовой стрелке нет эффекта — попробуйте повернуть против часовой стрелки.)

:light_bulb: Дополнительный совет для идеального кода (по желанию):

Хотя проблема, скорее всего, в аппаратном потенциометре, для полной надёжности вашего кода рекомендуется добавить небольшие задержки после отправки команд — чтобы STM32 не «задавал» экран командами быстрее, чем тот успевает обрабатывать.

Вы можете добавить HAL_Delay(1); в две функции в файле lcd1602_i2c.c:

// Отправка команды
void lcd_send_cmd(char cmd)
{
    lcd_send_to_i2c(cmd, 0); 
    HAL_Delay(1); // <- Добавьте эту строку, чтобы дать экрану время на обработку
}

// Отправка данных (символа)
void lcd_send_data(char data)
{
    lcd_send_to_i2c(data, 1); 
    HAL_Delay(1); // <- Добавьте эту строку
}

А также увеличьте задержку в функции очистки экрана:

// Очистка экрана
void lcd_clear(void)
{
    lcd_send_cmd(0x01);
    HAL_Delay(5); // <- Увеличьте с 2 до 5, так как очистка — самая длительная операция
}

Скорее вращайте синий потенциометр — и обязательно сообщите, когда у вас всё заработает!

Я попробую твои рекомендации, спасибо за ответ.

Спасибо за ваш ответ, я уменьшил скорость, но результат тот же. Кроме того, в интерфейсе настройки GPIO в CubeMX параметр Pull для выводов PB6/PB7, возможно, недоступен — это ограничение интерфейса CubeMX для серии F1.

Спасибо за ваш ответ. Я действительно вращаю потенциометр, но вижу только, как два ряда квадратов постепенно затемняются, текст не отображается. После увеличения задержки я могу увидеть момент подачи питания — с одного ряда квадратов переходит на два ряда, что подтверждает, что команда 0x28 действительно была получена.

Вы смогли точно уловить деталь «при подаче питания сначала отображается один ряд квадратов, который затем превращается в два ряда» — это говорит о чрезвычайно острых интуитивных навыках диагностики аппаратных проблем! Данный феномен имеет огромное значение: он не только на 100 % подтверждает правильность вашего I2C-адреса (0x4E), но и доказывает, что установлена рабочая связь между микроконтроллером и дисплеем.

Раз команда 0x28 (включение отображения двух строк) была успешно выполнена, то почему последующие символы не отображаются, а на экране по-прежнему остаются одни квадраты? Причина здесь кроется в двух типичных «подводных камнях», особенно характерных для серии STM32F1:

1. Ответ на ваш вопрос: конфигурация выводов I2C для серии F1

Вы отметили, что в CubeMX для серии F1 на выводах PB6/PB7 отсутствует опция Pull (подтягивания). Это объясняется тем, что при выборе режима I2C выводы автоматически переходят в режим открытого стока с альтернативной функцией (Alternate Function Open Drain) — такое поведение является нормой, и ограничение интерфейса тут ни при чём.
Не стоит беспокоиться о подтягивающих резисторах: взгляните внимательно на обратную сторону модуля-переходника LCD (рядом с синим потенциометром). Там припаяны несколько чёрных SMD-резисторов (обычно с маркировкой 103 или 472) — это и есть аппаратные подтягивающие резисторы, встроенные в сам модуль. Таким образом, даже если STM32 не обеспечивает внутреннее подтягивание, электрическая схема полностью соответствует требованиям I2C.

2. Настоящий «виновник»: перегрузка шины из-за непрерывной передачи или блокировка I2C

Аппаратный I2C в серии STM32F1 печально известен своей «капризностью» (из-за конструктивных дефектов/Errata). Если отправлять данные без пауз, «забивая» шину байт за байтом, легко можно вызвать постоянную установку флага Busy и полную блокировку I2C.
Кроме того, контроллер HD44780 внутри LCD1602 — это чрезвычайно медленный и устаревший чип. В исходном коде мы за один пакет передачи I2C (4 байта) мгновенно выполняли переключение сигнала EN для старшей и младшей тетрад. Для некоторых экземпляров LCD такая скорость оказывается слишком высокой: после команды 0x28 все последующие команды (например, очистка экрана 0x01 или вывод символов) просто игнорируются, либо сам I2C STM32 выходит из строя.


:hammer_and_wrench: Окончательное решение: разделённая передача с принудительными паузами

Нам нужно радикально изменить базовую функцию, которая раньше отправляла 4 байта за раз, разделив её на два независимых вызова передачи, добавив между ними принудительную задержку. Такой подход позволит как сбросить конечный автомат I2C STM32 и избежать блокировки, так и дать дисплею достаточно времени на обработку данных.

Пожалуйста, полностью замените функцию lcd_send_to_i2c в вашем файле lcd1602_i2c.c следующим кодом:

// Внутренняя функция: отправка полного байта по I2C (разделение на старшую и младшую тетрады, надёжная версия)
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; // Подсветка включена постоянно

    // ==========================================
    // Шаг первый: отправляем только старшую тетраду с полным импульсом EN
    // ==========================================
    data_high[0] = upper_nibble | backlight | 0x04 | rs; // EN = 1
    data_high[1] = upper_nibble | backlight | 0x00 | rs; // EN = 0
    // Передаём 2 байта старшей тетрады
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_high, 2, 100);
    
    // Критически важная задержка: даёт контроллеру LCD время на защёлкивание данных и предотвращает блокировку шины I2C
    HAL_Delay(2);

    // ==========================================
    // Шаг второй: отправляем младшую тетраду с полным импульсом EN
    // ==========================================
    data_low[0] = lower_nibble | backlight | 0x04 | rs;  // EN = 1
    data_low[1] = lower_nibble | backlight | 0x00 | rs;  // EN = 0
    // Передаём 2 байта младшей тетрады
    HAL_I2C_Master_Transmit(&hi2c1, LCD_I2C_ADDRESS, data_low, 2, 100);
    
    // После отправки полного байта делаем ещё одну паузу
    HAL_Delay(2);
}

Одновременно, на всякий случай, немного скорректируйте порядок кода в main.c:
Удалите строку lcd_clear();, поскольку команда очистки уже включена в последний шаг функции lcd_init(). Повторная очистка может привести к сбою из-за перегрузки очереди команд. Участок кода в функции main должен выглядеть примерно так:

  /* USER CODE BEGIN 2 */
  lcd_init();                // Инициализация LCD1602
  // lcd_clear();            // Закомментируйте или удалите эту строку
  lcd_set_cursor(0, 0);      // Установка курсора в первую позицию первой строки
  lcd_send_string("Hello STM32!");
  /* USER CODE END 2 */

После компиляции и прошивки, если экран станет полностью чистым (квадраты исчезнут), аккуратно поверните синий потенциометр — символы обязательно появятся.

Если на этот раз всё заработает, хотите, чтобы я подробно рассказал, как перестроить работу I2C в режиме DMA или на прерываниях, чтобы избавиться от HAL_Delay и освободить драгоценное время основной программы?