Введение в ESP-OPEN-SDK

разделы: Интернет вещей , дата: 14 марта 2019г.

Как я уже говорил, ESP8266 можно программировать двумя способами: либо через Arduino IDE, либо через тулчейн esp-open-sdk. Первый вариант я уже рассматривал на примере разработки температурного логера, в этот раз я хочу рассказать о работе с esp-open-sdk.

Тулчейн позволяет программировать на SDK функциях, которые поставляются в закрытых скомпилированных библиотеках называемых SDK. Имеется две версии SDK: RTOS SDK и NONOS SDK. Я буду рассматривать вариант без RTOS, при необходимости, "прикрутить" простенький диспечер задач будет несложно.

Non-OS SDK - это библиотека предоставляющая программный интерфейс приложения(API) для ESP8266 и включающая стек функций для приема и передачи через WiFi-соединение, доступа к аппаратным ресурсам и базовые функции контроля и управления модулем. Данное API позволяет программировать на более высоком уровне не вдаваясь в особенности архитектуры ESP8266.

SDK может быть интересен для опытных embedded - программистов, которых, возможно, тяготит использование Arduino IDE и Wiring, и которые не боятся остаться один на один с Си. Взамен вы получите: избавление от прослойки Arduino/Wiring, возможность использования вашей любимой системы управления проектом, возможность использования стороннего IDE для написания кода, а также возможность отладки через JTAG. Что вы теряете? Возможность использования Arduino библиотек.

Однако, если вы используете фреймворк Arduino, то для вас не доложно быть секретом, что сам он написан на функциях SDK. Т.о. изучение SDK существенно расширит ваши возможности при написании своих библиотек, да и собственно само программирование ESP8266, т.к. функции SDK доступны из Arduino.

Далее речь пойдёт исключительно о тулчейне "esp-open-sdk". В качестве целевой платы я буду использовать плату NodeMCU ESP8266, т.к. там есть автозагрузка прошивки, но в принципе может быть использована любая другая плата на модуле ESP12E/ESP12F.

    Список используемой документации:
  1. ESP8266EX Resources | Espressif Systems Страница с доступными ресурсами по ESP8266 на сайте производителя.
  2. ESP8266 Non-OS SDK API Reference ESP8266 Non-OS SDK API Reference версия 2.2
  3. ESP8266 SDK Getting Started Guide - руководство по работе со официальным SDK
  4. ESP8266 Technical Reference
  5. Xtensa Instruction Set Architecture (ISA)
  6. Статья на хабре: "Reverse Engineering ESP8266 — часть 1"
  7. Статья на хабре: "Reverse Engineering ESP8266 — часть 2"
  8. Статья Михаила Григорьева на хабре: Работа с ESP8266: Собираем компилятор и пишем первую прошивку

Содержание:

  1. Сборка esp-open-sdk
  2. Создание базового проекта
  3. Описание API для работы с GPIO
  4. Краткое описание ассемблера Xtensa
  5. Подключение JTAG отладчика на FT232H чипе
  6. Работа с GPIO16 через NonOS-SDK, подключение библиотеки "Driver_Lib"
  7. Вывод через UART

Посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/esp8266_sdk_examples

1) Сборка esp-open-sdk

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

Страницей проекта esp-open-sdk является https://github.com/pfalcon/esp-open-sdk, и там же имеется краткая инструкция по сборке тулчейна. Действуя в соответствии с этой инструкцией, перевым делом нужно будет скачать тулчейн:

$ git clone --recursive https://github.com/pfalcon/esp-open-sdk.git

После чего следует зайти в каталог esp-open-sdk и запустить сборку командой:

$ make STANDALONE=y

Сборка полностью автоматическая и занимает примерно минут 50-60. Первые полчаса происходит скачивание исходников в директорию crosstool-NG/.build/tarballs:

$ tree crosstool-NG/.build/tarballs/
crosstool-NG/.build/tarballs/
├── binutils-2.25.1.tar.bz2
├── cloog-0.18.4.tar.gz
├── expat-2.1.0.tar.gz
├── gcc-4.8.5.tar.bz2
├── gdb-7.10.tar.xz
├── gmp-6.0.0a.tar.xz
├── isl-0.14.tar.xz
├── mpc-1.0.3.tar.gz
├── mpfr-3.1.3.tar.xz
├── ncurses-6.0.tar.gz
├── newlib-2.0.0.tar.gz
└── xtensa_lx106.tar -> /home/flanker/mydev/esp8266/esp-open-sdk/crosstool-NG/overlays/xtensa_lx106.tar

0 directories, 12 files

После завершения скачивания, будет распаковка тарболов, наложение патчей и последующая сборка, которая в зависимости от мощности вашего компьютера может занять от 10-15 минут и больше. После завершения сборки, появится сообщение, что для использования тулчейна, следует добавить путь к нему в переменной окружения PATH:

Xtensa toolchain is built, to use it:

export PATH=/home/flanker/mydev/esp8266/esp-open-sdk/xtensa-lx106-elf/bin:$PATH

К сожалению, не всегда сборка проходит успешно. Тулчейн нормально собирался у меня в Slackware 14.2 с gcc версии 5.3.0, так же успешно проходила сборка в виртуалке Ubuntu 14.4, которая идёт с официальным тулченом. А вот в Slackware-current с gcc-8.2.0 сборка валится на этапе сборки GDB. В любом случае, если вам не удаётся собрать тулчейн, то следует обратиться к инструкции поэтапной сборки Макса Филипова.

После завершения сборки, также автоматически скачивается и устанавливается SDK версии: ESP8266_NONOS_SDK-2.1.0-18-g61248df.

Чтобы проверить работоспособность тулчейна, следует перейти в директорию examples/blinky, где будет исходник проверочной мигалки:

#include "ets_sys.h"
#include "osapi.h"
#include "gpio.h"
#include "os_type.h"

// ESP-12 modules have LED on GPIO2. Change to another GPIO
// for other boards.
static const int pin = 2;
static volatile os_timer_t some_timer;

void some_timerfunc(void *arg)
{
  //Do blinky stuff
  if (GPIO_REG_READ(GPIO_OUT_ADDRESS) & (1 << pin))
  {
    // set gpio low
    gpio_output_set(0, (1 << pin), 0, 0);
  }
  else
  {
    // set gpio high
    gpio_output_set((1 << pin), 0, 0, 0);
  }
}

void ICACHE_FLASH_ATTR user_init()
{
  // init gpio subsytem
  gpio_init();

  // configure UART TXD to be GPIO1, set as output
  PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_GPIO1);
  gpio_output_set(0, 0, (1 << pin), 0);

  // setup timer (500ms, repeating)
  os_timer_setfn(&some_timer, (os_timer_func_t *)some_timerfunc, NULL);
  os_timer_arm(&some_timer, 500, 1);
}

Командой make собираем прошивку:

$ make
xtensa-lx106-elf-gcc -I. -mlongcalls   -c -o blinky.o blinky.c
blinky.c: In function 'user_init':
blinky.c:36:3: warning: passing argument 1 of 'ets_timer_setfn' discards 'volatile' qualifier from pointer target type [enabled by default]
   os_timer_setfn(&some_timer, (os_timer_func_t *)some_timerfunc, NULL);
   ^
In file included from blinky.c:2:0:
/home/flanker/mydev/esp8266/esp-open-sdk/xtensa-lx106-elf/xtensa-lx106-elf/sysroot/usr/include/osapi.h:67:6: note: expected 'struct ETSTimer *' but argument is of type 'volatile struct ETSTimer *'
 void ets_timer_setfn(os_timer_t *ptimer, os_timer_func_t *pfunction, void *parg);
      ^
blinky.c:37:3: warning: passing argument 1 of 'ets_timer_arm_new' discards 'volatile' qualifier from pointer target type [enabled by default]
   os_timer_arm(&some_timer, 500, 1);
   ^
In file included from blinky.c:2:0:
/home/flanker/mydev/esp8266/esp-open-sdk/xtensa-lx106-elf/xtensa-lx106-elf/sysroot/usr/include/osapi.h:65:6: note: expected 'struct ETSTimer *' but argument is of type 'volatile struct ETSTimer *'
 void ets_timer_arm_new(os_timer_t *ptimer, uint32_t time, bool repeat_flag, bool ms_flag);
      ^
xtensa-lx106-elf-gcc -Teagle.app.v6.ld  blinky.o  -nostdlib -Wl,--start-group -lmain -lnet80211 -lwpa -llwip -lpp -lphy -lc -Wl,--end-group -lgcc -o blinky
esptool.py elf2image blinky
esptool.py v1.2

Далее имеется неприятный подводный камень. Прошивка будет работать, только если в ESP12 предварительно была записана прошивка AT-интерпретатора (проверьте!).

