Продолжаем учиться писать код для микроконтроллера ESP8266 и уже с использованием операционной системы реального времени FREEFTOS, с которой мы начали работу в прошлом занятии.
А на данном уроке мы, используя данную операционную систему, попробуем подключить к нашему контроллеру символьный дисплей разрешением в 4 строки по 20 символов с использованием шины I2C через специальный переходник.
С данным переходником мы знакомы, мы его уже подключали в уроке 10, а также смогли переопределить ножки SDA и SCL в уроке 18.
Теперь мы накопленные знания должны применить уже к использованию данного переходника в системе FreeRTOS, так как дисплей порой нам очень помогает в работе, а работа с ним в операционной системе несколько отличается от работы в автономном режиме.
Схему оставим с переопределёнными контактами из урока 18, хотя бы для того, чтобы не мигал постоянно при обмене по I2C светодиод
Проект за основу мы возьмём из прошлого урока с именем UART_TX_RTOS и назовём его I2C_LCD2004_RTOS.
Откроем наш проект в Eclipse и начнём добавлять в проект файлы для работы с переходником.
Чтобы нам полностью не писать данные файлы, добавим их из проекта урока 18 с именем I2C_LCD2004_REMAP. Также мы, благодаря тому, что будем эти файлы изменять, заодно и узнаем их отличие для операционной системы. Это файлы с именами i2c_user.h, lcd.h, i2c_user.c и lcd.c. Скопируем их в соответствующие папки нового проекта inc и src.
Причём в Makefile нам ввиду его универсальности теперь никак подключать эти библиотеки не нужно, они подключатся сами.
Начнем с файла i2c_user.h.
Первым делом удалим подключение данной библиотеки, так как такой в SDK RTOS нет
#include «osapi.h»
Вместо неё подключим сразу несколько
1 2 3 4 5 |
#include "esp_common.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp8266/pin_mux_register.h" #include "gpio.h" |
Вот эти макросы также слегка изменятся, поэтому сначала удалим их
#define I2C_MASTER_SDA_HIGH_SCL_HIGH() \
gpio_output_set(1<<I2C_MASTER_SDA_GPIO | 1<<I2C_MASTER_SCL_GPIO, 0, 1<<I2C_MASTER_SDA_GPIO | 1<<I2C_MASTER_SCL_GPIO, 0)
#define I2C_MASTER_SDA_HIGH_SCL_LOW() \
gpio_output_set(1<<I2C_MASTER_SDA_GPIO, 1<<I2C_MASTER_SCL_GPIO, 1<<I2C_MASTER_SDA_GPIO | 1<<I2C_MASTER_SCL_GPIO, 0)
#define I2C_MASTER_SDA_LOW_SCL_HIGH() \
gpio_output_set(1<<I2C_MASTER_SCL_GPIO, 1<<I2C_MASTER_SDA_GPIO, 1<<I2C_MASTER_SDA_GPIO | 1<<I2C_MASTER_SCL_GPIO, 0)
#define I2C_MASTER_SDA_LOW_SCL_LOW() \
gpio_output_set(0, 1<<I2C_MASTER_SDA_GPIO | 1<<I2C_MASTER_SCL_GPIO, 1<<I2C_MASTER_SDA_GPIO | 1<<I2C_MASTER_SCL_GPIO, 0)
И теперь они примут следующий вид
1 2 3 4 5 6 7 8 9 10 11 |
#define I2C_MASTER_SDA_HIGH_SCL_HIGH() \ GPIO_OUTPUT_SET(I2C_MASTER_SDA_GPIO, 1); GPIO_OUTPUT_SET(I2C_MASTER_SCL_GPIO, 1) #define I2C_MASTER_SDA_HIGH_SCL_LOW() \ GPIO_OUTPUT_SET(I2C_MASTER_SDA_GPIO, 1); GPIO_OUTPUT_SET(I2C_MASTER_SCL_GPIO, 0) #define I2C_MASTER_SDA_LOW_SCL_HIGH() \ GPIO_OUTPUT_SET(I2C_MASTER_SDA_GPIO, 0); GPIO_OUTPUT_SET(I2C_MASTER_SCL_GPIO, 1) #define I2C_MASTER_SDA_LOW_SCL_LOW() \ GPIO_OUTPUT_SET(I2C_MASTER_SDA_GPIO, 0); GPIO_OUTPUT_SET(I2C_MASTER_SCL_GPIO, 0) |
В файле lcd.h также удалим подключение данной библиотеки
#include «osapi.h»
А вместо неё подключим вот эту
1 |
#include "i2c_user.h" |
Переходим в файл i2c_user.c, в котором удалим подключение вот этих заголовочных файлов
#include «osapi.h»
#include «gpio.h»
А подключим мы теперь здесь вот этот заголовочный файл
1 2 |
#include "i2c_user.h" #include "esp8266/ets_sys.h" |
Удалим вот эти прототипы, с прерываниями тут будет немного по-другому
void ets_isr_mask(unsigned intr);
void ets_isr_unmask(unsigned intr);
Поэтому теперь в теле функции i2c_mas_gpio_init удалим вызов данного макроса
ETS_GPIO_INTR_DISABLE();
А макрос будет здесь такой, которым мы запретим все прерывания
1 |
ETS_INTR_LOCK(); |
Аналогично поступим и с разрешением прерываний в конце тела функции, удалив вызов вот этого макроса
ETS_GPIO_INTR_ENABLE();
А вместо него здесь будет вот такой
1 |
ETS_INTR_UNLOCK(); |
Переходим в файл lcd.c, в котором мы для начала удалим подключение данной библиотеки
#include «i2c_user.h»
Во всех местах использования функции ets_delay_us в качестве задержки мы будем использовать функцию os_delay_us, так что изменим её имя.
В функции LCD_ini вот эту задержку
os_delay_us(100000);
разобьём на две
os_delay_us(50000);
os_delay_us(50000);
так как использовать мы здесь в качестве аргумента можем только 16-разрядные величины.
С библиотеками пока закончим, проект наш теперь должен будет благополучно собраться.
В файле main.h подключим наши библиотеки
1 2 3 |
#include "gpio.h" #include "i2c_user.h" #include "lcd.h" |
Перейдём в main.c, в котором в функции user_rf_cal_sector_set исправим вычитаемое, почему-то у нас здесь ошибка
case FLASH_SIZE_4M_MAP_256_256:
rf_cal_sec = 128 - 5;
Добавим ещё две глобальных переменных перемен типа структуры, так как мы создадим целых 4 задачи
pData dt1, dt2, dt3, dt4;
В функции user_init() инициализируем наш переходник и дисплей, а также подготовим заголовки строк, соответствующие номерам наших задач
1 2 3 4 5 6 7 8 9 10 11 12 |
os_printf("\r\n\r\n"); i2c_mas_gpio_init(); I2C_MASTER_SDA_LOW_SCL_LOW(); LCD_ini(); LCD_SetPos(0,0); LCD_String("Task1:"); LCD_SetPos(0,1); LCD_String("Task2:"); LCD_SetPos(0,2); LCD_String("Task3:"); LCD_SetPos(0,3); LCD_String("Task4:"); |
Уменьшим задержку в первых двух переменных структуры, а также инициализируем остальные две переменные
dt1.del = 300; dt1.num_task = 1;
dt2.del = 400; dt2.num_task = 2;
dt3.del = 500; dt3.num_task = 3;
dt4.del = 600; dt4.num_task = 4;
Создадим ещё две задачи, также используя для них одну и ту же функцию задачи
1 2 3 |
xTaskCreate(task1, "task2", 256, (void *) &dt2, 1, NULL); xTaskCreate(task1, "task3", 256, (void *) &dt3, 1, NULL); xTaskCreate(task1, "task4", 256, (void *) &dt4, 1, NULL); |
Можно, казалось бы, уже приступить к изменению тела функции задачи, но для того, чтобы дисплей нормально работал и отзывался из всех задач и во избежание коллизий желательно передавать строки в него посредством очереди. С очередями мы знакомы, поэтому быстро сейчас этот вопрос решим.
Объявим глобальную переменную очереди
1 2 3 |
#include "main.h" //------------------------------------------------------ xQueueHandle xQueue; |
Создадим для неё тип структуры, аналогичный типу для параметров задач
1 2 3 4 5 6 |
xQueueHandle xQueue; typedef struct { unsigned char num_task; unsigned long cnt; } qData; |
Первое поле структуры — номер задачи, а второе — счётчик.
Для работы с дисплеем мы создадим отдельную приёмную задачу. Добавим для неё функцию
1 2 3 4 5 6 |
pData dt1, dt2, dt3, dt4; //------------------------------------------------------ void ICACHE_FLASH_ATTR vRecvTask(void *pvParameters) { } //------------------------------------------------------ |
В user_init() создадим очередь на 10 элементов типа структуры
1 2 |
dt4.del = 600; dt4.num_task = 4; xQueue = xQueueCreate(10, sizeof(qData)); |
Создадим также и задачу, у которой будет самый высокий приоритет, чтобы остальные задачи не приостановили её в какой-нибудь момент, что будет чревато искажением вывода информации на дисплей
1 2 |
xQueue = xQueueCreate(10, sizeof(qData)); xTaskCreate(vRecvTask, "vRecvTask", 256, NULL, 2, NULL); |
Сначала давайте изменим код функции передающей задачи task1, так как у нас там пока вывод информации производится в UART, а нам надо в дисплей. Создадим переменную структуры очереди
1 2 3 |
void ICACHE_FLASH_ATTR task1(void *pvParameters) { qData xReceivedData; |
Вместо переменной cnt мы здесь теперь будем использовать одноимённое поле переменной структуры, поэтому данную переменную удалим
uint32 cnt=0;
а аналогичное поле инициализируем
1 2 |
pData *pdt = (pData*) pvParameters; xReceivedData.cnt = 0; |
Инициализируем также и номер задачи в следующем поле
1 2 |
xReceivedData.cnt = 0; xReceivedData.num_task = pdt->num_task; |
Символьный массив нам здесь уже не потребуется, так как мы передаём не строки, а целые числа
char str01[20];
Объявим переменную состояния
1 2 |
xReceivedData.num_task = pdt->num_task; portBASE_TYPE xStatus; |
Вот эти строки в бесконечном цикле также удалим
snprintf(str01, sizeof(str01), «Task%d: %7lu», pdt->num_task, cnt);
os_printf(«%s\n», str01);
Отправим указатель на наши данные в очередь
1 2 3 |
while(1) { xStatus = xQueueSendToBack(xQueue, &xReceivedData, 0); |
Если вдруг вернётся плохой статус, отчитаемся об этом в UART
1 2 3 4 5 |
xStatus = xQueueSendToBack(xQueue, &xReceivedData, 0); if (xStatus != pdPASS) { os_printf("Could not send to the queue.\n"); } |
Счётчик также мы будем инкрементировать в поле
xReceivedData.cnt++;
if(xReceivedData.cnt>=10000000) xReceivedData.cnt=0;
С функцией передачи закончили, переходим к приёмной функции vRecvTask, в которой сначала также объявим переменную статуса
1 2 3 |
void ICACHE_FLASH_ATTR vRecvTask(void *pvParameters) { portBASE_TYPE xStatus; |
Затем объявим символьный массив
1 2 |
portBASE_TYPE xStatus; char str01[20]; |
Создадим переменную типа структуры очереди
1 2 |
char str01[20]; qData xReceivedData; |
Добавим бесконечный цикл, в котором сначала узнаем наполненность очереди, и, если она пустая, то выведем в терминальную программу соответствующее сообщение
1 2 3 4 5 6 7 8 |
qData xReceivedData; while(1) { if (uxQueueMessagesWaiting(xQueue) != 0) { os_printf("Queue should have been empty!\n"); } } |
Попытаемся забрать элемент (указатель на данные) из очереди
1 2 3 |
os_printf("Queue should have been empty!\n"); } xStatus = xQueueReceive(xQueue, &xReceivedData, 10000 /portTICK_RATE_MS); |
Таймаут здесь в 10 секунд, я думаю, этого вполне достаточно для того, чтобы дождаться данных.
И далее, если мы получили данные из очереди, то выведем на дисплей следующие строки, содержащие наши данные
1 2 3 4 5 6 7 |
xStatus = xQueueReceive(xQueue, &xReceivedData, 10000 /portTICK_RATE_MS); if (xStatus == pdPASS) { snprintf(str01, sizeof(str01), "%7lu", xReceivedData.cnt); LCD_SetPos(7,xReceivedData.num_task - 1); LCD_String(str01); } |
От номера задачи мы отняли единицу, так как строки нумеруются с нуля, а задачи — с единицы.
В противном случае отправим в UART сообщение, что мы не дождались данных
1 2 3 4 5 6 |
LCD_String(str01); } else { os_printf("Could not receive from the queue.\n"); } |
Соберём код, прошьём контроллер и посмотрим результат работы на дисплее
Если проверить состояние в терминальной программе, то мы там ничего не увидим, то есть никакие строки туда не приходят, значит у нас всё работает правильно, все задачи выполняться успевают, все данные передаются и принимаются.
Таким образом, на данному уроке нам удалось подключить символьный дисплей с переходником к нашему контроллеру, используя при этом операционную систему реального времени FreeRTOS, что, думаю в дальнейшем нам обязательно пригодится для мониторинга за состоянием программы. Пока, конечно, функция задачи работы с дисплеем у нас не универсальная, так как мы в неё передавали не строки, а специфические данные, но, тем не менее, мы настроили работу дисплея в условиях операционной системы.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP8266 Следующий урок
Модуль ESP NodeMCU можно купить здесь: Модуль ESP NodeMCU
Различные модули ЕSP8266 можно приобрести здесь Модули ЕSP8266
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Смотреть ВИДЕОУРОК в КuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Добавить комментарий