diff --git a/README.rst b/README.rst index 2afa421..49e722e 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,9 @@ development machine using the binutils-esp32ulp toolchain from Espressif. Status ------ -The most commonly used simple stuff should work. +The most commonly used stuff should work. Many ULP code examples found on +the web will work unmodified. Notably, assembler macros and #include processing +are not supported. Expressions in assembly source code are supported and get evaluated during assembling. Only expressions evaluating to a single integer are supported. @@ -29,7 +31,12 @@ ULP source files containing convenience macros such as WRITE_RTC_REG. The preprocessor and how to use it is documented here: `Preprocessor support `_. -There might be some stuff missing, some bugs and other symptoms of alpha +The minimum supported version of MicroPython is v1.12. py-esp32-ulp has been +tested with MicroPython v1.12 and v1.17. It has been tested on real ESP32 +devices with the chip type ESP32D0WDQ6 (revision 1) without SPIRAM. It has +also been tested on the Unix port. + +There might be some stuff missing, some bugs and other symptoms of beta software. Also, error and exception handling is rather rough yet. Please be patient or contribute missing parts or fixes. diff --git a/esp32_ulp/__init__.py b/esp32_ulp/__init__.py index e69de29..dddafc8 100644 --- a/esp32_ulp/__init__.py +++ b/esp32_ulp/__init__.py @@ -0,0 +1,32 @@ +from .util import garbage_collect + +from .preprocess import preprocess +from .assemble import Assembler +from .link import make_binary +garbage_collect('after import') + + +def src_to_binary(src): + assembler = Assembler() + src = preprocess(src) + assembler.assemble(src, remove_comments=False) # comments already removed by preprocessor + garbage_collect('before symbols export') + addrs_syms = assembler.symbols.export() + for addr, sym in addrs_syms: + print('%04d %s' % (addr, sym)) + + text, data, bss_len = assembler.fetch() + return make_binary(text, data, bss_len) + + +def assemble_file(filename): + with open(filename) as f: + src = f.read() + + binary = src_to_binary(src) + + if filename.endswith('.s') or filename.endswith('.S'): + filename = filename[:-2] + with open(filename + '.ulp', 'wb') as f: + f.write(binary) + diff --git a/esp32_ulp/__main__.py b/esp32_ulp/__main__.py index 209656f..6f69bea 100644 --- a/esp32_ulp/__main__.py +++ b/esp32_ulp/__main__.py @@ -1,36 +1,9 @@ import sys - -from .util import garbage_collect - -from .preprocess import preprocess -from .assemble import Assembler -from .link import make_binary -garbage_collect('after import') - - -def src_to_binary(src): - assembler = Assembler() - src = preprocess(src) - assembler.assemble(src, remove_comments=False) # comments already removed by preprocessor - garbage_collect('before symbols export') - addrs_syms = assembler.symbols.export() - for addr, sym in addrs_syms: - print('%04d %s' % (addr, sym)) - - text, data, bss_len = assembler.fetch() - return make_binary(text, data, bss_len) +from . import assemble_file def main(fn): - with open(fn) as f: - src = f.read() - - binary = src_to_binary(src) - - if fn.endswith('.s') or fn.endswith('.S'): - fn = fn[:-2] - with open(fn + '.ulp', 'wb') as f: - f.write(binary) + assemble_file(fn) if __name__ == '__main__': diff --git a/examples/blink.py b/examples/blink.py new file mode 100644 index 0000000..8ab0df0 --- /dev/null +++ b/examples/blink.py @@ -0,0 +1,114 @@ +""" +Simple example showing how to control a GPIO pin from the ULP coprocessor. + +The GPIO port is configured to be attached to the RTC module, and then set +to OUTPUT mode. To avoid re-initializing the GPIO on every wakeup, a magic +token gets set in memory. + +After every change of state, the ULP is put back to sleep again until the +next wakeup. The ULP wakes up every 500ms to change the state of the GPIO +pin. An LED attached to the GPIO pin would toggle on and off every 500ms. + +The end of the python script has a loop to show the value of the magic token +and the current state, so you can confirm the magic token gets set and watch +the state value changing. If the loop is stopped (Ctrl-C), the LED attached +to the GPIO pin continues to blink, because the ULP runs independently from +the main processor. +""" + +from esp32 import ULP +from machine import mem32 +from esp32_ulp import src_to_binary + +source = """\ +# constants from: +# https://github.com/espressif/esp-idf/blob/1cb31e5/components/soc/esp32/include/soc/soc.h +#define DR_REG_RTCIO_BASE 0x3ff48400 + +# constants from: +# https://github.com/espressif/esp-idf/blob/1cb31e5/components/soc/esp32/include/soc/rtc_io_reg.h +#define RTC_IO_TOUCH_PAD2_REG (DR_REG_RTCIO_BASE + 0x9c) +#define RTC_IO_TOUCH_PAD2_MUX_SEL_M (BIT(19)) +#define RTC_GPIO_OUT_REG (DR_REG_RTCIO_BASE + 0x0) +#define RTC_GPIO_ENABLE_W1TS_REG (DR_REG_RTCIO_BASE + 0x10) +#define RTC_GPIO_ENABLE_W1TC_REG (DR_REG_RTCIO_BASE + 0x14) +#define RTC_GPIO_ENABLE_W1TS_S 14 +#define RTC_GPIO_ENABLE_W1TC_S 14 +#define RTC_GPIO_OUT_DATA_S 14 + +# constants from: +# https://github.com/espressif/esp-idf/blob/1cb31e5/components/soc/esp32/include/soc/rtc_io_channel.h +#define RTCIO_GPIO2_CHANNEL 12 + +# When accessed from the RTC module (ULP) GPIOs need to be addressed by their channel number +.set gpio, RTCIO_GPIO2_CHANNEL +.set token, 0xcafe # magic token + +.text +magic: .long 0 +state: .long 0 + +.global entry +entry: + # load magic flag + move r0, magic + ld r1, r0, 0 + + # test if we have initialised already + sub r1, r1, token + jump after_init, eq # jump if magic == token (note: "eq" means the last instruction (sub) resulted in 0) + +init: + # connect GPIO to ULP (0: GPIO connected to digital GPIO module, 1: GPIO connected to analog RTC module) + WRITE_RTC_REG(RTC_IO_TOUCH_PAD2_REG, RTC_IO_TOUCH_PAD2_MUX_SEL_M, 1, 1); + + # GPIO shall be output, not input + WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + gpio, 1, 1); + + # store that we're done with initialisation + move r0, magic + move r1, token + st r1, r0, 0 + +after_init: + move r1, state + ld r0, r1, 0 + + move r2, 1 + sub r0, r2, r0 # toggle state + st r0, r1, 0 # store updated state + + jumpr on, 0, gt # if r0 (state) > 0, jump to 'on' + jump off # else jump to 'off' + +on: + # turn on led (set GPIO) + WRITE_RTC_REG(RTC_GPIO_ENABLE_W1TS_REG, RTC_GPIO_ENABLE_W1TS_S + gpio, 1, 1) + jump exit + +off: + # turn off led (clear GPIO) + WRITE_RTC_REG(RTC_GPIO_ENABLE_W1TC_REG, RTC_GPIO_ENABLE_W1TC_S + gpio, 1, 1) + jump exit + +exit: + halt # go back to sleep until next wakeup period +""" + +binary = src_to_binary(source) + +load_addr, entry_addr = 0, 8 + +ULP_MEM_BASE = 0x50000000 +ULP_DATA_MASK = 0xffff # ULP data is only in lower 16 bits + +ulp = ULP() +ulp.set_wakeup_period(0, 500000) # use timer0, wakeup after 500000usec (0.5s) +ulp.load_binary(load_addr, binary) + +ulp.run(entry_addr) + +while True: + print(hex(mem32[ULP_MEM_BASE + load_addr] & ULP_DATA_MASK), # magic token + hex(mem32[ULP_MEM_BASE + load_addr + 4] & ULP_DATA_MASK) # current state + ) diff --git a/examples/counter.py b/examples/counter.py index 959b6a5..77fb146 100644 --- a/examples/counter.py +++ b/examples/counter.py @@ -12,7 +12,7 @@ from esp32 import ULP from machine import mem32 -from esp32_ulp.__main__ import src_to_binary +from esp32_ulp import src_to_binary source = """\ data: .long 0 diff --git a/tests/01_compat_tests.sh b/tests/01_compat_tests.sh index 68f8bdc..2ca2573 100755 --- a/tests/01_compat_tests.sh +++ b/tests/01_compat_tests.sh @@ -4,6 +4,12 @@ set -e +calc_file_hash() { + local filename=$1 + + shasum < $1 | cut -d' ' -f1 +} + for src_file in $(ls -1 compat/*.S); do src_name="${src_file%.S}" @@ -36,6 +42,6 @@ for src_file in $(ls -1 compat/*.S); do xxd $bin_file exit 1 else - echo -e "\tBuild outputs match" + echo -e "\tBuild outputs match (sha1: $(calc_file_hash $ulp_file))" fi done diff --git a/tests/02_compat_rtc_tests.sh b/tests/02_compat_rtc_tests.sh index b609bb6..2752c0f 100755 --- a/tests/02_compat_rtc_tests.sh +++ b/tests/02_compat_rtc_tests.sh @@ -51,6 +51,12 @@ build_defines_db() { esp-idf/components/esp_common/include/*.h 1>$log_file } +calc_file_hash() { + local filename=$1 + + shasum < $1 | cut -d' ' -f1 +} + patch_test() { local test_name=$1 local out_file="${test_name}.tmp" @@ -150,6 +156,6 @@ for src_file in ulptool/src/ulp_examples/*/*.s binutils-esp32ulp/gas/testsuite/g xxd $bin_file exit 1 else - echo -e "\tBuild outputs match" + echo -e "\tBuild outputs match (sha1: $(calc_file_hash $ulp_file))" fi done diff --git a/tests/preprocess.py b/tests/preprocess.py index 5a3825d..be7cf61 100644 --- a/tests/preprocess.py +++ b/tests/preprocess.py @@ -11,6 +11,21 @@ def test(param): tests.append(param) +def resolve_relative_path(filename): + """ + Returns the full path to the filename provided, taken relative to the current file + e.g. + if this file was file.py at /path/to/file.py + and the provided relative filename was tests/unit.py + then the resulting path would be /path/to/tests/unit.py + """ + r = __file__.rsplit("/", 1) # poor man's os.path.dirname(__file__) + head = r[0] + if len(r) == 1 or not head: + return filename + return "%s/%s" % (head, filename) + + @test def test_replace_defines_should_return_empty_line_given_empty_string(): p = Preprocessor() @@ -204,7 +219,7 @@ def preprocess_should_replace_BIT_with_empty_string_unless_defined(): def test_process_include_file(): p = Preprocessor() - defines = p.process_include_file('fixtures/incl.h') + defines = p.process_include_file(resolve_relative_path('fixtures/incl.h')) assert defines['CONST1'] == '42' assert defines['CONST2'] == '99' @@ -216,8 +231,8 @@ def test_process_include_file(): def test_process_include_file_with_multiple_files(): p = Preprocessor() - defines = p.process_include_file('fixtures/incl.h') - defines = p.process_include_file('fixtures/incl2.h') + defines = p.process_include_file(resolve_relative_path('fixtures/incl.h')) + defines = p.process_include_file(resolve_relative_path('fixtures/incl2.h')) assert defines['CONST1'] == '42', "constant from incl.h" assert defines['CONST2'] == '123', "constant overridden by incl2.h" @@ -232,8 +247,8 @@ def test_process_include_file_using_database(): p = Preprocessor() p.use_db(db) - p.process_include_file('fixtures/incl.h') - p.process_include_file('fixtures/incl2.h') + p.process_include_file(resolve_relative_path('fixtures/incl.h')) + p.process_include_file(resolve_relative_path('fixtures/incl2.h')) assert db['CONST1'] == '42', "constant from incl.h" assert db['CONST2'] == '123', "constant overridden by incl2.h" @@ -250,7 +265,7 @@ def test_process_include_file_should_not_load_database_keys_into_instance_define p = Preprocessor() p.use_db(db) - p.process_include_file('fixtures/incl.h') + p.process_include_file(resolve_relative_path('fixtures/incl.h')) # a bit hackish to reference instance-internal state # but it's important to verify this, as we otherwise run out of memory on device