Подключаем плату NodeMCU к компьютеру и предварительно очищаем флешку командой:

$ esptool.py -p /dev/ttyUSB0 erase_flash

Далее переходим в директорию SDK "sdk/bin" и прошиваем модуль ESP12 AT-интерпретатором:

$ esptool.py -p /dev/ttyUSB0 write_flash -fm dio -ff 40m -fs 32m 0x00000 ./boot_v1.7.bin  0x01000 ./at/512+512/user1.1024.new.2.bin   0x3fc000 ./esp_init_data_default.bin 0x7e000 ./blank.bin 0x3fe000 ./blank.bin

Теперь возвращаемся в директорию с примером blinky, и загружаем прошивку командой "make flash":

$ make flash
esptool.py write_flash 0 blinky-0x00000.bin 0x10000 blinky-0x10000.bin
esptool.py v1.2
Connecting...
Auto-detected Flash size: 32m
Running Cesanta flasher stub...
Flash params set to 0x0040
Writing 36864 @ 0x0... 36864 (100 %)
Wrote 36864 bytes at 0x0 in 3.2 seconds (91.7 kbit/s)...
Writing 200704 @ 0x10000... 200704 (100 %)
Wrote 200704 bytes at 0x10000 in 17.5 seconds (91.9 kbit/s)...
Leaving...

Если всё было сделано правильно, то светодиод на GPIO_2 начнёт мигать с полупериодом в половину секунды.

Для дальнейших прошивок, загружать прошивку AT-интерпретатора больше не нужно.

2) Создание базового проекта

Сейчас мы напишем свою упрощённую версию тестовой мигалки и свой Makefile для сборки проекта.

Для начала создаём структуру каталогов:

mkdir -p  00_blink/{asm,inc,src}

Переходим в каталог 00_blink и создаём файл main.c с исходным текстом мигалки:

#include "ets_sys.h"
#include "gpio.h"

#define LED 2


void  dummy_loop(uint32_t count ){
    while(--count);
}

void ICACHE_FLASH_ATTR user_init()
{
    // init gpio subsytem
    gpio_init();

    // configure UART TXD to be GPIO1, set as output
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_GPIO1);
    gpio_output_set(0, 0, (1 << LED), 0);
    for(;;){
        dummy_loop(600000);
        gpio_output_set(0, (1 << LED), 0, 0);
        dummy_loop(600000);
        gpio_output_set((1 << LED), 0, 0, 0);
    }
}

Здесь функция user_init() - это функция которая по умолчанию вызывается первой при старте прошивки. Атрибут ICACHE_FLASH_ATTR указывает, что функция должна выполняться из флеш-памяти. При атрибуте IRAM_ATTR, функция будет выполняться из оперативной памяти. Все обработчики прерываний должны иметь атрибут IARM_ATTR.

Далее добавляем Makefile:

SDK=/here_should_be_path_to_your_ESP8266_SDK/ESP8266_NONOS_SDK-2.1.0-18-g61248df
CC=xtensa-lx106-elf-gcc
SIZE=xtensa-lx106-elf-size
ESPTOOL=esptool.py
INC  = -I./inc -mlongcalls
CFLAGS= -std=c99 -g -O0 -Wall  $(INC)
LDFLAGS= -L$(SDK)/lib  -T$(SDK)/ld/eagle.app.v6.ld
LDLIBS=-nostdlib -Wl,--start-group -lmain -lnet80211 -lwpa -llwip -lpp -lphy -lc -Wl,--end-group -lgcc
OBJ=main.o
TARGET=blink
.PHONY: all clean

%.o:	%.c
	$(CC) $(CFLAGS) -c -o $@ $<
all:	$(OBJ)
	$(CC) $(LDFLAGS) -g  -o $(TARGET).elf  $(OBJ) $(LDLIBS)
	$(SIZE)  $(TARGET).elf
	$(ESPTOOL) elf2image $(TARGET).elf
install:
	$(ESPTOOL) write_flash 0 $(TARGET).elf-0x00000.bin 0x10000 $(TARGET).elf-0x10000.bin
clean:
	@rm -v $(TARGET).elf $(OBJ) $(TARGET).elf-0x?0000.bin

В Makefile я установил стандарт С99, и выставил флаг "-g" для последующей отладки прошивки в gdb. В качестве скрипта компоновщика используется eagle.app.v6.ld.

Компилируем:

$ make all
xtensa-lx106-elf-gcc -std=c99 -g -O0 -Wall  -I./inc -mlongcalls -c -o main.o main.c
xtensa-lx106-elf-gcc -L/home/flanker/mydev/esp8266/esp-open-sdk/ESP8266_NONOS_SDK-2.1.0-18-g61248df/lib  -T/home/flanker/mydev/esp8266/esp-open-sdk/ESP8266_NONOS_SDK-2.1.0-18-g61248df/ld/eagle.app.v6.ld -g  -o blink.elf  main.o -nostdlib -Wl,--start-group -lmain -lnet80211 -lwpa -llwip -lpp -lphy -lc -Wl,--end-group -lgcc
xtensa-lx106-elf-size  blink.elf
   text    data     bss     dec     hex filename
 226116     898   25224  252238   3d94e blink.elf
esptool.py elf2image blink.elf
esptool.py v1.2

Структура файлов теперь выглядит так:

$ tree .
.
├── Makefile
├── asm
├── blink.elf
├── blink.elf-0x00000.bin
├── blink.elf-0x10000.bin
├── inc
├── main.c
├── main.o
└── src

Прошиваем:

$ make install
esptool.py write_flash 0 blink.elf-0x00000.bin 0x10000 blink.elf-0x10000.bin
esptool.py v1.2
Connecting...
Auto-detected Flash size: 32m
Running Cesanta flasher stub...
Flash params set to 0x0040
Writing 28672 @ 0x0... 28672 (100 %)
Wrote 28672 bytes at 0x0 in 2.5 seconds (91.9 kbit/s)...
Writing 200704 @ 0x10000... 200704 (100 %)
Wrote 200704 bytes at 0x10000 in 17.5 seconds (91.9 kbit/s)...
Leaving...

Но не все так просто. После пошивки, немого помигав, работа программы остановится. Если перезагрузить esp8266 нажатием на клавишу RST, то мигать будет, но цикл будет иногда сбиваться. ESP8266 постоянно перезагружается и это не дело. С помощью отладчика я выяснил, что происходит сброс по прерыванию Watchdog'а.

Согласно документации на SDK, если какая-либо функция выполняется слишком долго, скажем более 500 мс, то она должна вызывать функцию system_soft_wdt_feed() для сброса сторожевого таймера. Отключение watchdog'а производится вызовом функции system_soft_wdt_stop(), но использовать ее очень сильно не рекомендуют.

В Arduino имеется метод EspClass::wdtDisable(), и если посмотреть его реализацию там есть любопытный комментарий:

void EspClass::wdtDisable(void)
{
    /// Please don't stop software watchdog too long (less than 6 seconds),
    /// otherwise it will trigger hardware watchdog reset.
    system_soft_wdt_stop();
}

Т.е. получается, что совсем отключить watchdog штатными методами не удастся.

С учётом всего вышесказанного, добавляем в главный цикл вызов функции system_soft_wdt_feed():

#include "ets_sys.h"
#include "user_interface.h"
#include "gpio.h"

#define LED 2


void  dummy_loop(uint32_t count ){
    while(--count);
}

void ICACHE_FLASH_ATTR user_init()
{
    // init gpio subsytem
    gpio_init();

    // configure UART TXD to be GPIO1, set as output
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_GPIO1);
    gpio_output_set(0, 0, (1 << LED), 0);
    for(;;){
        dummy_loop(600000);
        system_soft_wdt_feed();
        gpio_output_set(0, (1 << LED), 0, 0);
        dummy_loop(600000);
        system_soft_wdt_feed();
        gpio_output_set((1 << LED), 0, 0, 0);
    }
}

В каталог inс необходимо будет добавить пока пустой заголовочный файл user_config.h. В этом файле предполагается хранение настроек WiFi: ssid, пароль и т.д. У нас он будет пока пустым.

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

В грубом приближении данная программа может служить тестом производительности архитектуры. У esp8266 тактовая частота составляет 80 МГц, у stm32f103c8t6 она равняется 72 МГц. Прошив их одной и той же программой (на уровне Си естественно), и положив обе платы рядом, я бы сказал, что светодиод на esp8266 мигает несколько быстрее.

3) Описание API для работы с GPIO

