Продолжаем тему поддержки аппаратного I2C в контроллере ESP32 и сегодня мы немного закрепим знания по данной теме, написав небольшую программу по работе с символьным дисплеем разрешением в 4 строки по 20 символов, подключенному по шине I2C к нашему контроллеру.
С данным переходником мы неоднократно уже работали, поэтому изучать нам тонкости его работы нет смысла, лишь напомню, что он преобразует цифровой код, поступающий последовательно по I2C в параллельный 8-разрядный код.
Ножки порта для I2C мы будем использовать те же, что и на прошлом занятии, а также тем же образом мы подключим ещё и логический анализатор для того, чтобы следить за отправкой данных в переходник при необходимости.
Соответственно, схема у нас получится следующая
Также ценность данного урока будет не только в закреплении работы с шиной, но и в том, что такой дисплей нам ещё пригодится для мониторинга некоторой работы с контроллером.
Проект мы сделаем из проекта прошлого урока с именем I2C_EEPROM и назовём его I2C_LCD2004.
Из каталога main проекта удалим сразу же файлы at24c.c и at24c.h.
Откроем проект в Espressif IDE и, пока его не собирая, сразу создадим два модуля (четыре файла) со следующим содержимым
1 2 3 4 5 6 7 8 9 10 11 |
#ifndef MAIN_I2C_USER_H_ #define MAIN_I2C_USER_H_ //--------------------------------------------------------------------- #include <stdint.h> #include "sdkconfig.h" #include "driver/i2c.h" #include "esp_err.h" #include <unistd.h> //--------------------------------------------------------------------- //--------------------------------------------------------------------- #endif /* MAIN_I2C_USER_H_ */ |
1 2 |
#include "i2c_user.h" //--------------------------------------------------------------------- |
1 2 3 4 5 6 7 8 |
#ifndef MAIN_LCD2004_H_ #define MAIN_LCD2004_H_ //--------------------------------------------------------------------- #include "i2c_user.h" #include <unistd.h> //--------------------------------------------------------------------- //--------------------------------------------------------------------- #endif /* MAIN_LCD2004_H_ */ |
1 2 |
#include "lcd2004.h" //------------------------------------------------ |
Также давайте для большей компактности кода в основном модуле создадим заголовочный файл main.h следующего содержания
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#ifndef MAIN_MAIN_H_ #define MAIN_MAIN_H_ //--------------------------------------------------------------------- #include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "driver/gpio.h" #include "driver/i2c.h" #include "esp_err.h" #include "esp_log.h" #include "sdkconfig.h" #include "i2c_user.h" #include "lcd2004.h" //--------------------------------------------------------------------- #endif /* MAIN_MAIN_H_ */ |
Откроем файл main.c и удалим оттуда подключения всех библиотек, так как они теперь у нас подключены в заголовочном файле, а вместо них подключи соответственно данный файл
1 |
#include "main.h" |
Удалим следующий макрос
#define I2C_ADDRESS 0x50
Удалим также объявление массивов
uint8_t rd_value[20] = {0};
uint8_t wr_value[20] = {0x14,0x13,0x12,0x11,0x10,
0x0F,0x0E,0x0D,0x0C,0x0B,
0x0A,0x09,0x08,0x07,0x06,
0x05,0x04,0x03,0x02,0x01};
А в функции app_main оставим только объявление переменной-итератора, объявление переменной кода ошибки, а также задержку в бесконечном цикле, заодно увеличив её. Содержимое функции будет теперь следующее
1 2 3 4 5 6 7 8 |
void app_main(void) { uint16_t i=0; esp_err_t ret; while (1) { vTaskDelay(100 / portTICK_PERIOD_MS); } } |
Не забываем также убрать подключение в файле CMakeLists.txt каталога main файла at24c.c и подключить туда наши два новые
set(COMPONENT_SRCS "main.c i2c_user.c lcd2004.c")
Теперь можно попробовать собрать наш проект.
Если всё нормально собралось, то движемся далее.
В файле i2c_user.c объявим макрос для бита проверки отклика, переменную для хранения номера модуля, а также макрос с частотой шины
1 2 3 4 5 6 7 |
#include "i2c_user.h" //------------------------------------------------ #define ACK_CHECK_EN 0x1 /*!< I2C master will check ack from slave*/ //--------------------------------------------------------------------- i2c_port_t i2c_port = I2C_NUM_0; #define I2C_FREQUENCY 400000 //--------------------------------------------------------------------- |
Хотя в документации на микросхему из переходника дана частота 100 килогерц, но при 400 переходник тоже нормально работает и сегодня мы в этом убедимся. Ниже добавим функцию инициализации модуля I2C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//--------------------------------------------------------------------- esp_err_t i2c_ini(void) { esp_err_t ret; i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = CONFIG_SDA_GPIO, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_io_num = CONFIG_SCL_GPIO, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = I2C_FREQUENCY }; ret = i2c_param_config(i2c_port, &conf); if (ret != ESP_OK) return ret; ret = i2c_driver_install(i2c_port, I2C_MODE_MASTER, 0, 0, 0); return ret; } //--------------------------------------------------------------------- |
Инициализация, в принципе, та же самая, что и для at24c, никаких изменений.
Также выше добавим функцию отправки байта в шину
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//--------------------------------------------------------------------- void I2C_SendByteByADDR(uint8_t c,uint8_t addr) { i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, addr, ACK_CHECK_EN); i2c_master_write_byte(cmd, c, ACK_CHECK_EN); i2c_master_stop(cmd); i2c_master_cmd_begin(i2c_port, cmd, 1000 / portTICK_RATE_MS); i2c_cmd_link_delete(cmd); usleep(1000*2); } //--------------------------------------------------------------------- |
Добавим прототип в заголовочном файле на наши функции и вызовем функцию инициализации шины в функции app_main файла main.c, а также выведем значение кода ошибки в терминал
1 2 3 |
esp_err_t ret; ret = i2c_ini(); ESP_LOGI(TAG, "i2c_ini: %d", ret); |
В файле lcd2004.h добавим макросы для различных команд
1 2 3 4 5 6 7 8 9 10 |
#include <unistd.h> //--------------------------------------------------------------------- #define e_set() LCD_WriteByteI2CLCD(portlcd|=0x04) ////установка линии Е в 1 #define e_reset() LCD_WriteByteI2CLCD(portlcd&=~0x04) //установка линии Е в 0 #define rs_set() LCD_WriteByteI2CLCD(portlcd|=0x01) //установка линии RS в 1 #define rs_reset() LCD_WriteByteI2CLCD(portlcd&=~0x01) //установка линии RS в 0 #define setled() LCD_WriteByteI2CLCD(portlcd|=0x08) //установка линии BL в 1 #define setwrite() LCD_WriteByteI2CLCD(portlcd&=~0x02) //установка линии RW в 0 #define setread() LCD_WriteByteI2CLCD(portlcd|=0x02) //установка линии RW в 1 //--------------------------------------------------------------------- |
В файле lcd2004.c добавим глобальную переменную для хранения значения данных порта и переменную для позиции курсора
1 2 3 4 5 |
#include "lcd2004.h" //------------------------------------------------ uint8_t portlcd=0; //Ячейка для хранения данных порта микросхемы расширения uint32_t CUR_POS; //------------------------------------------------ |
Ниже добавим функцию передачи байта в шину
1 2 3 4 5 6 7 |
uint32_t CUR_POS; //------------------------------------------------ inline void LCD_WriteByteI2CLCD(uint8_t bt) { I2C_SendByteByADDR(bt,0x4E); } //------------------------------------------------ |
Ниже также добавим функцию передачи полубайта
1 2 3 4 5 6 7 8 9 10 11 |
//------------------------------------------------ void sendhalfbyte(uint8_t c) { c<<=4; LCD_WriteByteI2CLCD((portlcd|=0x04)|c);//включаем линию E usleep(1); LCD_WriteByteI2CLCD(portlcd|c); LCD_WriteByteI2CLCD((portlcd&=~0x04)|c);//выключаем линию E usleep(1); } //------------------------------------------------ |
Останавливаться подробно на данных функциях мы не будем, так как они такие же как и в подобном уроке для ESP8266.
Ниже добавим функцию передачи байта при помощи переходника в дисплей
1 2 3 4 5 6 7 8 9 10 |
//------------------------------------------------ void sendbyte(uint8_t c, uint8_t mode) { if(mode==0) rs_reset(); else rs_set(); uint8_t hc=0; hc=c>>4; sendhalfbyte(hc);sendhalfbyte(c); } //------------------------------------------------ |
И также ниже добавим функции установки курсора и передачи в дисплей строки
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//------------------------------------------------ void LCD_SetPos(uint8_t x, uint8_t y) { switch(y) { case 0: CUR_POS = x|0x80; break; case 1: CUR_POS = (0x40+x)|0x80; break; case 2: CUR_POS = (0x14+x)|0x80; break; case 3: CUR_POS = (0x54+x)|0x80; break; } sendbyte(CUR_POS,0); } //------------------------------------------------ void LCD_String(char* st) { uint8_t i=0; while(st[i]!=0) { sendbyte(st[i],1); i++; } } //------------------------------------------------ |
Ну и в самом низу — функцию инициализации дисплея
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//------------------------------------------------ void LCD_ini(void) { usleep(50000); LCD_WriteByteI2CLCD(0); setwrite();//запись usleep(50000); usleep(50000); sendhalfbyte(0x03); usleep(4500); sendhalfbyte(0x03); usleep(4500); sendhalfbyte(0x03); usleep(4500); sendhalfbyte(0x02); sendbyte(0x28,0);//режим 4 бит, 2 линии (дл¤ нашего большого диспле¤ это 4 линии, шрифт 5х8 sendbyte(0x08,0);//дисплей пока выключаем usleep(1000); sendbyte(0x01,0);//уберем мусор usleep(2000); sendbyte(0x06,0);// пишем вправо usleep(1000); sendbyte(0x0C,0);//дисплей включаем (D=1), курсоры никакие не нужны sendbyte(0x02,0);//курсор на место sendbyte(0X80,0);//SET POS LINE 0 usleep(2000); setled();//подсветка } //------------------------------------------------ |
В файле lcd2004.h добавим прототипы требуемых нам вне данного модуля функций
1 2 3 4 5 6 7 |
#define setread() LCD_WriteByteI2CLCD(portlcd|=0x02) //установка линии RW в 1 //--------------------------------------------------------------------- void LCD_WriteByteI2CLCD(uint8_t bt); void LCD_ini(void); void LCD_SetPos(uint8_t x, uint8_t y); void LCD_String(char* st); //--------------------------------------------------------------------- |
Вернёмся в файл main.c.
Мы будем использовать отдельную задачу для передачи в дисплей строк, так как мы работаем всё-таки с операционной системой, тем более мы так и делали ранее в случае использования контроллера esp8266 и нам это будет не в диковинку. Поэтому объявим структуру для передачи строки, а также очередь
1 2 3 4 5 6 7 8 9 10 11 |
#include "main.h" //------------------------------------------------ typedef struct { unsigned char y_pos; unsigned char x_pos; char *str; } qLCDData; //------------------------------------------------ xQueueHandle lcd_string_queue = NULL; //------------------------------------------------ |
Также добавим функцию для задачи по передаче строки в дисплей, в которой в цикле будем забирать значения строк и позиции из очереди и передавать их в дисплей через переходник по шине I2C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static const char *TAG = "main"; //------------------------------------------------ void vLCDTask(void* arg) { BaseType_t xStatus; qLCDData xReceivedData; for(;;) { xStatus = xQueueReceive(lcd_string_queue, &xReceivedData, 10000 /portTICK_RATE_MS); if (xStatus == pdPASS) { LCD_SetPos(xReceivedData.x_pos,xReceivedData.y_pos); LCD_String(xReceivedData.str); } } } //------------------------------------------------ |
В функции app_main создадим очередь и задачу
1 2 3 4 |
esp_err_t ret; qLCDData xLCDData; lcd_string_queue = xQueueCreate(10, sizeof(qLCDData)); xTaskCreate(vLCDTask, "vLCDTask", 2048, NULL, 2, NULL); |
Приоритет нам большой не требуется, поэтому хватит и 2.
Объявим локальный символьный массив
1 2 |
uint16_t i=0; char str01[10]; |
Вызовем функцию инициализации дисплея и после небольшой задержки при помощи цикла попробуем передать в дисплей четыре строки
1 2 3 4 5 6 7 8 9 10 |
ESP_LOGI(TAG, "i2c_ini: %d", ret); LCD_ini(); vTaskDelay(100 / portTICK_PERIOD_MS); xLCDData.str = str01; for(i=0;i<4;i++){ xLCDData.x_pos = i*3; xLCDData.y_pos = i; sprintf(str01,"String %d",i+1); xQueueSendToBack(lcd_string_queue, &xLCDData, 0); } |
Соберём код, прошьём контроллер и посмотрим результат на дисплее
Также посмотрим возврат кода ошибки в терминале
Можно было бы и не смотреть, у нас же дисплей работает как положено.
Затем посмотрим передачу данных в программе логического анализа
Здесь также всё нормально и частота приличная, хотя меньше чем 400, почему-то частота устанавливается неточно, но для нас, в принципе, это некритично.
Подождём пару секунд, очистим в нижнюю строку (забьём пробелами), обнулим итератор и вновь приравняем в переменной структуры очереди указатель на строку нашему локальному указателю
1 2 3 4 5 6 7 8 9 |
xQueueSendToBack(lcd_string_queue, &xLCDData, 0); } vTaskDelay(2000 / portTICK_PERIOD_MS); xLCDData.x_pos = 9; xLCDData.y_pos = 3; xLCDData.str = " "; xQueueSendToBack(lcd_string_queue, &xLCDData, 0); i = 0; xLCDData.str = str01; |
Затем в бесконечном цикле будем наращивать счётчик и передавать его значение в виде строки в дисплей
1 2 3 4 5 6 7 |
while (1) { i++; if(i>65534) i=0; sprintf(str01,"%5d",i); xLCDData.x_pos = 9; xLCDData.y_pos = 3; xQueueSendToBack(lcd_string_queue, &xLCDData, 0); |
Соберём код, прошьём контроллер и посмотрим результат на дисплее
Всё отлично! Счётчик считает.
Итак, на данном уроке нам удалось подключить к контроллеру символьный дисплей LCD2004 посредством переходника по шине I2C. Также мы немного поупражнялись с очередью.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP32 Следующий урок
Недорогие отладочные платы ESP32 можно купить здесь Недорогие отладочные платы ESP32
Логический анализатор 16 каналов можно приобрести здесь
Переходник I2C to LCD можно приобрести здесь I2C to LCD1602 2004
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)
Для моего дисплея 2004 сработал i2c адресс 0x7E. (Заменил в файле lcd2004.c 8 строчку на I2C_SendByteByADDR(bt,0x7E);)