ulp: add build system integration and example

This commit is contained in:
Dmitry Yakovlev 2017-01-09 07:38:20 +03:00 committed by Ivan Grokhotkov
parent 573cc7d36f
commit a6e4e89592
18 changed files with 1849 additions and 239 deletions

View File

@ -0,0 +1,46 @@
// Copyright 2010-2016 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
// This file contains various convenience macros to be used in ULP programs.
// Helper macros to calculate bit field width from mask, using the preprocessor.
// Used later in READ_RTC_FIELD and WRITE_RTC_FIELD.
#define IS_BIT_SET(m, i) (((m) >> (i)) & 1)
#define MASK_TO_WIDTH_HELPER1(m, i) IS_BIT_SET(m, i)
#define MASK_TO_WIDTH_HELPER2(m, i) (MASK_TO_WIDTH_HELPER1(m, i) + MASK_TO_WIDTH_HELPER1(m, i + 1))
#define MASK_TO_WIDTH_HELPER4(m, i) (MASK_TO_WIDTH_HELPER2(m, i) + MASK_TO_WIDTH_HELPER2(m, i + 2))
#define MASK_TO_WIDTH_HELPER8(m, i) (MASK_TO_WIDTH_HELPER4(m, i) + MASK_TO_WIDTH_HELPER4(m, i + 4))
#define MASK_TO_WIDTH_HELPER16(m, i) (MASK_TO_WIDTH_HELPER8(m, i) + MASK_TO_WIDTH_HELPER8(m, i + 8))
#define MASK_TO_WIDTH_HELPER32(m, i) (MASK_TO_WIDTH_HELPER16(m, i) + MASK_TO_WIDTH_HELPER16(m, i + 16))
// Peripheral register access macros, build around REG_RD and REG_WR instructions.
// Registers defined in rtc_cntl_reg.h, rtc_io_reg.h, and sens_reg.h are usable with these macros.
// Read from rtc_reg[low_bit + bit_width - 1 : low_bit] into R0, bit_width <= 16
#define READ_RTC_REG(rtc_reg, low_bit, bit_width) \
REG_RD (((rtc_reg) - DR_REG_RTCCNTL_BASE) / 4), ((low_bit) + (bit_width) - 1), (low_bit)
// Write immediate value into rtc_reg[low_bit + bit_width - 1 : low_bit], bit_width <= 8
#define WRITE_RTC_REG(rtc_reg, low_bit, bit_width, value) \
REG_WR (((rtc_reg) - DR_REG_RTCCNTL_BASE) / 4), ((low_bit) + (bit_width) - 1), (low_bit), ((value) & 0xff)
// Read from a field in rtc_reg into R0, up to 16 bits
#define READ_RTC_FIELD(rtc_reg, field) \
READ_RTC_REG(rtc_reg, field ## _S, MASK_TO_WIDTH_HELPER16(field ## _V, 0))
// Write immediate value into a field in rtc_reg, up to 8 bits
#define WRITE_RTC_FIELD(rtc_reg, field, value) \
WRITE_RTC_REG(rtc_reg, field ## _S, MASK_TO_WIDTH_HELPER8(field ## _V, 0), ((value) & field ## _V))

View File

@ -0,0 +1,8 @@
ULP_BINUTILS_PREFIX ?= esp32ulp-elf-
export ULP_AS := $(ULP_BINUTILS_PREFIX)as
export ULP_LD := $(ULP_BINUTILS_PREFIX)ld
export ULP_OBJCOPY := $(ULP_BINUTILS_PREFIX)objcopy
export ULP_OBJDUMP := $(ULP_BINUTILS_PREFIX)objdump
export ULP_NM := $(ULP_BINUTILS_PREFIX)nm
export ULP_LD_TEMPLATE := $(IDF_PATH)/components/ulp/ld/esp32.ulp.ld
export ULP_MAP_GEN := $(IDF_PATH)/components/ulp/esp32ulp_mapgen.py

View File

@ -1,12 +1,7 @@
ULP coprocessor programming
===========================
Programming ULP coprocessor using C macros
==========================================
.. warning:: ULP coprocessor programming approach described here is experimental. It is probable that once binutils support for ULP is done, this preprocessor-based approach may be deprecated. We welcome discussion about and contributions to ULP programming tools.
ULP coprocessor is a simple FSM which is designed to perform measurements using ADC, temperature sensor, and external I2C sensors, while main processors are in deep sleep mode. ULP coprocessor can access RTC_SLOW_MEM memory region, and registers in RTC_CNTL, RTC_IO, and SARADC peripherals. ULP coprocessor uses fixed-width 32-bit instructions, 32-bit memory addressing, and has 4 general purpose 16-bit registers.
ULP coprocessor doesn't have a dedicated binutils port yet. Programming ULP coprocessor is possible by embedding assembly-like macros into an ESP32 application.
Here is an example how this can be done::
In addition to the existing binutils port for the ESP32 ULP coprocessor, it is possible to generate programs for the ULP by embedding assembly-like macros into an ESP32 application. Here is an example how this can be done::
const ulp_insn_t program[] = {
I_MOVI(R3, 16), // R3 <- 16

View File

@ -0,0 +1,96 @@
# Extra make rules for components containing ULP coprocessor code.
#
# ULP program(s) gets built and linked into the application.
# Steps taken here are explained in docs/ulp.rst
# Define names for files generated at different stages
ULP_ELF := $(ULP_APP_NAME).elf
ULP_MAP := $(ULP_ELF:.elf=.map)
ULP_SYM := $(ULP_ELF:.elf=.sym)
ULP_BIN := $(ULP_ELF:.elf=.bin)
ULP_EXPORTS_LD := $(ULP_ELF:.elf=.ld)
ULP_EXPORTS_HEADER := $(ULP_ELF:.elf=.h)
ULP_LD_SCRIPT := $(ULP_ELF:.elf=.common.ld)
ULP_OBJECTS := $(notdir $(ULP_S_SOURCES:.S=.ulp.o))
ULP_DEP := $(notdir $(ULP_S_SOURCES:.S=.ulp.d)) $(ULP_LD_SCRIPT:.ld=.d)
ULP_PREPROCESSED := $(notdir $(ULP_S_SOURCES:.S=.ulp.pS))
ULP_LISTINGS := $(notdir $(ULP_S_SOURCES:.S=.ulp.lst))
ULP_PREPROCESSOR_ARGS := \
$(addprefix -I ,$(COMPONENT_INCLUDES)) \
$(addprefix -I ,$(COMPONENT_EXTRA_INCLUDES)) \
-I$(COMPONENT_PATH) -D__ASSEMBLER__
-include $(ULP_DEP)
# Preprocess LD script used to link ULP program
$(ULP_LD_SCRIPT): $(ULP_LD_TEMPLATE)
$(summary) CPP $(notdir $@)
$(CC) $(CPPFLAGS) -MT $(ULP_LD_SCRIPT) -E -P -xc -o $@ $(ULP_PREPROCESSOR_ARGS) $<
# Generate preprocessed assembly files.
# To inspect these preprocessed files, add a ".PRECIOUS: %.ulp.pS" rule.
%.ulp.pS: $(COMPONENT_PATH)/ulp/%.S
$(summary) CPP $(notdir $<)
$(CC) $(CPPFLAGS) -MT $(patsubst %.ulp.pS,%.ulp.o,$@) -E -P -xc -o $@ $(ULP_PREPROCESSOR_ARGS) $<
# Compiled preprocessed files into object files.
%.ulp.o: %.ulp.pS
$(summary) ULP_AS $(notdir $@)
$(ULP_AS) -al=$(patsubst %.ulp.o,%.ulp.lst,$@) -o $@ $<
# Link object files and generate map file
$(ULP_ELF): $(ULP_OBJECTS) $(ULP_LD_SCRIPT)
$(summary) ULP_LD $(notdir $@)
$(ULP_LD) -o $@ -A elf32-esp32ulp -Map=$(ULP_MAP) -T $(ULP_LD_SCRIPT) $<
# Dump the list of global symbols in a convenient format.
$(ULP_SYM): $(ULP_ELF)
$(ULP_NM) -g -f posix $< > $@
# Dump the binary for inclusion into the project
$(COMPONENT_BUILD_DIR)/$(ULP_BIN): $(ULP_ELF)
$(summary) ULP_BIN $(notdir $@)
$(ULP_OBJCOPY) -O binary $< $@
# Left and right side of the rule are the same, but the right side
# is given as an absolute path.
# (Make can not resolve such things automatically)
$(ULP_EXPORTS_HEADER): $(COMPONENT_BUILD_DIR)/$(ULP_EXPORTS_HEADER)
# Artificial intermediate target to trigger generation of .h and .ld files.
.INTERMEDIATE: $(COMPONENT_NAME)_ulp_mapgen_intermediate
$(COMPONENT_BUILD_DIR)/$(ULP_EXPORTS_HEADER)\
$(COMPONENT_BUILD_DIR)/$(ULP_EXPORTS_LD): $(COMPONENT_NAME)_ulp_mapgen_intermediate
# Convert the symbols list into a header file and linker export script.
$(COMPONENT_NAME)_ulp_mapgen_intermediate: $(ULP_SYM)
$(summary) ULP_MAPGEN $(notdir $<)
$(ULP_MAP_GEN) -s $(ULP_SYM) -o $(ULP_EXPORTS_LD:.ld=)
# Building the component separately from the project should result in
# ULP files being built.
build: $(COMPONENT_BUILD_DIR)/$(ULP_EXPORTS_HEADER) \
$(COMPONENT_BUILD_DIR)/$(ULP_EXPORTS_LD) \
$(COMPONENT_BUILD_DIR)/$(ULP_BIN)
# Objects listed as being dependent on $(ULP_EXPORTS_HEADER) must also
# depend on $(ULP_SYM), to order build steps correctly.
$(ULP_EXP_DEP_OBJECTS) : $(ULP_EXPORTS_HEADER) $(ULP_SYM)
# Finally, set all the variables processed by the build system.
COMPONENT_EXTRA_CLEAN := $(ULP_OBJECTS) \
$(ULP_LD_SCRIPT) \
$(ULP_PREPROCESSED) \
$(ULP_ELF) $(ULP_BIN) \
$(ULP_MAP) $(ULP_SYM) \
$(ULP_EXPORTS_LD) \
$(ULP_EXPORTS_HEADER) \
$(ULP_DEP) \
$(ULP_LISTINGS)
COMPONENT_EMBED_FILES := $(COMPONENT_BUILD_DIR)/$(ULP_BIN)
COMPONENT_ADD_LDFLAGS := -l$(COMPONENT_NAME) -T $(COMPONENT_BUILD_DIR)/$(ULP_EXPORTS_LD)
COMPONENT_EXTRA_INCLUDES := $(COMPONENT_BUILD_DIR)

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python
# esp32ulp_mapgen utility converts a symbol list provided by nm into an export script
# for the linker and a header file.
#
# Copyright (c) 2016-2017 Espressif Systems (Shanghai) PTE LTD.
# Distributed under the terms of Apache License v2.0 found in the top-level LICENSE file.
from optparse import OptionParser
BASE_ADDR = 0x50000000;
def gen_ld_h_from_sym(f_sym, f_ld, f_h):
f_ld.write("/* Variable definitions for ESP32ULP linker\n");
f_ld.write(" * This file is generated automatically by esp32ulp_mapgen.py utility.\n");
f_ld.write(" */\n\n");
f_h.write("// Variable definitions for ESP32ULP\n");
f_h.write("// This file is generated automatically by esp32ulp_mapgen.py utility\n\n");
f_h.write("#pragma once\n\n");
for line in f_sym:
name, _, addr_str = line.split()
addr = int(addr_str, 16) + BASE_ADDR;
f_h.write("extern uint32_t ulp_{0};\n".format(name));
f_ld.write("PROVIDE ( ulp_{0} = 0x{1:08x} );\n".format(name, addr))
def main():
description = ( "This application generates .h and .ld files for symbols defined in input file. "
"The input symbols file can be generated using nm utility like this: "
"esp32-ulp-nm -g -f posix <elf_file> > <symbols_file>" );
parser = OptionParser(description=description)
parser.add_option("-s", "--symfile", dest="symfile",
help="symbols file name", metavar="SYMFILE")
parser.add_option("-o", "--outputfile", dest="outputfile",
help="destination .h and .ld files name prefix", metavar="OUTFILE")
(options, args) = parser.parse_args()
if options.symfile is None:
parser.print_help()
return 1
if options.outputfile is None:
parser.print_help()
return 1
with open(options.outputfile + ".h", 'w') as f_h, \
open(options.outputfile + ".ld", 'w') as f_ld, \
open(options.symfile) as f_sym: \
gen_ld_h_from_sym(f_sym, f_ld, f_h)
return 0
if __name__ == "__main__":
exit(main());

View File

@ -847,6 +847,34 @@ static inline uint32_t SOC_REG_TO_ULP_PERIPH_SEL(uint32_t reg) {
*/
esp_err_t ulp_process_macros_and_load(uint32_t load_addr, const ulp_insn_t* program, size_t* psize);
/**
* @brief Load ULP program binary into RTC memory
*
* ULP program binary should have the following format (all values little-endian):
*
* 1. MAGIC, (value 0x00706c75, 4 bytes)
* 2. TEXT_OFFSET, offset of .text section from binary start (2 bytes)
* 3. TEXT_SIZE, size of .text section (2 bytes)
* 4. DATA_SIZE, size of .data section (2 bytes)
* 5. BSS_SIZE, size of .bss section (2 bytes)
* 6. (TEXT_OFFSET - 16) bytes of arbitrary data (will not be loaded into RTC memory)
* 7. .text section
* 8. .data section
*
* Linker script in components/ulp/ld/esp32.ulp.ld produces ELF files which
* correspond to this format. This linker script produces binaries with load_addr == 0.
*
* @param load_addr address where the program should be loaded, expressed in 32-bit words
* @param program_binary pointer to program binary
* @param program_size size of the program binary
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if load_addr is out of range
* - ESP_ERR_INVALID_SIZE if program_size doesn't match (TEXT_OFFSET + TEXT_SIZE + DATA_SIZE)
* - ESP_ERR_NOT_SUPPORTED if the magic number is incorrect
*/
esp_err_t ulp_load_binary(uint32_t load_addr, const uint8_t* program_binary, size_t program_size);
/**
* @brief Run the program loaded into RTC memory
* @param entry_point entry point, expressed in 32-bit words

View File

@ -0,0 +1,35 @@
#include "sdkconfig.h"
#define ULP_BIN_MAGIC 0x00706c75
#define HEADER_SIZE 12
MEMORY
{
ram(RW) : ORIGIN = 0, LENGTH = CONFIG_ULP_COPROC_RESERVE_MEM
}
SECTIONS
{
.text : AT(HEADER_SIZE)
{
*(.text)
} >ram
.data :
{
. = ALIGN(4);
*(.data)
} >ram
.bss :
{
. = ALIGN(4);
*(.bss)
} >ram
.header : AT(0)
{
LONG(ULP_BIN_MAGIC)
SHORT(LOADADDR(.text))
SHORT(SIZEOF(.text))
SHORT(SIZEOF(.data))
SHORT(SIZEOF(.bss))
}
}

View File

@ -27,239 +27,17 @@
#include "sdkconfig.h"
static const char* TAG = "ulp";
typedef struct {
uint32_t label : 16;
uint32_t addr : 11;
uint32_t unused : 1;
uint32_t type : 4;
} reloc_info_t;
uint32_t magic;
uint16_t text_offset;
uint16_t text_size;
uint16_t data_size;
uint16_t bss_size;
} ulp_binary_header_t;
#define RELOC_TYPE_LABEL 0
#define RELOC_TYPE_BRANCH 1
#define ULP_BINARY_MAGIC_ESP32 (0x00706c75)
/* This record means: there is a label at address
* insn_addr, with number label_num.
*/
#define RELOC_INFO_LABEL(label_num, insn_addr) (reloc_info_t) { \
.label = label_num, \
.addr = insn_addr, \
.unused = 0, \
.type = RELOC_TYPE_LABEL }
/* This record means: there is a branch instruction at
* insn_addr, it needs to be changed to point to address
* of label label_num.
*/
#define RELOC_INFO_BRANCH(label_num, insn_addr) (reloc_info_t) { \
.label = label_num, \
.addr = insn_addr, \
.unused = 0, \
.type = RELOC_TYPE_BRANCH }
/* Processing branch and label macros involves four steps:
*
* 1. Iterate over program and count all instructions
* with "macro" opcode. Allocate relocations array
* with number of entries equal to number of macro
* instructions.
*
* 2. Remove all fake instructions with "macro" opcode
* and record their locations into relocations array.
* Removal is done using two pointers. Instructions
* are read from read_ptr, and written to write_ptr.
* When a macro instruction is encountered,
* its contents are recorded into the appropriate
* table, and then read_ptr is advanced again.
* When a real instruction is encountered, it is
* read via read_ptr and written to write_ptr.
* In the end, all macro instructions are removed,
* size of the program (expressed in words) is
* reduced by the total number of macro instructions
* which were present.
*
* 3. Sort relocations array by label number, and then
* by type ("label" or "branch") if label numbers
* match. This is done to simplify lookup on the next
* step.
*
* 4. Iterate over entries of relocations table.
* For each label number, label entry comes first
* because the array was sorted at the previous step.
* Label address is recorded, and all subsequent
* "branch" entries which point to the same label number
* are processed. For each branch entry, correct offset
* or absolute address is calculated, depending on branch
* type, and written into the appropriate field of
* the instruction.
*
*/
static esp_err_t do_single_reloc(ulp_insn_t* program, uint32_t load_addr,
reloc_info_t label_info, reloc_info_t branch_info)
{
size_t insn_offset = branch_info.addr - load_addr;
ulp_insn_t* insn = &program[insn_offset];
// B and BX have the same layout of opcode/sub_opcode fields,
// and share the same opcode
assert(insn->b.opcode == OPCODE_BRANCH
&& "branch macro was applied to a non-branch instruction");
switch (insn->b.sub_opcode) {
case SUB_OPCODE_B: {
int32_t offset = ((int32_t) label_info.addr) - ((int32_t) branch_info.addr);
uint32_t abs_offset = abs(offset);
uint32_t sign = (offset >= 0) ? 0 : 1;
if (abs_offset > 127) {
ESP_LOGW(TAG, "target out of range: branch from %x to %x",
branch_info.addr, label_info.addr);
return ESP_ERR_ULP_BRANCH_OUT_OF_RANGE;
}
insn->b.offset = abs_offset;
insn->b.sign = sign;
break;
}
case SUB_OPCODE_BX: {
assert(insn->bx.reg == 0 &&
"relocation applied to a jump with offset in register");
insn->bx.addr = label_info.addr;
break;
}
default:
assert(false && "unexpected sub-opcode");
}
return ESP_OK;
}
esp_err_t ulp_process_macros_and_load(uint32_t load_addr, const ulp_insn_t* program, size_t* psize)
{
const ulp_insn_t* read_ptr = program;
const ulp_insn_t* end = program + *psize;
size_t macro_count = 0;
// step 1: calculate number of macros
while (read_ptr < end) {
ulp_insn_t r_insn = *read_ptr;
if (r_insn.macro.opcode == OPCODE_MACRO) {
++macro_count;
}
++read_ptr;
}
size_t real_program_size = *psize - macro_count;
const size_t ulp_mem_end = CONFIG_ULP_COPROC_RESERVE_MEM / sizeof(ulp_insn_t);
if (load_addr > ulp_mem_end) {
ESP_LOGW(TAG, "invalid load address %x, max is %x",
load_addr, ulp_mem_end);
return ESP_ERR_ULP_INVALID_LOAD_ADDR;
}
if (real_program_size + load_addr > ulp_mem_end) {
ESP_LOGE(TAG, "program too big: %d words, max is %d words",
real_program_size, ulp_mem_end);
return ESP_ERR_ULP_SIZE_TOO_BIG;
}
// If no macros found, copy the program and return.
if (macro_count == 0) {
memcpy(((ulp_insn_t*) RTC_SLOW_MEM) + load_addr, program, *psize * sizeof(ulp_insn_t));
return ESP_OK;
}
reloc_info_t* reloc_info =
(reloc_info_t*) malloc(sizeof(reloc_info_t) * macro_count);
if (reloc_info == NULL) {
return ESP_ERR_NO_MEM;
}
// step 2: record macros into reloc_info array
// and remove them from then program
read_ptr = program;
ulp_insn_t* output_program = ((ulp_insn_t*) RTC_SLOW_MEM) + load_addr;
ulp_insn_t* write_ptr = output_program;
uint32_t cur_insn_addr = load_addr;
reloc_info_t* cur_reloc = reloc_info;
while (read_ptr < end) {
ulp_insn_t r_insn = *read_ptr;
if (r_insn.macro.opcode == OPCODE_MACRO) {
switch(r_insn.macro.sub_opcode) {
case SUB_OPCODE_MACRO_LABEL:
*cur_reloc = RELOC_INFO_LABEL(r_insn.macro.label,
cur_insn_addr);
break;
case SUB_OPCODE_MACRO_BRANCH:
*cur_reloc = RELOC_INFO_BRANCH(r_insn.macro.label,
cur_insn_addr);
break;
default:
assert(0 && "invalid sub_opcode for macro insn");
}
++read_ptr;
assert(read_ptr != end && "program can not end with macro insn");
++cur_reloc;
} else {
// normal instruction (not a macro)
*write_ptr = *read_ptr;
++read_ptr;
++write_ptr;
++cur_insn_addr;
}
}
// step 3: sort relocations array
int reloc_sort_func(const void* p_lhs, const void* p_rhs) {
const reloc_info_t lhs = *(const reloc_info_t*) p_lhs;
const reloc_info_t rhs = *(const reloc_info_t*) p_rhs;
if (lhs.label < rhs.label) {
return -1;
} else if (lhs.label > rhs.label) {
return 1;
}
// label numbers are equal
if (lhs.type < rhs.type) {
return -1;
} else if (lhs.type > rhs.type) {
return 1;
}
// both label number and type are equal
return 0;
}
qsort(reloc_info, macro_count, sizeof(reloc_info_t),
reloc_sort_func);
// step 4: walk relocations array and fix instructions
reloc_info_t* reloc_end = reloc_info + macro_count;
cur_reloc = reloc_info;
while(cur_reloc < reloc_end) {
reloc_info_t label_info = *cur_reloc;
assert(label_info.type == RELOC_TYPE_LABEL);
++cur_reloc;
while (cur_reloc < reloc_end) {
if (cur_reloc->type == RELOC_TYPE_LABEL) {
if(cur_reloc->label == label_info.label) {
ESP_LOGE(TAG, "duplicate label definition: %d",
label_info.label);
free(reloc_info);
return ESP_ERR_ULP_DUPLICATE_LABEL;
}
break;
}
if (cur_reloc->label != label_info.label) {
ESP_LOGE(TAG, "branch to an inexistent label: %d",
cur_reloc->label);
free(reloc_info);
return ESP_ERR_ULP_UNDEFINED_LABEL;
}
esp_err_t rc = do_single_reloc(output_program, load_addr,
label_info, *cur_reloc);
if (rc != ESP_OK) {
free(reloc_info);
return rc;
}
++cur_reloc;
}
}
free(reloc_info);
*psize = real_program_size;
return ESP_OK;
}
static const char* TAG = "ulp";
esp_err_t ulp_run(uint32_t entry_point)
{
@ -279,3 +57,46 @@ esp_err_t ulp_run(uint32_t entry_point)
SET_PERI_REG_MASK(RTC_CNTL_STATE0_REG, RTC_CNTL_ULP_CP_SLP_TIMER_EN);
return ESP_OK;
}
esp_err_t ulp_load_binary(uint32_t load_addr, const uint8_t* program_binary, size_t program_size)
{
size_t program_size_bytes = program_size * sizeof(uint32_t);
size_t load_addr_bytes = load_addr * sizeof(uint32_t);
if (program_size_bytes < sizeof(ulp_binary_header_t)) {
return ESP_ERR_INVALID_SIZE;
}
if (load_addr_bytes > CONFIG_ULP_COPROC_RESERVE_MEM) {
return ESP_ERR_INVALID_ARG;
}
if (load_addr_bytes + program_size_bytes > CONFIG_ULP_COPROC_RESERVE_MEM) {
return ESP_ERR_INVALID_SIZE;
}
// Make a copy of a header in case program_binary isn't aligned
ulp_binary_header_t header;
memcpy(&header, program_binary, sizeof(header));
if (header.magic != ULP_BINARY_MAGIC_ESP32) {
return ESP_ERR_NOT_SUPPORTED;
}
size_t total_size = (size_t) header.text_offset + (size_t) header.text_size +
(size_t) header.data_size;
ESP_LOGD(TAG, "program_size_bytes: %d total_size: %d offset: %d .text: %d, .data: %d, .bss: %d",
program_size_bytes, total_size, header.text_offset,
header.text_size, header.data_size, header.bss_size);
if (total_size != program_size_bytes) {
return ESP_ERR_INVALID_SIZE;
}
size_t text_data_size = header.text_size + header.data_size;
uint8_t* base = (uint8_t*) RTC_SLOW_MEM;
memcpy(base + load_addr_bytes, program_binary + header.text_offset, text_data_size);
memset(base + load_addr_bytes + text_data_size, 0, header.bss_size);
return ESP_OK;
}

262
components/ulp/ulp_macro.c Normal file
View File

@ -0,0 +1,262 @@
// Copyright 2010-2016 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "esp_attr.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp32/ulp.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/sens_reg.h"
#include "sdkconfig.h"
static const char* TAG = "ulp";
typedef struct {
uint32_t label : 16;
uint32_t addr : 11;
uint32_t unused : 1;
uint32_t type : 4;
} reloc_info_t;
#define RELOC_TYPE_LABEL 0
#define RELOC_TYPE_BRANCH 1
/* This record means: there is a label at address
* insn_addr, with number label_num.
*/
#define RELOC_INFO_LABEL(label_num, insn_addr) (reloc_info_t) { \
.label = label_num, \
.addr = insn_addr, \
.unused = 0, \
.type = RELOC_TYPE_LABEL }
/* This record means: there is a branch instruction at
* insn_addr, it needs to be changed to point to address
* of label label_num.
*/
#define RELOC_INFO_BRANCH(label_num, insn_addr) (reloc_info_t) { \
.label = label_num, \
.addr = insn_addr, \
.unused = 0, \
.type = RELOC_TYPE_BRANCH }
/* Processing branch and label macros involves four steps:
*
* 1. Iterate over program and count all instructions
* with "macro" opcode. Allocate relocations array
* with number of entries equal to number of macro
* instructions.
*
* 2. Remove all fake instructions with "macro" opcode
* and record their locations into relocations array.
* Removal is done using two pointers. Instructions
* are read from read_ptr, and written to write_ptr.
* When a macro instruction is encountered,
* its contents are recorded into the appropriate
* table, and then read_ptr is advanced again.
* When a real instruction is encountered, it is
* read via read_ptr and written to write_ptr.
* In the end, all macro instructions are removed,
* size of the program (expressed in words) is
* reduced by the total number of macro instructions
* which were present.
*
* 3. Sort relocations array by label number, and then
* by type ("label" or "branch") if label numbers
* match. This is done to simplify lookup on the next
* step.
*
* 4. Iterate over entries of relocations table.
* For each label number, label entry comes first
* because the array was sorted at the previous step.
* Label address is recorded, and all subsequent
* "branch" entries which point to the same label number
* are processed. For each branch entry, correct offset
* or absolute address is calculated, depending on branch
* type, and written into the appropriate field of
* the instruction.
*
*/
static esp_err_t do_single_reloc(ulp_insn_t* program, uint32_t load_addr,
reloc_info_t label_info, reloc_info_t branch_info)
{
size_t insn_offset = branch_info.addr - load_addr;
ulp_insn_t* insn = &program[insn_offset];
// B and BX have the same layout of opcode/sub_opcode fields,
// and share the same opcode
assert(insn->b.opcode == OPCODE_BRANCH
&& "branch macro was applied to a non-branch instruction");
switch (insn->b.sub_opcode) {
case SUB_OPCODE_B: {
int32_t offset = ((int32_t) label_info.addr) - ((int32_t) branch_info.addr);
uint32_t abs_offset = abs(offset);
uint32_t sign = (offset >= 0) ? 0 : 1;
if (abs_offset > 127) {
ESP_LOGW(TAG, "target out of range: branch from %x to %x",
branch_info.addr, label_info.addr);
return ESP_ERR_ULP_BRANCH_OUT_OF_RANGE;
}
insn->b.offset = abs_offset;
insn->b.sign = sign;
break;
}
case SUB_OPCODE_BX: {
assert(insn->bx.reg == 0 &&
"relocation applied to a jump with offset in register");
insn->bx.addr = label_info.addr;
break;
}
default:
assert(false && "unexpected sub-opcode");
}
return ESP_OK;
}
esp_err_t ulp_process_macros_and_load(uint32_t load_addr, const ulp_insn_t* program, size_t* psize)
{
const ulp_insn_t* read_ptr = program;
const ulp_insn_t* end = program + *psize;
size_t macro_count = 0;
// step 1: calculate number of macros
while (read_ptr < end) {
ulp_insn_t r_insn = *read_ptr;
if (r_insn.macro.opcode == OPCODE_MACRO) {
++macro_count;
}
++read_ptr;
}
size_t real_program_size = *psize - macro_count;
const size_t ulp_mem_end = CONFIG_ULP_COPROC_RESERVE_MEM / sizeof(ulp_insn_t);
if (load_addr > ulp_mem_end) {
ESP_LOGW(TAG, "invalid load address %x, max is %x",
load_addr, ulp_mem_end);
return ESP_ERR_ULP_INVALID_LOAD_ADDR;
}
if (real_program_size + load_addr > ulp_mem_end) {
ESP_LOGE(TAG, "program too big: %d words, max is %d words",
real_program_size, ulp_mem_end);
return ESP_ERR_ULP_SIZE_TOO_BIG;
}
// If no macros found, copy the program and return.
if (macro_count == 0) {
memcpy(((ulp_insn_t*) RTC_SLOW_MEM) + load_addr, program, *psize * sizeof(ulp_insn_t));
return ESP_OK;
}
reloc_info_t* reloc_info =
(reloc_info_t*) malloc(sizeof(reloc_info_t) * macro_count);
if (reloc_info == NULL) {
return ESP_ERR_NO_MEM;
}
// step 2: record macros into reloc_info array
// and remove them from then program
read_ptr = program;
ulp_insn_t* output_program = ((ulp_insn_t*) RTC_SLOW_MEM) + load_addr;
ulp_insn_t* write_ptr = output_program;
uint32_t cur_insn_addr = load_addr;
reloc_info_t* cur_reloc = reloc_info;
while (read_ptr < end) {
ulp_insn_t r_insn = *read_ptr;
if (r_insn.macro.opcode == OPCODE_MACRO) {
switch(r_insn.macro.sub_opcode) {
case SUB_OPCODE_MACRO_LABEL:
*cur_reloc = RELOC_INFO_LABEL(r_insn.macro.label,
cur_insn_addr);
break;
case SUB_OPCODE_MACRO_BRANCH:
*cur_reloc = RELOC_INFO_BRANCH(r_insn.macro.label,
cur_insn_addr);
break;
default:
assert(0 && "invalid sub_opcode for macro insn");
}
++read_ptr;
assert(read_ptr != end && "program can not end with macro insn");
++cur_reloc;
} else {
// normal instruction (not a macro)
*write_ptr = *read_ptr;
++read_ptr;
++write_ptr;
++cur_insn_addr;
}
}
// step 3: sort relocations array
int reloc_sort_func(const void* p_lhs, const void* p_rhs) {
const reloc_info_t lhs = *(const reloc_info_t*) p_lhs;
const reloc_info_t rhs = *(const reloc_info_t*) p_rhs;
if (lhs.label < rhs.label) {
return -1;
} else if (lhs.label > rhs.label) {
return 1;
}
// label numbers are equal
if (lhs.type < rhs.type) {
return -1;
} else if (lhs.type > rhs.type) {
return 1;
}
// both label number and type are equal
return 0;
}
qsort(reloc_info, macro_count, sizeof(reloc_info_t),
reloc_sort_func);
// step 4: walk relocations array and fix instructions
reloc_info_t* reloc_end = reloc_info + macro_count;
cur_reloc = reloc_info;
while(cur_reloc < reloc_end) {
reloc_info_t label_info = *cur_reloc;
assert(label_info.type == RELOC_TYPE_LABEL);
++cur_reloc;
while (cur_reloc < reloc_end) {
if (cur_reloc->type == RELOC_TYPE_LABEL) {
if(cur_reloc->label == label_info.label) {
ESP_LOGE(TAG, "duplicate label definition: %d",
label_info.label);
free(reloc_info);
return ESP_ERR_ULP_DUPLICATE_LABEL;
}
break;
}
if (cur_reloc->label != label_info.label) {
ESP_LOGE(TAG, "branch to an inexistent label: %d",
cur_reloc->label);
free(reloc_info);
return ESP_ERR_ULP_UNDEFINED_LABEL;
}
esp_err_t rc = do_single_reloc(output_program, load_addr,
label_info, *cur_reloc);
if (rc != ESP_OK) {
free(reloc_info);
return rc;
}
++cur_reloc;
}
}
free(reloc_info);
*psize = real_program_size;
return ESP_OK;
}