Наряду с WiFi модулем, GPIO являются для нас самой важной подсистемой в ESP8266. При работе с GPIO нас в первую очередь будет интересовать режим bit-banging на котором строятся программные варианты различных протоколов. С быстродействием у ESP8266 проблем нет, так же как и со свободным местом на флешке, зато есть проблемы со свободными выводами и аппаратными протоколами. На первое время, bit-banging будет у нас вместо лома для ответа на любые технические сложности. Поэтому изучение ESP8266 на мой взгляд логично будет начать с именно с подсистемы GPIO.

Функция gpio_init() которую мы использовали, не описана в ESP8266 Non-OS SDK API Reference, зато её объявление есть в заголовочном gpio.h со следующим комментарием:

/*
 * Initialize GPIO.  This includes reading the GPIO Configuration DataSet
 * to initialize "output enables" and pin configurations for each gpio pin.
 * Must be called once during startup.
 */
void gpio_init(void);

Можно предположить, что данная функция сбрасывает GPIO порты в какое-то начальное состояние.

Далее нас будут интересовать следующие макросы:

PIN_PULLUP_DIS(PIN_NAME) Disable pin pull-up /Отключить подтягивающий резистор/. Пример:
//Использовать MTDI пин в качестве GPIO12.
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12);
PIN_PULLUP_EN(PIN_NAME) Enable pin pull up /Включить подтягивающий резистор/.
PIN_FUNC_SELECT(PIN_NAME, FUNC) Select pin function /Выбор функции/.

Используемый в программе макрос PIN_FUNC_SELECT определён в заголовочном файле eagle_soc.h:

#define PIN_FUNC_SELECT(PIN_NAME, FUNC)  do { \
    WRITE_PERI_REG(PIN_NAME,   \
      (READ_PERI_REG(PIN_NAME) \
      &  (~(PERIPHS_IO_MUX_FUNC<<PERIPHS_IO_MUX_FUNC_S)))  \
      |( (((FUNC&BIT2)<<2)|(FUNC&0x3))<<PERIPHS_IO_MUX_FUNC_S) );  \
    } while (0)

Варианты значений FUNC для различных GPIO перечислены тут же:

//PIN Mux reg {{
#define PERIPHS_IO_MUX_FUNC             0x13
#define PERIPHS_IO_MUX_FUNC_S           4
#define PERIPHS_IO_MUX_PULLUP           BIT7
#define PERIPHS_IO_MUX_PULLUP2          BIT6
#define PERIPHS_IO_MUX_SLEEP_PULLUP     BIT3
#define PERIPHS_IO_MUX_SLEEP_PULLUP2    BIT2
#define PERIPHS_IO_MUX_SLEEP_OE         BIT1
#define PERIPHS_IO_MUX_OE               BIT0

#define PERIPHS_IO_MUX_CONF_U           (PERIPHS_IO_MUX + 0x00)
#define SPI0_CLK_EQU_SYS_CLK            BIT8
#define SPI1_CLK_EQU_SYS_CLK            BIT9
#define PERIPHS_IO_MUX_MTDI_U           (PERIPHS_IO_MUX + 0x04)
#define FUNC_GPIO12                     3
#define PERIPHS_IO_MUX_MTCK_U           (PERIPHS_IO_MUX + 0x08)
#define FUNC_GPIO13                     3
#define PERIPHS_IO_MUX_MTMS_U           (PERIPHS_IO_MUX + 0x0C)
#define FUNC_GPIO14                     3
#define PERIPHS_IO_MUX_MTDO_U           (PERIPHS_IO_MUX + 0x10)
#define FUNC_GPIO15                     3
#define FUNC_U0RTS                      4
#define PERIPHS_IO_MUX_U0RXD_U          (PERIPHS_IO_MUX + 0x14)
#define FUNC_GPIO3                      3
#define PERIPHS_IO_MUX_U0TXD_U          (PERIPHS_IO_MUX + 0x18)
#define FUNC_U0TXD                      0
#define FUNC_GPIO1                      3
#define PERIPHS_IO_MUX_SD_CLK_U         (PERIPHS_IO_MUX + 0x1c)
#define FUNC_SDCLK                      0
#define FUNC_SPICLK                     1
#define PERIPHS_IO_MUX_SD_DATA0_U       (PERIPHS_IO_MUX + 0x20)
#define FUNC_SDDATA0                    0
#define FUNC_SPIQ                       1
#define FUNC_U1TXD                      4
#define PERIPHS_IO_MUX_SD_DATA1_U       (PERIPHS_IO_MUX + 0x24)
#define FUNC_SDDATA1                    0
#define FUNC_SPID                       1
#define FUNC_U1RXD                      4
#define FUNC_SDDATA1_U1RXD              7
#define PERIPHS_IO_MUX_SD_DATA2_U       (PERIPHS_IO_MUX + 0x28)
#define FUNC_SDDATA2                    0
#define FUNC_SPIHD                      1
#define FUNC_GPIO9                      3
#define PERIPHS_IO_MUX_SD_DATA3_U       (PERIPHS_IO_MUX + 0x2c)
#define FUNC_SDDATA3                    0
#define FUNC_SPIWP                      1
#define FUNC_GPIO10                     3
#define PERIPHS_IO_MUX_SD_CMD_U         (PERIPHS_IO_MUX + 0x30)
#define FUNC_SDCMD                      0
#define FUNC_SPICS0                     1
#define PERIPHS_IO_MUX_GPIO0_U          (PERIPHS_IO_MUX + 0x34)
#define FUNC_GPIO0                      0
#define PERIPHS_IO_MUX_GPIO2_U          (PERIPHS_IO_MUX + 0x38)
#define FUNC_GPIO2                      0
#define FUNC_U1TXD_BK                   2
#define FUNC_U0TXD_BK                   4
#define PERIPHS_IO_MUX_GPIO4_U          (PERIPHS_IO_MUX + 0x3C)
#define FUNC_GPIO4                      0
#define PERIPHS_IO_MUX_GPIO5_U          (PERIPHS_IO_MUX + 0x40)
#define FUNC_GPIO5                      0

Честно говоря, мне не понятно, почему в строке:

PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_GPIO1);

производятся манипуляции с GPIO1, в то время как светодиод находится на GPIO2 ?!.

Функция gpio_output_set
Назначение функции Установка режима GPIO
Прототип void gpio_output_set(
uint32 set_mask,
uint32 clear_mask,
uint32 enable_mask,
uint32 disable_mask)
Параметры uint32 set_mask: установка высокого логического значения; 1: высокое значение; 0: устанавливать высокое значение не требуется;
uint32 clear_mask: установка низкого логического значения; 1: установить низкое значение; 0: устанавливать низкое значение не требуется;
uint32 enable_mask: включить заданный GPIO на выход
uint32 disable_mask: включить заданный GPIO на вход.
Возвращаемое значение отсутствует
Примеры gpio_output_set(BIT12, 0, BIT12, 0): Установить на GPIO12 высокий уровень.
gpio_output_set(0, BIT12, BIT12, 0): Установить на GPIO12 низкий уровень.
gpio_output_set(BIT12, BIT13, BIT12|BIT13, 0): Установить на GPIO12 высокий уровен, а на GPIO13 низкий уровень.
gpio_output_set(0, 0, 0, BIT12): Установить на GPIO12 режим цифрового входа.

Макросы для управления режимом GPIO:

GPIO input and output macros
GPIO_OUTPUT_SET(gpio_no, bit_value) Переводит GPIO в режим выхода.
GPIO_DIS_OUTPUT(gpio_no) Переводит GPIO в режим входа.
GPIO_INPUT_GET(gpio_no) Возвращает логический уровень GPIO находящегося в режиме входа.

Макросы для управления внешними прерываниями:

GPIO interrupt
ETS_GPIO_INTR_ATTACH(func, arg) Установить обработчик прерывания
ETS_GPIO_INTR_DISABLE() Отключить внешнее прерывание.
ETS_GPIO_INTR_ENABLE() Включить внешнее прерывание.

Функция для установки режима внешнего прерывания:

Функция gpio_pin_intr_state_set
Назначение функции установка условия срабатывания внешнего прерывания: по нарастающему фронту, по падающему и т.д.
Прототип void gpio_pin_intr_state_set(
uint32 i,
GPIO_INT_TYPE intr_state
)
Параметры uint32 i: ID пина. Если вы хотите указать к примеру GPIO14, то пожалуйста тспользуйте: GPIO_ID_PIN(14);
GPIO_INT_TYPE intr_state: условие срабатывания внешнего прерывания:
typedef enum {
GPIO_PIN_INTR_DISABLE = 0,
GPIO_PIN_INTR_POSEDGE = 1,
GPIO_PIN_INTR_NEGEDGE = 2,
GPIO_PIN_INTR_ANYEDGE = 3,
GPIO_PIN_INTR_LOLEVEL = 4,
GPIO_PIN_INTR_HILEVEL = 5
} GPIO_INT_TYPE;
Возвращаемое значение отсутствует

