Продолжаем тему поддержки программного I2C в контроллере ESP8266 и сегодня мы ещё закрепим знания по данной теме, продолжив работу с символьным дисплеем разрешением в 4 строки по 20 символов, подключенному по шине I2C к нашему контроллеру. И целью данного занятия будет переопределение шины I2C на другие ножки. А возможно это благодаря тому, что I2C в контроллере ESP8266 программный, поэтому неважно, какие ножки дёргать.
Но, так как мы работаем с библиотекой SDK, да ещё тем более у которой код для нас закрыт, то это становится немного посложнее. Но, тем не менее, попробуем с такой задачей справиться.
Возникла такая задача из-за того, что одна из ножек I2C является также ножкой SPI, а нам вполне может понадобиться одновременное использование данных интерфейсов. А другая ножка совпадает с ножкой управления свечением синего светодиода.
Поэтому сегодня мы попытаемся переопределить ножки I2C на такие контакты, которые используются мало где, а также написать код так, чтобы в дальнейшем мы могли назначить на SDA и SCL любые ножки GPIO.
Схему нашу мы соберём почти такую же как и в уроке 10, только провода с контактов SDA и SCL переходника мы подключим соответственно к ножкам отладочной платы D1 и D2, которые в плате подключены к ножкам контроллера GPIO5 и GPIO4
Проект урока сделан из проекта урока 10 с именем I2C_LCD2004 и назван I2C_LCD2004_REMAP.
Откроем наш проект в Eclipse и в файле main.c удалим подключение стандартной библиотеки SDK для работы с I2C
#include «driver/i2c_master.h»
У нас будет своя библиотека. Правда часть кода было взято из примера исходного кода из того же SDK.
Также на данном уроке мы автоматизируем файл сценария Makefile, чтобы впоследствии при подключении новых модулей нам не приходилось вносить в него никаких изменений.
По требованиям нашего будущего файла сценария для каждого файла с исходным кодом с расширением c должен быть одноимённый заголовочный файл с расширением h. Исключением будет только файл user_config.h.
Поэтому создадим файл main.h следующего содержания
1 2 3 4 5 6 7 8 9 10 11 12 |
#ifndef MAIN_H_ #define MAIN_H_ //------------------------------------------------ #include <stdio.h> #include "ets_sys.h" #include "osapi.h" #include "user_interface.h" #include "gpio.h" #include "lcd.h" #include "i2c_user.h" //------------------------------------------------ #endif /* MAIN_H_ */ |
В файле main.с теперь удалим подключение всех бибилиотек
#include <stdio.h>
#include «ets_sys.h»
#include «osapi.h»
#include «user_interface.h»
#include «gpio.h»
#include «lcd.h»
А вместо них подключим наш новый заголовочный файл
1 2 3 |
#include "main.h" //------------------------------------------------------ uint32 ICACHE_FLASH_ATTR user_rf_cal_sector_set(void) |
В файле i2c_user.c подключим две стандартные библиотеки SDK
1 2 3 |
#include "osapi.h" #include "gpio.h" #include "i2c_user.h" |
Добавим пока пустотелую функцию инициализации ножек I2C
1 2 3 4 5 6 |
#include "i2c_user.h" //------------------------------------------------ void ICACHE_FLASH_ATTR i2c_mas_gpio_init(void) { } //------------------------------------------------ |
Добавим прототип на данную функцию в заголовочном файле i2c_user.h, перейдём в тело функции user_init() файла main.c и удалим вызов следующей функции
i2c_master_gpio_init();
А вместо неё вызовем только что созданную нами
1 2 |
gpio_init(); i2c_mas_gpio_init(); |
В файле i2c_user.h удалим подключение стандартной библиотеки I2C
#include «driver/i2c_master.h»
Добавим свои макросы для назначения ножек GPIO, а также для установки различных комбинаций уровней на них
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include "osapi.h" //------------------------------------------------ #define I2C_MASTER_SDA_MUX PERIPHS_IO_MUX_GPIO5_U #define I2C_MASTER_SCL_MUX PERIPHS_IO_MUX_GPIO4_U #define I2C_MASTER_SDA_FUNC FUNC_GPIO5 #define I2C_MASTER_SCL_FUNC FUNC_GPIO4 #define I2C_MASTER_SDA_GPIO 5 #define I2C_MASTER_SCL_GPIO 4 //------------------------------------------------ #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) //------------------------------------------------ |
Перейдём в файл i2c_user.c и добавим следующие прототипы функций для функционирования работы с прерываниями
1 2 3 4 5 |
#include "i2c_user.h" //------------------------------------------------ void ets_isr_mask(unsigned intr); void ets_isr_unmask(unsigned intr); //------------------------------------------------ |
Добавим также парочку глобальных переменных
1 2 3 4 5 |
void ets_isr_unmask(unsigned intr); //------------------------------------------------ LOCAL uint8 m_nLastSDA; LOCAL uint8 m_nLastSCL; //------------------------------------------------ |
Добавим удобную функцию для управления уровнями на ножках SDA и SCL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
LOCAL uint8 m_nLastSCL; //------------------------------------------------ LOCAL void ICACHE_FLASH_ATTR i2c_mas_setDC(uint8 SDA, uint8 SCL) { SDA &= 0x01; SCL &= 0x01; m_nLastSDA = SDA; m_nLastSCL = SCL; if ((0 == SDA) && (0 == SCL)) { I2C_MASTER_SDA_LOW_SCL_LOW(); } else if ((0 == SDA) && (1 == SCL)) { I2C_MASTER_SDA_LOW_SCL_HIGH(); } else if ((1 == SDA) && (0 == SCL)) { I2C_MASTER_SDA_HIGH_SCL_LOW(); } else { I2C_MASTER_SDA_HIGH_SCL_HIGH(); } } //------------------------------------------------ |
Ниже добавим функцию, узнающую уровень на ножке SDA
1 2 3 4 5 6 7 8 9 |
//------------------------------------------------ LOCAL uint8 ICACHE_FLASH_ATTR i2c_mas_getDC(void) { uint8 sda_out; sda_out = GPIO_INPUT_GET(GPIO_ID_PIN(I2C_MASTER_SDA_GPIO)); return sda_out; } //------------------------------------------------ |
В файле i2c_user.h добавим макрос задержки
1 2 3 4 5 |
#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) //------------------------------------------------ #define i2c_mas_wait os_delay_us //------------------------------------------------ |
Вернёмся в файл i2c_user.c и после функции i2c_mas_getDC добавим функцию, обеспечивающую условие START
1 2 3 4 5 6 7 8 9 10 11 12 |
//------------------------------------------------ void ICACHE_FLASH_ATTR i2c_mas_start(void) { i2c_mas_setDC(1, m_nLastSCL); i2c_mas_wait(5); i2c_mas_setDC(1, 1); i2c_mas_wait(5); // sda 1, scl 1 i2c_mas_setDC(0, 1); i2c_mas_wait(5); // sda 0, scl 1 } //------------------------------------------------ |
Ниже добавим функцию, которая обеспечит условие STOP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//------------------------------------------------ void ICACHE_FLASH_ATTR i2c_mas_stop(void) { i2c_mas_wait(5); i2c_mas_setDC(0, m_nLastSCL); i2c_mas_wait(5); // sda 0 i2c_mas_setDC(0, 1); i2c_mas_wait(5); // sda 0, scl 1 i2c_mas_setDC(1, 1); i2c_mas_wait(5); // sda 1, scl 1 } //------------------------------------------------ |
Ниже добавим функцию, которая обеспечит подтверждение (Ack)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//------------------------------------------------ void ICACHE_FLASH_ATTR i2c_mas_setAck(uint8 level) { i2c_mas_setDC(m_nLastSDA, 0); i2c_mas_wait(5); i2c_mas_setDC(level, 0); i2c_mas_wait(5); // sda level, scl 0 i2c_mas_setDC(level, 1); i2c_mas_wait(8); // sda level, scl 1 i2c_mas_setDC(level, 0); i2c_mas_wait(5); // sda level, scl 0 i2c_mas_setDC(1, 0); i2c_mas_wait(5); } //------------------------------------------------ |
Ниже добавим функцию, которая узнает, передал ли подтверждение SLAVE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//------------------------------------------------ uint8 ICACHE_FLASH_ATTR i2c_mas_getAck(void) { uint8 retVal; i2c_mas_setDC(m_nLastSDA, 0); i2c_mas_wait(5); i2c_mas_setDC(1, 0); i2c_mas_wait(5); i2c_mas_setDC(1, 1); i2c_mas_wait(5); retVal = i2c_mas_getDC(); i2c_mas_wait(5); i2c_mas_setDC(1, 0); i2c_mas_wait(5); return retVal; } //------------------------------------------------ |
Далее добавим функцию отправки подтверждения
1 2 3 4 5 6 7 |
//------------------------------------------------ void ICACHE_FLASH_ATTR i2c_mas_send_ack(void) { i2c_mas_setAck(0x0); } //------------------------------------------------ |
Добавим функцию проверки подтверждения
1 2 3 4 5 6 7 8 9 10 11 |
//------------------------------------------------ bool ICACHE_FLASH_ATTR i2c_mas_checkAck(void) { if(i2c_mas_getAck()){ return FALSE; }else{ return TRUE; } } //------------------------------------------------ |
Далее добавим функцию отправки байта
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void ICACHE_FLASH_ATTR i2c_mas_writeByte(uint8 wrdata) { uint8 dat; sint8 i; i2c_mas_wait(5); i2c_mas_setDC(m_nLastSDA, 0); i2c_mas_wait(5); for (i = 7; i >= 0; i--) { dat = wrdata >> i; i2c_mas_setDC(dat, 0); i2c_mas_wait(5); i2c_mas_setDC(dat, 1); i2c_mas_wait(5); if (i == 0) { i2c_mas_wait(3); } i2c_mas_setDC(dat, 0); i2c_mas_wait(5); } } //------------------------------------------------ |
Далее добавим функцию инициализации I2C, в которой произведём серию импульсов на SCL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//------------------------------------------------ void ICACHE_FLASH_ATTR i2c_mas_init(void) { uint8 i; i2c_mas_setDC(1, 0); i2c_mas_wait(5); // when SCL = 0, toggle SDA to clear up i2c_mas_setDC(0, 0) ; i2c_mas_wait(5); i2c_mas_setDC(1, 0) ; i2c_mas_wait(5); // set data_cnt to max value for (i = 0; i < 28; i++) { i2c_mas_setDC(1, 0); i2c_mas_wait(5); // sda 1, scl 0 i2c_mas_setDC(1, 1); i2c_mas_wait(5); // sda 1, scl 1 } // reset all i2c_mas_stop(); return; } //------------------------------------------------ |
В функции отправки байта по адресу применим в вызовах функций новые имена
void I2C_SendByteByADDR(uint8_t c,uint8_t addr)
{
uint16_t i;
uint8_t ack;
i2c_mas_start();
//Transmit Address SLAVE
i2c_mas_writeByte(addr);
ack = i2c_mas_checkAck();
if(!ack)
{
os_printf("ADDR not ack\r\n");
}
//Send data
i2c_mas_writeByte(c);
i2c_mas_send_ack();
i2c_mas_stop();
}
Теперь осталось написать тело функции инициализации i2c_mas_gpio_init.
Отключим прерывания GPIO
1 2 3 |
void ICACHE_FLASH_ATTR i2c_mas_gpio_init(void) { ETS_GPIO_INTR_DISABLE(); |
Инициализируем ножки GPIO
1 2 3 4 5 6 7 8 |
ETS_GPIO_INTR_DISABLE(); PIN_FUNC_SELECT(I2C_MASTER_SDA_MUX, I2C_MASTER_SDA_FUNC); PIN_FUNC_SELECT(I2C_MASTER_SCL_MUX, I2C_MASTER_SCL_FUNC); GPIO_REG_WRITE(GPIO_PIN_ADDR(GPIO_ID_PIN(I2C_MASTER_SDA_GPIO)), GPIO_REG_READ(GPIO_PIN_ADDR(GPIO_ID_PIN(I2C_MASTER_SDA_GPIO))) | GPIO_PIN_PAD_DRIVER_SET(GPIO_PAD_DRIVER_ENABLE)); //open drain; GPIO_REG_WRITE(GPIO_ENABLE_ADDRESS, GPIO_REG_READ(GPIO_ENABLE_ADDRESS) | (1 << I2C_MASTER_SDA_GPIO)); GPIO_REG_WRITE(GPIO_PIN_ADDR(GPIO_ID_PIN(I2C_MASTER_SCL_GPIO)), GPIO_REG_READ(GPIO_PIN_ADDR(GPIO_ID_PIN(I2C_MASTER_SCL_GPIO))) | GPIO_PIN_PAD_DRIVER_SET(GPIO_PAD_DRIVER_ENABLE)); //open drain; GPIO_REG_WRITE(GPIO_ENABLE_ADDRESS, GPIO_REG_READ(GPIO_ENABLE_ADDRESS) | (1 << I2C_MASTER_SCL_GPIO)); |
Установим высокий уровень на ножке SCL
1 2 3 |
GPIO_REG_WRITE(GPIO_ENABLE_ADDRESS, GPIO_REG_READ(GPIO_ENABLE_ADDRESS) | (1 << I2C_MASTER_SCL_GPIO)); I2C_MASTER_SDA_HIGH_SCL_HIGH(); |
Разрешим прерывания
1 2 3 |
I2C_MASTER_SDA_HIGH_SCL_HIGH(); ETS_GPIO_INTR_ENABLE(); |
Вызовем функцию инициализации I2C
1 2 3 |
ETS_GPIO_INTR_ENABLE(); i2c_mas_init(); |
Если мы сейчас соберём код и прошьём контроллер, то у нас всё будет работать
Теперь давайте немного поработаем с файлом сценария Makefile, чтобы впредь мы могли добавлять и удалять модули без его переписывания в нашем проекте.
Для этого мы будем использовать функции и автоматические переменные. Про них очень подробно рассказано в уроке 34 по программированию на языке C.
Перед тем как вносить изменения в Makefile, желательно выполнить очистку проекта с помощью команды Clean.
Также необходимо удалить, если у кого остался в каталоге build, ассемблерный файл main.s.
Объявим переменную каталога с исходниками
1 2 |
ESPTOOL := esptool SRC_DIR = src/ |
Также объявим и проинициализируем переменные с путями к файлам с исходными текстами и к объектным файлам
1 2 3 4 |
SRC_DIR = src/ src_files := $(wildcard $(SRC_DIR)*) obj_files := $(addprefix build/, $(addsuffix .o, $(basename $(notdir $(src_files))))) |
Добавим функцию сборки модуля
1 2 3 4 5 6 |
obj_files := $(addprefix build/, $(addsuffix .o, $(basename $(notdir $(src_files))))) define build-obj @echo "CC" $@ @$(CC) -Iinc -I$(SDK_INC) -I$(SDK_INC)/json -I$(SDK_DRIVER_INC) $(CC_FLAGS) $< -o $@ endef |
Изменим цель сборки общего объектного архива, применим там теперь наши переменные, а также автоматические переменные
1 2 3 |
build/app_app.a: $(obj_files) @echo "AR build/app_app.a" @$(AR) cru build/app_app.a $^ |
Цели с именами src/main.o, src/lcd.o и rc/i2c_user.o удалим, а вместо них добавим общую цель
1 2 |
build/%.o: src/%.c inc/%.h inc/user_config.h $(call build-obj) |
В цели очистки проекта применим шаблон для объектных файлов
1 2 |
clean: @rm -v build/*.o build/app_app.a build/app.out build/app.out-0x00000.bin build/app.out-0x10000.bin |
После этого проект у нас должен будет благополучно собраться и объектные файлы теперь у нас окажутся в каталоге build, думаю так удобнее
Попробуем очистить проект. Файлы должны будут удалиться. Файл main.s, оставшийся там с незапамятных времён, удалим самостоятельно
Теперь можно ещё раз собрать проект, прошить контроллер, после чего дисплей наш должен по-прежнему отлично работать.
Таким образом, на данном уроке мы научились переопределять ножки GPIO шины I2C, а также сконструировали универсальный файл сценария Makefile, который в будущем позволит нам без его изменения добавлять и удалять модули наших проектов.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP8266 Следующий урок
Модуль ESP NodeMCU можно купить здесь: Модуль ESP NodeMCU
Различные модули ЕSP8266 можно приобрести здесь Модули ЕSP8266
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Добрый день! А вот вопрос, понятно что все функции из SDK, но вот почему нельзя тупо в SDK переназначить ножки? (Пробовал, не работает)
Тупо не надо, надо с чувством, с толком, с расстановкой…