diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 72f6917..bf54464 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -11,7 +11,7 @@ on: [push, pull_request] jobs: tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 with: diff --git a/docs/index.rst b/docs/index.rst index b50e01d..77906dc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -118,7 +118,7 @@ That file can then be loaded directly without assembling the source again. from esp32 import ULP ulp = ULP() - with open('test.ulp', 'r') as f: + with open('test.ulp', 'rb') as f: # load the binary into RTC memory ulp.load_binary(0, f.read()) diff --git a/esp32_ulp/assemble.py b/esp32_ulp/assemble.py index 8b79071..33cec42 100644 --- a/esp32_ulp/assemble.py +++ b/esp32_ulp/assemble.py @@ -219,13 +219,13 @@ def fill(self, section, amount, fill_byte): raise ValueError('fill in bss section not allowed') if section is TEXT: # TODO: text section should be filled with NOPs raise ValueError('fill/skip/align in text section not supported') - fill = int(fill_byte or 0).to_bytes(1, 'little') * amount + fill = int(self.opcodes.eval_arg(str(fill_byte or 0))).to_bytes(1, 'little') * amount self.offsets[section] += len(fill) if section is not BSS: self.sections[section].append(fill) def d_skip(self, amount, fill=None): - amount = int(amount) + amount = int(self.opcodes.eval_arg(amount)) self.fill(self.section, amount, fill) d_space = d_skip @@ -246,7 +246,7 @@ def d_global(self, symbol): self.symbols.set_global(symbol) def append_data(self, wordlen, args): - data = [int(arg).to_bytes(wordlen, 'little') for arg in args] + data = [int(self.opcodes.eval_arg(arg)).to_bytes(wordlen, 'little') for arg in args] self.append_section(b''.join(data)) def d_byte(self, *args): diff --git a/esp32_ulp/opcodes.py b/esp32_ulp/opcodes.py index 10fc3d1..03849a3 100644 --- a/esp32_ulp/opcodes.py +++ b/esp32_ulp/opcodes.py @@ -13,7 +13,7 @@ from uctypes import struct, addressof, LITTLE_ENDIAN, UINT32, BFUINT32, BF_POS, BF_LEN from .soc import * -from .util import split_tokens, validate_expression +from .util import split_tokens, validate_expression, parse_int # XXX dirty hack: use a global for the symbol table symbols = None @@ -285,7 +285,12 @@ def eval_arg(arg): _, _, sym_value = symbols.get_sym(token) parts.append(str(sym_value)) else: - parts.append(token) + try: + # attempt to parse, to convert numbers with base prefix correctly + int_token = parse_int(token) + parts.append(str(int_token)) + except ValueError: + parts.append(token) parts = "".join(parts) if not validate_expression(parts): raise ValueError('Unsupported expression: %s' % parts) @@ -311,7 +316,7 @@ def arg_qualify(arg): if arg_lower in ['--', 'eq', 'ov', 'lt', 'gt', 'ge', 'le']: return ARG(COND, arg_lower, arg) try: - return ARG(IMM, int(arg), arg) + return ARG(IMM, parse_int(arg), arg) except ValueError: pass try: diff --git a/esp32_ulp/opcodes_s2.py b/esp32_ulp/opcodes_s2.py index 91549af..3a9d643 100644 --- a/esp32_ulp/opcodes_s2.py +++ b/esp32_ulp/opcodes_s2.py @@ -12,7 +12,7 @@ from ucollections import namedtuple from uctypes import struct, addressof, LITTLE_ENDIAN, UINT32, BFUINT32, BF_POS, BF_LEN -from .util import split_tokens, validate_expression +from .util import split_tokens, validate_expression, parse_int # XXX dirty hack: use a global for the symbol table symbols = None @@ -301,7 +301,12 @@ def eval_arg(arg): _, _, sym_value = symbols.get_sym(token) parts.append(str(sym_value)) else: - parts.append(token) + try: + # attempt to parse, to convert numbers with base prefix correctly + int_token = parse_int(token) + parts.append(str(int_token)) + except ValueError: + parts.append(token) parts = "".join(parts) if not validate_expression(parts): raise ValueError('Unsupported expression: %s' % parts) @@ -327,7 +332,7 @@ def arg_qualify(arg): if arg_lower in ['--', 'eq', 'ov', 'lt', 'gt', 'ge', 'le']: return ARG(COND, arg_lower, arg) try: - return ARG(IMM, int(arg), arg) + return ARG(IMM, parse_int(arg), arg) except ValueError: pass try: diff --git a/esp32_ulp/util.py b/esp32_ulp/util.py index 78e7c85..5f1628d 100644 --- a/esp32_ulp/util.py +++ b/esp32_ulp/util.py @@ -77,6 +77,18 @@ def validate_expression(param): return True +def parse_int(literal): + """ + GNU as compatible parsing of string literals into integers + Specifically, GNU as treats literals starting with 0 as octal + All other literals are correctly parsed by Python + See: https://sourceware.org/binutils/docs/as/Integers.html + """ + if len(literal) >= 2 and (literal.startswith("0") or literal.startswith("-0")) and literal.lstrip("-0").isdigit(): + return int(literal, 8) + return int(literal, 0) + + def file_exists(filename): try: os.stat(filename) diff --git a/examples/fade_s2.py b/examples/fade_s2.py new file mode 100644 index 0000000..5f8d4ca --- /dev/null +++ b/examples/fade_s2.py @@ -0,0 +1,84 @@ +# +# This file is part of the micropython-esp32-ulp project, +# https://github.com/micropython/micropython-esp32-ulp +# +# SPDX-FileCopyrightText: 2018-2023, the micropython-esp32-ulp authors, see AUTHORS file. +# SPDX-License-Identifier: MIT + +""" +Example for: ESP32-S2 + +This example creates a PWM-like dimming effect using self-modifying ULP code. +The ULP program rewrites the `WAIT` instructions to control on/off LED durations, +simulating a variable duty cycle. + +Note: +The `WAIT` instruction uses an immediate operand (fixed value) for delay cycles. However, we can change the lower half of memory +to modify these values at runtime, simulating variable wait times via registers. +""" + +from esp32 import ULP +from machine import mem32 +from esp32_ulp import src_to_binary +from time import sleep + +source = """\ +# Pin with LED: (0 to 21) +.set led_pin, 4 + +# constants from: +# https://github.com/espressif/esp-idf/blob/v5.0.2/components/soc/esp32s2/include/soc/reg_base.h +#define DR_REG_RTCIO_BASE 0x3f408400 + +# constants from: +# Espressif Technical Reference Manual (TRM) Chapter 5.15 Register 5.63: +#define RTCIO_TOUCH_PADn_REG (DR_REG_RTCIO_BASE + 0x84 + 4 * led_pin) +#define RTCIO_TOUCH_PADn_MUX_SEL_M (BIT(19)) + +# constants from: +# https://github.com/espressif/esp-idf/blob/v5.0.2/components/soc/esp32s2/include/soc/rtc_io_reg.h +#define RTC_GPIO_OUT_REG (DR_REG_RTCIO_BASE + 0x0) +#define RTC_GPIO_ENABLE_REG (DR_REG_RTCIO_BASE + 0xc) +#define RTC_GPIO_ENABLE_S 10 +#define RTC_GPIO_OUT_DATA_S 10 + +.global entry +program_init: + # connect GPIO to ULP (0: GPIO connected to digital GPIO module, 1: GPIO connected to analog RTC module) + WRITE_RTC_REG(RTCIO_TOUCH_PADn_REG, RTCIO_TOUCH_PADn_MUX_SEL_M, 1, 1); + + # enable GPIO as output, not input (this also enables a pull-down by default) + WRITE_RTC_REG(RTC_GPIO_ENABLE_REG, RTC_GPIO_ENABLE_S + led_pin, 1, 1) + +set_waits: add r0, r0, 200 # Increase r0 (delay time) + move r3, wait_off + st r0, r3, 0 # Overwrite wait_off with new delay value + + move r2, 0xFFFF + sub r1, r2, r0 # Calculate complementary delay time + move r3, wait_on + st r1, r3, 0 # Overwrite wait_on with new value + + WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + led_pin, 1, 0) # turn off led (clear GPIO) +wait_off: wait 0 # Placeholder; value overwritten dynamically + WRITE_RTC_REG(RTC_GPIO_OUT_REG, RTC_GPIO_OUT_DATA_S + led_pin, 1, 1) # turn on led (set GPIO) +wait_on: wait 0 # Placeholder; value overwritten dynamically + +jump set_waits # Loop program + +""" + +binary = src_to_binary(source, cpu="esp32s2") # cpu is esp32 or esp32s2 + +load_addr, entry_addr = 0, 0 + +ULP_MEM_BASE = 0x50000000 + +ulp = ULP() +ulp.load_binary(load_addr, binary) + +ulp.run(entry_addr) + +while True: + print(hex(mem32[ULP_MEM_BASE + 40])) # show that the WAIT cycles are changing + sleep(0.5) diff --git a/examples/tsens_s2.py b/examples/tsens_s2.py new file mode 100644 index 0000000..58594e6 --- /dev/null +++ b/examples/tsens_s2.py @@ -0,0 +1,80 @@ +# +# This file is part of the micropython-esp32-ulp project, +# https://github.com/micropython/micropython-esp32-ulp +# +# SPDX-FileCopyrightText: 2018-2023, the micropython-esp32-ulp authors, see AUTHORS file. +# SPDX-License-Identifier: MIT + + +""" +Example for: ESP32-S2 + +Example showing how to use the TSENS instruction from the ULP +and access temperature data from the main CPU. + +Note that the temperature sensor clock needs to be enabled for the TSENS instruction to complete. + +""" + +from esp32 import ULP +from machine import mem32 +from esp32_ulp import src_to_binary +from time import sleep + +source = """\ +# constants from: +# https://github.com/espressif/esp-idf/blob/v5.0.2/components/soc/esp32s2/include/soc/reg_base.h +#define DR_REG_SENS_BASE 0x3f408800 + +# constants from: +# https://github.com/espressif/esp-idf/blob/v5.0.2/components/soc/esp32s2/include/soc/sens_reg.h +#define SENS_SAR_TSENS_CTRL2_REG (DR_REG_SENS_BASE + 0x0054) +#define SENS_TSENS_CLKGATE_EN_M (BIT(15)) + +.set token, 0xACED + +.text +magic: .long 0 +temperature_data: .long 0 + +.global entry +entry: + move r3, magic + ld r0, r3, 0 + jumpr start, token, eq #check if we have already initialized + +init: + # Set SENS_TSENS_CLKGATE_EN to enable temperature sensor clock. + WRITE_RTC_REG(SENS_SAR_TSENS_CTRL2_REG, SENS_TSENS_CLKGATE_EN_M, 1, 1) + + # Store temperature_data memory location in r2 + move r2, temperature_data + + # store that we're done with initialisation + move r0, token + st r0, r3, 0 + +start: + tsens r0, 1000 # make measurement for 1000 clock cycles + st r0, r2, 0 # store the temperature in memory to be read by main CPU + halt # go back to sleep until next wakeup period +""" + +binary = src_to_binary(source, cpu="esp32s2") # cpu is esp32 or esp32s2 + +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: + magic_token = hex(mem32[ULP_MEM_BASE + load_addr] & ULP_DATA_MASK) + current_temperature = 0.4386*(mem32[ULP_MEM_BASE + load_addr + 4] & ULP_DATA_MASK)-20.52 + print(magic_token, current_temperature) + sleep(0.5) \ No newline at end of file diff --git a/tests/01_compat_tests.sh b/tests/01_compat_tests.sh index 0732d34..a6fc2d1 100755 --- a/tests/01_compat_tests.sh +++ b/tests/01_compat_tests.sh @@ -53,6 +53,7 @@ run_tests_for_cpu() { echo -e "\tBuilding using binutils ($cpu)" gcc -I esp-idf/components/soc/$cpu/include -I esp-idf/components/esp_common/include \ + -I esp-idf/components/soc/$cpu/register \ -x assembler-with-cpp \ -E -o ${pre_file} $src_file esp32ulp-elf-as --mcpu=$cpu -o $obj_file ${pre_file} diff --git a/tests/02_compat_rtc_tests.sh b/tests/02_compat_rtc_tests.sh index 1e6a2d1..be2a98d 100755 --- a/tests/02_compat_rtc_tests.sh +++ b/tests/02_compat_rtc_tests.sh @@ -61,6 +61,7 @@ build_defines_db() { rm -f "${defines_db}" micropython -m esp32_ulp.parse_to_db \ esp-idf/components/soc/$cpu/include/soc/*.h \ + esp-idf/components/soc/$cpu/register/soc/*.h \ esp-idf/components/esp_common/include/*.h 1>$log_file # cache defines.db @@ -184,6 +185,7 @@ run_tests_for_cpu() { echo -e "\tBuilding using binutils ($cpu)" gcc -I esp-idf/components/soc/$cpu/include -I esp-idf/components/esp_common/include \ + -I esp-idf/components/soc/$cpu/register \ -x assembler-with-cpp \ -E -o ${pre_file} $src_file esp32ulp-elf-as --mcpu=$cpu -o $obj_file ${pre_file} diff --git a/tests/opcodes.py b/tests/opcodes.py index 3dc7453..f8109b5 100644 --- a/tests/opcodes.py +++ b/tests/opcodes.py @@ -7,7 +7,7 @@ from uctypes import UINT32, BFUINT32, BF_POS, BF_LEN from esp32_ulp.opcodes import make_ins, make_ins_struct_def -from esp32_ulp.opcodes import get_reg, get_imm, get_cond, arg_qualify, eval_arg, ARG, REG, IMM, SYM, COND +from esp32_ulp.opcodes import get_reg, get_imm, get_cond, arg_qualify, parse_int, eval_arg, ARG, REG, IMM, SYM, COND from esp32_ulp.assemble import SymbolTable, ABS, REL, TEXT import esp32_ulp.opcodes as opcodes @@ -46,6 +46,7 @@ def test_arg_qualify(): assert arg_qualify('-1') == ARG(IMM, -1, '-1') assert arg_qualify('1') == ARG(IMM, 1, '1') assert arg_qualify('0x20') == ARG(IMM, 32, '0x20') + assert arg_qualify('0100') == ARG(IMM, 64, '0100') assert arg_qualify('0o100') == ARG(IMM, 64, '0o100') assert arg_qualify('0b1000') == ARG(IMM, 8, '0b1000') assert arg_qualify('eq') == ARG(COND, 'eq', 'eq') @@ -96,6 +97,11 @@ def test_eval_arg(): assert eval_arg('const >> 1') == 21 assert eval_arg('(const|4)&0xf') == 0xe + assert eval_arg('0x7') == 7 + assert eval_arg('010') == 8 + assert eval_arg('-0x7') == -7 # negative + assert eval_arg('~0x7') == -8 # complement + assert_raises(ValueError, eval_arg, 'evil()') assert_raises(ValueError, eval_arg, 'def cafe()') assert_raises(ValueError, eval_arg, '1 ^ 2') @@ -105,14 +111,17 @@ def test_eval_arg(): opcodes.symbols = None -def assert_raises(exception, func, *args): +def assert_raises(exception, func, *args, message=None): try: func(*args) - except exception: + except exception as e: raised = True + actual_message = e.args[0] else: raised = False assert raised + if message: + assert actual_message == message, '%s == %s' % (actual_message, message) def test_reg_direct_ulp_addressing(): diff --git a/tests/opcodes_s2.py b/tests/opcodes_s2.py index 4525049..b9d74d3 100644 --- a/tests/opcodes_s2.py +++ b/tests/opcodes_s2.py @@ -7,7 +7,7 @@ from uctypes import UINT32, BFUINT32, BF_POS, BF_LEN from esp32_ulp.opcodes_s2 import make_ins, make_ins_struct_def -from esp32_ulp.opcodes_s2 import get_reg, get_imm, get_cond, arg_qualify, eval_arg, ARG, REG, IMM, SYM, COND +from esp32_ulp.opcodes_s2 import get_reg, get_imm, get_cond, arg_qualify, parse_int, eval_arg, ARG, REG, IMM, SYM, COND from esp32_ulp.assemble import SymbolTable, ABS, REL, TEXT import esp32_ulp.opcodes_s2 as opcodes @@ -46,6 +46,7 @@ def test_arg_qualify(): assert arg_qualify('-1') == ARG(IMM, -1, '-1') assert arg_qualify('1') == ARG(IMM, 1, '1') assert arg_qualify('0x20') == ARG(IMM, 32, '0x20') + assert arg_qualify('0100') == ARG(IMM, 64, '0100') assert arg_qualify('0o100') == ARG(IMM, 64, '0o100') assert arg_qualify('0b1000') == ARG(IMM, 8, '0b1000') assert arg_qualify('eq') == ARG(COND, 'eq', 'eq') @@ -96,6 +97,11 @@ def test_eval_arg(): assert eval_arg('const >> 1') == 21 assert eval_arg('(const|4)&0xf') == 0xe + assert eval_arg('0x7') == 7 + assert eval_arg('010') == 8 + assert eval_arg('-0x7') == -7 # negative + assert eval_arg('~0x7') == -8 # complement + assert_raises(ValueError, eval_arg, 'evil()') assert_raises(ValueError, eval_arg, 'def cafe()') assert_raises(ValueError, eval_arg, '1 ^ 2') @@ -105,14 +111,17 @@ def test_eval_arg(): opcodes.symbols = None -def assert_raises(exception, func, *args): +def assert_raises(exception, func, *args, message=None): try: func(*args) - except exception: + except exception as e: raised = True + actual_message = e.args[0] else: raised = False assert raised + if message: + assert actual_message == message, '%s == %s' % (actual_message, message) def test_reg_direct_ulp_addressing(): diff --git a/tests/util.py b/tests/util.py index 6aadc8b..5f487ae 100644 --- a/tests/util.py +++ b/tests/util.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: MIT import os -from esp32_ulp.util import split_tokens, validate_expression, file_exists +from esp32_ulp.util import split_tokens, validate_expression, parse_int, file_exists tests = [] @@ -18,6 +18,19 @@ def test(param): tests.append(param) +def assert_raises(exception, func, *args, message=None): + try: + func(*args) + except exception as e: + raised = True + actual_message = e.args[0] + else: + raised = False + assert raised + if message: + assert actual_message == message, '%s == %s' % (actual_message, message) + + @test def test_split_tokens(): assert split_tokens("") == [] @@ -69,6 +82,36 @@ def test_validate_expression(): assert validate_expression('def CAFE()') is False +@test +def test_parse_int(): + # decimal + assert parse_int("0") == 0, "0 == 0" + assert parse_int("5") == 5, "5 == 5" + assert parse_int("-0") == 0, "-0 == 0" + assert parse_int("-5") == -5, "-5 == -5" + # hex + assert parse_int("0x5") == 5, "0x5 == 5" + assert parse_int("0x5a") == 90, "0x5a == 90" + assert parse_int("-0x5a") == -90, "-0x5a == -90" + # binary + assert parse_int("0b1001") == 9, "0b1001 == 9" + assert parse_int("-0b1001") == -9, "-0b1001 == 9" + # octal + assert parse_int("07") == 7, "07 == 7" + assert parse_int("0100") == 64, "0100 == 64" + assert parse_int("0o210") == 136, "0o210 == 136" + assert parse_int("00000010") == 8, "00000010 == 8" + assert parse_int("-07") == -7, "-07 == -7" + assert parse_int("-0100") == -64, "-0100 == -64" + assert parse_int("-0o210") == -136, "-0o210 == -136" + assert parse_int("-00000010") == -8, "-00000010 == -8" + # negative cases + assert_raises(ValueError, parse_int, '0b123', message="invalid syntax for integer with base 2: '123'") + assert_raises(ValueError, parse_int, '0900', message="invalid syntax for integer with base 8: '0900'") + assert_raises(ValueError, parse_int, '0o900', message="invalid syntax for integer with base 8: '900'") + assert_raises(ValueError, parse_int, '0xg', message="invalid syntax for integer with base 16: 'g'") + + @test def test_file_exists(): testfile = '.testfile'