Также могут быть интересны функции настойки пробуждения из спящего режима с помощью внешнего прерывания:

void gpio_pin_wakeup_enable(uint32 i, GPIO_INT_TYPE intr_state);

void gpio_pin_wakeup_disable();

Но должен обратить внимание, что они могут работать только в режиме энергосбережения LIGT_SLEEP. Из deep_sleep esp8266 может разбудить только таймер или кнопка Reset.

4) Краткое описание ассемблера Xtensa

На сайте я уже рассматривал ассемблер следующих архитектур: AVR, MSP430, STM8, ARM. Сейчас я предлагаю вкратце познакомиться с ассемблером Xtensa, а точнее, с его вариантом для чипа ESP8266. Как обычно, я ни в коей мере не предлагаю писать прошивки целиком на ассемблере, но считаю, что знание ассемблера целевой архитектуры как минимум полезно. Так же как и в предыдущих случаях, я предлагаю начать знакомство с ассемблером Xtensa через дизассемблирование прошивки.

Прошивка складывается из собственного кода и кода закрытых библиотек SDK, которые линкуются вместе с нашим объектным модулем main.o. Чтобы не выискивать свой код по всей прошивке, предлагаю просто дизассемблировать объектный модуль main.o. Для этого в консоли выполняем команду: "xtensa-lx106-elf-objdump -S main.o", в результате чего получим следующий листинг:

main.o:     file format elf32-xtensa-le


Disassembly of section .literal:

00000000 <.literal>:
   0:   0818            l32i.n  a1, a8, 0
   2:   c06000          sub     a6, a0, a0
   5:   000927          bnone   a9, a2, 9 <.literal+0x9>
        ...

Disassembly of section .text:

00000000 <dummy_loop>:
   0:   e0c112          addi    a1, a1, -32
   3:   71f9            s32i.n  a15, a1, 28
   5:   01fd            mov.n   a15, a1
   7:   0f29            s32i.n  a2, a15, 0
   9:   0f28            l32i.n  a2, a15, 0
   b:   220b            addi.n  a2, a2, -1
   d:   0f29            s32i.n  a2, a15, 0
   f:   0f28            l32i.n  a2, a15, 0
  11:   ff4256          bnez    a2, 9 <dummy_loop+0x9>
  14:   0f1d            mov.n   a1, a15
  16:   71f8            l32i.n  a15, a1, 28
  18:   20c112          addi    a1, a1, 32
  1b:   f00d            ret.n
  1d:   000000          ill

00000020 <user_init>:
void  dummy_loop(uint32_t count ){
        while(--count);
}

void ICACHE_FLASH_ATTR user_init()
{
  20:   f0c112          addi    a1, a1, -16
  23:   3109            s32i.n  a0, a1, 12
  25:   21f9            s32i.n  a15, a1, 8
  27:   20f110          or      a15, a1, a1
        // init gpio subsytem
        gpio_init();
  2a:   000001          l32r    a0, fffc002c <user_init+0xfffc000c>
  2d:   0000c0          callx0  a0

        // configure UART TXD to be GPIO1, set as output
        PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_GPIO1);
  30:   000021          l32r    a2, fffc0030 <user_init+0xfffc0010>
  33:   000031          l32r    a3, fffc0034 <user_init+0xfffc0014>
  36:   0020c0          memw
  39:   0348            l32i.n  a4, a3, 0
  3b:   cfae32          movi    a3, 0xfffffecf
  3e:   104430          and     a4, a4, a3
  41:   033c            movi.n  a3, 48
  43:   203430          or      a3, a4, a3
  46:   0020c0          memw
  49:   0239            s32i.n  a3, a2, 0
        gpio_output_set(0, 0, (1 << LED), 0);
  4b:   020c            movi.n  a2, 0
  4d:   030c            movi.n  a3, 0
  4f:   440c            movi.n  a4, 4
  51:   050c            movi.n  a5, 0
  53:   000001          l32r    a0, fffc0054 <user_init+0xfffc0034>
  56:   0000c0          callx0  a0
        for(;;){
                dummy_loop(600000);
  59:   000021          l32r    a2, fffc005c <user_init+0xfffc003c>
  5c:   000005          call0   60 <user_init+0x40>
                system_soft_wdt_feed();
  5f:   000001          l32r    a0, fffc0060 <user_init+0xfffc0040>
  62:   0000c0          callx0  a0
        gpio_output_set(0, (1 << LED), 0, 0);
  65:   020c            movi.n  a2, 0
  67:   430c            movi.n  a3, 4
  69:   00a042          movi    a4, 0
  6c:   050c            movi.n  a5, 0
  6e:   000001          l32r    a0, fffc0070 <user_init+0xfffc0050>
  71:   0000c0          callx0  a0
                dummy_loop(600000);
  74:   000021          l32r    a2, fffc0074 <user_init+0xfffc0054>
  77:   000005          call0   78 <user_init+0x58>
                system_soft_wdt_feed();
  7a:   000001          l32r    a0, fffc007c <user_init+0xfffc005c>
  7d:   0000c0          callx0  a0
        gpio_output_set((1 << LED), 0, 0, 0);
  80:   420c            movi.n  a2, 4
  82:   030c            movi.n  a3, 0
  84:   00a042          movi    a4, 0
  87:   00a052          movi    a5, 0
  8a:   000001          l32r    a0, fffc008c <user_init+0xfffc006c>
  8d:   0000c0          callx0  a0
        }
  90:   fff146          j       59 <user_init+0x39>

здесь секция literal - это зарезервированная память в ОЗУ или IRAM, если угодно. Это память глобальных переменных. На ассемблерные инструкции в этой секции не стоит обращать внимания. Секция text - это сама программа которая располагается во флеш-памяти, и именно она будет нас интересовать.

Основным документом описывающим архитектуру Xtensa является Xtensa Instruction Set Architecture (ISA). Однако, в виду того, что процессоры Xtensa конфигурируемые, а указанный документ описывает общую архитектуру Xtensa, то приходится делать поправки с учётом спецификации esp8266. К счастью, на настоящее время, архитектура esp8266 уже довольно изученная. Хороший обзор основных фишек на английском можно почитать например здесь: http://cholla.mmto.org/esp8266/xtensa.html. Далее я предлагаю вольный перевод этого обзора:

  • ассемблерные инструкции esp8266 имеют 24-битную ширину, но многие инструкции имеют сокращённые 16-битные аналоги. Сокращённые аналоги имеют добавочное ".n" в суффиксе мнемоники команды.
  • esp8266 имеет 16 регистров общего назначения (РОН), которые обозначаются как a0, ..., a15. Сокращение "a" происходит от "address registers". Регистры равноправные, т.е. здесь нет деления на старшие и младшие как в AVR или Thumb/Thumb2.
  • Кроме регистров общего назначения, так же имеется регистр прерываний PS, 8-битный сдвиговый регистр ASR, регистр счётчик команд PC.
  • Регистра указателя стека нет, регистра состояния также нет.
  • Имеются специальные регистры, доступ к которым осуществляется через инструкции wsr и rsr
    wsr.intenable   a5
    rsr.ccount      a2
    wsr.intclear    a2
    xsr.ps    a2
    rsync
  • Gcc использует регистры: a0 в качестве регистра хранения адреса возврата, а1 - в качестве указателя стека, а2 - в качестве регистра передачи и возврата параметра в функцию.
  • Часто используется три РОН в качестве операндов. Например: and a5, a4, a3, будет означать a5=(a4 and a3).
  • Вызов подпрограмм и переходы походят на таковые в ARM/Thumb. По инструкции "j" осуществляется безусловный переход по относительному адресу. По инструкции "jx" осуществляется безусловный переход по адресу хранящемуся в регистре. По инструкции "call0" осуществляется вызов подпрограммы по относительному адресу. По инструкции "callx0" осуществляется вызов подпрограммы по адресу хранящемуся в регистре. Инструкция "ret" или "ret.n" осуществляет возврат из подпрограммы и эквивалента "jx a0".
  • Суффиксы в мнемонике команд указывают на режим адресации: "i" - означает непосредственную адресацию (immediate). "r" - означает относительную адресацию.
  • Суффикс "u" означает, что число в операнде является беззнаковым.
  • Загрузка и сохранение 32-битного числа осуществляется инструкциями l32 и s32. Число может быть загружено или сохранено с помощью относительной адресации инструкциями l32r и s32r, либо с помощью индексной адресации. Например: "l32i.n a3, a2, 0", "s32i.n a3, a2, 1". В первом случае в регистр а3 загружается 32-битное число содержащееся по адресу хранящемуся в регистре a2. Во втором случае в ячейку памяти по адресу хранящемуся в регистре a2 плюс один байт сохраняется число из регистра а3.
  • Возможно также загрузка и сохранение 16-и и 8-битного числа. Например инструкция l8ui загрузит 8-битное беззнаковое число. Для загрузки 16-битного числа имеются инструкции l16si и l16ui, которые загружают знаковое или беззнаковое число.
  • Инструкция mov может работать между регистрами, или может загрузить в регистр небольшое число.
    mov.n   a7, a2   ; a7 <-- a2
    movi.n a3, 0
    Инструкция mov в суффиксе может содержать условие своего выполнения. Например:
    moveqz  a7, a8, a9 ; a7 = a8 if a9 == 0
    movnez  a4, a3, a11 ; a4 = a3 if a11 != 0
    movgez  a2, a3, a4  ; a2 = a3 if a4 >= 0
    movltz  a6, a10, a11  ; a6 = a10 if a11 < 0

