Skip to content

Updated docs, less ugly imports and a useful example #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 <docs/preprocess.rst>`_.

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.
Expand Down
32 changes: 32 additions & 0 deletions esp32_ulp/__init__.py
Original file line number Diff line number Diff line change
@@ -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)

31 changes: 2 additions & 29 deletions esp32_ulp/__main__.py
Original file line number Diff line number Diff line change
@@ -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__':
Expand Down
114 changes: 114 additions & 0 deletions examples/blink.py
Original file line number Diff line number Diff line change
@@ -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
)
2 changes: 1 addition & 1 deletion examples/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion tests/01_compat_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down Expand Up @@ -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
8 changes: 7 additions & 1 deletion tests/02_compat_rtc_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
27 changes: 21 additions & 6 deletions tests/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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'
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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
Expand Down