From 1c2319c07f5dfa4bb096ff3ffadc74fc6921ed2f Mon Sep 17 00:00:00 2001 From: Wilko Nienhaus Date: Sun, 10 Oct 2021 08:01:42 +0300 Subject: [PATCH 1/6] fix unit tests for running on the ESP32 device When running unit tests on the ESP32, the current directory is root, while on the PC (or in the GitHub Actions), the tests/ directory is the current directory. Unit tests that rely on fixture files fails because of this. Now test scripts can look for fixture files relative to their own path to make them independent of the current directory. --- tests/preprocess.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) 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 From 10a037436311873eda66f7c121bf38900eed3650 Mon Sep 17 00:00:00 2001 From: Wilko Nienhaus Date: Sun, 10 Oct 2021 08:06:00 +0300 Subject: [PATCH 2/6] output SHA1 for files built during compat tests This allows for comparing binaries generated on the ESP32 with those generated on the PC with binutils-esp32ulp. During testing on the ESP32, given that there is no binutils-esp32ulp, only the py-esp32-ulp part of the compat tests can run. Calculating the SHA1 for each resulting file from the compat tests, then allows comparing those hashes with hashes calculated on the PC. --- tests/01_compat_tests.sh | 8 +++++++- tests/02_compat_rtc_tests.sh | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) 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 From 118bdab75dbf4e530538d7557615081bc3f93f47 Mon Sep 17 00:00:00 2001 From: Wilko Nienhaus Date: Sat, 9 Oct 2021 09:29:30 +0300 Subject: [PATCH 3/6] make imports less ugly If code outside our main entry point, e.g. our examples, wanted to use the src_to_binary function, it needed an ugly import of esp32_ulp.__main__. With this change the import becomes cleaner, e.g. `from esp32_ulp import src_to_binary`. The original "main" method, which took a filename as input and resulted in a ULP binary file as output, is now also more cleanly available as assemble_file() inside the esp32_ulp module. Fixes #39. --- esp32_ulp/__init__.py | 32 ++++++++++++++++++++++++++++++++ esp32_ulp/__main__.py | 31 ++----------------------------- examples/counter.py | 2 +- 3 files changed, 35 insertions(+), 30 deletions(-) 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/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 From 7309b40c93bd9c766c0521e259ae14aecd540b67 Mon Sep 17 00:00:00 2001 From: Wilko Nienhaus Date: Sat, 9 Oct 2021 19:39:21 +0300 Subject: [PATCH 4/6] add blink example This example makes the ULP turn on and off GPIO2 at regular intervals. If one attaches an LED to GPIO2 and GND, it would blink with a period of approximately 500ms for on and off each. The example defines all needed RTC constants, so it is usable even without a populated Defines DB. The example ends with a loop showing the contents of two memory fields, which the ULP code updates. This allows seeing that the ULP is working. (Can be useful for running the example, even when there is no LED connected to GPIO2). Fixes #12. --- examples/blink.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 examples/blink.py 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 + ) From 5f5a1de1b1629b6a177c3b1ac2c24e9c081fbfcb Mon Sep 17 00:00:00 2001 From: Wilko Nienhaus Date: Sat, 9 Oct 2021 19:32:31 +0300 Subject: [PATCH 5/6] document minimum supported MicroPython version Also, what versions and devices everything was tested on. Everything was tested all the way back to v1.10 today, and the current code works correctly. However, MicroPython v1.11 has a bug related to loading the ULP binary, so while py-esp32-ulp produces correct binaries on that version, they can not actually be used on the same version. To avoid explaining all this detail, MicroPython v1.12 was chosen as the minimum supported version. (Actually MicroPython v1.12 massively reduced its own memory consumption, which makes it much better for py-esp32-ulp anyway, especially when assembling larger sources.) Fixes #48. --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 2afa421..294ec2b 100644 --- a/README.rst +++ b/README.rst @@ -29,6 +29,11 @@ ULP source files containing convenience macros such as WRITE_RTC_REG. The preprocessor and how to use it is documented here: `Preprocessor support `_. +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 alpha software. Also, error and exception handling is rather rough yet. From afe05173346292f5fefd8c2d1b7666939a31b9e0 Mon Sep 17 00:00:00 2001 From: Wilko Nienhaus Date: Sat, 9 Oct 2021 19:43:11 +0300 Subject: [PATCH 6/6] update documentation ahead of v1 Add detail on what is not supported. Change from "alpha" to "beta" for expressing the relative quality level of this software. --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 294ec2b..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. @@ -34,7 +36,7 @@ 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 alpha +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.