5) Подключение JTAG отладчика на FT232H чипе


FT232H Board

Когда я в последний раз писал о STM32, то упоминал о недорогой плате с чипом FT232H, который может служить одноканальным JTAG отладчиком: "Отладка с помощью JTAG адаптера на чипе FT232H". Я понимаю, что тогда это привлекло мало внимания, т.к. китайские клоны ST-Link выглядят несколько привлекательнее, и ещё один отладчик, это уже не так интересно.

Однако в случае esp8266 у нас нет альтернативы JTAG, и данная плата - это самый дешёвый вариант. Проект добавления в OpenOCD поддержки eps8266 начался когда-то с этой темы: PRELIMINARY OPENOCD JTAG DEBUGGER SUPPORT FOR XTENSA/ESP8266, страница проекта: https://github.com/projectgus/openocd.

Проект в данное время заброшен, но ребята из VisualGDB сделали форк: https://github.com/sysprogs/esp8266-openocd, в котором была заявлена стабильная работа, им я и предлагаю воспользоваться.

Я собирал проект компилятором gcc-5.3.0. Для корректной сборки пришлось править права на исполняемые файлы, а также в одном заголовочном файле пришлось поправить директиву препроцессора. Преодолев все трудности, я таки заполучил OpenOCD c поддержкой esp8266.

$ openocd --version
Open On-Chip Debugger 0.9.0 (2018-12-31-13:01)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html

Теперь про подключение esp8266 к JTAG адаптеру. Мне бы не хотелось повторять материал написанный ранее в "Отладка с помощью JTAG адаптера на чипе FT232H", поэтому буду предполагать, что с основами вы уже знакомы.

Для подключения JTAG отладчика будем использовать пины AD0/TCK, AD1/TDI, AD2/TDO, AD3/TMS и пин "земли":

Для справки, в руководстве на чип можно найти полную распиновку чипа ft232h:

Со стороны esp8266 нам понадобятся следующие пины:

Для подключения JTAG адаптера к NodeMCU нужно соединить "землю" адаптера и "землю" платы NodeMCU. После этого подсоединяем: AD0 к GPIO13/D7, AD1 к GPIO12/D6, AD2 к GPIO15/D8, AD3 к GPIO14/D5. При подключении будем использовать независимое питание на NodeMCU и для JTAG адаптера. Адаптер нужно будет подключить в USB порт компьютера, а NodeMCU можно подключить к компьютеру или зарядке с microUSB.

Я должен предупредить о подводных камнях. В случае подключения JTAG-адаптера и платы NodeMCU к одному компьютеру, подключать следует сначала плату NodeMCU и только потом JTAG-адаптер. Предположим, что у нас в esp8266 залита прошивка мигалки базового проекта. При подключении NodeMCU к компьютеру, на плате начнёт мигать светодиод. Далее, при последующем подключении к компьютеру JTAG-адаптера, светодиод продолжит мигать. И мы будем использовать именно такой порядок подключения.

Если же сделать наоборот, т.е. сначала подключить к компьютеру JTAG-адаптер и только потом NodeMCU, то светодиод мигать не будет, даже если нажать RESET на плате. Такой вариант корректно работать не будет.

Разобравшись с подключением, создадим скрипт для JTAG адаптера:

interface ftdi
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x0c08 0x0f1b

Назовём его "interface.cfg". Теперь запускаем OpenOCD командой:

$ openocd -f ./interface.cfg  -f target/esp8266.cfg

В логе получим следующий вывод:

Open On-Chip Debugger 0.9.0 (2018-12-31-13:01)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
trst_and_srst separate srst_gates_jtag trst_push_pull srst_open_drain connect_deassert_srst
adapter speed: 1000 kHz
stop_wdt
Info : clock speed 1000 kHz
Info : TAP esp8266.cpu does not have IDCODE
Warn : Warning: Target not halted, breakpoint/watchpoint state may be unpredictable.

В другом окне запускаем отладчик:

$ xtensa-lx106-elf-gdb ./blink.elf --tui

Подключаемся к OpenOCD:

(gdb) target remote :3333

После этого даём команду OpenOCD

(gdb) monitor reset init

В ответ мы должны получить сообщение:

Can't assert TRST: nTRST signal is not defined
Can't assert SRST: nSRST signal is not defined
TAP esp8266.cpu does not have IDCODE
Can't assert SRST: nSRST signal is not defined
xtensa_deassert_reset: 'reset halt' is not supported for Xtensa. Have halted some time after resetting (not the same thing!)
target state: halted
halted: PC: 0x401000c5
debug cause: 0x20

После чего мигание светодиода должно прекратиться, т.к. процессор ESP8266 был остановлен на адресе 0x401000c5.

Командой load загружаем прошивку:

(gdb) load
Loading section .data, size 0x382 lma 0x3ffe8000
Loading section .rodata, size 0x388 lma 0x3ffe8390
Loading section .text, size 0x62e8 lma 0x40100000
Loading section .irom0.text, size 0x30ce0 lma 0x40210000
Start address 0x40100004, load size 227026
Transfer rate: 27 KB/sec, 3914 bytes/write.

В итоге мы оказываемся в начале выполнения прошиивки:

Если мы поставим точку останова, скажем на функцию dummy_loop, то трассировки у нас не получится, т.к. мы постоянно будем вываливаться в обработчик прерывания watchdog'а.

Вводим команду monitor help и получаем список доступных команд OpenOCD. Нас наиболее будут интересовать эти команды:

esp8266.cpu
      target command group (command valid any time)
  esp8266.cpu arp_examine
        used internally for reset processing
  esp8266.cpu arp_halt
        used internally for reset processing
  esp8266.cpu arp_halt_gdb
        used internally for reset processing to halt GDB
  esp8266.cpu arp_poll
        used internally for reset processing
  esp8266.cpu arp_reset
        used internally for reset processing
  esp8266.cpu arp_waitstate
        used internally for reset processing
  esp8266.cpu array2mem arrayname bitwidth address count
        Writes Tcl array of 8/16/32 bit numbers to target memory
  esp8266.cpu cget target_attribute
        returns the specified target attribute (command valid any time)
  esp8266.cpu configure [target_attribute ...]
        configure a new target for use (configuration command)
  esp8266.cpu curstate
        displays the current state of this target
  esp8266.cpu esp8266_autofeed_watchdog [enable|disable]
        Specifies whether OpenOCD will feed the ESP8266 software watchdog
        while the target is halted (command valid any time)
  esp8266.cpu eventlist
        displays a table of events defined for this target
  esp8266.cpu invoke-event event_name
        invoke handler for specified event
  esp8266.cpu mdb address [count]
        Display target memory as 8-bit bytes
  esp8266.cpu mdh address [count]
        Display target memory as 16-bit half-words
  esp8266.cpu mdw address [count]
        Display target memory as 32-bit words
  esp8266.cpu mem2array arrayname bitwidth address count
        Loads Tcl array of 8/16/32 bit numbers from target memory
  esp8266.cpu mwb address data [count]
        Write byte(s) to target memory
  esp8266.cpu mwh address data [count]
        Write 16-bit half-word(s) to target memory
  esp8266.cpu mww address data [count]
        Write 32-bit word(s) to target memory
  esp8266.cpu xtensa_no_interrupts_during_steps [enable|disable]
        Specifies whether the INTENABLE register will be set to 0 during
        single-stepping, temporarily disabling interrupts (command valid
        any time)
esp8266_autofeed_watchdog [enable|disable]
      Specifies whether OpenOCD will feed the ESP8266 software watchdog
      while the target is halted (command valid any time)

Команда esp8266.cpu xtensa_no_interrupts_during_steps enable запретит выполнение прерываний во время трассировки программы.

