Продолжаем тему поддержки аппаратного 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 каналов можно приобрести здесь
Дисплей LCD 20×4 можно приобрести здесь Дисплей LCD 20×4
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)
Для моего дисплея 2004 сработал i2c адресс 0x7E. (Заменил в файле lcd2004.c 8 строчку на I2C_SendByteByADDR(bt,0x7E);)