View File

@ -1 +1,161 @@
.. include:: ../components/ulp/README.rst
ULP coprocessor programming
===========================
.. toctree::
:maxdepth: 1
Instruction set reference <ulp_instruction_set>
Programming using macros (legacy) <ulp_macros>
ULP (Ultra Low Power) coprocessor is a simple FSM which is designed to perform measurements using ADC, temperature sensor, and external I2C sensors, while main processors are in deep sleep mode. ULP coprocessor can access RTC_SLOW_MEM memory region, and registers in RTC_CNTL, RTC_IO, and SARADC peripherals. ULP coprocessor uses fixed-width 32-bit instructions, 32-bit memory addressing, and has 4 general purpose 16-bit registers.
Installing the toolchain
------------------------
ULP coprocessor code is written in assembly and compiled using the `binutils-esp32ulp toolchain`_.
1. Download the toolchain using the links listed on this page:
https://github.com/espressif/binutils-esp32ulp/wiki#downloads
2. Extract the toolchain into a directory, and add the path to the ``bin/`` directory of the toolchain to the ``PATH`` environment variable.
Compiling ULP code
------------------
To compile ULP code as part of a component, the following steps must be taken:
1. ULP code, written in assembly, must be added to one or more files with `.S` extension. These files must be placed into a separate directory inside component directory, for instance `ulp/`.
.. note: This directory should not be added to the ``COMPONENT_SRCDIRS`` environment variable. The logic behind this is that the ESP-IDF build system will compile files found in ``COMPONENT_SRCDIRS`` based on their extensions. For ``.S`` files, ``xtensa-esp32-elf-as`` assembler is used. This is not desirable for ULP assembly files, so the easiest way to achieve the distinction is by placing ULP assembly files into a separate directory.
2. Modify the component makefile, adding the following::
ULP_APP_NAME ?= ulp_$(COMPONENT_NAME)
ULP_S_SOURCES = $(COMPONENT_PATH)/ulp/ulp_source_file.S
ULP_EXP_DEP_OBJECTS := main.o
include $(IDF_PATH)/components/ulp/component_ulp_common.mk
Here is each line explained:
ULP_APP_NAME
Name of the generated ULP application, without an extension. This name is used for build products of the ULP application: ELF file, map file, binary file, generated header file, and generated linker export file.
ULP_S_SOURCES
List of assembly files to be passed to the ULP assembler. These must be absolute paths, i.e. start with ``$(COMPONENT_PATH)``. Consider using ``$(addprefix)`` function if more than one file needs to be listed. Paths are relative to component build directory, so prefixing them is not necessary.
ULP_EXP_DEP_OBJECTS
List of object files names within the component which include the generated header file. This list is needed to build the dependencies correctly and ensure that the generated header file is created before any of these files are compiled. See section below explaining the concept of generated header files for ULP applications.
include $(IDF_PATH)/components/ulp/component_ulp_common.mk
Includes common definitions of ULP build steps. Defines build targets for ULP object files, ELF file, binary file, etc.
3. Build the application as usual (e.g. `make app`)
Inside, the build system will take the following steps to build ULP program:
1. **Run each assembly file (foo.S) through C preprocessor.** This step generates the preprocessed assembly files (foo.ulp.pS) in the component build directory. This step also generates dependency files (foo.ulp.d).
2. **Run preprocessed assembly sources through assembler.** This produces objects (foo.ulp.o) and listing (foo.ulp.lst) files. Listing files are generated for debugging purposes and are not used at later stages of build process.
3. **Run linker script template through C preprocessor.** The template is located in components/ulp/ld directory.
4. **Link object files into an output ELF file** (ulp_app_name.elf). Map file (ulp_app_name.map) generated at this stage may be useful for debugging purposes.
5. **Dump contents of the ELF file into binary** (ulp_app_name.bin) for embedding into the application.
6. **Generate list of global symbols** (ulp_app_name.sym) in the ELF file using esp32ulp-elf-nm.
7. **Create LD export script and header file** (ulp_app_name.ld and ulp_app_name.h) containing the symbols from ulp_app_name.sym. This is done using esp32ulp_mapgen.py utility.
8. **Add the generated binary to the list of binary files** to be emedded into the application.
Accessing ULP program variables
-------------------------------
Global symbols defined in the ULP program may be used inside the main program.
For example, ULP program may define a variable ``measurement_count`` which will define the number of ADC measurements the program needs to make before waking up the chip from deep sleep::
.global measurement_count
measurement_count: .long 0
/* later, use measurement_count */
move r3, measurement_count
ld r3, r3, 0
Main program needs to initialize this variable before ULP program is started. Build system makes this possible by generating a ``$(ULP_APP_NAME).h`` and ``$(ULP_APP_NAME).ld`` files which define global symbols present in the ULP program. This files include each global symbol defined in the ULP program, prefixed with ``ulp_``.
The header file contains declaration of the symbol::
extern uint32_t ulp_measurement_count;
Note that all symbols (variables, arrays, functions) are declared as ``uint32_t``. For functions and arrays, take address of the symbol and cast to the appropriate type.
The generated linker script file defines locations of symbols in RTC_SLOW_MEM::
PROVIDE ( ulp_measurement_count = 0x50000060 );
To access ULP program variables from the main program, include the generated header file and use variables as one normally would::
#include "ulp_app_name.h"
// later
void init_ulp_vars() {
ulp_measurement_count = 64;
}
Note that ULP program can only use lower 16 bits of each 32-bit word in RTC memory, because the registers are 16-bit, and there is no instruction to load from high part of the word.
Likewise, ULP store instruction writes register value into the lower 16 bit part of the 32-bit word. Upper 16 bits are written with a value which depends on the address of the store instruction, so when reading variables written by the ULP, main application needs to mask upper 16 bits, e.g.::
printf("Last measurement value: %d\n", ulp_last_measurement & UINT16_MAX);
Starting the ULP program
------------------------
To run a ULP program, main application needs to load the ULP program into RTC memory using ``ulp_load_binary`` function, and then start it using ``ulp_run`` function.
Note that "Enable Ultra Low Power (ULP) Coprocessor" option must be enabled in menuconfig in order to reserve memory for the ULP. "RTC slow memory reserved for coprocessor" option must be set to a value sufficient to store ULP code and data. If the application components contain multiple ULP programs, then the size of the RTC memory must be sufficient to hold the largest one.
Each ULP program is embedded into the ESP-IDF application as a binary blob. Application can reference this blob and load it in the following way (suppose ULP_APP_NAME was defined to ``ulp_app_name``::
extern const uint8_t bin_start[] asm("_binary_ulp_app_name_bin_start");
extern const uint8_t bin_end[] asm("_binary_ulp_app_name_bin_end");
void start_ulp_program() {
ESP_ERROR_CHECK( ulp_load_binary(
0 /* load address, set to 0 when using default linker scripts */,
bin_start,
(bin_end - bin_start) / sizeof(uint32_t)) );
}
.. doxygenfunction:: ulp_load_binary
Once the program is loaded into RTC memory, application can start it, passing the address of the entry point to ``ulp_run`` function::
ESP_ERROR_CHECK( ulp_run((&ulp_entry - RTC_SLOW_MEM) / sizeof(uint32_t)) );
.. doxygenfunction:: ulp_run
Declaration of the entry point symbol comes from the above mentioned generated header file, ``$(ULP_APP_NAME).h``. In assembly source of the ULP application, this symbol must be marked as ``.global``::
.global entry
entry:
/* code starts here */
ULP program flow
----------------
ULP coprocessor is started by a timer. The timer is started once ``ulp_run`` is called. The timer counts a number of RTC_SLOW_CLK ticks (by default, produced by an internal 150kHz RC oscillator). The number of ticks is set using ``SENS_ULP_CP_SLEEP_CYCx_REG`` registers (x = 0..4). When starting the ULP for the first time, ``SENS_ULP_CP_SLEEP_CYC0_REG`` will be used to obtain the number of timer ticks. Later the ULP program can select another ``SENS_ULP_CP_SLEEP_CYCx_REG`` register using ``sleep`` instruction.
Once the timer counts the number of ticks set by the selected ``SENS_ULP_CP_SLEEP_CYCx_REG`` register, ULP coprocessor powers up and starts running the program from the entry point set in the call to ``ulp_run``.
The program runs until it encounters a ``halt`` instruction or an illegal instruction. Once the program halts, ULP coprocessor powers down, and the timer is started again.
To disable the timer (effectively preventing the ULP program from running again), clear the ``RTC_CNTL_ULP_CP_SLP_TIMER_EN`` bit in the ``RTC_CNTL_STATE0_REG`` register. This can be done both from ULP code and from the main program.
.. _binutils-esp32ulp toolchain: https://github.com/espressif/binutils-esp32ulp

768
docs/ulp_instruction_set.rst Executable file
View File

@ -0,0 +1,768 @@
ULP coprocessor instruction set
+++++++++++++++++++++++++++++++
This document provides details about the instructions used by ESP32 ULP coprocessor assembler.
ULP coprocessor has 4 16-bit general purpose registers, labeled R0, R1, R2, R3. It also has an 8-bit counter register (stage_cnt) which can be used to implement loops. Stage count regiter is accessed using special instructions.
ULP coprocessor can access 8k bytes of RTC_SLOW_MEM memory region. Memory is addressed in 32-bit word units. It can also access peripheral registers in RTC_CNTL, RTC_IO, and SENS peripherals.
All instructions are 32-bit. Jump instructions, ALU instructions, peripheral register and memory access instructions are executed in 1 cycle. Instructions which work with peripherals (TSENS, ADC, I2C) take variable number of cycles, depending on peripheral operation.
The instruction syntax is case insensitive. Upper and lower case letters can be used and intermixed arbitrarily. This is true both for register names and instruction names.
Note about addressing
---------------------
ESP32 ULP coprocessor's JUMP, ST, LD instructions which take register as an argument (jump address, store/load base address) expect the argument to be expressed in 32-bit words.
Consider the following example program::
entry:
NOP
NOP
NOP
NOP
loop:
MOVE R1, loop
JUMP R1
When this program is assembled and linked, address of label ``loop`` will be equal to 16 (expressed in bytes). However `JUMP` instruction expects the address stored in register to be expressed in 32-bit words. To account for this common use case, assembler will convert the address of label `loop` from bytes to words, when generating ``MOVE`` instruction, so the code generated code will be equivalent to::
0000 NOP
0004 NOP
0008 NOP
000c NOP
0010 MOVE R1, 4
0014 JUMP R1
The other case is when the argument of ``MOVE`` instruction is not a label but a constant. In this case assembler will use the value as is, without any conversion::
.set val, 0x10
MOVE R1, val
In this case, value loaded into R1 will be ``0x10``.
Similar considerations apply to ``LD`` and ``ST`` instructions. Consider the following code::
.global array
array: .long 0
.long 0
.long 0
.long 0
MOVE R1, array
MOVE R2, 0x1234
ST R2, R1, 0 // write value of R2 into the first array element,
// i.e. array[0]
ST R2, R1, 4 // write value of R2 into the second array element
// (4 byte offset), i.e. array[1]
ADD R1, R1, 2 // this increments address by 2 words (8 bytes)
ST R2, R1, 0 // write value of R2 into the third array element,
// i.e. array[2]
**NOP** - no operation
----------------------
**Syntax:**
**NOP**
**Operands:**
None
**Description:**
No operation is performed. Only the PC is incremented.
**Example**::
1: NOP
**ADD** - Add to register
-------------------------
**Syntax:**
**ADD** *Rdst, Rsrc1, Rsrc2*
**ADD** *Rdst, Rsrc1, imm*
**Operands:**
- *Rdst* - Register R[0..3]
- *Rsrc1* - Register R[0..3]
- *Rsrc2* - Register R[0..3]
- *Imm* - 16-bit signed value
**Description:**
The instruction adds source register to another source register or to a 16-bit signed value and stores result to the destination register.
**Examples**::
1: ADD R1, R2, R3 //R1 = R2 + R3
2: Add R1, R2, 0x1234 //R1 = R2 + 0x1234
3: .set value1, 0x03 //constant value1=0x03
Add R1, R2, value1 //R1 = R2 + value1
4: .global label //declaration of variable label
Add R1, R2, label //R1 = R2 + label
...
label: nop //definition of variable label
**SUB** - Subtract from register
--------------------------------
**Syntax:**
**SUB** *Rdst, Rsrc1, Rsrc2*
**SUB** *Rdst, Rsrc1, imm*
**Operands:**
- *Rdst* - Register R[0..3]
- *Rsrc1* - Register R[0..3]
- *Rsrc2* - Register R[0..3]
- *Imm* - 16-bit signed value
**Description:**
The instruction subtracts the source register from another source register or subtracts 16-bit signed value from a source register, and stores result to the destination register.
**Examples:**::
1: SUB R1, R2, R3 //R1 = R2 - R3
2: sub R1, R2, 0x1234 //R1 = R2 - 0x1234
3: .set value1, 0x03 //constant value1=0x03
SUB R1, R2, value1 //R1 = R2 - value1
4: .global label //declaration of variable label
SUB R1, R2, label //R1 = R2 - label
....
label: nop //definition of variable label
**AND** - Logical AND of two operands
-------------------------------------
**Syntax:**
**AND** *Rdst, Rsrc1, Rsrc2*
**AND** *Rdst, Rsrc1, imm*
**Operands:**
- *Rdst* - Register R[0..3]
- *Rsrc1* - Register R[0..3]
- *Rsrc2* - Register R[0..3]
- *Imm* - 16-bit signed value
**Description:**
The instruction does logical AND of a source register and another source register or 16-bit signed value and stores result to the destination register.
**Example**::
1: AND R1, R2, R3 //R1 = R2 & R3
2: AND R1, R2, 0x1234 //R1 = R2 & 0x1234
3: .set value1, 0x03 //constant value1=0x03
AND R1, R2, value1 //R1 = R2 & value1
4: .global label //declaration of variable label
AND R1, R2, label //R1 = R2 & label
...
label: nop //definition of variable label
**OR** - Logical OR of two operands
-----------------------------------
**Syntax**
**OR** *Rdst, Rsrc1, Rsrc2*
**OR** *Rdst, Rsrc1, imm*
**Operands**
- *Rdst* - Register R[0..3]
- *Rsrc1* - Register R[0..3]
- *Rsrc2* - Register R[0..3]
- *Imm* - 16-bit signed value
**Description**
The instruction does logical OR of a source register and another source register or 16-bit signed value and stores result to the destination register.
**Examples**::
1: OR R1, R2, R3 //R1 = R2 \| R3
2: OR R1, R2, 0x1234 //R1 = R2 \| 0x1234
3: .set value1, 0x03 //constant value1=0x03
OR R1, R2, value1 //R1 = R2 \| value1
4: .global label //declaration of variable label
OR R1, R2, label //R1 = R2 \|label
...
label: nop //definition of variable label
**LSH** - Logical Shift Left
----------------------------
**Syntax**
**LSH** *Rdst, Rsrc1, Rsrc2*
**LSH** *Rdst, Rsrc1, imm*
**Operands**
- *Rdst* - Register R[0..3]
- *Rsrc1* - Register R[0..3]
- *Rsrc2* - Register R[0..3]
- *Imm* - 16-bit signed value
**Description**
The instruction does logical shift to left of source register to number of bits from another source register or 16-bit signed value and store result to the destination register.
**Examples**::
1: LSH R1, R2, R3 //R1 = R2 << R3
2: LSH R1, R2, 0x03 //R1 = R2 << 0x03
3: .set value1, 0x03 //constant value1=0x03
LSH R1, R2, value1 //R1 = R2 << value1
4: .global label //declaration of variable label
LSH R1, R2, label //R1 = R2 << label
...
label: nop //definition of variable label
**RSH** - Logical Shift Right
----------------------------
**Syntax**
**RSH** *Rdst, Rsrc1, Rsrc2*
**RSH** *Rdst, Rsrc1, imm*
**Operands**
*Rdst* - Register R[0..3]
*Rsrc1* - Register R[0..3]
*Rsrc2* - Register R[0..3]
*Imm* - 16-bit signed value
**Description**
The instruction does logical shift to right of source register to number of bits from another source register or 16-bit signed value and store result to the destination register.
**Examples**::
1: RSH R1, R2, R3 //R1 = R2 >> R3
2: RSH R1, R2, 0x03 //R1 = R2 >> 0x03
3: .set value1, 0x03 //constant value1=0x03
RSH R1, R2, value1 //R1 = R2 >> value1
4: .global label //declaration of variable label
RSH R1, R2, label //R1 = R2 >> label
label: nop //definition of variable label
**MOVE** Move to register
---------------------------
**Syntax**
**MOVE** *Rdst, Rsrc*
**MOVE** *Rdst, imm*
**Operands**
- *Rdst* Register R[0..3]
- *Rsrc* Register R[0..3]
- *Imm* 16-bit signed value
**Description**
The instruction move to destination register value from source register or 16-bit signed value.
Note that when a label is used as an immediate, the address of the label will be converted from bytes to words. This is because LD, ST, and JUMP instructions expect the address register value to be expressed in words rather than bytes. To avoid using an extra instruction
**Examples**::
1: MOVE R1, R2 //R1 = R2 >> R3
2: MOVE R1, 0x03 //R1 = R2 >> 0x03
3: .set value1, 0x03 //constant value1=0x03
MOVE R1, value1 //R1 = value1
4: .global label //declaration of label
MOVE R1, label //R1 = address_of(label) / 4
...
label: nop //definition of label
**ST** Store data to the memory
---------------------------------
**Syntax**
**ST** *Rsrc, Rdst, offset*
**Operands**
- *Rsrc* Register R[0..3], holds the 16-bit value to store
- *Rdst* Register R[0..3], address of the destination, in 32-bit words
- *Offset* 10-bit signed value, offset in bytes
**Description**
The instruction stores the 16-bit value of Rsrc to the lower half-word of memory with address Rdst+offset. The upper half-word is written with the current program counter (PC), expressed in words, shifted left by 5 bits::
Mem[Rdst + offset / 4]{31:0} = {PC[10:0], 5'b0, Rsrc[15:0]}
The application can use higher 16 bits to determine which instruction in the ULP program has written any particular word into memory.
**Examples**::
1: ST R1, R2, 0x12 //MEM[R2+0x12] = R1
2: .data //Data section definition
Addr1: .word 123 // Define label Addr1 16 bit
.set offs, 0x00 // Define constant offs
.text //Text section definition
MOVE R1, 1 // R1 = 1
MOVE R2, Addr1 // R2 = Addr1
ST R1, R2, offs // MEM[R2 + 0] = R1
// MEM[Addr1 + 0] will be 32'h600001
**LD** Load data from the memory
----------------------------------
**Syntax**
**LD** *Rdst, Rsrc, offset*
**Operands**
*Rdst* Register R[0..3], destination
*Rsrc* Register R[0..3], holds address of destination, in 32-bit words
*Offset* 10-bit signed value, offset in bytes
**Description**
The instruction loads lower 16-bit half-word from memory with address Rsrc+offset into the destination register Rdst::
Rdst[15:0] = Mem[Rsrc + offset / 4][15:0]
**Examples**::
1: LD R1, R2, 0x12 //R1 = MEM[R2+0x12]
2: .data //Data section definition
Addr1: .word 123 // Define label Addr1 16 bit
.set offs, 0x00 // Define constant offs
.text //Text section definition
MOVE R1, 1 // R1 = 1
MOVE R2, Addr1 // R2 = Addr1 / 4 (address of label is converted into words)
LD R1, R2, offs // R1 = MEM[R2 + 0]
// R1 will be 123
**JUMP** Jump to an absolute address
--------------------------------------
**Syntax**
**JUMP** *Rdst*
**JUMP** *ImmAddr*
**JUMP** *Rdst, Condition*
**JUMP** *ImmAddr, Condition*
**Operands**
- *Rdst* Register R[0..3] containing address to jump to (expressed in 32-bit words)
- *ImmAddr* 13 bits address (expressed in bytes), aligned to 4 bytes
- *Condition*:
- EQ jump if last ALU operation result was zero
- OV jump if last ALU has set overflow flag
**Description**
The instruction makes jump to the specified address. Jump can be either unconditional or based on an ALU flag.
**Examples**::
1: JUMP R1 // Jump to address in R1 (address in R1 is in 32-bit words)
2: JUMP 0x120, EQ // Jump to address 0x120 (in bytes) if ALU result is zero
3: JUMP label // Jump to label
...
label: nop // Definition of label
4: .global label // Declaration of global label
MOVE R1, label // R1 = label (value loaded into R1 is in words)
JUMP R1 // Jump to label
...
label: nop // Definition of label
**JUMPR** Jump to a relative offset (condition based on R0)
-------------------------------------------------------------
**Syntax**
**JUMPR** *Step, Threshold, Condition*
**Operands**
- *Step* relative shift from current position, in bytes
- *Threshold* threshold value for branch condition
- *Condition*:
- *GE* (greater or equal) jump if value in R0 >= threshold
- *LT* (less than) jump if value in R0 < threshold
**Description**
The instruction makes a jump to a relative address if condition is true. Condition is the result of comparison of R0 register value and the threshold value.
**Examples**::
1:pos: JUMPR 16, 20, GE // Jump to address (position + 16 bytes) if value in R0 >= 20
2: // Down counting loop using R0 register
MOVE R0, 16 // load 16 into R0
label: SUB R0, R0, 1 // R0--
NOP // do something
JUMPR label, 1, GE // jump to label if R0 >= 1
**JUMPS** Jump to a relative address (condition based on stage count)
-----------------------------------------------------------------------
**Syntax**
**JUMPS** *Step, Threshold, Condition*
**Operands**
- *Step* relative shift from current position, in bytes
- *Threshold* threshold value for branch condition
- *Condition*:
- *EQ* (equal) jump if value in stage_cnt == threshold
- *LT* (less than) jump if value in stage_cnt < threshold
- *GT* (greater than) jump if value in stage_cnt > threshold
**Description**
The instruction makes a jump to a relative address if condition is true. Condition is the result of comparison of count register value and threshold value.
**Examples**::
1:pos: JUMPS 16, 20, EQ // Jump to (position + 16 bytes) if stage_cnt == 20
2: // Up counting loop using stage count register
STAGE_RST // set stage_cnt to 0
label: STAGE_INC 1 // stage_cnt++
NOP // do something
JUMPS label, 16, LT // jump to label if stage_cnt < 16
**STAGE_RST** Reset stage count register
------------------------------------------
**Syntax**
**STAGE_RST**
**Operands**
No operands
**Description**
The instruction sets the stage count register to 0
**Examples**::
1: STAGE_RST // Reset stage count register
**STAGE_INC** Increment stage count register
----------------------------------------------
**Syntax**
**STAGE_INC** *Value*
**Operands**
- *Value* 8 bits value
**Description**
The instruction increments stage count register by given value.
**Examples**::
1: STAGE_INC 10 // stage_cnt += 10
2: // Up counting loop example:
STAGE_RST // set stage_cnt to 0
label: STAGE_INC 1 // stage_cnt++
NOP // do something
JUMPS label, 16, LT // jump to label if stage_cnt < 16
**STAGE_DEC** Decrement stage count register
----------------------------------------------
**Syntax**
**STAGE_DEC** *Value*
**Operands**
- *Value* 8 bits value
**Description**
The instruction decrements stage count register by given value.
**Examples**::
1: STAGE_DEC 10 // stage_cnt -= 10;
2: // Down counting loop exaple
STAGE_RST // set stage_cnt to 0
STAGE_INC 16 // increment stage_cnt to 16
label: STAGE_DEC 1 // stage_cnt--;
NOP // do something
JUMPS label, 0, GT // jump to label if stage_cnt > 0
**HALT** End the program
--------------------------
**Syntax**
**HALT**
**Operands**
No operands
**Description**
The instruction halt the processor to the power down mode
**Examples**::
1: HALT // Move chip to powerdown
**WAKE** wakeup the chip
--------------------------
**Syntax**
**WAKE**
**Operands**
No operands
**Description**
The instruction sends an interrupt from ULP to RTC controller.
- If the SoC is in deep sleep mode, and ULP wakeup is enabled, this causes the SoC to wake up.
- If the SoC is not in deep sleep mode, and ULP interrupt bit (RTC_CNTL_ULP_CP_INT_ENA) is set in RTC_CNTL_INT_ENA_REG register, RTC interrupt will be triggered.
**Examples**::
1: WAKE // Trigger wake up
REG_WR 0x006, 24, 24, 0 // Stop ULP timer (clear RTC_CNTL_ULP_CP_SLP_TIMER_EN)
HALT // Stop the ULP program
// After these instructions, SoC will wake up,
// and ULP will not run again until started by the main program.
**SLEEP** set ULP wakeup timer period
---------------------------------------
**Syntax**
**SLEEP** *sleep_reg*
**Operands**
- *sleep_reg* 0..4, selects one of ``SENS_ULP_CP_SLEEP_CYCx_REG`` registers.
**Description**
The instruction selects which of the ``SENS_ULP_CP_SLEEP_CYCx_REG`` (x = 0..4) register values is to be used by the ULP wakeup timer as wakeup period. By default, the value from ``SENS_ULP_CP_SLEEP_CYC0_REG`` is used.
**Examples**::
1: SLEEP 1 // Use period set in SENS_ULP_CP_SLEEP_CYC1_REG
2: .set sleep_reg, 4 // Set constant
SLEEP sleep_reg // Use period set in SENS_ULP_CP_SLEEP_CYC4_REG
**WAIT** wait some number of cycles
-------------------------------------
**Syntax**
**WAIT** *Cycles*
**Operands**
- *Cycles* number of cycles for wait
**Description**
The instruction delays for given number of cycles.
**Examples**::
1: WAIT 10 // Do nothing for 10 cycles
2: .set wait_cnt, 10 // Set a constant
WAIT wait_cnt // wait for 10 cycles
**TSENS** do measurement with temperature sensor
--------------------------------------------------
**Syntax**
- **TSENS** *Rdst, Wait_Delay*
**Operands**
- *Rdst* Destination Register R[0..3], result will be stored to this register
- *Wait_Delay* number of cycles used to perform the measurement
**Description**
The instruction performs measurement using TSENS and stores the result into a general purpose register.
**Examples**::
1: TSENS R1, 1000 // Measure temperature sensor for 1000 cycles,
// and store result to R1
**ADC** do measurement with ADC
---------------------------------
**Syntax**
**ADC** *Rdst, Sar_sel, Mux, Cycles*
**Operands**
- *Rdst* Destination Register R[0..3], result will be stored to this register
- *Sar_sel* selected ADC : 0=SARADC0, 1=SARADC1
- *Mux* - selected PAD, SARADC Pad[Mux+1] is enabled
- *Cycle* number of cycles used to perform measurement
**Description**
The instruction makes measurements from ADC.
**Examples**::
1: ADC R1, 0, 1, 100 // Measure value using ADC1 pad 2,
// for 100 cycles and move result to R1
**REG_RD** read from peripheral register
------------------------------------------
**Syntax**
**REG_RD** *Addr, High, Low*
**Operands**
- *Addr* register address, in 32-bit words
- *High* High part of R0
- *Low* Low part of R0
**Description**
The instruction reads up to 16 bits from a peripheral register into a general purpose register: ``R0 = REG[Addr][High:Low]``.
This instruction can access registers in RTC_CNTL, RTC_IO, and SENS peripherals. Address of the the register, as seen from the ULP,
can be calculated from the address of the same register on the DPORT bus as follows::
addr_ulp = (addr_dport - DR_REG_RTCCNTL_BASE) / 4
**Examples**::
1: REG_RD 0x120, 2, 0 // load 4 bits: R0 = {12'b0, REG[0x120][7:4]}
**REG_WR** write to peripheral register
-----------------------------------------
**Syntax**
**REG_WR** *Addr, High, Low, Data*
**Operands**
- *Addr* register address, in 32-bit words.
- *High* High part of R0
- *Low* Low part of R0
- *Data* value to write, 8 bits
**Description**
The instruction writes up to 8 bits from a general purpose register into a peripheral register. ``REG[Addr][High:Low] = data``
This instruction can access registers in RTC_CNTL, RTC_IO, and SENS peripherals. Address of the the register, as seen from the ULP,
can be calculated from the address of the same register on the DPORT bus as follows::
addr_ulp = (addr_dport - DR_REG_RTCCNTL_BASE) / 4
**Examples**::
1: REG_WR 0x120, 7, 0, 0x10 // set 8 bits: REG[0x120][7:0] = 0x10
Convenience macros for peripheral registers access
--------------------------------------------------
ULP source files are passed through C preprocessor before the assembler. This allows certain macros to be used to facilitate access to peripheral registers.
Some existing macros are defined in ``soc/soc_ulp.h`` header file. These macros allow access to the fields of peripheral registers by their names.
Peripheral registers names which can be used with these macros are the ones defined in ``soc/rtc_cntl_reg.h``, ``soc/rtc_io_reg.h``, and ``soc/sens_reg.h``.
READ_RTC_REG(rtc_reg, low_bit, bit_width)
Read up to 16 bits from rtc_reg[low_bit + bit_width - 1 : low_bit] into R0. For example::
#include "soc/soc_ulp.h"
#include "soc/rtc_cntl_reg.h"
/* Read 16 lower bits of RTC_CNTL_TIME0_REG into R0 */
READ_RTC_REG(RTC_CNTL_TIME0_REG, 0, 16)
READ_RTC_FIELD(rtc_reg, field)
Read from a field in rtc_reg into R0, up to 16 bits. For example::
#include "soc/soc_ulp.h"
#include "soc/sens_reg.h"
/* Read 8-bit SENS_TSENS_OUT field of SENS_SAR_SLAVE_ADDR3_REG into R0 */
READ_RTC_REG(SENS_SAR_SLAVE_ADDR3_REG, SENS_TSENS_OUT)
WRITE_RTC_REG(rtc_reg, low_bit, bit_width, value)
Write immediate value into rtc_reg[low_bit + bit_width - 1 : low_bit], bit_width <= 8. For example::
#include "soc/soc_ulp.h"
#include "soc/rtc_io_reg.h"
/* Set BIT(2) of RTC_GPIO_OUT_DATA_W1TS field in RTC_GPIO_OUT_W1TS_REG */
WRITE_RTC_REG(RTC_GPIO_OUT_W1TS_REG, RTC_GPIO_OUT_DATA_W1TS_S + 2, 1, 1)
WRITE_RTC_FIELD(rtc_reg, field, value)
Write immediate value into a field in rtc_reg, up to 8 bits. For example::
#include "soc/soc_ulp.h"
#include "soc/rtc_cntl_reg.h"
/* Set RTC_CNTL_ULP_CP_SLP_TIMER_EN field of RTC_CNTL_STATE0_REG to 0 */
READ_RTC_REG(RTC_CNTL_STATE0_REG, RTC_CNTL_ULP_CP_SLP_TIMER_EN, 0)

1
docs/ulp_macros.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../components/ulp/README.rst

View File

@ -0,0 +1,9 @@
#
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
# project subdirectory.
#
PROJECT_NAME := ulp-example
include $(IDF_PATH)/make/project.mk

View File

@ -0,0 +1,50 @@
# ULP Pulse Counting Example
This example demonstrates how to program the ULP coprocessor to count pulses on an IO while the main CPUs are either running some other code or are in deep sleep. See the README.md file in the upper level 'examples' directory for more information about examples.
ULP program written in assembly can be found in `ulp/pulse_cnt.S`. The build system assembles and links this program, converts it into binary format, and embeds it into the .rodata section of the ESP-IDF application.
At runtime, the main code running on the ESP32 (found in main.c) loads ULP program into the `RTC_SLOW_MEM` memory region using `ulp_load_binary` function. Main code configures the ULP program by setting up values of some variables and then starts it using `ulp_run`. Once the ULP program is started, it runs periodically, with the period set by the main program. The main program enables ULP wakeup source and puts the chip into deep sleep mode.
When the ULP program finds an edge in the input signal, it performs debouncing and increments the variable maintaining the total edge count. Once the edge count reaches certain value (set by the main program), ULP triggers wake up from deep sleep. Note that the ULP program keeps running and monitoring the input signal even when the SoC is woken up.
Upon wakeup, the main program saves total edge count into NVS and returns to deep sleep.
In this example the input signal is connected to GPIO0. Note that this pin was chosen because most development boards have a button connected to it, so the pulses to be counted can be generated by pressing the button. For real world applications this is not a good choice of a pin, because GPIO0 also acts as a bootstrapping pin. To change the pin number, check the ESP32 Chip Pin List document and adjust `gpio_num` and `ulp_io_number` variables in main.c.
## Example output
Note: GPIO15 is connected to GND to disable ROM bootloader output.
```
Not ULP wakeup, initializing ULP
Entering deep sleep
ULP wakeup, saving pulse count
Read pulse count from NVS: 384
Pulse count from ULP: 5
Wrote updated pulse count to NVS: 389
Entering deep sleep
ULP wakeup, saving pulse count
Read pulse count from NVS: 389
Pulse count from ULP: 5
Wrote updated pulse count to NVS: 394
Entering deep sleep
ULP wakeup, saving pulse count
Read pulse count from NVS: 394
Pulse count from ULP: 6
Wrote updated pulse count to NVS: 400
Entering deep sleep
ULP wakeup, saving pulse count
Read pulse count from NVS: 400
Pulse count from ULP: 5
Wrote updated pulse count to NVS: 405
Entering deep sleep
```
Note that in one case the pulse count captured by the ULP program is 6, even though the `edge_count_to_wake_up` variable is set to 10 by the main program. This shows that the ULP program keeps track of pulses while the main CPUs are starting up, so when pulses are sent rapidly it is possible to register more pulses between wake up and entry into app_main.

View File

@ -0,0 +1,25 @@
#
# ULP support additions to component makefile.
#
# 1. ULP_APP_NAME must be unique (if multiple components use ULP)
# Default value, override if necessary:
ULP_APP_NAME ?= ulp_$(COMPONENT_NAME)
#
# 2. Specify all assembly source files here.
# Files should be placed into a separate directory (in this case, ulp/),
# which should not be added to COMPONENT_SRCDIRS.
ULP_S_SOURCES = $(addprefix $(COMPONENT_PATH)/ulp/, \
pulse_cnt.S \
)
#
# 3. List all the component object files which include automatically
# generated ULP export file, $(ULP_APP_NAME).h:
ULP_EXP_DEP_OBJECTS := ulp_example_main.o
#
# 4. Include build rules for ULP program
include $(IDF_PATH)/components/ulp/component_ulp_common.mk
#
# End of ULP support additions to component makefile.
#

View File

@ -0,0 +1,135 @@
/* ULP Example: pulse counting
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
This file contains assembly code which runs on the ULP.
ULP wakes up to run this code at a certain period, determined by the values
in SENS_ULP_CP_SLEEP_CYCx_REG registers. On each wake up, the program checks
the input on GPIO0. If the value is different from the previous one, the
program "debounces" the input: on the next debounce_max_count wake ups,
it expects to see the same value of input.
If this condition holds true, the program increments edge_count and starts
waiting for input signal polarity to change again.
When the edge counter reaches certain value (set by the main program),
this program running triggers a wake up from deep sleep.
*/
/* ULP assembly files are passed through C preprocessor first, so include directives
and C macros may be used in these files
*/
#include "soc/rtc_cntl_reg.h"
#include "soc/rtc_io_reg.h"
#include "soc/soc_ulp.h"
/* Define variables, which go into .bss section (zero-initialized data) */
.bss
/* Next input signal edge expected: 0 (negative) or 1 (positive) */
.global next_edge
next_edge:
.long 0
/* Counter started when signal value changes.
Edge is "debounced" when the counter reaches zero. */
.global debounce_counter
debounce_counter:
.long 0
/* Value to which debounce_counter gets reset.
Set by the main program. */
.global debounce_max_count
debounce_max_count:
.long 0
/* Total number of signal edges acquired */
.global edge_count
edge_count:
.long 0
/* Number of edges to acquire before waking up the SoC.
Set by the main program. */
.global edge_count_to_wake_up
edge_count_to_wake_up:
.long 0
/* RTC IO number used to sample the input signal.
Set by main program. */
.global io_number
io_number:
.long 0
/* Code goes into .text section */
.text
.global entry
entry:
/* Read the value of lower 16 RTC IOs into R0 */
READ_RTC_FIELD(RTC_GPIO_IN_REG, RTC_GPIO_IN_NEXT)
/* Load io_number, extract the state of input */
move r3, io_number
ld r3, r3, 0
rsh r0, r0, r3
and r0, r0, 1
/* State of input changed? */
move r3, next_edge
ld r3, r3, 0
add r3, r0, r3
and r3, r3, 1
jump changed, eq
/* Not changed */
/* Reset debounce_counter to debounce_max_count */
move r3, debounce_max_count
move r2, debounce_counter
ld r3, r3, 0
st r3, r2, 0
/* End program */
halt
.global changed
changed:
/* Input state changed */
/* Has debounce_counter reached zero? */
move r3, debounce_counter
ld r2, r3, 0
add r2, r2, 0 /* dummy ADD to use "jump if ALU result is zero" */
jump edge_detected, eq
/* Not yet. Decrement debounce_counter */
sub r2, r2, 1
st r2, r3, 0
/* End program */
halt
.global edge_detected
edge_detected:
/* Reset debounce_counter to debounce_max_count */
move r3, debounce_max_count
move r2, debounce_counter
ld r3, r3, 0
st r3, r2, 0
/* Flip next_edge */
move r3, next_edge
ld r2, r3, 0
add r2, r2, 1
and r2, r2, 1
st r2, r3, 0
/* Increment edge_count */
move r3, edge_count
ld r2, r3, 0
add r2, r2, 1
st r2, r3, 0
/* Compare edge_count to edge_count_to_wake_up */
move r3, edge_count_to_wake_up
ld r3, r3, 0
sub r3, r3, r2
jump wake_up, eq
/* Not yet. End program */
halt
.global wake_up
wake_up:
/* Wake up the SoC, end program */
wake
halt

View File

@ -0,0 +1,106 @@
/* ULP Example
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include "esp_deep_sleep.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/sens_reg.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#include "esp32/ulp.h"
#include "ulp_main.h"
extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start");
extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end");
static void init_ulp_program();
static void update_pulse_count();
void app_main()
{
esp_deep_sleep_wakeup_cause_t cause = esp_deep_sleep_get_wakeup_cause();
if (cause != ESP_DEEP_SLEEP_WAKEUP_ULP) {
printf("Not ULP wakeup, initializing ULP\n");
init_ulp_program();
} else {
printf("ULP wakeup, saving pulse count\n");
update_pulse_count();
}
printf("Entering deep sleep\n\n");
ESP_ERROR_CHECK( esp_deep_sleep_enable_ulp_wakeup() );
esp_deep_sleep_start();
}
static void init_ulp_program()
{
esp_err_t err = ulp_load_binary(0, ulp_main_bin_start,
(ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));
ESP_ERROR_CHECK(err);
/* Initialize some variables used by ULP program.
* Each 'ulp_xyz' variable corresponds to 'xyz' variable in the ULP program.
* These variables are declared in an auto generated header file,
* 'ulp_main.h', name of this file is defined in component.mk as ULP_APP_NAME.
* These variables are located in RTC_SLOW_MEM and can be accessed both by the
* ULP and the main CPUs.
*
* Note that the ULP reads only the lower 16 bits of these variables.
*/
ulp_debounce_counter = 3;
ulp_debounce_max_count = 3;
ulp_next_edge = 0;
ulp_io_number = 11; /* GPIO0 is RTC_IO 11 */
ulp_edge_count_to_wake_up = 10;
/* Initialize GPIO0 as RTC IO, input, disable pullup and pulldown */
gpio_num_t gpio_num = GPIO_NUM_0;
rtc_gpio_set_direction(gpio_num, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_dis(gpio_num);
rtc_gpio_pullup_dis(gpio_num);
rtc_gpio_hold_en(gpio_num);
/* Set ULP wake up period to T = 20ms (3095 cycles of RTC_SLOW_CLK clock).
* Minimum pulse width has to be T * (ulp_debounce_counter + 1) = 80ms.
*/
REG_SET_FIELD(SENS_ULP_CP_SLEEP_CYC0_REG, SENS_SLEEP_CYCLES_S0, 3095);
/* Start the program */
err = ulp_run((&ulp_entry - RTC_SLOW_MEM) / sizeof(uint32_t));
ESP_ERROR_CHECK(err);
}
static void update_pulse_count()
{
const char* namespace = "plusecnt";
const char* count_key = "count";
ESP_ERROR_CHECK( nvs_flash_init() );
nvs_handle handle;
ESP_ERROR_CHECK( nvs_open(namespace, NVS_READWRITE, &handle));
uint32_t pulse_count = 0;
esp_err_t err = nvs_get_u32(handle, count_key, &pulse_count);
assert(err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND);
printf("Read pulse count from NVS: %5d\n", pulse_count);
/* ULP program counts signal edges, convert that to the number of pulses */
uint32_t pulse_count_from_ulp = (ulp_edge_count & UINT16_MAX) / 2;
/* In case of an odd number of edges, keep one until next time */
ulp_edge_count = ulp_edge_count % 2;
printf("Pulse count from ULP: %5d\n", pulse_count_from_ulp);
/* Save the new pulse count to NVS */
pulse_count += pulse_count_from_ulp;
ESP_ERROR_CHECK(nvs_set_u32(handle, count_key, pulse_count));
ESP_ERROR_CHECK(nvs_commit(handle));
nvs_close(handle);
printf("Wrote updated pulse count to NVS: %5d\n", pulse_count);
}

View File

@ -0,0 +1,11 @@
# Enable ULP
CONFIG_ULP_COPROC_ENABLED=y
CONFIG_ULP_COPROC_RESERVE_MEM=1024
# Some flash chips need extra time to wake up
# Set this a bit higher to improve out-of-the-box experience
CONFIG_ESP32_DEEP_SLEEP_WAKEUP_DELAY=500
# Set log level to Warning to produce clean output
CONFIG_LOG_BOOTLOADER_LEVEL_WARN=y
CONFIG_LOG_BOOTLOADER_LEVEL=2
CONFIG_LOG_DEFAULT_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL=2