(gdb) monitor esp8266.cpu xtensa_no_interrupts_during_steps enable
Interrupt suppression during single-stepping is now enabled

Теперь ставим точку останова:

(gdb) break dummy_loop
Breakpoint 1 at 0x401000c1: file main.c, line 9.

И запускаем прошивку на выполнение командой: "continue" или её сокращением: "c". Далее отладка выполняется обычным образом.

6) Работа с GPIO16 через NonOS-SDK, подключение библиотеки "Driver_Lib"

Теперь, если мы захотим помигать не тем светодиодом, который расположен на модуле ESP12, а тем, что подключен к D0/GPIO16, то на нам придется научится работать с GPIO16.

GPIO16 занимает особое положение в ESP8266, так как относится к подсистеме RTC. Через GPIO16 происходит пробуждение из спящего режима, поэтому управляется он немного по другому. Подсказка, о том, как это делается, была найдена здесь: GPIO16 and setting RST line - ESP8266 Developer Zone.

Пример который у меня впервые заработал, выглядел так:

#include "ets_sys.h"
#include "user_interface.h"
#include "gpio.h"

void  dummy_loop(uint32_t count ){
    while(--count) {
    }
}

void ICACHE_FLASH_ATTR user_init()
{
    // init gpio subsystem
    gpio_init();

    // https://bbs.espressif.com/viewtopic.php?t=1521  
    // map GPIO16 as an I/O pin
    uint32_t val = READ_PERI_REG(PAD_XPD_DCDC_CONF) & 0xffffffbc;
    WRITE_PERI_REG(PAD_XPD_DCDC_CONF, val | 0x00000001);    //mux configuration for XPD_DCDC to output rtc_gpio0
    val = READ_PERI_REG(RTC_GPIO_CONF) & 0xfffffffe;
    WRITE_PERI_REG(RTC_GPIO_CONF, val | 0x00000000);        // mux configuration for out enable

    for(;;){
        dummy_loop(600000);
        system_soft_wdt_stop();
        SET_PERI_REG_MASK(RTC_GPIO_ENABLE, (uint32_t)0x01);
        dummy_loop(600000);
        system_soft_wdt_feed();
        CLEAR_PERI_REG_MASK(RTC_GPIO_ENABLE, (uint32_t)0x01);
    }
}

Это не совсем корректный пример, он работает по тому принципу, что и неисправные часы, которые дважды в день показывают правильное время. Что же здесь происходит?

Первым делом, "мапится" вывод XPD_DCDC (он же GPIO16) в качестве пина ввода-вывода:

uint32_t val = READ_PERI_REG(PAD_XPD_DCDC_CONF) & 0xffffffbc;
WRITE_PERI_REG(PAD_XPD_DCDC_CONF, val | 0x00000001);    //mux configuration for XPD_DCDC to output rtc_gpio0

Затем GPIO16 переводится в push-pull режим:

val = READ_PERI_REG(RTC_GPIO_CONF) & 0xfffffffe;
WRITE_PERI_REG(RTC_GPIO_CONF, val | 0x00000000);        // mux configuration for out enable

В главном цикле, вместо задания GPIO16 высокого или низкого логического уровня, попросту GPIO16 отключается или включается:

SET_PERI_REG_MASK(RTC_GPIO_ENABLE, (uint32_t)0x01);
CLEAR_PERI_REG_MASK(RTC_GPIO_ENABLE, (uint32_t)0x01);

Программа некорректная, но создает иллюзию обратного. Что можно сказать по этому коду? Здесь мы имеем сплошное использование макросов, непонятных регистров: PAD_XPD_DCDC_CONF, RTC_GPIO_ENABLE, "магических чисел" навроде: 0xffffffbc или 0xfffffffe.

Определения макросов можно найди в заголовочном файле eagle_soc.h:

//Registers Operation {{
#define ETS_UNCACHED_ADDR(addr) (addr)
#define ETS_CACHED_ADDR(addr) (addr)


#define READ_PERI_REG(addr) (*((volatile uint32_t *)ETS_UNCACHED_ADDR(addr)))
#define WRITE_PERI_REG(addr, val) (*((volatile uint32_t *)ETS_UNCACHED_ADDR(addr))) = (uint32_t)(val)
#define CLEAR_PERI_REG_MASK(reg, mask) WRITE_PERI_REG((reg), (READ_PERI_REG(reg)&(~(mask))))
#define SET_PERI_REG_MASK(reg, mask)   WRITE_PERI_REG((reg), (READ_PERI_REG(reg)|(mask)))
#define GET_PERI_REG_BITS(reg, hipos,lowpos)      ((READ_PERI_REG(reg)>>(lowpos))&((1<<((hipos)-(lowpos)+1))-1))
#define SET_PERI_REG_BITS(reg,bit_map,value,shift) (WRITE_PERI_REG((reg),(READ_PERI_REG(reg)&(~((bit_map)<<(shift))))|((value)<<(shift)) ))
//}}

Там же можно найти регистры RTC модуля:

//RTC reg {{
#define REG_RTC_BASE  PERIPHS_RTC_BASEADDR

#define RTC_STORE0                              (REG_RTC_BASE + 0x030)
#define RTC_STORE1                              (REG_RTC_BASE + 0x034)
#define RTC_STORE2                              (REG_RTC_BASE + 0x038)
#define RTC_STORE3                              (REG_RTC_BASE + 0x03C)

#define RTC_GPIO_OUT                            (REG_RTC_BASE + 0x068)
#define RTC_GPIO_ENABLE                         (REG_RTC_BASE + 0x074)
#define RTC_GPIO_IN_DATA                        (REG_RTC_BASE + 0x08C)
#define RTC_GPIO_CONF                           (REG_RTC_BASE + 0x090)
#define PAD_XPD_DCDC_CONF                       (REG_RTC_BASE + 0x0A0)
//}}

Но вот описания этих регистров, вы нигде не найдете.

Корректный вариант программы с последовательным переводом GPIO16 в высокое и низкое логическое значение, выглядит так:

#include "ets_sys.h"
#include "user_interface.h"
#include "gpio.h"

#define LED 16


void  dummy_loop(uint32_t count ){
    while(--count) {
    }
}

void ICACHE_FLASH_ATTR user_init()
{
    gpio_init();

    // map GPIO16 as an I/O pin
    // https://bbs.espressif.com/viewtopic.php?t=1521
    uint32_t pinHigh;
    uint32_t val = READ_PERI_REG(PAD_XPD_DCDC_CONF) & 0xffffffbc;
    WRITE_PERI_REG(PAD_XPD_DCDC_CONF, val | 0x00000001);    //mux configuration for XPD_DCDC to output rtc_gpio0
    val = READ_PERI_REG(RTC_GPIO_CONF) & 0xfffffffe;
    WRITE_PERI_REG(RTC_GPIO_CONF, val | 0x00000000);        // mux configuration for out enable
    //  push-pull mode
    val = READ_PERI_REG(RTC_GPIO_ENABLE);
    WRITE_PERI_REG(RTC_GPIO_ENABLE, val | (uint32_t)0x01);
    val = READ_PERI_REG(RTC_GPIO_OUT);

    for(;;){
        dummy_loop(600000);
        system_soft_wdt_stop();
        WRITE_PERI_REG(RTC_GPIO_OUT, (val | (uint32_t)0x01)); // high level 
        dummy_loop(600000);
        system_soft_wdt_feed();
        WRITE_PERI_REG(RTC_GPIO_OUT, (val & ~(uint32_t)0x01)); // low level
    }
}

Проблема этого примера в том, что здесь все делается через регистры. Кроме того, по этим регистрам нет никакой документации. Это совсем не то что обещалось в начале: "Данное API позволяет программировать на более высоком уровне не вдаваясь в особенности архитектуры ESP8266". На самом деле, в составе SDK имеется библиотека "driver_lib" c открытыми исходными кодам для работы с GPIO16 и другой периферией. Ее структура выглядит так:

.
├── Makefile
├── README.md
├── driver
│   ├── Makefile
│   ├── gpio16.c
│   ├── hw_timer.c
│   ├── i2c_master.c
│   ├── key.c
│   ├── sdio_slv.c
│   ├── spi.c
│   ├── spi_interface.c
│   ├── spi_overlap.c
│   └── uart.c
├── include
│   └── driver
│       ├── gpio16.h
│       ├── i2c_master.h
│       ├── key.h
│       ├── sdio_slv.h
│       ├── slc_register.h
│       ├── spi.h
│       ├── spi_interface.h
│       ├── spi_overlap.h
│       ├── spi_register.h
│       ├── uart.h
│       └── uart_register.h
└── make_lib.sh

Для примера, файл gpio16.c имеет следующее содержание:

/*
 * ESPRESSIF MIT License
 *
 * Copyright (c) 2016 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
 *
 * Permission is hereby granted for use on ESPRESSIF SYSTEMS ESP8266 only, in which case,
 * it is free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished
 * to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */

#include "ets_sys.h"
#include "osapi.h"
#include "driver/gpio16.h"

void ICACHE_FLASH_ATTR
gpio16_output_conf(void)
{
    WRITE_PERI_REG(PAD_XPD_DCDC_CONF,
                   (READ_PERI_REG(PAD_XPD_DCDC_CONF) & 0xffffffbc) | (uint32)0x1);  // mux configuration for XPD_DCDC to output rtc_gpio0

    WRITE_PERI_REG(RTC_GPIO_CONF,
                   (READ_PERI_REG(RTC_GPIO_CONF) & (uint32)0xfffffffe) | (uint32)0x0);  //mux configuration for out enable

    WRITE_PERI_REG(RTC_GPIO_ENABLE,
                   (READ_PERI_REG(RTC_GPIO_ENABLE) & (uint32)0xfffffffe) | (uint32)0x1);    //out enable
}

void ICACHE_FLASH_ATTR
gpio16_output_set(uint8 value)
{
    WRITE_PERI_REG(RTC_GPIO_OUT,
                   (READ_PERI_REG(RTC_GPIO_OUT) & (uint32)0xfffffffe) | (uint32)(value & 1));
}

void ICACHE_FLASH_ATTR
gpio16_input_conf(void)
{
    WRITE_PERI_REG(PAD_XPD_DCDC_CONF,
                   (READ_PERI_REG(PAD_XPD_DCDC_CONF) & 0xffffffbc) | (uint32)0x1);  // mux configuration for XPD_DCDC and rtc_gpio0 connection

    WRITE_PERI_REG(RTC_GPIO_CONF,
                   (READ_PERI_REG(RTC_GPIO_CONF) & (uint32)0xfffffffe) | (uint32)0x0);  //mux configuration for out enable

    WRITE_PERI_REG(RTC_GPIO_ENABLE,
                   READ_PERI_REG(RTC_GPIO_ENABLE) & (uint32)0xfffffffe);    //out disable
}

uint8 ICACHE_FLASH_ATTR
gpio16_input_get(void)
{
    return (uint8)(READ_PERI_REG(RTC_GPIO_IN_DATA) & 1);
}

Т.е. весь нужный функционал для работы с GPIO16 в нем уже есть.

Нам потребуется внести изменения в Makefile, чтобы добавить библиотеку в проект:

SDK=/here_should_be_path_to_your_ESP8266_SDK/ESP8266_NONOS_SDK-2.1.0-18-g61248df
DRIVER=/here_should_be_path_to_your_ESP8266_SDK/ESP8266_NONOS_SDK-2.1.0-18-g61248df/driver_lib
CC=xtensa-lx106-elf-gcc
SIZE=xtensa-lx106-elf-size
ESPTOOL=esptool.py
INC  = -I./inc -mlongcalls
INC += -I$(DRIVER)/include
CFLAGS= -std=c99 -ggdb -O0 -Wall  $(INC)
LDFLAGS= -L$(SDK)/lib  -T$(SDK)/ld/eagle.app.v6.ld
LDLIBS=-nostdlib -Wl,--start-group -lmain -lnet80211 -lwpa -llwip -lpp -lphy -lc -Wl,--end-group -lgcc
OBJ=main.o gpio16.o
TARGET=gpio16
.PHONY: all clean

%.o:	$(DRIVER)/driver/%.c
	$(CC) $(CFLAGS) -c -o $@ $<
%.o:	%.c
	$(CC) $(CFLAGS) -c -o $@ $<
all:	$(OBJ)
	$(CC) $(LDFLAGS) -ggdb  -o $(TARGET).elf  $(OBJ) $(LDLIBS)
	$(SIZE)  $(TARGET).elf
	$(ESPTOOL) elf2image $(TARGET).elf
install:
	$(ESPTOOL) write_flash 0 $(TARGET).elf-0x00000.bin 0x10000 $(TARGET).elf-0x10000.bin
clean:
	@rm -v $(TARGET).elf $(OBJ) $(TARGET).elf-0x?0000.bin    

Тогда программа с использование этой библиотеки будет выглядеть так:

#include "ets_sys.h"
#include "user_interface.h"
#include "gpio.h"
#include "driver/gpio16.h"

void  dummy_loop(uint32_t count ){
    while(--count) {
    }
}

void ICACHE_FLASH_ATTR user_init()
{
    gpio_init();

    // map GPIO16 as push-pull  pin
    gpio16_output_conf();

    for(;;){
        dummy_loop(600000);
        system_soft_wdt_stop();
        gpio16_output_set(0x1);     // High
        dummy_loop(600000);
        system_soft_wdt_feed();
        gpio16_output_set(0x0);     // Low
    }
}

Ну, это уже кое-что.

7) Вывод через UART

В ESP8266 имеется два UART интерфейса: UART0 и UART1. UART0 имеет два независимых fifo буфера размером в 128 байт, по опустошению которого или по переполнению, можно вызывать прерывание. К UART0 подключен UART-USB адаптер CP2102 и он является основным интерфейсом. UART1 имеет только одну линию для передачи, он является отладочным интерфесом:

Т.к. к USB у нас подключен UART0, его и попытаемся задействовать. В ESP8266 Non-OS SDK API Reference описаны три функции для работы UART. Первая функция инициализирует UART интерфейс:

Функция uart_init
Назначение функции Устанавливает скорость работы обоих UART интерфейсов
Прототип void uart_init (UartBautRateuart0_br, UartBautRate uart1_br)
Параметры UartBautRate uart0_br: устанавливаемый битрейт интерфейса uart0;
UartBautRate uart1_br: устанавливаемый битрейт интерфейса uart1;
Возможные значения битрейта typedef enum { BIT_RATE_9600 = 9600, BIT_RATE_19200 = 19200, BIT_RATE_38400 = 38400, BIT_RATE_57600 = 57600, BIT_RATE_74880 = 74880, BIT_RATE_115200 = 115200, BIT_RATE_230400 = 230400, BIT_RATE_460800 = 460800, BIT_RATE_921600 = 921600 } UartBautRate;
Возвращаемое значение отсутствует.

Следующая функция позволяет переслать содержимое буфера через UART0:

uart0_tx_buffer
Назначение Пересылает содержимое буфера через UART0
Прототип void uart0_tx_buffer(uint8 *buf, uint16 len)
Параметры uint8 *buf: указатель на буфер, содержимое которого следует переправить через UART0;
uint16 len: длина буфера.
Возвращаемое значение отсутствует.

Третья функция может содержать обработчик прерывания интерфейса UART0:

uart0_rx_intr_handler
Назначение позволяет установить обработчик прерывания для входящих данных
Прототип void uart0_rx_intr_handler(void *para)
Параметры void *para:: указатель на RcvMsgBuff структуру.

Остальные функции объявлены в заголовочном файле "driver_lib/include/driver/uart.h", и никак не документированы:

void uart_init(UartBautRate uart0_br, UartBautRate uart1_br);
void uart0_sendStr(const char *str);

//void ICACHE_FLASH_ATTR uart_test_rx();
STATUS uart_tx_one_char(uint8 uart, uint8 TxChar);
STATUS uart_tx_one_char_no_wait(uint8 uart, uint8 TxChar);
void  uart1_sendStr_no_wait(const char *str);
struct UartBuffer*  Uart_Buf_Init();


#if UART_BUFF_EN
LOCAL void  Uart_Buf_Cpy(struct UartBuffer* pCur, char* pdata , uint16 data_len);
void  uart_buf_free(struct UartBuffer* pBuff);
void  tx_buff_enq(char* pdata, uint16 data_len );
LOCAL void  tx_fifo_insert(struct UartBuffer* pTxBuff, uint8 data_len,  uint8 uart_no);
void  tx_start_uart_buffer(uint8 uart_no);
uint16  rx_buff_deq(char* pdata, uint16 data_len );
void  Uart_rx_buff_enq();
#endif
void  uart_rx_intr_enable(uint8 uart_no);
void  uart_rx_intr_disable(uint8 uart_no);
void uart0_tx_buffer(uint8 *buf, uint16 len);

//==============================================
#define FUNC_UART0_CTS 4
#define FUNC_U0CTS                      4
#define FUNC_U1TXD_BK                   2
#define UART_LINE_INV_MASK  (0x3f<<19)
void UART_SetWordLength(uint8 uart_no, UartBitsNum4Char len);
void UART_SetStopBits(uint8 uart_no, UartStopBitsNum bit_num);
void UART_SetLineInverse(uint8 uart_no, UART_LineLevelInverse inverse_mask);
void UART_SetParity(uint8 uart_no, UartParityMode Parity_mode);
void UART_SetBaudrate(uint8 uart_no,uint32 baud_rate);
void UART_SetFlowCtrl(uint8 uart_no,UART_HwFlowCtrl flow_ctrl,uint8 rx_thresh);
void UART_WaitTxFifoEmpty(uint8 uart_no , uint32 time_out_us); //do not use if tx flow control enabled
void UART_ResetFifo(uint8 uart_no);
void UART_ClearIntrStatus(uint8 uart_no,uint32 clr_mask);
void UART_SetIntrEna(uint8 uart_no,uint32 ena_mask);
void UART_SetPrintPort(uint8 uart_no);
bool UART_CheckOutputFinished(uint8 uart_no, uint32 time_out_us);
//==============================================

Но это еще не все неприятности. Дело в том, что просто так добавить в проект файл "driver_lib/driver/uart.c" не получится. В Espressif "забыли" добавить в SDK прототипы функций которые нужны для компиляции модуля. И при попытке компиляции выдаст ошибку об отсутствии объявлений функций ets_isr_mask() и ets_isr_unmask().

Проблема широко известная и легко гуглится, так же как и файл с прототипами недостающих функций. Я взял вариант такого файла у Михаила Григорьева https://github.com/CHERTS/esp8266-devkit/blob/master/Espressif/examples/ESP8266/esphttpd/libesphttpd/include/espmissingincludes.h, закоментировал в нем лишнее для меня пока функции, и положил его в директорию inc текущего проекта.

Т.о. содержимое файла "inc/espmissingincludes.h" получилось таким:

#ifndef ESPMISSINGINCLUDES_H
#define ESPMISSINGINCLUDES_H

#include <stdint.h>
#include <c_types.h>


int strcasecmp(const char *a, const char *b);
#ifndef FREERTOS
#include <eagle_soc.h>
#include <ets_sys.h>
/*
//Missing function prototypes in include folders. Gcc will warn on these if we don't define 'em anywhere.
//MOST OF THESE ARE GUESSED! but they seem to swork and shut up the compiler.
typedef struct espconn espconn;

int atoi(const char *nptr);
void ets_install_putc1(void (*routine)(char c));
void ets_isr_attach(int intr, void (*handler)(void *), void *arg);
*/
void ets_isr_mask(unsigned intr);
void ets_isr_unmask(unsigned intr);
/*
int ets_memcmp(const void *s1, const void *s2, size_t n);
void *ets_memcpy(void *dest, const void *src, size_t n);
void *ets_memset(void *s, int c, size_t n);
int ets_sprintf(char *str, const char *format, ...)  __attribute__ ((format (printf, 2, 3)));
int ets_str2macaddr(void *, void *);
int ets_strcmp(const char *s1, const char *s2);
char *ets_strcpy(char *dest, const char *src);
int ets_strlen(const char *s);
int ets_strncmp(const char *s1, const char *s2, unsigned int len);
char *ets_strncpy(char *dest, const char *src, size_t n);
char *ets_strstr(const char *haystack, const char *needle);
void ets_timer_arm_new(os_timer_t *a, uint32_t b, bool repeat, bool isMstimer);
void ets_timer_disarm(os_timer_t *a);
void ets_timer_setfn(os_timer_t *t, ETSTimerFunc *fn, void *parg);
void ets_update_cpu_frequency(int freqmhz);
void *os_memmove(void *dest, const void *src, size_t n);
int os_printf(const char *format, ...)  __attribute__ ((format (printf, 1, 2)));
int os_snprintf(char *str, size_t size, const char *format, ...) __attribute__ ((format (printf, 3, 4)));
int os_printf_plus(const char *format, ...)  __attribute__ ((format (printf, 1, 2)));
void uart_div_modify(uint8 no, uint32 freq);
uint8 wifi_get_opmode(void);
uint32 system_get_time();
int rand(void);
void ets_bzero(void *s, size_t n);
void ets_delay_us(uint16_t ms);

//Hack: this is defined in SDK 1.4.0 and undefined in 1.3.0. It's only used for this, the symbol itself
//has no meaning here.
#ifndef RC_LIMIT_P2P_11N
//Defs for SDK <1.4.0
void *pvPortMalloc(size_t xWantedSize);
void *pvPortZalloc(size_t);
void vPortFree(void *ptr);
void *vPortMalloc(size_t xWantedSize);
void pvPortFree(void *ptr);
#else
void *pvPortMalloc(size_t xWantedSize, const char *file, unsigned line);
void *pvPortZalloc(size_t, const char *file, unsigned line);
void vPortFree(void *ptr, const char *file, unsigned line);
void *vPortMalloc(size_t xWantedSize, const char *file, unsigned line);
void pvPortFree(void *ptr, const char *file, unsigned line);
#endif
*/
//Standard PIN_FUNC_SELECT gives a warning. Replace by a non-warning one.
#ifdef PIN_FUNC_SELECT
#undef PIN_FUNC_SELECT
#define PIN_FUNC_SELECT(PIN_NAME, FUNC)  do { \
    WRITE_PERI_REG(PIN_NAME,   \
                                (READ_PERI_REG(PIN_NAME) \
                                     &  (~(PERIPHS_IO_MUX_FUNC<<PERIPHS_IO_MUX_FUNC_S)))  \
                                     |( (((FUNC&BIT2)<<2)|(FUNC&0x3))<<PERIPHS_IO_MUX_FUNC_S) );  \
    } while (0)
#endif

#endif

#endif

Объявление этого файла нужно будет добавить в "driver_lib/include/driver/uart.h". Также, модуль uart.o нужно будет добавить в Makefile:

OBJ=main.o gpio16.o uart.o

Осталось дело за малым, написать тестовую программу:

#include "ets_sys.h"
#include "user_interface.h"
#include "gpio.h"
#include "driver/gpio16.h"
#include "driver/uart.h"
#include "espmissingincludes.h"


void  dummy_loop(uint32_t count ){
    while(--count) {
    }
}

void ICACHE_FLASH_ATTR user_init()
{
    gpio_init();
    // map GPIO16 as push-pull  pin
    gpio16_output_conf();
    // UART config
    uart_init(BIT_RATE_115200, BIT_RATE_115200);
    // let's go... 

    for(;;){
        dummy_loop(600000);
        system_soft_wdt_stop();
        gpio16_output_set(0x1);     // High
        dummy_loop(600000);
        system_soft_wdt_feed();
        gpio16_output_set(0x0);     // Low
        uart0_sendStr("Test ESP8266\r\n");
    }
}

Также полезной функцией может os_printf(). C ее помощью можно вывести различную отладочную информацию. Например, составим такую программу:

#include "ets_sys.h"
#include "user_interface.h"
#include "gpio.h"
#include "osapi.h"
#include "driver/gpio16.h"
#include "driver/uart.h"
#include "espmissingincludes.h"

//void ets_isr_mask(unsigned intr);
//void ets_isr_unmask(unsigned intr);

void  dummy_loop(uint32_t count ){
    while(--count) {
    }
}

void ICACHE_FLASH_ATTR user_init()
{
    gpio_init();
    // map GPIO16 as push-pull  pin
    gpio16_output_conf();
    // UART config
    uart_init(BIT_RATE_115200, BIT_RATE_115200);
    // let's go... 
    system_set_os_print(0x01);

    for(;;){
        dummy_loop(6000000);
        system_soft_wdt_stop();
        gpio16_output_set(0x1);     // High
        dummy_loop(6000000);
        system_soft_wdt_feed();
        gpio16_output_set(0x0);     // Low
//      uart0_sendStr("Test ESP8266\r\n");
        os_printf("SDK version: %s\n", system_get_sdk_version());
        os_printf("Chip ID: 0x%x\n", system_get_chip_id());
        os_printf("--- meminfo----\n");
        system_print_meminfo();
        os_printf("Heap size: %u\n", system_get_free_heap_size());
        os_printf("CPU frequency: %u\n", system_get_cpu_freq());
        os_printf("System time: %ums\n", system_get_time());
        os_printf("RTC time: %u\n", system_get_rtc_time());
        os_printf("Boot version: %u\n", system_get_boot_version());
        os_printf("Userbin address: %u\n", system_get_userbin_addr());
        os_printf("Boot mode: %u\n", system_get_boot_mode());
    }
}

Результат работы программы:

На этом пока все, посмотреть исходники, сборочные файлы, скачать скомпилированные прошивки, можно с портала GITLAB https://gitlab.com/flank1er/esp8266_sdk_examples

поделиться: