From 062e82a7cd60165db7edd8ee32105ea19a31ae2f Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 19 Jul 2025 11:24:34 -0500 Subject: [PATCH 001/143] py/objint_mpz: Fix pow3 where third argument is zero. This finding is based on fuzzing MicroPython. I manually minimized the test case it provided. Signed-off-by: Jeff Epler --- py/objint_mpz.c | 7 ++++--- tests/basics/builtin_pow3_intbig.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/py/objint_mpz.c b/py/objint_mpz.c index 6f2ea616c779c..ea4e409a257e8 100644 --- a/py/objint_mpz.c +++ b/py/objint_mpz.c @@ -356,9 +356,10 @@ static mpz_t *mp_mpz_for_int(mp_obj_t arg, mpz_t *temp) { mp_obj_t mp_obj_int_pow3(mp_obj_t base, mp_obj_t exponent, mp_obj_t modulus) { if (!mp_obj_is_int(base) || !mp_obj_is_int(exponent) || !mp_obj_is_int(modulus)) { mp_raise_TypeError(MP_ERROR_TEXT("pow() with 3 arguments requires integers")); + } else if (modulus == MP_OBJ_NEW_SMALL_INT(0)) { + mp_raise_ValueError(MP_ERROR_TEXT("divide by zero")); } else { - mp_obj_t result = mp_obj_new_int_from_ull(0); // Use the _from_ull version as this forces an mpz int - mp_obj_int_t *res_p = (mp_obj_int_t *)MP_OBJ_TO_PTR(result); + mp_obj_int_t *res_p = mp_obj_int_new_mpz(); mpz_t l_temp, r_temp, m_temp; mpz_t *lhs = mp_mpz_for_int(base, &l_temp); @@ -376,7 +377,7 @@ mp_obj_t mp_obj_int_pow3(mp_obj_t base, mp_obj_t exponent, mp_obj_t modulus) { if (mod == &m_temp) { mpz_deinit(mod); } - return result; + return MP_OBJ_FROM_PTR(res_p); } } #endif diff --git a/tests/basics/builtin_pow3_intbig.py b/tests/basics/builtin_pow3_intbig.py index bedc8b36b7edd..41d2acbc0cc75 100644 --- a/tests/basics/builtin_pow3_intbig.py +++ b/tests/basics/builtin_pow3_intbig.py @@ -20,3 +20,8 @@ print(hex(pow(y, x-1, x))) # Should be 1, since x is prime print(hex(pow(y, y-1, x))) # Should be a 'big value' print(hex(pow(y, y-1, y))) # Should be a 'big value' + +try: + print(pow(1, 2, 0)) +except ValueError: + print("ValueError") From 3c69277ba9e0f1e66c341c084320f7a763e1bdeb Mon Sep 17 00:00:00 2001 From: Yoctopuce dev Date: Mon, 21 Jul 2025 15:54:27 +0200 Subject: [PATCH 002/143] py/objint_longlong: Fix overflow check in mp_obj_int_get_checked. This is to fix an outstanding TODO. The test cases is using a range as this will exist in all builds, but `mp_obj_get_int` is used in many different parts of code where an overflow is more likely to occur. Signed-off-by: Yoctopuce dev --- py/objint_longlong.c | 13 +++++++++++-- tests/basics/int_64_basics.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/py/objint_longlong.c b/py/objint_longlong.c index 339ce7cfd8e96..8b8fdc62e8b02 100644 --- a/py/objint_longlong.c +++ b/py/objint_longlong.c @@ -308,8 +308,17 @@ mp_int_t mp_obj_int_get_truncated(mp_const_obj_t self_in) { } mp_int_t mp_obj_int_get_checked(mp_const_obj_t self_in) { - // TODO: Check overflow - return mp_obj_int_get_truncated(self_in); + if (mp_obj_is_small_int(self_in)) { + return MP_OBJ_SMALL_INT_VALUE(self_in); + } else { + const mp_obj_int_t *self = self_in; + long long value = self->val; + mp_int_t truncated = (mp_int_t)value; + if ((long long)truncated == value) { + return truncated; + } + } + mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("overflow converting long int to machine word")); } mp_uint_t mp_obj_int_get_uint_checked(mp_const_obj_t self_in) { diff --git a/tests/basics/int_64_basics.py b/tests/basics/int_64_basics.py index 289ea49b65ece..2a161dac0ba14 100644 --- a/tests/basics/int_64_basics.py +++ b/tests/basics/int_64_basics.py @@ -125,6 +125,22 @@ x = 1 << 62 print('a' * (x + 4 - x)) +# test overflow check in mp_obj_get_int_maybe +x = 1 << 32 +r = None +try: + r = range(0, x) +except OverflowError: + # 32-bit target, correctly handled the overflow of x + print("ok") +if r is not None: + if len(r) == x: + # 64-bit target, everything is just a small-int + print("ok") + else: + # 32-bit target that did not handle the overflow of x + print("unhandled overflow") + # negative shifts are invalid try: print((1 << 48) >> -4) From 3a72f95919323d7a36cb3d153d92de90d64853a1 Mon Sep 17 00:00:00 2001 From: Yoctopuce dev Date: Sun, 20 Jul 2025 16:38:33 +0200 Subject: [PATCH 003/143] py/objint_longlong: Fix longlong interoperability with floats. Current longlong implementation does not allow a float as RHS of mathematic operators, as it lacks the delegation code present in mpz. Signed-off-by: Yoctopuce dev --- py/objint_longlong.c | 17 +++++++++++++++++ tests/float/int_64_float.py | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/float/int_64_float.py diff --git a/py/objint_longlong.c b/py/objint_longlong.c index 8b8fdc62e8b02..1a6242b97921e 100644 --- a/py/objint_longlong.c +++ b/py/objint_longlong.c @@ -165,11 +165,28 @@ mp_obj_t mp_obj_int_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_i rhs_val = MP_OBJ_SMALL_INT_VALUE(rhs_in); } else if (mp_obj_is_exact_type(rhs_in, &mp_type_int)) { rhs_val = ((mp_obj_int_t *)rhs_in)->val; + #if MICROPY_PY_BUILTINS_FLOAT + } else if (mp_obj_is_float(rhs_in)) { + return mp_obj_float_binary_op(op, (mp_float_t)lhs_val, rhs_in); + #endif + #if MICROPY_PY_BUILTINS_COMPLEX + } else if (mp_obj_is_type(rhs_in, &mp_type_complex)) { + return mp_obj_complex_binary_op(op, (mp_float_t)lhs_val, 0, rhs_in); + #endif } else { // delegate to generic function to check for extra cases return mp_obj_int_binary_op_extra_cases(op, lhs_in, rhs_in); } + #if MICROPY_PY_BUILTINS_FLOAT + if (op == MP_BINARY_OP_TRUE_DIVIDE || op == MP_BINARY_OP_INPLACE_TRUE_DIVIDE) { + if (rhs_val == 0) { + goto zero_division; + } + return mp_obj_new_float((mp_float_t)lhs_val / (mp_float_t)rhs_val); + } + #endif + switch (op) { case MP_BINARY_OP_ADD: case MP_BINARY_OP_INPLACE_ADD: diff --git a/tests/float/int_64_float.py b/tests/float/int_64_float.py new file mode 100644 index 0000000000000..ffdb0c71dbafb --- /dev/null +++ b/tests/float/int_64_float.py @@ -0,0 +1,25 @@ +# test int64 operation with float/complex + +i = 1 << 40 + +# convert int64 to float on rhs +print("%.5g" % (2.0 * i)) + +# negative int64 as float +print("%.5g" % float(-i)) + +# this should convert to float +print("%.5g" % (i / 5)) + +# these should delegate to float +print("%.5g" % (i * 1.2)) +print("%.5g" % (i / 1.2)) + +# negative power should produce float +print("%.5g" % (i**-1)) +print("%.5g" % ((2 + i - i) ** -3)) + +try: + i / 0 +except ZeroDivisionError: + print("ZeroDivisionError") From 82db5c81e027f6ad305a43ec3c90a13ba319e3b4 Mon Sep 17 00:00:00 2001 From: Anson Mansfield Date: Fri, 19 Jul 2024 13:01:09 -0400 Subject: [PATCH 004/143] tests/basics: Add tests for PEP487 __set_name__. Including the stochastic tests needed to guarantee sensitivity to the potential iterate-while-modifying hazard a naive implementation might have. Signed-off-by: Anson Mansfield --- tests/basics/class_descriptor.py | 20 ++- tests/basics/class_setname_hazard.py | 182 ++++++++++++++++++++++ tests/basics/class_setname_hazard_rand.py | 111 +++++++++++++ 3 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 tests/basics/class_setname_hazard.py create mode 100644 tests/basics/class_setname_hazard_rand.py diff --git a/tests/basics/class_descriptor.py b/tests/basics/class_descriptor.py index 83d31674301d5..feaed2fbb2a43 100644 --- a/tests/basics/class_descriptor.py +++ b/tests/basics/class_descriptor.py @@ -1,22 +1,28 @@ class Descriptor: def __get__(self, obj, cls): - print('get') + print("get") print(type(obj) is Main) print(cls is Main) - return 'result' + return "result" def __set__(self, obj, val): - print('set') + print("set") print(type(obj) is Main) print(val) def __delete__(self, obj): - print('delete') + print("delete") print(type(obj) is Main) + def __set_name__(self, owner, name): + print("set_name", name) + print(owner.__name__ == "Main") + + class Main: Forward = Descriptor() + m = Main() try: m.__class__ @@ -26,15 +32,15 @@ class Main: raise SystemExit r = m.Forward -if 'Descriptor' in repr(r.__class__): +if "Descriptor" in repr(r.__class__): # Target doesn't support descriptors. - print('SKIP') + print("SKIP") raise SystemExit # Test assignment and deletion. print(r) -m.Forward = 'a' +m.Forward = "a" del m.Forward # Test that lookup of descriptors like __get__ are not passed into __getattr__. diff --git a/tests/basics/class_setname_hazard.py b/tests/basics/class_setname_hazard.py new file mode 100644 index 0000000000000..77c0409346282 --- /dev/null +++ b/tests/basics/class_setname_hazard.py @@ -0,0 +1,182 @@ +# Test that __set_name__ can access and mutate its owner argument. + + +def skip_if_no_descriptors(): + class Descriptor: + def __get__(self, obj, cls): + return + + class TestClass: + Forward = Descriptor() + + a = TestClass() + try: + a.__class__ + except AttributeError: + # Target doesn't support __class__. + print("SKIP") + raise SystemExit + + b = a.Forward + if "Descriptor" in repr(b.__class__): + # Target doesn't support descriptors. + print("SKIP") + raise SystemExit + + +skip_if_no_descriptors() + + +# Test basic accesses and mutations. + + +class GetSibling: + def __set_name__(self, owner, name): + print(getattr(owner, name + "_sib")) + + +class GetSiblingTest: + desc = GetSibling() + desc_sib = 111 + + +t110 = GetSiblingTest() + + +class SetSibling: + def __set_name__(self, owner, name): + setattr(owner, name + "_sib", 121) + + +class SetSiblingTest: + desc = SetSibling() + + +t120 = SetSiblingTest() + +print(t120.desc_sib) + + +class DelSibling: + def __set_name__(self, owner, name): + delattr(owner, name + "_sib") + + +class DelSiblingTest: + desc = DelSibling() + desc_sib = 131 + + +t130 = DelSiblingTest() + +try: + print(t130.desc_sib) +except AttributeError: + print("AttributeError") + + +class GetSelf: + x = 211 + + def __set_name__(self, owner, name): + print(getattr(owner, name).x) + + +class GetSelfTest: + desc = GetSelf() + + +t210 = GetSelfTest() + + +class SetSelf: + def __set_name__(self, owner, name): + setattr(owner, name, 221) + + +class SetSelfTest: + desc = SetSelf() + + +t220 = SetSelfTest() + +print(t220.desc) + + +class DelSelf: + def __set_name__(self, owner, name): + delattr(owner, name) + + +class DelSelfTest: + desc = DelSelf() + + +t230 = DelSelfTest() + +try: + print(t230.desc) +except AttributeError: + print("AttributeError") + + +# Test exception behavior. + + +class Raise: + def __set_name__(self, owner, name): + raise Exception() + + +try: + + class RaiseTest: + desc = Raise() +except Exception as e: # CPython raises RuntimeError, MicroPython propagates the original exception + print("Exception") + + +# Ensure removed/overwritten class members still get __set_name__ called. + + +class SetSpecific: + def __init__(self, sib_name, sib_replace): + self.sib_name = sib_name + self.sib_replace = sib_replace + + def __set_name__(self, owner, name): + setattr(owner, self.sib_name, self.sib_replace) + + +class SetReplaceTest: + a = SetSpecific("b", 312) # one of these is changed first + b = SetSpecific("a", 311) + + +t310 = SetReplaceTest() +print(t310.a) +print(t310.b) + + +class DelSpecific: + def __init__(self, sib_name): + self.sib_name = sib_name + + def __set_name__(self, owner, name): + delattr(owner, self.sib_name) + + +class DelReplaceTest: + a = DelSpecific("b") # one of these is removed first + b = DelSpecific("a") + + +t320 = DelReplaceTest() +try: + print(t320.a) +except AttributeError: + print("AttributeError") +try: + print(t320.b) +except AttributeError: + print("AttributeError") diff --git a/tests/basics/class_setname_hazard_rand.py b/tests/basics/class_setname_hazard_rand.py new file mode 100644 index 0000000000000..4c9934c3bf068 --- /dev/null +++ b/tests/basics/class_setname_hazard_rand.py @@ -0,0 +1,111 @@ +# Test to make sure there's no sequence hazard even when a __set_name__ implementation +# mutates and reorders the namespace of its owner class. +# VERY hard bug to prove out except via a stochastic test. + + +try: + from random import choice + import re +except ImportError: + print("SKIP") + raise SystemExit + + +def skip_if_no_descriptors(): + class Descriptor: + def __get__(self, obj, cls): + return + + class TestClass: + Forward = Descriptor() + + a = TestClass() + try: + a.__class__ + except AttributeError: + # Target doesn't support __class__. + print("SKIP") + raise SystemExit + + b = a.Forward + if "Descriptor" in repr(b.__class__): + # Target doesn't support descriptors. + print("SKIP") + raise SystemExit + + +skip_if_no_descriptors() + +letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +# Would be r"[A-Z]{5}", but not all ports support the {n} quantifier. +junk_re = re.compile(r"[A-Z][A-Z][A-Z][A-Z][A-Z]") + + +def junk_fill(obj, n=10): # Add randomly-generated attributes to an object. + for i in range(n): + name = "".join(choice(letters) for j in range(5)) + setattr(obj, name, object()) + + +def junk_clear(obj): # Remove attributes added by junk_fill. + to_del = [name for name in dir(obj) if junk_re.match(name)] + for name in to_del: + delattr(obj, name) + + +def junk_sequencer(): + global runs + try: + while True: + owner, name = yield + runs[name] = runs.get(name, 0) + 1 + junk_fill(owner) + finally: + junk_clear(owner) + + +class JunkMaker: + def __set_name__(self, owner, name): + global seq + seq.send((owner, name)) + + +runs = {} +seq = junk_sequencer() +next(seq) + + +class Main: + a = JunkMaker() + b = JunkMaker() + c = JunkMaker() + d = JunkMaker() + e = JunkMaker() + f = JunkMaker() + g = JunkMaker() + h = JunkMaker() + i = JunkMaker() + j = JunkMaker() + k = JunkMaker() + l = JunkMaker() + m = JunkMaker() + n = JunkMaker() + o = JunkMaker() + p = JunkMaker() + q = JunkMaker() + r = JunkMaker() + s = JunkMaker() + t = JunkMaker() + u = JunkMaker() + v = JunkMaker() + w = JunkMaker() + x = JunkMaker() + y = JunkMaker() + z = JunkMaker() + + +seq.close() + +for k in letters.lower(): + print(k, runs.get(k, 0)) From 4412753f0cf46fb28093a90d58988f541a531df4 Mon Sep 17 00:00:00 2001 From: Anson Mansfield Date: Fri, 19 Jul 2024 18:23:45 -0400 Subject: [PATCH 005/143] py/objtype: Add support for PEP487 __set_name__. This commit adds support for the `__set_name__` data model method specified by PEP487 - Simpler customisation of class creation. This includes support for methods that mutate the owner class, and avoids the naive modify-while-iterating hazard possible in a naive implementation like micropython/micropython#15503. Note that based on the benchmarks in micropython/micropython#16825, this is also as fast or faster than the naive implementation, thanks to clever data layout in `setname_list_t`, and the way this allows the capture step to run during an existing loop through the class dict. Other rejected approaches for dealing with the hazard include: - python/cpython#72983 During the implementation of this feature for MicroPython, it was discovered that some versions of CPython also have this naive hazard. CPython resolved this bug in BPO-28797 and now makes a complete flat copy of the class's dict to iterate. This design decision doesn't make much sense for a microcontroller though, even if it's perfectly reasonable in the desktop world where memcpy might actually be cheaper than a hard-to-branch-predict conditional; and it's also motivated in their case by error-tracing considerations. - micropython/micropython#16816 This is an equivalent implementation to CPython's approach that places this copy directly on the stack; however it is both slower and has larger code size than the approach taken here. - micropython/micropython#15503 The simplest implementation is to just not worry about it and let the user face the consequences if they mutate the owner class. That's not a very friendly behavior, though, and it's not actually much more performant than this implementation on either time or code size. - micropython/micropython#17693 Another alternative is to do the same as #15503 but leverage MicroPython's existing `is_fixed` field in its dict type to convert attempted mutations of the owner dict into `AttributeError`s. This is safer than just leaving the open hazard, but there's still important use-cases for owner-mutating descriptors, and the performance gain is small enough that it isn't worth missing support for those cases. - combined micropython/micropython#17693 with this Another version of this feature used a new feature define, `MICROPY_PY_METACLASSES_LITE`, to control whether this algorithm or the naive version is used. This was rejected in favor of simplicity, based on the very limited performance margin the naive version has (which in some cases even goes _against_ it). Signed-off-by: Anson Mansfield --- py/objtype.c | 90 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/py/objtype.c b/py/objtype.c index f2173c79a173e..818ceeb0589fb 100644 --- a/py/objtype.c +++ b/py/objtype.c @@ -661,8 +661,8 @@ static void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *des // try __getattr__ if (attr != MP_QSTR___getattr__) { #if MICROPY_PY_DESCRIPTORS - // With descriptors enabled, don't delegate lookups of __get__/__set__/__delete__. - if (attr == MP_QSTR___get__ || attr == MP_QSTR___set__ || attr == MP_QSTR___delete__) { + // With descriptors enabled, don't delegate lookups of __get__/__set__/__delete__/__set_name__. + if (attr == MP_QSTR___get__ || attr == MP_QSTR___set__ || attr == MP_QSTR___delete__ || attr == MP_QSTR___set_name__) { return; } #endif @@ -960,7 +960,7 @@ static bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) { #endif #if MICROPY_PY_DESCRIPTORS static const uint8_t to_check[] = { - MP_QSTR___get__, MP_QSTR___set__, MP_QSTR___delete__, + MP_QSTR___get__, MP_QSTR___set__, MP_QSTR___delete__, // not needed for MP_QSTR___set_name__ though }; for (size_t i = 0; i < MP_ARRAY_SIZE(to_check); ++i) { mp_obj_t dest_temp[2]; @@ -974,6 +974,48 @@ static bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) { } #endif +#if MICROPY_PY_DESCRIPTORS +// Shared data layout for the __set_name__ call and a linked list of calls to be made. +typedef union _setname_list_t setname_list_t; +union _setname_list_t { + mp_obj_t call[4]; + struct { + mp_obj_t _meth; + mp_obj_t _self; + setname_list_t *next; // can use the "owner" argument position temporarily for the linked list + mp_obj_t _name; + }; +}; + +// Append any `__set_name__` method on `value` to the setname list, with its per-attr args +static setname_list_t *setname_maybe_bind_append(setname_list_t *tail, mp_obj_t name, mp_obj_t value) { + // make certain our type-punning is safe: + MP_STATIC_ASSERT_NONCONSTEXPR(offsetof(setname_list_t, next) == offsetof(setname_list_t, call[2])); + + // tail is a blank list entry + mp_load_method_maybe(value, MP_QSTR___set_name__, tail->call); + if (tail->call[1] != MP_OBJ_NULL) { + // Each time a __set_name__ is found, leave it in-place in the former tail and allocate a new tail + tail->next = m_new_obj(setname_list_t); + tail->next->next = NULL; + tail->call[3] = name; + return tail->next; + } else { + return tail; + } +} + +// Execute the captured `__set_name__` calls, destroying the setname list in the process. +static inline void setname_consume_call_all(setname_list_t *head, mp_obj_t owner) { + setname_list_t *next; + while ((next = head->next) != NULL) { + head->call[2] = owner; + mp_call_method_n_kw(2, 0, head->call); + head = next; + } +} +#endif + static void type_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { (void)kind; mp_obj_type_t *self = MP_OBJ_TO_PTR(self_in); @@ -1210,20 +1252,38 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) } } + #if MICROPY_PY_DESCRIPTORS + // To avoid any dynamic allocations when no __set_name__ exists, + // the head of this list is kept on the stack (marked blank with `next = NULL`). + setname_list_t setname_list = { .next = NULL }; + setname_list_t *setname_tail = &setname_list; + #endif + #if ENABLE_SPECIAL_ACCESSORS - // Check if the class has any special accessor methods - if (!(o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS)) { - for (size_t i = 0; i < locals_ptr->map.alloc; i++) { - if (mp_map_slot_is_filled(&locals_ptr->map, i)) { - const mp_map_elem_t *elem = &locals_ptr->map.table[i]; - if (check_for_special_accessors(elem->key, elem->value)) { - o->flags |= MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS; - break; - } + // Check if the class has any special accessor methods, + // and accumulate bound __set_name__ methods that need to be called + for (size_t i = 0; i < locals_ptr->map.alloc; i++) { + #if !MICROPY_PY_DESCRIPTORS + // __set_name__ needs to scan the entire locals map, can't early-terminate + if (o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS) { + break; + } + #endif + + if (mp_map_slot_is_filled(&locals_ptr->map, i)) { + const mp_map_elem_t *elem = &locals_ptr->map.table[i]; + + if (!(o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS) // elidable when the early-termination check is enabled + && check_for_special_accessors(elem->key, elem->value)) { + o->flags |= MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS; } + + #if MICROPY_PY_DESCRIPTORS + setname_tail = setname_maybe_bind_append(setname_tail, elem->key, elem->value); + #endif } } - #endif + #endif // ENABLE_SPECIAL_ACCESSORS const mp_obj_type_t *native_base; size_t num_native_bases = instance_count_native_bases(o, &native_base); @@ -1241,6 +1301,10 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) } } + #if MICROPY_PY_DESCRIPTORS + setname_consume_call_all(&setname_list, MP_OBJ_FROM_PTR(o)); + #endif + return MP_OBJ_FROM_PTR(o); } From d5dc5547427ef171e3ede4f8ac35ea9bf6f53fca Mon Sep 17 00:00:00 2001 From: Anson Mansfield Date: Mon, 21 Jul 2025 08:33:17 -0400 Subject: [PATCH 006/143] docs: Document PEP487 __set_name__ implementation. Signed-off-by: Anson Mansfield --- docs/differences/python_36.rst | 7 ++++++- py/mpconfig.h | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/differences/python_36.rst b/docs/differences/python_36.rst index 3315b0594dafc..18da79f8f8431 100644 --- a/docs/differences/python_36.rst +++ b/docs/differences/python_36.rst @@ -25,7 +25,8 @@ Python 3.6 beta 1 was released on 12 Sep 2016, and a summary of the new features +--------------------------------------------------------+--------------------------------------------------+-----------------+ | `PEP 468 `_ | Preserving the order of *kwargs* in a function | | +--------------------------------------------------------+--------------------------------------------------+-----------------+ - | `PEP 487 `_ | Simpler customization of class creation | | + | `PEP 487 `_ | Simpler customization of class creation | Partial | + | | | [#setname]_ | +--------------------------------------------------------+--------------------------------------------------+-----------------+ | `PEP 520 `_ | Preserving Class Attribute Definition Order | | +--------------------------------------------------------+--------------------------------------------------+-----------------+ @@ -198,3 +199,7 @@ Changes to built-in modules: +--------------------------------------------------------------------------------------------------------------+----------------+ | The *compress()* and *decompress()* functions now accept keyword arguments | | +--------------------------------------------------------------------------------------------------------------+----------------+ + +.. rubric:: Notes + +.. [#setname] Currently, only :func:`__set_name__` is implemented. diff --git a/py/mpconfig.h b/py/mpconfig.h index 619bce2ab290a..6ef6ec52e7cb6 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -1129,7 +1129,7 @@ typedef time_t mp_timestamp_t; #define MICROPY_PY_FUNCTION_ATTRS_CODE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES) #endif -// Whether to support the descriptors __get__, __set__, __delete__ +// Whether to support the descriptors __get__, __set__, __delete__, __set_name__ // This costs some code size and makes load/store/delete of instance // attributes slower for the classes that use this feature #ifndef MICROPY_PY_DESCRIPTORS From c3e77ad6db279c37bd39e467e7bbef7750fd5f7b Mon Sep 17 00:00:00 2001 From: Anson Mansfield Date: Wed, 26 Feb 2025 12:38:08 -0500 Subject: [PATCH 007/143] tests/internal_bench/class_create: Benchmark class creation. Signed-off-by: Anson Mansfield --- tests/internal_bench/class_create-0-empty.py | 11 ++++++++++ tests/internal_bench/class_create-1-slots.py | 12 +++++++++++ .../internal_bench/class_create-1.1-slots5.py | 12 +++++++++++ .../class_create-2-classattr.py | 11 ++++++++++ .../class_create-2.1-classattr5.py | 15 +++++++++++++ .../class_create-2.3-classattr5objs.py | 20 ++++++++++++++++++ .../class_create-3-instancemethod.py | 12 +++++++++++ .../class_create-4-classmethod.py | 13 ++++++++++++ .../class_create-4.1-classmethod_implicit.py | 12 +++++++++++ .../class_create-5-staticmethod.py | 13 ++++++++++++ .../class_create-6-getattribute.py | 12 +++++++++++ .../class_create-6.1-getattr.py | 12 +++++++++++ .../class_create-6.2-property.py | 13 ++++++++++++ .../class_create-6.3-descriptor.py | 17 +++++++++++++++ .../internal_bench/class_create-7-inherit.py | 14 +++++++++++++ .../class_create-7.1-inherit_initsubclass.py | 16 ++++++++++++++ .../class_create-8-metaclass_setname.py | 17 +++++++++++++++ .../class_create-8.1-metaclass_setname5.py | 21 +++++++++++++++++++ 18 files changed, 253 insertions(+) create mode 100644 tests/internal_bench/class_create-0-empty.py create mode 100644 tests/internal_bench/class_create-1-slots.py create mode 100644 tests/internal_bench/class_create-1.1-slots5.py create mode 100644 tests/internal_bench/class_create-2-classattr.py create mode 100644 tests/internal_bench/class_create-2.1-classattr5.py create mode 100644 tests/internal_bench/class_create-2.3-classattr5objs.py create mode 100644 tests/internal_bench/class_create-3-instancemethod.py create mode 100644 tests/internal_bench/class_create-4-classmethod.py create mode 100644 tests/internal_bench/class_create-4.1-classmethod_implicit.py create mode 100644 tests/internal_bench/class_create-5-staticmethod.py create mode 100644 tests/internal_bench/class_create-6-getattribute.py create mode 100644 tests/internal_bench/class_create-6.1-getattr.py create mode 100644 tests/internal_bench/class_create-6.2-property.py create mode 100644 tests/internal_bench/class_create-6.3-descriptor.py create mode 100644 tests/internal_bench/class_create-7-inherit.py create mode 100644 tests/internal_bench/class_create-7.1-inherit_initsubclass.py create mode 100644 tests/internal_bench/class_create-8-metaclass_setname.py create mode 100644 tests/internal_bench/class_create-8.1-metaclass_setname5.py diff --git a/tests/internal_bench/class_create-0-empty.py b/tests/internal_bench/class_create-0-empty.py new file mode 100644 index 0000000000000..1fd8ccd925715 --- /dev/null +++ b/tests/internal_bench/class_create-0-empty.py @@ -0,0 +1,11 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-1-slots.py b/tests/internal_bench/class_create-1-slots.py new file mode 100644 index 0000000000000..9b3e4b9570da2 --- /dev/null +++ b/tests/internal_bench/class_create-1-slots.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + l = ["x"] + for i in range(num // 40): + + class X: + __slots__ = l + + +bench.run(test) diff --git a/tests/internal_bench/class_create-1.1-slots5.py b/tests/internal_bench/class_create-1.1-slots5.py new file mode 100644 index 0000000000000..ccac77dec9daa --- /dev/null +++ b/tests/internal_bench/class_create-1.1-slots5.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + l = ["a", "b", "c", "d", "x"] + for i in range(num // 40): + + class X: + __slots__ = l + + +bench.run(test) diff --git a/tests/internal_bench/class_create-2-classattr.py b/tests/internal_bench/class_create-2-classattr.py new file mode 100644 index 0000000000000..049a7dab170c1 --- /dev/null +++ b/tests/internal_bench/class_create-2-classattr.py @@ -0,0 +1,11 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + x = 1 + + +bench.run(test) diff --git a/tests/internal_bench/class_create-2.1-classattr5.py b/tests/internal_bench/class_create-2.1-classattr5.py new file mode 100644 index 0000000000000..5051e7dcca70d --- /dev/null +++ b/tests/internal_bench/class_create-2.1-classattr5.py @@ -0,0 +1,15 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + a = 0 + b = 0 + c = 0 + d = 0 + x = 1 + + +bench.run(test) diff --git a/tests/internal_bench/class_create-2.3-classattr5objs.py b/tests/internal_bench/class_create-2.3-classattr5objs.py new file mode 100644 index 0000000000000..74540865dcd14 --- /dev/null +++ b/tests/internal_bench/class_create-2.3-classattr5objs.py @@ -0,0 +1,20 @@ +import bench + + +class Class: + pass + + +def test(num): + instance = Class() + for i in range(num // 40): + + class X: + a = instance + b = instance + c = instance + d = instance + x = instance + + +bench.run(test) diff --git a/tests/internal_bench/class_create-3-instancemethod.py b/tests/internal_bench/class_create-3-instancemethod.py new file mode 100644 index 0000000000000..e8c201cb2c3c6 --- /dev/null +++ b/tests/internal_bench/class_create-3-instancemethod.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + def x(self): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-4-classmethod.py b/tests/internal_bench/class_create-4-classmethod.py new file mode 100644 index 0000000000000..f34962bc67117 --- /dev/null +++ b/tests/internal_bench/class_create-4-classmethod.py @@ -0,0 +1,13 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + @classmethod + def x(cls): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-4.1-classmethod_implicit.py b/tests/internal_bench/class_create-4.1-classmethod_implicit.py new file mode 100644 index 0000000000000..f2d1fcfd18821 --- /dev/null +++ b/tests/internal_bench/class_create-4.1-classmethod_implicit.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + def __new__(cls): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-5-staticmethod.py b/tests/internal_bench/class_create-5-staticmethod.py new file mode 100644 index 0000000000000..0633556667544 --- /dev/null +++ b/tests/internal_bench/class_create-5-staticmethod.py @@ -0,0 +1,13 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + @staticmethod + def x(): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-6-getattribute.py b/tests/internal_bench/class_create-6-getattribute.py new file mode 100644 index 0000000000000..10a4fe7ce8d9f --- /dev/null +++ b/tests/internal_bench/class_create-6-getattribute.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + def __getattribute__(self, name): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-6.1-getattr.py b/tests/internal_bench/class_create-6.1-getattr.py new file mode 100644 index 0000000000000..b4b9ba2f5525b --- /dev/null +++ b/tests/internal_bench/class_create-6.1-getattr.py @@ -0,0 +1,12 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + def __getattr__(self, name): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-6.2-property.py b/tests/internal_bench/class_create-6.2-property.py new file mode 100644 index 0000000000000..cf847b6dc9c9f --- /dev/null +++ b/tests/internal_bench/class_create-6.2-property.py @@ -0,0 +1,13 @@ +import bench + + +def test(num): + for i in range(num // 40): + + class X: + @property + def x(self): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-6.3-descriptor.py b/tests/internal_bench/class_create-6.3-descriptor.py new file mode 100644 index 0000000000000..7b0a635726380 --- /dev/null +++ b/tests/internal_bench/class_create-6.3-descriptor.py @@ -0,0 +1,17 @@ +import bench + + +class D: + def __get__(self, instance, owner=None): + pass + + +def test(num): + descriptor = D() + for i in range(num // 40): + + class X: + x = descriptor + + +bench.run(test) diff --git a/tests/internal_bench/class_create-7-inherit.py b/tests/internal_bench/class_create-7-inherit.py new file mode 100644 index 0000000000000..f48fb215e0ab8 --- /dev/null +++ b/tests/internal_bench/class_create-7-inherit.py @@ -0,0 +1,14 @@ +import bench + + +def test(num): + class B: + pass + + for i in range(num // 40): + + class X(B): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-7.1-inherit_initsubclass.py b/tests/internal_bench/class_create-7.1-inherit_initsubclass.py new file mode 100644 index 0000000000000..0660fa86258fe --- /dev/null +++ b/tests/internal_bench/class_create-7.1-inherit_initsubclass.py @@ -0,0 +1,16 @@ +import bench + + +def test(num): + class B: + @classmethod + def __init_subclass__(cls): + pass + + for i in range(num // 40): + + class X(B): + pass + + +bench.run(test) diff --git a/tests/internal_bench/class_create-8-metaclass_setname.py b/tests/internal_bench/class_create-8-metaclass_setname.py new file mode 100644 index 0000000000000..e4515b54279d1 --- /dev/null +++ b/tests/internal_bench/class_create-8-metaclass_setname.py @@ -0,0 +1,17 @@ +import bench + + +class D: + def __set_name__(self, owner, name): + pass + + +def test(num): + descriptor = D() + for i in range(num // 40): + + class X: + x = descriptor + + +bench.run(test) diff --git a/tests/internal_bench/class_create-8.1-metaclass_setname5.py b/tests/internal_bench/class_create-8.1-metaclass_setname5.py new file mode 100644 index 0000000000000..5daa3f8471bca --- /dev/null +++ b/tests/internal_bench/class_create-8.1-metaclass_setname5.py @@ -0,0 +1,21 @@ +import bench + + +class D: + def __set_name__(self, owner, name): + pass + + +def test(num): + descriptor = D() + for i in range(num // 40): + + class X: + a = descriptor + b = descriptor + c = descriptor + d = descriptor + x = descriptor + + +bench.run(test) From b9d6d6af4b1ac1a900351bebe5294604fea0119b Mon Sep 17 00:00:00 2001 From: Anson Mansfield Date: Sun, 20 Jul 2025 12:07:05 -0400 Subject: [PATCH 008/143] tests/internal_bench/var: Benchmark descriptor access. Signed-off-by: Anson Mansfield --- .../var-6.2-instance-speciallookup.py | 19 ++++++++++++++++++ .../var-6.3-instance-property.py | 17 ++++++++++++++++ .../var-6.4-instance-descriptor.py | 20 +++++++++++++++++++ .../var-6.5-instance-getattr.py | 16 +++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 tests/internal_bench/var-6.2-instance-speciallookup.py create mode 100644 tests/internal_bench/var-6.3-instance-property.py create mode 100644 tests/internal_bench/var-6.4-instance-descriptor.py create mode 100644 tests/internal_bench/var-6.5-instance-getattr.py diff --git a/tests/internal_bench/var-6.2-instance-speciallookup.py b/tests/internal_bench/var-6.2-instance-speciallookup.py new file mode 100644 index 0000000000000..71845f3aaa23c --- /dev/null +++ b/tests/internal_bench/var-6.2-instance-speciallookup.py @@ -0,0 +1,19 @@ +import bench + + +class Foo: + def __init__(self): + self.num = 20000000 + + def __getattr__(self, name): # just trigger the 'special lookups' flag on the class + pass + + +def test(num): + o = Foo() + i = 0 + while i < o.num: + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-6.3-instance-property.py b/tests/internal_bench/var-6.3-instance-property.py new file mode 100644 index 0000000000000..b4426ef7928e1 --- /dev/null +++ b/tests/internal_bench/var-6.3-instance-property.py @@ -0,0 +1,17 @@ +import bench + + +class Foo: + @property + def num(self): + return 20000000 + + +def test(num): + o = Foo() + i = 0 + while i < o.num: + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-6.4-instance-descriptor.py b/tests/internal_bench/var-6.4-instance-descriptor.py new file mode 100644 index 0000000000000..b4df69f878f10 --- /dev/null +++ b/tests/internal_bench/var-6.4-instance-descriptor.py @@ -0,0 +1,20 @@ +import bench + + +class Descriptor: + def __get__(self, instance, owner=None): + return 20000000 + + +class Foo: + num = Descriptor() + + +def test(num): + o = Foo() + i = 0 + while i < o.num: + i += 1 + + +bench.run(test) diff --git a/tests/internal_bench/var-6.5-instance-getattr.py b/tests/internal_bench/var-6.5-instance-getattr.py new file mode 100644 index 0000000000000..3b2ef6721105f --- /dev/null +++ b/tests/internal_bench/var-6.5-instance-getattr.py @@ -0,0 +1,16 @@ +import bench + + +class Foo: + def __getattr__(self, name): + return 20000000 + + +def test(num): + o = Foo() + i = 0 + while i < o.num: + i += 1 + + +bench.run(test) From 88cb6bc818546e7fe8b862c784e1e4a6b8a12b8a Mon Sep 17 00:00:00 2001 From: Anson Mansfield Date: Sun, 20 Jul 2025 13:47:01 -0400 Subject: [PATCH 009/143] tests/run-internalbench.py: Allow running internalbench on hardware. Signed-off-by: Anson Mansfield --- tests/run-internalbench.py | 101 ++++++++++++++++++++++++++++--------- tests/run-tests.py | 52 +++++++++++-------- 2 files changed, 109 insertions(+), 44 deletions(-) diff --git a/tests/run-internalbench.py b/tests/run-internalbench.py index c9f783e474c9c..99c6304afe9d6 100755 --- a/tests/run-internalbench.py +++ b/tests/run-internalbench.py @@ -8,6 +8,10 @@ from glob import glob from collections import defaultdict +run_tests_module = __import__("run-tests") +sys.path.append(run_tests_module.base_path("../tools")) +import pyboard + if os.name == "nt": MICROPYTHON = os.getenv( "MICROPY_MICROPYTHON", "../ports/windows/build-standard/micropython.exe" @@ -15,13 +19,39 @@ else: MICROPYTHON = os.getenv("MICROPY_MICROPYTHON", "../ports/unix/build-standard/micropython") +injected_bench_code = b""" +import time + +class bench_class: + ITERS = 20000000 + + @staticmethod + def run(test): + t = time.ticks_us() + test(bench_class.ITERS) + t = time.ticks_diff(time.ticks_us(), t) + s, us = divmod(t, 1_000_000) + print("{}.{:06}".format(s, us)) + +import sys +sys.modules['bench'] = bench_class +""" + -def run_tests(pyb, test_dict): +def execbench(pyb, filename, iters): + with open(filename, "rb") as f: + pyfile = f.read() + code = (injected_bench_code + pyfile).replace(b"20000000", str(iters).encode("utf-8")) + return pyb.exec(code).replace(b"\r\n", b"\n") + + +def run_tests(pyb, test_dict, iters): test_count = 0 testcase_count = 0 for base_test, tests in sorted(test_dict.items()): print(base_test + ":") + baseline = None for test_file in tests: # run MicroPython if pyb is None: @@ -36,20 +66,25 @@ def run_tests(pyb, test_dict): # run on pyboard pyb.enter_raw_repl() try: - output_mupy = pyb.execfile(test_file).replace(b"\r\n", b"\n") + output_mupy = execbench(pyb, test_file[0], iters) except pyboard.PyboardError: output_mupy = b"CRASH" - output_mupy = float(output_mupy.strip()) + try: + output_mupy = float(output_mupy.strip()) + except ValueError: + output_mupy = -1 test_file[1] = output_mupy testcase_count += 1 - test_count += 1 - baseline = None - for t in tests: if baseline is None: - baseline = t[1] - print(" %.3fs (%+06.2f%%) %s" % (t[1], (t[1] * 100 / baseline) - 100, t[0])) + baseline = test_file[1] + print( + " %.3fs (%+06.2f%%) %s" + % (test_file[1], (test_file[1] * 100 / baseline) - 100, test_file[0]) + ) + + test_count += 1 print("{} tests performed ({} individual testcases)".format(test_count, testcase_count)) @@ -58,27 +93,47 @@ def run_tests(pyb, test_dict): def main(): - cmd_parser = argparse.ArgumentParser(description="Run tests for MicroPython.") - cmd_parser.add_argument("--pyboard", action="store_true", help="run the tests on the pyboard") + cmd_parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=f"""Run and manage tests for MicroPython. + +{run_tests_module.test_instance_description} +{run_tests_module.test_directory_description} +""", + epilog=run_tests_module.test_instance_epilog, + ) + cmd_parser.add_argument( + "-t", "--test-instance", default="unix", help="the MicroPython instance to test" + ) + cmd_parser.add_argument( + "-b", "--baudrate", default=115200, help="the baud rate of the serial device" + ) + cmd_parser.add_argument("-u", "--user", default="micro", help="the telnet login username") + cmd_parser.add_argument("-p", "--password", default="python", help="the telnet login password") + cmd_parser.add_argument( + "-d", "--test-dirs", nargs="*", help="input test directories (if no files given)" + ) + cmd_parser.add_argument( + "-I", + "--iters", + type=int, + default=200_000, + help="number of test iterations, only for remote instances (default 200,000)", + ) cmd_parser.add_argument("files", nargs="*", help="input test files") args = cmd_parser.parse_args() # Note pyboard support is copied over from run-tests.py, not tests, and likely needs revamping - if args.pyboard: - import pyboard - - pyb = pyboard.Pyboard("/dev/ttyACM0") - pyb.enter_raw_repl() - else: - pyb = None + pyb = run_tests_module.get_test_instance( + args.test_instance, args.baudrate, args.user, args.password + ) if len(args.files) == 0: - if pyb is None: - # run PC tests - test_dirs = ("internal_bench",) + if args.test_dirs: + test_dirs = tuple(args.test_dirs) else: - # run pyboard tests - test_dirs = ("basics", "float", "pyb") + test_dirs = ("internal_bench",) + tests = sorted( test_file for test_files in (glob("{}/*.py".format(dir)) for dir in test_dirs) @@ -95,7 +150,7 @@ def main(): continue test_dict[m.group(1)].append([t, None]) - if not run_tests(pyb, test_dict): + if not run_tests(pyb, test_dict, args.iters): sys.exit(1) diff --git a/tests/run-tests.py b/tests/run-tests.py index a428665db03e0..59aec327fa063 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -1145,29 +1145,11 @@ def __call__(self, parser, args, value, option): args.filters.append((option, re.compile(value))) -def main(): - global injected_import_hook_code - - cmd_parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description="""Run and manage tests for MicroPython. - +test_instance_description = """\ By default the tests are run against the unix port of MicroPython. To run it against something else, use the -t option. See below for details. - -Tests are discovered by scanning test directories for .py files or using the -specified test files. If test files nor directories are specified, the script -expects to be ran in the tests directory (where this file is located) and the -builtin tests suitable for the target platform are ran. - -When running tests, run-tests.py compares the MicroPython output of the test with the output -produced by running the test through CPython unless a .exp file is found, in which -case it is used as comparison. - -If a test fails, run-tests.py produces a pair of .out and .exp files in the result -directory with the MicroPython output and the expectations, respectively. -""", - epilog="""\ +""" +test_instance_epilog = """\ The -t option accepts the following for the test instance: - unix - use the unix port of MicroPython, specified by the MICROPY_MICROPYTHON environment variable (which defaults to the standard variant of either the unix @@ -1183,7 +1165,35 @@ def main(): - execpty: - execute a command and attach to the printed /dev/pts/ device - ... - connect to the given IPv4 address - anything else specifies a serial port +""" +test_directory_description = """\ +Tests are discovered by scanning test directories for .py files or using the +specified test files. If test files nor directories are specified, the script +expects to be ran in the tests directory (where this file is located) and the +builtin tests suitable for the target platform are ran. +""" + + +def main(): + global injected_import_hook_code + + cmd_parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=f"""Run and manage tests for MicroPython. + +{test_instance_description} +{test_directory_description} + +When running tests, run-tests.py compares the MicroPython output of the test with the output +produced by running the test through CPython unless a .exp file is found, in which +case it is used as comparison. + +If a test fails, run-tests.py produces a pair of .out and .exp files in the result +directory with the MicroPython output and the expectations, respectively. +""", + epilog=f"""\ +{test_instance_epilog} Options -i and -e can be multiple and processed in the order given. Regex "search" (vs "match") operation is used. An action (include/exclude) of the last matching regex is used: From ca9916968c06437fa39549b0995ceb811209f994 Mon Sep 17 00:00:00 2001 From: TianShuang Ke Date: Thu, 27 Jun 2024 22:36:18 +0800 Subject: [PATCH 010/143] esp32: Add support for ESP32-C2 (aka ESP8684). Includes: esp32/esp32c2: Adapt to target chip ESP32C2. esp32/esp32c2: Fix heap size is too small to enable Bluetooth. Signed-off-by: TianShuangKe Signed-off-by: Angus Gratton --- docs/esp32/quickref.rst | 3 +++ ports/esp32/README.md | 4 ++-- .../esp32/boards/ESP32_GENERIC_C2/board.json | 22 +++++++++++++++++++ ports/esp32/boards/ESP32_GENERIC_C2/board.md | 3 +++ .../ESP32_GENERIC_C2/mpconfigboard.cmake | 7 ++++++ .../boards/ESP32_GENERIC_C2/mpconfigboard.h | 7 ++++++ ports/esp32/boards/sdkconfig.base | 6 ++--- ports/esp32/boards/sdkconfig.c2 | 11 ++++++++++ ports/esp32/esp32_rmt.c | 6 ++++- ports/esp32/machine_adc.c | 6 +++++ ports/esp32/machine_bitstream.c | 11 ++++++---- ports/esp32/machine_i2c.c | 6 ++++- ports/esp32/machine_pin.c | 2 +- ports/esp32/machine_pin.h | 17 ++++++++++++++ ports/esp32/modesp32.c | 2 ++ ports/esp32/modmachine.c | 6 +++++ ports/esp32/mpconfigport.h | 4 +++- tests/extmod/machine_spi_rate.py | 2 +- tests/ports/esp32/esp32_idf_heap_info.py | 17 ++++++++++++-- tests/ports/esp32/esp32_idf_heap_info.py.exp | 4 ++-- 20 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 ports/esp32/boards/ESP32_GENERIC_C2/board.json create mode 100644 ports/esp32/boards/ESP32_GENERIC_C2/board.md create mode 100644 ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake create mode 100644 ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.h create mode 100644 ports/esp32/boards/sdkconfig.c2 diff --git a/docs/esp32/quickref.rst b/docs/esp32/quickref.rst index c394414a76f51..49f546a17c03f 100644 --- a/docs/esp32/quickref.rst +++ b/docs/esp32/quickref.rst @@ -802,6 +802,9 @@ The RMT is ESP32-specific and allows generation of accurate digital pulses with # The channel resolution is 100ns (1/(source_freq/clock_div)). r.write_pulses((1, 20, 2, 40), 0) # Send 0 for 100ns, 1 for 2000ns, 0 for 200ns, 1 for 4000ns +The ESP32-C2 family does not include any RMT peripheral, so this class is +unavailable on those SoCs. + OneWire driver -------------- diff --git a/ports/esp32/README.md b/ports/esp32/README.md index 4adff66328df2..dd4584772cf84 100644 --- a/ports/esp32/README.md +++ b/ports/esp32/README.md @@ -5,8 +5,8 @@ This is a port of MicroPython to the Espressif ESP32 series of microcontrollers. It uses the ESP-IDF framework and MicroPython runs as a task under FreeRTOS. -Currently supports ESP32, ESP32-C3, ESP32-C6, ESP32-S2 and ESP32-S3 -(ESP8266 is supported by a separate MicroPython port). +Currently supports ESP32, ESP32-C2 (aka ESP8684), ESP32-C3, ESP32-C6, ESP32-S2 +and ESP32-S3 (ESP8266 is supported by a separate MicroPython port). Supported features include: - REPL (Python prompt) over UART0. diff --git a/ports/esp32/boards/ESP32_GENERIC_C2/board.json b/ports/esp32/boards/ESP32_GENERIC_C2/board.json new file mode 100644 index 0000000000000..da0931a0e4459 --- /dev/null +++ b/ports/esp32/boards/ESP32_GENERIC_C2/board.json @@ -0,0 +1,22 @@ +{ + "deploy": [ + "../deploy.md" + ], + "deploy_options": { + "flash_offset": "0" + }, + "docs": "", + "features": [ + "BLE", + "External Flash", + "WiFi" + ], + "images": [ + "esp32c2_devkitmini.jpg" + ], + "mcu": "esp32c2", + "product": "ESP32-C2", + "thumbnail": "", + "url": "https://www.espressif.com/en/products/modules", + "vendor": "Espressif" +} diff --git a/ports/esp32/boards/ESP32_GENERIC_C2/board.md b/ports/esp32/boards/ESP32_GENERIC_C2/board.md new file mode 100644 index 0000000000000..b8ed10069feb3 --- /dev/null +++ b/ports/esp32/boards/ESP32_GENERIC_C2/board.md @@ -0,0 +1,3 @@ +The following files are firmware images that should work on most ESP32-C2-based +boards with at least 4MiB of flash and 26MHz crystal frequency. This includes +ESP8684-WROOM and ESP8684-MINI modules. diff --git a/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake new file mode 100644 index 0000000000000..e9d4989959f0f --- /dev/null +++ b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake @@ -0,0 +1,7 @@ +set(IDF_TARGET esp32c2) + +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.ble + boards/sdkconfig.c2 +) diff --git a/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.h b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.h new file mode 100644 index 0000000000000..ee918aaba5493 --- /dev/null +++ b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.h @@ -0,0 +1,7 @@ +// This configuration is for a generic ESP32C2 board with 4MiB (or more) of flash. + +#define MICROPY_HW_BOARD_NAME "ESP32C2 module" +#define MICROPY_HW_MCU_NAME "ESP32C2" + +#define MICROPY_HW_ENABLE_SDCARD (0) +#define MICROPY_PY_MACHINE_I2S (0) diff --git a/ports/esp32/boards/sdkconfig.base b/ports/esp32/boards/sdkconfig.base index 30740af434ddb..69abc63bdf008 100644 --- a/ports/esp32/boards/sdkconfig.base +++ b/ports/esp32/boards/sdkconfig.base @@ -125,9 +125,9 @@ CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y CONFIG_ETH_SPI_ETHERNET_DM9051=y # Using newlib "nano" formatting saves size on SoCs where "nano" formatting -# functions are in ROM. Note some newer chips (c2,c6) have "full" newlib -# formatting in ROM instead and should override this, check -# ESP_ROM_HAS_NEWLIB_NANO_FORMAT. +# functions are in ROM. ESP32-C6 (and possibly other new chips) have "full" +# newlib formatting in ROM instead and should override this, check +# ESP_ROM_HAS_NEWLIB_NANO_FORMAT in ESP-IDF. CONFIG_NEWLIB_NANO_FORMAT=y # IRAM/DRAM split protection is a memory protection feature on some parts diff --git a/ports/esp32/boards/sdkconfig.c2 b/ports/esp32/boards/sdkconfig.c2 new file mode 100644 index 0000000000000..e85bcdba35af2 --- /dev/null +++ b/ports/esp32/boards/sdkconfig.c2 @@ -0,0 +1,11 @@ +# +# Main XTAL Config +# +CONFIG_XTAL_FREQ_26=y +# CONFIG_XTAL_FREQ_40 is not set +CONFIG_XTAL_FREQ=26 +# Increase NimBLE stack size for functional BT +CONFIG_BT_NIMBLE_TASK_STACK_SIZE=5120 + +# Decrease mDNS stack size to save RAM +CONFIG_MDNS_TASK_STACK_SIZE=3072 diff --git a/ports/esp32/esp32_rmt.c b/ports/esp32/esp32_rmt.c index 6890e16bf79f7..f3bfbecdd11d1 100644 --- a/ports/esp32/esp32_rmt.c +++ b/ports/esp32/esp32_rmt.c @@ -30,6 +30,8 @@ #include "modesp32.h" #include "esp_task.h" + +#if SOC_RMT_SUPPORTED #include "driver/rmt.h" // This exposes the ESP32's RMT module to MicroPython. RMT is provided by the Espressif ESP-IDF: @@ -105,7 +107,7 @@ esp_err_t rmt_driver_install_core1(uint8_t channel_id) { return rmt_driver_install(channel_id, 0, 0); } -#endif +#endif // MP_TASK_COREID==0 static mp_obj_t esp32_rmt_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { static const mp_arg_t allowed_args[] = { @@ -387,3 +389,5 @@ MP_DEFINE_CONST_OBJ_TYPE( print, esp32_rmt_print, locals_dict, &esp32_rmt_locals_dict ); + +#endif // SOC_RMT_SUPPORTED diff --git a/ports/esp32/machine_adc.c b/ports/esp32/machine_adc.c index 02acaa22da035..c5575d45ec7cc 100644 --- a/ports/esp32/machine_adc.c +++ b/ports/esp32/machine_adc.c @@ -87,6 +87,12 @@ static const machine_adc_obj_t madc_obj[] = { {{&machine_adc_type}, ADCBLOCK2, ADC_CHANNEL_7, GPIO_NUM_27}, {{&machine_adc_type}, ADCBLOCK2, ADC_CHANNEL_8, GPIO_NUM_25}, {{&machine_adc_type}, ADCBLOCK2, ADC_CHANNEL_9, GPIO_NUM_26}, + #elif CONFIG_IDF_TARGET_ESP32C2 + {{&machine_adc_type}, ADCBLOCK1, ADC_CHANNEL_0, GPIO_NUM_0}, + {{&machine_adc_type}, ADCBLOCK1, ADC_CHANNEL_1, GPIO_NUM_1}, + {{&machine_adc_type}, ADCBLOCK1, ADC_CHANNEL_2, GPIO_NUM_2}, + {{&machine_adc_type}, ADCBLOCK1, ADC_CHANNEL_3, GPIO_NUM_3}, + {{&machine_adc_type}, ADCBLOCK1, ADC_CHANNEL_4, GPIO_NUM_4}, #elif CONFIG_IDF_TARGET_ESP32C3 {{&machine_adc_type}, ADCBLOCK1, ADC_CHANNEL_0, GPIO_NUM_0}, {{&machine_adc_type}, ADCBLOCK1, ADC_CHANNEL_1, GPIO_NUM_1}, diff --git a/ports/esp32/machine_bitstream.c b/ports/esp32/machine_bitstream.c index 6296ff06708c1..ed7fcc407df6c 100644 --- a/ports/esp32/machine_bitstream.c +++ b/ports/esp32/machine_bitstream.c @@ -90,6 +90,7 @@ static void IRAM_ATTR machine_bitstream_high_low_bitbang(mp_hal_pin_obj_t pin, u mp_hal_quiet_timing_exit(irq_state); } +#if SOC_RMT_SUPPORTED /******************************************************************************/ // RMT implementation @@ -172,16 +173,18 @@ static void machine_bitstream_high_low_rmt(mp_hal_pin_obj_t pin, uint32_t *timin // Cancel RMT output to GPIO pin. esp_rom_gpio_connect_out_signal(pin, SIG_GPIO_OUT_IDX, false, false); } - +#endif /******************************************************************************/ // Interface to machine.bitstream void machine_bitstream_high_low(mp_hal_pin_obj_t pin, uint32_t *timing_ns, const uint8_t *buf, size_t len) { - if (esp32_rmt_bitstream_channel_id < 0) { - machine_bitstream_high_low_bitbang(pin, timing_ns, buf, len); - } else { + #if SOC_RMT_SUPPORTED + if (esp32_rmt_bitstream_channel_id >= 0) { machine_bitstream_high_low_rmt(pin, timing_ns, buf, len, esp32_rmt_bitstream_channel_id); + return; } + #endif + machine_bitstream_high_low_bitbang(pin, timing_ns, buf, len); } #endif // MICROPY_PY_MACHINE_BITSTREAM diff --git a/ports/esp32/machine_i2c.c b/ports/esp32/machine_i2c.c index 259101ee7e145..4a5fd717631a9 100644 --- a/ports/esp32/machine_i2c.c +++ b/ports/esp32/machine_i2c.c @@ -55,7 +55,11 @@ #endif #if SOC_I2C_SUPPORT_XTAL -#define I2C_SCLK_FREQ XTAL_CLK_FREQ +#if CONFIG_XTAL_FREQ > 0 +#define I2C_SCLK_FREQ (CONFIG_XTAL_FREQ * 1000000) +#else +#error "I2C uses XTAL but no configured freq" +#endif // CONFIG_XTAL_FREQ #elif SOC_I2C_SUPPORT_APB #define I2C_SCLK_FREQ APB_CLK_FREQ #else diff --git a/ports/esp32/machine_pin.c b/ports/esp32/machine_pin.c index 4ab79f0a26460..9999223b59d50 100644 --- a/ports/esp32/machine_pin.c +++ b/ports/esp32/machine_pin.c @@ -152,7 +152,7 @@ static mp_obj_t machine_pin_obj_init_helper(const machine_pin_obj_t *self, size_ // reset the pin to digital if this is a mode-setting init (grab it back from ADC) if (args[ARG_mode].u_obj != mp_const_none) { if (rtc_gpio_is_valid_gpio(index)) { - #if !(CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6) + #if !(CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6) rtc_gpio_deinit(index); #endif } diff --git a/ports/esp32/machine_pin.h b/ports/esp32/machine_pin.h index 47f1ddebebf44..6fe9ef0e7787b 100644 --- a/ports/esp32/machine_pin.h +++ b/ports/esp32/machine_pin.h @@ -71,6 +71,23 @@ #define MICROPY_HW_ENABLE_GPIO38 (1) #define MICROPY_HW_ENABLE_GPIO39 (1) +#elif CONFIG_IDF_TARGET_ESP32C2 + +#define MICROPY_HW_ENABLE_GPIO0 (1) +#define MICROPY_HW_ENABLE_GPIO1 (1) +#define MICROPY_HW_ENABLE_GPIO2 (1) +#define MICROPY_HW_ENABLE_GPIO3 (1) +#define MICROPY_HW_ENABLE_GPIO4 (1) +#define MICROPY_HW_ENABLE_GPIO5 (1) +#define MICROPY_HW_ENABLE_GPIO6 (1) +#define MICROPY_HW_ENABLE_GPIO7 (1) +#define MICROPY_HW_ENABLE_GPIO8 (1) +#define MICROPY_HW_ENABLE_GPIO9 (1) +#define MICROPY_HW_ENABLE_GPIO10 (1) +#define MICROPY_HW_ENABLE_GPIO18 (1) +#define MICROPY_HW_ENABLE_GPIO19 (1) // UART0_RXD +#define MICROPY_HW_ENABLE_GPIO20 (1) // UART0_TXD + #elif CONFIG_IDF_TARGET_ESP32C3 #define MICROPY_HW_ENABLE_GPIO0 (1) diff --git a/ports/esp32/modesp32.c b/ports/esp32/modesp32.c index fcd6ed9fa83b8..bf7aec3944228 100644 --- a/ports/esp32/modesp32.c +++ b/ports/esp32/modesp32.c @@ -299,7 +299,9 @@ static const mp_rom_map_elem_t esp32_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_NVS), MP_ROM_PTR(&esp32_nvs_type) }, { MP_ROM_QSTR(MP_QSTR_Partition), MP_ROM_PTR(&esp32_partition_type) }, + #if SOC_RMT_SUPPORTED { MP_ROM_QSTR(MP_QSTR_RMT), MP_ROM_PTR(&esp32_rmt_type) }, + #endif #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 { MP_ROM_QSTR(MP_QSTR_ULP), MP_ROM_PTR(&esp32_ulp_type) }, #endif diff --git a/ports/esp32/modmachine.c b/ports/esp32/modmachine.c index 0d7ea44c66960..06360e8e8999c 100644 --- a/ports/esp32/modmachine.c +++ b/ports/esp32/modmachine.c @@ -99,6 +99,11 @@ static mp_obj_t mp_machine_get_freq(void) { static void mp_machine_set_freq(size_t n_args, const mp_obj_t *args) { mp_int_t freq = mp_obj_get_int(args[0]) / 1000000; + #if CONFIG_IDF_TARGET_ESP32C2 + if (freq != 80 && freq != 120) { + mp_raise_ValueError(MP_ERROR_TEXT("frequency must be 80MHz or 120MHz")); + } + #else if (freq != 20 && freq != 40 && freq != 80 && freq != 160 #if !(CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6) && freq != 240 @@ -110,6 +115,7 @@ static void mp_machine_set_freq(size_t n_args, const mp_obj_t *args) { mp_raise_ValueError(MP_ERROR_TEXT("frequency must be 20MHz, 40MHz, 80Mhz, 160MHz or 240MHz")); #endif } + #endif esp_pm_config_t pm = { .max_freq_mhz = freq, .min_freq_mhz = freq, diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index 721f22de11c55..142c1b1be2eb6 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -32,7 +32,7 @@ #ifndef MICROPY_GC_INITIAL_HEAP_SIZE #if CONFIG_IDF_TARGET_ESP32 #define MICROPY_GC_INITIAL_HEAP_SIZE (56 * 1024) -#elif CONFIG_IDF_TARGET_ESP32S2 && !CONFIG_SPIRAM +#elif CONFIG_IDF_TARGET_ESP32C2 || (CONFIG_IDF_TARGET_ESP32S2 && !CONFIG_SPIRAM) #define MICROPY_GC_INITIAL_HEAP_SIZE (36 * 1024) #else #define MICROPY_GC_INITIAL_HEAP_SIZE (64 * 1024) @@ -165,6 +165,8 @@ #define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "mpy-esp32s2" #elif CONFIG_IDF_TARGET_ESP32S3 #define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "mpy-esp32s3" +#elif CONFIG_IDF_TARGET_ESP32C2 +#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "mpy-esp32c2" #elif CONFIG_IDF_TARGET_ESP32C3 #define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "mpy-esp32c3" #elif CONFIG_IDF_TARGET_ESP32C6 diff --git a/tests/extmod/machine_spi_rate.py b/tests/extmod/machine_spi_rate.py index c65095f22a1a5..fe15b66fe648a 100644 --- a/tests/extmod/machine_spi_rate.py +++ b/tests/extmod/machine_spi_rate.py @@ -25,7 +25,7 @@ spi_instances = ((0, Pin(18), Pin(19), Pin(16)),) elif "esp32" in sys.platform: impl = str(sys.implementation) - if "ESP32C3" in impl or "ESP32C6" in impl: + if any(soc in impl for soc in ("ESP32C2", "ESP32C3", "ESP32C6")): spi_instances = ((1, Pin(4), Pin(5), Pin(6)),) else: spi_instances = ((1, Pin(18), Pin(19), Pin(21)), (2, Pin(18), Pin(19), Pin(21))) diff --git a/tests/ports/esp32/esp32_idf_heap_info.py b/tests/ports/esp32/esp32_idf_heap_info.py index fdd89161f4310..2f45295938df7 100644 --- a/tests/ports/esp32/esp32_idf_heap_info.py +++ b/tests/ports/esp32/esp32_idf_heap_info.py @@ -5,6 +5,19 @@ print("SKIP") raise SystemExit +import sys + +# idf_heap_info() is expected to return at least this many +# regions for HEAP_DATA and HEAP_EXEC, respectively. +MIN_DATA = 3 +MIN_EXEC = 3 + +impl = str(sys.implementation) +if "ESP32C2" in impl: + # ESP32-C2 is less fragmented (yay!) and only has two memory regions + MIN_DATA = 2 + MIN_EXEC = 2 + # region tuple is: (size, free, largest free, min free) # we check that each region's size is > 0 and that the free amounts are smaller than the size @@ -22,12 +35,12 @@ def chk_heap(kind, regions): # try getting heap regions regions = esp32.idf_heap_info(esp32.HEAP_DATA) -print("HEAP_DATA >2:", len(regions) > 2) +print("HEAP_DATA >=MIN:", len(regions) >= MIN_DATA) chk_heap("HEAP_DATA", regions) # try getting code regions regions = esp32.idf_heap_info(esp32.HEAP_EXEC) -print("HEAP_EXEC >2:", len(regions) > 2) +print("HEAP_EXEC >=MIN:", len(regions) >= MIN_EXEC) chk_heap("HEAP_EXEC", regions) # try invalid param diff --git a/tests/ports/esp32/esp32_idf_heap_info.py.exp b/tests/ports/esp32/esp32_idf_heap_info.py.exp index 2b63bf3259fc6..7fc50fc1624df 100644 --- a/tests/ports/esp32/esp32_idf_heap_info.py.exp +++ b/tests/ports/esp32/esp32_idf_heap_info.py.exp @@ -1,5 +1,5 @@ -HEAP_DATA >2: True +HEAP_DATA >=MIN: True HEAP_DATA [True, True, True, True] -HEAP_EXEC >2: True +HEAP_EXEC >=MIN: True HEAP_EXEC [True, True, True, True] [] From 77c9eb7795da13dfe499f45f19e867362799b38a Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Fri, 25 Jul 2025 13:22:00 +1000 Subject: [PATCH 011/143] esp32: Add "Free RAM" optimisation config flags. This is necessary for ESP32-C2 Wi-Fi & BT to work reliably (and for TLS to work at all). On IDF 5.4.2 the free static RAM goes from 60KB to 100KB, and there will also be a reduction in lwIP & Wi-Fi memory use at runtime. The performance trade-off seems low for most use cases, although it will probably be significant for certain combinations of load (i.e. heavy TCP/IP, heavy BT throughput, and some peripheral driver functions). Added as a set of config flags because this is potentially useful on other SoCs where the goal is to maximise RAM available for MicroPython. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- .../ESP32_GENERIC_C2/mpconfigboard.cmake | 2 + ports/esp32/boards/sdkconfig.free_ram | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 ports/esp32/boards/sdkconfig.free_ram diff --git a/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake index e9d4989959f0f..d935c22b886a0 100644 --- a/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake +++ b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake @@ -4,4 +4,6 @@ set(SDKCONFIG_DEFAULTS boards/sdkconfig.base boards/sdkconfig.ble boards/sdkconfig.c2 + # C2 has unusably low free RAM without these optimisations + boards/sdkconfig.free_ram ) diff --git a/ports/esp32/boards/sdkconfig.free_ram b/ports/esp32/boards/sdkconfig.free_ram new file mode 100644 index 0000000000000..36c6455e3e6da --- /dev/null +++ b/ports/esp32/boards/sdkconfig.free_ram @@ -0,0 +1,44 @@ +# This is a collection of sdkconfig settings that frees RAM at runtime, +# at the expense of performance. +# +# Not all options will work on all SoC families, but adding this sdkconfig +# set to a board should increase the free memory. +# +# - Many options free IRAM, which on most ESP32 families leads to +# free DRAM at runtime (original ESP32 and S2 may not). +# - The other options reduce runtime DRAM usage from the heap. +# +# IMPORTANT: If you enable these config settings on a custom build then you may +# encounter bugs or crashes. If you choose to open a MicroPython bug report then +# please mention these config settings! + +# Place functions in flash whenever possible to free IRAM +CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH=y +CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y +CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH=y + +# Use the SPI flash functions in ROM (when available). This may limit flash chip +# support and cause issues with some flash chips. Each SoC family has different +# set of chip support baked into ROM. +CONFIG_SPI_FLASH_ROM_IMPL=y + +# Run the Bluetooth controller from flash not IRAM +CONFIG_BT_CTRL_RUN_IN_FLASH_ONLY=y + +# lwIP adjustments to limit runtime memory usage (at expense of performance, and/or +# a reduction in number of active connections). +CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=16 +CONFIG_LWIP_MAX_SOCKETS=6 +CONFIG_LWIP_MAX_ACTIVE_TCP=8 + +# These lwIP values are recommended to scale relative to the Wi-Fi buffer numbers +CONFIG_LWIP_TCP_WND_DEFAULT=3072 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=3072 + +# Wi-Fi adjustments to reduce peak runtime memory usage, at expense of peak +# performance +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=8 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=12 +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=12 +CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=2 +CONFIG_ESP_WIFI_RX_BA_WIN=12 From d4399b3230c0020d0f5bc2dd42f19c4579f3bf3e Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 24 Jul 2025 12:35:24 +1000 Subject: [PATCH 012/143] esp32: Fix first line ESP32-C2 serial output after reset or deepsleep. ESP32-C2 ROM prints at 74880bps (same as ESP8266), so need a newline before first MicroPython output to avoid it being appended on end of a line of noise. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- .../boards/ESP32_GENERIC_C2/board_init.c | 35 +++++++++++++++++++ .../ESP32_GENERIC_C2/mpconfigboard.cmake | 5 +++ .../boards/ESP32_GENERIC_C2/mpconfigboard.h | 3 ++ ports/esp32/boards/sdkconfig.c2 | 6 ++++ 4 files changed, 49 insertions(+) create mode 100644 ports/esp32/boards/ESP32_GENERIC_C2/board_init.c diff --git a/ports/esp32/boards/ESP32_GENERIC_C2/board_init.c b/ports/esp32/boards/ESP32_GENERIC_C2/board_init.c new file mode 100644 index 0000000000000..355fe2bf0b24d --- /dev/null +++ b/ports/esp32/boards/ESP32_GENERIC_C2/board_init.c @@ -0,0 +1,35 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "py/mpconfig.h" + +void GENERIC_C2_board_startup(void) { + // With a 26MHz crystal the ESP32-C2 ROM prints output at 74880 which is + // interpreted mostly as noise, then boot.py output and/or the REPL banner + // prints at the end of a line of noise unless we inject a newline here + printf("\n"); + + boardctrl_startup(); +} diff --git a/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake index d935c22b886a0..7a8b0e0b3d4af 100644 --- a/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake +++ b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.cmake @@ -7,3 +7,8 @@ set(SDKCONFIG_DEFAULTS # C2 has unusably low free RAM without these optimisations boards/sdkconfig.free_ram ) + +set(MICROPY_SOURCE_BOARD + ${MICROPY_BOARD_DIR}/board_init.c +) + diff --git a/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.h b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.h index ee918aaba5493..999465373e817 100644 --- a/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.h +++ b/ports/esp32/boards/ESP32_GENERIC_C2/mpconfigboard.h @@ -5,3 +5,6 @@ #define MICROPY_HW_ENABLE_SDCARD (0) #define MICROPY_PY_MACHINE_I2S (0) + +#define MICROPY_BOARD_STARTUP GENERIC_C2_board_startup +void GENERIC_C2_board_startup(void); diff --git a/ports/esp32/boards/sdkconfig.c2 b/ports/esp32/boards/sdkconfig.c2 index e85bcdba35af2..194d815b6f27d 100644 --- a/ports/esp32/boards/sdkconfig.c2 +++ b/ports/esp32/boards/sdkconfig.c2 @@ -4,6 +4,12 @@ CONFIG_XTAL_FREQ_26=y # CONFIG_XTAL_FREQ_40 is not set CONFIG_XTAL_FREQ=26 + +# When using 26MHz crystal the baud rate defaults to 74880, +# same as ESP8266 - MicroPython uses 115200, so switch early +CONFIG_ESP_CONSOLE_UART_CUSTOM=y +CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 + # Increase NimBLE stack size for functional BT CONFIG_BT_NIMBLE_TASK_STACK_SIZE=5120 From ab4af2c1a61550cd300ff8dcd57fce6099f3338e Mon Sep 17 00:00:00 2001 From: Yanfeng Liu Date: Tue, 29 Jul 2025 18:10:17 +0800 Subject: [PATCH 013/143] py/mphal: Add stddef.h header for size_t. This includes "stddef.h" for `size_t` to resolve NuttX integration build issues. Signed-off-by: Yanfeng Liu --- py/mphal.h | 1 + 1 file changed, 1 insertion(+) diff --git a/py/mphal.h b/py/mphal.h index a4f222d0b1e11..d52e10be44c3c 100644 --- a/py/mphal.h +++ b/py/mphal.h @@ -27,6 +27,7 @@ #define MICROPY_INCLUDED_PY_MPHAL_H #include +#include #include "py/mpconfig.h" #ifdef MICROPY_MPHALPORT_H From 135c1cc7cd6077eda2dc860b3b7b51d742f6f630 Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 23 Jul 2025 14:44:11 +1000 Subject: [PATCH 014/143] extmod/modtls_mbedtls: Do gc_collect and retry ssl_init on any error. Contrary to the docs, mbedtls can return more than just MBEDTLS_ERR_SSL_ALLOC_FAILED when `mbedtls_ssl_setup()` fails. At least MBEDTLS_ERR_MD_ALLOC_FAILED was also seen on ESP32_GENERIC, but there could possibly be other error codes. To cover all these codes, just check if `ret` is non-0, and in that case do a `gc_collect()` and retry the init. Signed-off-by: Damien George --- extmod/modtls_mbedtls.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extmod/modtls_mbedtls.c b/extmod/modtls_mbedtls.c index 418275440f309..58634257328da 100644 --- a/extmod/modtls_mbedtls.c +++ b/extmod/modtls_mbedtls.c @@ -639,7 +639,7 @@ static mp_obj_t ssl_socket_make_new(mp_obj_ssl_context_t *ssl_context, mp_obj_t ret = mbedtls_ssl_setup(&o->ssl, &ssl_context->conf); #if !MICROPY_MBEDTLS_CONFIG_BARE_METAL - if (ret == MBEDTLS_ERR_SSL_ALLOC_FAILED) { + if (ret != 0) { // If mbedTLS relies on platform libc heap for buffers (i.e. esp32 // port), then run a GC pass and then try again. This is useful because // it may free a Python object (like an old SSL socket) whose finaliser From 68434b4be7ea9d212d5f402b75e2ffac8434ee04 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 15 Jul 2025 12:00:37 +1000 Subject: [PATCH 015/143] zephyr/mpconfigport_minimal: Use MICROPY_CONFIG_ROM_LEVEL_MINIMUM. This commit adjusts the configuration of the minimal zephyr build to use MICROPY_CONFIG_ROM_LEVEL_MINIMUM. That's a lot cleaner than explicitly enabling/disabling options. Prior to this change the minimal build for qemu_cortex_m3 had size: Memory region Used Size Region Size %age Used FLASH: 114436 B 256 KB 43.65% RAM: 26320 B 64 KB 40.16% and had the following test results (running using the CI settings, ie `-d basics float --exclude inf_nan_arith`): 352 tests performed (7092 individual testcases) 352 tests passed 254 tests skipped: ... With the changes here the qemu_cortex_m3 size is now: Memory region Used Size Region Size %age Used FLASH: 99428 B 256 KB 37.93% RAM: 26312 B 64 KB 40.15% That's a good decrease of about 15k firmware size. And the test suite still passes with: 342 tests performed (6776 individual testcases) 341 tests passed 265 tests skipped: ... Signed-off-by: Damien George --- ports/zephyr/mpconfigport_minimal.h | 37 +++++++++++++---------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/ports/zephyr/mpconfigport_minimal.h b/ports/zephyr/mpconfigport_minimal.h index a0a7f97394610..24e0c9f1adc05 100644 --- a/ports/zephyr/mpconfigport_minimal.h +++ b/ports/zephyr/mpconfigport_minimal.h @@ -30,41 +30,36 @@ // Included here to get basic Zephyr environment (macros, etc.) #include +// Use the minimum configuration level to get a small but useful system. +#ifndef MICROPY_CONFIG_ROM_LEVEL +#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_MINIMUM) +#endif + // Usually passed from Makefile #ifndef MICROPY_HEAP_SIZE #define MICROPY_HEAP_SIZE (16 * 1024) #endif +#define MICROPY_ENABLE_COMPILER (1) +#define MICROPY_ENABLE_EXTERNAL_IMPORT (1) #define MICROPY_STACK_CHECK (1) #define MICROPY_ENABLE_GC (1) #define MICROPY_HELPER_REPL (1) #define MICROPY_REPL_AUTO_INDENT (1) #define MICROPY_KBD_EXCEPTION (1) -#define MICROPY_CPYTHON_COMPAT (0) -#define MICROPY_PY_ASYNC_AWAIT (0) -#define MICROPY_PY_ATTRTUPLE (0) -#define MICROPY_PY_BUILTINS_ENUMERATE (0) -#define MICROPY_PY_BUILTINS_FILTER (0) -#define MICROPY_PY_BUILTINS_MIN_MAX (0) -#define MICROPY_PY_BUILTINS_PROPERTY (0) -#define MICROPY_PY_BUILTINS_RANGE_ATTRS (0) -#define MICROPY_PY_BUILTINS_REVERSED (0) -#define MICROPY_PY_BUILTINS_SET (0) -#define MICROPY_PY_BUILTINS_SLICE (0) -#define MICROPY_PY_ARRAY (0) -#define MICROPY_PY_COLLECTIONS (0) -#define MICROPY_PY_CMATH (0) -#define MICROPY_PY_IO (0) -#define MICROPY_PY_STRUCT (0) -#define MICROPY_PY_SYS_MODULES (0) #define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_LONGLONG) #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_FLOAT) #define MICROPY_PY_BUILTINS_COMPLEX (0) -// Saving extra crumbs to make sure binary fits in 128K -#define MICROPY_COMP_CONST_FOLDING (0) -#define MICROPY_COMP_CONST (0) -#define MICROPY_COMP_DOUBLE_TUPLE_ASSIGN (0) +// These features are enabled to get the test suite passing. +#define MICROPY_FULL_CHECKS (1) +#define MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG (1) +#define MICROPY_MULTIPLE_INHERITANCE (1) +#define MICROPY_PY_ASSIGN_EXPR (1) +#define MICROPY_PY_BUILTINS_STR_OP_MODULO (1) +#define MICROPY_PY_BUILTINS_BYTEARRAY (1) +#define MICROPY_PY_BUILTINS_DICT_FROMKEYS (1) +#define MICROPY_PY_SYS (1) #ifdef CONFIG_BOARD #define MICROPY_HW_BOARD_NAME "zephyr-" CONFIG_BOARD From 4360da16845051d6744669f1a33a715f147c1494 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 15 Jul 2025 12:30:09 +1000 Subject: [PATCH 016/143] zephyr/mpconfigport: Use MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES. This commit adjusts the configuration of the standard zephyr build to use MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES. That's a lot cleaner than explicitly enabling/disabling options, and allows boards to more easily fine-tune the settings, eg select a different feature level. Features that are now enabled are: - async/await keyword support - `filter`, `property` and `reversed` builtins - `range` attributes - `str.count()` method - `array` module with `array.array` object - `collections` module with `collections.namedtuple` object - `struct` module with everything - `id = const()` and constant folding in the compiler Bulding qemu_cortex_m3, the code size was originally: Memory region Used Size Region Size %age Used FLASH: 193864 B 256 KB 73.95% RAM: 61992 B 64 KB 94.59% and with this commit it is now: Memory region Used Size Region Size %age Used FLASH: 200698 B 256 KB 76.56% RAM: 61992 B 64 KB 94.59% That's a mild increase of +6834 bytes flash usage for a good selection of new features. Signed-off-by: Damien George --- ports/zephyr/mpconfigport.h | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/ports/zephyr/mpconfigport.h b/ports/zephyr/mpconfigport.h index 62226a2ded738..848e04b389e58 100644 --- a/ports/zephyr/mpconfigport.h +++ b/ports/zephyr/mpconfigport.h @@ -31,6 +31,11 @@ #include #include +// Use the basic configuration level to get a balance between size and features. +#ifndef MICROPY_CONFIG_ROM_LEVEL +#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_BASIC_FEATURES) +#endif + // Usually passed from Makefile #ifndef MICROPY_HEAP_SIZE #define MICROPY_HEAP_SIZE (16 * 1024) @@ -48,20 +53,11 @@ #define MICROPY_REPL_AUTO_INDENT (1) #define MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF (1) #define MICROPY_KBD_EXCEPTION (1) -#define MICROPY_PY_ASYNC_AWAIT (0) #define MICROPY_PY_BUILTINS_BYTES_HEX (1) -#define MICROPY_PY_BUILTINS_FILTER (0) -#define MICROPY_PY_BUILTINS_PROPERTY (0) -#define MICROPY_PY_BUILTINS_RANGE_ATTRS (0) -#define MICROPY_PY_BUILTINS_REVERSED (0) -#define MICROPY_PY_BUILTINS_STR_COUNT (0) #define MICROPY_PY_BUILTINS_MEMORYVIEW (1) #define MICROPY_PY_BUILTINS_HELP (1) #define MICROPY_PY_BUILTINS_HELP_TEXT zephyr_help_text -#define MICROPY_PY_ARRAY (0) #define MICROPY_PY_ARRAY_SLICE_ASSIGN (1) -#define MICROPY_PY_COLLECTIONS (0) -#define MICROPY_PY_CMATH (0) #define MICROPY_PY_MICROPYTHON_MEM_INFO (1) #define MICROPY_PY_MACHINE (1) #define MICROPY_PY_MACHINE_INCLUDEFILE "ports/zephyr/modmachine.c" @@ -80,7 +76,6 @@ #endif #define MICROPY_PY_MACHINE_PWM (1) #define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/zephyr/machine_pwm.c" -#define MICROPY_PY_STRUCT (0) #ifdef CONFIG_NETWORKING // If we have networking, we likely want errno comfort #define MICROPY_PY_ERRNO (1) @@ -96,7 +91,6 @@ #define MICROPY_PY_BINASCII (1) #define MICROPY_PY_HASHLIB (1) #define MICROPY_PY_OS (1) -#define MICROPY_PY_TIME (1) #define MICROPY_PY_TIME_TIME_TIME_NS (1) #define MICROPY_PY_TIME_INCLUDEFILE "ports/zephyr/modtime.c" #define MICROPY_PY_ZEPHYR (1) @@ -117,11 +111,6 @@ #define MICROPY_FATFS_RPATH (2) #define MICROPY_FATFS_NORTC (1) -// Saving extra crumbs to make sure binary fits in 128K -#define MICROPY_COMP_CONST_FOLDING (0) -#define MICROPY_COMP_CONST (0) -#define MICROPY_COMP_DOUBLE_TUPLE_ASSIGN (0) - // When CONFIG_THREAD_CUSTOM_DATA is enabled, MICROPY_PY_THREAD is enabled automatically #ifdef CONFIG_THREAD_CUSTOM_DATA #define MICROPY_PY_THREAD (1) From 6a8c45b6c4683fa2dc68cab62219b0edad4dd15d Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 20 Jul 2025 21:37:35 +1000 Subject: [PATCH 017/143] docs/library/bluetooth: Document all allowed args to UUID constructor. Signed-off-by: Damien George --- docs/library/bluetooth.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/library/bluetooth.rst b/docs/library/bluetooth.rst index b09c370abd46d..251ff399ecaa2 100644 --- a/docs/library/bluetooth.rst +++ b/docs/library/bluetooth.rst @@ -764,4 +764,5 @@ Constructor The **value** can be either: - A 16-bit integer. e.g. ``0x2908``. + - An object with the buffer protocol and that is 2, 4 or 16 bytes long, e.g. ``b'\x08\x29'``. - A 128-bit UUID string. e.g. ``'6E400001-B5A3-F393-E0A9-E50E24DCCA9E'``. From a9b038a57e2ca730dd95e79fc89491d0d1154e6e Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 20 Jul 2025 21:38:03 +1000 Subject: [PATCH 018/143] examples/bluetooth/ble_advertising.py: Fix decoding UUIDs. The UUID32 case was incorrect: first, the " --- examples/bluetooth/ble_advertising.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/bluetooth/ble_advertising.py b/examples/bluetooth/ble_advertising.py index 2fe17d640b9d2..f0b97d7f5d93e 100644 --- a/examples/bluetooth/ble_advertising.py +++ b/examples/bluetooth/ble_advertising.py @@ -79,12 +79,9 @@ def decode_name(payload): def decode_services(payload): services = [] - for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE): - services.append(bluetooth.UUID(struct.unpack(" Date: Tue, 22 Jul 2025 14:52:17 +1000 Subject: [PATCH 019/143] tests/run-natmodtests.py: Automatically skip tests that are too large. This follows a similar change made for `run-tests.py` in commit 229104558fb7f9d283b7302bc3720bc35c5c49cf. The change here uses the same logic to detect if a natmod test is too big for the target (eg overflows (I)RAM loading the native .mpy), by printing "START TEST" at the start of the test. Typical output is now something like this: ... pass extmod/random_basic.py pass extmod/random_extra_float.py pass extmod/random_extra.py SKIP extmod/random_seed_default.py LRGE extmod/re1.py SKIP extmod/re_debug.py pass extmod/re_error.py pass extmod/re_group.py pass extmod/re_groups.py ... and the tests that are too large are reported at the end, and written to the `_result.json` file. Signed-off-by: Damien George --- tests/run-natmodtests.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/run-natmodtests.py b/tests/run-natmodtests.py index f9d2074f6f02b..55adb6049a3e8 100755 --- a/tests/run-natmodtests.py +++ b/tests/run-natmodtests.py @@ -170,6 +170,7 @@ def run_tests(target_truth, target, args, resolved_arch): print("skip {} - mpy file not compiled".format(test_file)) continue test_script += bytes(injected_import_hook_code.format(test_module), "ascii") + test_script += b"print('START TEST')\n" test_script += test_file_data # Run test under MicroPython @@ -177,8 +178,18 @@ def run_tests(target_truth, target, args, resolved_arch): # Work out result of test extra = "" + result_out = result_out.removeprefix(b"START TEST\n") if error is None and result_out == b"SKIP\n": result = "SKIP" + elif ( + error is not None + and error.args[0] == "exception" + and error.args[1] == b"" + and b"MemoryError" in error.args[2] + ): + # Test had a MemoryError before anything (should be at least "START TEST") + # was printed, so the test is too big for the target. + result = "LRGE" elif error is not None: result = "FAIL" extra = " - " + str(error) @@ -203,6 +214,8 @@ def run_tests(target_truth, target, args, resolved_arch): test_results.append((test_file, "pass", "")) elif result == "SKIP": test_results.append((test_file, "skip", "")) + elif result == "LRGE": + test_results.append((test_file, "skip", "too large")) else: test_results.append((test_file, "fail", "")) From 4bdf2a2dc0b418aa014ef1c28b55a563f0c9f7e2 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Tue, 29 Jul 2025 10:30:10 +1000 Subject: [PATCH 020/143] tests/multi_bluetooth: Extend the deep sleep test timeout. As per comment, if a boot.py is present that connects to Wi-Fi then waking can take a little longer. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- tests/multi_bluetooth/stress_deepsleep_reconnect.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/multi_bluetooth/stress_deepsleep_reconnect.py b/tests/multi_bluetooth/stress_deepsleep_reconnect.py index 7c34c0360670f..b588b4000b4ed 100644 --- a/tests/multi_bluetooth/stress_deepsleep_reconnect.py +++ b/tests/multi_bluetooth/stress_deepsleep_reconnect.py @@ -5,7 +5,9 @@ from micropython import const import time, machine, bluetooth -TIMEOUT_MS = 4000 +# Note: This value can be much lower most of the time, but an ESP32 with a boot.py +# that connects to Wi-Fi may take an extra 5 seconds after reboot. +TIMEOUT_MS = 8000 _IRQ_CENTRAL_CONNECT = const(1) _IRQ_CENTRAL_DISCONNECT = const(2) From fdbd23268d69d77a411aa5c2d792eaf5e77d454a Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Tue, 29 Jul 2025 10:45:58 +1000 Subject: [PATCH 021/143] tests/run-multitests.py: Escape encoding errors instead of crashing. It's possible for a test to output non-ASCII characters (for example, due to a hard fault or serial noise or memory corruption). Rather than crashing the test runner, backslash escape those characters and treat them as program output. Refactors the string encoding step to a single helper to avoid copy-paste. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- tests/run-multitests.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/run-multitests.py b/tests/run-multitests.py index 92bd64193d8ce..4412d0fde7fda 100755 --- a/tests/run-multitests.py +++ b/tests/run-multitests.py @@ -132,6 +132,11 @@ def get_host_ip(_ip_cache=[]): return _ip_cache[0] +def decode(output): + # Convenience function to convert raw process or serial output to ASCII + return str(output, "ascii", "backslashreplace") + + class PyInstance: def __init__(self): pass @@ -190,7 +195,7 @@ def run_script(self, script): output = p.stdout except subprocess.CalledProcessError as er: err = er - return str(output.strip(), "ascii"), err + return decode(output.strip()), err def start_script(self, script): self.popen = subprocess.Popen( @@ -217,7 +222,7 @@ def readline(self): self.finished = self.popen.poll() is not None return None, None else: - return str(out.rstrip(), "ascii"), None + return decode(out.rstrip()), None def write(self, data): self.popen.stdin.write(data) @@ -229,7 +234,7 @@ def is_finished(self): def wait_finished(self): self.popen.wait() out = self.popen.stdout.read() - return str(out, "ascii"), "" + return decode(out), "" class PyInstancePyboard(PyInstance): @@ -264,7 +269,7 @@ def run_script(self, script): output = self.pyb.exec_(script) except pyboard.PyboardError as er: err = er - return str(output.strip(), "ascii"), err + return decode(output.strip()), err def start_script(self, script): self.pyb.enter_raw_repl() @@ -283,13 +288,13 @@ def readline(self): if out.endswith(b"\x04"): self.finished = True out = out[:-1] - err = str(self.pyb.read_until(1, b"\x04"), "ascii") + err = decode(self.pyb.read_until(1, b"\x04")) err = err[:-1] if not out and not err: return None, None else: err = None - return str(out.rstrip(), "ascii"), err + return decode(out.rstrip()), err def write(self, data): self.pyb.serial.write(data) @@ -299,7 +304,7 @@ def is_finished(self): def wait_finished(self): out, err = self.pyb.follow(10, None) - return str(out, "ascii"), str(err, "ascii") + return decode(out), decode(err) def prepare_test_file_list(test_files): From 241ee163c0c8a33267c699af59492641d0770f30 Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 25 Jul 2025 00:09:18 +1000 Subject: [PATCH 022/143] py/objboundmeth: Add option to use mp_is_equal instead of == comparison. This option is needed for ports such as webassembly where objects are proxied and can be identical without being the same C pointer. Signed-off-by: Damien George --- py/mpconfig.h | 8 ++++++++ py/objboundmeth.c | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/py/mpconfig.h b/py/mpconfig.h index 6ef6ec52e7cb6..c316aa4b2d894 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -1129,6 +1129,14 @@ typedef time_t mp_timestamp_t; #define MICROPY_PY_FUNCTION_ATTRS_CODE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES) #endif +// Whether bound_method can just use == (feature disabled), or requires a call to +// mp_obj_equal (feature enabled), to test equality of the self and meth entities. +// This is only needed if objects and functions can be identical without being the +// same thing, eg when using an object proxy. +#ifndef MICROPY_PY_BOUND_METHOD_FULL_EQUALITY_CHECK +#define MICROPY_PY_BOUND_METHOD_FULL_EQUALITY_CHECK (0) +#endif + // Whether to support the descriptors __get__, __set__, __delete__, __set_name__ // This costs some code size and makes load/store/delete of instance // attributes slower for the classes that use this feature diff --git a/py/objboundmeth.c b/py/objboundmeth.c index e3503ff154a65..6df67f7bf9e0c 100644 --- a/py/objboundmeth.c +++ b/py/objboundmeth.c @@ -102,7 +102,11 @@ static mp_obj_t bound_meth_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_ } mp_obj_bound_meth_t *lhs = MP_OBJ_TO_PTR(lhs_in); mp_obj_bound_meth_t *rhs = MP_OBJ_TO_PTR(rhs_in); + #if MICROPY_PY_BOUND_METHOD_FULL_EQUALITY_CHECK + return mp_obj_new_bool(mp_obj_equal(lhs->self, rhs->self) && mp_obj_equal(lhs->meth, rhs->meth)); + #else return mp_obj_new_bool(lhs->self == rhs->self && lhs->meth == rhs->meth); + #endif } #if MICROPY_PY_FUNCTION_ATTRS From 813f0c1cb9460502d0d8de24106f7ee8cc0723ee Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 21 Jul 2025 23:40:47 +1000 Subject: [PATCH 023/143] webassembly/objjsproxy: Implement equality for JsProxy objects. Signed-off-by: Damien George --- ports/webassembly/mpconfigport.h | 1 + ports/webassembly/objjsproxy.c | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/ports/webassembly/mpconfigport.h b/ports/webassembly/mpconfigport.h index eea6f02a02633..544e76f80540d 100644 --- a/ports/webassembly/mpconfigport.h +++ b/ports/webassembly/mpconfigport.h @@ -54,6 +54,7 @@ #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_DOUBLE) #define MICROPY_USE_INTERNAL_ERRNO (1) #define MICROPY_USE_INTERNAL_PRINTF (0) +#define MICROPY_PY_BOUND_METHOD_FULL_EQUALITY_CHECK (1) #define MICROPY_EPOCH_IS_1970 (1) #define MICROPY_PY_ASYNCIO_TASK_QUEUE_PUSH_CALLBACK (1) diff --git a/ports/webassembly/objjsproxy.c b/ports/webassembly/objjsproxy.c index 28fef901477c3..45f329d7e336b 100644 --- a/ports/webassembly/objjsproxy.c +++ b/ports/webassembly/objjsproxy.c @@ -266,6 +266,23 @@ static mp_obj_t jsproxy_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const } } +static mp_obj_t jsproxy_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t rhs_in) { + if (!mp_obj_is_type(rhs_in, &mp_type_jsproxy)) { + return MP_OBJ_NULL; // op not supported + } + + mp_obj_jsproxy_t *lhs = MP_OBJ_TO_PTR(lhs_in); + mp_obj_jsproxy_t *rhs = MP_OBJ_TO_PTR(rhs_in); + + switch (op) { + case MP_BINARY_OP_EQUAL: + return mp_obj_new_bool(lhs->ref == rhs->ref); + + default: + return MP_OBJ_NULL; // op not supported + } +} + EM_JS(void, proxy_js_free_obj, (int js_ref), { if (js_ref >= PROXY_JS_REF_NUM_STATIC) { proxy_js_ref[js_ref] = undefined; @@ -566,6 +583,7 @@ MP_DEFINE_CONST_OBJ_TYPE( MP_TYPE_FLAG_ITER_IS_GETITER, print, jsproxy_print, call, jsproxy_call, + binary_op, jsproxy_binary_op, attr, mp_obj_jsproxy_attr, subscr, jsproxy_subscr, iter, jsproxy_getiter From ffa98cb0143c43af9f4c61142784a08a19f660c5 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 21 Jul 2025 23:41:16 +1000 Subject: [PATCH 024/143] webassembly/proxy_js: Reuse JsProxy ref if object matches. This reduces memory use by reusing objects, and improves identity/equality relationships of JavaScript objects on the Python side. In 77bd8fe5b80b0e7e02cdb6b4272c401ae3dca638 PyProxy's were reused when the same Python object was proxied across to JavaScript. This commit does the same thing but for JsProxy's going from JS to Python. If an existing JsProxy reference exists for the JS object about to be proxied across, then it's reused. This helps reduce the number of alive objects (memory use), and, more importantly, improves equality relationships of JavaScript objects on the Python side. Eg we now get, on the Python side: import js print(js.Object == js.Object) that prints True. Previously it was False. Note that this change does not make identity work with `is`, for example `js.Object is js.Object` is actually False. With more work that could be made True but for now we leave that as-is. The behaviour with this commit matches Pyodide semantics. Signed-off-by: Damien George --- ports/webassembly/objjsproxy.c | 1 + ports/webassembly/proxy_js.js | 12 +++++++++++- tests/ports/webassembly/py_proxy_identity.mjs | 9 +++++++++ tests/ports/webassembly/py_proxy_identity.mjs.exp | 9 ++++++++- tests/ports/webassembly/run_python_async.mjs.exp | 6 +++--- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/ports/webassembly/objjsproxy.c b/ports/webassembly/objjsproxy.c index 45f329d7e336b..a8b21a744565c 100644 --- a/ports/webassembly/objjsproxy.c +++ b/ports/webassembly/objjsproxy.c @@ -285,6 +285,7 @@ static mp_obj_t jsproxy_binary_op(mp_binary_op_t op, mp_obj_t lhs_in, mp_obj_t r EM_JS(void, proxy_js_free_obj, (int js_ref), { if (js_ref >= PROXY_JS_REF_NUM_STATIC) { + proxy_js_ref_map.delete(proxy_js_ref[js_ref]); proxy_js_ref[js_ref] = undefined; if (js_ref < proxy_js_ref_next) { proxy_js_ref_next = js_ref; diff --git a/ports/webassembly/proxy_js.js b/ports/webassembly/proxy_js.js index 9e7c233e30bfc..cbd6e5b0088cc 100644 --- a/ports/webassembly/proxy_js.js +++ b/ports/webassembly/proxy_js.js @@ -62,6 +62,7 @@ class PythonError extends Error { function proxy_js_init() { globalThis.proxy_js_ref = [globalThis, undefined]; globalThis.proxy_js_ref_next = PROXY_JS_REF_NUM_STATIC; + globalThis.proxy_js_ref_map = new Map(); globalThis.proxy_js_map = new Map(); globalThis.proxy_js_existing = [undefined]; globalThis.pyProxyFinalizationRegistry = new FinalizationRegistry( @@ -95,8 +96,15 @@ function proxy_js_check_existing(c_ref) { return globalThis.proxy_js_existing.length - 1; } -// js_obj cannot be undefined +// The `js_obj` argument cannot be `undefined`. +// Returns an integer reference to the given `js_obj`. function proxy_js_add_obj(js_obj) { + // See if there is an existing JsProxy reference, and use that if there is. + const existing_ref = proxy_js_ref_map.get(js_obj); + if (existing_ref !== undefined) { + return existing_ref; + } + // Search for the first free slot in proxy_js_ref. while (proxy_js_ref_next < proxy_js_ref.length) { if (proxy_js_ref[proxy_js_ref_next] === undefined) { @@ -104,6 +112,7 @@ function proxy_js_add_obj(js_obj) { const id = proxy_js_ref_next; ++proxy_js_ref_next; proxy_js_ref[id] = js_obj; + proxy_js_ref_map.set(js_obj, id); return id; } ++proxy_js_ref_next; @@ -113,6 +122,7 @@ function proxy_js_add_obj(js_obj) { const id = proxy_js_ref.length; proxy_js_ref[id] = js_obj; proxy_js_ref_next = proxy_js_ref.length; + proxy_js_ref_map.set(js_obj, id); return id; } diff --git a/tests/ports/webassembly/py_proxy_identity.mjs b/tests/ports/webassembly/py_proxy_identity.mjs index d4a720b738a6c..97dab2e78367a 100644 --- a/tests/ports/webassembly/py_proxy_identity.mjs +++ b/tests/ports/webassembly/py_proxy_identity.mjs @@ -23,4 +23,13 @@ js.eventTarget.addEventListener("event", callback) js.eventTarget.dispatchEvent(js.event) js.eventTarget.removeEventListener("event", callback) js.eventTarget.dispatchEvent(js.event) + +print("Object equality") +print(js.Object == js.Object) +print(js.Object.assign == js.Object.assign) + +print("Array equality") +print(js.Array == js.Array) +print(js.Array.prototype == js.Array.prototype) +print(js.Array.prototype.push == js.Array.prototype.push) `); diff --git a/tests/ports/webassembly/py_proxy_identity.mjs.exp b/tests/ports/webassembly/py_proxy_identity.mjs.exp index 01ccf0d892653..344a0a202364c 100644 --- a/tests/ports/webassembly/py_proxy_identity.mjs.exp +++ b/tests/ports/webassembly/py_proxy_identity.mjs.exp @@ -1,3 +1,10 @@ PyProxy { _ref: 3 } PyProxy { _ref: 3 } true -callback +callback +Object equality +True +True +Array equality +True +True +True diff --git a/tests/ports/webassembly/run_python_async.mjs.exp b/tests/ports/webassembly/run_python_async.mjs.exp index ad6c49e336e5c..4dff64a605384 100644 --- a/tests/ports/webassembly/run_python_async.mjs.exp +++ b/tests/ports/webassembly/run_python_async.mjs.exp @@ -2,16 +2,16 @@ 1 py 1 - + py 2 2 resolved 123 3 = TEST 2 ========== 1 - + py 1 - + py 2 2 setTimeout resolved From e4e1c9f4132f839dac0291557d9b992f67577fd3 Mon Sep 17 00:00:00 2001 From: Yoctopuce dev Date: Fri, 4 Jul 2025 22:32:00 +0200 Subject: [PATCH 025/143] py/parsenum: Refactor float parsing code. This commit extracts from the current float parsing code two functions which could be reused elsewhere in MicroPython. The code used to multiply a float x by a power of 10 is also simplified by applying the binary exponent separately from the power of 5. This avoids the risk of overflow in the intermediate stage, before multiplying by x. Signed-off-by: Yoctopuce dev --- py/parsenum.c | 211 +++++++++++++++++++++++++++----------------------- py/parsenum.h | 5 ++ 2 files changed, 117 insertions(+), 99 deletions(-) diff --git a/py/parsenum.c b/py/parsenum.c index fcc69091737d0..019491b5132ae 100644 --- a/py/parsenum.c +++ b/py/parsenum.c @@ -195,6 +195,8 @@ mp_obj_t mp_parse_num_integer(const char *restrict str_, size_t len, int base, m } } +#if MICROPY_PY_BUILTINS_FLOAT + enum { REAL_IMAG_STATE_START = 0, REAL_IMAG_STATE_HAVE_REAL = 1, @@ -207,25 +209,39 @@ typedef enum { PARSE_DEC_IN_EXP, } parse_dec_in_t; -#if MICROPY_PY_BUILTINS_FLOAT // MANTISSA_MAX is used to retain precision while not overflowing mantissa -// SMALL_NORMAL_VAL is the smallest power of 10 that is still a normal float -// EXACT_POWER_OF_10 is the largest value of x so that 10^x can be stored exactly in a float -// Note: EXACT_POWER_OF_10 is at least floor(log_5(2^mantissa_length)). Indeed, 10^n = 2^n * 5^n -// so we only have to store the 5^n part in the mantissa (the 2^n part will go into the float's -// exponent). +#define MANTISSA_MAX (sizeof(mp_float_uint_t) == 8 ? 0x1999999999999998ULL : 0x19999998U) + +// MAX_EXACT_POWER_OF_5 is the largest value of x so that 5^x can be stored exactly in a float #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT -#define MANTISSA_MAX 0x19999998U -#define SMALL_NORMAL_VAL (1e-37F) -#define SMALL_NORMAL_EXP (-37) -#define EXACT_POWER_OF_10 (9) +#define MAX_EXACT_POWER_OF_5 (10) #elif MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE -#define MANTISSA_MAX 0x1999999999999998ULL -#define SMALL_NORMAL_VAL (1e-307) -#define SMALL_NORMAL_EXP (-307) -#define EXACT_POWER_OF_10 (22) +#define MAX_EXACT_POWER_OF_5 (22) #endif +// Helper to compute `num * (10.0 ** dec_exp)` +mp_float_t mp_decimal_exp(mp_float_t num, int dec_exp) { + + if (dec_exp == 0 || num == MICROPY_FLOAT_CONST(0.0)) { + return num; + } + mp_float_union_t res = {num}; + // Multiply first by (2.0 ** dec_exp) via the exponent + // - this will ensure that the result of `pow()` is always in mp_float_t range + // when the result is expected to be in mp_float_t range (e.g. during format) + // - we don't need to care about p.exp overflow, as (5.0 ** dec_exp) will anyway + // force the final result toward the proper edge if needed (0.0 or inf) + res.p.exp += dec_exp; + // Use positive exponents when they are more precise then negative + if (dec_exp < 0 && dec_exp >= -MAX_EXACT_POWER_OF_5) { + res.f /= MICROPY_FLOAT_C_FUN(pow)(5, -dec_exp); + } else { + res.f *= MICROPY_FLOAT_C_FUN(pow)(5, dec_exp); + } + return (mp_float_t)res.f; +} + + // Break out inner digit accumulation routine to ease trailing zero deferral. static mp_float_uint_t accept_digit(mp_float_uint_t p_mantissa, unsigned int dig, int *p_exp_extra, int in) { // Core routine to ingest an additional digit. @@ -244,6 +260,85 @@ static mp_float_uint_t accept_digit(mp_float_uint_t p_mantissa, unsigned int dig return p_mantissa; } } + +// Helper to parse an unsigned decimal number into a mp_float_t +const char *mp_parse_float_internal(const char *str, size_t len, mp_float_t *res) { + const char *top = str + len; + + parse_dec_in_t in = PARSE_DEC_IN_INTG; + bool exp_neg = false; + mp_float_uint_t mantissa = 0; + int exp_val = 0; + int exp_extra = 0; + int trailing_zeros_intg = 0, trailing_zeros_frac = 0; + while (str < top) { + unsigned int dig = *str++; + if ('0' <= dig && dig <= '9') { + dig -= '0'; + if (in == PARSE_DEC_IN_EXP) { + // don't overflow exp_val when adding next digit, instead just truncate + // it and the resulting float will still be correct, either inf or 0.0 + // (use INT_MAX/2 to allow adding exp_extra at the end without overflow) + if (exp_val < (INT_MAX / 2 - 9) / 10) { + exp_val = 10 * exp_val + dig; + } + } else { + if (dig == 0 || mantissa >= MANTISSA_MAX) { + // Defer treatment of zeros in fractional part. If nothing comes afterwards, ignore them. + // Also, once we reach MANTISSA_MAX, treat every additional digit as a trailing zero. + if (in == PARSE_DEC_IN_INTG) { + ++trailing_zeros_intg; + } else { + ++trailing_zeros_frac; + } + } else { + // Time to un-defer any trailing zeros. Intg zeros first. + while (trailing_zeros_intg) { + mantissa = accept_digit(mantissa, 0, &exp_extra, PARSE_DEC_IN_INTG); + --trailing_zeros_intg; + } + while (trailing_zeros_frac) { + mantissa = accept_digit(mantissa, 0, &exp_extra, PARSE_DEC_IN_FRAC); + --trailing_zeros_frac; + } + mantissa = accept_digit(mantissa, dig, &exp_extra, in); + } + } + } else if (in == PARSE_DEC_IN_INTG && dig == '.') { + in = PARSE_DEC_IN_FRAC; + } else if (in != PARSE_DEC_IN_EXP && ((dig | 0x20) == 'e')) { + in = PARSE_DEC_IN_EXP; + if (str < top) { + if (str[0] == '+') { + str++; + } else if (str[0] == '-') { + str++; + exp_neg = true; + } + } + if (str == top) { + return NULL; + } + } else if (dig == '_') { + continue; + } else { + // unknown character + str--; + break; + } + } + + // work out the exponent + if (exp_neg) { + exp_val = -exp_val; + } + exp_val += exp_extra + trailing_zeros_intg; + + // At this point, we just need to multiply the mantissa by its base 10 exponent. + *res = (mp_float_t)mp_decimal_exp(mantissa, exp_val); + + return str; +} #endif // MICROPY_PY_BUILTINS_FLOAT #if MICROPY_PY_BUILTINS_COMPLEX @@ -295,91 +390,9 @@ parse_start:; dec_val = MICROPY_FLOAT_C_FUN(nan)(""); } else { // string should be a decimal number - parse_dec_in_t in = PARSE_DEC_IN_INTG; - bool exp_neg = false; - mp_float_uint_t mantissa = 0; - int exp_val = 0; - int exp_extra = 0; - int trailing_zeros_intg = 0, trailing_zeros_frac = 0; - while (str < top) { - unsigned int dig = *str++; - if ('0' <= dig && dig <= '9') { - dig -= '0'; - if (in == PARSE_DEC_IN_EXP) { - // don't overflow exp_val when adding next digit, instead just truncate - // it and the resulting float will still be correct, either inf or 0.0 - // (use INT_MAX/2 to allow adding exp_extra at the end without overflow) - if (exp_val < (INT_MAX / 2 - 9) / 10) { - exp_val = 10 * exp_val + dig; - } - } else { - if (dig == 0 || mantissa >= MANTISSA_MAX) { - // Defer treatment of zeros in fractional part. If nothing comes afterwards, ignore them. - // Also, once we reach MANTISSA_MAX, treat every additional digit as a trailing zero. - if (in == PARSE_DEC_IN_INTG) { - ++trailing_zeros_intg; - } else { - ++trailing_zeros_frac; - } - } else { - // Time to un-defer any trailing zeros. Intg zeros first. - while (trailing_zeros_intg) { - mantissa = accept_digit(mantissa, 0, &exp_extra, PARSE_DEC_IN_INTG); - --trailing_zeros_intg; - } - while (trailing_zeros_frac) { - mantissa = accept_digit(mantissa, 0, &exp_extra, PARSE_DEC_IN_FRAC); - --trailing_zeros_frac; - } - mantissa = accept_digit(mantissa, dig, &exp_extra, in); - } - } - } else if (in == PARSE_DEC_IN_INTG && dig == '.') { - in = PARSE_DEC_IN_FRAC; - } else if (in != PARSE_DEC_IN_EXP && ((dig | 0x20) == 'e')) { - in = PARSE_DEC_IN_EXP; - if (str < top) { - if (str[0] == '+') { - str++; - } else if (str[0] == '-') { - str++; - exp_neg = true; - } - } - if (str == top) { - goto value_error; - } - } else if (dig == '_') { - continue; - } else { - // unknown character - str--; - break; - } - } - - // work out the exponent - if (exp_neg) { - exp_val = -exp_val; - } - - // apply the exponent, making sure it's not a subnormal value - exp_val += exp_extra + trailing_zeros_intg; - dec_val = (mp_float_t)mantissa; - if (exp_val < SMALL_NORMAL_EXP) { - exp_val -= SMALL_NORMAL_EXP; - dec_val *= SMALL_NORMAL_VAL; - } - - // At this point, we need to multiply the mantissa by its base 10 exponent. If possible, - // we would rather manipulate numbers that have an exact representation in IEEE754. It - // turns out small positive powers of 10 do, whereas small negative powers of 10 don't. - // So in that case, we'll yield a division of exact values rather than a multiplication - // of slightly erroneous values. - if (exp_val < 0 && exp_val >= -EXACT_POWER_OF_10) { - dec_val /= MICROPY_FLOAT_C_FUN(pow)(10, -exp_val); - } else { - dec_val *= MICROPY_FLOAT_C_FUN(pow)(10, exp_val); + str = mp_parse_float_internal(str, top - str, &dec_val); + if (!str) { + goto value_error; } } diff --git a/py/parsenum.h b/py/parsenum.h index f444632d23021..a807cb09d0572 100644 --- a/py/parsenum.h +++ b/py/parsenum.h @@ -34,6 +34,11 @@ mp_obj_t mp_parse_num_integer(const char *restrict str, size_t len, int base, mp_lexer_t *lex); +#if MICROPY_PY_BUILTINS_FLOAT +mp_float_t mp_decimal_exp(mp_float_t num, int dec_exp); +const char *mp_parse_float_internal(const char *str, size_t len, mp_float_t *res); +#endif + #if MICROPY_PY_BUILTINS_COMPLEX mp_obj_t mp_parse_num_decimal(const char *str, size_t len, bool allow_imag, bool force_complex, mp_lexer_t *lex); From dbbaa959c85c04dbbcde5908b5d0775b574e44e7 Mon Sep 17 00:00:00 2001 From: Yoctopuce dev Date: Fri, 6 Jun 2025 14:55:21 +0200 Subject: [PATCH 026/143] py/formatfloat: Improve accuracy of float formatting code. Following discussions in PR #16666, this commit updates the float formatting code to improve the `repr` reversibility, i.e. the percentage of valid floating point numbers that do parse back to the same number when formatted by `repr` (in CPython it's 100%). This new code offers a choice of 3 float conversion methods, depending on the desired tradeoff between code size and conversion precision: - BASIC method is the smallest code footprint - APPROX method uses an iterative method to approximate the exact representation, which is a bit slower but but does not have a big impact on code size. It provides `repr` reversibility on >99.8% of the cases in double precision, and on >98.5% in single precision (except with REPR_C, where reversibility is 100% as the last two bits are not taken into account). - EXACT method uses higher-precision floats during conversion, which provides perfect results but has a higher impact on code size. It is faster than APPROX method, and faster than the CPython equivalent implementation. It is however not available on all compilers when using FLOAT_IMPL_DOUBLE. Here is the table comparing the impact of the three conversion methods on code footprint on PYBV10 (using single-precision floats) and reversibility rate for both single-precision and double-precision floats. The table includes current situation as a baseline for the comparison: PYBV10 REPR_C FLOAT DOUBLE current = 364688 12.9% 27.6% 37.9% basic = 364812 85.6% 60.5% 85.7% approx = 365080 100.0% 98.5% 99.8% exact = 366408 100.0% 100.0% 100.0% Signed-off-by: Yoctopuce dev --- ports/esp8266/mpconfigport.h | 1 + ports/unix/coverage.c | 20 - py/formatfloat.c | 757 ++++++++++++++--------- py/formatfloat.h | 1 + py/misc.h | 19 + py/mpconfig.h | 21 + py/mpprint.c | 12 +- py/mpprint.h | 1 + py/objcomplex.c | 31 +- py/objfloat.c | 18 +- py/parsenum.c | 50 +- py/parsenum.h | 2 +- tests/float/float_format.py | 17 +- tests/float/float_format_accuracy.py | 73 +++ tests/float/float_format_ints.py | 32 +- tests/float/float_struct_e.py | 2 +- tests/float/float_struct_e_doubleprec.py | 43 ++ tests/float/float_struct_e_fp30.py | 43 ++ tests/float/string_format_modulo3.py | 2 +- tests/float/string_format_modulo3.py.exp | 2 - tests/ports/unix/extra_coverage.py.exp | 4 - tests/ports/unix/ffi_float2.py | 3 +- tests/ports/unix/ffi_float2.py.exp | 12 +- tests/run-tests.py | 4 + 24 files changed, 775 insertions(+), 395 deletions(-) create mode 100644 tests/float/float_format_accuracy.py create mode 100644 tests/float/float_struct_e_doubleprec.py create mode 100644 tests/float/float_struct_e_fp30.py delete mode 100644 tests/float/string_format_modulo3.py.exp diff --git a/ports/esp8266/mpconfigport.h b/ports/esp8266/mpconfigport.h index bc2957190262b..323fba67f5b07 100644 --- a/ports/esp8266/mpconfigport.h +++ b/ports/esp8266/mpconfigport.h @@ -106,6 +106,7 @@ #define MICROPY_PY_OS_URANDOM (1) #define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_MPZ) #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_FLOAT) +#define MICROPY_FLOAT_FORMAT_IMPL (MICROPY_FLOAT_FORMAT_IMPL_BASIC) #define MICROPY_WARNINGS (1) #define MICROPY_PY_STR_BYTES_CMP_WARN (1) #define MICROPY_STREAMS_POSIX_API (1) diff --git a/ports/unix/coverage.c b/ports/unix/coverage.c index f071049eded50..b7c3d2c25ef6e 100644 --- a/ports/unix/coverage.c +++ b/ports/unix/coverage.c @@ -612,26 +612,6 @@ static mp_obj_t extra_coverage(void) { mp_emitter_warning(MP_PASS_CODE_SIZE, "test"); } - // format float - { - mp_printf(&mp_plat_print, "# format float\n"); - - // format with inadequate buffer size - char buf[5]; - mp_format_float(1, buf, sizeof(buf), 'g', 0, '+'); - mp_printf(&mp_plat_print, "%s\n", buf); - - // format with just enough buffer so that precision must be - // set from 0 to 1 twice - char buf2[8]; - mp_format_float(1, buf2, sizeof(buf2), 'g', 0, '+'); - mp_printf(&mp_plat_print, "%s\n", buf2); - - // format where precision is trimmed to avoid buffer overflow - mp_format_float(1, buf2, sizeof(buf2), 'e', 0, '+'); - mp_printf(&mp_plat_print, "%s\n", buf2); - } - // binary { mp_printf(&mp_plat_print, "# binary\n"); diff --git a/py/formatfloat.c b/py/formatfloat.c index 7cd471018da98..1ea34f84bf7fb 100644 --- a/py/formatfloat.c +++ b/py/formatfloat.c @@ -33,392 +33,537 @@ #include #include #include "py/formatfloat.h" +#include "py/parsenum.h" /*********************************************************************** Routine for converting a arbitrary floating point number into a string. - The code in this function was inspired from Fred Bayer's pdouble.c. - Since pdouble.c was released as Public Domain, I'm releasing this - code as public domain as well. + The code in this function was inspired from Dave Hylands's previous + version, which was itself inspired from Fred Bayer's pdouble.c. The original code can be found in https://github.com/dhylands/format-float - Dave Hylands - ***********************************************************************/ -#if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT -// 1 sign bit, 8 exponent bits, and 23 mantissa bits. -// exponent values 0 and 255 are reserved, exponent can be 1 to 254. -// exponent is stored with a bias of 127. -// The min and max floats are on the order of 1x10^37 and 1x10^-37 - -#define FPTYPE float -#define FPCONST(x) x##F -#define FPROUND_TO_ONE 0.9999995F -#define FPDECEXP 32 -#define FPMIN_BUF_SIZE 6 // +9e+99 +// Float formatting debug code is intended for use in ports/unix only, +// as it uses the libc float printing function as a reference. +#define DEBUG_FLOAT_FORMATTING 0 + +#if DEBUG_FLOAT_FORMATTING +#define DEBUG_PRINTF(...) fprintf(stderr, __VA_ARGS__) +#else +#define DEBUG_PRINTF(...) +#endif + +#if MICROPY_FLOAT_FORMAT_IMPL == MICROPY_FLOAT_FORMAT_IMPL_EXACT || MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE +#define MP_FFUINT_FMT "%lu" +#else +#define MP_FFUINT_FMT "%u" +#endif + +static inline int fp_expval(mp_float_t x) { + mp_float_union_t fb = { x }; + return (int)fb.p.exp - MP_FLOAT_EXP_OFFSET; +} -#define FLT_SIGN_MASK 0x80000000 +#if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE -static inline int fp_signbit(float x) { - mp_float_union_t fb = {x}; - return fb.i & FLT_SIGN_MASK; +static inline int fp_isless1(mp_float_t x) { + return x < 1.0; } -#define fp_isnan(x) isnan(x) -#define fp_isinf(x) isinf(x) -static inline int fp_iszero(float x) { - mp_float_union_t fb = {x}; - return fb.i == 0; + +static inline int fp_iszero(mp_float_t x) { + return x == 0.0; } -static inline int fp_isless1(float x) { - mp_float_union_t fb = {x}; + +#if MICROPY_FLOAT_FORMAT_IMPL != MICROPY_FLOAT_FORMAT_IMPL_APPROX +static inline int fp_equal(mp_float_t x, mp_float_t y) { + return x == y; +} +#else +static inline mp_float_t fp_diff(mp_float_t x, mp_float_t y) { + return x - y; +} +#endif + +#elif MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT + +// The functions below are roughly equivalent to the ones above, +// but they are optimized to reduce code footprint by skipping +// handling for special values such as nan, inf, +/-0.0 +// for ports where FP support is done in software. +// +// They also take into account lost bits of REPR_C as needed. + +static inline int fp_isless1(mp_float_t x) { + mp_float_union_t fb = { x }; return fb.i < 0x3f800000; } -#elif MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE +static inline int fp_iszero(mp_float_t x) { + mp_float_union_t x_check = { x }; + return !x_check.i; // this is valid for REPR_C as well +} -#define FPTYPE double -#define FPCONST(x) x -#define FPROUND_TO_ONE 0.999999999995 -#define FPDECEXP 256 -#define FPMIN_BUF_SIZE 7 // +9e+199 -#define fp_signbit(x) signbit(x) -#define fp_isnan(x) isnan(x) -#define fp_isinf(x) isinf(x) -#define fp_iszero(x) (x == 0) -#define fp_isless1(x) (x < 1.0) +#if MICROPY_FLOAT_FORMAT_IMPL != MICROPY_FLOAT_FORMAT_IMPL_APPROX +static inline int fp_equal(mp_float_t x, mp_float_t y) { + mp_float_union_t x_check = { x }; + mp_float_union_t y_check = { y }; + #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_C + return (x_check.i & ~3) == (y_check.i & ~3); + #else + return x_check.i == y_check.i; + #endif +} +#else +static inline mp_float_t fp_diff(mp_float_t x, mp_float_t y) { + #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_C + mp_float_union_t x_check = { x }; + mp_float_union_t y_check = { y }; + x_check.i &= ~3; + y_check.i &= ~3; + return x_check.f - y_check.f; + #else + return x - y; + #endif +} +#endif -#endif // MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT/DOUBLE +#endif -static inline int fp_expval(FPTYPE x) { - mp_float_union_t fb = {x}; - return (int)((fb.i >> MP_FLOAT_FRAC_BITS) & (~(0xFFFFFFFF << MP_FLOAT_EXP_BITS))) - MP_FLOAT_EXP_OFFSET; +#if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT +#define FPMIN_BUF_SIZE 6 // +9e+99 +#define MAX_MANTISSA_DIGITS (9) +#define SAFE_MANTISSA_DIGITS (6) +#elif MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE +#define FPMIN_BUF_SIZE 7 // +9e+199 +#define MAX_MANTISSA_DIGITS (19) +#define SAFE_MANTISSA_DIGITS (16) +#endif + +// Internal formatting flags +#define FMT_MODE_E 0x01 // render using scientific notation (%e) +#define FMT_MODE_G 0x02 // render using general format (%g) +#define FMT_MODE_F 0x04 // render using using expanded fixed-point format (%f) +#define FMT_E_CASE 0x20 // don't change this value (used for case conversion!) + +static char *mp_prepend_zeros(char *s, int cnt) { + *s++ = '0'; + *s++ = '.'; + while (cnt > 0) { + *s++ = '0'; + cnt--; + } + return s; } -int mp_format_float(FPTYPE f, char *buf, size_t buf_size, char fmt, int prec, char sign) { +// Helper to convert a decimal mantissa (provided as an mp_large_float_uint_t) to string +static int mp_format_mantissa(mp_large_float_uint_t mantissa, mp_large_float_uint_t mantissa_cap, char *buf, char *s, + int num_digits, int max_exp_zeros, int trailing_zeros, int dec, int e, int fmt_flags) { - char *s = buf; + DEBUG_PRINTF("mantissa=" MP_FFUINT_FMT " exp=%d (cap=" MP_FFUINT_FMT "):\n", mantissa, e, mantissa_cap); - if (buf_size <= FPMIN_BUF_SIZE) { - // FPMIN_BUF_SIZE is the minimum size needed to store any FP number. - // If the buffer does not have enough room for this (plus null terminator) - // then don't try to format the float. + if (mantissa) { + // If rounding/searching created an extra digit or removed too many, fix mantissa first + if (mantissa >= mantissa_cap) { + if (fmt_flags & FMT_MODE_F) { + assert(e >= 0); + num_digits++; + dec++; + } else { + mantissa /= 10; + e++; + } + } + } - if (buf_size >= 2) { - *s++ = '?'; + // When 'g' format is used, replace small exponents by explicit zeros + if ((fmt_flags & FMT_MODE_G) && e != 0) { + if (e >= 0) { + // If 0 < e < max_exp_zeros, expand positive exponent into trailing zeros + if (e < max_exp_zeros) { + dec += e; + if (dec >= num_digits) { + trailing_zeros = dec - (num_digits - 1); + } + e = 0; + } + } else { + // If -4 <= e < 0, expand negative exponent without losing significant digits + if (e >= -4) { + int cnt = 0; + while (e < 0 && !(mantissa % 10)) { + mantissa /= 10; + cnt++; + e++; + } + num_digits -= cnt; + s = mp_prepend_zeros(s, cnt - e - 1); + dec = 255; + e = 0; + } } - if (buf_size >= 1) { - *s = '\0'; + } + + // Convert the integer mantissa to string + for (int digit = num_digits - 1; digit >= 0; digit--) { + int digit_ofs = (digit > dec ? digit + 1 : digit); + s[digit_ofs] = '0' + (int)(mantissa % 10); + mantissa /= 10; + } + int dot = (dec >= 255); + if (dec + 1 < num_digits) { + dot = 1; + s++; + s[dec] = '.'; + } + s += num_digits; + #if DEBUG_FLOAT_FORMATTING + *s = 0; + DEBUG_PRINTF(" = %s exp=%d num_digits=%d zeros=%d dec=%d\n", buf, e, num_digits, trailing_zeros, dec); + #endif + + // Append or remove trailing zeros, as required by format + if (trailing_zeros) { + dec -= num_digits - 1; + while (trailing_zeros--) { + if (!dec--) { + *s++ = '.'; + dot = 1; + } + *s++ = '0'; } - return buf_size >= 2; } - if (fp_signbit(f) && !fp_isnan(f)) { - *s++ = '-'; - f = -f; - } else { - if (sign) { - *s++ = sign; + if (fmt_flags & FMT_MODE_G) { + // 'g' format requires to remove trailing zeros after decimal point + if (dot) { + while (s[-1] == '0') { + s--; + } + if (s[-1] == '.') { + s--; + } + } + } + + // Append the exponent if needed + if (((e != 0) || (fmt_flags & FMT_MODE_E)) && !(fmt_flags & FMT_MODE_F)) { + *s++ = 'E' | (fmt_flags & FMT_E_CASE); + if (e >= 0) { + *s++ = '+'; + } else { + *s++ = '-'; + e = -e; } + if (e >= 100) { + *s++ = '0' + (e / 100); + } + *s++ = '0' + ((e / 10) % 10); + *s++ = '0' + (e % 10); } + *s = '\0'; + DEBUG_PRINTF(" ===> %s\n", buf); - // buf_remaining contains bytes available for digits and exponent. - // It is buf_size minus room for the sign and null byte. - int buf_remaining = buf_size - 1 - (s - buf); + return s - buf; +} +// minimal value expected for buf_size, to avoid checking everywhere for overflow +#define MIN_BUF_SIZE (MAX_MANTISSA_DIGITS + 10) + +int mp_format_float(mp_float_t f_entry, char *buf_entry, size_t buf_size, char fmt, int prec, char sign) { + assert(buf_size >= MIN_BUF_SIZE); + + // Handle sign + mp_float_t f = f_entry; + char *buf = buf_entry; + if (signbit(f_entry) && !isnan(f_entry)) { + f = -f; + sign = '-'; + } + if (sign) { + *buf++ = sign; + buf_size--; + } + + // Handle inf/nan + char uc = fmt & 0x20; { - char uc = fmt & 0x20; - if (fp_isinf(f)) { + char *s = buf; + if (isinf(f)) { *s++ = 'I' ^ uc; *s++ = 'N' ^ uc; *s++ = 'F' ^ uc; goto ret; - } else if (fp_isnan(f)) { + } else if (isnan(f)) { *s++ = 'N' ^ uc; *s++ = 'A' ^ uc; *s++ = 'N' ^ uc; ret: *s = '\0'; - return s - buf; + return s - buf_entry; } } + // Decode format character + int fmt_flags = (unsigned char)uc; // setup FMT_E_CASE, clear all other bits + char lofmt = (char)(fmt | 0x20); // fmt in lowercase + if (lofmt == 'f') { + fmt_flags |= FMT_MODE_F; + } else if (lofmt == 'g') { + fmt_flags |= FMT_MODE_G; + } else { + fmt_flags |= FMT_MODE_E; + } + + // When precision is unspecified, default to 6 if (prec < 0) { prec = 6; } - char e_char = 'E' | (fmt & 0x20); // e_char will match case of fmt - fmt |= 0x20; // Force fmt to be lowercase - char org_fmt = fmt; - if (fmt == 'g' && prec == 0) { - prec = 1; + // Use high precision for `repr`, but switch to exponent mode + // after 16 digits in any case to match CPython behaviour + int max_exp_zeros = (prec < (int)buf_size - 3 ? prec : (int)buf_size - 3); + if (prec == MP_FLOAT_REPR_PREC) { + prec = MAX_MANTISSA_DIGITS; + max_exp_zeros = 16; } - int e; - int dec = 0; - char e_sign = '\0'; - int num_digits = 0; - int signed_e = 0; - // Approximate power of 10 exponent from binary exponent. - // abs(e_guess) is lower bound on abs(power of 10 exponent). - int e_guess = (int)(fp_expval(f) * FPCONST(0.3010299956639812)); // 1/log2(10). - if (fp_iszero(f)) { - e = 0; - if (fmt == 'f') { - // Truncate precision to prevent buffer overflow - if (prec + 2 > buf_remaining) { - prec = buf_remaining - 2; - } - num_digits = prec + 1; - } else { - // Truncate precision to prevent buffer overflow - if (prec + 6 > buf_remaining) { - prec = buf_remaining - 6; - } - if (fmt == 'e') { - e_sign = '+'; - } + // Precompute the exact decimal exponent of f, such that + // abs(e) is lower bound on abs(power of 10 exponent). + int e = 0; + if (!fp_iszero(f)) { + // Approximate power of 10 exponent from binary exponent. + e = (int)(fp_expval(f) * MICROPY_FLOAT_CONST(0.3010299956639812)); // 1/log2(10). + int positive_exp = !fp_isless1(f); + mp_float_t u_base = (mp_float_t)mp_decimal_exp((mp_large_float_t)1.0, e + positive_exp); + while ((f >= u_base) == positive_exp) { + e += (positive_exp ? 1 : -1); + u_base = (mp_float_t)mp_decimal_exp((mp_large_float_t)1.0, e + positive_exp); } - } else if (fp_isless1(f)) { - FPTYPE f_entry = f; // Save f in case we go to 'f' format. - // Build negative exponent - e = -e_guess; - FPTYPE u_base = MICROPY_FLOAT_C_FUN(pow)(10, -e); - while (u_base > f) { - ++e; - u_base = MICROPY_FLOAT_C_FUN(pow)(10, -e); - } - // Normalize out the inferred unit. Use divide because - // pow(10, e) * pow(10, -e) is slightly < 1 for some e in float32 - // (e.g. print("%.12f" % ((1e13) * (1e-13)))) - f /= u_base; - - // If the user specified 'g' format, and e is <= 4, then we'll switch - // to the fixed format ('f') - - if (fmt == 'f' || (fmt == 'g' && e <= 4)) { - fmt = 'f'; - dec = 0; + } - if (org_fmt == 'g') { - prec += (e - 1); - } + // For 'e' format, prec is # digits after the decimal + // For 'f' format, prec is # digits after the decimal + // For 'g' format, prec is the max number of significant digits + // + // For 'e' & 'g' format, there will be a single digit before the decimal + // For 'f' format, zeros must be expanded instead of using an exponent. + // Make sure there is enough room in the buffer for them, or switch to format 'g'. + if ((fmt_flags & FMT_MODE_F) && e > 0) { + int req_size = e + prec + 2; + if (req_size > (int)buf_size) { + fmt_flags ^= FMT_MODE_F; + fmt_flags |= FMT_MODE_G; + prec++; + } + } - // truncate precision to prevent buffer overflow - if (prec + 2 > buf_remaining) { - prec = buf_remaining - 2; + // To work independently of the format, we precompute: + // - the max number of significant digits to produce + // - the number of leading zeros to prepend (mode f only) + // - the number of trailing zeros to append + int max_digits = prec; + int lead_zeros = 0; + int trail_zeros = 0; + if (fmt_flags & FMT_MODE_F) { + if (max_digits > (int)buf_size - 3) { + // cannot satisfy requested number of decimals given buf_size, sorry + max_digits = (int)buf_size - 3; + } + if (e < 0) { + if (max_digits > 2 && e < -2) { + // Insert explicit leading zeros + lead_zeros = (-e < max_digits ? -e : max_digits) - 2; + max_digits -= lead_zeros; + } else { + max_digits++; } - - num_digits = prec; - signed_e = 0; - f = f_entry; - ++num_digits; } else { - // For e & g formats, we'll be printing the exponent, so set the - // sign. - e_sign = '-'; - dec = 0; - - if (prec > (buf_remaining - FPMIN_BUF_SIZE)) { - prec = buf_remaining - FPMIN_BUF_SIZE; - if (fmt == 'g') { - prec++; - } - } - signed_e = -e; + max_digits += e + 1; } } else { - // Build positive exponent. - // We don't modify f at this point to avoid inaccuracies from - // scaling it. Instead, we find the product of powers of 10 - // that is not greater than it, and use that to start the - // mantissa. - e = e_guess; - FPTYPE next_u = MICROPY_FLOAT_C_FUN(pow)(10, e + 1); - while (f >= next_u) { - ++e; - next_u = MICROPY_FLOAT_C_FUN(pow)(10, e + 1); + if (!(fmt_flags & FMT_MODE_G) || max_digits == 0) { + max_digits++; } + } + if (max_digits > MAX_MANTISSA_DIGITS) { + // use trailing zeros to avoid overflowing the mantissa + trail_zeros = max_digits - MAX_MANTISSA_DIGITS; + max_digits = MAX_MANTISSA_DIGITS; + } + int overhead = (fmt_flags & FMT_MODE_F ? 3 : FPMIN_BUF_SIZE + 1); + if (trail_zeros > (int)buf_size - max_digits - overhead) { + // cannot satisfy requested number of decimals given buf_size, sorry + trail_zeros = (int)buf_size - max_digits - overhead; + } - // If the user specified fixed format (fmt == 'f') and e makes the - // number too big to fit into the available buffer, then we'll - // switch to the 'e' format. - - if (fmt == 'f') { - if (e >= buf_remaining) { - fmt = 'e'; - } else if ((e + prec + 2) > buf_remaining) { - prec = buf_remaining - e - 2; - if (prec < 0) { - // This means no decimal point, so we can add one back - // for the decimal. - prec++; - } - } - } - if (fmt == 'e' && prec > (buf_remaining - FPMIN_BUF_SIZE)) { - prec = buf_remaining - FPMIN_BUF_SIZE; - } - if (fmt == 'g') { - // Truncate precision to prevent buffer overflow - if (prec + (FPMIN_BUF_SIZE - 1) > buf_remaining) { - prec = buf_remaining - (FPMIN_BUF_SIZE - 1); - } - } - // If the user specified 'g' format, and e is < prec, then we'll switch - // to the fixed format. + // When the caller asks for more precision than available for sure, + // Look for a shorter (rounded) representation first, and only dig + // into more digits if there is no short representation. + int num_digits = (SAFE_MANTISSA_DIGITS < max_digits ? SAFE_MANTISSA_DIGITS : max_digits); +try_again: + ; - if (fmt == 'g' && e < prec) { - fmt = 'f'; - prec -= (e + 1); - } - if (fmt == 'f') { - dec = e; - num_digits = prec + e + 1; + char *s = buf; + int extra_zeros = trail_zeros + (max_digits - num_digits); + int decexp; + int dec = 0; + + if (fp_iszero(f)) { + // no need for scaling 0.0 + decexp = 0; + } else if (fmt_flags & FMT_MODE_F) { + decexp = num_digits - 1; + if (e < 0) { + // Negative exponent: we keep a single leading zero in the mantissa, + // as using more would waste precious digits needed for accuracy. + if (lead_zeros > 0) { + // We are using leading zeros + s = mp_prepend_zeros(s, lead_zeros); + decexp += lead_zeros + 1; + dec = 255; // no decimal dot + } else { + // Small negative exponent, work directly on the mantissa + dec = 0; + } } else { - e_sign = '+'; + // Positive exponent: we will add trailing zeros separately + decexp -= e; + dec = e; } - signed_e = e; + } else { + decexp = num_digits - e - 1; } - if (prec < 0) { - // This can happen when the prec is trimmed to prevent buffer overflow - prec = 0; + DEBUG_PRINTF("input=%.19g e=%d fmt=%c max_d=%d num_d=%d decexp=%d dec=%d l0=%d r0=%d\n", + (double)f, e, lofmt, max_digits, num_digits, decexp, dec, lead_zeros, extra_zeros); + + // At this point, + // - buf points to beginning of output buffer for the unsigned representation + // - num_digits == the number of mantissa digits to add + // - (dec + 1) == the number of digits to print before adding a decimal point + // - decexp == the power of 10 exponent to apply to f to get the decimal mantissa + // - e == the power of 10 exponent to append ('e' or 'g' format) + mp_large_float_uint_t mantissa_cap = 10; + for (int n = 1; n < num_digits; n++) { + mantissa_cap *= 10; } - // At this point e contains the absolute value of the power of 10 exponent. - // (dec + 1) == the number of dgits before the decimal. - - // For e, prec is # digits after the decimal - // For f, prec is # digits after the decimal - // For g, prec is the max number of significant digits - // - // For e & g there will be a single digit before the decimal - // for f there will be e digits before the decimal - - if (fmt == 'e') { - num_digits = prec + 1; - } else if (fmt == 'g') { - if (prec == 0) { - prec = 1; + // Build the decimal mantissa into a large uint + mp_large_float_uint_t mantissa = 1; + if (sizeof(mp_large_float_t) == sizeof(mp_float_t) && num_digits > SAFE_MANTISSA_DIGITS && decexp > 1) { + // if we don't have large floats, use integer multiply to produce the last digits + if (num_digits > SAFE_MANTISSA_DIGITS + 1 && decexp > 2) { + mantissa = 100; + decexp -= 2; + } else { + mantissa = 10; + decexp -= 1; } - num_digits = prec; } - - int d = 0; - for (int digit_index = signed_e; num_digits >= 0; --digit_index) { - FPTYPE u_base = FPCONST(1.0); - if (digit_index > 0) { - // Generate 10^digit_index for positive digit_index. - u_base = MICROPY_FLOAT_C_FUN(pow)(10, digit_index); - } - for (d = 0; d < 9; ++d) { - if (f < u_base) { - break; - } - f -= u_base; - } - // We calculate one more digit than we display, to use in rounding - // below. So only emit the digit if it's one that we display. - if (num_digits > 0) { - // Emit this number (the leading digit). - *s++ = '0' + d; - if (dec == 0 && prec > 0) { - *s++ = '.'; - } - } - --dec; - --num_digits; - if (digit_index <= 0) { - // Once we get below 1.0, we scale up f instead of calculating - // negative powers of 10 in u_base. This provides better - // renditions of exact decimals like 1/16 etc. - f *= FPCONST(10.0); + mp_large_float_t mantissa_f = mp_decimal_exp((mp_large_float_t)f, decexp); + mantissa *= (mp_large_float_uint_t)(mantissa_f + (mp_large_float_t)0.5); + DEBUG_PRINTF("input=%.19g fmt=%c num_digits=%d dec=%d mantissa=" MP_FFUINT_FMT " r0=%d\n", (double)f, lofmt, num_digits, dec, mantissa, extra_zeros); + + // Finally convert the decimal mantissa to a floating-point string, according to formatting rules + int reprlen = mp_format_mantissa(mantissa, mantissa_cap, buf, s, num_digits, max_exp_zeros, extra_zeros, dec, e, fmt_flags); + assert(reprlen + 1 <= (int)buf_size); + + #if MICROPY_FLOAT_FORMAT_IMPL != MICROPY_FLOAT_FORMAT_IMPL_APPROX + + if (num_digits < max_digits) { + // The initial precision might not be sufficient for an exact representation + // for all numbers. If the result is not exact, restart using next precision. + // parse the resulting number and compare against the original + mp_float_t check; + DEBUG_PRINTF("input=%.19g, compare to float('%s')\n", (double)f, buf); + mp_parse_float_internal(buf, reprlen, &check); + if (!fp_equal(check, f)) { + num_digits++; + DEBUG_PRINTF("Not perfect, retry using more digits (%d)\n", num_digits); + goto try_again; } } - // Rounding. If the next digit to print is >= 5, round up. - if (d >= 5) { - char *rs = s; - rs--; - while (1) { - if (*rs == '.') { - rs--; - continue; - } - if (*rs < '0' || *rs > '9') { - // + or - - rs++; // So we sit on the digit to the right of the sign - break; + + #else + + // The initial decimal mantissa might not have been be completely accurate due + // to the previous loating point operations. The best way to verify this is to + // parse the resulting number and compare against the original + mp_float_t check; + DEBUG_PRINTF("input=%.19g, compare to float('%s')\n", (double)f, buf); + mp_parse_float_internal(buf, reprlen, &check); + mp_float_t diff = fp_diff(check, f); + mp_float_t best_diff = diff; + mp_large_float_uint_t best_mantissa = mantissa; + + if (fp_iszero(diff)) { + // we have a perfect match + DEBUG_PRINTF(MP_FFUINT_FMT ": perfect match (direct)\n", mantissa); + } else { + // In order to get the best possible representation, we will perform a + // dichotomic search for a reversible representation. + // This will also provide optimal rounding on the fly. + unsigned err_range = 1; + if (num_digits > SAFE_MANTISSA_DIGITS) { + err_range <<= 3 * (num_digits - SAFE_MANTISSA_DIGITS); + } + int maxruns = 3 + 3 * (MAX_MANTISSA_DIGITS - SAFE_MANTISSA_DIGITS); + while (maxruns-- > 0) { + // update mantissa according to dichotomic search + if (signbit(diff)) { + mantissa += err_range; + } else { + // mantissa is expected to always have more significant digits than err_range + assert(mantissa >= err_range); + mantissa -= err_range; } - if (*rs < '9') { - (*rs)++; + // retry conversion + reprlen = mp_format_mantissa(mantissa, mantissa_cap, buf, s, num_digits, max_exp_zeros, extra_zeros, dec, e, fmt_flags); + assert(reprlen + 1 <= (int)buf_size); + DEBUG_PRINTF("input=%.19g, compare to float('%s')\n", (double)f, buf); + mp_parse_float_internal(buf, reprlen, &check); + DEBUG_PRINTF("check=%.19g num_digits=%d e=%d mantissa=" MP_FFUINT_FMT "\n", (double)check, num_digits, e, mantissa); + diff = fp_diff(check, f); + if (fp_iszero(diff)) { + // we have a perfect match + DEBUG_PRINTF(MP_FFUINT_FMT ": perfect match\n", mantissa); break; } - *rs = '0'; - if (rs == buf) { - break; + // keep track of our best estimate + mp_float_t delta = MICROPY_FLOAT_C_FUN(fabs)(diff) - MICROPY_FLOAT_C_FUN(fabs)(best_diff); + if (signbit(delta) || (fp_iszero(delta) && !(mantissa % 10u))) { + best_diff = diff; + best_mantissa = mantissa; } - rs--; - } - if (*rs == '0') { - // We need to insert a 1 - if (rs[1] == '.' && fmt != 'f') { - // We're going to round 9.99 to 10.00 - // Move the decimal point - rs[0] = '.'; - rs[1] = '0'; - if (e_sign == '-') { - e--; - if (e == 0) { - e_sign = '+'; - } - } else { - e++; - } + // string repr is not perfect: continue a dichotomic improvement + DEBUG_PRINTF(MP_FFUINT_FMT ": %.19g, err_range=%d\n", mantissa, (double)check, err_range); + if (err_range > 1) { + err_range >>= 1; } else { - // Need at extra digit at the end to make room for the leading '1' - // but if we're at the buffer size limit, just drop the final digit. - if ((size_t)(s + 1 - buf) < buf_size) { - s++; + // We have tried all possible mantissa, without finding a reversible repr. + // Check if we have an alternate precision to try. + if (num_digits < max_digits) { + num_digits++; + DEBUG_PRINTF("Failed to find a perfect match, try with more digits (%d)\n", num_digits); + goto try_again; } + // Otherwise, keep the closest one, which is either the first one or the last one. + if (mantissa == best_mantissa) { + // Last guess is the best one + DEBUG_PRINTF(MP_FFUINT_FMT ": last guess was the best one\n", mantissa); + } else { + // We had a better guess earlier + DEBUG_PRINTF(MP_FFUINT_FMT ": use best guess\n", mantissa); + reprlen = mp_format_mantissa(best_mantissa, mantissa_cap, buf, s, num_digits, max_exp_zeros, extra_zeros, dec, e, fmt_flags); + } + break; } - char *ss = s; - while (ss > rs) { - *ss = ss[-1]; - ss--; - } - *rs = '1'; } } + #endif - // verify that we did not overrun the input buffer so far - assert((size_t)(s + 1 - buf) <= buf_size); - - if (org_fmt == 'g' && prec > 0) { - // Remove trailing zeros and a trailing decimal point - while (s[-1] == '0') { - s--; - } - if (s[-1] == '.') { - s--; - } - } - // Append the exponent - if (e_sign) { - *s++ = e_char; - *s++ = e_sign; - if (FPMIN_BUF_SIZE == 7 && e >= 100) { - *s++ = '0' + (e / 100); - } - *s++ = '0' + ((e / 10) % 10); - *s++ = '0' + (e % 10); - } - *s = '\0'; - - // verify that we did not overrun the input buffer - assert((size_t)(s + 1 - buf) <= buf_size); - - return s - buf; + return buf + reprlen - buf_entry; } #endif // MICROPY_FLOAT_IMPL != MICROPY_FLOAT_IMPL_NONE diff --git a/py/formatfloat.h b/py/formatfloat.h index 9a1643b4ddff5..7b1414672b77b 100644 --- a/py/formatfloat.h +++ b/py/formatfloat.h @@ -29,6 +29,7 @@ #include "py/mpconfig.h" #if MICROPY_PY_BUILTINS_FLOAT +#define MP_FLOAT_REPR_PREC (99) // magic `prec` value for optimal `repr` behaviour int mp_format_float(mp_float_t f, char *buf, size_t bufSize, char fmt, int prec, char sign); #endif diff --git a/py/misc.h b/py/misc.h index e034485838954..86ac2ec9a1061 100644 --- a/py/misc.h +++ b/py/misc.h @@ -277,6 +277,25 @@ typedef union _mp_float_union_t { mp_float_uint_t i; } mp_float_union_t; +#if MICROPY_FLOAT_FORMAT_IMPL == MICROPY_FLOAT_FORMAT_IMPL_EXACT + +#if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT +// Exact float conversion requires using internally a bigger sort of floating point +typedef double mp_large_float_t; +#elif MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_DOUBLE +typedef long double mp_large_float_t; +#endif +// Always use a 64 bit mantissa for formatting and parsing +typedef uint64_t mp_large_float_uint_t; + +#else // MICROPY_FLOAT_FORMAT_IMPL != MICROPY_FLOAT_FORMAT_IMPL_EXACT + +// No bigger floating points +typedef mp_float_t mp_large_float_t; +typedef mp_float_uint_t mp_large_float_uint_t; + +#endif + #endif // MICROPY_PY_BUILTINS_FLOAT /** ROM string compression *************/ diff --git a/py/mpconfig.h b/py/mpconfig.h index c316aa4b2d894..caa63fef3f6b0 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -861,6 +861,27 @@ typedef double mp_float_t; #define MICROPY_PY_BUILTINS_COMPLEX (MICROPY_PY_BUILTINS_FLOAT) #endif +// Float to string conversion implementations +// +// Note that the EXACT method is only available if the compiler supports +// floating points larger than mp_float_t: +// - with MICROPY_FLOAT_IMPL_FLOAT, the compiler needs to support `double` +// - with MICROPY_FLOAT_IMPL_DOUBLE, the compiler needs to support `long double` +// +#define MICROPY_FLOAT_FORMAT_IMPL_BASIC (0) // smallest code, but inexact +#define MICROPY_FLOAT_FORMAT_IMPL_APPROX (1) // slightly bigger, almost perfect +#define MICROPY_FLOAT_FORMAT_IMPL_EXACT (2) // bigger code, and 100% exact repr + +#ifndef MICROPY_FLOAT_FORMAT_IMPL +#if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT +#define MICROPY_FLOAT_FORMAT_IMPL (MICROPY_FLOAT_FORMAT_IMPL_APPROX) +#elif defined(__SIZEOF_LONG_DOUBLE__) && __SIZEOF_LONG_DOUBLE__ > __SIZEOF_DOUBLE__ +#define MICROPY_FLOAT_FORMAT_IMPL (MICROPY_FLOAT_FORMAT_IMPL_EXACT) +#else +#define MICROPY_FLOAT_FORMAT_IMPL (MICROPY_FLOAT_FORMAT_IMPL_APPROX) +#endif +#endif + // Whether to use the native _Float16 for 16-bit float support #ifndef MICROPY_FLOAT_USE_NATIVE_FLT16 #ifdef __FLT16_MAX__ diff --git a/py/mpprint.c b/py/mpprint.c index f1d8bd0c57366..bd7a250878b89 100644 --- a/py/mpprint.c +++ b/py/mpprint.c @@ -338,7 +338,7 @@ int mp_print_mp_int(const mp_print_t *print, mp_obj_t x, unsigned int base, int #if MICROPY_PY_BUILTINS_FLOAT int mp_print_float(const mp_print_t *print, mp_float_t f, char fmt, unsigned int flags, char fill, int width, int prec) { - char buf[32]; + char buf[36]; char sign = '\0'; int chrs = 0; @@ -349,11 +349,17 @@ int mp_print_float(const mp_print_t *print, mp_float_t f, char fmt, unsigned int sign = ' '; } - int len = mp_format_float(f, buf, sizeof(buf), fmt, prec, sign); + int len = mp_format_float(f, buf, sizeof(buf) - 3, fmt, prec, sign); char *s = buf; - if ((flags & PF_FLAG_ADD_PERCENT) && (size_t)(len + 1) < sizeof(buf)) { + if ((flags & PF_FLAG_ALWAYS_DECIMAL) && strchr(buf, '.') == NULL && strchr(buf, 'e') == NULL && strchr(buf, 'n') == NULL) { + buf[len++] = '.'; + buf[len++] = '0'; + buf[len] = '\0'; + } + + if (flags & PF_FLAG_ADD_PERCENT) { buf[len++] = '%'; buf[len] = '\0'; } diff --git a/py/mpprint.h b/py/mpprint.h index 583f00bda8099..250ea24b87840 100644 --- a/py/mpprint.h +++ b/py/mpprint.h @@ -36,6 +36,7 @@ #define PF_FLAG_CENTER_ADJUST (0x020) #define PF_FLAG_ADD_PERCENT (0x040) #define PF_FLAG_SHOW_OCTAL_LETTER (0x080) +#define PF_FLAG_ALWAYS_DECIMAL (0x100) #define PF_FLAG_SEP_POS (9) // must be above all the above PF_FLAGs #if MICROPY_PY_IO && MICROPY_PY_SYS_STDFILES diff --git a/py/objcomplex.c b/py/objcomplex.c index 85b5852845737..805899edf4396 100644 --- a/py/objcomplex.c +++ b/py/objcomplex.c @@ -45,29 +45,18 @@ typedef struct _mp_obj_complex_t { static void complex_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) { (void)kind; mp_obj_complex_t *o = MP_OBJ_TO_PTR(o_in); - #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT - char buf[16]; - #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_C - const int precision = 6; - #else - const int precision = 7; - #endif - #else - char buf[32]; - const int precision = 16; - #endif - if (o->real == 0) { - mp_format_float(o->imag, buf, sizeof(buf), 'g', precision, '\0'); - mp_printf(print, "%sj", buf); + const char *suffix; + int flags = 0; + if (o->real != 0) { + mp_print_str(print, "("); + mp_print_float(print, o->real, 'g', 0, '\0', -1, MP_FLOAT_REPR_PREC); + flags = PF_FLAG_SHOW_SIGN; + suffix = "j)"; } else { - mp_format_float(o->real, buf, sizeof(buf), 'g', precision, '\0'); - mp_printf(print, "(%s", buf); - if (o->imag >= 0 || isnan(o->imag)) { - mp_print_str(print, "+"); - } - mp_format_float(o->imag, buf, sizeof(buf), 'g', precision, '\0'); - mp_printf(print, "%sj)", buf); + suffix = "j"; } + mp_print_float(print, o->imag, 'g', flags, '\0', -1, MP_FLOAT_REPR_PREC); + mp_print_str(print, suffix); } static mp_obj_t complex_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { diff --git a/py/objfloat.c b/py/objfloat.c index 81b0daa620916..125b576fb61c3 100644 --- a/py/objfloat.c +++ b/py/objfloat.c @@ -110,23 +110,7 @@ mp_int_t mp_float_hash(mp_float_t src) { static void float_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) { (void)kind; mp_float_t o_val = mp_obj_float_get(o_in); - #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT - char buf[16]; - #if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_C - const int precision = 6; - #else - const int precision = 7; - #endif - #else - char buf[32]; - const int precision = 16; - #endif - mp_format_float(o_val, buf, sizeof(buf), 'g', precision, '\0'); - mp_print_str(print, buf); - if (strchr(buf, '.') == NULL && strchr(buf, 'e') == NULL && strchr(buf, 'n') == NULL) { - // Python floats always have decimal point (unless inf or nan) - mp_print_str(print, ".0"); - } + mp_print_float(print, o_val, 'g', PF_FLAG_ALWAYS_DECIMAL, '\0', -1, MP_FLOAT_REPR_PREC); } static mp_obj_t float_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { diff --git a/py/parsenum.c b/py/parsenum.c index 019491b5132ae..e18002306a259 100644 --- a/py/parsenum.c +++ b/py/parsenum.c @@ -210,7 +210,7 @@ typedef enum { } parse_dec_in_t; // MANTISSA_MAX is used to retain precision while not overflowing mantissa -#define MANTISSA_MAX (sizeof(mp_float_uint_t) == 8 ? 0x1999999999999998ULL : 0x19999998U) +#define MANTISSA_MAX (sizeof(mp_large_float_uint_t) == 8 ? 0x1999999999999998ULL : 0x19999998U) // MAX_EXACT_POWER_OF_5 is the largest value of x so that 5^x can be stored exactly in a float #if MICROPY_FLOAT_IMPL == MICROPY_FLOAT_IMPL_FLOAT @@ -220,11 +220,45 @@ typedef enum { #endif // Helper to compute `num * (10.0 ** dec_exp)` -mp_float_t mp_decimal_exp(mp_float_t num, int dec_exp) { - - if (dec_exp == 0 || num == MICROPY_FLOAT_CONST(0.0)) { +mp_large_float_t mp_decimal_exp(mp_large_float_t num, int dec_exp) { + if (dec_exp == 0 || num == (mp_large_float_t)(0.0)) { return num; } + + #if MICROPY_FLOAT_FORMAT_IMPL == MICROPY_FLOAT_FORMAT_IMPL_EXACT + + // If the assert below fails, it means you have chosen MICROPY_FLOAT_FORMAT_IMPL_EXACT + // manually on a platform where `larger floats` are not supported, which would + // result in inexact conversions. To fix this issue, change your `mpconfigport.h` + // and select MICROPY_FLOAT_FORMAT_IMPL_APPROX instead + assert(sizeof(mp_large_float_t) > sizeof(mp_float_t)); + + // Perform power using simple multiplications, to avoid + // dependency to higher-precision pow() function + int neg_exp = (dec_exp < 0); + if (neg_exp) { + dec_exp = -dec_exp; + } + mp_large_float_t res = num; + mp_large_float_t expo = (mp_large_float_t)10.0; + while (dec_exp) { + if (dec_exp & 1) { + if (neg_exp) { + res /= expo; + } else { + res *= expo; + } + } + dec_exp >>= 1; + if (dec_exp) { + expo *= expo; + } + } + return res; + + #else + // MICROPY_FLOAT_FORMAT_IMPL != MICROPY_FLOAT_FORMAT_IMPL_EXACT + mp_float_union_t res = {num}; // Multiply first by (2.0 ** dec_exp) via the exponent // - this will ensure that the result of `pow()` is always in mp_float_t range @@ -238,12 +272,14 @@ mp_float_t mp_decimal_exp(mp_float_t num, int dec_exp) { } else { res.f *= MICROPY_FLOAT_C_FUN(pow)(5, dec_exp); } - return (mp_float_t)res.f; + return (mp_large_float_t)res.f; + + #endif } // Break out inner digit accumulation routine to ease trailing zero deferral. -static mp_float_uint_t accept_digit(mp_float_uint_t p_mantissa, unsigned int dig, int *p_exp_extra, int in) { +static mp_large_float_uint_t accept_digit(mp_large_float_uint_t p_mantissa, unsigned int dig, int *p_exp_extra, int in) { // Core routine to ingest an additional digit. if (p_mantissa < MANTISSA_MAX) { // dec_val won't overflow so keep accumulating @@ -267,7 +303,7 @@ const char *mp_parse_float_internal(const char *str, size_t len, mp_float_t *res parse_dec_in_t in = PARSE_DEC_IN_INTG; bool exp_neg = false; - mp_float_uint_t mantissa = 0; + mp_large_float_uint_t mantissa = 0; int exp_val = 0; int exp_extra = 0; int trailing_zeros_intg = 0, trailing_zeros_frac = 0; diff --git a/py/parsenum.h b/py/parsenum.h index a807cb09d0572..d532cb194a5d8 100644 --- a/py/parsenum.h +++ b/py/parsenum.h @@ -35,7 +35,7 @@ mp_obj_t mp_parse_num_integer(const char *restrict str, size_t len, int base, mp_lexer_t *lex); #if MICROPY_PY_BUILTINS_FLOAT -mp_float_t mp_decimal_exp(mp_float_t num, int dec_exp); +mp_large_float_t mp_decimal_exp(mp_large_float_t num, int dec_exp); const char *mp_parse_float_internal(const char *str, size_t len, mp_float_t *res); #endif diff --git a/tests/float/float_format.py b/tests/float/float_format.py index 98ed0eb096fa4..0eb8b232b063a 100644 --- a/tests/float/float_format.py +++ b/tests/float/float_format.py @@ -2,14 +2,25 @@ # general rounding for val in (116, 1111, 1234, 5010, 11111): - print("%.0f" % val) - print("%.1f" % val) - print("%.3f" % val) + print("Test on %d / 1000:" % val) + for fmt in ("%.5e", "%.3e", "%.1e", "%.0e", "%.3f", "%.1f", "%.0f", "%.3g", "%.1g", "%.0g"): + print(fmt, fmt % (val / 1000)) + +# make sure round-up to the next unit is handled properly +for val in range(4, 9): + divi = 10**val + print("Test on 99994 / (10 ** %d):" % val) + for fmt in ("%.5e", "%.3e", "%.1e", "%.0e", "%.3f", "%.1f", "%.0f", "%.3g", "%.1g", "%.0g"): + print(fmt, fmt % (99994 / divi)) # make sure rounding is done at the correct precision for prec in range(8): print(("%%.%df" % prec) % 6e-5) +# make sure trailing zeroes are added properly +for prec in range(8): + print(("%%.%df" % prec) % 1e19) + # check certain cases that had a digit value of 10 render as a ":" character print("%.2e" % float("9" * 51 + "e-39")) print("%.2e" % float("9" * 40 + "e-21")) diff --git a/tests/float/float_format_accuracy.py b/tests/float/float_format_accuracy.py new file mode 100644 index 0000000000000..f9467f9c05d89 --- /dev/null +++ b/tests/float/float_format_accuracy.py @@ -0,0 +1,73 @@ +# Test accuracy of `repr` conversions. +# This test also increases code coverage for corner cases. + +try: + import array, math, random +except ImportError: + print("SKIP") + raise SystemExit + +# The largest errors come from seldom used very small numbers, near the +# limit of the representation. So we keep them out of this test to keep +# the max relative error display useful. +if float("1e-100") == 0.0: + # single-precision + float_type = "f" + float_size = 4 + # testing range + min_expo = -96 # i.e. not smaller than 1.0e-29 + # Expected results (given >=50'000 samples): + # - MICROPY_FLTCONV_IMPL_EXACT: 100% exact conversions + # - MICROPY_FLTCONV_IMPL_APPROX: >=98.53% exact conversions, max relative error <= 1.01e-7 + min_success = 0.980 # with only 1200 samples, the success rate is lower + max_rel_err = 1.1e-7 + # REPR_C is typically used with FORMAT_IMPL_BASIC, which has a larger error + is_REPR_C = float("1.0000001") == float("1.0") + if is_REPR_C: # REPR_C + min_success = 0.83 + max_rel_err = 5.75e-07 +else: + # double-precision + float_type = "d" + float_size = 8 + # testing range + min_expo = -845 # i.e. not smaller than 1.0e-254 + # Expected results (given >=200'000 samples): + # - MICROPY_FLTCONV_IMPL_EXACT: 100% exact conversions + # - MICROPY_FLTCONV_IMPL_APPROX: >=99.83% exact conversions, max relative error <= 2.7e-16 + min_success = 0.997 # with only 1200 samples, the success rate is lower + max_rel_err = 2.7e-16 + + +# Deterministic pseudorandom generator. Designed to be uniform +# on mantissa values and exponents, not on the represented number +def pseudo_randfloat(): + rnd_buff = bytearray(float_size) + for _ in range(float_size): + rnd_buff[_] = random.getrandbits(8) + return array.array(float_type, rnd_buff)[0] + + +random.seed(42) +stats = 0 +N = 1200 +max_err = 0 +for _ in range(N): + f = pseudo_randfloat() + while type(f) is not float or math.isinf(f) or math.isnan(f) or math.frexp(f)[1] <= min_expo: + f = pseudo_randfloat() + + str_f = repr(f) + f2 = float(str_f) + if f2 == f: + stats += 1 + else: + error = abs((f2 - f) / f) + if max_err < error: + max_err = error + +print(N, "values converted") +if stats / N >= min_success and max_err <= max_rel_err: + print("float format accuracy OK") +else: + print("FAILED: repr rate=%.3f%% max_err=%.3e" % (100 * stats / N, max_err)) diff --git a/tests/float/float_format_ints.py b/tests/float/float_format_ints.py index df4444166c5fa..7b7b30c4b340b 100644 --- a/tests/float/float_format_ints.py +++ b/tests/float/float_format_ints.py @@ -12,14 +12,42 @@ print(title, "with format", f_fmt, "gives", f_fmt.format(f)) print(title, "with format", g_fmt, "gives", g_fmt.format(f)) +# The tests below check border cases involving all mantissa bits. +# In case of REPR_C, where the mantissa is missing two bits, the +# the string representation for such numbers might not always be exactly +# the same but nevertheless be correct, so we must allow a few exceptions. +is_REPR_C = float("1.0000001") == float("1.0") + # 16777215 is 2^24 - 1, the largest integer that can be completely held # in a float32. -print("{:f}".format(16777215)) +val_str = "{:f}".format(16777215) + +# When using REPR_C, 16777215.0 is the same as 16777212.0 or 16777214.4 +# (depending on the implementation of pow() function, the result may differ) +if is_REPR_C and (val_str == "16777212.000000" or val_str == "16777214.400000"): + val_str = "16777215.000000" + +print(val_str) + # 4294967040 = 16777215 * 128 is the largest integer that is exactly # represented by a float32 and that will also fit within a (signed) int32. # The upper bound of our integer-handling code is actually double this, # but that constant might cause trouble on systems using 32 bit ints. -print("{:f}".format(2147483520)) +val_str = "{:f}".format(2147483520) + +# When using FLOAT_IMPL_FLOAT, 2147483520.0 == 2147483500.0 +# Both representations are valid, the second being "simpler" +is_float32 = float("1e300") == float("inf") +if is_float32 and val_str == "2147483500.000000": + val_str = "2147483520.000000" + +# When using REPR_C, 2147483520.0 is the same as 2147483200.0 +# Both representations are valid, the second being "simpler" +if is_REPR_C and val_str == "2147483200.000000": + val_str = "2147483520.000000" + +print(val_str) + # Very large positive integers can be a test for precision and resolution. # This is a weird way to represent 1e38 (largest power of 10 for float32). print("{:.6e}".format(float("9" * 30 + "e8"))) diff --git a/tests/float/float_struct_e.py b/tests/float/float_struct_e.py index 403fbc5db4cde..ba4134f3393ed 100644 --- a/tests/float/float_struct_e.py +++ b/tests/float/float_struct_e.py @@ -32,7 +32,7 @@ for i in (j, -j): x = struct.pack(" Date: Fri, 1 Aug 2025 10:30:55 +1000 Subject: [PATCH 027/143] github/workflows: Build unix port for docs and run workflow more often. The unix port is needed to build the docs, due to the cpydiff tests which run both CPython and MicroPython (unix port). That was previously not failing the CI because the output from MicroPython was: /bin/sh: 1: ../ports/unix/build-standard/micropython: not found which doesn't match the CPython output for any of the cpydiff tests, and so it was considered a "pass" in terms of the output differing. Also, run the docs workflow when py/ or tests/cpydiff/ changes, because the cpydiff results may change when the core code changes. Signed-off-by: Damien George --- .github/workflows/docs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d01a4b50c9810..62a6f69fc3924 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,6 +5,8 @@ on: pull_request: paths: - docs/** + - py/** + - tests/cpydiff/** concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -19,5 +21,7 @@ jobs: - uses: actions/setup-python@v5 - name: Install Python packages run: pip install -r docs/requirements.txt + - name: Build unix port + run: source tools/ci.sh && ci_unix_build_helper - name: Build docs run: make -C docs/ html From 947d5448b477343fb1b113e24cb3d3b904a9d3e7 Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 1 Aug 2025 11:12:08 +1000 Subject: [PATCH 028/143] tests/cpydiff: Remove passing types_float_rounding test. Since commit dbbaa959c85c04dbbcde5908b5d0775b574e44e7, this test now produces the same output on MicroPython as CPython does, namely -1e+01. Signed-off-by: Damien George --- tests/cpydiff/types_float_rounding.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 tests/cpydiff/types_float_rounding.py diff --git a/tests/cpydiff/types_float_rounding.py b/tests/cpydiff/types_float_rounding.py deleted file mode 100644 index 206e359ed9be7..0000000000000 --- a/tests/cpydiff/types_float_rounding.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -categories: Types,float -description: uPy and CPython outputs formats may differ -cause: Unknown -workaround: Unknown -""" - -print("%.1g" % -9.9) From f8f6d71940cb34c762d919a255f084d18fbcf1c1 Mon Sep 17 00:00:00 2001 From: Damien George Date: Thu, 31 Jul 2025 18:52:45 +1000 Subject: [PATCH 029/143] nrf/drivers/bluetooth: Change soft-device download URL to self hosted. The existing URLs have started to return a HTTP 403. The simplest way around this is to host the files at micropython.org, and point to them from the download script. The soft-device files have been retrieved from: - https://www.nordicsemi.com/Products/Development-software/s110/download - https://www.nordicsemi.com/Products/Development-software/s132/download - https://www.nordicsemi.com/Products/Development-software/s140/download Signed-off-by: Damien George --- .../drivers/bluetooth/download_ble_stack.sh | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/ports/nrf/drivers/bluetooth/download_ble_stack.sh b/ports/nrf/drivers/bluetooth/download_ble_stack.sh index 7886682b718e0..5004d7a28eff0 100755 --- a/ports/nrf/drivers/bluetooth/download_ble_stack.sh +++ b/ports/nrf/drivers/bluetooth/download_ble_stack.sh @@ -10,12 +10,9 @@ function download_s110_nrf51_8_0_0 mkdir -p $1/s110_nrf51_8.0.0 cd $1/s110_nrf51_8.0.0 - wget --post-data="fileName=DeviceDownload&ids=DBBEB2467E4A4EBCB791C2E7BE3FC7A8" https://www.nordicsemi.com/api/sitecore/Products/MedialibraryZipDownload2 - mv MedialibraryZipDownload2 temp.zip - unzip -u temp.zip + wget https://micropython.org/resources/nrf-soft-device/s110nrf51800.zip unzip -u s110nrf51800.zip rm s110nrf51800.zip - rm temp.zip cd - } @@ -29,12 +26,9 @@ function download_s132_nrf52_6_1_1 mkdir -p $1/s132_nrf52_6.1.1 cd $1/s132_nrf52_6.1.1 - wget --post-data="fileName=DeviceDownload&ids=3AB3E86666FE4361A4A3B7E0D1CBB9B9" https://www.nordicsemi.com/api/sitecore/Products/MedialibraryZipDownload2 - mv MedialibraryZipDownload2 temp.zip - unzip -u temp.zip + wget https://micropython.org/resources/nrf-soft-device/s132nrf52611.zip unzip -u s132nrf52611.zip rm s132nrf52611.zip - rm temp.zip cd - } @@ -48,12 +42,9 @@ function download_s140_nrf52_6_1_1 mkdir -p $1/s140_nrf52_6.1.1 cd $1/s140_nrf52_6.1.1 - wget --post-data="fileName=DeviceDownload&ids=CE89BA7633C540AFA48AB88E934DBF05" https://www.nordicsemi.com/api/sitecore/Products/MedialibraryZipDownload2 - mv MedialibraryZipDownload2 temp.zip - unzip -u temp.zip + wget https://micropython.org/resources/nrf-soft-device/s140nrf52611.zip unzip -u s140nrf52611.zip rm s140nrf52611.zip - rm temp.zip cd - } @@ -67,12 +58,9 @@ function download_s140_nrf52_7_3_0 mkdir -p $1/s140_nrf52_7.3.0 cd $1/s140_nrf52_7.3.0 - wget --post-data="fileName=DeviceDownload&ids=59452FDD13BA46EEAD0810A57359F294" https://www.nordicsemi.com/api/sitecore/Products/MedialibraryZipDownload2 - mv MedialibraryZipDownload2 temp.zip - unzip -u temp.zip + wget https://micropython.org/resources/nrf-soft-device/s140_nrf52_7.3.0.zip unzip -u s140_nrf52_7.3.0.zip rm s140_nrf52_7.3.0.zip - rm temp.zip cd - } From f67a3703118be7d97629130d99630996ff3cb255 Mon Sep 17 00:00:00 2001 From: SiZiOUS Date: Sat, 19 Jul 2025 18:45:42 +0200 Subject: [PATCH 030/143] embed/port: Fix alloca include for Windows platforms. When building the embedded port on MinGW-w64, I receive the following error: fatal error: alloca.h: No such file or directory MinGW-w64 (used on MSYS2) doesn't include `alloca.h`, but `alloca()` is provided via `malloc.h` instead. And this fix is also needed for other Windows build systems. Signed-off-by: SiZiOUS --- ports/embed/port/mpconfigport_common.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ports/embed/port/mpconfigport_common.h b/ports/embed/port/mpconfigport_common.h index 8e19859ed2ed9..aa65640fcd58c 100644 --- a/ports/embed/port/mpconfigport_common.h +++ b/ports/embed/port/mpconfigport_common.h @@ -34,8 +34,13 @@ typedef long mp_off_t; // Need to provide a declaration/definition of alloca() #if defined(__FreeBSD__) || defined(__NetBSD__) +// BSD #include +#elif defined(_WIN32) +// Windows +#include #else +// Other OS #include #endif From 69ead7d98ef30df3b6bd4485633490e80fca1718 Mon Sep 17 00:00:00 2001 From: Yoctopuce dev Date: Tue, 28 Jan 2025 00:26:08 +0100 Subject: [PATCH 031/143] py/parse: Add support for math module constants and float folding. Add a new MICROPY_COMP_CONST_FLOAT feature, enabled by in mpy-cross and when compiling with MICROPY_CONFIG_ROM_LEVEL_CORE_FEATURES. The new feature leverages the code of MICROPY_COMP_CONST_FOLDING to support folding of floating point constants. If MICROPY_COMP_MODULE_CONST is defined as well, math module constants are made available at compile time. For example: _DEG_TO_GRADIANT = const(math.pi / 180) _INVALID_VALUE = const(math.nan) A few corner cases had to be handled: - The float const folding code should not fold expressions resulting into complex results, as the mpy parser for complex immediates has limitations. - The constant generation code must distinguish between -0.0 and 0.0, which are different even if C consider them as ==. This change removes previous limitations on the use of `const()` expressions that would result in floating point number, so the test cases of micropython/const_error have to be updated. Additional test cases have been added to cover the new repr() code (from a previous commit). A few other simple test cases have been added to handle the use of floats in `const()` expressions, but the float folding code itself is also tested when running general float test cases, as float expressions often get resolved at compile-time (with this change). Signed-off-by: Yoctopuce dev --- mpy-cross/mpconfigport.h | 4 +- py/builtin.h | 1 + py/emitcommon.c | 17 ++++- py/mpconfig.h | 7 +++ py/parse.c | 89 +++++++++++++++++++-------- tests/float/float_parse_doubleprec.py | 6 ++ tests/micropython/const_error.py | 2 - tests/micropython/const_error.py.exp | 2 - tests/micropython/const_float.py | 23 +++++++ tests/micropython/const_float.py.exp | 4 ++ tests/micropython/const_math.py | 18 ++++++ tests/micropython/const_math.py.exp | 1 + 12 files changed, 143 insertions(+), 31 deletions(-) create mode 100644 tests/micropython/const_float.py create mode 100644 tests/micropython/const_float.py.exp create mode 100644 tests/micropython/const_math.py create mode 100644 tests/micropython/const_math.py.exp diff --git a/mpy-cross/mpconfigport.h b/mpy-cross/mpconfigport.h index 94a598c9954b8..81cbfc2eeb937 100644 --- a/mpy-cross/mpconfigport.h +++ b/mpy-cross/mpconfigport.h @@ -55,6 +55,7 @@ #define MICROPY_COMP_CONST_FOLDING (1) #define MICROPY_COMP_MODULE_CONST (1) #define MICROPY_COMP_CONST (1) +#define MICROPY_COMP_CONST_FLOAT (1) #define MICROPY_COMP_DOUBLE_TUPLE_ASSIGN (1) #define MICROPY_COMP_TRIPLE_TUPLE_ASSIGN (1) #define MICROPY_COMP_RETURN_IF_EXPR (1) @@ -88,7 +89,8 @@ #define MICROPY_PY_ARRAY (0) #define MICROPY_PY_ATTRTUPLE (0) #define MICROPY_PY_COLLECTIONS (0) -#define MICROPY_PY_MATH (0) +#define MICROPY_PY_MATH (MICROPY_COMP_CONST_FLOAT) +#define MICROPY_PY_MATH_CONSTANTS (MICROPY_COMP_CONST_FLOAT) #define MICROPY_PY_CMATH (0) #define MICROPY_PY_GC (0) #define MICROPY_PY_IO (0) diff --git a/py/builtin.h b/py/builtin.h index 6efe3e8facabd..388bc8470053d 100644 --- a/py/builtin.h +++ b/py/builtin.h @@ -138,6 +138,7 @@ extern const mp_obj_module_t mp_module_sys; extern const mp_obj_module_t mp_module_errno; extern const mp_obj_module_t mp_module_uctypes; extern const mp_obj_module_t mp_module_machine; +extern const mp_obj_module_t mp_module_math; extern const char MICROPY_PY_BUILTINS_HELP_TEXT[]; diff --git a/py/emitcommon.c b/py/emitcommon.c index a9eb6e2021fc8..1f701db80a0a9 100644 --- a/py/emitcommon.c +++ b/py/emitcommon.c @@ -25,6 +25,7 @@ */ #include +#include #include "py/emit.h" #include "py/nativeglue.h" @@ -72,7 +73,21 @@ static bool strictly_equal(mp_obj_t a, mp_obj_t b) { } return true; } else { - return mp_obj_equal(a, b); + if (!mp_obj_equal(a, b)) { + return false; + } + #if MICROPY_PY_BUILTINS_FLOAT && MICROPY_COMP_CONST_FLOAT + if (a_type == &mp_type_float) { + mp_float_t a_val = mp_obj_float_get(a); + if (a_val == (mp_float_t)0.0) { + // Although 0.0 == -0.0, they are not strictly_equal and + // must be stored as two different constants in .mpy files + mp_float_t b_val = mp_obj_float_get(b); + return signbit(a_val) == signbit(b_val); + } + } + #endif + return true; } } diff --git a/py/mpconfig.h b/py/mpconfig.h index caa63fef3f6b0..ed5ccccb6a52a 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -490,6 +490,13 @@ #define MICROPY_COMP_CONST (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES) #endif +// Whether to enable float constant folding like 1.2+3.4 (when MICROPY_COMP_CONST_FOLDING is also enabled) +// and constant optimisation like id = const(1.2) (when MICROPY_COMP_CONST is also enabled) +// and constant lookup like math.inf (when MICROPY_COMP_MODULE_CONST is also enabled) +#ifndef MICROPY_COMP_CONST_FLOAT +#define MICROPY_COMP_CONST_FLOAT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES) +#endif + // Whether to enable optimisation of: a, b = c, d // Costs 124 bytes (Thumb2) #ifndef MICROPY_COMP_DOUBLE_TUPLE_ASSIGN diff --git a/py/parse.c b/py/parse.c index db89fb58450e3..91eea3e368907 100644 --- a/py/parse.c +++ b/py/parse.c @@ -336,18 +336,34 @@ static uint8_t peek_rule(parser_t *parser, size_t n) { } #endif -bool mp_parse_node_get_int_maybe(mp_parse_node_t pn, mp_obj_t *o) { +#if MICROPY_COMP_CONST_FOLDING || MICROPY_EMIT_INLINE_ASM +static bool mp_parse_node_get_number_maybe(mp_parse_node_t pn, mp_obj_t *o) { if (MP_PARSE_NODE_IS_SMALL_INT(pn)) { *o = MP_OBJ_NEW_SMALL_INT(MP_PARSE_NODE_LEAF_SMALL_INT(pn)); return true; } else if (MP_PARSE_NODE_IS_STRUCT_KIND(pn, RULE_const_object)) { mp_parse_node_struct_t *pns = (mp_parse_node_struct_t *)pn; *o = mp_parse_node_extract_const_object(pns); - return mp_obj_is_int(*o); + return mp_obj_is_int(*o) + #if MICROPY_COMP_CONST_FLOAT + || mp_obj_is_float(*o) + #endif + ; } else { return false; } } +#endif + +#if MICROPY_EMIT_INLINE_ASM +bool mp_parse_node_get_int_maybe(mp_parse_node_t pn, mp_obj_t *o) { + return mp_parse_node_get_number_maybe(pn, o) + #if MICROPY_COMP_CONST_FLOAT + && mp_obj_is_int(*o) + #endif + ; +} +#endif #if MICROPY_COMP_CONST_TUPLE || MICROPY_COMP_CONST static bool mp_parse_node_is_const(mp_parse_node_t pn) { @@ -642,12 +658,32 @@ static const mp_rom_map_elem_t mp_constants_table[] = { #if MICROPY_PY_UCTYPES { MP_ROM_QSTR(MP_QSTR_uctypes), MP_ROM_PTR(&mp_module_uctypes) }, #endif + #if MICROPY_PY_BUILTINS_FLOAT && MICROPY_PY_MATH && MICROPY_COMP_CONST_FLOAT + { MP_ROM_QSTR(MP_QSTR_math), MP_ROM_PTR(&mp_module_math) }, + #endif // Extra constants as defined by a port MICROPY_PORT_CONSTANTS }; static MP_DEFINE_CONST_MAP(mp_constants_map, mp_constants_table); #endif +static bool binary_op_maybe(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs, mp_obj_t *res) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_t tmp = mp_binary_op(op, lhs, rhs); + #if MICROPY_PY_BUILTINS_COMPLEX + if (mp_obj_is_type(tmp, &mp_type_complex)) { + return false; + } + #endif + *res = tmp; + nlr_pop(); + return true; + } else { + return false; + } +} + static bool fold_logical_constants(parser_t *parser, uint8_t rule_id, size_t *num_args) { if (rule_id == RULE_or_test || rule_id == RULE_and_test) { @@ -706,7 +742,7 @@ static bool fold_logical_constants(parser_t *parser, uint8_t rule_id, size_t *nu } static bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) { - // this code does folding of arbitrary integer expressions, eg 1 + 2 * 3 + 4 + // this code does folding of arbitrary numeric expressions, eg 1 + 2 * 3 + 4 // it does not do partial folding, eg 1 + 2 + x -> 3 + x mp_obj_t arg0; @@ -716,7 +752,7 @@ static bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) { || rule_id == RULE_power) { // folding for binary ops: | ^ & ** mp_parse_node_t pn = peek_result(parser, num_args - 1); - if (!mp_parse_node_get_int_maybe(pn, &arg0)) { + if (!mp_parse_node_get_number_maybe(pn, &arg0)) { return false; } mp_binary_op_t op; @@ -732,58 +768,61 @@ static bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) { for (ssize_t i = num_args - 2; i >= 0; --i) { pn = peek_result(parser, i); mp_obj_t arg1; - if (!mp_parse_node_get_int_maybe(pn, &arg1)) { + if (!mp_parse_node_get_number_maybe(pn, &arg1)) { return false; } + #if !MICROPY_COMP_CONST_FLOAT if (op == MP_BINARY_OP_POWER && mp_obj_int_sign(arg1) < 0) { // ** can't have negative rhs return false; } - arg0 = mp_binary_op(op, arg0, arg1); + #endif + if (!binary_op_maybe(op, arg0, arg1, &arg0)) { + return false; + } } } else if (rule_id == RULE_shift_expr || rule_id == RULE_arith_expr || rule_id == RULE_term) { // folding for binary ops: << >> + - * @ / % // mp_parse_node_t pn = peek_result(parser, num_args - 1); - if (!mp_parse_node_get_int_maybe(pn, &arg0)) { + if (!mp_parse_node_get_number_maybe(pn, &arg0)) { return false; } for (ssize_t i = num_args - 2; i >= 1; i -= 2) { pn = peek_result(parser, i - 1); mp_obj_t arg1; - if (!mp_parse_node_get_int_maybe(pn, &arg1)) { + if (!mp_parse_node_get_number_maybe(pn, &arg1)) { return false; } mp_token_kind_t tok = MP_PARSE_NODE_LEAF_ARG(peek_result(parser, i)); - if (tok == MP_TOKEN_OP_AT || tok == MP_TOKEN_OP_SLASH) { - // Can't fold @ or / + if (tok == MP_TOKEN_OP_AT) { + // Can't fold @ + return false; + } + #if !MICROPY_COMP_CONST_FLOAT + if (tok == MP_TOKEN_OP_SLASH) { + // Can't fold / return false; } + #endif mp_binary_op_t op = MP_BINARY_OP_LSHIFT + (tok - MP_TOKEN_OP_DBL_LESS); - int rhs_sign = mp_obj_int_sign(arg1); - if (op <= MP_BINARY_OP_RSHIFT) { - // << and >> can't have negative rhs - if (rhs_sign < 0) { - return false; - } - } else if (op >= MP_BINARY_OP_FLOOR_DIVIDE) { - // % and // can't have zero rhs - if (rhs_sign == 0) { - return false; - } + if (!binary_op_maybe(op, arg0, arg1, &arg0)) { + return false; } - arg0 = mp_binary_op(op, arg0, arg1); } } else if (rule_id == RULE_factor_2) { // folding for unary ops: + - ~ mp_parse_node_t pn = peek_result(parser, 0); - if (!mp_parse_node_get_int_maybe(pn, &arg0)) { + if (!mp_parse_node_get_number_maybe(pn, &arg0)) { return false; } mp_token_kind_t tok = MP_PARSE_NODE_LEAF_ARG(peek_result(parser, 1)); mp_unary_op_t op; if (tok == MP_TOKEN_OP_TILDE) { + if (!mp_obj_is_int(arg0)) { + return false; + } op = MP_UNARY_OP_INVERT; } else { assert(tok == MP_TOKEN_OP_PLUS || tok == MP_TOKEN_OP_MINUS); // should be @@ -855,7 +894,7 @@ static bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) { return false; } // id1.id2 - // look it up in constant table, see if it can be replaced with an integer + // look it up in constant table, see if it can be replaced with an integer or a float mp_parse_node_struct_t *pns1 = (mp_parse_node_struct_t *)pn1; assert(MP_PARSE_NODE_IS_ID(pns1->nodes[0])); qstr q_base = MP_PARSE_NODE_LEAF_ARG(pn0); @@ -866,7 +905,7 @@ static bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) { } mp_obj_t dest[2]; mp_load_method_maybe(elem->value, q_attr, dest); - if (!(dest[0] != MP_OBJ_NULL && mp_obj_is_int(dest[0]) && dest[1] == MP_OBJ_NULL)) { + if (!(dest[0] != MP_OBJ_NULL && (mp_obj_is_int(dest[0]) || mp_obj_is_float(dest[0])) && dest[1] == MP_OBJ_NULL)) { return false; } arg0 = dest[0]; diff --git a/tests/float/float_parse_doubleprec.py b/tests/float/float_parse_doubleprec.py index 81fcadcee88b4..c1b0b4823b038 100644 --- a/tests/float/float_parse_doubleprec.py +++ b/tests/float/float_parse_doubleprec.py @@ -19,3 +19,9 @@ print(float("1.00000000000000000000e-307")) print(float("10.0000000000000000000e-308")) print(float("100.000000000000000000e-309")) + +# ensure repr() adds an extra digit when needed for accurate parsing +print(float(repr(float("2.0") ** 100)) == float("2.0") ** 100) + +# ensure repr does not add meaningless extra digits (1.234999999999) +print(repr(1.2345)) diff --git a/tests/micropython/const_error.py b/tests/micropython/const_error.py index d35be530a7c8d..950360e4dc782 100644 --- a/tests/micropython/const_error.py +++ b/tests/micropython/const_error.py @@ -18,8 +18,6 @@ def test_syntax(code): # these operations are not supported within const test_syntax("A = const(1 @ 2)") -test_syntax("A = const(1 / 2)") -test_syntax("A = const(1 ** -2)") test_syntax("A = const(1 << -2)") test_syntax("A = const(1 >> -2)") test_syntax("A = const(1 % 0)") diff --git a/tests/micropython/const_error.py.exp b/tests/micropython/const_error.py.exp index 3edc3efe9c3e9..bef69eb32ea7d 100644 --- a/tests/micropython/const_error.py.exp +++ b/tests/micropython/const_error.py.exp @@ -5,5 +5,3 @@ SyntaxError SyntaxError SyntaxError SyntaxError -SyntaxError -SyntaxError diff --git a/tests/micropython/const_float.py b/tests/micropython/const_float.py new file mode 100644 index 0000000000000..c3a0df0276bf8 --- /dev/null +++ b/tests/micropython/const_float.py @@ -0,0 +1,23 @@ +# test constant optimisation, with consts that are floats + +from micropython import const + +# check we can make consts from floats +F1 = const(2.5) +F2 = const(-0.3) +print(type(F1), F1) +print(type(F2), F2) + +# check arithmetic with floats +F3 = const(F1 + F2) +F4 = const(F1**2) +print(F3, F4) + +# check int operations with float results +F5 = const(1 / 2) +F6 = const(2**-2) +print(F5, F6) + +# note: we also test float expression folding when +# we're compiling test cases in tests/float, as +# many expressions are resolved at compile time. diff --git a/tests/micropython/const_float.py.exp b/tests/micropython/const_float.py.exp new file mode 100644 index 0000000000000..17a86a6d936c2 --- /dev/null +++ b/tests/micropython/const_float.py.exp @@ -0,0 +1,4 @@ + 2.5 + -0.3 +2.2 6.25 +0.5 0.25 diff --git a/tests/micropython/const_math.py b/tests/micropython/const_math.py new file mode 100644 index 0000000000000..7ee5edc6d3240 --- /dev/null +++ b/tests/micropython/const_math.py @@ -0,0 +1,18 @@ +# Test expressions based on math module constants +try: + import math +except ImportError: + print("SKIP") + raise SystemExit + +from micropython import const + +# check that we can make consts from math constants +# (skip if the target has MICROPY_COMP_MODULE_CONST disabled) +try: + exec("two_pi = const(2.0 * math.pi)") +except SyntaxError: + print("SKIP") + raise SystemExit + +print(math.cos(two_pi)) diff --git a/tests/micropython/const_math.py.exp b/tests/micropython/const_math.py.exp new file mode 100644 index 0000000000000..d3827e75a5cad --- /dev/null +++ b/tests/micropython/const_math.py.exp @@ -0,0 +1 @@ +1.0 From f39434e9fb17405754277dd62dfea788a42d5afb Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 27 Jul 2025 22:16:24 +0100 Subject: [PATCH 032/143] py/asmthumb: Don't corrupt base register in large offset store. asm_thumb_store_reg_reg_offset() modifies the base register when storing with a large offset which triggers the generic path. If a variable lives in that register, this corrupts it. Fix this by saving the base register on the stack before modifying it. Signed-off-by: Chris Webb --- py/asmthumb.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py/asmthumb.c b/py/asmthumb.c index 18c3db9e4e28f..93860d2fdc1a2 100644 --- a/py/asmthumb.c +++ b/py/asmthumb.c @@ -492,8 +492,10 @@ void asm_thumb_store_reg_reg_offset(asm_thumb_t *as, uint reg_src, uint reg_base asm_thumb_op32(as, (OP_LDR_STR_W_HI(operation_size, reg_base) | OP_STR_W), OP_LDR_STR_W_LO(reg_src, (offset << operation_size))); } else { // Must use the generic sequence + asm_thumb_op16(as, OP_PUSH_RLIST(1 << reg_base)); asm_thumb_add_reg_reg_offset(as, reg_base, reg_base, offset - 31, operation_size); asm_thumb_op16(as, ((OP_LDR_STR_TABLE[operation_size] | OP_STR) << 11) | (31 << 6) | (reg_base << 3) | reg_src); + asm_thumb_op16(as, OP_POP_RLIST(1 << reg_base)); } } From 953da2080eb9115536101627c7406727c5d44f8b Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 27 Jul 2025 22:35:02 +0100 Subject: [PATCH 033/143] tests/micropython: Test that viper offset stores don't clobber base reg. When running the viper boundary tests, assert that the offset stores don't clobber the base register, which is saved and temporarily modified on some architectures. Signed-off-by: Chris Webb --- tests/micropython/viper_ptr16_store_boundary_intbig.py | 4 ++++ tests/micropython/viper_ptr32_store_boundary_intbig.py | 4 ++++ tests/micropython/viper_ptr8_store_boundary_intbig.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/tests/micropython/viper_ptr16_store_boundary_intbig.py b/tests/micropython/viper_ptr16_store_boundary_intbig.py index 1694c61ac0a61..2193eddae1385 100644 --- a/tests/micropython/viper_ptr16_store_boundary_intbig.py +++ b/tests/micropython/viper_ptr16_store_boundary_intbig.py @@ -3,7 +3,9 @@ SET_TEMPLATE = """ @micropython.viper def set{off}(dest: ptr16): + saved = dest dest[{off}] = {val} + assert int(saved) == int(dest) set{off}(buffer) print(hex(get_index(buffer, {off}))) """ @@ -15,7 +17,9 @@ def set{off}(dest: ptr16): @micropython.viper def set_index(dest: ptr16, i: int, val: uint): + saved = dest dest[i] = val + assert int(saved) == int(dest) def get_index(src, i): diff --git a/tests/micropython/viper_ptr32_store_boundary_intbig.py b/tests/micropython/viper_ptr32_store_boundary_intbig.py index 5109abb9dcaa8..b44f31b00af4d 100644 --- a/tests/micropython/viper_ptr32_store_boundary_intbig.py +++ b/tests/micropython/viper_ptr32_store_boundary_intbig.py @@ -3,7 +3,9 @@ SET_TEMPLATE = """ @micropython.viper def set{off}(dest: ptr32): + saved = dest dest[{off}] = {val} + assert int(saved) == int(dest) set{off}(buffer) print(hex(get_index(buffer, {off}))) """ @@ -15,7 +17,9 @@ def set{off}(dest: ptr32): @micropython.viper def set_index(dest: ptr32, i: int, val: uint): + saved = dest dest[i] = val + assert int(saved) == int(dest) def get_index(src, i): diff --git a/tests/micropython/viper_ptr8_store_boundary_intbig.py b/tests/micropython/viper_ptr8_store_boundary_intbig.py index e1fe6dcae3279..d22a062743321 100644 --- a/tests/micropython/viper_ptr8_store_boundary_intbig.py +++ b/tests/micropython/viper_ptr8_store_boundary_intbig.py @@ -3,7 +3,9 @@ SET_TEMPLATE = """ @micropython.viper def set{off}(dest: ptr8): + saved = dest dest[{off}] = {val} + assert int(saved) == int(dest) set{off}(buffer) print(hex(get_index(buffer, {off}))) """ @@ -15,7 +17,9 @@ def set{off}(dest: ptr8): @micropython.viper def set_index(dest: ptr8, i: int, val: uint): + saved = dest dest[i] = val + assert int(dest) == int(saved) def get_index(src: ptr8, i: int): From 4ba626ab5a6c47588d25c40ac1dc20d59e33760d Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Mon, 28 Jul 2025 16:30:04 +0200 Subject: [PATCH 034/143] tools/mpremote: Fix errno.ENOTBLK attribute error on Windows. Not all errors defined in stdlib errno are available on Windows. Specifically, errno.ENOTBLK is not. Fixes issue #17773. Signed-off-by: Jos Verlinde --- tools/mpremote/mpremote/mp_errno.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/mpremote/mpremote/mp_errno.py b/tools/mpremote/mpremote/mp_errno.py index 37cb1e0cb9b04..e2554ef1db6e8 100644 --- a/tools/mpremote/mpremote/mp_errno.py +++ b/tools/mpremote/mpremote/mp_errno.py @@ -1,4 +1,5 @@ import errno +import platform # This table maps numeric values defined by `py/mperrno.h` to host errno code. MP_ERRNO_TABLE = { @@ -16,7 +17,6 @@ 12: errno.ENOMEM, 13: errno.EACCES, 14: errno.EFAULT, - 15: errno.ENOTBLK, 16: errno.EBUSY, 17: errno.EEXIST, 18: errno.EXDEV, @@ -51,3 +51,5 @@ 115: errno.EINPROGRESS, 125: errno.ECANCELED, } +if platform.system() != "Windows": + MP_ERRNO_TABLE[15] = errno.ENOTBLK From dea949e860c0adc615171d25c7df79b59639f8ed Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Mon, 28 Jul 2025 16:52:11 +0200 Subject: [PATCH 035/143] tools/mpremote: Update ESPxxx detection for USB-CDC ports. Detection of ESP-XX devices was based on just the names of the USB driver name, and did not account for the switch of the newer ESP-xx devices to USB-CDC. On Windows this caused unwanted/unneeded resets as the DTR/RTS signals are also used for automatic device reset over USB-CDC. See https://github.com/micropython/micropython/issues/9659#issuecomment-3124704572 This commit uses the Espressif registered VID 0x303A to detect USB-CDC ports, to enable the same DTR/RTS settings as used on the UART-USB connection. Also improved the robustness of the code using `getattr()`. Signed-off-by: Jos Verlinde --- tools/mpremote/mpremote/transport_serial.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/mpremote/mpremote/transport_serial.py b/tools/mpremote/mpremote/transport_serial.py index 53fc48553b167..daeff02b594e2 100644 --- a/tools/mpremote/mpremote/transport_serial.py +++ b/tools/mpremote/mpremote/transport_serial.py @@ -40,6 +40,8 @@ from .console import VT_ENABLED from .transport import TransportError, TransportExecError, Transport +VID_ESPRESSIF = 0x303A # Espressif Incorporated + class SerialTransport(Transport): fs_hook_mount = "/remote" # MUST match the mount point in fs_hook_code @@ -71,7 +73,10 @@ def __init__(self, device, baudrate=115200, wait=0, exclusive=True, timeout=None self.serial = serial.Serial(**serial_kwargs) self.serial.port = device portinfo = list(serial.tools.list_ports.grep(device)) # type: ignore - if portinfo and portinfo[0].manufacturer != "Microsoft": + if portinfo and ( + getattr(portinfo[0], "vid", 0) == VID_ESPRESSIF + or getattr(portinfo[0], "manufacturer", "") != "Microsoft" + ): # ESP8266/ESP32 boards use RTS/CTS for flashing and boot mode selection. # DTR False: to avoid using the reset button will hang the MCU in bootloader mode # RTS False: to prevent pulses on rts on serial.close() that would POWERON_RESET an ESPxx From 41987c6cf43c18ff387e88d227469bf8b8e29b55 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 29 Jul 2025 17:19:09 +1000 Subject: [PATCH 036/143] rp2/rp2_pio: Configure jmp_pin for PIO use if it's isolation is set. Signed-off-by: Damien George --- ports/rp2/rp2_pio.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ports/rp2/rp2_pio.c b/ports/rp2/rp2_pio.c index d936553b55573..8fc7c0e786754 100644 --- a/ports/rp2/rp2_pio.c +++ b/ports/rp2/rp2_pio.c @@ -683,8 +683,10 @@ static mp_obj_t rp2_state_machine_init_helper(const rp2_state_machine_obj_t *sel } // Configure jmp pin, if needed. + int jmp_pin = -1; if (args[ARG_jmp_pin].u_obj != mp_const_none) { - sm_config_set_jmp_pin(&config, mp_hal_get_pin_obj(args[ARG_jmp_pin].u_obj)); + jmp_pin = mp_hal_get_pin_obj(args[ARG_jmp_pin].u_obj); + sm_config_set_jmp_pin(&config, jmp_pin); } // Configure sideset pin, if needed. @@ -716,6 +718,18 @@ static mp_obj_t rp2_state_machine_init_helper(const rp2_state_machine_obj_t *sel if (set_config.base >= 0) { asm_pio_init_gpio(self->pio, self->sm, &set_config); } + #if !PICO_RP2040 + if (jmp_pin >= 0) { + // On RP2350 pins by default have their isolation enabled. This means they will + // not work as input to a PIO without further configuration. That's different to + // RP2040 where pins can work as PIO input from a reset. To make RP2350 have + // similar behaviour as RP2040, configure the jmp pin for PIO use if it's isolation + // is enabled (which means it's probably unconfigured from reset). + if (pads_bank0_hw->io[jmp_pin] & PADS_BANK0_GPIO0_ISO_BITS) { + pio_gpio_init(self->pio, jmp_pin); + } + } + #endif if (sideset_config.base >= 0) { asm_pio_init_gpio(self->pio, self->sm, &sideset_config); } From a9a606bf5d518375efb2b836a07df7d95dbd7b56 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 29 Jul 2025 17:25:35 +1000 Subject: [PATCH 037/143] docs/library/rp2.StateMachine: Add a note about PIO in and jmp pins. Signed-off-by: Damien George --- docs/library/rp2.StateMachine.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/library/rp2.StateMachine.rst b/docs/library/rp2.StateMachine.rst index 1cb87e90b6e2d..4984be0b2183c 100644 --- a/docs/library/rp2.StateMachine.rst +++ b/docs/library/rp2.StateMachine.rst @@ -58,6 +58,11 @@ Methods - *pull_thresh* is the threshold in bits before auto-pull or conditional re-pulling is triggered. + Note: pins used for *in_base* need to be configured manually for input (or + otherwise) so that the PIO can see the desired signal (they could be input + pins, output pins, or connected to a different peripheral). The *jmp_pin* + can also be configured manually, but by default will be an input pin. + .. method:: StateMachine.active([value]) Gets or sets whether the state machine is currently running. From c9b52b2b7f164745eda7210ccf6640fce3737f59 Mon Sep 17 00:00:00 2001 From: Dryw Wade Date: Thu, 31 Jul 2025 15:37:46 -0600 Subject: [PATCH 038/143] rp2/CMakeLists.txt: Fix flash size check logic. Follow up to 6bfb83e30aa28e7bbfb0f77f378da05b32574f3d, if the variable `PICO_FLASH_SIZE_BYTES` is not a numeric constant, eg "(2 * 1024 * 1024)", then it won't pass the GREATER check. So change the if logic to just test if it's defined. Signed-off-by: Dryw Wade --- ports/rp2/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/rp2/CMakeLists.txt b/ports/rp2/CMakeLists.txt index 49ba8d77d6352..120d07bcce1be 100644 --- a/ports/rp2/CMakeLists.txt +++ b/ports/rp2/CMakeLists.txt @@ -528,7 +528,7 @@ target_link_options(${MICROPY_TARGET} PRIVATE -Wl,--wrap=runtime_init_clocks ) -if(PICO_FLASH_SIZE_BYTES GREATER 0) +if(DEFINED PICO_FLASH_SIZE_BYTES) target_link_options(${MICROPY_TARGET} PRIVATE -Wl,--defsym=__micropy_flash_size__=${PICO_FLASH_SIZE_BYTES} ) From 769453c7504ebf71d65a57767fbeea41f2de63f4 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 17 Jun 2025 23:17:43 -0400 Subject: [PATCH 039/143] rp2/rp2_pio: Fix use of PIO2 in prog data structure. The RP2350 PIO2 State Machines (8, 9, 10, 11) did not work. The data structure used to pass the PIO arguments was missing an entry for PIO2, thus causing the PIO2 instances to write wrong data to wrong locations. Fixes issue #17509. Signed-off-by: Matt Westveld --- ports/rp2/modules/rp2.py | 15 ++++++++------- ports/rp2/rp2_pio.c | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ports/rp2/modules/rp2.py b/ports/rp2/modules/rp2.py index 6068926036baf..442a802b3e9d7 100644 --- a/ports/rp2/modules/rp2.py +++ b/ports/rp2/modules/rp2.py @@ -7,12 +7,13 @@ _PROG_DATA = const(0) _PROG_OFFSET_PIO0 = const(1) _PROG_OFFSET_PIO1 = const(2) -_PROG_EXECCTRL = const(3) -_PROG_SHIFTCTRL = const(4) -_PROG_OUT_PINS = const(5) -_PROG_SET_PINS = const(6) -_PROG_SIDESET_PINS = const(7) -_PROG_MAX_FIELDS = const(8) +_PROG_OFFSET_PIO2 = const(3) +_PROG_EXECCTRL = const(4) +_PROG_SHIFTCTRL = const(5) +_PROG_OUT_PINS = const(6) +_PROG_SET_PINS = const(7) +_PROG_SIDESET_PINS = const(8) +_PROG_MAX_FIELDS = const(9) class PIOASMError(Exception): @@ -50,7 +51,7 @@ def __init__( | autopull << 17 | autopush << 16 ) - self.prog = [array("H"), -1, -1, execctrl, shiftctrl, out_init, set_init, sideset_init] + self.prog = [array("H"), -1, -1, -1, execctrl, shiftctrl, out_init, set_init, sideset_init] self.wrap_used = False diff --git a/ports/rp2/rp2_pio.c b/ports/rp2/rp2_pio.c index 8fc7c0e786754..611e74a1587d2 100644 --- a/ports/rp2/rp2_pio.c +++ b/ports/rp2/rp2_pio.c @@ -212,6 +212,7 @@ enum { PROG_DATA, PROG_OFFSET_PIO0, PROG_OFFSET_PIO1, + PROG_OFFSET_PIO2, PROG_EXECCTRL, PROG_SHIFTCTRL, PROG_OUT_PINS, From ab7c5a1733b394af43035bdcf844b91fd7756dd9 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Thu, 10 Jul 2025 01:50:55 +0900 Subject: [PATCH 040/143] docs/library/btree: Fix method links to explicitly specify class. So they don't clash with other potential references. Signed-off-by: Koudai Aono --- docs/library/btree.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/library/btree.rst b/docs/library/btree.rst index 9d1dcf1110dc7..5746695511157 100644 --- a/docs/library/btree.rst +++ b/docs/library/btree.rst @@ -151,10 +151,10 @@ Constants .. data:: INCL - A flag for `keys()`, `values()`, `items()` methods to specify that + A flag for :meth:`btree.keys`, :meth:`btree.values`, :meth:`btree.items` methods to specify that scanning should be inclusive of the end key. .. data:: DESC - A flag for `keys()`, `values()`, `items()` methods to specify that + A flag for :meth:`btree.keys`, :meth:`btree.values`, :meth:`btree.items` methods to specify that scanning should be in descending direction of keys. From 1b578fe2c04402b1fe85b49fb5a89c23ae961f89 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 27 May 2025 12:21:57 +1000 Subject: [PATCH 041/143] extmod/machine_i2c_target: Add new machine.I2CTarget class. This commit implements a generic I2C target/peripheral/"slave" device, called `machine.I2CTarget`. It can work in two separate modes: - A general device with interrupts/events/callbacks for low-level I2C operations like address match, read request and stop. - A memory device that allows reading/writing a specific region of memory (or "registers") on the target I2C device. To make a memory device is very simple: from machine import I2CTarget mem = bytearray(8) i2c = I2CTarget(addr=67, mem=mem) That's all that's needed to start the I2C target. From then on it will respond to any I2C controller on the bus, allowing reads and writes to the mem bytearray. It's also possible to register to receive events. For example to be notified when the memory is read/written: from machine import I2CTarget def irq_handler(i2c_target): flags = i2c_target.irq().flags() if flags & I2CTarget.IRQ_END_READ: print("controller read target at addr", i2c_target.memaddr) if flags & I2CTarget.IRQ_END_WRITE: print("controller wrote target at addr", i2c_target.memaddr) mem = bytearray(8) i2c = I2CTarget(addr=67, mem=mem) i2c.irq(irq_handler) Instead of a memory device, an arbitrary I2C device can be implemented using all the events (see docs). This is based on the discussion in #3935. Signed-off-by: Damien George --- extmod/extmod.cmake | 1 + extmod/extmod.mk | 1 + extmod/machine_i2c_target.c | 424 ++++++++++++++++++++++++++++++++++++ extmod/modmachine.c | 3 + extmod/modmachine.h | 6 + 5 files changed, 435 insertions(+) create mode 100644 extmod/machine_i2c_target.c diff --git a/extmod/extmod.cmake b/extmod/extmod.cmake index 7cacd0a43329e..9ca869582e026 100644 --- a/extmod/extmod.cmake +++ b/extmod/extmod.cmake @@ -11,6 +11,7 @@ set(MICROPY_SOURCE_EXTMOD ${MICROPY_EXTMOD_DIR}/machine_adc_block.c ${MICROPY_EXTMOD_DIR}/machine_bitstream.c ${MICROPY_EXTMOD_DIR}/machine_i2c.c + ${MICROPY_EXTMOD_DIR}/machine_i2c_target.c ${MICROPY_EXTMOD_DIR}/machine_i2s.c ${MICROPY_EXTMOD_DIR}/machine_mem.c ${MICROPY_EXTMOD_DIR}/machine_pulse.c diff --git a/extmod/extmod.mk b/extmod/extmod.mk index 997dd3ba98ead..37151ad120e68 100644 --- a/extmod/extmod.mk +++ b/extmod/extmod.mk @@ -6,6 +6,7 @@ SRC_EXTMOD_C += \ extmod/machine_adc_block.c \ extmod/machine_bitstream.c \ extmod/machine_i2c.c \ + extmod/machine_i2c_target.c \ extmod/machine_i2s.c \ extmod/machine_mem.c \ extmod/machine_pinbase.c \ diff --git a/extmod/machine_i2c_target.c b/extmod/machine_i2c_target.c new file mode 100644 index 0000000000000..f63be183c86b6 --- /dev/null +++ b/extmod/machine_i2c_target.c @@ -0,0 +1,424 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/runtime.h" + +#if MICROPY_PY_MACHINE_I2C_TARGET + +#include "extmod/modmachine.h" +#include "shared/runtime/mpirq.h" + +enum { + // Events exposed to Python. + I2C_TARGET_IRQ_ADDR_MATCH_READ = 1 << 0, + I2C_TARGET_IRQ_ADDR_MATCH_WRITE = 1 << 1, + I2C_TARGET_IRQ_READ_REQ = 1 << 2, + I2C_TARGET_IRQ_WRITE_REQ = 1 << 3, + I2C_TARGET_IRQ_END_READ = 1 << 4, + I2C_TARGET_IRQ_END_WRITE = 1 << 5, + + // Internal event, not exposed to Python. + I2C_TARGET_IRQ_MEM_ADDR_MATCH = 1 << 6, +}; + +// Define the IRQs that require a hard interrupt. +#define I2C_TARGET_IRQ_ALL_HARD ( \ + I2C_TARGET_IRQ_ADDR_MATCH_READ \ + | I2C_TARGET_IRQ_ADDR_MATCH_WRITE \ + | I2C_TARGET_IRQ_READ_REQ \ + | I2C_TARGET_IRQ_WRITE_REQ \ + ) + +enum { + STATE_INACTIVE, + STATE_IDLE, + STATE_ADDR_MATCH_READ, + STATE_ADDR_MATCH_WRITE, + STATE_MEM_ADDR_SELECT, + STATE_READING, + STATE_WRITING, +}; + +typedef struct _machine_i2c_target_data_t { + uint8_t state; + uint8_t mem_addr_count; + uint8_t mem_addrsize; + uint32_t mem_addr_last; + uint32_t mem_addr; + uint32_t mem_len; + uint8_t *mem_buf; +} machine_i2c_target_data_t; + +typedef struct _machine_i2c_target_irq_obj_t { + mp_irq_obj_t base; + uint32_t flags; + uint32_t trigger; +} machine_i2c_target_irq_obj_t; + +// The port must provide implementations of these low-level I2C target functions. + +static void mp_machine_i2c_target_event_callback(machine_i2c_target_irq_obj_t *irq); + +// Read up to N bytes, and return the number of bytes read. +static size_t mp_machine_i2c_target_read_bytes(machine_i2c_target_obj_t *self, size_t len, uint8_t *buf); + +// Write (or buffer) N bytes, and return the number of bytes written/buffered. +static size_t mp_machine_i2c_target_write_bytes(machine_i2c_target_obj_t *self, size_t len, const uint8_t *buf); + +// Configure the given events to trigger an interrupt. +static void mp_machine_i2c_target_irq_config(machine_i2c_target_obj_t *self, unsigned int trigger); + +static mp_obj_t mp_machine_i2c_target_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args); +static void mp_machine_i2c_target_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind); +static void mp_machine_i2c_target_deinit(machine_i2c_target_obj_t *self); + +static const mp_irq_methods_t machine_i2c_target_irq_methods; + +static machine_i2c_target_data_t machine_i2c_target_data[MICROPY_PY_MACHINE_I2C_TARGET_MAX]; + +// Needed to retain a root pointer to the memory object. +MP_REGISTER_ROOT_POINTER(mp_obj_t machine_i2c_target_mem_obj[MICROPY_PY_MACHINE_I2C_TARGET_MAX]); + +// Needed to retain a root pointer to the IRQ object. +MP_REGISTER_ROOT_POINTER(void *machine_i2c_target_irq_obj[MICROPY_PY_MACHINE_I2C_TARGET_MAX]); + +static bool handle_event(machine_i2c_target_data_t *data, unsigned int trigger) { + unsigned int id = data - &machine_i2c_target_data[0]; + if (trigger & I2C_TARGET_IRQ_MEM_ADDR_MATCH) { + data->mem_addr_last = data->mem_addr; + } + machine_i2c_target_irq_obj_t *irq = MP_STATE_PORT(machine_i2c_target_irq_obj[id]); + if (irq != NULL && (trigger & irq->trigger)) { + irq->flags = trigger & irq->trigger; + mp_machine_i2c_target_event_callback(irq); + return true; // irq handled + } + return false; // irq not handled +} + +static void machine_i2c_target_data_init(machine_i2c_target_data_t *data, mp_obj_t mem_obj, mp_int_t mem_addrsize) { + data->state = STATE_IDLE; + data->mem_addr_count = 0; + data->mem_addrsize = 0; + data->mem_addr_last = 0; + data->mem_addr = 0; + data->mem_len = 0; + data->mem_buf = NULL; + if (mem_obj != mp_const_none) { + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(mem_obj, &bufinfo, MP_BUFFER_RW); + if (mem_addrsize < 0 || mem_addrsize > 32 || mem_addrsize % 8 != 0) { + mp_raise_ValueError(MP_ERROR_TEXT("mem_addrsize must be 0, 8, 16, 24 or 32")); + } + data->mem_addrsize = mem_addrsize / 8; + data->mem_len = bufinfo.len; + data->mem_buf = bufinfo.buf; + } +} + +static void machine_i2c_target_data_reset_helper(machine_i2c_target_data_t *data) { + if (data->state == STATE_READING) { + handle_event(data, I2C_TARGET_IRQ_END_READ); + } else if (data->state == STATE_ADDR_MATCH_WRITE || data->state == STATE_WRITING) { + handle_event(data, I2C_TARGET_IRQ_END_WRITE); + } + data->state = STATE_IDLE; +} + +static void machine_i2c_target_data_addr_match(machine_i2c_target_data_t *data, bool read) { + machine_i2c_target_data_reset_helper(data); + if (read) { + handle_event(data, I2C_TARGET_IRQ_ADDR_MATCH_READ); + data->state = STATE_ADDR_MATCH_READ; + } else { + handle_event(data, I2C_TARGET_IRQ_ADDR_MATCH_WRITE); + data->state = STATE_ADDR_MATCH_WRITE; + } +} + +static void machine_i2c_target_data_read_request(machine_i2c_target_obj_t *self, machine_i2c_target_data_t *data) { + // Let the user handle the read request. + bool event_handled = handle_event(data, I2C_TARGET_IRQ_READ_REQ); + if (data->mem_buf == NULL) { + data->state = STATE_READING; + if (!event_handled) { + // No data source, just write out a zero. + uint8_t val = 0; + mp_machine_i2c_target_write_bytes(self, 1, &val); + } + } else { + // Have a buffer. + if (data->state == STATE_MEM_ADDR_SELECT) { + // Got a short memory address. + data->mem_addr %= data->mem_len; + handle_event(data, I2C_TARGET_IRQ_MEM_ADDR_MATCH); + } + if (data->state != STATE_READING) { + data->state = STATE_READING; + } + uint8_t val = data->mem_buf[data->mem_addr++]; + if (data->mem_addr >= data->mem_len) { + data->mem_addr = 0; + } + mp_machine_i2c_target_write_bytes(self, 1, &val); + } +} + +static void machine_i2c_target_data_write_request(machine_i2c_target_obj_t *self, machine_i2c_target_data_t *data) { + // Let the user handle the write request. + bool event_handled = handle_event(data, I2C_TARGET_IRQ_WRITE_REQ); + if (data->mem_buf == NULL) { + data->state = STATE_WRITING; + if (!event_handled) { + // No data sink, just read and discard the incoming byte. + uint8_t buf = 0; + mp_machine_i2c_target_read_bytes(self, 1, &buf); + } + } else { + // Have a buffer. + uint8_t buf[4] = {0}; + size_t n = mp_machine_i2c_target_read_bytes(self, sizeof(buf), &buf[0]); + for (size_t i = 0; i < n; ++i) { + uint8_t val = buf[i]; + if (data->state == STATE_ADDR_MATCH_WRITE) { + data->state = STATE_MEM_ADDR_SELECT; + data->mem_addr = 0; + data->mem_addr_count = data->mem_addrsize; + } + if (data->state == STATE_MEM_ADDR_SELECT && data->mem_addr_count > 0) { + data->mem_addr = data->mem_addr << 8 | val; + --data->mem_addr_count; + if (data->mem_addr_count == 0) { + data->mem_addr %= data->mem_len; + handle_event(data, I2C_TARGET_IRQ_MEM_ADDR_MATCH); + } + } else { + if (data->state == STATE_MEM_ADDR_SELECT) { + data->state = STATE_WRITING; + } + data->mem_buf[data->mem_addr++] = val; + if (data->mem_addr >= data->mem_len) { + data->mem_addr = 0; + } + } + } + } +} + +static inline void machine_i2c_target_data_restart_or_stop(machine_i2c_target_data_t *data) { + machine_i2c_target_data_reset_helper(data); +} + +static inline void machine_i2c_target_data_stop(machine_i2c_target_data_t *data) { + machine_i2c_target_data_reset_helper(data); +} + +// The port provides implementations of its bindings in this file. +#include MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE + +static void machine_i2c_target_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + size_t index = mp_machine_i2c_target_get_index(self); + machine_i2c_target_data_t *data = &machine_i2c_target_data[index]; + if (dest[0] == MP_OBJ_NULL) { + // Load attribute. + if (attr == MP_QSTR_memaddr) { + dest[0] = mp_obj_new_int(data->mem_addr_last); + } else { + // Continue lookup in locals_dict. + dest[1] = MP_OBJ_SENTINEL; + } + } +} + +// I2CTarget.deinit() +static mp_obj_t machine_i2c_target_deinit(mp_obj_t self_in) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + size_t index = mp_machine_i2c_target_get_index(self); + if (machine_i2c_target_data[index].state != STATE_INACTIVE) { + machine_i2c_target_data[index].state = STATE_INACTIVE; + mp_machine_i2c_target_deinit(self); + MP_STATE_PORT(machine_i2c_target_mem_obj[index]) = MP_OBJ_NULL; + MP_STATE_PORT(machine_i2c_target_irq_obj[index]) = NULL; + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(machine_i2c_target_deinit_obj, machine_i2c_target_deinit); + +// I2CTarget.readinto(buf) +static mp_obj_t machine_i2c_target_readinto(mp_obj_t self_in, mp_obj_t buf_in) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(buf_in, &bufinfo, MP_BUFFER_READ); + return MP_OBJ_NEW_SMALL_INT(mp_machine_i2c_target_read_bytes(self, bufinfo.len, bufinfo.buf)); +} +static MP_DEFINE_CONST_FUN_OBJ_2(machine_i2c_target_readinto_obj, machine_i2c_target_readinto); + +// I2CTarget.write(data) +static mp_obj_t machine_i2c_target_write(mp_obj_t self_in, mp_obj_t data_in) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(data_in, &bufinfo, MP_BUFFER_READ); + return MP_OBJ_NEW_SMALL_INT(mp_machine_i2c_target_write_bytes(self, bufinfo.len, bufinfo.buf)); +} +static MP_DEFINE_CONST_FUN_OBJ_2(machine_i2c_target_write_obj, machine_i2c_target_write); + +static machine_i2c_target_irq_obj_t *machine_i2c_target_get_irq(machine_i2c_target_obj_t *self) { + // Get the IRQ object. + size_t index = mp_machine_i2c_target_get_index(self); + machine_i2c_target_irq_obj_t *irq = MP_STATE_PORT(machine_i2c_target_irq_obj[index]); + + // Allocate the IRQ object if it doesn't already exist. + if (irq == NULL) { + irq = m_new_obj(machine_i2c_target_irq_obj_t); + irq->base.base.type = &mp_irq_type; + irq->base.methods = (mp_irq_methods_t *)&machine_i2c_target_irq_methods; + irq->base.parent = MP_OBJ_FROM_PTR(self); + irq->base.handler = mp_const_none; + irq->base.ishard = false; + MP_STATE_PORT(machine_i2c_target_irq_obj[index]) = irq; + } + return irq; +} + +// I2CTarget.irq(handler=None, trigger=IRQ_END_READ|IRQ_END_WRITE, hard=False) +static mp_obj_t machine_i2c_target_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_handler, ARG_trigger, ARG_hard }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_handler, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_trigger, MP_ARG_INT, {.u_int = I2C_TARGET_IRQ_END_READ | I2C_TARGET_IRQ_END_WRITE} }, + { MP_QSTR_hard, MP_ARG_BOOL, {.u_bool = false} }, + }; + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + machine_i2c_target_irq_obj_t *irq = machine_i2c_target_get_irq(self); + + if (n_args > 1 || kw_args->used != 0) { + // Update IRQ data. + mp_obj_t handler = args[ARG_handler].u_obj; + mp_uint_t trigger = args[ARG_trigger].u_int; + bool hard = args[ARG_hard].u_bool; + + #if MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ + if ((trigger & I2C_TARGET_IRQ_ALL_HARD) && !hard) { + mp_raise_ValueError(MP_ERROR_TEXT("hard IRQ required")); + } + #else + if (hard) { + mp_raise_ValueError(MP_ERROR_TEXT("hard IRQ unsupported")); + } + #endif + + // Disable all IRQs while data is updated. + mp_machine_i2c_target_irq_config(self, 0); + + // Update IRQ data. + irq->base.handler = handler; + irq->base.ishard = hard; + irq->flags = 0; + irq->trigger = trigger; + + // Enable IRQ if a handler is given. + if (handler != mp_const_none && trigger != 0) { + mp_machine_i2c_target_irq_config(self, trigger); + } + } + return MP_OBJ_FROM_PTR(irq); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(machine_i2c_target_irq_obj, 1, machine_i2c_target_irq); + +static const mp_rom_map_elem_t machine_i2c_target_locals_dict_table[] = { + #if MICROPY_PY_MACHINE_I2C_TARGET_FINALISER + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&machine_i2c_target_deinit_obj) }, + #endif + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&machine_i2c_target_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&machine_i2c_target_readinto_obj) }, + { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&machine_i2c_target_write_obj) }, + { MP_ROM_QSTR(MP_QSTR_irq), MP_ROM_PTR(&machine_i2c_target_irq_obj) }, + + #if MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ + { MP_ROM_QSTR(MP_QSTR_IRQ_ADDR_MATCH_READ), MP_ROM_INT(I2C_TARGET_IRQ_ADDR_MATCH_READ) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_ADDR_MATCH_WRITE), MP_ROM_INT(I2C_TARGET_IRQ_ADDR_MATCH_WRITE) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_READ_REQ), MP_ROM_INT(I2C_TARGET_IRQ_READ_REQ) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_WRITE_REQ), MP_ROM_INT(I2C_TARGET_IRQ_WRITE_REQ) }, + #endif + { MP_ROM_QSTR(MP_QSTR_IRQ_END_READ), MP_ROM_INT(I2C_TARGET_IRQ_END_READ) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_END_WRITE), MP_ROM_INT(I2C_TARGET_IRQ_END_WRITE) }, +}; +static MP_DEFINE_CONST_DICT(machine_i2c_target_locals_dict, machine_i2c_target_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + machine_i2c_target_type, + MP_QSTR_I2CTarget, + MP_TYPE_FLAG_NONE, + make_new, mp_machine_i2c_target_make_new, + print, mp_machine_i2c_target_print, + attr, &machine_i2c_target_attr, + locals_dict, &machine_i2c_target_locals_dict + ); + +static mp_uint_t machine_i2c_target_irq_trigger(mp_obj_t self_in, mp_uint_t new_trigger) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + size_t index = mp_machine_i2c_target_get_index(self); + machine_i2c_target_irq_obj_t *irq = MP_STATE_PORT(machine_i2c_target_irq_obj[index]); + mp_machine_i2c_target_irq_config(self, 0); + irq->flags = 0; + irq->trigger = new_trigger; + mp_machine_i2c_target_irq_config(self, new_trigger); + return 0; +} + +static mp_uint_t machine_i2c_target_irq_info(mp_obj_t self_in, mp_uint_t info_type) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + size_t index = mp_machine_i2c_target_get_index(self); + machine_i2c_target_irq_obj_t *irq = MP_STATE_PORT(machine_i2c_target_irq_obj[index]); + if (info_type == MP_IRQ_INFO_FLAGS) { + return irq->flags; + } else if (info_type == MP_IRQ_INFO_TRIGGERS) { + return irq->trigger; + } + return 0; +} + +static const mp_irq_methods_t machine_i2c_target_irq_methods = { + .trigger = machine_i2c_target_irq_trigger, + .info = machine_i2c_target_irq_info, +}; + +#if !MICROPY_PY_MACHINE_I2C_TARGET_FINALISER +void mp_machine_i2c_target_deinit_all(void) { + for (size_t i = 0; i < MICROPY_PY_MACHINE_I2C_TARGET_MAX; ++i) { + if (machine_i2c_target_data[i].state != STATE_INACTIVE) { + machine_i2c_target_deinit(MP_OBJ_FROM_PTR(&machine_i2c_target_obj[i])); + } + } +} +#endif + +#endif // MICROPY_PY_MACHINE_I2C_TARGET diff --git a/extmod/modmachine.c b/extmod/modmachine.c index f2570123e3751..28b60683b1e58 100644 --- a/extmod/modmachine.c +++ b/extmod/modmachine.c @@ -219,6 +219,9 @@ static const mp_rom_map_elem_t machine_module_globals_table[] = { #if MICROPY_PY_MACHINE_I2C { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&machine_i2c_type) }, #endif + #if MICROPY_PY_MACHINE_I2C_TARGET + { MP_ROM_QSTR(MP_QSTR_I2CTarget), MP_ROM_PTR(&machine_i2c_target_type) }, + #endif #if MICROPY_PY_MACHINE_I2S { MP_ROM_QSTR(MP_QSTR_I2S), MP_ROM_PTR(&machine_i2s_type) }, #endif diff --git a/extmod/modmachine.h b/extmod/modmachine.h index 26010be8e18f9..ef507aca7408c 100644 --- a/extmod/modmachine.h +++ b/extmod/modmachine.h @@ -129,6 +129,7 @@ // A port must provide these types, but they are otherwise opaque. typedef struct _machine_adc_obj_t machine_adc_obj_t; typedef struct _machine_adc_block_obj_t machine_adc_block_obj_t; +typedef struct _machine_i2c_target_obj_t machine_i2c_target_obj_t; typedef struct _machine_i2s_obj_t machine_i2s_obj_t; typedef struct _machine_pwm_obj_t machine_pwm_obj_t; typedef struct _machine_uart_obj_t machine_uart_obj_t; @@ -203,6 +204,7 @@ extern const machine_mem_obj_t machine_mem32_obj; extern const mp_obj_type_t machine_adc_type; extern const mp_obj_type_t machine_adc_block_type; extern const mp_obj_type_t machine_i2c_type; +extern const mp_obj_type_t machine_i2c_target_type; extern const mp_obj_type_t machine_i2s_type; extern const mp_obj_type_t machine_mem_type; extern const mp_obj_type_t machine_pin_type; @@ -261,6 +263,10 @@ int mp_machine_i2c_transfer_adaptor(mp_obj_base_t *self, uint16_t addr, size_t n int mp_machine_soft_i2c_transfer(mp_obj_base_t *self, uint16_t addr, size_t n, mp_machine_i2c_buf_t *bufs, unsigned int flags); #endif +#if MICROPY_PY_MACHINE_I2C_TARGET +void mp_machine_i2c_target_deinit_all(void); +#endif + #if MICROPY_PY_MACHINE_SPI mp_obj_t mp_machine_spi_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args); MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(mp_machine_spi_read_obj); From 9b1778fc77af2f1631143adc0f4f760e184260ac Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 27 May 2025 11:06:08 +1000 Subject: [PATCH 042/143] stm32/i2c: Move I2C IRQ handlers from stm32_it.c to i2c.c. And add MP_STATIC_ASSERT to statically check that the IRQ names are correct on the MCU that it's compiled for. Signed-off-by: Damien George --- ports/stm32/i2c.c | 68 ++++++++++++++++++++++++++++++++++++++++++ ports/stm32/stm32_it.c | 61 ------------------------------------- 2 files changed, 68 insertions(+), 61 deletions(-) diff --git a/ports/stm32/i2c.c b/ports/stm32/i2c.c index a1fde7e6ba114..883734bb1af60 100644 --- a/ports/stm32/i2c.c +++ b/ports/stm32/i2c.c @@ -590,4 +590,72 @@ int i2c_find_peripheral(mp_obj_t id) { return i2c_id; } +#if MICROPY_PY_PYB_LEGACY + +#if defined(MICROPY_HW_I2C1_SCL) +void I2C1_EV_IRQHandler(void) { + MP_STATIC_ASSERT(I2C1_EV_IRQn > 0); + IRQ_ENTER(I2C1_EV_IRQn); + i2c_ev_irq_handler(1); + IRQ_EXIT(I2C1_EV_IRQn); +} + +void I2C1_ER_IRQHandler(void) { + MP_STATIC_ASSERT(I2C1_ER_IRQn > 0); + IRQ_ENTER(I2C1_ER_IRQn); + i2c_er_irq_handler(1); + IRQ_EXIT(I2C1_ER_IRQn); +} +#endif // defined(MICROPY_HW_I2C1_SCL) + +#if defined(MICROPY_HW_I2C2_SCL) +void I2C2_EV_IRQHandler(void) { + MP_STATIC_ASSERT(I2C2_EV_IRQn > 0); + IRQ_ENTER(I2C2_EV_IRQn); + i2c_ev_irq_handler(2); + IRQ_EXIT(I2C2_EV_IRQn); +} + +void I2C2_ER_IRQHandler(void) { + MP_STATIC_ASSERT(I2C2_ER_IRQn > 0); + IRQ_ENTER(I2C2_ER_IRQn); + i2c_er_irq_handler(2); + IRQ_EXIT(I2C2_ER_IRQn); +} +#endif // defined(MICROPY_HW_I2C2_SCL) + +#if defined(MICROPY_HW_I2C3_SCL) +void I2C3_EV_IRQHandler(void) { + MP_STATIC_ASSERT(I2C3_EV_IRQn > 0); + IRQ_ENTER(I2C3_EV_IRQn); + i2c_ev_irq_handler(3); + IRQ_EXIT(I2C3_EV_IRQn); +} + +void I2C3_ER_IRQHandler(void) { + MP_STATIC_ASSERT(I2C3_ER_IRQn > 0); + IRQ_ENTER(I2C3_ER_IRQn); + i2c_er_irq_handler(3); + IRQ_EXIT(I2C3_ER_IRQn); +} +#endif // defined(MICROPY_HW_I2C3_SCL) + +#if defined(MICROPY_HW_I2C4_SCL) +void I2C4_EV_IRQHandler(void) { + MP_STATIC_ASSERT(I2C4_EV_IRQn > 0); + IRQ_ENTER(I2C4_EV_IRQn); + i2c_ev_irq_handler(4); + IRQ_EXIT(I2C4_EV_IRQn); +} + +void I2C4_ER_IRQHandler(void) { + MP_STATIC_ASSERT(I2C4_ER_IRQn > 0); + IRQ_ENTER(I2C4_ER_IRQn); + i2c_er_irq_handler(4); + IRQ_EXIT(I2C4_ER_IRQn); +} +#endif // defined(MICROPY_HW_I2C4_SCL) + +#endif // MICROPY_PY_PYB_LEGACY + #endif // MICROPY_HW_ENABLE_HW_I2C diff --git a/ports/stm32/stm32_it.c b/ports/stm32/stm32_it.c index 3639e2f0499dd..9eda3cb23974f 100644 --- a/ports/stm32/stm32_it.c +++ b/ports/stm32/stm32_it.c @@ -80,7 +80,6 @@ #include "uart.h" #include "storage.h" #include "dma.h" -#include "i2c.h" #include "usb.h" #if defined(MICROPY_HW_USB_FS) @@ -987,63 +986,3 @@ void LPUART2_IRQHandler(void) { IRQ_EXIT(LPUART2_IRQn); } #endif - -#if MICROPY_PY_PYB_LEGACY - -#if defined(MICROPY_HW_I2C1_SCL) -void I2C1_EV_IRQHandler(void) { - IRQ_ENTER(I2C1_EV_IRQn); - i2c_ev_irq_handler(1); - IRQ_EXIT(I2C1_EV_IRQn); -} - -void I2C1_ER_IRQHandler(void) { - IRQ_ENTER(I2C1_ER_IRQn); - i2c_er_irq_handler(1); - IRQ_EXIT(I2C1_ER_IRQn); -} -#endif // defined(MICROPY_HW_I2C1_SCL) - -#if defined(MICROPY_HW_I2C2_SCL) -void I2C2_EV_IRQHandler(void) { - IRQ_ENTER(I2C2_EV_IRQn); - i2c_ev_irq_handler(2); - IRQ_EXIT(I2C2_EV_IRQn); -} - -void I2C2_ER_IRQHandler(void) { - IRQ_ENTER(I2C2_ER_IRQn); - i2c_er_irq_handler(2); - IRQ_EXIT(I2C2_ER_IRQn); -} -#endif // defined(MICROPY_HW_I2C2_SCL) - -#if defined(MICROPY_HW_I2C3_SCL) -void I2C3_EV_IRQHandler(void) { - IRQ_ENTER(I2C3_EV_IRQn); - i2c_ev_irq_handler(3); - IRQ_EXIT(I2C3_EV_IRQn); -} - -void I2C3_ER_IRQHandler(void) { - IRQ_ENTER(I2C3_ER_IRQn); - i2c_er_irq_handler(3); - IRQ_EXIT(I2C3_ER_IRQn); -} -#endif // defined(MICROPY_HW_I2C3_SCL) - -#if defined(MICROPY_HW_I2C4_SCL) -void I2C4_EV_IRQHandler(void) { - IRQ_ENTER(I2C4_EV_IRQn); - i2c_ev_irq_handler(4); - IRQ_EXIT(I2C4_EV_IRQn); -} - -void I2C4_ER_IRQHandler(void) { - IRQ_ENTER(I2C4_ER_IRQn); - i2c_er_irq_handler(4); - IRQ_EXIT(I2C4_ER_IRQn); -} -#endif // defined(MICROPY_HW_I2C4_SCL) - -#endif // MICROPY_PY_PYB_LEGACY From a4ca42f094f0c33ba8d4e8db2f9351586c3104e2 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sat, 19 Jul 2025 01:59:41 +1000 Subject: [PATCH 043/143] stm32/i2cslave: Change irq handler name to i2c_slave_irq_handler. Remove the "ev" part, so this handler can be generalised to also handle error IRQs. Signed-off-by: Damien George --- ports/stm32/i2cslave.c | 4 ++-- ports/stm32/i2cslave.h | 2 +- ports/stm32/mboot/main.c | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ports/stm32/i2cslave.c b/ports/stm32/i2cslave.c index a575c53085168..44604db69f2df 100644 --- a/ports/stm32/i2cslave.c +++ b/ports/stm32/i2cslave.c @@ -35,7 +35,7 @@ void i2c_slave_init_helper(i2c_slave_t *i2c, int addr) { i2c->CR1 = I2C_CR1_ACK | I2C_CR1_PE; } -void i2c_slave_ev_irq_handler(i2c_slave_t *i2c) { +void i2c_slave_irq_handler(i2c_slave_t *i2c) { uint32_t sr1 = i2c->SR1; if (sr1 & I2C_SR1_ADDR) { // Address matched @@ -70,7 +70,7 @@ void i2c_slave_init_helper(i2c_slave_t *i2c, int addr) { i2c->CR1 |= I2C_CR1_PE; } -void i2c_slave_ev_irq_handler(i2c_slave_t *i2c) { +void i2c_slave_irq_handler(i2c_slave_t *i2c) { uint32_t isr = i2c->ISR; if (isr & I2C_ISR_ADDR) { // Address matched diff --git a/ports/stm32/i2cslave.h b/ports/stm32/i2cslave.h index cc4e7f9be92e3..a2108ab2384cb 100644 --- a/ports/stm32/i2cslave.h +++ b/ports/stm32/i2cslave.h @@ -78,7 +78,7 @@ static inline void i2c_slave_shutdown(i2c_slave_t *i2c, int irqn) { NVIC_DisableIRQ(irqn); } -void i2c_slave_ev_irq_handler(i2c_slave_t *i2c); +void i2c_slave_irq_handler(i2c_slave_t *i2c); // These should be provided externally int i2c_slave_process_addr_match(i2c_slave_t *i2c, int rw); diff --git a/ports/stm32/mboot/main.c b/ports/stm32/mboot/main.c index 2be8793351e86..59f2d875798da 100644 --- a/ports/stm32/mboot/main.c +++ b/ports/stm32/mboot/main.c @@ -1755,7 +1755,7 @@ void SysTick_Handler(void) { #if defined(MBOOT_I2C_SCL) void I2Cx_EV_IRQHandler(void) { - i2c_slave_ev_irq_handler(MBOOT_I2Cx); + i2c_slave_irq_handler(MBOOT_I2Cx); } #endif From 17d0449ac80723fd5ab4f5cbba28b39818046814 Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 1 Aug 2025 17:02:53 +1000 Subject: [PATCH 044/143] stm32/i2cslave: Add functions to read/write I2C data. Instead of requiring the callback to consume/provide the data. This allows the data to be consumed/provided later on, which will stretch the I2C clock until that occurs. Signed-off-by: Damien George --- ports/stm32/i2cslave.c | 12 ++++++++---- ports/stm32/i2cslave.h | 24 ++++++++++++++++++++++-- ports/stm32/mboot/main.c | 14 ++++++++------ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/ports/stm32/i2cslave.c b/ports/stm32/i2cslave.c index 44604db69f2df..e60edf53b0017 100644 --- a/ports/stm32/i2cslave.c +++ b/ports/stm32/i2cslave.c @@ -45,10 +45,12 @@ void i2c_slave_irq_handler(i2c_slave_t *i2c) { i2c_slave_process_addr_match(i2c, (sr2 >> I2C_SR2_TRA_Pos) & 1); } if (sr1 & I2C_SR1_TXE) { - i2c->DR = i2c_slave_process_tx_byte(i2c); + // This callback must call i2c_slave_write_byte. + i2c_slave_process_tx_byte(i2c); } if (sr1 & I2C_SR1_RXNE) { - i2c_slave_process_rx_byte(i2c, i2c->DR); + // This callback must call i2c_slave_read_byte. + i2c_slave_process_rx_byte(i2c); } if (sr1 & I2C_SR1_STOPF) { // STOPF only set at end of RX mode (in TX mode AF is set on NACK) @@ -80,10 +82,12 @@ void i2c_slave_irq_handler(i2c_slave_t *i2c) { i2c_slave_process_addr_match(i2c, (i2c->ISR >> I2C_ISR_DIR_Pos) & 1); } if (isr & I2C_ISR_TXIS) { - i2c->TXDR = i2c_slave_process_tx_byte(i2c); + // This callback must call i2c_slave_write_byte. + i2c_slave_process_tx_byte(i2c); } if (isr & I2C_ISR_RXNE) { - i2c_slave_process_rx_byte(i2c, i2c->RXDR); + // This callback must call i2c_slave_read_byte. + i2c_slave_process_rx_byte(i2c); } if (isr & I2C_ISR_STOPF) { // STOPF only set for STOP condition, not a repeated START diff --git a/ports/stm32/i2cslave.h b/ports/stm32/i2cslave.h index a2108ab2384cb..edead6cb2c50a 100644 --- a/ports/stm32/i2cslave.h +++ b/ports/stm32/i2cslave.h @@ -28,6 +28,8 @@ #include STM32_HAL_H +#if defined(STM32F4) || defined(STM32F7) || defined(STM32H7) || defined(STM32WB) + #if !defined(I2C2_BASE) // This MCU doesn't have I2C2_BASE, define it so that the i2c_idx calculation works. #define I2C2_BASE (I2C1_BASE + ((I2C3_BASE - I2C1_BASE) / 2)) @@ -78,13 +80,31 @@ static inline void i2c_slave_shutdown(i2c_slave_t *i2c, int irqn) { NVIC_DisableIRQ(irqn); } +static inline void i2c_slave_write_byte(i2c_slave_t *i2c, uint8_t value) { + #if defined(STM32F4) + i2c->DR = value; + #else + i2c->TXDR = value; + #endif +} + +static inline uint8_t i2c_slave_read_byte(i2c_slave_t *i2c) { + #if defined(STM32F4) + return i2c->DR; + #else + return i2c->RXDR; + #endif +} + void i2c_slave_irq_handler(i2c_slave_t *i2c); // These should be provided externally int i2c_slave_process_addr_match(i2c_slave_t *i2c, int rw); -int i2c_slave_process_rx_byte(i2c_slave_t *i2c, uint8_t val); +int i2c_slave_process_rx_byte(i2c_slave_t *i2c); void i2c_slave_process_rx_end(i2c_slave_t *i2c); -uint8_t i2c_slave_process_tx_byte(i2c_slave_t *i2c); +void i2c_slave_process_tx_byte(i2c_slave_t *i2c); void i2c_slave_process_tx_end(i2c_slave_t *i2c); +#endif + #endif // MICROPY_INCLUDED_STM32_I2CSLAVE_H diff --git a/ports/stm32/mboot/main.c b/ports/stm32/mboot/main.c index 59f2d875798da..bd796c89638cc 100644 --- a/ports/stm32/mboot/main.c +++ b/ports/stm32/mboot/main.c @@ -805,9 +805,9 @@ int i2c_slave_process_addr_match(i2c_slave_t *i2c, int rw) { return 0; // ACK } -int i2c_slave_process_rx_byte(i2c_slave_t *i2c, uint8_t val) { +int i2c_slave_process_rx_byte(i2c_slave_t *i2c) { if (i2c_obj.cmd_buf_pos < sizeof(i2c_obj.cmd_buf)) { - i2c_obj.cmd_buf[i2c_obj.cmd_buf_pos++] = val; + i2c_obj.cmd_buf[i2c_obj.cmd_buf_pos++] = i2c_slave_read_byte(i2c); } return 0; // ACK } @@ -909,15 +909,17 @@ void i2c_slave_process_rx_end(i2c_slave_t *i2c) { i2c_obj.cmd_arg_sent = false; } -uint8_t i2c_slave_process_tx_byte(i2c_slave_t *i2c) { +void i2c_slave_process_tx_byte(i2c_slave_t *i2c) { + uint8_t value; if (i2c_obj.cmd_send_arg) { i2c_obj.cmd_arg_sent = true; - return i2c_obj.cmd_arg; + value = i2c_obj.cmd_arg; } else if (i2c_obj.cmd_buf_pos < sizeof(i2c_obj.cmd_buf)) { - return i2c_obj.cmd_buf[i2c_obj.cmd_buf_pos++]; + value = i2c_obj.cmd_buf[i2c_obj.cmd_buf_pos++]; } else { - return 0; + value = 0; } + i2c_slave_write_byte(i2c, value); } void i2c_slave_process_tx_end(i2c_slave_t *i2c) { From 2443878bd903a79c1522bd0a572dcd93e5ae777e Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 1 Aug 2025 17:06:26 +1000 Subject: [PATCH 045/143] stm32/i2cslave: Support i2c_slave_process_tx_end callback on F4. The rounds out the F4 implementation to match the other supported MCUs. Signed-off-by: Damien George --- ports/stm32/i2cslave.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ports/stm32/i2cslave.c b/ports/stm32/i2cslave.c index e60edf53b0017..f3bb232f7fbb8 100644 --- a/ports/stm32/i2cslave.c +++ b/ports/stm32/i2cslave.c @@ -28,8 +28,14 @@ #if defined(STM32F4) +// The hardware triggers the following IRQs for the given scenarios: +// - scan (0-length write): ADDR STOPF +// - write of n bytes: ADDR RXNE*n STOPF +// - read of n bytes: ADDR TXE*(n+1) AF +// - write of n bytes then read of m bytes: ADDR RXNE*n ADDR TXE*(m+1) AF + void i2c_slave_init_helper(i2c_slave_t *i2c, int addr) { - i2c->CR2 = I2C_CR2_ITBUFEN | I2C_CR2_ITEVTEN | 4 << I2C_CR2_FREQ_Pos; + i2c->CR2 = I2C_CR2_ITBUFEN | I2C_CR2_ITEVTEN | 4 << I2C_CR2_FREQ_Pos | I2C_CR2_ITERREN; i2c->OAR1 = 1 << 14 | addr << 1; i2c->OAR2 = 0; i2c->CR1 = I2C_CR1_ACK | I2C_CR1_PE; @@ -37,6 +43,14 @@ void i2c_slave_init_helper(i2c_slave_t *i2c, int addr) { void i2c_slave_irq_handler(i2c_slave_t *i2c) { uint32_t sr1 = i2c->SR1; + + // Clear all error flags. + i2c->SR1 &= ~(I2C_SR1_SMBALERT | I2C_SR1_TIMEOUT | I2C_SR1_PECERR | I2C_SR1_OVR | I2C_SR1_AF | I2C_SR1_ARLO | I2C_SR1_BERR); + + if (sr1 & I2C_SR1_AF) { + // A NACK in TX mode, which is a stop condition. + i2c_slave_process_tx_end(i2c); + } if (sr1 & I2C_SR1_ADDR) { // Address matched // Read of SR1, SR2 needed to clear ADDR bit From 78d16672e1bbc2727b879ecaa8ce12be69616c1e Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 29 Jul 2025 00:19:24 +1000 Subject: [PATCH 046/143] stm32/i2cslave: Account for slow addr_match callback. Signed-off-by: Damien George --- ports/stm32/i2cslave.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ports/stm32/i2cslave.c b/ports/stm32/i2cslave.c index f3bb232f7fbb8..0e4fbf48913de 100644 --- a/ports/stm32/i2cslave.c +++ b/ports/stm32/i2cslave.c @@ -94,6 +94,9 @@ void i2c_slave_irq_handler(i2c_slave_t *i2c) { i2c->ISR = I2C_ISR_TXE; i2c->ICR = I2C_ICR_ADDRCF; i2c_slave_process_addr_match(i2c, (i2c->ISR >> I2C_ISR_DIR_Pos) & 1); + // Re-read ISR in case i2c_slave_process_addr_match() took some time + // to process and TXIS/RXNE was set in the meantime. + isr = i2c->ISR; } if (isr & I2C_ISR_TXIS) { // This callback must call i2c_slave_write_byte. From 01e570a347c1c679db93931a7f143c74762174fa Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 27 May 2025 12:22:23 +1000 Subject: [PATCH 047/143] stm32/machine_i2c_target: Implement I2CTarget class. Works, tested on PYBV10, PYBD_SF2 and PYBD_SF6: buf = bytearray(16) machine.I2CTargetMemory("X", addr=67, mem=buf) Signed-off-by: Damien George --- ports/stm32/Makefile | 1 + ports/stm32/i2c.c | 95 +++++++++++++-- ports/stm32/i2c.h | 2 + ports/stm32/irq.h | 2 + ports/stm32/machine_i2c_target.c | 179 +++++++++++++++++++++++++++++ ports/stm32/main.c | 3 + ports/stm32/mboot/main.c | 1 + ports/stm32/mpconfigboard_common.h | 6 + ports/stm32/mpconfigport.h | 4 + 9 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 ports/stm32/machine_i2c_target.c diff --git a/ports/stm32/Makefile b/ports/stm32/Makefile index 37d70dcdbf028..37ce8fbd8383d 100644 --- a/ports/stm32/Makefile +++ b/ports/stm32/Makefile @@ -266,6 +266,7 @@ SRC_C += \ bufhelper.c \ dma.c \ i2c.c \ + i2cslave.c \ pyb_i2c.c \ spi.c \ pyb_spi.c \ diff --git a/ports/stm32/i2c.c b/ports/stm32/i2c.c index 883734bb1af60..4effb23438cf6 100644 --- a/ports/stm32/i2c.c +++ b/ports/stm32/i2c.c @@ -29,6 +29,7 @@ #include "py/mphal.h" #include "py/runtime.h" #include "i2c.h" +#include "i2cslave.h" #if MICROPY_HW_ENABLE_HW_I2C @@ -551,6 +552,10 @@ static const uint8_t i2c_available = #endif ; +#if MICROPY_HW_ENABLE_HW_I2C_TARGET +uint8_t i2c_target_enabled; +#endif + int i2c_find_peripheral(mp_obj_t id) { int i2c_id = 0; if (mp_obj_is_str(id)) { @@ -590,20 +595,38 @@ int i2c_find_peripheral(mp_obj_t id) { return i2c_id; } -#if MICROPY_PY_PYB_LEGACY +#if MICROPY_HW_ENABLE_HW_I2C_TARGET || MICROPY_PY_PYB_LEGACY #if defined(MICROPY_HW_I2C1_SCL) void I2C1_EV_IRQHandler(void) { MP_STATIC_ASSERT(I2C1_EV_IRQn > 0); IRQ_ENTER(I2C1_EV_IRQn); - i2c_ev_irq_handler(1); + #if MICROPY_HW_ENABLE_HW_I2C_TARGET + if (i2c_target_enabled & 1) { + i2c_slave_irq_handler(I2C1); + } else + #endif + { + #if MICROPY_PY_PYB_LEGACY + i2c_ev_irq_handler(1); + #endif + } IRQ_EXIT(I2C1_EV_IRQn); } void I2C1_ER_IRQHandler(void) { MP_STATIC_ASSERT(I2C1_ER_IRQn > 0); IRQ_ENTER(I2C1_ER_IRQn); - i2c_er_irq_handler(1); + #if MICROPY_HW_ENABLE_HW_I2C_TARGET + if (i2c_target_enabled & 1) { + i2c_slave_irq_handler(I2C1); + } else + #endif + { + #if MICROPY_PY_PYB_LEGACY + i2c_er_irq_handler(1); + #endif + } IRQ_EXIT(I2C1_ER_IRQn); } #endif // defined(MICROPY_HW_I2C1_SCL) @@ -612,14 +635,32 @@ void I2C1_ER_IRQHandler(void) { void I2C2_EV_IRQHandler(void) { MP_STATIC_ASSERT(I2C2_EV_IRQn > 0); IRQ_ENTER(I2C2_EV_IRQn); - i2c_ev_irq_handler(2); + #if MICROPY_HW_ENABLE_HW_I2C_TARGET + if (i2c_target_enabled & 2) { + i2c_slave_irq_handler(I2C2); + } else + #endif + { + #if MICROPY_PY_PYB_LEGACY + i2c_ev_irq_handler(2); + #endif + } IRQ_EXIT(I2C2_EV_IRQn); } void I2C2_ER_IRQHandler(void) { MP_STATIC_ASSERT(I2C2_ER_IRQn > 0); IRQ_ENTER(I2C2_ER_IRQn); - i2c_er_irq_handler(2); + #if MICROPY_HW_ENABLE_HW_I2C_TARGET + if (i2c_target_enabled & 2) { + i2c_slave_irq_handler(I2C2); + } else + #endif + { + #if MICROPY_PY_PYB_LEGACY + i2c_er_irq_handler(2); + #endif + } IRQ_EXIT(I2C2_ER_IRQn); } #endif // defined(MICROPY_HW_I2C2_SCL) @@ -628,14 +669,32 @@ void I2C2_ER_IRQHandler(void) { void I2C3_EV_IRQHandler(void) { MP_STATIC_ASSERT(I2C3_EV_IRQn > 0); IRQ_ENTER(I2C3_EV_IRQn); - i2c_ev_irq_handler(3); + #if MICROPY_HW_ENABLE_HW_I2C_TARGET + if (i2c_target_enabled & 4) { + i2c_slave_irq_handler(I2C3); + } else + #endif + { + #if MICROPY_PY_PYB_LEGACY + i2c_ev_irq_handler(3); + #endif + } IRQ_EXIT(I2C3_EV_IRQn); } void I2C3_ER_IRQHandler(void) { MP_STATIC_ASSERT(I2C3_ER_IRQn > 0); IRQ_ENTER(I2C3_ER_IRQn); - i2c_er_irq_handler(3); + #if MICROPY_HW_ENABLE_HW_I2C_TARGET + if (i2c_target_enabled & 4) { + i2c_slave_irq_handler(I2C3); + } else + #endif + { + #if MICROPY_PY_PYB_LEGACY + i2c_er_irq_handler(3); + #endif + } IRQ_EXIT(I2C3_ER_IRQn); } #endif // defined(MICROPY_HW_I2C3_SCL) @@ -644,14 +703,32 @@ void I2C3_ER_IRQHandler(void) { void I2C4_EV_IRQHandler(void) { MP_STATIC_ASSERT(I2C4_EV_IRQn > 0); IRQ_ENTER(I2C4_EV_IRQn); - i2c_ev_irq_handler(4); + #if MICROPY_HW_ENABLE_HW_I2C_TARGET + if (i2c_target_enabled & 8) { + i2c_slave_irq_handler(I2C4); + } else + #endif + { + #if MICROPY_PY_PYB_LEGACY + i2c_ev_irq_handler(4); + #endif + } IRQ_EXIT(I2C4_EV_IRQn); } void I2C4_ER_IRQHandler(void) { MP_STATIC_ASSERT(I2C4_ER_IRQn > 0); IRQ_ENTER(I2C4_ER_IRQn); - i2c_er_irq_handler(4); + #if MICROPY_HW_ENABLE_HW_I2C_TARGET + if (i2c_target_enabled & 8) { + i2c_slave_irq_handler(I2C4); + } else + #endif + { + #if MICROPY_PY_PYB_LEGACY + i2c_er_irq_handler(4); + #endif + } IRQ_EXIT(I2C4_ER_IRQn); } #endif // defined(MICROPY_HW_I2C4_SCL) diff --git a/ports/stm32/i2c.h b/ports/stm32/i2c.h index a48076842cbbf..26c55ec00e6eb 100644 --- a/ports/stm32/i2c.h +++ b/ports/stm32/i2c.h @@ -46,6 +46,8 @@ extern I2C_HandleTypeDef I2CHandle4; extern const mp_obj_type_t pyb_i2c_type; extern const pyb_i2c_obj_t pyb_i2c_obj[4]; +extern uint8_t i2c_target_enabled; + void i2c_init0(void); int pyb_i2c_init(I2C_HandleTypeDef *i2c); int pyb_i2c_init_freq(const pyb_i2c_obj_t *self, mp_int_t freq); diff --git a/ports/stm32/irq.h b/ports/stm32/irq.h index dfe901ff74b24..3348175420c41 100644 --- a/ports/stm32/irq.h +++ b/ports/stm32/irq.h @@ -177,6 +177,8 @@ static inline void restore_irq_pri(uint32_t state) { #define IRQ_PRI_HSEM NVIC_EncodePriority(NVIC_PRIORITYGROUP_4, 10, 0) +#define IRQ_PRI_I2C NVIC_EncodePriority(NVIC_PRIORITYGROUP_4, 12, 0) + // Interrupt priority for non-special timers. #define IRQ_PRI_TIMX NVIC_EncodePriority(NVIC_PRIORITYGROUP_4, 13, 0) diff --git a/ports/stm32/machine_i2c_target.c b/ports/stm32/machine_i2c_target.c new file mode 100644 index 0000000000000..83031677ebb27 --- /dev/null +++ b/ports/stm32/machine_i2c_target.c @@ -0,0 +1,179 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_i2c_target.c via MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE. + +#include "i2c.h" +#include "i2cslave.h" +#include "irq.h" + +typedef struct _machine_i2c_target_obj_t { + mp_obj_base_t base; + I2C_TypeDef *i2c; + uint16_t irqn_ev; + uint16_t irqn_er; + mp_hal_pin_obj_t scl; + mp_hal_pin_obj_t sda; +} machine_i2c_target_obj_t; + +static const machine_i2c_target_obj_t machine_i2c_target_obj[] = { + #if defined(MICROPY_HW_I2C1_SCL) + {{&machine_i2c_target_type}, I2C1, I2C1_EV_IRQn, I2C1_ER_IRQn, MICROPY_HW_I2C1_SCL, MICROPY_HW_I2C1_SDA}, + #else + {{NULL}, NULL, 0, 0, NULL, NULL}, + #endif + #if defined(MICROPY_HW_I2C2_SCL) + {{&machine_i2c_target_type}, I2C2, I2C2_EV_IRQn, I2C2_ER_IRQn, MICROPY_HW_I2C2_SCL, MICROPY_HW_I2C2_SDA}, + #else + {{NULL}, NULL, 0, 0, NULL, NULL}, + #endif + #if defined(MICROPY_HW_I2C3_SCL) + {{&machine_i2c_target_type}, I2C3, I2C3_EV_IRQn, I2C3_ER_IRQn, MICROPY_HW_I2C3_SCL, MICROPY_HW_I2C3_SDA}, + #else + {{NULL}, NULL, 0, 0, NULL, NULL}, + #endif + #if defined(MICROPY_HW_I2C4_SCL) + {{&machine_i2c_target_type}, I2C4, I2C4_EV_IRQn, I2C4_ER_IRQn, MICROPY_HW_I2C4_SCL, MICROPY_HW_I2C4_SDA}, + #else + {{NULL}, NULL, 0, 0, NULL, NULL}, + #endif +}; + +/******************************************************************************/ +// stm32 hardware bindings + +static machine_i2c_target_obj_t *get_self(i2c_slave_t *i2c) { + size_t i2c_id = ((uintptr_t)i2c - I2C1_BASE) / (I2C2_BASE - I2C1_BASE); + return (machine_i2c_target_obj_t *)&machine_i2c_target_obj[i2c_id]; +} + +static machine_i2c_target_data_t *get_data(i2c_slave_t *i2c) { + size_t i2c_id = ((uintptr_t)i2c - I2C1_BASE) / (I2C2_BASE - I2C1_BASE); + return &machine_i2c_target_data[i2c_id]; +} + +int i2c_slave_process_addr_match(i2c_slave_t *i2c, int rw) { + machine_i2c_target_data_addr_match(get_data(i2c), rw); + return 0; +} + +int i2c_slave_process_rx_byte(i2c_slave_t *i2c) { + machine_i2c_target_data_write_request(get_self(i2c), get_data(i2c)); + return 0; +} + +void i2c_slave_process_rx_end(i2c_slave_t *i2c) { + machine_i2c_target_data_stop(get_data(i2c)); +} + +void i2c_slave_process_tx_byte(i2c_slave_t *i2c) { + machine_i2c_target_data_read_request(get_self(i2c), get_data(i2c)); +} + +void i2c_slave_process_tx_end(i2c_slave_t *i2c) { + machine_i2c_target_data_stop(get_data(i2c)); +} + +/******************************************************************************/ +// I2CTarget port implementation + +static inline size_t mp_machine_i2c_target_get_index(machine_i2c_target_obj_t *self) { + return self - &machine_i2c_target_obj[0]; +} + +static inline void mp_machine_i2c_target_event_callback(machine_i2c_target_irq_obj_t *irq) { + mp_irq_handler(&irq->base); +} + +static size_t mp_machine_i2c_target_read_bytes(machine_i2c_target_obj_t *self, size_t len, uint8_t *buf) { + if (len > 0) { + buf[0] = i2c_slave_read_byte(self->i2c); + len = 1; + } + return len; +} + +static size_t mp_machine_i2c_target_write_bytes(machine_i2c_target_obj_t *self, size_t len, const uint8_t *buf) { + if (len > 0) { + i2c_slave_write_byte(self->i2c, buf[0]); + len = 1; + } + return len; +} + +static inline void mp_machine_i2c_target_irq_config(machine_i2c_target_obj_t *self, unsigned int trigger) { + (void)self; + (void)trigger; +} + +static mp_obj_t mp_machine_i2c_target_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + // Parse arguments. + enum { ARG_id, ARG_addr, ARG_addrsize, ARG_mem, ARG_mem_addrsize }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_id, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 7} }, + { MP_QSTR_mem, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_mem_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Work out I2C bus. + int i2c_id = i2c_find_peripheral(args[ARG_id].u_obj); + + // Get static target object. + machine_i2c_target_obj_t *self = (machine_i2c_target_obj_t *)&machine_i2c_target_obj[i2c_id - 1]; + + // Initialise data. + MP_STATE_PORT(machine_i2c_target_mem_obj)[i2c_id - 1] = args[ARG_mem].u_obj; + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id - 1]; + machine_i2c_target_data_init(data, args[ARG_mem].u_obj, args[ARG_mem_addrsize].u_int); + + // Initialise the I2C target. + mp_hal_pin_config_alt(self->scl, MP_HAL_PIN_MODE_ALT_OPEN_DRAIN, MP_HAL_PIN_PULL_NONE, AF_FN_I2C, i2c_id); + mp_hal_pin_config_alt(self->sda, MP_HAL_PIN_MODE_ALT_OPEN_DRAIN, MP_HAL_PIN_PULL_NONE, AF_FN_I2C, i2c_id); + i2c_slave_init(self->i2c, self->irqn_ev, IRQ_PRI_I2C, args[ARG_addr].u_int); + NVIC_SetPriority(self->irqn_er, IRQ_PRI_I2C); + NVIC_EnableIRQ(self->irqn_er); + + i2c_target_enabled |= 1 << (i2c_id - 1); + + return MP_OBJ_FROM_PTR(self); +} + +static void mp_machine_i2c_target_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "I2CTarget(%u, addr=%u)", + self - &machine_i2c_target_obj[0] + 1, + (self->i2c->OAR1 >> 1) & 0x7f); +} + +static void mp_machine_i2c_target_deinit(machine_i2c_target_obj_t *self) { + i2c_slave_shutdown(self->i2c, self->irqn_ev); + NVIC_DisableIRQ(self->irqn_er); +} diff --git a/ports/stm32/main.c b/ports/stm32/main.c index 137e132817483..af4d7f8bbb6d3 100644 --- a/ports/stm32/main.c +++ b/ports/stm32/main.c @@ -746,6 +746,9 @@ void stm32_main(uint32_t reset_mode) { #if MICROPY_PY_PYB_LEGACY && MICROPY_HW_ENABLE_HW_I2C pyb_i2c_deinit_all(); #endif + #if MICROPY_PY_MACHINE_I2C_TARGET + mp_machine_i2c_target_deinit_all(); + #endif #if MICROPY_HW_ENABLE_CAN pyb_can_deinit_all(); #endif diff --git a/ports/stm32/mboot/main.c b/ports/stm32/mboot/main.c index bd796c89638cc..e40413e4e7a27 100644 --- a/ports/stm32/mboot/main.c +++ b/ports/stm32/mboot/main.c @@ -61,6 +61,7 @@ // IRQ priorities (encoded values suitable for NVIC_SetPriority) // Most values are defined in irq.h. +#undef IRQ_PRI_I2C #define IRQ_PRI_I2C (NVIC_EncodePriority(NVIC_PRIORITYGROUP_4, 1, 0)) #if defined(MBOOT_CLK_PLLM) diff --git a/ports/stm32/mpconfigboard_common.h b/ports/stm32/mpconfigboard_common.h index 9fa9bf7714877..dcbbbccceb6b0 100644 --- a/ports/stm32/mpconfigboard_common.h +++ b/ports/stm32/mpconfigboard_common.h @@ -639,8 +639,14 @@ #if defined(MICROPY_HW_I2C1_SCL) || defined(MICROPY_HW_I2C2_SCL) \ || defined(MICROPY_HW_I2C3_SCL) || defined(MICROPY_HW_I2C4_SCL) #define MICROPY_HW_ENABLE_HW_I2C (1) +#if defined(STM32F4) || defined(STM32F7) || defined(STM32H7) || defined(STM32WB) +#define MICROPY_HW_ENABLE_HW_I2C_TARGET (1) +#else +#define MICROPY_HW_ENABLE_HW_I2C_TARGET (0) +#endif #else #define MICROPY_HW_ENABLE_HW_I2C (0) +#define MICROPY_HW_ENABLE_HW_I2C_TARGET (0) #endif // Enable CAN if there are any peripherals defined diff --git a/ports/stm32/mpconfigport.h b/ports/stm32/mpconfigport.h index 35deb93c6a0bf..191503cd4956f 100644 --- a/ports/stm32/mpconfigport.h +++ b/ports/stm32/mpconfigport.h @@ -129,6 +129,10 @@ #define MICROPY_PY_MACHINE_PULSE (1) #define MICROPY_PY_MACHINE_PIN_MAKE_NEW mp_pin_make_new #define MICROPY_PY_MACHINE_I2C (MICROPY_HW_ENABLE_HW_I2C) +#define MICROPY_PY_MACHINE_I2C_TARGET (MICROPY_HW_ENABLE_HW_I2C_TARGET) +#define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/stm32/machine_i2c_target.c" +#define MICROPY_PY_MACHINE_I2C_TARGET_MAX (4) +#define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_I2S_INCLUDEFILE "ports/stm32/machine_i2s.c" #define MICROPY_PY_MACHINE_I2S_CONSTANT_RX (I2S_MODE_MASTER_RX) From 56d2b47370d90616ae7ad6b6f078446efd69d0ff Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 30 Jul 2025 11:23:11 +1000 Subject: [PATCH 048/143] rp2/machine_i2c: Factor default pin macros to header file. So they can be reused by the I2CTarget implementation. Signed-off-by: Damien George --- ports/rp2/machine_i2c.c | 40 +----------------------- ports/rp2/machine_i2c.h | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 39 deletions(-) create mode 100644 ports/rp2/machine_i2c.h diff --git a/ports/rp2/machine_i2c.c b/ports/rp2/machine_i2c.c index 94212fb487043..99a94ec2f1a00 100644 --- a/ports/rp2/machine_i2c.c +++ b/ports/rp2/machine_i2c.c @@ -28,51 +28,13 @@ #include "py/mphal.h" #include "py/mperrno.h" #include "extmod/modmachine.h" +#include "machine_i2c.h" #include "hardware/i2c.h" #define DEFAULT_I2C_FREQ (400000) #define DEFAULT_I2C_TIMEOUT (50000) -#ifdef MICROPY_HW_I2C_NO_DEFAULT_PINS - -// With no default I2C, need to require the pin args. -#define MICROPY_HW_I2C0_SCL (0) -#define MICROPY_HW_I2C0_SDA (0) -#define MICROPY_HW_I2C1_SCL (0) -#define MICROPY_HW_I2C1_SDA (0) -#define MICROPY_I2C_PINS_ARG_OPTS MP_ARG_REQUIRED - -#else - -// Most boards do not require pin args. -#define MICROPY_I2C_PINS_ARG_OPTS 0 - -#ifndef MICROPY_HW_I2C0_SCL -#if PICO_DEFAULT_I2C == 0 -#define MICROPY_HW_I2C0_SCL (PICO_DEFAULT_I2C_SCL_PIN) -#define MICROPY_HW_I2C0_SDA (PICO_DEFAULT_I2C_SDA_PIN) -#else -#define MICROPY_HW_I2C0_SCL (9) -#define MICROPY_HW_I2C0_SDA (8) -#endif -#endif - -#ifndef MICROPY_HW_I2C1_SCL -#if PICO_DEFAULT_I2C == 1 -#define MICROPY_HW_I2C1_SCL (PICO_DEFAULT_I2C_SCL_PIN) -#define MICROPY_HW_I2C1_SDA (PICO_DEFAULT_I2C_SDA_PIN) -#else -#define MICROPY_HW_I2C1_SCL (7) -#define MICROPY_HW_I2C1_SDA (6) -#endif -#endif -#endif - -// SDA/SCL on even/odd pins, I2C0/I2C1 on even/odd pairs of pins. -#define IS_VALID_SCL(i2c, pin) (((pin) & 1) == 1 && (((pin) & 2) >> 1) == (i2c)) -#define IS_VALID_SDA(i2c, pin) (((pin) & 1) == 0 && (((pin) & 2) >> 1) == (i2c)) - typedef struct _machine_i2c_obj_t { mp_obj_base_t base; i2c_inst_t *const i2c_inst; diff --git a/ports/rp2/machine_i2c.h b/ports/rp2/machine_i2c.h new file mode 100644 index 0000000000000..da8cb5f8567d0 --- /dev/null +++ b/ports/rp2/machine_i2c.h @@ -0,0 +1,68 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2020-2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_RP2_MACHINE_I2C_H +#define MICROPY_INCLUDED_RP2_MACHINE_I2C_H + +#ifdef MICROPY_HW_I2C_NO_DEFAULT_PINS + +// With no default I2C, need to require the pin args. +#define MICROPY_HW_I2C0_SCL (0) +#define MICROPY_HW_I2C0_SDA (0) +#define MICROPY_HW_I2C1_SCL (0) +#define MICROPY_HW_I2C1_SDA (0) +#define MICROPY_I2C_PINS_ARG_OPTS MP_ARG_REQUIRED + +#else + +// Most boards do not require pin args. +#define MICROPY_I2C_PINS_ARG_OPTS 0 + +#ifndef MICROPY_HW_I2C0_SCL +#if PICO_DEFAULT_I2C == 0 +#define MICROPY_HW_I2C0_SCL (PICO_DEFAULT_I2C_SCL_PIN) +#define MICROPY_HW_I2C0_SDA (PICO_DEFAULT_I2C_SDA_PIN) +#else +#define MICROPY_HW_I2C0_SCL (9) +#define MICROPY_HW_I2C0_SDA (8) +#endif +#endif + +#ifndef MICROPY_HW_I2C1_SCL +#if PICO_DEFAULT_I2C == 1 +#define MICROPY_HW_I2C1_SCL (PICO_DEFAULT_I2C_SCL_PIN) +#define MICROPY_HW_I2C1_SDA (PICO_DEFAULT_I2C_SDA_PIN) +#else +#define MICROPY_HW_I2C1_SCL (7) +#define MICROPY_HW_I2C1_SDA (6) +#endif +#endif +#endif + +// SDA/SCL on even/odd pins, I2C0/I2C1 on even/odd pairs of pins. +#define IS_VALID_SCL(i2c, pin) (((pin) & 1) == 1 && (((pin) & 2) >> 1) == (i2c)) +#define IS_VALID_SDA(i2c, pin) (((pin) & 1) == 0 && (((pin) & 2) >> 1) == (i2c)) + +#endif // MICROPY_INCLUDED_RP2_MACHINE_I2C_H From 1839340dda5040fab0d05a5c58660ad0f4b5513e Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 27 May 2025 14:00:05 +1000 Subject: [PATCH 049/143] rp2/machine_i2c_target: Implement I2CTarget class. Signed-off-by: Damien George --- ports/rp2/machine_i2c_target.c | 304 +++++++++++++++++++++++++++++++++ ports/rp2/main.c | 4 + ports/rp2/mpconfigport.h | 4 + 3 files changed, 312 insertions(+) create mode 100644 ports/rp2/machine_i2c_target.c diff --git a/ports/rp2/machine_i2c_target.c b/ports/rp2/machine_i2c_target.c new file mode 100644 index 0000000000000..dc3010727b522 --- /dev/null +++ b/ports/rp2/machine_i2c_target.c @@ -0,0 +1,304 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_i2c_target.c via MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE. + +#include "machine_i2c.h" +#include "hardware/i2c.h" + +typedef struct _machine_i2c_target_obj_t { + mp_obj_base_t base; + i2c_inst_t *const i2c_inst; + mp_hal_pin_obj_t scl; + mp_hal_pin_obj_t sda; + uint8_t state; + bool stop_pending; + bool irq_active; +} machine_i2c_target_obj_t; + +static machine_i2c_target_obj_t machine_i2c_target_obj[] = { + {{&machine_i2c_target_type}, i2c0, MICROPY_HW_I2C0_SCL, MICROPY_HW_I2C0_SDA}, + {{&machine_i2c_target_type}, i2c1, MICROPY_HW_I2C1_SCL, MICROPY_HW_I2C1_SDA}, +}; + +/******************************************************************************/ +// RP2xxx hardware bindings + +static void check_stop_pending(machine_i2c_target_obj_t *self) { + if (self->irq_active) { + return; + } + if (self->stop_pending && !(self->i2c_inst->hw->status & I2C_IC_STATUS_RFNE_BITS)) { + unsigned int i2c_id = self - &machine_i2c_target_obj[0]; + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id]; + self->stop_pending = false; + self->state = STATE_IDLE; + machine_i2c_target_data_restart_or_stop(data); + } +} + +static void i2c_target_handler(i2c_inst_t *i2c) { + unsigned int i2c_id = i2c == i2c0 ? 0 : 1; + machine_i2c_target_obj_t *self = &machine_i2c_target_obj[i2c_id]; + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id]; + + self->irq_active = true; + + // Get the interrupt status. + uint32_t intr_stat = i2c->hw->intr_stat; + + if (intr_stat & I2C_IC_INTR_STAT_R_TX_ABRT_BITS) { + // Clear the TX_ABRT condition. + (void)i2c->hw->clr_tx_abrt; + } + + if (intr_stat & I2C_IC_INTR_STAT_R_START_DET_BITS) { + // Controller sent a start condition. + // Reset all state machines in case something went wrong. + (void)i2c->hw->clr_start_det; + if (self->state != STATE_IDLE) { + machine_i2c_target_data_reset_helper(data); + self->state = STATE_IDLE; + } + } + + if (intr_stat & I2C_IC_INTR_STAT_R_RX_FULL_BITS) { + // Data from controller is available for reading. + // Mask interrupt until I2C_DATA_CMD is read from. + i2c->hw->intr_mask &= ~I2C_IC_INTR_MASK_M_RX_FULL_BITS; + if (self->state != STATE_WRITING) { + machine_i2c_target_data_addr_match(data, false); + } + machine_i2c_target_data_write_request(self, data); + self->state = STATE_WRITING; + } + + if (intr_stat & (I2C_IC_INTR_STAT_R_RD_REQ_BITS | I2C_IC_INTR_STAT_R_RX_DONE_BITS)) { + // Controller is requesting data. + (void)i2c->hw->clr_rx_done; + (void)i2c->hw->clr_rd_req; + i2c->hw->intr_mask &= ~I2C_IC_INTR_MASK_M_RD_REQ_BITS; + if (self->state != STATE_READING) { + machine_i2c_target_data_addr_match(data, true); + } + machine_i2c_target_data_read_request(self, data); + self->state = STATE_READING; + } + + if (intr_stat & I2C_IC_INTR_STAT_R_STOP_DET_BITS) { + // Controller has generated a stop condition. + (void)i2c->hw->clr_stop_det; + if (self->state == STATE_IDLE) { + machine_i2c_target_data_addr_match(data, false); + } + if (i2c->hw->status & I2C_IC_STATUS_RFNE_BITS) { + self->stop_pending = true; + } else { + machine_i2c_target_data_restart_or_stop(data); + self->state = STATE_IDLE; + } + } + + self->irq_active = false; + check_stop_pending(self); +} + +static void i2c_target_irq_handler(void) { + uint i2c_index = __get_current_exception() - VTABLE_FIRST_IRQ - I2C0_IRQ; + i2c_inst_t *i2c = i2c_get_instance(i2c_index); + i2c_target_handler(i2c); +} + +static void i2c_target_init(i2c_inst_t *i2c, uint16_t addr, bool addr_10bit) { + i2c->hw->enable = 0; + + // Configure general settings, target address and FIFO levels. + i2c->hw->con = + I2C_IC_CON_RX_FIFO_FULL_HLD_CTRL_BITS + | I2C_IC_CON_STOP_DET_IFADDRESSED_BITS; + if (addr_10bit) { + i2c->hw->con |= I2C_IC_CON_IC_10BITADDR_SLAVE_BITS; + } + i2c->hw->sar = addr; + i2c->hw->tx_tl = 1; + i2c->hw->rx_tl = 0; // interrupt when at least 1 byte is available + (void)i2c->hw->clr_intr; + + // Enable interrupts. + i2c->hw->intr_mask = + I2C_IC_INTR_MASK_M_START_DET_BITS + | I2C_IC_INTR_MASK_M_STOP_DET_BITS + | I2C_IC_INTR_MASK_M_RX_DONE_BITS + | I2C_IC_INTR_MASK_M_TX_ABRT_BITS + | I2C_IC_INTR_MASK_M_RD_REQ_BITS + | I2C_IC_INTR_MASK_M_RX_FULL_BITS + ; + + i2c->hw->enable = 1; + + // Enable interrupt for current core. + uint i2c_index = i2c_hw_index(i2c); + uint num = I2C0_IRQ + i2c_index; + irq_set_exclusive_handler(num, i2c_target_irq_handler); + irq_set_enabled(num, true); +} + +static void i2c_target_deinit(i2c_inst_t *i2c) { + uint i2c_index = i2c_hw_index(i2c); + uint num = I2C0_IRQ + i2c_index; + irq_set_enabled(num, false); + irq_remove_handler(num, i2c_target_irq_handler); + + i2c->hw->intr_mask = 0; + i2c->hw->enable = 0; + i2c->hw->con = I2C_IC_CON_IC_SLAVE_DISABLE_BITS; +} + +/******************************************************************************/ +// I2CTarget port implementation + +static inline size_t mp_machine_i2c_target_get_index(machine_i2c_target_obj_t *self) { + return self - &machine_i2c_target_obj[0]; +} + +static inline void mp_machine_i2c_target_event_callback(machine_i2c_target_irq_obj_t *irq) { + mp_irq_handler(&irq->base); +} + +static size_t mp_machine_i2c_target_read_bytes(machine_i2c_target_obj_t *self, size_t len, uint8_t *buf) { + i2c_hw_t *i2c_hw = self->i2c_inst->hw; + + // Read from the RX FIFO. + size_t i = 0; + while (i < len && (i2c_hw->status & I2C_IC_STATUS_RFNE_BITS)) { + buf[i++] = i2c_hw->data_cmd; + } + + // Re-enable RX_FULL interrupt. + i2c_hw->intr_mask |= I2C_IC_INTR_MASK_M_RX_FULL_BITS; + + check_stop_pending(self); + + return i; +} + +static size_t mp_machine_i2c_target_write_bytes(machine_i2c_target_obj_t *self, size_t len, const uint8_t *buf) { + i2c_hw_t *i2c_hw = self->i2c_inst->hw; + + // Write to the TX FIFO. + size_t i = 0; + while (i < len && (i2c_hw->status & I2C_IC_STATUS_TFNF_BITS)) { + i2c_hw->data_cmd = buf[i++]; + } + + // Re-enable RD_REQ interrupt. + i2c_hw->intr_mask |= I2C_IC_INTR_MASK_M_RD_REQ_BITS; + + return i; +} + +static inline void mp_machine_i2c_target_irq_config(machine_i2c_target_obj_t *self, unsigned int trigger) { + (void)self; + (void)trigger; +} + +static mp_obj_t mp_machine_i2c_target_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_id, ARG_addr, ARG_addrsize, ARG_mem, ARG_mem_addrsize, ARG_scl, ARG_sda }; + static const mp_arg_t allowed_args[] = { + #ifdef PICO_DEFAULT_I2C + { MP_QSTR_id, MP_ARG_INT, {.u_int = PICO_DEFAULT_I2C} }, + #else + { MP_QSTR_id, MP_ARG_INT | MP_ARG_REQUIRED }, + #endif + { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 7} }, + { MP_QSTR_mem, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_mem_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, + { MP_QSTR_scl, MICROPY_I2C_PINS_ARG_OPTS | MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_sda, MICROPY_I2C_PINS_ARG_OPTS | MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + int i2c_id = args[ARG_id].u_int; + + // Check if the I2C bus is valid + if (i2c_id < 0 || i2c_id >= MP_ARRAY_SIZE(machine_i2c_target_obj)) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("I2CTarget(%d) doesn't exist"), i2c_id); + } + + // Get static peripheral object. + machine_i2c_target_obj_t *self = &machine_i2c_target_obj[i2c_id]; + + // Set SCL/SDA pins if configured. + if (args[ARG_scl].u_obj != mp_const_none) { + int scl = mp_hal_get_pin_obj(args[ARG_scl].u_obj); + if (!IS_VALID_SCL(i2c_id, scl)) { + mp_raise_ValueError(MP_ERROR_TEXT("bad SCL pin")); + } + self->scl = scl; + } + if (args[ARG_sda].u_obj != mp_const_none) { + int sda = mp_hal_get_pin_obj(args[ARG_sda].u_obj); + if (!IS_VALID_SDA(i2c_id, sda)) { + mp_raise_ValueError(MP_ERROR_TEXT("bad SDA pin")); + } + self->sda = sda; + } + + // Initialise I2C target state and data. + self->state = STATE_IDLE; + self->stop_pending = false; + self->irq_active = false; + MP_STATE_PORT(machine_i2c_target_mem_obj)[i2c_id] = args[ARG_mem].u_obj; + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id]; + machine_i2c_target_data_init(data, args[ARG_mem].u_obj, args[ARG_mem_addrsize].u_int); + + // Initialise I2C target hardware. + i2c_target_init(self->i2c_inst, args[ARG_addr].u_int, args[ARG_addrsize].u_int == 10); + gpio_set_function(self->scl, GPIO_FUNC_I2C); + gpio_set_function(self->sda, GPIO_FUNC_I2C); + gpio_set_pulls(self->scl, true, 0); + gpio_set_pulls(self->sda, true, 0); + + return MP_OBJ_FROM_PTR(self); +} + +static void mp_machine_i2c_target_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + i2c_hw_t *i2c_hw = i2c_get_hw(self->i2c_inst); + mp_printf(print, "I2CTarget(%u, addr=%u, scl=%u, sda=%u)", + self - &machine_i2c_target_obj[0], i2c_hw->sar, self->scl, self->sda); +} + +static void mp_machine_i2c_target_deinit(machine_i2c_target_obj_t *self) { + gpio_set_function(self->scl, GPIO_FUNC_SIO); + gpio_set_function(self->sda, GPIO_FUNC_SIO); + i2c_target_deinit(self->i2c_inst); +} diff --git a/ports/rp2/main.c b/ports/rp2/main.c index 0f10f63c6d296..1ffcabdfa0d14 100644 --- a/ports/rp2/main.c +++ b/ports/rp2/main.c @@ -34,6 +34,7 @@ #include "py/mperrno.h" #include "py/mphal.h" #include "extmod/modbluetooth.h" +#include "extmod/modmachine.h" #include "extmod/modnetwork.h" #include "shared/readline/readline.h" #include "shared/runtime/gchelper.h" @@ -257,6 +258,9 @@ int main(int argc, char **argv) { machine_pwm_deinit_all(); machine_pin_deinit(); machine_uart_deinit_all(); + #if MICROPY_PY_MACHINE_I2C_TARGET + mp_machine_i2c_target_deinit_all(); + #endif #if MICROPY_PY_THREAD mp_thread_deinit(); #endif diff --git a/ports/rp2/mpconfigport.h b/ports/rp2/mpconfigport.h index 35afea4fac5f4..d35563bd07028 100644 --- a/ports/rp2/mpconfigport.h +++ b/ports/rp2/mpconfigport.h @@ -171,6 +171,10 @@ #define MICROPY_PY_MACHINE_PWM (1) #define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/rp2/machine_pwm.c" #define MICROPY_PY_MACHINE_I2C (1) +#define MICROPY_PY_MACHINE_I2C_TARGET (1) +#define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/rp2/machine_i2c_target.c" +#define MICROPY_PY_MACHINE_I2C_TARGET_MAX (2) +#define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_I2S (1) #define MICROPY_PY_MACHINE_I2S_INCLUDEFILE "ports/rp2/machine_i2s.c" From 0c50343145c04b0b67c4b1eb3a337c4b0ea70b1f Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 23 Jun 2025 12:41:01 +1000 Subject: [PATCH 050/143] zephyr/machine_i2c_target: Implement I2CTarget class. Tested and working on rpi_pico and nucleo_wb55rg. Signed-off-by: Damien George --- ports/zephyr/boards/nucleo_wb55rg.conf | 1 + ports/zephyr/boards/rpi_pico.conf | 1 + ports/zephyr/machine_i2c_target.c | 191 +++++++++++++++++++++++++ ports/zephyr/main.c | 4 + ports/zephyr/mpconfigport.h | 6 + 5 files changed, 203 insertions(+) create mode 100644 ports/zephyr/machine_i2c_target.c diff --git a/ports/zephyr/boards/nucleo_wb55rg.conf b/ports/zephyr/boards/nucleo_wb55rg.conf index adfab367c892a..1c9c2f794cb05 100644 --- a/ports/zephyr/boards/nucleo_wb55rg.conf +++ b/ports/zephyr/boards/nucleo_wb55rg.conf @@ -4,6 +4,7 @@ CONFIG_NETWORKING=n CONFIG_FLASH=y CONFIG_FLASH_MAP=y CONFIG_I2C=y +CONFIG_I2C_TARGET=y CONFIG_SPI=y # Bluetooth diff --git a/ports/zephyr/boards/rpi_pico.conf b/ports/zephyr/boards/rpi_pico.conf index 6b31bc9f98bcb..683279ddc2c1c 100644 --- a/ports/zephyr/boards/rpi_pico.conf +++ b/ports/zephyr/boards/rpi_pico.conf @@ -13,6 +13,7 @@ CONFIG_NETWORKING=n CONFIG_FLASH=y CONFIG_FLASH_MAP=y CONFIG_I2C=y +CONFIG_I2C_TARGET=y CONFIG_SPI=y # MicroPython config. diff --git a/ports/zephyr/machine_i2c_target.c b/ports/zephyr/machine_i2c_target.c new file mode 100644 index 0000000000000..236f1334883a1 --- /dev/null +++ b/ports/zephyr/machine_i2c_target.c @@ -0,0 +1,191 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_i2c_target.c via MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE. + +#include + +#include "zephyr_device.h" + +typedef struct _machine_i2c_target_obj_t { + mp_obj_base_t base; + const struct device *dev; + struct i2c_target_config cfg; + uint8_t state; + uint8_t data_byte; +} machine_i2c_target_obj_t; + +static machine_i2c_target_obj_t machine_i2c_target_obj[] = { + {.base = {&machine_i2c_target_type}, .dev = NULL}, +}; + +/******************************************************************************/ +// zephyr bindings +// +// Note that it's possible to get callbacks in either of these sequences: +// - read_requested read_processed read_processed ... (eg STM32) +// - read_requested read_processed read_requested read_processed ... (eg RP2xxx / Design Ware) + +static int i2c_target_write_requested(struct i2c_target_config *config) { + machine_i2c_target_obj_t *self = CONTAINER_OF(config, machine_i2c_target_obj_t, cfg); + self->state = STATE_WRITING; + machine_i2c_target_data_addr_match(&machine_i2c_target_data[0], false); + return 0; +} + +static int i2c_target_write_received(struct i2c_target_config *config, uint8_t val) { + machine_i2c_target_obj_t *self = CONTAINER_OF(config, machine_i2c_target_obj_t, cfg); + machine_i2c_target_data_t *data = &machine_i2c_target_data[0]; + self->data_byte = val; + machine_i2c_target_data_write_request(self, data); + return 0; +} + +static int i2c_target_read_requested(struct i2c_target_config *config, uint8_t *val) { + machine_i2c_target_obj_t *self = CONTAINER_OF(config, machine_i2c_target_obj_t, cfg); + machine_i2c_target_data_t *data = &machine_i2c_target_data[0]; + if (self->state != STATE_READING) { + machine_i2c_target_data_addr_match(data, true); + machine_i2c_target_data_read_request(self, data); + self->state = STATE_READING; + } + *val = self->data_byte; + return 0; +} + +static int i2c_target_read_processed(struct i2c_target_config *config, uint8_t *val) { + machine_i2c_target_obj_t *self = CONTAINER_OF(config, machine_i2c_target_obj_t, cfg); + machine_i2c_target_data_t *data = &machine_i2c_target_data[0]; + machine_i2c_target_data_read_request(self, data); + *val = self->data_byte; + return 0; +} + +// called only on stop, not restart +static int i2c_target_stop(struct i2c_target_config *config) { + machine_i2c_target_obj_t *self = CONTAINER_OF(config, machine_i2c_target_obj_t, cfg); + if (self->state == STATE_IDLE) { + // Assume a stop without a start is a 0-byte write. + machine_i2c_target_data_addr_match(&machine_i2c_target_data[0], false); + } + self->state = STATE_IDLE; + machine_i2c_target_data_t *data = &machine_i2c_target_data[0]; + machine_i2c_target_data_stop(data); + return 0; +} + +static struct i2c_target_callbacks i2c_target_callbacks = { + .write_requested = i2c_target_write_requested, + .read_requested = i2c_target_read_requested, + .write_received = i2c_target_write_received, + .read_processed = i2c_target_read_processed, + .stop = i2c_target_stop, +}; + +/******************************************************************************/ +// I2CTarget port implementation + +static inline size_t mp_machine_i2c_target_get_index(machine_i2c_target_obj_t *self) { + return 0; +} + +static void mp_machine_i2c_target_event_callback(machine_i2c_target_irq_obj_t *irq) { + char dummy; + void *orig_top = MP_STATE_THREAD(stack_top); + mp_uint_t orig_limit = MP_STATE_THREAD(stack_limit); + MP_STATE_THREAD(stack_top) = &dummy; + MP_STATE_THREAD(stack_limit) = CONFIG_ISR_STACK_SIZE - 512; + mp_irq_handler(&irq->base); + MP_STATE_THREAD(stack_top) = orig_top; + MP_STATE_THREAD(stack_limit) = orig_limit; +} + +static size_t mp_machine_i2c_target_read_bytes(machine_i2c_target_obj_t *self, size_t len, uint8_t *buf) { + buf[0] = self->data_byte; + return 1; +} + +static size_t mp_machine_i2c_target_write_bytes(machine_i2c_target_obj_t *self, size_t len, const uint8_t *buf) { + self->data_byte = buf[0]; + return 1; +} + +static inline void mp_machine_i2c_target_irq_config(machine_i2c_target_obj_t *self, unsigned int trigger) { + (void)self; + (void)trigger; +} + +static mp_obj_t mp_machine_i2c_target_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_id, ARG_addr, ARG_addrsize, ARG_mem, ARG_mem_addrsize, ARG_scl, ARG_sda }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_id, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 7} }, + { MP_QSTR_mem, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_mem_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, + }; + + // Parse arguments. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + const struct device *dev = zephyr_device_find(args[ARG_id].u_obj); + + machine_i2c_target_obj_t *self = &machine_i2c_target_obj[0]; + if (!(self->dev == NULL || self->dev == dev)) { + mp_raise_ValueError(MP_ERROR_TEXT("only one I2CTarget supported")); + } + self->dev = dev; + self->cfg.flags = 0; + self->cfg.address = args[ARG_addr].u_int; + self->cfg.callbacks = &i2c_target_callbacks; + + // Initialise data. + self->state = STATE_IDLE; + MP_STATE_PORT(machine_i2c_target_mem_obj)[0] = args[ARG_mem].u_obj; + machine_i2c_target_data_t *data = &machine_i2c_target_data[0]; + machine_i2c_target_data_init(data, args[ARG_mem].u_obj, args[ARG_mem_addrsize].u_int); + + // Initialise the I2C target. + int ret = i2c_target_register(self->dev, &self->cfg); + if (ret < 0) { + mp_raise_OSError(-ret); + } + + return MP_OBJ_FROM_PTR(self); +} + +static void mp_machine_i2c_target_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "I2CTarget(%s, addr=%u)", + self->dev == NULL ? "" : self->dev->name, self->cfg.address); +} + +static void mp_machine_i2c_target_deinit(machine_i2c_target_obj_t *self) { + i2c_target_unregister(self->dev, &self->cfg); + self->dev = NULL; +} diff --git a/ports/zephyr/main.c b/ports/zephyr/main.c index eaef34a7868d3..f7ac997d92afd 100644 --- a/ports/zephyr/main.c +++ b/ports/zephyr/main.c @@ -53,6 +53,7 @@ #include "shared/runtime/pyexec.h" #include "shared/readline/readline.h" #include "extmod/modbluetooth.h" +#include "extmod/modmachine.h" #if MICROPY_VFS #include "extmod/vfs.h" @@ -197,6 +198,9 @@ int real_main(void) { #if MICROPY_PY_MACHINE machine_pin_deinit(); #endif + #if MICROPY_PY_MACHINE_I2C_TARGET + mp_machine_i2c_target_deinit_all(); + #endif #if MICROPY_PY_THREAD mp_thread_deinit(); diff --git a/ports/zephyr/mpconfigport.h b/ports/zephyr/mpconfigport.h index 848e04b389e58..fbf8dbcc7a709 100644 --- a/ports/zephyr/mpconfigport.h +++ b/ports/zephyr/mpconfigport.h @@ -62,6 +62,12 @@ #define MICROPY_PY_MACHINE (1) #define MICROPY_PY_MACHINE_INCLUDEFILE "ports/zephyr/modmachine.c" #define MICROPY_PY_MACHINE_I2C (1) +#ifdef CONFIG_I2C_TARGET +#define MICROPY_PY_MACHINE_I2C_TARGET (1) +#define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/zephyr/machine_i2c_target.c" +#define MICROPY_PY_MACHINE_I2C_TARGET_MAX (1) +#define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) +#endif #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_SPI (1) #define MICROPY_PY_MACHINE_SPI_MSB (SPI_TRANSFER_MSB) From 6e72cae619f4172205249b831382bd43205a4b10 Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 16 Jul 2025 16:30:56 +1000 Subject: [PATCH 051/143] alif/machine_i2c_target: Implement I2CTarget class. Signed-off-by: Damien George --- ports/alif/irq.h | 1 + ports/alif/machine_i2c_target.c | 325 ++++++++++++++++++++++++++++++++ ports/alif/main.c | 4 + ports/alif/mpconfigport.h | 4 + 4 files changed, 334 insertions(+) create mode 100644 ports/alif/machine_i2c_target.c diff --git a/ports/alif/irq.h b/ports/alif/irq.h index 02df524a49c86..86b739795c173 100644 --- a/ports/alif/irq.h +++ b/ports/alif/irq.h @@ -49,6 +49,7 @@ #define IRQ_PRI_HWSEM NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 8, 0) #define IRQ_PRI_GPU NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 10, 0) #define IRQ_PRI_GPIO NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 50, 0) +#define IRQ_PRI_I2C NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 60, 0) #define IRQ_PRI_RTC NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 100, 0) #define IRQ_PRI_CYW43 NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 126, 0) #define IRQ_PRI_PENDSV NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 127, 0) diff --git a/ports/alif/machine_i2c_target.c b/ports/alif/machine_i2c_target.c new file mode 100644 index 0000000000000..cdc106049a08d --- /dev/null +++ b/ports/alif/machine_i2c_target.c @@ -0,0 +1,325 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_i2c_target.c via MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE. + +#include "i2c.h" + +#define I2C_IC_CON_RX_FIFO_FULL_HLD_CTRL (1 << 9) +#define I2C_IC_CON_TX_EMPTY_CTRL (1 << 8) +#define I2C_IC_CON_STOP_DET_IFADDRESSED (1 << 7) + +typedef struct _machine_i2c_target_obj_t { + mp_obj_base_t base; + I2C_Type *i2c; + mp_hal_pin_obj_t scl; + mp_hal_pin_obj_t sda; + uint8_t state; + bool stop_pending; + bool irq_active; +} machine_i2c_target_obj_t; + +static machine_i2c_target_obj_t machine_i2c_target_obj[] = { + #if defined(MICROPY_HW_I2C0_SCL) + [0] = {{&machine_i2c_target_type}, (I2C_Type *)I2C0_BASE, MICROPY_HW_I2C0_SCL, MICROPY_HW_I2C0_SDA}, + #endif + #if defined(MICROPY_HW_I2C1_SCL) + [1] = {{&machine_i2c_target_type}, (I2C_Type *)I2C1_BASE, MICROPY_HW_I2C1_SCL, MICROPY_HW_I2C1_SDA}, + #endif + #if defined(MICROPY_HW_I2C2_SCL) + [2] = {{&machine_i2c_target_type}, (I2C_Type *)I2C2_BASE, MICROPY_HW_I2C2_SCL, MICROPY_HW_I2C2_SDA}, + #endif + #if defined(MICROPY_HW_I2C3_SCL) + [3] = {{&machine_i2c_target_type}, (I2C_Type *)I2C3_BASE, MICROPY_HW_I2C3_SCL, MICROPY_HW_I2C3_SDA}, + #endif +}; + +/******************************************************************************/ +// Alif I2C hardware bindings +// +// The hardware triggers the following IRQs for the given scenarios: +// - scan (0-byte write) of another target: START_DET +// - scan (0-byte write) of us: START_DET STOP_DET +// - write of n bytes: START_DET RX_FULL*n STOP_DET +// - write of n bytes then read of m bytes: START_DET RX_FULL*n START_DET RD_REQ*m RX_DONE STOP_DET + +static inline unsigned int i2c_reg_base_to_index(I2C_Type *i2c) { + return ((uintptr_t)i2c - I2C0_BASE) / (I2C1_BASE - I2C0_BASE); +} + +static const uint32_t i2c_irq_num[] = { I2C0_IRQ_IRQn, I2C1_IRQ_IRQn, I2C2_IRQ_IRQn, I2C3_IRQ_IRQn }; + +static void check_stop_pending(machine_i2c_target_obj_t *self) { + if (self->irq_active) { + return; + } + if (self->stop_pending && !(self->i2c->I2C_STATUS & I2C_IC_STATUS_RECEIVE_FIFO_NOT_EMPTY)) { + unsigned int i2c_id = self - &machine_i2c_target_obj[0]; + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id]; + self->stop_pending = false; + self->state = STATE_IDLE; + machine_i2c_target_data_restart_or_stop(data); + } +} + +static void i2c_target_irq_handler(machine_i2c_target_obj_t *self) { + unsigned int i2c_id = self - &machine_i2c_target_obj[0]; + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id]; + I2C_Type *i2c = self->i2c; + + self->irq_active = true; + + // Get the interrupt status. + uint32_t intr_stat = i2c->I2C_RAW_INTR_STAT; + + if (intr_stat & I2C_IC_INTR_STAT_TX_ABRT) { + // Clear the TX_ABRT condition. + (void)i2c->I2C_CLR_TX_ABRT; + } + + if (intr_stat & I2C_IC_INTR_STAT_START_DET) { + // Controller sent a start condition. + // Reset all state machines in case something went wrong. + (void)i2c->I2C_CLR_START_DET; + if (self->state != STATE_IDLE) { + machine_i2c_target_data_reset_helper(data); + self->state = STATE_IDLE; + } + } + + if (intr_stat & I2C_IC_INTR_STAT_RX_FULL) { + // Data from controller is available for reading. + // Mask interrupt until I2C_DATA_CMD is read from. + i2c->I2C_INTR_MASK &= ~I2C_IC_INTR_STAT_RX_FULL; + if (self->state != STATE_WRITING) { + machine_i2c_target_data_addr_match(data, false); + } + machine_i2c_target_data_write_request(self, data); + self->state = STATE_WRITING; + } + + if (intr_stat & (I2C_IC_INTR_STAT_RD_REQ | I2C_IC_INTR_STAT_RX_DONE)) { + // Controller is requesting data. + // Note: for RX_DONE interrupt, no data needs to be written but this event is + // needed to match the expected I2CTarget event behaviour. A TX_ABTR interrupt + // will be fired after the unused byte is written to I2C_DATA_CMD, and clearing + // that abort is required to reset the hardware I2C state machine. + (void)i2c->I2C_CLR_RX_DONE; + (void)i2c->I2C_CLR_RD_REQ; + i2c->I2C_INTR_MASK &= ~I2C_IC_INTR_STAT_RD_REQ; + if (self->state != STATE_READING) { + machine_i2c_target_data_addr_match(data, true); + } + machine_i2c_target_data_read_request(self, data); + self->state = STATE_READING; + } + + if (intr_stat & I2C_IC_INTR_STAT_STOP_DET) { + // Controller has generated a stop condition. + (void)i2c->I2C_CLR_STOP_DET; + if (self->state == STATE_IDLE) { + machine_i2c_target_data_addr_match(data, false); + } + if (i2c->I2C_STATUS & I2C_IC_STATUS_RECEIVE_FIFO_NOT_EMPTY) { + self->stop_pending = true; + } else { + machine_i2c_target_data_restart_or_stop(data); + self->state = STATE_IDLE; + } + } + + self->irq_active = false; + check_stop_pending(self); +} + +void I2C0_IRQHandler(void) { + i2c_target_irq_handler(&machine_i2c_target_obj[0]); +} + +void I2C1_IRQHandler(void) { + i2c_target_irq_handler(&machine_i2c_target_obj[1]); +} + +void I2C2_IRQHandler(void) { + i2c_target_irq_handler(&machine_i2c_target_obj[2]); +} + +void I2C3_IRQHandler(void) { + i2c_target_irq_handler(&machine_i2c_target_obj[3]); +} + +static void i2c_target_init(I2C_Type *i2c, uint16_t addr, bool addr_10bit) { + i2c_disable(i2c); + + uint32_t ic_con_reg = 0; + ic_con_reg |= I2C_IC_CON_RX_FIFO_FULL_HLD_CTRL; + ic_con_reg |= I2C_IC_CON_STOP_DET_IFADDRESSED; + if (addr_10bit) { + ic_con_reg |= I2C_SLAVE_10BIT_ADDR_MODE; + } + i2c->I2C_CON = ic_con_reg; + i2c->I2C_SAR = addr & I2C_IC_SAR_10BIT_ADDR_MASK; + i2c->I2C_TX_TL = 1; + i2c->I2C_RX_TL = 0; // interrupt when at least 1 byte is available + i2c_clear_all_interrupt(i2c); + + // Enable interrupts. + i2c->I2C_INTR_MASK = + I2C_IC_INTR_STAT_STOP_DET + | I2C_IC_INTR_STAT_RX_DONE + | I2C_IC_INTR_STAT_TX_ABRT + | I2C_IC_INTR_STAT_RD_REQ + | I2C_IC_INTR_STAT_RX_FULL + ; + + i2c_enable(i2c); + + // Enable I2C interrupts. + uint32_t irq_num = i2c_irq_num[i2c_reg_base_to_index(i2c)]; + NVIC_ClearPendingIRQ(irq_num); + NVIC_SetPriority(irq_num, IRQ_PRI_I2C); + NVIC_EnableIRQ(irq_num); +} + +static void i2c_target_deinit(I2C_Type *i2c) { + uint32_t irq_num = i2c_irq_num[i2c_reg_base_to_index(i2c)]; + NVIC_DisableIRQ(irq_num); + i2c_disable(i2c); +} + +/******************************************************************************/ +// I2CTarget port implementation + +static inline size_t mp_machine_i2c_target_get_index(machine_i2c_target_obj_t *self) { + return self - &machine_i2c_target_obj[0]; +} + +static inline void mp_machine_i2c_target_event_callback(machine_i2c_target_irq_obj_t *irq) { + mp_irq_handler(&irq->base); +} + +static size_t mp_machine_i2c_target_read_bytes(machine_i2c_target_obj_t *self, size_t len, uint8_t *buf) { + I2C_Type *i2c = self->i2c; + + // Read from the RX FIFO. + size_t i = 0; + while (i < len && (i2c->I2C_STATUS & I2C_IC_STATUS_RECEIVE_FIFO_NOT_EMPTY)) { + buf[i++] = i2c->I2C_DATA_CMD; + } + + // Re-enable RX_FULL interrupt. + i2c->I2C_INTR_MASK |= I2C_IC_INTR_STAT_RX_FULL; + + check_stop_pending(self); + + return i; +} + +static size_t mp_machine_i2c_target_write_bytes(machine_i2c_target_obj_t *self, size_t len, const uint8_t *buf) { + // Write to the TX FIFO. + size_t i = 0; + while (i < len && (self->i2c->I2C_STATUS & I2C_IC_STATUS_TRANSMIT_FIFO_NOT_FULL)) { + self->i2c->I2C_DATA_CMD = buf[i++]; + } + + // Re-enable RD_REQ interrupt. + self->i2c->I2C_INTR_MASK |= I2C_IC_INTR_STAT_RD_REQ; + + return 1; +} + +static inline void mp_machine_i2c_target_irq_config(machine_i2c_target_obj_t *self, unsigned int trigger) { + (void)self; + (void)trigger; +} + +static mp_obj_t mp_machine_i2c_target_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_id, ARG_addr, ARG_addrsize, ARG_mem, ARG_mem_addrsize, ARG_scl, ARG_sda }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_id, MP_ARG_INT | MP_ARG_REQUIRED }, + { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 7} }, + { MP_QSTR_mem, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_mem_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, + { MP_QSTR_scl, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_sda, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + }; + + // Parse arguments. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + int i2c_id = args[ARG_id].u_int; + + // Check if the I2C bus is valid + if (i2c_id < 0 || i2c_id >= MP_ARRAY_SIZE(machine_i2c_target_obj)) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("I2CTarget(%d) doesn't exist"), i2c_id); + } + + // Get static peripheral object. + machine_i2c_target_obj_t *self = &machine_i2c_target_obj[i2c_id]; + + // Disable I2C controller. + i2c_disable(self->i2c); + + // Initialise data. + self->state = STATE_IDLE; + self->stop_pending = false; + self->irq_active = false; + MP_STATE_PORT(machine_i2c_target_mem_obj)[i2c_id] = args[ARG_mem].u_obj; + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id]; + machine_i2c_target_data_init(data, args[ARG_mem].u_obj, args[ARG_mem_addrsize].u_int); + + // Set SCL/SDA pins if given. + if (args[ARG_scl].u_obj != mp_const_none) { + self->scl = mp_hal_get_pin_obj(args[ARG_scl].u_obj); + } + if (args[ARG_sda].u_obj != mp_const_none) { + self->sda = mp_hal_get_pin_obj(args[ARG_sda].u_obj); + } + + // Configure I2C pins. + mp_hal_pin_config(self->scl, MP_HAL_PIN_MODE_ALT, MP_HAL_PIN_PULL_UP, + MP_HAL_PIN_SPEED_LOW, MP_HAL_PIN_DRIVE_8MA, MP_HAL_PIN_ALT(I2C_SCL, i2c_id), true); + mp_hal_pin_config(self->sda, MP_HAL_PIN_MODE_ALT, MP_HAL_PIN_PULL_UP, + MP_HAL_PIN_SPEED_LOW, MP_HAL_PIN_DRIVE_8MA, MP_HAL_PIN_ALT(I2C_SDA, i2c_id), true); + + // Initialise the I2C target. + i2c_target_init(self->i2c, args[ARG_addr].u_int, args[ARG_addrsize].u_int == 10); + + return MP_OBJ_FROM_PTR(self); +} + +static void mp_machine_i2c_target_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "I2CTarget(%u, addr=%u, scl=" MP_HAL_PIN_FMT ", sda=" MP_HAL_PIN_FMT ")", + self - &machine_i2c_target_obj[0], self->i2c->I2C_SAR, mp_hal_pin_name(self->scl), mp_hal_pin_name(self->sda)); +} + +static void mp_machine_i2c_target_deinit(machine_i2c_target_obj_t *self) { + i2c_target_deinit(self->i2c); +} diff --git a/ports/alif/main.c b/ports/alif/main.c index ab5e85d5b9db6..308d8df900e66 100644 --- a/ports/alif/main.c +++ b/ports/alif/main.c @@ -31,6 +31,7 @@ #include "py/mphal.h" #include "py/stackctrl.h" #include "extmod/modbluetooth.h" +#include "extmod/modmachine.h" #include "extmod/modnetwork.h" #include "shared/readline/readline.h" #include "shared/runtime/gchelper.h" @@ -164,6 +165,9 @@ int main(void) { #if MICROPY_PY_BLUETOOTH mp_bluetooth_deinit(); #endif + #if MICROPY_PY_MACHINE_I2C_TARGET + mp_machine_i2c_target_deinit_all(); + #endif soft_timer_deinit(); machine_pin_irq_deinit(); gc_sweep_all(); diff --git a/ports/alif/mpconfigport.h b/ports/alif/mpconfigport.h index ddfc551bf8983..a8c8c842977ed 100644 --- a/ports/alif/mpconfigport.h +++ b/ports/alif/mpconfigport.h @@ -132,6 +132,10 @@ #define MICROPY_PY_MACHINE_PULSE (1) #define MICROPY_PY_MACHINE_I2C (MICROPY_HW_ENABLE_HW_I2C) #define MICROPY_PY_MACHINE_I2C_TRANSFER_WRITE1 (1) +#define MICROPY_PY_MACHINE_I2C_TARGET (MICROPY_HW_ENABLE_HW_I2C) +#define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/alif/machine_i2c_target.c" +#define MICROPY_PY_MACHINE_I2C_TARGET_MAX (4) +#define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_SPI (1) #define MICROPY_PY_MACHINE_SOFTSPI (1) From 67a442d8fabef4f203dd6b13ded46b3836232522 Mon Sep 17 00:00:00 2001 From: Damien George Date: Thu, 31 Jul 2025 23:52:39 +1000 Subject: [PATCH 052/143] alif/machine_i2c: Allow changing I2C SCL/SDA pins. Signed-off-by: Damien George --- ports/alif/machine_i2c.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ports/alif/machine_i2c.c b/ports/alif/machine_i2c.c index a710aeeb01132..356c893dc7021 100644 --- a/ports/alif/machine_i2c.c +++ b/ports/alif/machine_i2c.c @@ -125,9 +125,12 @@ mp_obj_t machine_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_t n self->freq = args[ARG_freq].u_int; self->timeout = args[ARG_timeout].u_int; - // here we would check the scl/sda pins and configure them, but it's not implemented - if (args[ARG_scl].u_obj != mp_const_none || args[ARG_sda].u_obj != mp_const_none) { - mp_raise_ValueError(MP_ERROR_TEXT("explicit choice of scl/sda is not implemented")); + // Set SCL/SDA pins if given. + if (args[ARG_scl].u_obj != mp_const_none) { + self->scl = mp_hal_get_pin_obj(args[ARG_scl].u_obj); + } + if (args[ARG_sda].u_obj != mp_const_none) { + self->sda = mp_hal_get_pin_obj(args[ARG_sda].u_obj); } // Disable I2C controller. From 5c78762c166a22d66269357bc0ffe2129586dca0 Mon Sep 17 00:00:00 2001 From: robert-hh Date: Thu, 29 May 2025 09:48:02 +0200 Subject: [PATCH 053/143] mimxrt/machine_i2c_target: Support I2C target mode. The functionality is similar to the RP2 implementation. The supported address size is 7 bit. In order to achieve a sufficient response, the target I2C IRQ handler has to run from RAM, causing much more code moved to RAM than required. Tested with Teensy 4.1, MIMXRT1021EVK, MIMXRT1011EVK and MIMXRT1170, using both a On-Board SoftI2C as controller and a RP2 Pico as external controller. Signed-off-by: Damien George Signed-off-by: robert-hh --- ports/mimxrt/boards/common.ld | 2 +- ports/mimxrt/machine_i2c.c | 13 +-- ports/mimxrt/machine_i2c.h | 38 +++++++ ports/mimxrt/machine_i2c_target.c | 168 ++++++++++++++++++++++++++++++ ports/mimxrt/main.c | 1 + ports/mimxrt/mpconfigport.h | 5 + 6 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 ports/mimxrt/machine_i2c.h create mode 100644 ports/mimxrt/machine_i2c_target.c diff --git a/ports/mimxrt/boards/common.ld b/ports/mimxrt/boards/common.ld index 477ba38bc89a3..dcbc0a42366a3 100644 --- a/ports/mimxrt/boards/common.ld +++ b/ports/mimxrt/boards/common.ld @@ -98,7 +98,7 @@ SECTIONS .text : { . = ALIGN(4); - *(EXCLUDE_FILE(*fsl_flexspi.o *gc.o *vm.o *parse*.o *runtime*.o *map.o *mpirq.o ) .text*) /* .text* sections (code) */ + *(EXCLUDE_FILE(*fsl_flexspi.o *gc.o *vm.o *runtime*.o *map.o *mpirq.o *machine_i2c_target.o *fsl_lpi2c.o) .text*) /* .text* sections (code) */ *(.rodata) /* .rodata sections (constants, strings, etc.) */ *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ *(.glue_7) /* glue arm to thumb code */ diff --git a/ports/mimxrt/machine_i2c.c b/ports/mimxrt/machine_i2c.c index d170804f4f058..aa128e6ff6fe2 100644 --- a/ports/mimxrt/machine_i2c.c +++ b/ports/mimxrt/machine_i2c.c @@ -33,11 +33,7 @@ #include "fsl_iomuxc.h" #include "fsl_lpi2c.h" - -#define DEFAULT_I2C_ID (0) -#define DEFAULT_I2C_FREQ (400000) -#define DEFAULT_I2C_DRIVE (6) -#define DEFAULT_I2C_TIMEOUT (50000) +#include "machine_i2c.h" typedef struct _machine_i2c_obj_t { mp_obj_base_t base; @@ -57,12 +53,11 @@ typedef struct _iomux_table_t { uint32_t configRegister; } iomux_table_t; -static const uint8_t i2c_index_table[] = MICROPY_HW_I2C_INDEX; -static LPI2C_Type *i2c_base_ptr_table[] = LPI2C_BASE_PTRS; +const uint8_t i2c_index_table[] = MICROPY_HW_I2C_INDEX; +LPI2C_Type *i2c_base_ptr_table[] = LPI2C_BASE_PTRS; +const uint8_t micropy_hw_i2c_num = MICROPY_HW_I2C_NUM; static const iomux_table_t iomux_table[] = { IOMUX_TABLE_I2C }; -#define MICROPY_HW_I2C_NUM ARRAY_SIZE(i2c_index_table) - #define SCL (iomux_table[index]) #define SDA (iomux_table[index + 1]) diff --git a/ports/mimxrt/machine_i2c.h b/ports/mimxrt/machine_i2c.h new file mode 100644 index 0000000000000..c6d561ca585f1 --- /dev/null +++ b/ports/mimxrt/machine_i2c.h @@ -0,0 +1,38 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * Copyright (c) 2025 Robert Hammelrath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#define DEFAULT_I2C_ID (0) +#define DEFAULT_I2C_FREQ (400000) +#define DEFAULT_I2C_DRIVE (6) +#define DEFAULT_I2C_TIMEOUT (50000) +#define DEFAULT_I2C_FILTER_NS (200) +#define MICROPY_HW_I2C_NUM ARRAY_SIZE(i2c_index_table) + +extern const uint8_t i2c_index_table[]; +extern LPI2C_Type *i2c_base_ptr_table[]; +extern bool lpi2c_set_iomux(int8_t hw_i2c, uint8_t drive); +extern const uint8_t micropy_hw_i2c_num; diff --git a/ports/mimxrt/machine_i2c_target.c b/ports/mimxrt/machine_i2c_target.c new file mode 100644 index 0000000000000..aa408071f31bd --- /dev/null +++ b/ports/mimxrt/machine_i2c_target.c @@ -0,0 +1,168 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * Copyright (c) 2025 Robert Hammelrath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_i2c_target.c via MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE. + +#include "fsl_lpi2c.h" +#include "machine_i2c.h" +#include CLOCK_CONFIG_H + +typedef struct _machine_i2c_target_obj_t { + mp_obj_base_t base; + LPI2C_Type *i2c_inst; + uint8_t i2c_id; + uint8_t addr; + lpi2c_slave_config_t slave_config; + lpi2c_slave_handle_t handle; +} machine_i2c_target_obj_t; + +static void lpi2c_slave_callback(LPI2C_Type *base, lpi2c_slave_transfer_t *xfer, void *param) { + machine_i2c_target_obj_t *self = (machine_i2c_target_obj_t *)param; + machine_i2c_target_data_t *data = &machine_i2c_target_data[self->i2c_id]; + + switch (xfer->event) { + case kLPI2C_SlaveAddressMatchEvent: + // Controller addressed us. + machine_i2c_target_data_addr_match(data, xfer->receivedAddress & 1); + break; + case kLPI2C_SlaveReceiveEvent: + // Data from controller is available for reading. + machine_i2c_target_data_write_request(self, data); + break; + case kLPI2C_SlaveTransmitEvent: + // Controller is requesting data. + machine_i2c_target_data_read_request(self, data); + break; + case kLPI2C_SlaveCompletionEvent: + // Transfer done. + machine_i2c_target_data_stop(data); + break; + default: + break; + } +} + +/******************************************************************************/ +// I2CTarget port implementation + +static inline size_t mp_machine_i2c_target_get_index(machine_i2c_target_obj_t *self) { + return self->i2c_id; +} + +static inline void mp_machine_i2c_target_event_callback(machine_i2c_target_irq_obj_t *irq) { + mp_irq_handler(&irq->base); +} + +static size_t mp_machine_i2c_target_read_bytes(machine_i2c_target_obj_t *self, size_t len, uint8_t *buf) { + // LPI2C_Type *i2c_inst = self->i2c_inst; + // mp_int_t i = 0; + // mp_int_t val = 0; + // while (i < len && !((val = i2c_inst->SRDR) & LPI2C_SRDR_RXEMPTY_MASK)) { + // buf[i++] = (uint8_t)(val & 0xff); + // } + // return i; + // Simple and fast version for len == 1 + buf[0] = (uint8_t)(self->i2c_inst->SRDR); + return 1; +} + +static size_t mp_machine_i2c_target_write_bytes(machine_i2c_target_obj_t *self, size_t len, const uint8_t *buf) { + self->i2c_inst->STDR = buf[0]; + return 1; +} + +static inline void mp_machine_i2c_target_irq_config(machine_i2c_target_obj_t *self, unsigned int trigger) { + (void)self; + (void)trigger; +} + +static mp_obj_t mp_machine_i2c_target_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_id, ARG_addr, ARG_addrsize, ARG_mem, ARG_mem_addrsize, ARG_drive }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_id, MP_ARG_INT, {.u_int = DEFAULT_I2C_ID} }, + { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 7} }, + { MP_QSTR_mem, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE}}, + { MP_QSTR_mem_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, + { MP_QSTR_drive, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = DEFAULT_I2C_DRIVE} }, + }; + + // Parse arguments. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Get I2C bus. + int i2c_id = args[ARG_id].u_int; + if (i2c_id < 0 || i2c_id >= micropy_hw_i2c_num || i2c_index_table[i2c_id] == 0) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("I2C(%d) doesn't exist"), i2c_id); + } + int i2c_hw_id = i2c_index_table[i2c_id]; // the hw i2c number 1..n + + // Get I2C Object. + machine_i2c_target_obj_t *self = mp_obj_malloc_with_finaliser(machine_i2c_target_obj_t, &machine_i2c_target_type); + self->i2c_id = i2c_id; + self->i2c_inst = i2c_base_ptr_table[i2c_hw_id]; + uint8_t drive = args[ARG_drive].u_int; + if (drive < 1 || drive > 7) { + drive = DEFAULT_I2C_DRIVE; + } + // Set the target address. + self->addr = args[ARG_addr].u_int; + // Initialise data. + MP_STATE_PORT(machine_i2c_target_mem_obj)[i2c_id] = args[ARG_mem].u_obj; + machine_i2c_target_data_t *data = &machine_i2c_target_data[self->i2c_id]; + machine_i2c_target_data_init(data, args[ARG_mem].u_obj, args[ARG_mem_addrsize].u_int); + + // Initialise the GPIO pins + lpi2c_set_iomux(i2c_hw_id, drive); + // Initialise the I2C peripheral + LPI2C_SlaveGetDefaultConfig(&self->slave_config); + self->slave_config.address0 = self->addr; + self->slave_config.sdaGlitchFilterWidth_ns = DEFAULT_I2C_FILTER_NS; + self->slave_config.sclGlitchFilterWidth_ns = DEFAULT_I2C_FILTER_NS; + LPI2C_SlaveInit(self->i2c_inst, &self->slave_config, BOARD_BOOTCLOCKRUN_LPI2C_CLK_ROOT); + // Create the LPI2C handle for the non-blocking transfer + LPI2C_SlaveTransferCreateHandle(self->i2c_inst, &self->handle, lpi2c_slave_callback, self); + // Start accepting I2C transfers on the LPI2C slave peripheral + status_t reVal = LPI2C_SlaveTransferNonBlocking(self->i2c_inst, &self->handle, + kLPI2C_SlaveAddressMatchEvent | kLPI2C_SlaveTransmitEvent | kLPI2C_SlaveReceiveEvent | kLPI2C_SlaveCompletionEvent); + if (reVal != kStatus_Success) { + mp_raise_ValueError(MP_ERROR_TEXT("cannot start I2C")); + } + return MP_OBJ_FROM_PTR(self); +} + +static void mp_machine_i2c_target_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "I2CTarget(%u, addr=%u)", self->i2c_id, self->addr); +} + +// Stop the Slave transfer and free the memory objects. +static void mp_machine_i2c_target_deinit(machine_i2c_target_obj_t *self) { + LPI2C_SlaveDeinit(self->i2c_inst); +} diff --git a/ports/mimxrt/main.c b/ports/mimxrt/main.c index 6b9d4fa17afe4..7166171f17c2d 100644 --- a/ports/mimxrt/main.c +++ b/ports/mimxrt/main.c @@ -55,6 +55,7 @@ #endif #include "systick.h" +#include "extmod/modmachine.h" #include "extmod/modnetwork.h" #include "extmod/vfs.h" diff --git a/ports/mimxrt/mpconfigport.h b/ports/mimxrt/mpconfigport.h index e1c605f452a32..8ceb3b418370b 100644 --- a/ports/mimxrt/mpconfigport.h +++ b/ports/mimxrt/mpconfigport.h @@ -92,6 +92,11 @@ uint32_t trng_random_u32(void); #define MICROPY_PY_MACHINE_PWM (1) #define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/mimxrt/machine_pwm.c" #define MICROPY_PY_MACHINE_I2C (1) +#define MICROPY_PY_MACHINE_I2C_TARGET (1) +#define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/mimxrt/machine_i2c_target.c" +#define MICROPY_PY_MACHINE_I2C_TARGET_MAX (FSL_FEATURE_SOC_LPI2C_COUNT) +#define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) +#define MICROPY_PY_MACHINE_I2C_TARGET_FINALISER (1) #ifndef MICROPY_PY_MACHINE_I2S #define MICROPY_PY_MACHINE_I2S (0) #endif From 79d182deb28a478413c187e1db33681a46e1d20e Mon Sep 17 00:00:00 2001 From: robert-hh Date: Thu, 5 Jun 2025 20:48:25 +0200 Subject: [PATCH 054/143] samd/machine_i2c_target: Support I2C target mode. Supporting readfrom_mem*(). writeto_mem() and a set of IRQs. Enabled by default for SAMD51 devices and SAMD21 devices with external flash. Tested with ItsyBitsy M4 and ItsyBitsy M0 with both on-board SoftI2C and a RP2 Pico as controller. Signed-off-by: Damien George Signed-off-by: robert-hh --- .../ADAFRUIT_NEOKEY_TRINKEY/mpconfigboard.h | 1 + ports/samd/machine_i2c.c | 38 ++-- ports/samd/machine_i2c_target.c | 213 ++++++++++++++++++ ports/samd/main.c | 3 +- ports/samd/mcu/samd21/mpconfigmcu.h | 3 + ports/samd/mcu/samd51/mpconfigmcu.h | 3 + ports/samd/mpconfigport.h | 4 + ports/samd/pin_af.c | 16 ++ ports/samd/pin_af.h | 2 + ports/samd/samd_soc.c | 2 +- 10 files changed, 259 insertions(+), 26 deletions(-) create mode 100644 ports/samd/machine_i2c_target.c diff --git a/ports/samd/boards/ADAFRUIT_NEOKEY_TRINKEY/mpconfigboard.h b/ports/samd/boards/ADAFRUIT_NEOKEY_TRINKEY/mpconfigboard.h index eb4704ff8cb97..bf44bd661c0a3 100644 --- a/ports/samd/boards/ADAFRUIT_NEOKEY_TRINKEY/mpconfigboard.h +++ b/ports/samd/boards/ADAFRUIT_NEOKEY_TRINKEY/mpconfigboard.h @@ -8,6 +8,7 @@ #define MICROPY_PY_MACHINE_SOFTI2C (0) #define MICROPY_PY_MACHINE_SOFTSPI (0) #define MICROPY_PY_MACHINE_I2C (0) +#define MICROPY_PY_MACHINE_I2C_TARGET (0) #define MICROPY_PY_MACHINE_SPI (0) #define MICROPY_PY_MACHINE_UART (0) #define MICROPY_PY_MACHINE_ADC (0) diff --git a/ports/samd/machine_i2c.c b/ports/samd/machine_i2c.c index 172518523d208..50548b62a7d68 100644 --- a/ports/samd/machine_i2c.c +++ b/ports/samd/machine_i2c.c @@ -29,13 +29,11 @@ #if MICROPY_PY_MACHINE_I2C -#include "py/mphal.h" #include "py/mperrno.h" #include "extmod/modmachine.h" #include "samd_soc.h" #include "pin_af.h" #include "genhdr/pins.h" -#include "clock_config.h" #define DEFAULT_I2C_FREQ (400000) #define RISETIME_NS (200) @@ -79,9 +77,9 @@ static void i2c_send_command(Sercom *i2c, uint8_t command) { } void common_i2c_irq_handler(int i2c_id) { - // handle Sercom I2C IRQ + // Handle Sercom I2C IRQ for controller mode. machine_i2c_obj_t *self = MP_STATE_PORT(sercom_table[i2c_id]); - // Handle IRQ + if (self != NULL) { Sercom *i2c = self->instance; // For now, clear all interrupts @@ -114,7 +112,8 @@ void common_i2c_irq_handler(int i2c_id) { } else { // On any error, e.g. ARBLOST or BUSERROR, stop the transmission self->len = 0; self->state = state_buserr; - i2c->I2CM.INTFLAG.reg |= SERCOM_I2CM_INTFLAG_ERROR; + i2c->I2CM.INTFLAG.reg = SERCOM_I2CM_INTFLAG_ERROR | + SERCOM_I2CM_INTFLAG_SB | SERCOM_I2CM_INTFLAG_MB; } } } @@ -158,28 +157,19 @@ mp_obj_t machine_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_t n // Get the peripheral object. machine_i2c_obj_t *self = mp_obj_malloc(machine_i2c_obj_t, &machine_i2c_type); self->id = id; - self->instance = sercom_instance[self->id]; + self->instance = sercom_instance[id]; // Set SCL/SDA pins. - self->scl = mp_hal_get_pin_obj(args[ARG_scl].u_obj); - self->sda = mp_hal_get_pin_obj(args[ARG_sda].u_obj); + self->sda = pin_config_for_i2c(args[ARG_sda].u_obj, id, 0); + self->scl = pin_config_for_i2c(args[ARG_scl].u_obj, id, 1); + MP_STATE_PORT(sercom_table[id]) = self; - sercom_pad_config_t scl_pad_config = get_sercom_config(self->scl, self->id); - sercom_pad_config_t sda_pad_config = get_sercom_config(self->sda, self->id); - if (sda_pad_config.pad_nr != 0 || scl_pad_config.pad_nr != 1) { - mp_raise_ValueError(MP_ERROR_TEXT("invalid sda/scl pin")); - } - MP_STATE_PORT(sercom_table[self->id]) = self; self->freq = args[ARG_freq].u_int; // The unit for ARG_timeout is us, but the code uses ms. self->timeout = args[ARG_timeout].u_int / 1000; - // Configure the Pin mux. - mp_hal_set_pin_mux(self->scl, scl_pad_config.alt_fct); - mp_hal_set_pin_mux(self->sda, sda_pad_config.alt_fct); - // Set up the clocks - enable_sercom_clock(self->id); + enable_sercom_clock(id); // Initialise the I2C peripheral Sercom *i2c = self->instance; @@ -207,13 +197,13 @@ mp_obj_t machine_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_t n i2c->I2CM.BAUD.reg = baud; // Enable interrupts - sercom_register_irq(self->id, &common_i2c_irq_handler); + sercom_register_irq(id, &common_i2c_irq_handler); #if defined(MCU_SAMD21) - NVIC_EnableIRQ(SERCOM0_IRQn + self->id); + NVIC_EnableIRQ(SERCOM0_IRQn + id); #elif defined(MCU_SAMD51) - NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * self->id); // MB interrupt - NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * self->id + 1); // SB interrupt - NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * self->id + 3); // ERROR interrupt + NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * id); // MB interrupt + NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * id + 1); // SB interrupt + NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * id + 3); // ERROR interrupt #endif // Now enable I2C. diff --git a/ports/samd/machine_i2c_target.c b/ports/samd/machine_i2c_target.c new file mode 100644 index 0000000000000..054ca81dd1a34 --- /dev/null +++ b/ports/samd/machine_i2c_target.c @@ -0,0 +1,213 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2020-2021 Damien P. George + * Copyright (c) 2022-2025 Robert Hammelrath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/runtime.h" +#include "samd_soc.h" +#include "pin_af.h" +#include "genhdr/pins.h" + +#define TRANSMIT (1) +#define RECEIVE (0) +#define NACK_RECVD (i2c->I2CS.STATUS.bit.RXNACK == 1) +#define IRQ_AMATCH (i2c->I2CS.INTFLAG.bit.AMATCH == 1) +#define IRQ_DRDY (i2c->I2CS.INTFLAG.bit.DRDY == 1) +#define IRQ_STOP (i2c->I2CS.INTFLAG.bit.PREC == 1) + +#define PREPARE_ACK i2c->I2CS.CTRLB.bit.ACKACT = 0 +#define PREPARE_NACK i2c->I2CS.CTRLB.bit.ACKACT = 1 + +typedef struct _machine_i2c_target_obj_t { + mp_obj_base_t base; + Sercom *instance; + uint8_t id; + uint8_t scl; + uint8_t sda; + uint8_t addr; + uint8_t direction; +} machine_i2c_target_obj_t; + +void common_i2c_target_irq_handler(int i2c_id) { + // Handle Sercom I2C IRQ for target memory mode. + machine_i2c_target_obj_t *self = MP_STATE_PORT(sercom_table[i2c_id]); + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id]; + + if (self != NULL) { + Sercom *i2c = self->instance; + + if (IRQ_AMATCH) { + // Address match. + self->direction = i2c->I2CS.STATUS.bit.DIR; + machine_i2c_target_data_addr_match(data, self->direction); + // Send ACK + i2c->I2CS.CTRLB.bit.CMD = 3; + + } else if (IRQ_DRDY) { + // Data to be handled, depending in the direction + if (self->direction == TRANSMIT) { + machine_i2c_target_data_read_request(self, data); + } else { + machine_i2c_target_data_write_request(self, data); + } + // ACK will be sent in mp_machine_i2c_target_read_bytes/mp_machine_i2c_target_write_bytes. + } else if (IRQ_STOP) { + // Stop detected. Just reset the data machine. + machine_i2c_target_data_stop(data); + i2c->I2CS.INTFLAG.reg |= SERCOM_I2CS_INTFLAG_PREC; + + } else { // On any error clear the interrupts and reset the data state. + machine_i2c_target_data_stop(data); + i2c->I2CS.INTFLAG.reg = SERCOM_I2CS_INTFLAG_ERROR | SERCOM_I2CS_INTFLAG_AMATCH | + SERCOM_I2CS_INTFLAG_DRDY | SERCOM_I2CS_INTFLAG_PREC; + } + } +} + +/******************************************************************************/ +// I2CTarget port implementation + +static inline size_t mp_machine_i2c_target_get_index(machine_i2c_target_obj_t *self) { + return self->id; +} + +static void mp_machine_i2c_target_event_callback(machine_i2c_target_irq_obj_t *irq) { + mp_irq_handler(&irq->base); +} + +static size_t mp_machine_i2c_target_read_bytes(machine_i2c_target_obj_t *self, size_t len, uint8_t *buf) { + Sercom *i2c = self->instance; + buf[0] = i2c->I2CS.DATA.reg; + i2c->I2CS.CTRLB.bit.CMD = 3; // send ACK + return 1; +} + +static size_t mp_machine_i2c_target_write_bytes(machine_i2c_target_obj_t *self, size_t len, const uint8_t *buf) { + Sercom *i2c = self->instance; + i2c->I2CS.DATA.reg = buf[0]; + i2c->I2CS.CTRLB.bit.CMD = 3; // send ACK + return 1; +} + +static inline void mp_machine_i2c_target_irq_config(machine_i2c_target_obj_t *self, unsigned int trigger) { + (void)self; + (void)trigger; +} + +mp_obj_t mp_machine_i2c_target_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_id, ARG_addr, ARG_addrsize, ARG_mem, ARG_mem_addrsize, ARG_scl, ARG_sda }; + static const mp_arg_t allowed_args[] = { + #if MICROPY_HW_DEFAULT_I2C_ID < 0 + { MP_QSTR_id, MP_ARG_REQUIRED | MP_ARG_INT, {.u_int = -1} }, + #else + { MP_QSTR_id, MP_ARG_INT, {.u_int = MICROPY_HW_DEFAULT_I2C_ID} }, + #endif + { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 7} }, + { MP_QSTR_mem, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_mem_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, + #if defined(pin_SCL) && defined(pin_SDA) + { MP_QSTR_scl, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = pin_SCL} }, + { MP_QSTR_sda, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = pin_SDA} }, + #else + { MP_QSTR_scl, MP_ARG_REQUIRED | MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_sda, MP_ARG_REQUIRED | MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + #endif + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Get I2C bus. + int id = args[ARG_id].u_int; + if (id < 0 || id >= SERCOM_INST_NUM) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("I2C(%d) doesn't exist"), id); + } + + // Get the peripheral object. + machine_i2c_target_obj_t *self = mp_obj_malloc_with_finaliser(machine_i2c_target_obj_t, &machine_i2c_target_type); + self->id = id; + self->instance = sercom_instance[id]; + + // Set SCL/SDA pins. + self->sda = pin_config_for_i2c(args[ARG_sda].u_obj, id, 0); + self->scl = pin_config_for_i2c(args[ARG_scl].u_obj, id, 1); + + MP_STATE_PORT(sercom_table[id]) = self; + + // Get the address and initialise data. + self->addr = args[ARG_addr].u_int; + MP_STATE_PORT(machine_i2c_target_mem_obj)[id] = args[ARG_mem].u_obj; + machine_i2c_target_data_t *data = &machine_i2c_target_data[id]; + machine_i2c_target_data_init(data, args[ARG_mem].u_obj, args[ARG_mem_addrsize].u_int); + + // Set up the clocks + enable_sercom_clock(id); + + // Initialise the I2C peripheral + Sercom *i2c = self->instance; + // Reset the device + i2c->I2CS.CTRLA.reg = SERCOM_I2CM_CTRLA_SWRST; + while (i2c->I2CS.SYNCBUSY.bit.SWRST == 1) { + } + + // Set to slave mode, enable SCl timeout, set the address + i2c->I2CS.CTRLA.reg = SERCOM_I2CS_CTRLA_MODE(0x04) + | SERCOM_I2CS_CTRLA_SEXTTOEN | SERCOM_I2CS_CTRLA_LOWTOUTEN; + i2c->I2CS.ADDR.reg = self->addr << 1; + + // Enable interrupts + sercom_register_irq(id, &common_i2c_target_irq_handler); + #if defined(MCU_SAMD21) + NVIC_EnableIRQ(SERCOM0_IRQn + id); + #elif defined(MCU_SAMD51) + NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * id); + NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * id + 1); + NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * id + 2); + NVIC_EnableIRQ(SERCOM0_0_IRQn + 4 * id + 3); + #endif + i2c->I2CS.INTENSET.reg = SERCOM_I2CS_INTENSET_DRDY | SERCOM_I2CS_INTENSET_AMATCH | + SERCOM_I2CS_INTENSET_PREC | SERCOM_I2CS_INTENSET_ERROR; + + // Now enable I2C. + sercom_enable(i2c, 1); + + return MP_OBJ_FROM_PTR(self); +} + +static void mp_machine_i2c_target_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "I2C(%u, scl=\"%q\", sda=\"%q\", addr=%u)", + self->id, pin_find_by_id(self->scl)->name, pin_find_by_id(self->sda)->name, + self->addr); +} + +// Stop the Slave transfer and free the memory objects. +static void mp_machine_i2c_target_deinit(machine_i2c_target_obj_t *self) { + // Disable I2C + sercom_enable(self->instance, 0); + MP_STATE_PORT(sercom_table[self->id]) = NULL; +} diff --git a/ports/samd/main.c b/ports/samd/main.c index a7da95582f76c..475f57703d076 100644 --- a/ports/samd/main.c +++ b/ports/samd/main.c @@ -30,6 +30,7 @@ #include "py/mperrno.h" #include "py/mphal.h" #include "py/stackctrl.h" +#include "extmod/modmachine.h" #include "shared/readline/readline.h" #include "shared/runtime/gchelper.h" #include "shared/runtime/pyexec.h" @@ -101,7 +102,7 @@ void samd_main(void) { mp_usbd_deinit(); #endif gc_sweep_all(); - #if MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_SPI || MICROPY_PY_MACHINE_UART + #if MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_I2C_TARGET || MICROPY_PY_MACHINE_SPI || MICROPY_PY_MACHINE_UART sercom_deinit_all(); #endif mp_deinit(); diff --git a/ports/samd/mcu/samd21/mpconfigmcu.h b/ports/samd/mcu/samd21/mpconfigmcu.h index f0a7a73e0c027..a29d5c0a04db4 100644 --- a/ports/samd/mcu/samd21/mpconfigmcu.h +++ b/ports/samd/mcu/samd21/mpconfigmcu.h @@ -78,6 +78,9 @@ unsigned long trng_random_u32(int delay); #ifndef MICROPY_PY_ONEWIRE #define MICROPY_PY_ONEWIRE (SAMD21_EXTRA_FEATURES) #endif +#ifndef MICROPY_PY_MACHINE_I2C_TARGET +#define MICROPY_PY_MACHINE_I2C_TARGET (SAMD21_EXTRA_FEATURES) +#endif #ifndef MICROPY_PY_MACHINE_PIN_BOARD_CPU #define MICROPY_PY_MACHINE_PIN_BOARD_CPU (1) diff --git a/ports/samd/mcu/samd51/mpconfigmcu.h b/ports/samd/mcu/samd51/mpconfigmcu.h index 8cce90b886c07..a1ff208eb5939 100644 --- a/ports/samd/mcu/samd51/mpconfigmcu.h +++ b/ports/samd/mcu/samd51/mpconfigmcu.h @@ -16,6 +16,9 @@ #define MICROPY_PY_RANDOM_SEED_INIT_FUNC (trng_random_u32()) unsigned long trng_random_u32(void); #define MICROPY_PY_MACHINE_UART_IRQ (1) +#ifndef MICROPY_PY_MACHINE_I2C_TARGET +#define MICROPY_PY_MACHINE_I2C_TARGET (1) +#endif // fatfs configuration used in ffconf.h #define MICROPY_FATFS_ENABLE_LFN (1) diff --git a/ports/samd/mpconfigport.h b/ports/samd/mpconfigport.h index 514f383948838..7b423bf0babff 100644 --- a/ports/samd/mpconfigport.h +++ b/ports/samd/mpconfigport.h @@ -127,6 +127,10 @@ #define MICROPY_PY_MACHINE_WDT_INCLUDEFILE "ports/samd/machine_wdt.c" #define MICROPY_PY_MACHINE_WDT_TIMEOUT_MS (1) #define MICROPY_PLATFORM_VERSION "ASF4" +#define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/samd/machine_i2c_target.c" +#define MICROPY_PY_MACHINE_I2C_TARGET_MAX (SERCOM_INST_NUM) +#define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) +#define MICROPY_PY_MACHINE_I2C_TARGET_FINALISER (1) #define MP_STATE_PORT MP_STATE_VM diff --git a/ports/samd/pin_af.c b/ports/samd/pin_af.c index 5d05b6d18d5c1..35cac27aa5700 100644 --- a/ports/samd/pin_af.c +++ b/ports/samd/pin_af.c @@ -161,3 +161,19 @@ pwm_config_t get_pwm_config(int pin_id, int wanted_dev, uint8_t device_status[]) } #endif + +#if MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_I2C_TARGET + +// Configure a I2C pin. Used by machine_i2c.c and machine_i2c_target.c. +uint8_t pin_config_for_i2c(mp_obj_t pin_obj, uint8_t id, uint8_t pad_nr) { + uint8_t pin = mp_hal_get_pin_obj(pin_obj); + sercom_pad_config_t pad_config = get_sercom_config(pin, id); + if (pad_config.pad_nr != pad_nr) { + mp_raise_ValueError(MP_ERROR_TEXT("invalid sda/scl pin")); + } + // Configure the Pin mux. + mp_hal_set_pin_mux(pin, pad_config.alt_fct); + return pin; +} + +#endif diff --git a/ports/samd/pin_af.h b/ports/samd/pin_af.h index 83839a0503255..bc65e8ae23bf9 100644 --- a/ports/samd/pin_af.h +++ b/ports/samd/pin_af.h @@ -100,3 +100,5 @@ adc_config_t get_adc_config(int pin_id, int32_t flag); pwm_config_t get_pwm_config(int pin_id, int wanted_dev, uint8_t used_dev[]); const machine_pin_obj_t *pin_find_by_id(int pin_id); const machine_pin_obj_t *pin_find(mp_obj_t pin); + +uint8_t pin_config_for_i2c(mp_obj_t pin_obj, uint8_t id, uint8_t pad_nr); diff --git a/ports/samd/samd_soc.c b/ports/samd/samd_soc.c index e78032513c216..fb6eb2083a304 100644 --- a/ports/samd/samd_soc.c +++ b/ports/samd/samd_soc.c @@ -122,7 +122,7 @@ void samd_init(void) { machine_rtc_start(false); } -#if MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_SPI || MICROPY_PY_MACHINE_UART +#if MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_I2C_TARGET || MICROPY_PY_MACHINE_SPI || MICROPY_PY_MACHINE_UART Sercom *sercom_instance[] = SERCOM_INSTS; MP_REGISTER_ROOT_POINTER(void *sercom_table[SERCOM_INST_NUM]); From ac5b1bce9986775e787a24f77f827c4ad50ffff5 Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 30 Jul 2025 11:26:42 +1000 Subject: [PATCH 055/143] esp32/machine_i2c: Factor default pin macros to header file. So the implementation of I2CTarget can use them. Signed-off-by: Damien George --- ports/esp32/machine_i2c.c | 21 +--------------- ports/esp32/machine_i2c.h | 52 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 ports/esp32/machine_i2c.h diff --git a/ports/esp32/machine_i2c.c b/ports/esp32/machine_i2c.c index 4a5fd717631a9..a9e5f0d3b32cb 100644 --- a/ports/esp32/machine_i2c.c +++ b/ports/esp32/machine_i2c.c @@ -28,32 +28,13 @@ #include "py/mphal.h" #include "py/mperrno.h" #include "extmod/modmachine.h" +#include "machine_i2c.h" #include "driver/i2c.h" #include "hal/i2c_ll.h" #if MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_SOFTI2C -#ifndef MICROPY_HW_I2C0_SCL -#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 -#define MICROPY_HW_I2C0_SCL (GPIO_NUM_9) -#define MICROPY_HW_I2C0_SDA (GPIO_NUM_8) -#else -#define MICROPY_HW_I2C0_SCL (GPIO_NUM_18) -#define MICROPY_HW_I2C0_SDA (GPIO_NUM_19) -#endif -#endif - -#ifndef MICROPY_HW_I2C1_SCL -#if CONFIG_IDF_TARGET_ESP32 -#define MICROPY_HW_I2C1_SCL (GPIO_NUM_25) -#define MICROPY_HW_I2C1_SDA (GPIO_NUM_26) -#else -#define MICROPY_HW_I2C1_SCL (GPIO_NUM_9) -#define MICROPY_HW_I2C1_SDA (GPIO_NUM_8) -#endif -#endif - #if SOC_I2C_SUPPORT_XTAL #if CONFIG_XTAL_FREQ > 0 #define I2C_SCLK_FREQ (CONFIG_XTAL_FREQ * 1000000) diff --git a/ports/esp32/machine_i2c.h b/ports/esp32/machine_i2c.h new file mode 100644 index 0000000000000..299baa6dd6843 --- /dev/null +++ b/ports/esp32/machine_i2c.h @@ -0,0 +1,52 @@ + +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2019-2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_ESP32_MACHINE_I2C_H +#define MICROPY_INCLUDED_ESP32_MACHINE_I2C_H + +// Configure default I2C0 pins. +#ifndef MICROPY_HW_I2C0_SCL +#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 +#define MICROPY_HW_I2C0_SCL (GPIO_NUM_9) +#define MICROPY_HW_I2C0_SDA (GPIO_NUM_8) +#else +#define MICROPY_HW_I2C0_SCL (GPIO_NUM_18) +#define MICROPY_HW_I2C0_SDA (GPIO_NUM_19) +#endif +#endif + +// Configure default I2C1 pins. +#ifndef MICROPY_HW_I2C1_SCL +#if CONFIG_IDF_TARGET_ESP32 +#define MICROPY_HW_I2C1_SCL (GPIO_NUM_25) +#define MICROPY_HW_I2C1_SDA (GPIO_NUM_26) +#else +#define MICROPY_HW_I2C1_SCL (GPIO_NUM_9) +#define MICROPY_HW_I2C1_SDA (GPIO_NUM_8) +#endif +#endif + +#endif // MICROPY_INCLUDED_ESP32_MACHINE_I2C_H From 7bc83afee2115d1a470bc2e901d998b63cef7b25 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 28 Jul 2025 15:20:19 +1000 Subject: [PATCH 056/143] esp32/machine_i2c_target: Implement I2CTarget class. Only soft IRQs are supported. Signed-off-by: Damien George --- ports/esp32/boards/sdkconfig.base | 4 + ports/esp32/machine_i2c_target.c | 225 ++++++++++++++++++++++++++++++ ports/esp32/main.c | 5 + ports/esp32/mpconfigport.h | 4 + 4 files changed, 238 insertions(+) create mode 100644 ports/esp32/machine_i2c_target.c diff --git a/ports/esp32/boards/sdkconfig.base b/ports/esp32/boards/sdkconfig.base index 69abc63bdf008..2f1835c9242d4 100644 --- a/ports/esp32/boards/sdkconfig.base +++ b/ports/esp32/boards/sdkconfig.base @@ -139,3 +139,7 @@ CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT=n # Further limit total sockets in TIME-WAIT when there are many short-lived # connections. CONFIG_LWIP_MAX_ACTIVE_TCP=12 + +# Enable new I2C slave API, and disable conflict check. +CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK=y +CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y diff --git a/ports/esp32/machine_i2c_target.c b/ports/esp32/machine_i2c_target.c new file mode 100644 index 0000000000000..84795e6f138f8 --- /dev/null +++ b/ports/esp32/machine_i2c_target.c @@ -0,0 +1,225 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_i2c_target.c via MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE. + +#include "machine_i2c.h" +#include "driver/i2c_slave.h" + +// These headers are needed to call i2c_ll_txfifo_rst(). +#include "hal/i2c_ll.h" +#include "../i2c_private.h" + +typedef struct _machine_i2c_target_obj_t { + mp_obj_base_t base; + i2c_slave_dev_handle_t handle; + i2c_slave_config_t config; + uint8_t state; + bool stop_pending; + bool irq_active; + int index; + const i2c_slave_rx_done_event_data_t *rx_done_event_data; +} machine_i2c_target_obj_t; + +static machine_i2c_target_obj_t machine_i2c_target_obj[I2C_NUM_MAX]; + +/******************************************************************************/ +// ESP-IDF hardware bindings + +// Called when the controller is about to read from the TX/send buffer. +static bool i2c_slave_request_cb(i2c_slave_dev_handle_t i2c_slave, const i2c_slave_request_event_data_t *evt_data, void *arg) { + machine_i2c_target_obj_t *self = arg; + machine_i2c_target_data_t *data = &machine_i2c_target_data[self->config.i2c_port]; + + // Flush hardware TX FIFO to get rid of any data from a previous read. + i2c_ll_txfifo_rst(self->handle->base->hal.dev); + + // Perform an entire read transaction, including start, read and stop events. + // We don't know how much data the controller will read, so write the entire + // memory buffer to the TX FIFO. + machine_i2c_target_data_addr_match(data, true); + for (uint32_t i = 0; i < data->mem_len; ++i) { + machine_i2c_target_data_read_request(self, data); + } + machine_i2c_target_data_restart_or_stop(data); + + // A higher priority task was not woken up. + return false; +} + +// Called when the controller has written into the RX/receive buffer. +static bool i2c_slave_receive_cb(i2c_slave_dev_handle_t i2c_slave, const i2c_slave_rx_done_event_data_t *evt_data, void *arg) { + machine_i2c_target_obj_t *self = arg; + machine_i2c_target_data_t *data = &machine_i2c_target_data[self->config.i2c_port]; + + // Perform an entire write transaction, including start, read and stop events. + machine_i2c_target_data_addr_match(data, false); + self->index = 0; + self->rx_done_event_data = evt_data; + while (self->index < self->rx_done_event_data->length) { + machine_i2c_target_data_write_request(self, data); + } + machine_i2c_target_data_restart_or_stop(data); + + // A higher priority task was not woken up. + return false; +} + +static void i2c_target_init(machine_i2c_target_obj_t *self, machine_i2c_target_data_t *data, uint32_t addr, uint32_t addrsize, bool first_init) { + if (!first_init && self->handle != NULL) { + i2c_del_slave_device(self->handle); + self->handle = NULL; + } + + self->config.clk_source = I2C_CLK_SRC_DEFAULT; + self->config.slave_addr = addr; + self->config.send_buf_depth = data->mem_len; + self->config.receive_buf_depth = data->mem_len; + if (addrsize == 7) { + self->config.addr_bit_len = I2C_ADDR_BIT_LEN_7; + } else { + #if SOC_I2C_SUPPORT_10BIT_ADDR + self->config.addr_bit_len = I2C_ADDR_BIT_LEN_10; + #else + mp_raise_ValueError(MP_ERROR_TEXT("10-bit address unsupported")); + #endif + } + self->config.intr_priority = 0; // 0 selects the default + self->config.flags.allow_pd = 0; + self->config.flags.enable_internal_pullup = 1; + + ESP_ERROR_CHECK(i2c_new_slave_device(&self->config, &self->handle)); + i2c_slave_event_callbacks_t cbs = { + .on_receive = i2c_slave_receive_cb, + .on_request = i2c_slave_request_cb, + }; + ESP_ERROR_CHECK(i2c_slave_register_event_callbacks(self->handle, &cbs, self)); +} + +/******************************************************************************/ +// I2CTarget port implementation + +static inline size_t mp_machine_i2c_target_get_index(machine_i2c_target_obj_t *self) { + return self->config.i2c_port; +} + +static void mp_machine_i2c_target_event_callback(machine_i2c_target_irq_obj_t *irq) { + mp_irq_handler(&irq->base); +} + +static size_t mp_machine_i2c_target_read_bytes(machine_i2c_target_obj_t *self, size_t len, uint8_t *buf) { + size_t i = 0; + while (i < len && self->index < self->rx_done_event_data->length) { + buf[i++] = self->rx_done_event_data->buffer[self->index++]; + } + return i; +} + +static size_t mp_machine_i2c_target_write_bytes(machine_i2c_target_obj_t *self, size_t len, const uint8_t *buf) { + uint32_t write_len; + i2c_slave_write(self->handle, buf, len, &write_len, 1000); + return write_len; +} + +static void mp_machine_i2c_target_irq_config(machine_i2c_target_obj_t *self, unsigned int trigger) { +} + +static mp_obj_t mp_machine_i2c_target_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_id, ARG_addr, ARG_addrsize, ARG_mem, ARG_mem_addrsize, ARG_scl, ARG_sda }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_id, MP_ARG_INT, {.u_int = 0} }, + { MP_QSTR_addr, MP_ARG_REQUIRED | MP_ARG_INT }, + { MP_QSTR_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 7} }, + { MP_QSTR_mem, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_mem_addrsize, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8} }, + { MP_QSTR_scl, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_sda, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + }; + + // Parse args. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + int i2c_id = args[ARG_id].u_int; + + // Check if the I2C bus is valid + if (i2c_id < 0 || i2c_id >= MP_ARRAY_SIZE(machine_i2c_target_obj)) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("I2CTarget(%d) doesn't exist"), i2c_id); + } + + // Get static peripheral object. + machine_i2c_target_obj_t *self = &machine_i2c_target_obj[i2c_id]; + + bool first_init = false; + if (self->base.type == NULL) { + // Created for the first time, set default pins + self->base.type = &machine_i2c_target_type; + self->config.i2c_port = i2c_id; + if (self->config.i2c_port == 0) { + self->config.scl_io_num = MICROPY_HW_I2C0_SCL; + self->config.sda_io_num = MICROPY_HW_I2C0_SDA; + } else { + self->config.scl_io_num = MICROPY_HW_I2C1_SCL; + self->config.sda_io_num = MICROPY_HW_I2C1_SDA; + } + first_init = true; + } + + // Initialise data. + self->state = STATE_IDLE; + self->stop_pending = false; + self->irq_active = false; + MP_STATE_PORT(machine_i2c_target_mem_obj)[i2c_id] = args[ARG_mem].u_obj; + machine_i2c_target_data_t *data = &machine_i2c_target_data[i2c_id]; + machine_i2c_target_data_init(data, args[ARG_mem].u_obj, args[ARG_mem_addrsize].u_int); + + // Set SCL/SDA pins if configured. + if (args[ARG_scl].u_obj != mp_const_none) { + self->config.scl_io_num = mp_hal_get_pin_obj(args[ARG_scl].u_obj); + } + if (args[ARG_sda].u_obj != mp_const_none) { + self->config.sda_io_num = mp_hal_get_pin_obj(args[ARG_sda].u_obj); + } + + // Initialise the I2C target. + i2c_target_init(self, data, args[ARG_addr].u_int, args[ARG_addrsize].u_int, first_init); + + return MP_OBJ_FROM_PTR(self); +} + +static void mp_machine_i2c_target_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_i2c_target_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "I2CTarget(%u, addr=%u, scl=%u, sda=%u)", + self->config.i2c_port, self->config.slave_addr, self->config.scl_io_num, self->config.sda_io_num); +} + +static void mp_machine_i2c_target_deinit(machine_i2c_target_obj_t *self) { + if (self->handle != NULL) { + i2c_del_slave_device(self->handle); + self->handle = NULL; + } +} diff --git a/ports/esp32/main.c b/ports/esp32/main.c index f048aa85f5f7f..f85fb6c084f90 100644 --- a/ports/esp32/main.c +++ b/ports/esp32/main.c @@ -51,6 +51,7 @@ #include "py/repl.h" #include "py/gc.h" #include "py/mphal.h" +#include "extmod/modmachine.h" #include "shared/readline/readline.h" #include "shared/runtime/pyexec.h" #include "shared/timeutils/timeutils.h" @@ -199,7 +200,11 @@ void mp_task(void *pvParameter) { machine_pwm_deinit_all(); // TODO: machine_rmt_deinit_all(); machine_pins_deinit(); + #if MICROPY_PY_MACHINE_I2C_TARGET + mp_machine_i2c_target_deinit_all(); + #endif machine_deinit(); + #if MICROPY_PY_SOCKET_EVENTS socket_events_deinit(); #endif diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index 142c1b1be2eb6..1844018030b8b 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -138,6 +138,10 @@ #define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/esp32/machine_pwm.c" #define MICROPY_PY_MACHINE_I2C (1) #define MICROPY_PY_MACHINE_I2C_TRANSFER_WRITE1 (1) +// I2C target hardware is limited on ESP32 (eg read event comes after the read) so we only support newer SoCs. +#define MICROPY_PY_MACHINE_I2C_TARGET (SOC_I2C_SUPPORT_SLAVE && !CONFIG_IDF_TARGET_ESP32) +#define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/esp32/machine_i2c_target.c" +#define MICROPY_PY_MACHINE_I2C_TARGET_MAX (2) #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_SPI (1) #define MICROPY_PY_MACHINE_SOFTSPI (1) From 6558d519a22154795c0da4101bc7bbd1527ed176 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 27 May 2025 14:00:31 +1000 Subject: [PATCH 057/143] tests/extmod_hardware: Add self unittest for I2CTarget. This test uses a SoftI2C controller wired to an I2CTarget on the one board, and tests all functionality of the I2CTarget class. Signed-off-by: Damien George --- tests/extmod_hardware/machine_i2c_target.py | 307 ++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 tests/extmod_hardware/machine_i2c_target.py diff --git a/tests/extmod_hardware/machine_i2c_target.py b/tests/extmod_hardware/machine_i2c_target.py new file mode 100644 index 0000000000000..763e6f4771e0f --- /dev/null +++ b/tests/extmod_hardware/machine_i2c_target.py @@ -0,0 +1,307 @@ +# Test machine.I2CTarget. +# +# IMPORTANT: This test requires hardware connections: a SoftI2C instance must be +# wired to a hardware I2C target. See pin definitions below. + +import sys + +try: + from machine import Pin, SoftI2C, I2CTarget +except ImportError: + print("SKIP") + raise SystemExit + +import unittest + +ADDR = 67 + +kwargs_target = {} + +# Configure pins based on the target. +if sys.platform == "alif" and sys.implementation._build == "ALIF_ENSEMBLE": + args_controller = {"scl": "P1_1", "sda": "P1_0"} + args_target = (0,) # on pins P0_3/P0_2 +elif sys.platform == "esp32": + args_controller = {"scl": 5, "sda": 6} + args_target = (0,) # on pins 9/8 for C3 and S3, 18/19 for others + kwargs_target = {"scl": 9, "sda": 8} +elif sys.platform == "rp2": + args_controller = {"scl": 5, "sda": 4} + args_target = (1,) +elif sys.platform == "pyboard": + if sys.implementation._build == "NUCLEO_WB55": + args_controller = {"scl": "B8", "sda": "B9"} + args_target = (3,) + else: + args_controller = {"scl": "X1", "sda": "X2"} + args_target = ("X",) +elif "zephyr-nucleo_wb55rg" in sys.implementation._machine: + # PB8=I2C1_SCL, PB9=I2C1_SDA (on Arduino header D15/D14) + # PC0=I2C3_SCL, PC1=I2C3_SDA (on Arduino header A0/A1) + args_controller = {"scl": Pin(("gpiob", 8)), "sda": Pin(("gpiob", 9))} + args_target = ("i2c3",) +elif "zephyr-rpi_pico" in sys.implementation._machine: + args_controller = {"scl": Pin(("gpio0", 5)), "sda": Pin(("gpio0", 4))} + args_target = ("i2c1",) # on gpio7/gpio6 +elif sys.platform == "mimxrt": + if "Teensy" in sys.implementation._machine: + args_controller = {"scl": "A6", "sda": "A3"} # D20/D17 + else: + args_controller = {"scl": "D0", "sda": "D1"} + args_target = (0,) # pins 19/18 On Teensy 4.x +elif sys.platform == "samd": + args_controller = {"scl": "D5", "sda": "D1"} + args_target = () +else: + print("Please add support for this test on this platform.") + raise SystemExit + + +def config_pull_up(): + Pin(args_controller["scl"], Pin.OPEN_DRAIN, Pin.PULL_UP) + Pin(args_controller["sda"], Pin.OPEN_DRAIN, Pin.PULL_UP) + + +class TestMemory(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.mem = bytearray(8) + cls.i2c = SoftI2C(**args_controller) + cls.i2c_target = I2CTarget(*args_target, **kwargs_target, addr=ADDR, mem=cls.mem) + config_pull_up() + + @classmethod + def tearDownClass(cls): + cls.i2c_target.deinit() + + def test_scan(self): + self.assertIn(ADDR, self.i2c.scan()) + + def test_write(self): + self.mem[:] = b"01234567" + self.i2c.writeto_mem(ADDR, 0, b"test") + self.assertEqual(self.mem, bytearray(b"test4567")) + self.i2c.writeto_mem(ADDR, 4, b"TEST") + self.assertEqual(self.mem, bytearray(b"testTEST")) + + def test_write_wrap(self): + self.mem[:] = b"01234567" + self.i2c.writeto_mem(ADDR, 6, b"test") + self.assertEqual(self.mem, bytearray(b"st2345te")) + + @unittest.skipIf(sys.platform == "esp32", "write lengths larger than buffer unsupported") + def test_write_wrap_large(self): + self.mem[:] = b"01234567" + self.i2c.writeto_mem(ADDR, 0, b"testTESTmore") + self.assertEqual(self.mem, bytearray(b"moreTEST")) + + def test_read(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 4), b"0123") + self.assertEqual(self.i2c.readfrom_mem(ADDR, 4, 4), b"4567") + + def test_read_wrap(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 4), b"0123") + self.assertEqual(self.i2c.readfrom_mem(ADDR, 2, 4), b"2345") + self.assertEqual(self.i2c.readfrom_mem(ADDR, 6, 4), b"6701") + + @unittest.skipIf(sys.platform == "esp32", "read lengths larger than buffer unsupported") + def test_read_wrap_large(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.readfrom_mem(ADDR, 0, 12), b"012345670123") + + def test_write_read(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.writeto(ADDR, b"\x02"), 1) + self.assertEqual(self.i2c.readfrom(ADDR, 4), b"2345") + + @unittest.skipIf(sys.platform == "esp32", "read after read unsupported") + def test_write_read_read(self): + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.writeto(ADDR, b"\x02"), 1) + self.assertEqual(self.i2c.readfrom(ADDR, 4), b"2345") + self.assertEqual(self.i2c.readfrom(ADDR, 4), b"7012") + + +@unittest.skipUnless(hasattr(I2CTarget, "IRQ_END_READ"), "IRQ unsupported") +class TestMemoryIRQ(unittest.TestCase): + @staticmethod + def irq_handler(i2c_target): + flags = i2c_target.irq().flags() + TestMemoryIRQ.events[TestMemoryIRQ.num_events] = flags + TestMemoryIRQ.events[TestMemoryIRQ.num_events + 1] = i2c_target.memaddr + TestMemoryIRQ.num_events += 2 + + @classmethod + def setUpClass(cls): + cls.mem = bytearray(8) + cls.events = [0] * 8 + cls.num_events = 0 + cls.i2c = SoftI2C(**args_controller) + cls.i2c_target = I2CTarget(*args_target, **kwargs_target, addr=ADDR, mem=cls.mem) + cls.i2c_target.irq(TestMemoryIRQ.irq_handler) + config_pull_up() + + @classmethod + def tearDownClass(cls): + cls.i2c_target.deinit() + + @unittest.skipIf(sys.platform == "esp32", "scan doesn't trigger IRQ_END_WRITE") + def test_scan(self): + TestMemoryIRQ.num_events = 0 + self.i2c.scan() + self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_WRITE, 0]) + + def test_write(self): + TestMemoryIRQ.num_events = 0 + self.mem[:] = b"01234567" + self.i2c.writeto_mem(ADDR, 2, b"test") + self.assertEqual(self.mem, bytearray(b"01test67")) + self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_WRITE, 2]) + + def test_read(self): + TestMemoryIRQ.num_events = 0 + self.mem[:] = b"01234567" + self.assertEqual(self.i2c.readfrom_mem(ADDR, 2, 4), b"2345") + self.assertEqual(self.events[: self.num_events], [I2CTarget.IRQ_END_READ, 2]) + + +@unittest.skipUnless(hasattr(I2CTarget, "IRQ_WRITE_REQ"), "IRQ unsupported") +@unittest.skipIf(sys.platform == "mimxrt", "not working") +@unittest.skipIf(sys.platform == "pyboard", "can't queue more than one byte") +@unittest.skipIf(sys.platform == "samd", "not working") +@unittest.skipIf(sys.platform == "zephyr", "must call readinto/write in IRQ handler") +class TestPolling(unittest.TestCase): + @staticmethod + def irq_handler(i2c_target, buf=bytearray(1)): + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_READ_REQ: + i2c_target.write(b"0123") + + @classmethod + def setUpClass(cls): + cls.i2c = SoftI2C(**args_controller) + cls.i2c_target = I2CTarget(*args_target, addr=ADDR) + cls.i2c_target.irq( + TestPolling.irq_handler, + I2CTarget.IRQ_WRITE_REQ | I2CTarget.IRQ_READ_REQ, + hard=True, + ) + config_pull_up() + + @classmethod + def tearDownClass(cls): + cls.i2c_target.deinit() + + def test_read(self): + # Can't write data up front, must wait until IRQ_READ_REQ. + # self.assertEqual(self.i2c_target.write(b"abcd"), 4) + self.assertEqual(self.i2c.readfrom(ADDR, 4), b"0123") + + def test_write(self): + # Can do the read outside the IRQ, but requires IRQ_WRITE_REQ trigger to be set. + self.assertEqual(self.i2c.writeto(ADDR, b"0123"), 4) + buf = bytearray(8) + self.assertEqual(self.i2c_target.readinto(buf), 4) + self.assertEqual(buf, b"0123\x00\x00\x00\x00") + + +@unittest.skipUnless(hasattr(I2CTarget, "IRQ_ADDR_MATCH_READ"), "IRQ unsupported") +class TestIRQ(unittest.TestCase): + @staticmethod + def irq_handler(i2c_target, buf=bytearray(1)): + flags = i2c_target.irq().flags() + TestIRQ.events[TestIRQ.num_events] = flags + TestIRQ.num_events += 1 + if flags & I2CTarget.IRQ_READ_REQ: + i2c_target.write(b"Y") + if flags & I2CTarget.IRQ_WRITE_REQ: + i2c_target.readinto(buf) + TestIRQ.events[TestIRQ.num_events] = buf[0] + TestIRQ.num_events += 1 + + @classmethod + def setUpClass(cls): + cls.events = [0] * 8 + cls.num_events = 0 + cls.i2c = SoftI2C(**args_controller) + cls.i2c_target = I2CTarget(*args_target, addr=ADDR) + cls.i2c_target.irq( + TestIRQ.irq_handler, + I2CTarget.IRQ_ADDR_MATCH_READ + | I2CTarget.IRQ_ADDR_MATCH_WRITE + | I2CTarget.IRQ_WRITE_REQ + | I2CTarget.IRQ_READ_REQ + | I2CTarget.IRQ_END_READ + | I2CTarget.IRQ_END_WRITE, + hard=True, + ) + config_pull_up() + + @classmethod + def tearDownClass(cls): + cls.i2c_target.deinit() + + def test_scan(self): + TestIRQ.num_events = 0 + self.i2c.scan() + self.assertEqual( + self.events[: self.num_events], + [ + I2CTarget.IRQ_ADDR_MATCH_WRITE, + I2CTarget.IRQ_END_WRITE, + ], + ) + + def test_write(self): + TestIRQ.num_events = 0 + self.i2c.writeto(ADDR, b"XYZ") + self.assertEqual( + self.events[: self.num_events], + [ + I2CTarget.IRQ_ADDR_MATCH_WRITE, + I2CTarget.IRQ_WRITE_REQ, + ord(b"X"), + I2CTarget.IRQ_WRITE_REQ, + ord(b"Y"), + I2CTarget.IRQ_WRITE_REQ, + ord(b"Z"), + I2CTarget.IRQ_END_WRITE, + ], + ) + + def test_read(self): + TestIRQ.num_events = 0 + self.assertEqual(self.i2c.readfrom(ADDR, 1), b"Y") + self.assertEqual( + self.events[: self.num_events], + [ + I2CTarget.IRQ_ADDR_MATCH_READ, + I2CTarget.IRQ_READ_REQ, + I2CTarget.IRQ_READ_REQ, + I2CTarget.IRQ_END_READ, + ], + ) + + def test_write_read(self): + TestIRQ.num_events = 0 + self.i2c.writeto(ADDR, b"X", False) + self.assertEqual(self.i2c.readfrom(ADDR, 1), b"Y") + self.assertEqual( + self.events[: self.num_events], + [ + I2CTarget.IRQ_ADDR_MATCH_WRITE, + I2CTarget.IRQ_WRITE_REQ, + ord(b"X"), + I2CTarget.IRQ_END_WRITE, + I2CTarget.IRQ_ADDR_MATCH_READ, + I2CTarget.IRQ_READ_REQ, + I2CTarget.IRQ_READ_REQ, + I2CTarget.IRQ_END_READ, + ], + ) + + +if __name__ == "__main__": + unittest.main() From 277b615f261e3ab9c9c19d8b6d2afb6c5f9a5612 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 22 Jul 2025 18:21:06 +1000 Subject: [PATCH 058/143] tests/multi_extmod: Add I2CTarget multi tests. These require two boards wired together, SCL-SCL and SDA-SDA. Signed-off-by: Damien George --- tests/multi_extmod/machine_i2c_target_irq.py | 137 ++++++++++++++++++ .../machine_i2c_target_irq.py.exp | 15 ++ .../multi_extmod/machine_i2c_target_memory.py | 79 ++++++++++ .../machine_i2c_target_memory.py.exp | 16 ++ 4 files changed, 247 insertions(+) create mode 100644 tests/multi_extmod/machine_i2c_target_irq.py create mode 100644 tests/multi_extmod/machine_i2c_target_irq.py.exp create mode 100644 tests/multi_extmod/machine_i2c_target_memory.py create mode 100644 tests/multi_extmod/machine_i2c_target_memory.py.exp diff --git a/tests/multi_extmod/machine_i2c_target_irq.py b/tests/multi_extmod/machine_i2c_target_irq.py new file mode 100644 index 0000000000000..eafd9dfdca838 --- /dev/null +++ b/tests/multi_extmod/machine_i2c_target_irq.py @@ -0,0 +1,137 @@ +# Test I2CTarget IRQs and clock stretching. +# +# Requires two instances with their SCL and SDA lines connected together. +# Any combination of the below supported boards can be used. +# +# Notes: +# - pull-up resistors may be needed +# - alif use 1.8V signalling + +import sys +import time +from machine import I2C, I2CTarget + +if not hasattr(I2CTarget, "IRQ_ADDR_MATCH_READ"): + print("SKIP") + raise SystemExit + +ADDR = 67 +clock_stretch_us = 200 + +# Configure pins based on the target. +if sys.platform == "alif": + i2c_args = (1,) # pins P3_7/P3_6 + i2c_kwargs = {} +elif sys.platform == "mimxrt": + i2c_args = (0,) # pins 19/18 on Teensy 4.x + i2c_kwargs = {} + clock_stretch_us = 50 # mimxrt cannot delay too long in the IRQ handler +elif sys.platform == "rp2": + i2c_args = (0,) + i2c_kwargs = {"scl": 9, "sda": 8} +elif sys.platform == "pyboard": + i2c_args = ("Y",) + i2c_kwargs = {} +elif sys.platform == "samd": + i2c_args = () # pins SCL/SDA + i2c_kwargs = {} +elif "zephyr-rpi_pico" in sys.implementation._machine: + i2c_args = ("i2c1",) # on gpio7/gpio6 + i2c_kwargs = {} +else: + print("Please add support for this test on this platform.") + raise SystemExit + + +def simple_irq(i2c_target): + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_ADDR_MATCH_READ: + print("IRQ_ADDR_MATCH_READ") + if flags & I2CTarget.IRQ_ADDR_MATCH_WRITE: + print("IRQ_ADDR_MATCH_WRITE") + + # Force clock stretching. + time.sleep_us(clock_stretch_us) + + +class I2CTargetMemory: + def __init__(self, i2c_target, mem): + self.buf1 = bytearray(1) + self.mem = mem + self.memaddr = 0 + self.state = 0 + i2c_target.irq( + self.irq, + I2CTarget.IRQ_ADDR_MATCH_WRITE | I2CTarget.IRQ_READ_REQ | I2CTarget.IRQ_WRITE_REQ, + hard=True, + ) + + def irq(self, i2c_target): + # Force clock stretching. + time.sleep_us(clock_stretch_us) + + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_ADDR_MATCH_WRITE: + self.state = 0 + if flags & I2CTarget.IRQ_READ_REQ: + self.buf1[0] = self.mem[self.memaddr] + self.memaddr += 1 + i2c_target.write(self.buf1) + if flags & I2CTarget.IRQ_WRITE_REQ: + i2c_target.readinto(self.buf1) + if self.state == 0: + self.state = 1 + self.memaddr = self.buf1[0] + else: + self.mem[self.memaddr] = self.buf1[0] + self.memaddr += 1 + self.memaddr %= len(self.mem) + + # Force clock stretching. + time.sleep_us(clock_stretch_us) + + +# I2C controller +def instance0(): + i2c = I2C(*i2c_args, **i2c_kwargs) + multitest.next() + for iteration in range(2): + print("controller iteration", iteration) + multitest.wait("target stage 1") + i2c.writeto_mem(ADDR, 2, "0123") + multitest.broadcast("controller stage 2") + multitest.wait("target stage 3") + print(i2c.readfrom_mem(ADDR, 2, 4)) + multitest.broadcast("controller stage 4") + print("done") + + +# I2C target +def instance1(): + multitest.next() + + for iteration in range(2): + print("target iteration", iteration) + buf = bytearray(b"--------") + if iteration == 0: + # Use built-in memory capability of I2CTarget. + i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR, mem=buf) + i2c_target.irq( + simple_irq, + I2CTarget.IRQ_ADDR_MATCH_READ | I2CTarget.IRQ_ADDR_MATCH_WRITE, + hard=True, + ) + else: + # Implement a memory device by hand. + i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR) + I2CTargetMemory(i2c_target, buf) + + multitest.broadcast("target stage 1") + multitest.wait("controller stage 2") + print(buf) + multitest.broadcast("target stage 3") + multitest.wait("controller stage 4") + + i2c_target.deinit() + + print("done") diff --git a/tests/multi_extmod/machine_i2c_target_irq.py.exp b/tests/multi_extmod/machine_i2c_target_irq.py.exp new file mode 100644 index 0000000000000..a17c8f43858b3 --- /dev/null +++ b/tests/multi_extmod/machine_i2c_target_irq.py.exp @@ -0,0 +1,15 @@ +--- instance0 --- +controller iteration 0 +b'0123' +controller iteration 1 +b'0123' +done +--- instance1 --- +target iteration 0 +IRQ_ADDR_MATCH_WRITE +bytearray(b'--0123--') +IRQ_ADDR_MATCH_WRITE +IRQ_ADDR_MATCH_READ +target iteration 1 +bytearray(b'--0123--') +done diff --git a/tests/multi_extmod/machine_i2c_target_memory.py b/tests/multi_extmod/machine_i2c_target_memory.py new file mode 100644 index 0000000000000..6b3f0d03eb7fe --- /dev/null +++ b/tests/multi_extmod/machine_i2c_target_memory.py @@ -0,0 +1,79 @@ +# Test basic use of I2CTarget and a memory buffer. +# +# Requires two instances with their SCL and SDA lines connected together. +# Any combination of the below supported boards can be used. +# +# Notes: +# - pull-up resistors may be needed +# - alif use 1.8V signalling + +import sys +from machine import I2C, I2CTarget + +ADDR = 67 + +# Configure pins based on the target. +if sys.platform == "alif": + i2c_args = (1,) # pins P3_7/P3_6 + i2c_kwargs = {} +elif sys.platform == "esp32": + i2c_args = (1,) # on pins 9/8 + i2c_kwargs = {} +elif sys.platform == "mimxrt": + i2c_args = (0,) # pins 19/18 on Teensy 4.x + i2c_kwargs = {} +elif sys.platform == "rp2": + i2c_args = (0,) + i2c_kwargs = {"scl": 9, "sda": 8} +elif sys.platform == "pyboard": + i2c_args = ("Y",) + i2c_kwargs = {} +elif sys.platform == "samd": + i2c_args = () # pins SCL/SDA + i2c_kwargs = {} +elif "zephyr-rpi_pico" in sys.implementation._machine: + i2c_args = ("i2c1",) # on gpio7/gpio6 + i2c_kwargs = {} +else: + print("Please add support for this test on this platform.") + raise SystemExit + + +def simple_irq(i2c_target): + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_END_READ: + print("IRQ_END_READ", i2c_target.memaddr) + if flags & I2CTarget.IRQ_END_WRITE: + print("IRQ_END_WRITE", i2c_target.memaddr) + + +# I2C controller +def instance0(): + i2c = I2C(*i2c_args, **i2c_kwargs) + multitest.next() + for iteration in range(2): + print("controller iteration", iteration) + multitest.wait("target stage 1") + i2c.writeto_mem(ADDR, 2 + iteration, "0123") + multitest.broadcast("controller stage 2") + multitest.wait("target stage 3") + print(i2c.readfrom_mem(ADDR, 2 + iteration, 4)) + multitest.broadcast("controller stage 4") + print("done") + + +# I2C target +def instance1(): + buf = bytearray(b"--------") + i2c_target = I2CTarget(*i2c_args, **i2c_kwargs, addr=ADDR, mem=buf) + i2c_target.irq(simple_irq) + multitest.next() + for iteration in range(2): + print("target iteration", iteration) + multitest.broadcast("target stage 1") + multitest.wait("controller stage 2") + print(buf) + multitest.broadcast("target stage 3") + multitest.wait("controller stage 4") + i2c_target.deinit() + print("done") diff --git a/tests/multi_extmod/machine_i2c_target_memory.py.exp b/tests/multi_extmod/machine_i2c_target_memory.py.exp new file mode 100644 index 0000000000000..71386cfe769d9 --- /dev/null +++ b/tests/multi_extmod/machine_i2c_target_memory.py.exp @@ -0,0 +1,16 @@ +--- instance0 --- +controller iteration 0 +b'0123' +controller iteration 1 +b'0123' +done +--- instance1 --- +target iteration 0 +IRQ_END_WRITE 2 +bytearray(b'--0123--') +IRQ_END_READ 2 +target iteration 1 +IRQ_END_WRITE 3 +bytearray(b'--00123-') +IRQ_END_READ 3 +done From bf6f229cf3feb6767d37b9df9dcf1e2d9abc49bd Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 27 May 2025 15:55:46 +1000 Subject: [PATCH 059/143] docs/library: Document the new machine.I2CTarget class. With some working examples that show how to use all the features. Signed-off-by: Damien George --- docs/library/machine.I2CTarget.rst | 174 +++++++++++++++++++++++++++++ docs/library/machine.rst | 1 + 2 files changed, 175 insertions(+) create mode 100644 docs/library/machine.I2CTarget.rst diff --git a/docs/library/machine.I2CTarget.rst b/docs/library/machine.I2CTarget.rst new file mode 100644 index 0000000000000..0e4af84cb687c --- /dev/null +++ b/docs/library/machine.I2CTarget.rst @@ -0,0 +1,174 @@ +.. currentmodule:: machine +.. _machine.I2CTarget: + +class I2CTarget -- an I2C target device +======================================= + +An I2C target is a device which connects to an I2C bus and is controlled by an +I2C controller. I2C targets can take many forms. The :class:`machine.I2CTarget` +class implements an I2C target that can be configured as a memory/register device, +or as an arbitrary I2C device by using callbacks (if supported by the port). + +Example usage for the case of a memory device:: + + from machine import I2CTarget + + # Create the backing memory for the I2C target. + mem = bytearray(8) + + # Create an I2C target. Depending on the port, extra parameters + # may be required to select the peripheral and/or pins to use. + i2c = I2CTarget(addr=67, mem=mem) + + # At this point an I2C controller can read and write `mem`. + ... + + # Deinitialise the I2C target. + i2c.deinit() + +Note that some ports require an ``id``, and maybe ``scl`` and ``sda`` pins, to be +passed to the `I2CTarget` constructor, to select the hardware I2C instance and +pins that it connects to. + +When configured as a memory device, it's also possible to register to receive events. +For example to be notified when the memory is read/written:: + + from machine import I2CTarget + + # Define an IRQ handler, for I2C events. + def irq_handler(i2c_target): + flags = i2c_target.irq().flags() + if flags & I2CTarget.IRQ_END_READ: + print("controller read target at addr", i2c_target.memaddr) + if flags & I2CTarget.IRQ_END_WRITE: + print("controller wrote target at addr", i2c_target.memaddr) + + # Create the I2C target and register to receive default events. + mem = bytearray(8) + i2c = I2CTarget(addr=67, mem=mem) + i2c.irq(irq_handler) + +More complicated I2C devices can be implemented using the full set of events. For +example, to see the raw events as they are triggered:: + + from machine import I2CTarget + + # Define an IRQ handler that prints the event id and responds to reads/writes. + def irq_handler(i2c_target, buf=bytearray(1)): + flags = i2c_target.irq().flags() + print(flags) + if flags & I2CTarget.IRQ_READ_REQ: + i2c_target.write(buf) + if flags & I2CTarget.IRQ_WRITE_REQ: + i2c_target.readinto(buf) + + # Create the I2C target and register to receive all events. + i2c = I2CTarget(addr=67) + all_triggers = ( + I2CTarget.IRQ_ADDR_MATCH_READ + | I2CTarget.IRQ_ADDR_MATCH_WRITE + | I2CTarget.IRQ_READ_REQ + | I2CTarget.IRQ_WRITE_REQ + | I2CTarget.IRQ_END_READ + | I2CTarget.IRQ_END_WRITE + ) + i2c.irq(irq_handler, trigger=all_triggers, hard=True) + +Constructors +------------ + +.. class:: I2CTarget(id, addr, *, addrsize=7, mem=None, mem_addrsize=8, scl=None, sda=None) + + Construct and return a new I2CTarget object using the following parameters: + + - *id* identifies a particular I2C peripheral. Allowed values depend on the + particular port/board. Some ports have a default in which case this parameter + can be omitted. + - *addr* is the I2C address of the target. + - *addrsize* is the number of bits in the I2C target address. Valid values + are 7 and 10. + - *mem* is an object with the buffer protocol that is writable. If not + specified then there is no backing memory and data must be read/written + using the :meth:`I2CTarget.readinto` and :meth:`I2CTarget.write` methods. + - *mem_addrsize* is the number of bits in the memory address. Valid values + are 0, 8, 16, 24 and 32. + - *scl* is a pin object specifying the pin to use for SCL. + - *sda* is a pin object specifying the pin to use for SDA. + + Note that some ports/boards will have default values of *scl* and *sda* + that can be changed in this constructor. Others will have fixed values + of *scl* and *sda* that cannot be changed. + +General Methods +--------------- + +.. method:: I2CTarget.deinit() + + Deinitialise the I2C target. After this method is called the hardware will no + longer respond to requests on the I2C bus, and no other methods can be called. + +.. method:: I2CTarget.readinto(buf) + + Read into the given buffer any pending bytes written by the I2C controller. + Returns the number of bytes read. + +.. method:: I2CTarget.write(buf) + + Write out the bytes from the given buffer, to be passed to the I2C controller + after it sends a read request. Returns the number of bytes written. Most ports + only accept one byte at a time to this method. + +.. method:: I2CTarget.irq(handler=None, trigger=IRQ_END_READ|IRQ_END_WRITE, hard=False) + + Configure an IRQ *handler* to be called when an event occurs. The possible events are + given by the following constants, which can be or'd together and passed to the *trigger* + argument: + + - ``IRQ_ADDR_MATCH_READ`` indicates that the target was addressed by a + controller for a read transaction. + - ``IRQ_ADDR_MATCH_READ`` indicates that the target was addressed by a + controller for a write transaction. + - ``IRQ_READ_REQ`` indicates that the controller is requesting data, and this + request must be satisfied by calling `I2CTarget.write` with the data to be + passed back to the controller. + - ``IRQ_WRITE_REQ`` indicates that the controller has written data, and the + data must be read by calling `I2CTarget.readinto`. + - ``IRQ_END_READ`` indicates that the controller has finished a read transaction. + - ``IRQ_END_WRITE`` indicates that the controller has finished a write transaction. + + Not all triggers are available on all ports. If a port has the constant then that + event is available. + + Note the following restrictions: + + - ``IRQ_ADDR_MATCH_READ``, ``IRQ_ADDR_MATCH_READ``, ``IRQ_READ_REQ`` and + ``IRQ_WRITE_REQ`` must be handled by a hard IRQ callback (with the *hard* argument + set to ``True``). This is because these events have very strict timing requirements + and must usually be satisfied synchronously with the hardware event. + + - ``IRQ_END_READ`` and ``IRQ_END_WRITE`` may be handled by either a soft or hard + IRQ callback (although note that all events must be registered with the same handler, + so if any events need a hard callback then all events must be hard). + + - If a memory buffer has been supplied in the constructor then ``IRQ_END_WRITE`` + is not emitted for the transaction that writes the memory address. This is to + allow ``IRQ_END_READ`` and ``IRQ_END_WRITE`` to function correctly as soft IRQ + callbacks, where the IRQ handler may be called quite some time after the actual + hardware event. + +.. attribute:: I2CTarget.memaddr + + The integer value of the most recent memory address that was selected by the I2C + controller (only valid if ``mem`` was specified in the constructor). + +Constants +--------- + +.. data:: I2CTarget.IRQ_ADDR_MATCH_READ + I2CTarget.IRQ_ADDR_MATCH_WRITE + I2CTarget.IRQ_READ_REQ + I2CTarget.IRQ_WRITE_REQ + I2CTarget.IRQ_END_READ + I2CTarget.IRQ_END_WRITE + + IRQ trigger sources. diff --git a/docs/library/machine.rst b/docs/library/machine.rst index 76d111f11ef3d..3c22aa9af6524 100644 --- a/docs/library/machine.rst +++ b/docs/library/machine.rst @@ -264,6 +264,7 @@ Classes machine.UART.rst machine.SPI.rst machine.I2C.rst + machine.I2CTarget.rst machine.I2S.rst machine.RTC.rst machine.Timer.rst From c3f3339c87444acec814a150fdad28e650483405 Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Tue, 15 Aug 2023 13:03:45 +0100 Subject: [PATCH 060/143] esp32/modesp32: Add esp32.PCNT class. Add a new `esp32.PCNT` class that provides complete, low-level support to the ESP32 PCNT pulse counting hardware units. This can be used as a building block to implement the higher-level `machine.Counter` and `machine.Encoder` classes. This is enabled by default on all OG, S2, S3, C6 boards, but not on C3 (as the PCNT peripheral is not supported). Original implementation by: Jonathan Hogg Signed-off-by: Jim Mussared Signed-off-by: Angus Gratton --- ports/esp32/boards/sdkconfig.base | 1 + ports/esp32/esp32_common.cmake | 1 + ports/esp32/esp32_pcnt.c | 513 ++++++++++++++++++++++++++++++ ports/esp32/main.c | 5 + ports/esp32/modesp32.c | 3 + ports/esp32/modesp32.h | 6 + ports/esp32/mpconfigport.h | 4 + 7 files changed, 533 insertions(+) create mode 100644 ports/esp32/esp32_pcnt.c diff --git a/ports/esp32/boards/sdkconfig.base b/ports/esp32/boards/sdkconfig.base index 2f1835c9242d4..4bbccf77d17d7 100644 --- a/ports/esp32/boards/sdkconfig.base +++ b/ports/esp32/boards/sdkconfig.base @@ -117,6 +117,7 @@ CONFIG_ADC_CAL_LUT_ENABLE=y CONFIG_UART_ISR_IN_IRAM=y # IDF 5 deprecated +CONFIG_PCNT_SUPPRESS_DEPRECATE_WARN=y CONFIG_RMT_SUPPRESS_DEPRECATE_WARN=y CONFIG_ETH_USE_SPI_ETHERNET=y diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake index 09b120391305f..9e8acf8897c6a 100644 --- a/ports/esp32/esp32_common.cmake +++ b/ports/esp32/esp32_common.cmake @@ -127,6 +127,7 @@ list(APPEND MICROPY_SOURCE_PORT modesp.c esp32_nvs.c esp32_partition.c + esp32_pcnt.c esp32_rmt.c esp32_ulp.c modesp32.c diff --git a/ports/esp32/esp32_pcnt.c b/ports/esp32/esp32_pcnt.c new file mode 100644 index 0000000000000..36d43ab4559c6 --- /dev/null +++ b/ports/esp32/esp32_pcnt.c @@ -0,0 +1,513 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021-22 Jonathan Hogg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +#include "py/runtime.h" +#include "py/mphal.h" +#include "py/obj.h" + +#if MICROPY_PY_ESP32_PCNT + +#include "shared/runtime/mpirq.h" + +#include "modesp32.h" +#include "driver/pcnt.h" + +#if !MICROPY_ENABLE_FINALISER +#error "esp32.PCNT requires MICROPY_ENABLE_FINALISER." +#endif + +typedef struct _esp32_pcnt_irq_obj_t { + mp_irq_obj_t base; + uint32_t flags; + uint32_t trigger; +} esp32_pcnt_irq_obj_t; + +typedef struct _esp32_pcnt_obj_t { + mp_obj_base_t base; + pcnt_unit_t unit; + esp32_pcnt_irq_obj_t *irq; + struct _esp32_pcnt_obj_t *next; +} esp32_pcnt_obj_t; + +// Linked list of PCNT units. +MP_REGISTER_ROOT_POINTER(struct _esp32_pcnt_obj_t *esp32_pcnt_obj_head); + +// Once off installation of the PCNT ISR service (using the default service). +// Persists across soft reset. +static bool pcnt_isr_service_installed = false; + +static mp_obj_t esp32_pcnt_deinit(mp_obj_t self_in); + +void esp32_pcnt_deinit_all(void) { + esp32_pcnt_obj_t **pcnt = &MP_STATE_PORT(esp32_pcnt_obj_head); + while (*pcnt != NULL) { + esp32_pcnt_deinit(MP_OBJ_FROM_PTR(*pcnt)); + *pcnt = (*pcnt)->next; + } +} + +static void esp32_pcnt_init_helper(esp32_pcnt_obj_t *self, size_t n_pos_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { + ARG_channel, + ARG_pin, + ARG_rising, + ARG_falling, + ARG_mode_pin, + ARG_mode_low, + ARG_mode_high, + ARG_min, + ARG_max, + ARG_filter, + ARG_threshold0, + ARG_threshold1, + ARG_value, + }; + + static const mp_arg_t allowed_args[] = { + { MP_QSTR_channel, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + // Applies to the channel. + { MP_QSTR_pin, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_rising, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_falling, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_mode_pin, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_mode_low, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_mode_high, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + // Applies to the whole unit. + { MP_QSTR_min, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_max, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_filter, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_threshold0, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + { MP_QSTR_threshold1, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + // Implicitly zero if min, max, threshold0/1 are set. + { MP_QSTR_value, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_pos_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // The pin/mode_pin, rising, falling, mode_low, mode_high args all apply + // to the channel (defaults to channel zero). + mp_uint_t channel = args[ARG_channel].u_int; + if (channel >= PCNT_CHANNEL_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("channel")); + } + + if (args[ARG_pin].u_obj != MP_OBJ_NULL || args[ARG_mode_pin].u_obj != MP_OBJ_NULL) { + // If you set mode_pin, you must also set pin. + if (args[ARG_pin].u_obj == MP_OBJ_NULL) { + mp_raise_ValueError(MP_ERROR_TEXT("pin")); + } + + mp_hal_pin_obj_t pin = PCNT_PIN_NOT_USED; + mp_hal_pin_obj_t mode_pin = PCNT_PIN_NOT_USED; + + // Set to None to disable pin/mode_pin. + if (args[ARG_pin].u_obj != mp_const_none) { + pin = mp_hal_get_pin_obj(args[ARG_pin].u_obj); + } + if (args[ARG_mode_pin].u_obj != MP_OBJ_NULL && args[ARG_mode_pin].u_obj != mp_const_none) { + mode_pin = mp_hal_get_pin_obj(args[ARG_mode_pin].u_obj); + } + + pcnt_set_pin(self->unit, channel, pin, mode_pin); + } + + if ( + args[ARG_rising].u_obj != MP_OBJ_NULL || args[ARG_falling].u_obj != MP_OBJ_NULL || + args[ARG_mode_low].u_obj != MP_OBJ_NULL || args[ARG_mode_high].u_obj != MP_OBJ_NULL + ) { + mp_uint_t rising = args[ARG_rising].u_obj == MP_OBJ_NULL ? PCNT_COUNT_DIS : mp_obj_get_int(args[ARG_rising].u_obj); + mp_uint_t falling = args[ARG_falling].u_obj == MP_OBJ_NULL ? PCNT_COUNT_DIS : mp_obj_get_int(args[ARG_falling].u_obj); + mp_uint_t mode_low = args[ARG_mode_low].u_obj == MP_OBJ_NULL ? PCNT_MODE_KEEP : mp_obj_get_int(args[ARG_mode_low].u_obj); + mp_uint_t mode_high = args[ARG_mode_high].u_obj == MP_OBJ_NULL ? PCNT_MODE_KEEP : mp_obj_get_int(args[ARG_mode_high].u_obj); + if (rising >= PCNT_COUNT_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("rising")); + } + if (falling >= PCNT_COUNT_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("falling")); + } + if (mode_low >= PCNT_MODE_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("mode_low")); + } + if (mode_high >= PCNT_MODE_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("mode_high")); + } + pcnt_set_mode(self->unit, channel, rising, falling, mode_high, mode_low); + } + + // The rest of the arguments apply to the whole unit. + + if (args[ARG_filter].u_obj != MP_OBJ_NULL) { + mp_uint_t filter = mp_obj_get_int(args[ARG_filter].u_obj); + if (filter > 1023) { + mp_raise_ValueError(MP_ERROR_TEXT("filter")); + } + if (filter) { + check_esp_err(pcnt_set_filter_value(self->unit, filter)); + check_esp_err(pcnt_filter_enable(self->unit)); + } else { + check_esp_err(pcnt_filter_disable(self->unit)); + } + } + + bool clear = false; + if (args[ARG_value].u_obj != MP_OBJ_NULL) { + mp_int_t value = mp_obj_get_int(args[ARG_value].u_obj); + if (value != 0) { + mp_raise_ValueError(MP_ERROR_TEXT("value")); + } + clear = true; + } + + if (args[ARG_min].u_obj != MP_OBJ_NULL) { + mp_int_t minimum = mp_obj_get_int(args[ARG_min].u_obj); + if (minimum < -32768 || minimum > 0) { + mp_raise_ValueError(MP_ERROR_TEXT("minimum")); + } + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_L_LIM, minimum)); + clear = true; + } + + if (args[ARG_max].u_obj != MP_OBJ_NULL) { + mp_int_t maximum = mp_obj_get_int(args[ARG_max].u_obj); + if (maximum < 0 || maximum > 32767) { + mp_raise_ValueError(MP_ERROR_TEXT("maximum")); + } + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_H_LIM, maximum)); + clear = true; + } + + if (args[ARG_threshold0].u_obj != MP_OBJ_NULL) { + mp_int_t threshold0 = mp_obj_get_int(args[ARG_threshold0].u_obj); + if (threshold0 < -32768 || threshold0 > 32767) { + mp_raise_ValueError(MP_ERROR_TEXT("threshold0")); + } + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_THRES_0, threshold0)); + clear = true; + } + + if (args[ARG_threshold1].u_obj != MP_OBJ_NULL) { + mp_int_t threshold1 = mp_obj_get_int(args[ARG_threshold1].u_obj); + if (threshold1 < -32768 || threshold1 > 32767) { + mp_raise_ValueError(MP_ERROR_TEXT("threshold1")); + } + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_THRES_1, threshold1)); + clear = true; + } + + if (clear) { + check_esp_err(pcnt_counter_clear(self->unit)); + } +} + +// Disable any events, and remove the ISR handler for this unit. +static void esp32_pcnt_disable_events_for_unit(esp32_pcnt_obj_t *self) { + if (!self->irq) { + return; + } + + // Disable all possible events and remove the ISR. + for (pcnt_evt_type_t evt_type = PCNT_EVT_THRES_1; evt_type <= PCNT_EVT_ZERO; evt_type <<= 1) { + check_esp_err(pcnt_event_disable(self->unit, evt_type)); + } + check_esp_err(pcnt_isr_handler_remove(self->unit)); + + // Clear IRQ object state. + self->irq->base.handler = mp_const_none; + self->irq->trigger = 0; +} + +static mp_obj_t esp32_pcnt_make_new(const mp_obj_type_t *type, size_t n_pos_args, size_t n_kw_args, const mp_obj_t *args) { + if (n_pos_args < 1) { + mp_raise_TypeError(MP_ERROR_TEXT("id")); + } + + pcnt_unit_t unit = mp_obj_get_int(args[0]); + if (unit < 0 || unit >= PCNT_UNIT_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("invalid id")); + } + + // Try and find an existing instance for this unit. + esp32_pcnt_obj_t *self = MP_STATE_PORT(esp32_pcnt_obj_head); + while (self) { + if (self->unit == unit) { + break; + } + self = self->next; + } + + if (!self) { + // Unused unit, create a new esp32_pcnt_obj_t instance and put it at + // the head of the list. + self = mp_obj_malloc(esp32_pcnt_obj_t, &esp32_pcnt_type); + self->unit = unit; + self->irq = NULL; + self->next = MP_STATE_PORT(esp32_pcnt_obj_head); + MP_STATE_PORT(esp32_pcnt_obj_head) = self; + + // Ensure the unit is in a known (deactivated) state. + esp32_pcnt_deinit(MP_OBJ_FROM_PTR(self)); + } + + mp_map_t kw_args; + mp_map_init_fixed_table(&kw_args, n_kw_args, args + n_pos_args); + esp32_pcnt_init_helper(self, 0, args + n_pos_args, &kw_args); + + // Ensure the global PCNT ISR service is installed. + if (!pcnt_isr_service_installed) { + check_esp_err(pcnt_isr_service_install(ESP_INTR_FLAG_IRAM)); + pcnt_isr_service_installed = true; + } + + // And enable for this unit. + check_esp_err(pcnt_intr_enable(self->unit)); + + return MP_OBJ_FROM_PTR(self); +} + +static void esp32_pcnt_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "PCNT(%u)", self->unit); +} + +static mp_obj_t esp32_pcnt_init(size_t n_pos_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + esp32_pcnt_init_helper(self, n_pos_args - 1, pos_args + 1, kw_args); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(esp32_pcnt_init_obj, 1, esp32_pcnt_init); + +static mp_obj_t esp32_pcnt_deinit(mp_obj_t self_in) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + + // Remove IRQ and events. + esp32_pcnt_disable_events_for_unit(self); + + // Deactivate both channels. + pcnt_config_t channel_config = { + .unit = self->unit, + .pulse_gpio_num = PCNT_PIN_NOT_USED, + .pos_mode = PCNT_COUNT_DIS, + .neg_mode = PCNT_COUNT_DIS, + .ctrl_gpio_num = PCNT_PIN_NOT_USED, + .lctrl_mode = PCNT_MODE_KEEP, + .hctrl_mode = PCNT_MODE_KEEP, + .counter_l_lim = 0, + .counter_h_lim = 0, + }; + for (pcnt_channel_t channel = 0; channel <= 1; ++channel) { + channel_config.channel = channel; + check_esp_err(pcnt_unit_config(&channel_config)); + } + + // Disable filters & thresholds, pause & clear. + check_esp_err(pcnt_filter_disable(self->unit)); + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_THRES_0, 0)); + check_esp_err(pcnt_set_event_value(self->unit, PCNT_EVT_THRES_1, 0)); + check_esp_err(pcnt_counter_pause(self->unit)); + check_esp_err(pcnt_counter_clear(self->unit)); + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(esp32_pcnt_deinit_obj, esp32_pcnt_deinit); + +static mp_obj_t esp32_pcnt_value(size_t n_args, const mp_obj_t *pos_args) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + + // Optionally use pcnt.value(True) to clear the counter but only support a + // value of zero. Note: This can lead to skipped counts. + if (n_args == 2) { + if (mp_obj_get_int(pos_args[1]) != 0) { + mp_raise_ValueError(MP_ERROR_TEXT("value")); + } + } + + // This loop ensures that the caller's state (as inferred from IRQs, e.g. + // under/overflow) corresponds to the returned value, by synchronously + // flushing all pending IRQs. + int16_t value; + while (true) { + check_esp_err(pcnt_get_counter_value(self->unit, &value)); + if (self->irq && self->irq->flags && self->irq->base.handler != mp_const_none) { + // The handler must call irq.flags() to clear self->irq->base.flags, + // otherwise this will be an infinite loop. + mp_call_function_1(self->irq->base.handler, self->irq->base.parent); + } else { + break; + } + } + + if (n_args == 2) { + // Value was given, and we've already checked it was zero, so clear + // the counter. + check_esp_err(pcnt_counter_clear(self->unit)); + } + + return MP_OBJ_NEW_SMALL_INT(value); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp32_pcnt_value_obj, 1, 2, esp32_pcnt_value); + +static mp_uint_t esp32_pcnt_irq_trigger(mp_obj_t self_in, mp_uint_t new_trigger) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + self->irq->trigger = new_trigger; + for (pcnt_evt_type_t evt_type = PCNT_EVT_THRES_1; evt_type <= PCNT_EVT_ZERO; evt_type <<= 1) { + if (new_trigger & evt_type) { + pcnt_event_enable(self->unit, evt_type); + } else { + pcnt_event_disable(self->unit, evt_type); + } + } + return 0; +} + +static mp_uint_t esp32_pcnt_irq_info(mp_obj_t self_in, mp_uint_t info_type) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (info_type == MP_IRQ_INFO_FLAGS) { + // Atomically get-and-clear the flags. + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + mp_uint_t flags = self->irq->flags; + self->irq->flags = 0; + MICROPY_END_ATOMIC_SECTION(atomic_state); + return flags; + } else if (info_type == MP_IRQ_INFO_TRIGGERS) { + return self->irq->trigger; + } + return 0; +} + +static const mp_irq_methods_t esp32_pcnt_irq_methods = { + .trigger = esp32_pcnt_irq_trigger, + .info = esp32_pcnt_irq_info, +}; + +static IRAM_ATTR void esp32_pcnt_intr_handler(void *arg) { + esp32_pcnt_obj_t *self = (esp32_pcnt_obj_t *)arg; + pcnt_unit_t unit = self->unit; + uint32_t status; + pcnt_get_event_status(unit, &status); + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + self->irq->flags |= status; + MICROPY_END_ATOMIC_SECTION(atomic_state); + mp_irq_handler(&self->irq->base); +} + +static mp_obj_t esp32_pcnt_irq(size_t n_pos_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_handler, ARG_trigger }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_handler, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_trigger, MP_ARG_INT, {.u_int = PCNT_EVT_ZERO} }, + }; + + esp32_pcnt_obj_t *self = pos_args[0]; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_pos_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + if (!self->irq) { + // Create IRQ object if necessary. This instance persists across a + // de-init. + self->irq = mp_obj_malloc(esp32_pcnt_irq_obj_t, &mp_irq_type); + self->irq->base.methods = (mp_irq_methods_t *)&esp32_pcnt_irq_methods; + self->irq->base.parent = MP_OBJ_FROM_PTR(self); + self->irq->base.ishard = false; + self->irq->base.handler = mp_const_none; + self->irq->trigger = 0; + } + + if (n_pos_args > 1 || kw_args->used != 0) { + // Update IRQ data. + + mp_obj_t handler = args[ARG_handler].u_obj; + mp_uint_t trigger = args[ARG_trigger].u_int; + + if (trigger < PCNT_EVT_THRES_1 || trigger >= (PCNT_EVT_ZERO << 1)) { + mp_raise_ValueError(MP_ERROR_TEXT("trigger")); + } + + if (handler != mp_const_none) { + self->irq->base.handler = handler; + self->irq->trigger = trigger; + pcnt_isr_handler_add(self->unit, esp32_pcnt_intr_handler, (void *)self); + esp32_pcnt_irq_trigger(MP_OBJ_FROM_PTR(self), trigger); + } else { + // Remove the ISR, disable all events, clear the IRQ object state. + esp32_pcnt_disable_events_for_unit(self); + } + } + + return MP_OBJ_FROM_PTR(self->irq); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(esp32_pcnt_irq_obj, 1, esp32_pcnt_irq); + +static mp_obj_t esp32_pcnt_start(mp_obj_t self_in) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_esp_err(pcnt_counter_resume(self->unit)); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(esp32_pcnt_start_obj, esp32_pcnt_start); + +static mp_obj_t esp32_pcnt_stop(mp_obj_t self_in) { + esp32_pcnt_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_esp_err(pcnt_counter_pause(self->unit)); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(esp32_pcnt_stop_obj, esp32_pcnt_stop); + +static const mp_rom_map_elem_t esp32_pcnt_locals_dict_table[] = { + // Methods + { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&esp32_pcnt_init_obj) }, + { MP_ROM_QSTR(MP_QSTR_value), MP_ROM_PTR(&esp32_pcnt_value_obj) }, + { MP_ROM_QSTR(MP_QSTR_irq), MP_ROM_PTR(&esp32_pcnt_irq_obj) }, + { MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(&esp32_pcnt_start_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&esp32_pcnt_stop_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&esp32_pcnt_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&esp32_pcnt_deinit_obj) }, + + // Constants + { MP_ROM_QSTR(MP_QSTR_IGNORE), MP_ROM_INT(PCNT_COUNT_DIS) }, + { MP_ROM_QSTR(MP_QSTR_INCREMENT), MP_ROM_INT(PCNT_COUNT_INC) }, + { MP_ROM_QSTR(MP_QSTR_DECREMENT), MP_ROM_INT(PCNT_COUNT_DEC) }, + { MP_ROM_QSTR(MP_QSTR_NORMAL), MP_ROM_INT(PCNT_MODE_KEEP) }, + { MP_ROM_QSTR(MP_QSTR_REVERSE), MP_ROM_INT(PCNT_MODE_REVERSE) }, + { MP_ROM_QSTR(MP_QSTR_HOLD), MP_ROM_INT(PCNT_MODE_DISABLE) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_ZERO), MP_ROM_INT(PCNT_EVT_ZERO) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_THRESHOLD0), MP_ROM_INT(PCNT_EVT_THRES_0) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_THRESHOLD1), MP_ROM_INT(PCNT_EVT_THRES_1) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_MIN), MP_ROM_INT(PCNT_EVT_L_LIM) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_MAX), MP_ROM_INT(PCNT_EVT_H_LIM) }, +}; +static MP_DEFINE_CONST_DICT(esp32_pcnt_locals_dict, esp32_pcnt_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + esp32_pcnt_type, + MP_QSTR_PCNT, + MP_TYPE_FLAG_NONE, + make_new, esp32_pcnt_make_new, + print, esp32_pcnt_print, + locals_dict, &esp32_pcnt_locals_dict + ); + +#endif // MICROPY_PY_ESP32_PCNT diff --git a/ports/esp32/main.c b/ports/esp32/main.c index f85fb6c084f90..1523e07f916d6 100644 --- a/ports/esp32/main.c +++ b/ports/esp32/main.c @@ -61,6 +61,7 @@ #include "uart.h" #include "usb.h" #include "usb_serial_jtag.h" +#include "modesp32.h" #include "modmachine.h" #include "modnetwork.h" @@ -181,6 +182,10 @@ void mp_task(void *pvParameter) { machine_timer_deinit_all(); + #if MICROPY_PY_ESP32_PCNT + esp32_pcnt_deinit_all(); + #endif + #if MICROPY_PY_THREAD mp_thread_deinit(); #endif diff --git a/ports/esp32/modesp32.c b/ports/esp32/modesp32.c index bf7aec3944228..858be2ed05f81 100644 --- a/ports/esp32/modesp32.c +++ b/ports/esp32/modesp32.c @@ -299,6 +299,9 @@ static const mp_rom_map_elem_t esp32_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_NVS), MP_ROM_PTR(&esp32_nvs_type) }, { MP_ROM_QSTR(MP_QSTR_Partition), MP_ROM_PTR(&esp32_partition_type) }, + #if MICROPY_PY_ESP32_PCNT + { MP_ROM_QSTR(MP_QSTR_PCNT), MP_ROM_PTR(&esp32_pcnt_type) }, + #endif #if SOC_RMT_SUPPORTED { MP_ROM_QSTR(MP_QSTR_RMT), MP_ROM_PTR(&esp32_rmt_type) }, #endif diff --git a/ports/esp32/modesp32.h b/ports/esp32/modesp32.h index a685b7b38fe6f..81ab94dc633ae 100644 --- a/ports/esp32/modesp32.h +++ b/ports/esp32/modesp32.h @@ -66,6 +66,12 @@ extern const mp_obj_type_t esp32_partition_type; extern const mp_obj_type_t esp32_rmt_type; extern const mp_obj_type_t esp32_ulp_type; +#if MICROPY_PY_ESP32_PCNT +extern const mp_obj_type_t esp32_pcnt_type; + +void esp32_pcnt_deinit_all(void); +#endif + esp_err_t rmt_driver_install_core1(uint8_t channel_id); #endif // MICROPY_INCLUDED_ESP32_MODESP32_H diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index 1844018030b8b..48ad39ef7ed17 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -70,6 +70,7 @@ #define MICROPY_USE_INTERNAL_ERRNO (0) // errno.h from xtensa-esp32-elf/sys-include/sys #define MICROPY_USE_INTERNAL_PRINTF (0) // ESP32 SDK requires its own printf #define MICROPY_SCHEDULER_DEPTH (8) +#define MICROPY_SCHEDULER_STATIC_NODES (1) #define MICROPY_VFS (1) // control over Python builtins @@ -194,6 +195,9 @@ #define MICROPY_PY_ONEWIRE (1) #define MICROPY_PY_SOCKET_EVENTS (MICROPY_PY_WEBREPL) #define MICROPY_PY_BLUETOOTH_RANDOM_ADDR (1) +#ifndef MICROPY_PY_ESP32_PCNT +#define MICROPY_PY_ESP32_PCNT (SOC_PCNT_SUPPORTED) +#endif // fatfs configuration #define MICROPY_FATFS_ENABLE_LFN (1) From e54553c496bf915ed0263c2d2a61d31d61032dea Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Sat, 10 Sep 2022 17:08:27 +0100 Subject: [PATCH 061/143] docs/esp32: Add documentation for esp32.PCNT. Document the new `esp32.PCNT` class for hardware pulse counting. Originally authored by: Jonathan Hogg Signed-off-by: Jim Mussared --- docs/esp32/quickref.rst | 21 ++++++ docs/library/esp32.rst | 142 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/docs/esp32/quickref.rst b/docs/esp32/quickref.rst index 49f546a17c03f..7127cc402bd12 100644 --- a/docs/esp32/quickref.rst +++ b/docs/esp32/quickref.rst @@ -566,6 +566,27 @@ ESP32 S2: Provided to deinit the adc driver. +Pulse Counter (pin pulse/edge counting) +--------------------------------------- + +The ESP32 provides up to 8 pulse counter peripherals depending on the hardware, +with id 0..7. These can be configured to count rising and/or falling edges on +any input pin. + +Use the :ref:`esp32.PCNT ` class:: + + from machine import Pin + from esp32 import PCNT + + counter = PCNT(0, pin=Pin(2), rising=PCNT.INCREMENT) # create counter + counter.start() # start counter + count = counter.value() # read count, -32768..32767 + counter.value(0) # reset counter + count = counter.value(0) # read and reset + +The PCNT hardware supports monitoring multiple pins in a single unit to +implement quadrature decoding or up/down signal counters. + Software SPI bus ---------------- diff --git a/docs/library/esp32.rst b/docs/library/esp32.rst index 4be6dc2671c52..0cf0ebc62636d 100644 --- a/docs/library/esp32.rst +++ b/docs/library/esp32.rst @@ -195,6 +195,148 @@ Constants Used in `idf_heap_info`. + +.. _esp32.PCNT: + +PCNT +---- + +This class provides access to the ESP32 hardware support for pulse counting. +There are 8 pulse counter units, with id 0..7. + +See the :ref:`machine.Counter ` and +:ref:`machine.Encoder ` classes for simpler and portable +abstractions of common pulse counting applications. These classes are +implemented as thin Python shims around :class:`PCNT`. + +.. class:: PCNT(id, *, ...) + + Returns the singleton PCNT instance for the given unit ``id``. + + Keyword arguments are passed to the ``init()`` method as described + below. + +.. method:: PCNT.init(*, ...) + + (Re-)initialise a pulse counter unit. Supported keyword arguments are: + + - ``channel``: see description below + - ``pin``: the input Pin to monitor for pulses + - ``rising``: an action to take on a rising edge - one of + ``PCNT.INCREMENT``, ``PCNT.DECREMENT`` or ``PCNT.IGNORE`` (the default) + - ``falling``: an action to take on a falling edge (takes the save values + as the ``rising`` argument). + - ``mode_pin``: ESP32 pulse counters support monitoring a second pin and + altering the behaviour of the counter based on its level - set this + keyword to any input Pin + - ``mode_low``: set to either ``PCNT.HOLD`` or ``PCNT.REVERSE`` to + either suspend counting or reverse the direction of the counter (i.e., + ``PCNT.INCREMENT`` behaves as ``PCNT.DECREMENT`` and vice versa) + when ``mode_pin`` is low + - ``mode_high``: as ``mode_low`` but for the behaviour when ``mode_pin`` + is high + - ``filter``: set to a value 1..1023, in ticks of the 80MHz clock, to + enable the pulse width filter + - ``min``: set to the minimum level of the counter value when + decrementing (-32768..-1) or 0 to disable + - ``max``: set to the maximum level of the counter value when + incrementing (1..32767) or 0 to disable + - ``threshold0``: sets the counter value for the + ``PCNT.IRQ_THRESHOLD0`` event (see ``irq`` method) + - ``threshold1``: sets the counter value for the + ``PCNT.IRQ_THRESHOLD1`` event (see ``irq`` method) + - ``value``: can be set to ``0`` to reset the counter value + + The hardware initialisation is done in stages and so some of the keyword + arguments can be used in groups or in isolation to partially reconfigure a + unit: + + - the ``pin`` keyword (optionally combined with ``mode_pin``) can be used + to change just the bound pin(s) + - ``rising``, ``falling``, ``mode_low`` and ``mode_high`` can be used + (singly or together) to change the counting logic - omitted keywords + use their default (``PCNT.IGNORE`` or ``PCNT.NORMAL``) + - ``filter`` can be used to change only the pulse width filter (with 0 + disabling it) + - each of ``min``, ``max``, ``threshold0`` and ``threshold1`` can + be used to change these limit/event values individually; however, + setting any will reset the counter to zero (i.e., they imply + ``value=0``) + + Each pulse counter unit supports two channels, 0 and 1, each able to + monitor different pins with different counting logic but updating the same + counter value. Use ``channel=1`` with the ``pin``, ``rising``, ``falling``, + ``mode_pin``, ``mode_low`` and ``mode_high`` keywords to configure the + second channel. + + The second channel can be used to configure 4X quadrature decoding with a + single counter unit:: + + pin_a = Pin(2, Pin.INPUT, pull=Pin.PULL_UP) + pin_b = Pin(3, Pin.INPUT, pull=Pin.PULL_UP) + rotary = PCNT(0, min=-32000, max=32000) + rotary.init(channel=0, pin=pin_a, falling=PCNT.INCREMENT, rising=PCNT.DECREMENT, mode_pin=pin_b, mode_low=PCNT.REVERSE) + rotary.init(channel=1, pin=pin_b, falling=PCNT.DECREMENT, rising=PCNT.INCREMENT, mode_pin=pin_a, mode_low=PCNT.REVERSE) + rotary.start() + +.. method:: PCNT.value([value]) + + Call this method with no arguments to return the current counter value. + + If the optional *value* argument is set to ``0`` then the counter is + reset (but the previous value is returned). Read and reset is not atomic and + so it is possible for a pulse to be missed. Any value other than ``0`` will + raise an error. + +.. method:: PCNT.irq(handler=None, trigger=PCNT.IRQ_ZERO) + + ESP32 pulse counters support interrupts on these counter events: + + - ``PCNT.IRQ_ZERO``: the counter has reset to zero + - ``PCNT.IRQ_MIN``: the counter has hit the ``min`` value + - ``PCNT.IRQ_MAX``: the counter has hit the ``max`` value + - ``PCNT.IRQ_THRESHOLD0``: the counter has hit the ``threshold0`` value + - ``PCNT.IRQ_THRESHOLD1``: the counter has hit the ``threshold1`` value + + ``trigger`` should be a bit-mask of the desired events OR'ed together. The + ``handler`` function should take a single argument which is the + :class:`PCNT` instance that raised the event. + + This method returns a callback object. The callback object can be used to + access the bit-mask of events that are outstanding on the PCNT unit.:: + + def pcnt_irq(pcnt): + flags = pcnt.irq().flags() + if flags & PCNT.IRQ_ZERO: + # reset + if flags & PCNT.IRQ_MAX: + # overflow... + ... etc + + pcnt.irq(handler=pcnt_irq, trigger=PCNT.IRQ_ZERO | PCNT.IRQ_MAX | ...) + + **Note:** Accessing ``irq.flags()`` will clear the flags, so only call it + once per invocation of the handler. + + The handler is called with the MicroPython scheduler and so will run at a + point after the interrupt. If another interrupt occurs before the handler + has been called then the events will be coalesced together into a single + call and the bit mask will indicate all events that have occurred. + + To avoid race conditions between a handler being called and retrieving the + current counter value, the ``value()`` method will force execution of any + pending events before returning the current counter value (and potentially + resetting the value). + + Only one handler can be in place per-unit. Set ``handler`` to ``None`` to + disable the event interrupt. + +.. Note:: + ESP32 pulse counters reset to *zero* when reaching the minimum or maximum + value. Thus the ``IRQ_ZERO`` event will also trigger when either of these + events occurs. + + .. _esp32.RMT: RMT From 327655905e9f523070301f2f35459197d46db4fb Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Sat, 10 Sep 2022 16:47:19 +0100 Subject: [PATCH 062/143] esp32/modules/machine.py: Add Counter and Encoder classes. Adds a Python override of the `machine` module, which delegates to the built-in module and adds an implementation of `Counter` and `Encoder`, based on the `esp32.PCNT` class. Original implementation by: Jonathan Hogg Signed-off-by: Jim Mussared --- ports/esp32/modules/machine.py | 192 +++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 ports/esp32/modules/machine.py diff --git a/ports/esp32/modules/machine.py b/ports/esp32/modules/machine.py new file mode 100644 index 0000000000000..9cfda12f17753 --- /dev/null +++ b/ports/esp32/modules/machine.py @@ -0,0 +1,192 @@ +import sys + +_path = sys.path +sys.path = () +try: + import machine as _machine +finally: + sys.path = _path + del _path + del sys + + +from micropython import const +import esp32 + +if hasattr(esp32, "PCNT"): + _PCNT_RANGE = const(32000) + + class _CounterBase: + _PCNT = esp32.PCNT + # Singletons, keyed by PCNT unit_id (shared by both Counter & Encoder). + _INSTANCES = {} + + # Use __new__ to implement a singleton rather than a factory function, + # because we need to be able to provide class attributes, e.g. + # Counter.RISING, which is not possible if Counter was a function + # (functions cannot have attributes in MicroPython). + def __new__(cls, unit_id, *_args, **_kwargs): + # Find an existing instance for this PCNT unit id. + self = cls._INSTANCES.get(unit_id) + + if self: + # Verify that this PCNT is being used for the same type + # (Encoder or Counter). + if not isinstance(self, cls): + raise ValueError("PCNT in use") + else: + # Previously unused PCNT unit. + self = object.__new__(cls) + cls._INSTANCES[unit_id] = self + + # __init__ will now be called with the same args. + return self + + def __init__(self, unit_id, *args, filter_ns=0, **kwargs): + self._unit_id = unit_id + + if not hasattr(self, "_pcnt"): + # New instance, or previously deinit-ed. + self._pcnt = self._PCNT(unit_id, min=-_PCNT_RANGE, max=_PCNT_RANGE) + elif not (args or kwargs): + # Existing instance, and no args, so accessing the existing + # singleton without reconfiguring. Note: This means that + # Counter/Encoder cannot be partially re-initalised. Either + # you get the existing instance as-is (by passing no arguments + # other than the id), or you must pass all the necessary + # arguments to additionally re-configure it. + return + + # Counter- or Encoder-specific configuration of self._pcnt. + self._configure(*args, **kwargs) + + # Common unit configuration. + self._pcnt.init( + filter=min(max(0, filter_ns * 80 // 1000), 1023), + value=0, + ) + + # Note: We track number-of-overflows rather than the actual count in + # order to avoid the IRQ handler overflowing MicroPython's "small int" + # range. This gives an effective range of 2**30 overflows. User code + # should use counter.value(0) to reset the overflow count. + # The ESP32 PCNT resets to zero on under/overflow (i.e. it does not wrap + # around to the opposite limit), so each overflow corresponds to exactly + # _PCNT_RANGE counts. + + # Reset counter state. + self._overflows = 0 + self._offset = 0 + + # Install IRQ handler to handle under/overflow. + self._pcnt.irq(self._overflow, self._PCNT.IRQ_MIN | self._PCNT.IRQ_MAX) + + # Start counting. + self._pcnt.start() + + # Handle counter under/overflow. + def _overflow(self, pcnt): + mask = pcnt.irq().flags() + if mask & self._PCNT.IRQ_MIN: + self._overflows -= 1 + elif mask & self._PCNT.IRQ_MAX: + self._overflows += 1 + + # Public machine.Counter & machine.Encoder API. + def init(self, *args, **kwargs): + self.__init__(self._unit_id, *args, **kwargs) + + # Public machine.Counter & machine.Encoder API. + def deinit(self): + if hasattr(self, "_pcnt"): + self._pcnt.deinit() + del self._pcnt + + # Public machine.Counter & machine.Encoder API. + def value(self, value=None): + if not hasattr(self, "_pcnt"): + raise RuntimeError("not initialised") + + # This loop deals with the possibility that a PCNT overflow occurs + # between retrieving self._overflows and self._pcnt.value(). + while True: + overflows = self._overflows + current = self._pcnt.value() + # Calling PCNT.value() forces any pending interrupts to run + # for this PCNT unit. So self._overflows must now be the the + # value corresponding to the value we read. + if self._overflows == overflows: + break + + # Compute the result including the number of times we've cycled + # through the range, and any applied offset. + result = overflows * _PCNT_RANGE + current + self._offset + + # If a new value is specified, then zero out the overflows, and set + # self._offset so that it zeros out the current PCNT value. The + # mutation to self._overflows is atomic w.r.t. the overflow IRQ + # handler because the scheduler only runs on branch instructions. + if value is not None: + self._overflows -= overflows + self._offset = value - current + + return result + + class Counter(_CounterBase): + # Public machine.Counter API. + RISING = 1 + FALLING = 2 + UP = _CounterBase._PCNT.INCREMENT + DOWN = _CounterBase._PCNT.DECREMENT + + # Counter-specific configuration. + def _configure(self, src, edge=RISING, direction=UP): + # Only use the first channel. + self._pcnt.init( + channel=0, + pin=src, + rising=direction if edge & Counter.RISING else self._PCNT.IGNORE, + falling=direction if edge & Counter.FALLING else self._PCNT.IGNORE, + ) + + class Encoder(_CounterBase): + # Encoder-specific configuration. + def _configure(self, phase_a, phase_b, phases=1): + if phases not in (1, 2, 4): + raise ValueError("phases") + # Configure the first channel. + self._pcnt.init( + channel=0, + pin=phase_a, + falling=self._PCNT.INCREMENT, + rising=self._PCNT.DECREMENT, + mode_pin=phase_b, + mode_low=self._PCNT.HOLD if phases == 1 else self._PCNT.REVERSE, + ) + if phases == 4: + # For 4x quadrature, enable the second channel. + self._pcnt.init( + channel=1, + pin=phase_b, + falling=self._PCNT.DECREMENT, + rising=self._PCNT.INCREMENT, + mode_pin=phase_a, + mode_low=self._PCNT.REVERSE, + ) + else: + # For 1x and 2x quadrature, disable the second channel. + self._pcnt.init(channel=1, pin=None, rising=self._PCNT.IGNORE) + self._phases = phases + + def phases(self): + return self._phases + + del _CounterBase + + +del esp32 + + +# Delegate to built-in machine module. +def __getattr__(attr): + return getattr(_machine, attr) From 641ca2eb0624fc0df2b358f1a4652525af123fd7 Mon Sep 17 00:00:00 2001 From: Jonathan Hogg Date: Mon, 12 Sep 2022 18:37:22 +0100 Subject: [PATCH 063/143] docs/library/machine: Add docs for Counter and Encoder. Add documentation for `machine.Counter` and `machine.Encoder` as currently implemented by the esp32 port, but intended to be implemented by other ports. Originally authored by: Ihor Nehrutsa and Jonathan Hogg . Signed-off-by: Jim Mussared --- docs/esp32/quickref.rst | 15 ++++++ docs/library/esp32.rst | 3 ++ docs/library/machine.Counter.rst | 93 ++++++++++++++++++++++++++++++++ docs/library/machine.Encoder.rst | 72 +++++++++++++++++++++++++ docs/library/machine.rst | 2 + docs/reference/glossary.rst | 7 +++ 6 files changed, 192 insertions(+) create mode 100644 docs/library/machine.Counter.rst create mode 100644 docs/library/machine.Encoder.rst diff --git a/docs/esp32/quickref.rst b/docs/esp32/quickref.rst index 7127cc402bd12..2c667a0f014db 100644 --- a/docs/esp32/quickref.rst +++ b/docs/esp32/quickref.rst @@ -587,6 +587,21 @@ Use the :ref:`esp32.PCNT ` class:: The PCNT hardware supports monitoring multiple pins in a single unit to implement quadrature decoding or up/down signal counters. +See the :ref:`machine.Counter ` and +:ref:`machine.Encoder ` classes for simpler abstractions of +common pulse counting applications:: + + from machine import Pin, Counter + + counter = Counter(0, Pin(2)) # create a counter as above and start it + count = counter.value() # read the count as an arbitrary precision signed integer + + encoder = Encoder(0, Pin(12), Pin(14)) # create an encoder and begin counting + count = encoder.value() # read the count as an arbitrary precision signed integer + +Note that the id passed to these ``Counter()`` and ``Encoder()`` objects must be +a PCNT id. + Software SPI bus ---------------- diff --git a/docs/library/esp32.rst b/docs/library/esp32.rst index 0cf0ebc62636d..e5f39c7f59aa1 100644 --- a/docs/library/esp32.rst +++ b/docs/library/esp32.rst @@ -336,6 +336,9 @@ implemented as thin Python shims around :class:`PCNT`. value. Thus the ``IRQ_ZERO`` event will also trigger when either of these events occurs. +See the :ref:`machine.Counter ` and +:ref:`machine.Encoder ` classes for simpler abstractions of +common pulse counting applications. .. _esp32.RMT: diff --git a/docs/library/machine.Counter.rst b/docs/library/machine.Counter.rst new file mode 100644 index 0000000000000..f89a6d5b4f17a --- /dev/null +++ b/docs/library/machine.Counter.rst @@ -0,0 +1,93 @@ +.. currentmodule:: machine +.. _machine.Counter: + +class Counter -- pulse counter +============================== + +Counter implements pulse counting by monitoring an input signal and counting +rising or falling edges. + +Minimal example usage:: + + from machine import Pin, Counter + + counter = Counter(0, Pin(0, Pin.IN)) # create Counter for pin 0 and begin counting + value = counter.value() # retrieve current pulse count + +Availability: **ESP32** + +Constructors +------------ + +.. class:: Counter(id, ...) + + Returns the singleton Counter object for the the given *id*. Values of *id* + depend on a particular port and its hardware. Values 0, 1, etc. are commonly + used to select hardware block #0, #1, etc. + + Additional arguments are passed to the :meth:`init` method described below, + and will cause the Counter instance to be re-initialised and reset. + + On ESP32, the *id* corresponds to a :ref:`PCNT unit `. + +Methods +------- + +.. method:: Counter.init(src, *, ...) + + Initialise and reset the Counter with the given parameters: + + - *src* specifies the input pin as a :ref:`machine.Pin ` object. + May be omitted on ports that have a predefined pin for a given hardware + block. + + Additional keyword-only parameters that may be supported by a port are: + + - *edge* specifies the edge to count. Either ``Counter.RISING`` (the default) + or ``Counter.FALLING``. *(Supported on ESP32)* + + - *direction* specifies the direction to count. Either ``Counter.UP`` (the + default) or ``Counter.DOWN``. *(Supported on ESP32)* + + - *filter_ns* specifies a minimum period of time in nanoseconds that the + source signal needs to be stable for a pulse to be counted. Implementations + should use the longest filter supported by the hardware that is less than + or equal to this value. The default is 0 (no filter). *(Supported on ESP32)* + +.. method:: Counter.deinit() + + Stops the Counter, disabling any interrupts and releasing hardware resources. + A Soft Reset should deinitialize all Counter objects. + +.. method:: Counter.value([value]) + + Get, and optionally set, the counter value as a signed integer. + Implementations must aim to do the get and set atomically (i.e. without + leading to skipped counts). + + This counter value could exceed the range of a :term:`small integer`, which + means that calling :meth:`Counter.value` could cause a heap allocation, but + implementations should aim to ensure that internal state only uses small + integers and therefore will not allocate until the user calls + :meth:`Counter.value`. + + For example, on ESP32, the internal state counts overflows of the hardware + counter (every 32000 counts), which means that it will not exceed the small + integer range until ``2**30 * 32000`` counts (slightly over 1 year at 1MHz). + + In general, it is recommended that you should use ``Counter.value(0)`` to reset + the counter (i.e. to measure the counts since the last call), and this will + avoid this problem. + +Constants +--------- + +.. data:: Counter.RISING + Counter.FALLING + + Select the pulse edge. + +.. data:: Counter.UP + Counter.DOWN + + Select the counting direction. diff --git a/docs/library/machine.Encoder.rst b/docs/library/machine.Encoder.rst new file mode 100644 index 0000000000000..fc2de32084867 --- /dev/null +++ b/docs/library/machine.Encoder.rst @@ -0,0 +1,72 @@ +.. currentmodule:: machine +.. _machine.Encoder: + +class Encoder -- quadrature decoding +==================================== + +Encoder implements decoding of quadrature signals as commonly output from +rotary encoders, by counting either up or down depending on the order of two +input pulses. + +Minimal example usage:: + + from machine import Pin, Encoder + + counter = Counter(0, Pin(0, Pin.IN), Pin(1, Pin.IN)) # create Encoder for pins 0, 1 and begin counting + value = counter.value() # retrieve current count + +Availability: **ESP32** + +Constructors +------------ + +.. class:: Encoder(id, ...) + + Returns the singleton Encoder object for the the given *id*. Values of *id* + depend on a particular port and its hardware. Values 0, 1, etc. are commonly + used to select hardware block #0, #1, etc. + + Additional arguments are passed to the :meth:`init` method described below, + and will cause the Encoder instance to be re-initialised and reset. + + On ESP32, the *id* corresponds to a :ref:`PCNT unit `. + +Methods +------- + +.. method:: Encoder.init(phase_a, phase_b, *, ...) + + Initialise and reset the Encoder with the given parameters: + + - *phase_a* specifies the first input pin as a + :ref:`machine.Pin ` object. + + - *phase_b* specifies the second input pin as a + :ref:`machine.Pin ` object. + + These pins may be omitted on ports that have predefined pins for a given + hardware block. + + Additional keyword-only parameters that may be supported by a port are: + + - *filter_ns* specifies a minimum period of time in nanoseconds that the + source signal needs to be stable for a pulse to be counted. Implementations + should use the longest filter supported by the hardware that is less than + or equal to this value. The default is 0 (no filter). *(Supported on ESP32)* + + - *phases* specifies the number of signal edges to count and thus the + granularity of the decoding. e.g. 4 phases corresponds to "4x quadrature + decoding", and will result in four counts per pulse. Ports may support + either 1, 2, or 4 phases and the default is 1 phase. *(Supported on ESP32)* + +.. method:: Encoder.deinit() + + Stops the Encoder, disabling any interrupts and releasing hardware resources. + A Soft Reset should deinitialize all Encoder objects. + +.. method:: Encoder.value([value]) + + Get, and optionally set, the encoder value as a signed integer. + Implementations should aim to do the get and set atomically. + + See :meth:`machine.Counter.value` for details about overflow of this value. diff --git a/docs/library/machine.rst b/docs/library/machine.rst index 3c22aa9af6524..7acaddde815bc 100644 --- a/docs/library/machine.rst +++ b/docs/library/machine.rst @@ -268,6 +268,8 @@ Classes machine.I2S.rst machine.RTC.rst machine.Timer.rst + machine.Counter.rst + machine.Encoder.rst machine.WDT.rst machine.SD.rst machine.SDCard.rst diff --git a/docs/reference/glossary.rst b/docs/reference/glossary.rst index efaa8f607f72a..ab0b22049bd67 100644 --- a/docs/reference/glossary.rst +++ b/docs/reference/glossary.rst @@ -188,6 +188,13 @@ Glossary Most MicroPython boards make a REPL available over a UART, and this is typically accessible on a host PC via USB. + small integer + MicroPython optimises the internal representation of integers such that + "small" values do not take up space on the heap, and calculations with + them do not require heap allocation. On most 32-bit ports, this + corresponds to values in the interval ``-2**30 <= x < 2**30``, but this + should be considered an implementation detail and not relied upon. + stream Also known as a "file-like object". A Python object which provides sequential read-write access to the underlying data. A stream object From 907c5e99769635678e4bfb8dda5c3c0e2aec040f Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 30 Jul 2025 17:20:36 +1000 Subject: [PATCH 064/143] tests/extmod_hardware: Add basic tests for machine.Counter and Encoder. These don't test any advanced features, just the basics. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- tests/extmod_hardware/machine_counter.py | 90 +++++++++++++++++++++ tests/extmod_hardware/machine_encoder.py | 99 ++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 tests/extmod_hardware/machine_counter.py create mode 100644 tests/extmod_hardware/machine_encoder.py diff --git a/tests/extmod_hardware/machine_counter.py b/tests/extmod_hardware/machine_counter.py new file mode 100644 index 0000000000000..62ac1fed47ce7 --- /dev/null +++ b/tests/extmod_hardware/machine_counter.py @@ -0,0 +1,90 @@ +# Test machine.Counter implementation +# +# IMPORTANT: This test requires hardware connections: the out_pin and in_pin +# must be wired together. + +try: + from machine import Counter +except ImportError: + print("SKIP") + raise SystemExit + +import sys +from machine import Pin + +if "esp32" in sys.platform: + id = 0 + out_pin = 4 + in_pin = 5 +else: + print("Please add support for this test on this platform.") + raise SystemExit + +import unittest + +out_pin = Pin(out_pin, mode=Pin.OUT) +in_pin = Pin(in_pin, mode=Pin.IN) + + +def toggle(times): + for _ in range(times): + out_pin(1) + out_pin(0) + + +class TestCounter(unittest.TestCase): + def setUp(self): + out_pin(0) + self.counter = Counter(id, in_pin) + + def tearDown(self): + self.counter.deinit() + + def assertCounter(self, value): + self.assertEqual(self.counter.value(), value) + + def test_connections(self): + # Test the hardware connections are correct. If this test fails, all tests will fail. + out_pin(1) + self.assertEqual(1, in_pin()) + out_pin(0) + self.assertEqual(0, in_pin()) + + def test_count_rising(self): + self.assertCounter(0) + toggle(100) + self.assertCounter(100) + out_pin(1) + self.assertEqual(self.counter.value(0), 101) + self.assertCounter(0) # calling value(0) resets + out_pin(0) + self.assertCounter(0) # no rising edge + out_pin(1) + self.assertCounter(1) + + def test_change_directions(self): + self.assertCounter(0) + toggle(100) + self.assertCounter(100) + self.counter.init(in_pin, direction=Counter.DOWN) + self.assertCounter(0) # calling init() zeroes the counter + self.counter.value(100) # need to manually reset the value + self.assertCounter(100) + toggle(25) + self.assertCounter(75) + + def test_count_falling(self): + self.counter.init(in_pin, direction=Counter.UP, edge=Counter.FALLING) + toggle(20) + self.assertCounter(20) + out_pin(1) + self.assertCounter(20) # no falling edge + out_pin(0) + self.assertCounter(21) + self.counter.value(-(2**24)) + toggle(20) + self.assertCounter(-(2**24 - 20)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/machine_encoder.py b/tests/extmod_hardware/machine_encoder.py new file mode 100644 index 0000000000000..9bd2bb464178c --- /dev/null +++ b/tests/extmod_hardware/machine_encoder.py @@ -0,0 +1,99 @@ +# Test machine.Encoder implementation +# +# IMPORTANT: This test requires hardware connections: +# - out0_pin and in0_pin must be wired together. +# - out1_pin and in1_pin must be wired together. + +try: + from machine import Encoder +except ImportError: + print("SKIP") + raise SystemExit + +import sys +from machine import Pin + +if "esp32" in sys.platform: + id = 0 + out0_pin = 4 + in0_pin = 5 + out1_pin = 12 + in1_pin = 13 +else: + print("Please add support for this test on this platform.") + raise SystemExit + +import unittest + +out0_pin = Pin(out0_pin, mode=Pin.OUT) +in0_pin = Pin(in0_pin, mode=Pin.IN) +out1_pin = Pin(out1_pin, mode=Pin.OUT) +in1_pin = Pin(in1_pin, mode=Pin.IN) + + +class TestEncoder(unittest.TestCase): + def setUp(self): + out0_pin(0) + out1_pin(0) + self.enc = Encoder(id, in0_pin, in1_pin) + self.pulses = 0 # track the expected encoder position in software + + def tearDown(self): + self.enc.deinit() + + def rotate(self, pulses): + for _ in range(abs(pulses)): + self.pulses += 1 if (pulses > 0) else -1 + q = self.pulses % 4 + # Only one pin should change state each "step" so output won't glitch + out0_pin(q in (1, 2)) + out1_pin(q in (2, 3)) + + def assertPosition(self, value): + self.assertEqual(self.enc.value(), value) + + def test_connections(self): + # Test the hardware connections are correct. If this test fails, all tests will fail. + for ch, outp, inp in ((0, out0_pin, in0_pin), (1, out1_pin, in1_pin)): + print("Testing channel ", ch) + outp(1) + self.assertEqual(1, inp()) + outp(0) + self.assertEqual(0, inp()) + + def test_basics(self): + self.assertPosition(0) + self.rotate(100) + self.assertPosition(100 // 4) + self.rotate(-100) + self.assertPosition(0) + + def test_partial(self): + # With phase=1 (default), need 4x pulses to count a rotation + self.assertPosition(0) + self.rotate(1) + self.assertPosition(0) + self.rotate(1) + self.assertPosition(0) + self.rotate(1) + self.assertPosition(1) # only 3 pulses to count first rotation? + self.rotate(1) + self.assertPosition(1) + self.rotate(1) + self.assertPosition(1) + self.rotate(1) + self.assertPosition(1) + self.rotate(1) + self.assertPosition(2) # 4 for next rotation + self.rotate(-1) + self.assertPosition(1) + self.rotate(-4) + self.assertPosition(0) + self.rotate(-4) + self.assertPosition(-1) + self.rotate(-3) + self.assertPosition(-1) + + +if __name__ == "__main__": + unittest.main() From 026a20da3e803328c4fc9bce66b9931d18f12c1e Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Thu, 1 May 2025 17:12:45 +0200 Subject: [PATCH 065/143] tools/mpremote: Add platformdirs dependency to requirements.txt. Needed to easily find the user configuration file. Signed-off-by: Jos Verlinde --- tools/mpremote/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/mpremote/requirements.txt b/tools/mpremote/requirements.txt index 6209cde5c331d..0aa2bb42a951e 100644 --- a/tools/mpremote/requirements.txt +++ b/tools/mpremote/requirements.txt @@ -1,2 +1,3 @@ pyserial >= 3.3 importlib_metadata >= 1.4; python_version < "3.8" +platformdirs >= 4.3.7 From 64b3944b0121710199d8d5958415ce71f21d67a7 Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Thu, 1 May 2025 17:17:06 +0200 Subject: [PATCH 066/143] tools/mpremote: Locate config.py location across different host OSes. Use `platformdirs.user_config_dir()` (see https://platformdirs.readthedocs.io/en/latest/api.html#user-config-directory) to provide portability across many different OSes and configuration styles. Signed-off-by: Jos Verlinde --- tools/mpremote/mpremote/main.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tools/mpremote/mpremote/main.py b/tools/mpremote/mpremote/main.py index 0aec1efad1431..b31186ba2e1c9 100644 --- a/tools/mpremote/mpremote/main.py +++ b/tools/mpremote/mpremote/main.py @@ -22,6 +22,8 @@ from collections.abc import Mapping from textwrap import dedent +import platformdirs + from .commands import ( CommandError, do_connect, @@ -439,13 +441,7 @@ def load_user_config(): config.commands = {} # Get config file name. - path = os.getenv("XDG_CONFIG_HOME") - if path is None: - path = os.getenv("HOME") - if path is None: - return config - path = os.path.join(path, ".config") - path = os.path.join(path, _PROG) + path = platformdirs.user_config_dir(appname=_PROG, appauthor=False) config_file = os.path.join(path, "config.py") # Check if config file exists. @@ -457,6 +453,9 @@ def load_user_config(): config_data = f.read() prev_cwd = os.getcwd() os.chdir(path) + # Pass in the config path so that the config file can use it. + config.__dict__["config_path"] = path + config.__dict__["__file__"] = config_file exec(config_data, config.__dict__) os.chdir(prev_cwd) From a9dd741e66a6afba3a6e862d134246d157a56777 Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Thu, 1 May 2025 17:25:19 +0200 Subject: [PATCH 067/143] docs/reference/mpremote: Document location of config file. Signed-off-by: Jos Verlinde --- docs/reference/mpremote.rst | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/reference/mpremote.rst b/docs/reference/mpremote.rst index bee008c637319..8aecc3a5d4831 100644 --- a/docs/reference/mpremote.rst +++ b/docs/reference/mpremote.rst @@ -108,7 +108,7 @@ The full list of supported commands are: **Note:** Instead of using the ``connect`` command, there are several :ref:`pre-defined shortcuts ` for common device paths. For example the ``a0`` shortcut command is equivalent to - ``connect /dev/ttyACM0`` (Linux), or ``c0`` for ``COM0`` (Windows). + ``connect /dev/ttyACM0`` (Linux), or ``c1`` for ``COM1`` (Windows). **Note:** The ``auto`` option will only detect USB serial ports, i.e. a serial port that has an associated USB VID/PID (i.e. CDC/ACM or FTDI-style @@ -487,9 +487,16 @@ Shortcuts can be defined using the macro system. Built-in shortcuts are: - ``cat``, ``edit``, ``ls``, ``cp``, ``rm``, ``mkdir``, ``rmdir``, ``touch``: Aliases for ``fs `` -Additional shortcuts can be defined by in user-configuration files, which is -located at ``.config/mpremote/config.py``. This file should define a -dictionary named ``commands``. The keys of this dictionary are the shortcuts +Additional shortcuts can be defined in the user configuration file ``mpremote/config.py``, +located in the User Configuration Directory. +The correct location for each OS is determined using the ``platformdirs`` module. + +This is typically: +- ``$XDG_CONFIG_HOME/mpremote/config.py`` +- ``$HOME/.config/mpremote/config.py`` +- ``$env:LOCALAPPDATA/mpremote/config.py`` + +The ``config.py``` file should define a dictionary named ``commands``. The keys of this dictionary are the shortcuts and the values are either a string or a list-of-strings: .. code-block:: python3 From e6739fc87e9d633484aeb72fa2efac14e4ff7681 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 8 Jul 2025 14:02:42 +0100 Subject: [PATCH 068/143] rp2/rp2_flash: Add binary info for ROMFS. This describes the ROMFS location and size in Pico SDK's binary declaration format, so it can be read from a .uf2 file for use with various tools. Signed-off-by: Phil Howard --- ports/rp2/rp2_flash.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ports/rp2/rp2_flash.c b/ports/rp2/rp2_flash.c index d6b9e13653315..4d41d45733c04 100644 --- a/ports/rp2/rp2_flash.c +++ b/ports/rp2/rp2_flash.c @@ -103,6 +103,18 @@ bi_decl(bi_block_device( BINARY_INFO_BLOCK_DEV_FLAG_WRITE | BINARY_INFO_BLOCK_DEV_FLAG_PT_UNKNOWN)); +#if MICROPY_HW_ROMFS_BYTES +// Tag the ROMFS partition in the binary +bi_decl(bi_block_device( + BINARY_INFO_TAG_MICROPYTHON, + "ROMFS", + XIP_BASE + MICROPY_HW_ROMFS_BASE, + MICROPY_HW_ROMFS_BYTES, + NULL, + BINARY_INFO_BLOCK_DEV_FLAG_READ | + BINARY_INFO_BLOCK_DEV_FLAG_PT_UNKNOWN)); +#endif + // This is a workaround to pico-sdk #2201: https://github.com/raspberrypi/pico-sdk/issues/2201 // which means the multicore_lockout_victim_is_initialized returns true even after core1 is reset. static bool use_multicore_lockout(void) { From 3c9546ea0911b50d4b85ad4046864c90f84b3fd3 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sat, 2 Aug 2025 08:52:05 +1000 Subject: [PATCH 069/143] esp32/mpconfigport: Disable I2CTarget on ESP32-C6 to reduce code size. I2CTarget costs about 8k of flash size on ESP32-S2, and about 11k on ESP32-C6. The ESP32-C6 only has about 8k remaining, so disable I2CTarget on that SoC until more flash can be made available. Signed-off-by: Damien George --- ports/esp32/mpconfigport.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index 48ad39ef7ed17..cc10fd0b7bbb4 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -140,7 +140,8 @@ #define MICROPY_PY_MACHINE_I2C (1) #define MICROPY_PY_MACHINE_I2C_TRANSFER_WRITE1 (1) // I2C target hardware is limited on ESP32 (eg read event comes after the read) so we only support newer SoCs. -#define MICROPY_PY_MACHINE_I2C_TARGET (SOC_I2C_SUPPORT_SLAVE && !CONFIG_IDF_TARGET_ESP32) +// ESP32C6 does not have enough flash space so also disable it on that SoC. +#define MICROPY_PY_MACHINE_I2C_TARGET (SOC_I2C_SUPPORT_SLAVE && !CONFIG_IDF_TARGET_ESP32 && !CONFIG_IDF_TARGET_ESP32C6) #define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/esp32/machine_i2c_target.c" #define MICROPY_PY_MACHINE_I2C_TARGET_MAX (2) #define MICROPY_PY_MACHINE_SOFTI2C (1) From 658a2e3dbd0316204a13f2478d98ea3f8649f064 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sat, 2 Aug 2025 08:39:16 +1000 Subject: [PATCH 070/143] github/workflows: Add a CI job to build ESP32-C2 and ESP32-C6 boards. So that all six supported SoCs are built by CI. Signed-off-by: Damien George --- .github/workflows/ports_esp32.yml | 1 + tools/ci.sh | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/ports_esp32.yml b/.github/workflows/ports_esp32.yml index 4c07f89437757..36ab341cfcdd2 100644 --- a/.github/workflows/ports_esp32.yml +++ b/.github/workflows/ports_esp32.yml @@ -25,6 +25,7 @@ jobs: ci_func: # names are functions in ci.sh - esp32_build_cmod_spiram_s2 - esp32_build_s3_c3 + - esp32_build_c2_c6 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/tools/ci.sh b/tools/ci.sh index d787cbcf0733d..8f045639b80c9 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -214,6 +214,13 @@ function ci_esp32_build_s3_c3 { make ${MAKEOPTS} -C ports/esp32 BOARD=ESP32_GENERIC_C3 } +function ci_esp32_build_c2_c6 { + ci_esp32_build_common + + make ${MAKEOPTS} -C ports/esp32 BOARD=ESP32_GENERIC_C2 + make ${MAKEOPTS} -C ports/esp32 BOARD=ESP32_GENERIC_C6 +} + ######################################################################################## # ports/esp8266 From c0252d73c6499cf8c09fa57e17f4b530315b0ee0 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 2 Aug 2025 15:24:45 -0500 Subject: [PATCH 071/143] py/parse: Fix missing nlr_pop call in complex path of binary_op_maybe. Reproducer (needs to be run as one compilation unit): ans = (-1) ** 2.3 aa Fixes issue #17815. Signed-off-by: Jeff Epler --- py/parse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/parse.c b/py/parse.c index 91eea3e368907..6260987b28a35 100644 --- a/py/parse.c +++ b/py/parse.c @@ -671,13 +671,13 @@ static bool binary_op_maybe(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs, mp_ob nlr_buf_t nlr; if (nlr_push(&nlr) == 0) { mp_obj_t tmp = mp_binary_op(op, lhs, rhs); + nlr_pop(); #if MICROPY_PY_BUILTINS_COMPLEX if (mp_obj_is_type(tmp, &mp_type_complex)) { return false; } #endif *res = tmp; - nlr_pop(); return true; } else { return false; From 7c8ae78a0384ce3fac2e9421df2590ef7d7031ef Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 4 Aug 2025 01:45:30 +1000 Subject: [PATCH 072/143] lib/micropython-lib: Update submodule to latest. This brings in: - lora: fix SNR value in SX126x received packets - utop: add initial implementation for ESP32 - utop: print MicroPython memory info - utop: print IDF heap details - urllib.urequest: add support for headers to urequest.urlopen - aiorepl: use blocking reads for raw REPL and raw paste - errno: add ENOTCONN constant - logging: allow logging.exception helper to handle tracebacks - aioble-l2cap: raise correct error if l2cap disconnects during send - abc: add ABC base class - aiohttp: fix partial reads by using readexactly - aiorepl: handle stream shutdown Signed-off-by: Damien George --- lib/micropython-lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/micropython-lib b/lib/micropython-lib index 5b496e944ec04..34c4ee1647ac4 160000 --- a/lib/micropython-lib +++ b/lib/micropython-lib @@ -1 +1 @@ -Subproject commit 5b496e944ec045177afa1620920a168410b7f60b +Subproject commit 34c4ee1647ac4b177ae40adf0ec514660e433dc0 From 255d74b5a8c31ec06d1c2c528f7888f6c2dfc246 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 4 Aug 2025 02:00:47 +1000 Subject: [PATCH 073/143] renesas-ra/mpconfigport: Enable MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE. This setting was missed in df05caea6c6437a8b4756ec502a5e6210f4b6256. It's needed for this port to pass its `tests/ports/renesas-ra/modtime.py` test. Signed-off-by: Damien George --- ports/renesas-ra/mpconfigport.h | 1 + 1 file changed, 1 insertion(+) diff --git a/ports/renesas-ra/mpconfigport.h b/ports/renesas-ra/mpconfigport.h index 868cdbc7d6ada..70d38e7d20e0e 100644 --- a/ports/renesas-ra/mpconfigport.h +++ b/ports/renesas-ra/mpconfigport.h @@ -99,6 +99,7 @@ #ifndef MICROPY_FLOAT_IMPL // can be configured by each board via mpconfigboard.mk #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_FLOAT) #endif +#define MICROPY_TIME_SUPPORT_Y1969_AND_BEFORE (1) #define MICROPY_USE_INTERNAL_ERRNO (1) #define MICROPY_SCHEDULER_DEPTH (8) #define MICROPY_SCHEDULER_STATIC_NODES (1) From d5ecda05eb743839e9011f3d23657b39cd05e6c0 Mon Sep 17 00:00:00 2001 From: Damien George Date: Thu, 7 Aug 2025 10:33:26 +1000 Subject: [PATCH 074/143] ports: Allow MICROPY_PY_MACHINE_I2C_TARGET to be disabled by board cfg. Signed-off-by: Damien George --- ports/alif/mpconfigport.h | 2 ++ ports/esp32/mpconfigport.h | 2 ++ ports/mimxrt/mpconfigport.h | 2 ++ ports/rp2/mpconfigport.h | 2 ++ ports/stm32/mpconfigboard_common.h | 2 ++ 5 files changed, 10 insertions(+) diff --git a/ports/alif/mpconfigport.h b/ports/alif/mpconfigport.h index a8c8c842977ed..8a65721bb0ffc 100644 --- a/ports/alif/mpconfigport.h +++ b/ports/alif/mpconfigport.h @@ -132,10 +132,12 @@ #define MICROPY_PY_MACHINE_PULSE (1) #define MICROPY_PY_MACHINE_I2C (MICROPY_HW_ENABLE_HW_I2C) #define MICROPY_PY_MACHINE_I2C_TRANSFER_WRITE1 (1) +#ifndef MICROPY_PY_MACHINE_I2C_TARGET #define MICROPY_PY_MACHINE_I2C_TARGET (MICROPY_HW_ENABLE_HW_I2C) #define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/alif/machine_i2c_target.c" #define MICROPY_PY_MACHINE_I2C_TARGET_MAX (4) #define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) +#endif #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_SPI (1) #define MICROPY_PY_MACHINE_SOFTSPI (1) diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index cc10fd0b7bbb4..e47b333c73698 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -139,11 +139,13 @@ #define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/esp32/machine_pwm.c" #define MICROPY_PY_MACHINE_I2C (1) #define MICROPY_PY_MACHINE_I2C_TRANSFER_WRITE1 (1) +#ifndef MICROPY_PY_MACHINE_I2C_TARGET // I2C target hardware is limited on ESP32 (eg read event comes after the read) so we only support newer SoCs. // ESP32C6 does not have enough flash space so also disable it on that SoC. #define MICROPY_PY_MACHINE_I2C_TARGET (SOC_I2C_SUPPORT_SLAVE && !CONFIG_IDF_TARGET_ESP32 && !CONFIG_IDF_TARGET_ESP32C6) #define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/esp32/machine_i2c_target.c" #define MICROPY_PY_MACHINE_I2C_TARGET_MAX (2) +#endif #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_SPI (1) #define MICROPY_PY_MACHINE_SOFTSPI (1) diff --git a/ports/mimxrt/mpconfigport.h b/ports/mimxrt/mpconfigport.h index 8ceb3b418370b..3cf6550d7b07b 100644 --- a/ports/mimxrt/mpconfigport.h +++ b/ports/mimxrt/mpconfigport.h @@ -92,11 +92,13 @@ uint32_t trng_random_u32(void); #define MICROPY_PY_MACHINE_PWM (1) #define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/mimxrt/machine_pwm.c" #define MICROPY_PY_MACHINE_I2C (1) +#ifndef MICROPY_PY_MACHINE_I2C_TARGET #define MICROPY_PY_MACHINE_I2C_TARGET (1) #define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/mimxrt/machine_i2c_target.c" #define MICROPY_PY_MACHINE_I2C_TARGET_MAX (FSL_FEATURE_SOC_LPI2C_COUNT) #define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) #define MICROPY_PY_MACHINE_I2C_TARGET_FINALISER (1) +#endif #ifndef MICROPY_PY_MACHINE_I2S #define MICROPY_PY_MACHINE_I2S (0) #endif diff --git a/ports/rp2/mpconfigport.h b/ports/rp2/mpconfigport.h index d35563bd07028..c312293ace6e9 100644 --- a/ports/rp2/mpconfigport.h +++ b/ports/rp2/mpconfigport.h @@ -171,10 +171,12 @@ #define MICROPY_PY_MACHINE_PWM (1) #define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/rp2/machine_pwm.c" #define MICROPY_PY_MACHINE_I2C (1) +#ifndef MICROPY_PY_MACHINE_I2C_TARGET #define MICROPY_PY_MACHINE_I2C_TARGET (1) #define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/rp2/machine_i2c_target.c" #define MICROPY_PY_MACHINE_I2C_TARGET_MAX (2) #define MICROPY_PY_MACHINE_I2C_TARGET_HARD_IRQ (1) +#endif #define MICROPY_PY_MACHINE_SOFTI2C (1) #define MICROPY_PY_MACHINE_I2S (1) #define MICROPY_PY_MACHINE_I2S_INCLUDEFILE "ports/rp2/machine_i2s.c" diff --git a/ports/stm32/mpconfigboard_common.h b/ports/stm32/mpconfigboard_common.h index dcbbbccceb6b0..a8e50be5c6a8a 100644 --- a/ports/stm32/mpconfigboard_common.h +++ b/ports/stm32/mpconfigboard_common.h @@ -639,11 +639,13 @@ #if defined(MICROPY_HW_I2C1_SCL) || defined(MICROPY_HW_I2C2_SCL) \ || defined(MICROPY_HW_I2C3_SCL) || defined(MICROPY_HW_I2C4_SCL) #define MICROPY_HW_ENABLE_HW_I2C (1) +#ifndef MICROPY_HW_ENABLE_HW_I2C_TARGET #if defined(STM32F4) || defined(STM32F7) || defined(STM32H7) || defined(STM32WB) #define MICROPY_HW_ENABLE_HW_I2C_TARGET (1) #else #define MICROPY_HW_ENABLE_HW_I2C_TARGET (0) #endif +#endif #else #define MICROPY_HW_ENABLE_HW_I2C (0) #define MICROPY_HW_ENABLE_HW_I2C_TARGET (0) From ce109af712452474dfe3864f82b5430f4e2a5ff0 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 6 Aug 2025 15:47:15 +1000 Subject: [PATCH 075/143] esp32/machine_timer: Enable timer clock source for ESP32C6. Otherwise the PLL is not enabled. These changes are adapted from `timer_legacy.h` in ESP-IDF. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/esp32/machine_timer.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ports/esp32/machine_timer.c b/ports/esp32/machine_timer.c index a104288f6ee3c..faa47d59afc10 100644 --- a/ports/esp32/machine_timer.c +++ b/ports/esp32/machine_timer.c @@ -38,6 +38,8 @@ #include "hal/timer_hal.h" #include "hal/timer_ll.h" #include "soc/timer_periph.h" +#include "esp_private/esp_clk_tree_common.h" +#include "esp_private/periph_ctrl.h" #include "machine_timer.h" #define TIMER_DIVIDER 8 @@ -163,8 +165,18 @@ static void machine_timer_isr_handler(machine_timer_obj_t *self) { void machine_timer_enable(machine_timer_obj_t *self) { // Initialise the timer. timer_hal_init(&self->hal_context, self->group, self->index); + + PERIPH_RCC_ACQUIRE_ATOMIC(timer_group_periph_signals.groups[self->index].module, ref_count) { + if (ref_count == 0) { + timer_ll_enable_bus_clock(self->index, true); + timer_ll_reset_register(self->index); + } + } + timer_ll_enable_counter(self->hal_context.dev, self->index, false); + esp_clk_tree_enable_src(GPTIMER_CLK_SRC_DEFAULT, true); timer_ll_set_clock_source(self->hal_context.dev, self->index, GPTIMER_CLK_SRC_DEFAULT); + timer_ll_enable_clock(self->hal_context.dev, self->index, true); timer_ll_set_clock_prescale(self->hal_context.dev, self->index, TIMER_DIVIDER); timer_hal_set_counter_value(&self->hal_context, 0); timer_ll_set_count_direction(self->hal_context.dev, self->index, GPTIMER_COUNT_UP); From 593ae04eeb06b6fd7c6232eb41912b8d26170ddd Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 6 Aug 2025 15:57:38 +1000 Subject: [PATCH 076/143] esp32/machine_timer: Fix machine.Timer() tick frequency on ESP32C2,C6. Also future-proofs this code for other chips. Apart form C6 and C2, all currently supported chips use APB clock for GPTIMER_CLK_SRC_DEFAULT. ESP32-C2 uses 40MHz PLL but APB_CLK_FREQ was 26MHz. ESP32-C6 uses 80MHz PLL but APB_CLK_FREQ was 40MHz. Implementation now gets the correct frequency from ESP-IDF. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/esp32/machine_timer.c | 24 +++++++++++++++--------- ports/esp32/machine_timer.h | 9 ++------- ports/esp32/machine_uart.c | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/ports/esp32/machine_timer.c b/ports/esp32/machine_timer.c index faa47d59afc10..3fb893aadf1ab 100644 --- a/ports/esp32/machine_timer.c +++ b/ports/esp32/machine_timer.c @@ -42,11 +42,9 @@ #include "esp_private/periph_ctrl.h" #include "machine_timer.h" +#define TIMER_CLK_SRC GPTIMER_CLK_SRC_DEFAULT #define TIMER_DIVIDER 8 -// TIMER_BASE_CLK is normally 80MHz. TIMER_DIVIDER ought to divide this exactly -#define TIMER_SCALE (APB_CLK_FREQ / TIMER_DIVIDER) - #define TIMER_FLAGS 0 const mp_obj_type_t machine_timer_type; @@ -54,6 +52,14 @@ const mp_obj_type_t machine_timer_type; static mp_obj_t machine_timer_init_helper(machine_timer_obj_t *self, mp_uint_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); static mp_obj_t machine_timer_deinit(mp_obj_t self_in); +uint32_t machine_timer_freq_hz(void) { + // The timer source clock is APB or a fixed PLL (depending on chip), both constant frequency. + uint32_t freq; + check_esp_err(esp_clk_tree_src_get_freq_hz(TIMER_CLK_SRC, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq)); + assert(freq % TIMER_DIVIDER == 0); // Source clock should divide evenly into TIMER_DIVIDER + return freq / TIMER_DIVIDER; +} + void machine_timer_deinit_all(void) { // Disable, deallocate and remove all timers from list machine_timer_obj_t **t = &MP_STATE_PORT(machine_timer_obj_head); @@ -68,7 +74,7 @@ void machine_timer_deinit_all(void) { static void machine_timer_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { machine_timer_obj_t *self = self_in; qstr mode = self->repeat ? MP_QSTR_PERIODIC : MP_QSTR_ONE_SHOT; - uint64_t period = self->period / (TIMER_SCALE / 1000); // convert to ms + uint64_t period = self->period / (machine_timer_freq_hz() / 1000); // convert to ms #if SOC_TIMER_GROUP_TIMERS_PER_GROUP == 1 mp_printf(print, "Timer(%u, mode=%q, period=%lu)", self->group, mode, period); #else @@ -174,8 +180,8 @@ void machine_timer_enable(machine_timer_obj_t *self) { } timer_ll_enable_counter(self->hal_context.dev, self->index, false); - esp_clk_tree_enable_src(GPTIMER_CLK_SRC_DEFAULT, true); - timer_ll_set_clock_source(self->hal_context.dev, self->index, GPTIMER_CLK_SRC_DEFAULT); + esp_clk_tree_enable_src(TIMER_CLK_SRC, true); + timer_ll_set_clock_source(self->hal_context.dev, self->index, TIMER_CLK_SRC); timer_ll_enable_clock(self->hal_context.dev, self->index, true); timer_ll_set_clock_prescale(self->hal_context.dev, self->index, TIMER_DIVIDER); timer_hal_set_counter_value(&self->hal_context, 0); @@ -236,7 +242,7 @@ static mp_obj_t machine_timer_init_helper(machine_timer_obj_t *self, mp_uint_t n #if MICROPY_PY_BUILTINS_FLOAT if (args[ARG_freq].u_obj != mp_const_none) { - self->period = (uint64_t)(TIMER_SCALE / mp_obj_get_float(args[ARG_freq].u_obj)); + self->period = (uint64_t)(machine_timer_freq_hz() / mp_obj_get_float(args[ARG_freq].u_obj)); } #else if (args[ARG_freq].u_int != 0xffffffff) { @@ -244,7 +250,7 @@ static mp_obj_t machine_timer_init_helper(machine_timer_obj_t *self, mp_uint_t n } #endif else { - self->period = (((uint64_t)args[ARG_period].u_int) * TIMER_SCALE) / args[ARG_tick_hz].u_int; + self->period = (((uint64_t)args[ARG_period].u_int) * machine_timer_freq_hz()) / args[ARG_tick_hz].u_int; } self->repeat = args[ARG_mode].u_int; @@ -280,7 +286,7 @@ static mp_obj_t machine_timer_value(mp_obj_t self_in) { mp_raise_ValueError(MP_ERROR_TEXT("timer not set")); } uint64_t result = timer_ll_get_counter_value(self->hal_context.dev, self->index); - return MP_OBJ_NEW_SMALL_INT((mp_uint_t)(result / (TIMER_SCALE / 1000))); // value in ms + return MP_OBJ_NEW_SMALL_INT((mp_uint_t)(result / (machine_timer_freq_hz() / 1000))); // value in ms } static MP_DEFINE_CONST_FUN_OBJ_1(machine_timer_value_obj, machine_timer_value); diff --git a/ports/esp32/machine_timer.h b/ports/esp32/machine_timer.h index 10fe2f39c90ae..5dd0ce95ac802 100644 --- a/ports/esp32/machine_timer.h +++ b/ports/esp32/machine_timer.h @@ -34,13 +34,6 @@ #include "hal/timer_ll.h" #include "soc/timer_periph.h" -#define TIMER_DIVIDER 8 - -// TIMER_BASE_CLK is normally 80MHz. TIMER_DIVIDER ought to divide this exactly -#define TIMER_SCALE (APB_CLK_FREQ / TIMER_DIVIDER) - -#define TIMER_FLAGS 0 - typedef struct _machine_timer_obj_t { mp_obj_base_t base; @@ -64,4 +57,6 @@ machine_timer_obj_t *machine_timer_create(mp_uint_t timer); void machine_timer_enable(machine_timer_obj_t *self); void machine_timer_disable(machine_timer_obj_t *self); +uint32_t machine_timer_freq_hz(void); + #endif // MICROPY_INCLUDED_ESP32_MACHINE_TIMER_H diff --git a/ports/esp32/machine_uart.c b/ports/esp32/machine_uart.c index 661c07138e535..c4dab2cae5c81 100644 --- a/ports/esp32/machine_uart.c +++ b/ports/esp32/machine_uart.c @@ -58,7 +58,7 @@ #define UART_IRQ_RXIDLE (0x1000) #define UART_IRQ_BREAK (1 << UART_BREAK) #define MP_UART_ALLOWED_FLAGS (UART_IRQ_RX | UART_IRQ_RXIDLE | UART_IRQ_BREAK) -#define RXIDLE_TIMER_MIN (5000) // 500 us +#define RXIDLE_TIMER_MIN (machine_timer_freq_hz() * 5 / 10000) // 500us minimum rxidle time #define UART_QUEUE_SIZE (3) enum { @@ -535,7 +535,7 @@ static void uart_irq_configure_timer(machine_uart_obj_t *self, mp_uint_t trigger self->mp_irq_obj->ishard = false; uint32_t baudrate; uart_get_baudrate(self->uart_num, &baudrate); - mp_int_t period = TIMER_SCALE * 20 / baudrate + 1; + mp_int_t period = machine_timer_freq_hz() * 20 / baudrate + 1; if (period < RXIDLE_TIMER_MIN) { period = RXIDLE_TIMER_MIN; } From 4ce2dd2cdab6e57f3982fc899f15a2103d71b0be Mon Sep 17 00:00:00 2001 From: Damien George Date: Sat, 9 Aug 2025 23:31:31 +1000 Subject: [PATCH 077/143] all: Bump version to 1.26.0. Signed-off-by: Damien George --- py/mpconfig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/mpconfig.h b/py/mpconfig.h index ed5ccccb6a52a..8775b74bb61bc 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -32,7 +32,7 @@ #define MICROPY_VERSION_MAJOR 1 #define MICROPY_VERSION_MINOR 26 #define MICROPY_VERSION_MICRO 0 -#define MICROPY_VERSION_PRERELEASE 1 +#define MICROPY_VERSION_PRERELEASE 0 // Combined version as a 32-bit number for convenience to allow version // comparison. Doesn't include prerelease state. From 7e8705fe2bdb083535e85c206b9fed9d2754e665 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 10 Aug 2025 08:42:11 +1000 Subject: [PATCH 078/143] all: Bump version to 1.27.0-preview. Signed-off-by: Damien George --- py/mpconfig.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py/mpconfig.h b/py/mpconfig.h index 8775b74bb61bc..dd41ff6a70867 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -30,9 +30,9 @@ // as well as a fallback to generate MICROPY_GIT_TAG if the git repo or tags // are unavailable. #define MICROPY_VERSION_MAJOR 1 -#define MICROPY_VERSION_MINOR 26 +#define MICROPY_VERSION_MINOR 27 #define MICROPY_VERSION_MICRO 0 -#define MICROPY_VERSION_PRERELEASE 0 +#define MICROPY_VERSION_PRERELEASE 1 // Combined version as a 32-bit number for convenience to allow version // comparison. Doesn't include prerelease state. From c35427cbdbe105c8a3be099f8fd2946fa96c4a22 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sat, 26 Jul 2025 17:31:24 +1000 Subject: [PATCH 079/143] tests/run-tests.py: Move tests to skip with native emitter to a list. This makes `run-tests.py` a little more organised, by putting all the tests-to-skip-when-using-the-native-emitter in a dedicated list. This should make it easier to maintain the list, and understand why a test is there. Signed-off-by: Damien George --- tests/run-tests.py | 85 ++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/tests/run-tests.py b/tests/run-tests.py index 48dfc2f26e070..cb3a2c3cd1630 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -102,6 +102,48 @@ def open(self, path, mode): # Platforms associated with the unix port, values of `sys.platform`. PC_PLATFORMS = ("darwin", "linux", "win32") +# Tests to skip for specific emitters. +emitter_tests_to_skip = { + # Some tests are known to fail with native emitter. + # Remove them from the below when they work. + "native": ( + # These require raise_varargs. + "basics/gen_yield_from_close.py", + "basics/try_finally_return2.py", + "basics/try_reraise.py", + "basics/try_reraise2.py", + "misc/features.py", + # These require checking for unbound local. + "basics/annotate_var.py", + "basics/del_deref.py", + "basics/del_local.py", + "basics/scope_implicit.py", + "basics/unboundlocal.py", + # These require "raise from". + "basics/exception_chain.py", + # These require proper traceback info. + "basics/sys_tracebacklimit.py", + "misc/print_exception.py", + "micropython/emg_exc.py", + "micropython/heapalloc_traceback.py", + "micropython/opt_level_lineno.py", + # These require stack-allocated slice optimisation. + "micropython/heapalloc_slice.py", + # These require running the scheduler. + "micropython/schedule.py", + "extmod/asyncio_event_queue.py", + "extmod/asyncio_iterator_event.py", + # These require sys.exc_info(). + "misc/sys_exc_info.py", + # These require sys.settrace(). + "misc/sys_settrace_features.py", + "misc/sys_settrace_generator.py", + "misc/sys_settrace_loop.py", + # These are bytecode-specific tests. + "stress/bytecode_limit.py", + ), +} + # Tests to skip on specific targets. # These are tests that are difficult to detect that they should not be run on the given target. platform_tests_to_skip = { @@ -824,6 +866,9 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add("basics/exception_chain.py") # warning is not printed skip_tests.add("micropython/meminfo.py") # output is very different to PC output + # Skip emitter-specific tests. + skip_tests.update(emitter_tests_to_skip.get(args.emit, ())) + # Skip platform-specific tests. skip_tests.update(platform_tests_to_skip.get(args.platform, ())) @@ -837,46 +882,6 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): # Works but CPython uses '\' path separator skip_tests.add("import/import_file.py") - # Some tests are known to fail with native emitter - # Remove them from the below when they work - if args.emit == "native": - skip_tests.add("basics/gen_yield_from_close.py") # require raise_varargs - skip_tests.update( - {"basics/%s.py" % t for t in "try_reraise try_reraise2".split()} - ) # require raise_varargs - skip_tests.add("basics/annotate_var.py") # requires checking for unbound local - skip_tests.add("basics/del_deref.py") # requires checking for unbound local - skip_tests.add("basics/del_local.py") # requires checking for unbound local - skip_tests.add("basics/exception_chain.py") # raise from is not supported - skip_tests.add("basics/scope_implicit.py") # requires checking for unbound local - skip_tests.add("basics/sys_tracebacklimit.py") # requires traceback info - skip_tests.add("basics/try_finally_return2.py") # requires raise_varargs - skip_tests.add("basics/unboundlocal.py") # requires checking for unbound local - skip_tests.add("misc/features.py") # requires raise_varargs - skip_tests.add( - "misc/print_exception.py" - ) # because native doesn't have proper traceback info - skip_tests.add("misc/sys_exc_info.py") # sys.exc_info() is not supported for native - skip_tests.add("misc/sys_settrace_features.py") # sys.settrace() not supported - skip_tests.add("misc/sys_settrace_generator.py") # sys.settrace() not supported - skip_tests.add("misc/sys_settrace_loop.py") # sys.settrace() not supported - skip_tests.add( - "micropython/emg_exc.py" - ) # because native doesn't have proper traceback info - skip_tests.add( - "micropython/heapalloc_slice.py" - ) # because native doesn't do the stack-allocated slice optimisation - skip_tests.add( - "micropython/heapalloc_traceback.py" - ) # because native doesn't have proper traceback info - skip_tests.add( - "micropython/opt_level_lineno.py" - ) # native doesn't have proper traceback info - skip_tests.add("micropython/schedule.py") # native code doesn't check pending events - skip_tests.add("stress/bytecode_limit.py") # bytecode specific test - skip_tests.add("extmod/asyncio_event_queue.py") # native can't run schedule - skip_tests.add("extmod/asyncio_iterator_event.py") # native can't run schedule - def run_one_test(test_file): test_file = test_file.replace("\\", "/") test_file_abspath = os.path.abspath(test_file).replace("\\", "/") From 1401fdb8b39bf08e9ed9d4c1293207a6bdc93cf8 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sat, 26 Jul 2025 19:11:36 +1000 Subject: [PATCH 080/143] tests/run-tests.py: Run tests-with-regex-output as normal tests. Some tests (currently given by the `special_tests` list) have output which must be mached via a regex, because it can change from run to run (eg the address of an object is printed). These tests are currently classified as `is_special` in the test runner, which means they get special treatment. In particular they don't set the emitter as specified by `args.emit`. That means these tests do not run via .mpy or using the native emitter, even if those options are given on the command line. This commit fixes that by considering `is_special` as different to `tests_with_regex_output`. The former is used for things like target feature detection (which are not really tests) and when extra command line options need to be passed to the unix micropython executable. The latter (now called `tests_with_regex_output`) are specifically for tests that have output to be matched via regex. The `thread_exc2.py` test now needs to be excluded when running using the native emitter, because the native emitter doesn't print traceback info. And the `sys_settrace_cov.py` test needs to be excluded because set-trace output is different with the native emitter. Signed-off-by: Damien George --- tests/run-tests.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/run-tests.py b/tests/run-tests.py index cb3a2c3cd1630..859f16f484a85 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -127,6 +127,7 @@ def open(self, path, mode): "micropython/emg_exc.py", "micropython/heapalloc_traceback.py", "micropython/opt_level_lineno.py", + "thread/thread_exc2.py", # These require stack-allocated slice optimisation. "micropython/heapalloc_slice.py", # These require running the scheduler. @@ -136,6 +137,7 @@ def open(self, path, mode): # These require sys.exc_info(). "misc/sys_exc_info.py", # These require sys.settrace(). + "misc/sys_settrace_cov.py", "misc/sys_settrace_features.py", "misc/sys_settrace_generator.py", "misc/sys_settrace_loop.py", @@ -392,7 +394,7 @@ def run_script_on_remote_target(pyb, args, test_file, is_special): return had_crash, output_mupy -special_tests = [ +tests_with_regex_output = [ base_path(file) for file in ( "micropython/meminfo.py", @@ -409,10 +411,7 @@ def run_micropython(pyb, args, test_file, test_file_abspath, is_special=False): had_crash = False if pyb is None: # run on PC - if ( - test_file_abspath.startswith((base_path("cmdline/"), base_path("feature_check/"))) - or test_file_abspath in special_tests - ): + if test_file_abspath.startswith((base_path("cmdline/"), base_path("feature_check/"))): # special handling for tests of the unix cmdline program is_special = True @@ -544,7 +543,7 @@ def send_get(what): if is_special and not had_crash and b"\nSKIP\n" in output_mupy: return b"SKIP\n" - if is_special or test_file_abspath in special_tests: + if is_special or test_file_abspath in tests_with_regex_output: # convert parts of the output that are not stable across runs with open(test_file + ".exp", "rb") as f: lines_exp = [] From 1273751a3b6f8a14679378e2e082e03979cb2ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Giessen?= Date: Tue, 22 Jul 2025 15:57:49 +0200 Subject: [PATCH 081/143] esp32: Support building against IDFv5.5. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniël van de Giessen --- ports/esp32/boards/sdkconfig.base | 1 + ports/esp32/esp32_common.cmake | 2 +- ports/esp32/machine_timer.c | 5 +++++ ports/esp32/modespnow.c | 11 ++++++++++- ports/esp32/network_wlan.c | 7 ++++++- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ports/esp32/boards/sdkconfig.base b/ports/esp32/boards/sdkconfig.base index 4bbccf77d17d7..78b09ec6b24b0 100644 --- a/ports/esp32/boards/sdkconfig.base +++ b/ports/esp32/boards/sdkconfig.base @@ -119,6 +119,7 @@ CONFIG_UART_ISR_IN_IRAM=y # IDF 5 deprecated CONFIG_PCNT_SUPPRESS_DEPRECATE_WARN=y CONFIG_RMT_SUPPRESS_DEPRECATE_WARN=y +CONFIG_TOUCH_SUPPRESS_DEPRECATE_WARN=y CONFIG_ETH_USE_SPI_ETHERNET=y CONFIG_ETH_SPI_ETHERNET_W5500=y diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake index 9e8acf8897c6a..79a60adac9f36 100644 --- a/ports/esp32/esp32_common.cmake +++ b/ports/esp32/esp32_common.cmake @@ -265,7 +265,7 @@ target_include_directories(${MICROPY_TARGET} PUBLIC # Add additional extmod and usermod components. if (MICROPY_PY_BTREE) - target_link_libraries(${MICROPY_TARGET} micropy_extmod_btree) + target_link_libraries(${MICROPY_TARGET} $) endif() target_link_libraries(${MICROPY_TARGET} usermod) diff --git a/ports/esp32/machine_timer.c b/ports/esp32/machine_timer.c index 3fb893aadf1ab..b0292a0379fea 100644 --- a/ports/esp32/machine_timer.c +++ b/ports/esp32/machine_timer.c @@ -181,8 +181,13 @@ void machine_timer_enable(machine_timer_obj_t *self) { timer_ll_enable_counter(self->hal_context.dev, self->index, false); esp_clk_tree_enable_src(TIMER_CLK_SRC, true); + #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0) timer_ll_set_clock_source(self->hal_context.dev, self->index, TIMER_CLK_SRC); timer_ll_enable_clock(self->hal_context.dev, self->index, true); + #else + timer_ll_set_clock_source(self->group, self->index, TIMER_CLK_SRC); + timer_ll_enable_clock(self->group, self->index, true); + #endif timer_ll_set_clock_prescale(self->hal_context.dev, self->index, TIMER_DIVIDER); timer_hal_set_counter_value(&self->hal_context, 0); timer_ll_set_count_direction(self->hal_context.dev, self->index, GPTIMER_COUNT_UP); diff --git a/ports/esp32/modespnow.c b/ports/esp32/modespnow.c index 7873ff8977876..ab50032ffeb51 100644 --- a/ports/esp32/modespnow.c +++ b/ports/esp32/modespnow.c @@ -179,7 +179,11 @@ static mp_obj_t espnow_make_new(const mp_obj_type_t *type, size_t n_args, } // Forward declare the send and recv ESPNow callbacks +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0) static void send_cb(const uint8_t *mac_addr, esp_now_send_status_t status); +#else +static void send_cb(const esp_now_send_info_t *tx_info, esp_now_send_status_t status); +#endif static void recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *msg, int msg_len); @@ -539,7 +543,12 @@ static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(espnow_send_obj, 2, 4, espnow_send); // Callback triggered when a sent packet is acknowledged by the peer (or not). // Just count the number of responses and number of failures. // These are used in the send() logic. -static void send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) { +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0) +static void send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) +#else +static void send_cb(const esp_now_send_info_t *tx_info, esp_now_send_status_t status) +#endif +{ esp_espnow_obj_t *self = _get_singleton(); self->tx_responses++; if (status != ESP_NOW_SEND_SUCCESS) { diff --git a/ports/esp32/network_wlan.c b/ports/esp32/network_wlan.c index e85d1328fdc2b..e20af4806c43d 100644 --- a/ports/esp32/network_wlan.c +++ b/ports/esp32/network_wlan.c @@ -767,6 +767,9 @@ static const mp_rom_map_elem_t wlan_if_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_ENT), MP_ROM_INT(WIFI_AUTH_WPA3_ENTERPRISE) }, { MP_ROM_QSTR(MP_QSTR_SEC_WPA2_WPA3_ENT), MP_ROM_INT(WIFI_AUTH_WPA2_WPA3_ENTERPRISE) }, #endif + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + { MP_ROM_QSTR(MP_QSTR_SEC_WPA_ENT), MP_ROM_INT(WIFI_AUTH_WPA_ENTERPRISE) }, + #endif { MP_ROM_QSTR(MP_QSTR_PM_NONE), MP_ROM_INT(WIFI_PS_NONE) }, { MP_ROM_QSTR(MP_QSTR_PM_PERFORMANCE), MP_ROM_INT(WIFI_PS_MIN_MODEM) }, @@ -774,7 +777,9 @@ static const mp_rom_map_elem_t wlan_if_locals_dict_table[] = { }; static MP_DEFINE_CONST_DICT(wlan_if_locals_dict, wlan_if_locals_dict_table); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +_Static_assert(WIFI_AUTH_MAX == 17, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h"); +#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) _Static_assert(WIFI_AUTH_MAX == 16, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h"); #elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) _Static_assert(WIFI_AUTH_MAX == 14, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h"); From 361c615f3e272df012fc18315e226f9131d25c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Giessen?= Date: Thu, 10 Jul 2025 11:05:19 +0200 Subject: [PATCH 082/143] esp32/network_ppp: Use thread-safe API for PPPoS input. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A small follow-up to 3b1e22c66947271e8b60eddf4e8aa6dadc6d9a7d, in which the entire PPP implementation was reworked to more closely resemble the extmod version. One of the differences between extmod and the ESP32 port is that the ESP32 port uses the thread-safe API, but in that changeset the PPP input function was accidentally changed to use the non-safe API. Signed-off-by: Daniël van de Giessen --- ports/esp32/network_ppp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/esp32/network_ppp.c b/ports/esp32/network_ppp.c index 8b700c98ef386..77e4385ca1ee1 100644 --- a/ports/esp32/network_ppp.c +++ b/ports/esp32/network_ppp.c @@ -163,7 +163,7 @@ static mp_obj_t network_ppp_poll(size_t n_args, const mp_obj_t *args) { } mp_printf(&mp_plat_print, ")\n"); #endif - pppos_input(self->pcb, (u8_t *)buf, len); + pppos_input_tcpip(self->pcb, (u8_t *)buf, len); total_len += len; } From adcfdf625be4ce1a8506dccc33ea60ab5d84ab46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Giessen?= Date: Thu, 10 Jul 2025 14:43:29 +0200 Subject: [PATCH 083/143] esp32/network_ppp: Use non-thread-safe API inside status callback. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status callback runs on the lwIP tcpip_thread, and thus must use the non-thread-safe API because the thread-safe API would cause a deadlock. Signed-off-by: Daniël van de Giessen --- extmod/network_ppp_lwip.c | 8 +++++--- ports/esp32/network_ppp.c | 11 ++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/extmod/network_ppp_lwip.c b/extmod/network_ppp_lwip.c index 2c3dac92012a0..883275f48196b 100644 --- a/extmod/network_ppp_lwip.c +++ b/extmod/network_ppp_lwip.c @@ -62,8 +62,6 @@ typedef struct _network_ppp_obj_t { const mp_obj_type_t mp_network_ppp_lwip_type; -static mp_obj_t network_ppp___del__(mp_obj_t self_in); - static void network_ppp_stream_uart_irq_disable(network_ppp_obj_t *self) { if (self->stream == mp_const_none) { return; @@ -88,8 +86,12 @@ static void network_ppp_status_cb(ppp_pcb *pcb, int err_code, void *ctx) { // only need to free the PPP PCB, not close it. self->state = STATE_ACTIVE; } + network_ppp_stream_uart_irq_disable(self); // Clean up the PPP PCB. - network_ppp___del__(MP_OBJ_FROM_PTR(self)); + if (ppp_free(pcb) == ERR_OK) { + self->state = STATE_INACTIVE; + self->pcb = NULL; + } break; default: self->state = STATE_ERROR; diff --git a/ports/esp32/network_ppp.c b/ports/esp32/network_ppp.c index 77e4385ca1ee1..3a9cb239db893 100644 --- a/ports/esp32/network_ppp.c +++ b/ports/esp32/network_ppp.c @@ -68,8 +68,6 @@ typedef struct _network_ppp_obj_t { const mp_obj_type_t esp_network_ppp_lwip_type; -static mp_obj_t network_ppp___del__(mp_obj_t self_in); - static void network_ppp_stream_uart_irq_disable(network_ppp_obj_t *self) { if (self->stream == mp_const_none) { return; @@ -94,8 +92,15 @@ static void network_ppp_status_cb(ppp_pcb *pcb, int err_code, void *ctx) { // only need to free the PPP PCB, not close it. self->state = STATE_ACTIVE; } + network_ppp_stream_uart_irq_disable(self); // Clean up the PPP PCB. - network_ppp___del__(MP_OBJ_FROM_PTR(self)); + // Note: Because we use pppapi_close instead of ppp_close, this + // callback will run on the lwIP tcpip_thread, thus to prevent a + // deadlock we must use the non-threadsafe function here. + if (ppp_free(pcb) == ERR_OK) { + self->state = STATE_INACTIVE; + self->pcb = NULL; + } break; default: self->state = STATE_ERROR; From 8c47ff7153a7a54eced6c8177698339abe4973ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Giessen?= Date: Tue, 8 Jul 2025 15:52:17 +0200 Subject: [PATCH 084/143] esp32/network_ppp: Correctly clean up PPP PCB after close. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If PPP is still connected, freeing the PCB will fail and thus instead we should trigger a disconnect and wait for the lwIP callback to actually free the PCB. When PPP is not connected we should check if the freeing failed, warn the user if so, and only mark the connection as inactive if not. When all this happens during garbage collection the best case is that the PPP connection is already dead, which means the callback will be called immediately and cleanup will happen correctly. The worst case is that the connection is still alive, thus we are unable to free the PCB (lwIP won't let us) and it remains referenced in the netif_list, meaning a use-after-free happens later when lwIP traverses that linked list. This change does not fully prevent that, but it *does* improve how the PPP.active(False) method on the ESP32 port behaves: It no longer immediately tries to free (and fails), but instead triggers a disconnect and lets the cleanup happen correctly through the status callback. Signed-off-by: Daniël van de Giessen --- extmod/network_ppp_lwip.c | 19 ++++++++++--------- ports/esp32/network_ppp.c | 18 +++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/extmod/network_ppp_lwip.c b/extmod/network_ppp_lwip.c index 883275f48196b..12205521f670f 100644 --- a/extmod/network_ppp_lwip.c +++ b/extmod/network_ppp_lwip.c @@ -119,17 +119,18 @@ static mp_obj_t network_ppp_make_new(const mp_obj_type_t *type, size_t n_args, s static mp_obj_t network_ppp___del__(mp_obj_t self_in) { network_ppp_obj_t *self = MP_OBJ_TO_PTR(self_in); - if (self->state >= STATE_ACTIVE) { - if (self->state >= STATE_ERROR) { - // Still connected over the stream. - // Force the connection to close, with nocarrier=1. - self->state = STATE_INACTIVE; - ppp_close(self->pcb, 1); - } - network_ppp_stream_uart_irq_disable(self); + + network_ppp_stream_uart_irq_disable(self); + if (self->state >= STATE_ERROR) { + // Still connected over the stream. + // Force the connection to close, with nocarrier=1. + ppp_close(self->pcb, 1); + } else if (self->state >= STATE_ACTIVE) { // Free PPP PCB and reset state. + if (ppp_free(self->pcb) != ERR_OK) { + mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("ppp_free failed")); + } self->state = STATE_INACTIVE; - ppp_free(self->pcb); self->pcb = NULL; } return mp_const_none; diff --git a/ports/esp32/network_ppp.c b/ports/esp32/network_ppp.c index 3a9cb239db893..18e0c8816889a 100644 --- a/ports/esp32/network_ppp.c +++ b/ports/esp32/network_ppp.c @@ -128,17 +128,17 @@ static mp_obj_t network_ppp_make_new(const mp_obj_type_t *type, size_t n_args, s static mp_obj_t network_ppp___del__(mp_obj_t self_in) { network_ppp_obj_t *self = MP_OBJ_TO_PTR(self_in); - if (self->state >= STATE_ACTIVE) { - if (self->state >= STATE_ERROR) { - // Still connected over the stream. - // Force the connection to close, with nocarrier=1. - self->state = STATE_INACTIVE; - pppapi_close(self->pcb, 1); - } - network_ppp_stream_uart_irq_disable(self); + network_ppp_stream_uart_irq_disable(self); + if (self->state >= STATE_ERROR) { + // Still connected over the stream. + // Force the connection to close, with nocarrier=1. + pppapi_close(self->pcb, 1); + } else if (self->state >= STATE_ACTIVE) { // Free PPP PCB and reset state. + if (pppapi_free(self->pcb) != ERR_OK) { + mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("pppapi_free failed")); + } self->state = STATE_INACTIVE; - pppapi_free(self->pcb); self->pcb = NULL; } return mp_const_none; From 20e1ae07336a89e7054562a1bef26fae99802210 Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Sun, 3 Aug 2025 00:09:09 +0200 Subject: [PATCH 085/143] tools/mpremote: Fix encoding error in PyboardCommand. This is a fix for utf-8 decoding errors that are thrown when non-utf-8 content is received. For instance during a reboot of an ESP8266 module. The fix is to handle conversion errors by replacing illegal characters. Note that these illegal characters most often occur during an MCU reboot sequence when the MCU is using baudrates different from 115200. Signed-off-by: Jos Verlinde --- tools/mpremote/mpremote/transport_serial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/mpremote/mpremote/transport_serial.py b/tools/mpremote/mpremote/transport_serial.py index daeff02b594e2..e2490a7caf8be 100644 --- a/tools/mpremote/mpremote/transport_serial.py +++ b/tools/mpremote/mpremote/transport_serial.py @@ -795,7 +795,7 @@ def rd_str(self): if n == 0: return "" else: - return str(self.fin.read(n), "utf8") + return str(self.fin.read(n), "utf8", errors="backslashreplace") def wr_s8(self, i): self.fout.write(struct.pack(" {n}") From 6e450dba7e5042355462b807400ba8d389fc17e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Giessen?= Date: Thu, 7 Aug 2025 13:30:13 +0200 Subject: [PATCH 086/143] tools/codeformat.py: Iterate lines instead of modifying list in-place. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Lechner Signed-off-by: Daniël van de Giessen --- tools/codeformat.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tools/codeformat.py b/tools/codeformat.py index c65174ddc1278..afba5c336d81d 100755 --- a/tools/codeformat.py +++ b/tools/codeformat.py @@ -92,21 +92,20 @@ def fixup_c(filename): # Write out file with fixups. with open(filename, "w", newline="") as f: dedent_stack = [] - while lines: - # Get next line. - l = lines.pop(0) - + for line_number, line in enumerate(lines, 1): # Dedent #'s to match indent of following line (not previous line). - m = re.match(r"( +)#(if |ifdef |ifndef |elif |else|endif)", l) + m = re.match(r"( +)#(if |ifdef |ifndef |elif |else|endif)", line) if m: indent = len(m.group(1)) directive = m.group(2) if directive in ("if ", "ifdef ", "ifndef "): - l_next = lines[0] - indent_next = len(re.match(r"( *)", l_next).group(1)) - if indent - 4 == indent_next and re.match(r" +(} else |case )", l_next): + # Line numbers are 1-based, and lists are always 0-based, + # thus this retrieves the next line, not the current one + line_next = lines[line_number] + indent_next = len(re.match(r"( *)", line_next).group(1)) + if indent - 4 == indent_next and re.match(r" +(} else |case )", line_next): # This #-line (and all associated ones) needs dedenting by 4 spaces. - l = l[4:] + line = line[4:] dedent_stack.append(indent - 4) else: # This #-line does not need dedenting. @@ -116,12 +115,12 @@ def fixup_c(filename): # This associated #-line needs dedenting to match the #if. indent_diff = indent - dedent_stack[-1] assert indent_diff >= 0 - l = l[indent_diff:] + line = line[indent_diff:] if directive == "endif": dedent_stack.pop() # Write out line. - f.write(l) + f.write(line) assert not dedent_stack, filename From 485dac783b8ba7b88fdbf28fcdf54eb053cd8ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Giessen?= Date: Tue, 5 Aug 2025 13:21:59 +0200 Subject: [PATCH 087/143] tools/codeformat.py: Print filename + linenumber when dedenting fails. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniël van de Giessen --- tools/codeformat.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/codeformat.py b/tools/codeformat.py index afba5c336d81d..7f13a059f45b4 100755 --- a/tools/codeformat.py +++ b/tools/codeformat.py @@ -75,6 +75,10 @@ UNCRUSTIFY_CFG = os.path.join(TOP, "tools/uncrustify.cfg") +class IndentationError(ValueError): + pass + + def list_files(paths, exclusions=None, prefix=""): files = set() for pattern in paths: @@ -111,6 +115,10 @@ def fixup_c(filename): # This #-line does not need dedenting. dedent_stack.append(-1) else: + if len(dedent_stack) == 0: + raise IndentationError( + f'dedent stack is empty for "{directive}" at {filename}:{line_number}' + ) if dedent_stack[-1] >= 0: # This associated #-line needs dedenting to match the #if. indent_diff = indent - dedent_stack[-1] From 14ccdeb4d7b9b88ab012258e77d3070340fbc3da Mon Sep 17 00:00:00 2001 From: Jared Hancock Date: Mon, 25 Mar 2024 20:58:51 -0500 Subject: [PATCH 088/143] extmod/modre: Add support for start- and endpos. Pattern objects have two additional parameters for the ::search and ::match methods to define the starting and ending position of the subject within the string to be searched. This allows for searching a sub-string without creating a slice. However, one caveat of using the start-pos rather than a slice is that the start anchor (`^`) remains anchored to the beginning of the text. Signed-off-by: Jared Hancock --- docs/library/re.rst | 14 +++++- extmod/modre.c | 25 +++++++++- tests/extmod/re_start_end_pos.py | 78 ++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 tests/extmod/re_start_end_pos.py diff --git a/docs/library/re.rst b/docs/library/re.rst index 19b15d2d2c299..b8aeefd90cfa4 100644 --- a/docs/library/re.rst +++ b/docs/library/re.rst @@ -154,8 +154,8 @@ Regex objects Compiled regular expression. Instances of this class are created using `re.compile()`. -.. method:: regex.match(string) - regex.search(string) +.. method:: regex.match(string, [pos, [endpos]]) + regex.search(string, [pos, [endpos]]) regex.sub(replace, string, count=0, flags=0, /) Similar to the module-level functions :meth:`match`, :meth:`search` @@ -163,6 +163,16 @@ Compiled regular expression. Instances of this class are created using Using methods is (much) more efficient if the same regex is applied to multiple strings. + The optional second parameter *pos* gives an index in the string where the + search is to start; it defaults to ``0``. This is not completely equivalent + to slicing the string; the ``'^'`` pattern character matches at the real + beginning of the string and at positions just after a newline, but not + necessarily at the index where the search is to start. + + The optional parameter *endpos* limits how far the string will be searched; + it will be as if the string is *endpos* characters long, so only the + characters from *pos* to ``endpos - 1`` will be searched for a match. + .. method:: regex.split(string, max_split=-1, /) Split a *string* using regex. If *max_split* is given, it specifies diff --git a/extmod/modre.c b/extmod/modre.c index d17ec68d50eda..85e5d1b0f74f6 100644 --- a/extmod/modre.c +++ b/extmod/modre.c @@ -196,10 +196,11 @@ static void re_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t // Note: this function can't be named re_exec because it may clash with system headers, eg on FreeBSD static mp_obj_t re_exec_helper(bool is_anchored, uint n_args, const mp_obj_t *args) { - (void)n_args; mp_obj_re_t *self; + bool was_compiled = false; if (mp_obj_is_type(args[0], (mp_obj_type_t *)&re_type)) { self = MP_OBJ_TO_PTR(args[0]); + was_compiled = true; } else { self = MP_OBJ_TO_PTR(mod_re_compile(1, args)); } @@ -207,6 +208,28 @@ static mp_obj_t re_exec_helper(bool is_anchored, uint n_args, const mp_obj_t *ar size_t len; subj.begin_line = subj.begin = mp_obj_str_get_data(args[1], &len); subj.end = subj.begin + len; + + if (was_compiled && n_args > 2) { + // Arg #2 is starting-pos + mp_int_t startpos = mp_obj_get_int(args[2]); + if (startpos > (mp_int_t)len) { + startpos = len; + } else if (startpos < 0) { + startpos = 0; + } + subj.begin += startpos; + if (n_args > 3) { + // Arg #3 is ending-pos + mp_int_t endpos = mp_obj_get_int(args[3]); + if (endpos > (mp_int_t)len) { + endpos = len; + } else if (endpos < startpos) { + endpos = startpos; + } + subj.end = subj.begin_line + endpos; + } + } + int caps_num = (self->re.sub + 1) * 2; mp_obj_match_t *match = m_new_obj_var(mp_obj_match_t, caps, char *, caps_num); // cast is a workaround for a bug in msvc: it treats const char** as a const pointer instead of a pointer to pointer to const char diff --git a/tests/extmod/re_start_end_pos.py b/tests/extmod/re_start_end_pos.py new file mode 100644 index 0000000000000..bd16584374b89 --- /dev/null +++ b/tests/extmod/re_start_end_pos.py @@ -0,0 +1,78 @@ +# test start and end pos specification + +try: + import re +except ImportError: + print("SKIP") + raise SystemExit + + +def print_groups(match): + print("----") + try: + if match is not None: + i = 0 + while True: + print(match.group(i)) + i += 1 + except IndexError: + pass + + +p = re.compile(r"o") +m = p.match("dog") +print_groups(m) + +m = p.match("dog", 1) +print_groups(m) + +m = p.match("dog", 2) +print_groups(m) + +# No match past end of input +m = p.match("dog", 5) +print_groups(m) + +m = p.match("dog", 0, 1) +print_groups(m) + +# Caret only matches the actual beginning +p = re.compile(r"^o") +m = p.match("dog", 1) +print_groups(m) + +# End at beginning means searching empty string +p = re.compile(r"o") +m = p.match("dog", 1, 1) +print_groups(m) + +# End before the beginning doesn't match anything +m = p.match("dog", 2, 1) +print_groups(m) + +# Negative starting values don't crash +m = p.search("dog", -2) +print_groups(m) + +m = p.search("dog", -2, -5) +print_groups(m) + +# Search also works +print("--search") + +p = re.compile(r"o") +m = p.search("dog") +print_groups(m) + +m = p.search("dog", 1) +print_groups(m) + +m = p.search("dog", 2) +print_groups(m) + +# Negative starting values don't crash +m = p.search("dog", -2) +print_groups(m) + +m = p.search("dog", -2, -5) +print_groups(m) From f10707febb41e042e929efaf9ee4a66b847a68d4 Mon Sep 17 00:00:00 2001 From: Jared Hancock Date: Fri, 6 Jun 2025 18:12:29 -0500 Subject: [PATCH 089/143] extmod/modlwip: Support `family` specification in getaddrinfo. `socket.getaddrinfo()` supports the specification of an address family; however, the LwIP implementation does not use it. This change allows the application to specify the address family request in DNS resolution. If no family is specified, it falls back to the default preference configured with `network.ipconfig()`. Signed-off-by: Jared Hancock --- extmod/modlwip.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/extmod/modlwip.c b/extmod/modlwip.c index b84b3b7626bfe..26aab6e81094a 100644 --- a/extmod/modlwip.c +++ b/extmod/modlwip.c @@ -1802,10 +1802,11 @@ static mp_obj_t lwip_getaddrinfo(size_t n_args, const mp_obj_t *args) { mp_obj_t host_in = args[0], port_in = args[1]; const char *host = mp_obj_str_get_str(host_in); mp_int_t port = mp_obj_get_int(port_in); + mp_int_t family = 0; // If constraints were passed then check they are compatible with the supported params if (n_args > 2) { - mp_int_t family = mp_obj_get_int(args[2]); + family = mp_obj_get_int(args[2]); mp_int_t type = 0; mp_int_t proto = 0; mp_int_t flags = 0; @@ -1818,7 +1819,7 @@ static mp_obj_t lwip_getaddrinfo(size_t n_args, const mp_obj_t *args) { } } } - if (!((family == 0 || family == MOD_NETWORK_AF_INET) + if (!((family == 0 || family == MOD_NETWORK_AF_INET || family == MOD_NETWORK_AF_INET6) && (type == 0 || type == MOD_NETWORK_SOCK_STREAM) && proto == 0 && flags == 0)) { @@ -1829,11 +1830,23 @@ static mp_obj_t lwip_getaddrinfo(size_t n_args, const mp_obj_t *args) { getaddrinfo_state_t state; state.status = 0; + #if LWIP_VERSION_MAJOR >= 2 + // If family was specified, then try and resolve the address type as + // requested. Otherwise, use the default from network configuration. + if (family == MOD_NETWORK_AF_INET) { + family = LWIP_DNS_ADDRTYPE_IPV4; + } else if (family == MOD_NETWORK_AF_INET6) { + family = LWIP_DNS_ADDRTYPE_IPV6; + } else { + family = mp_mod_network_prefer_dns_use_ip_version == 4 ? LWIP_DNS_ADDRTYPE_IPV4_IPV6 : LWIP_DNS_ADDRTYPE_IPV6_IPV4; + } + #endif + MICROPY_PY_LWIP_ENTER #if LWIP_VERSION_MAJOR < 2 err_t ret = dns_gethostbyname(host, (ip_addr_t *)&state.ipaddr, lwip_getaddrinfo_cb, &state); #else - err_t ret = dns_gethostbyname_addrtype(host, (ip_addr_t *)&state.ipaddr, lwip_getaddrinfo_cb, &state, mp_mod_network_prefer_dns_use_ip_version == 4 ? LWIP_DNS_ADDRTYPE_IPV4_IPV6 : LWIP_DNS_ADDRTYPE_IPV6_IPV4); + err_t ret = dns_gethostbyname_addrtype(host, (ip_addr_t *)&state.ipaddr, lwip_getaddrinfo_cb, &state, family); #endif MICROPY_PY_LWIP_EXIT From b1d5c656de3f11fc1729f165c8d9709594ccbfb6 Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Thu, 24 Jul 2025 23:48:01 +0200 Subject: [PATCH 090/143] tests/micropython: Remove big ints dependence for viper boundary tests. This commit provides an implementation for viper boundary tests that can work even without big int support. Since it uses a fixed-size buffer to hold values to work with, this should work on any platform as long as its integers are at least 32 bits wide, regardless its configuration on how big integers can get. Signed-off-by: Alessandro Gatti --- ...ntbig.py => viper_ptr16_store_boundary.py} | 43 ++++++++++--------- ....exp => viper_ptr16_store_boundary.py.exp} | 0 ...ntbig.py => viper_ptr32_store_boundary.py} | 42 +++++++++--------- ....exp => viper_ptr32_store_boundary.py.exp} | 0 ...intbig.py => viper_ptr8_store_boundary.py} | 42 +++++++++--------- ...y.exp => viper_ptr8_store_boundary.py.exp} | 0 6 files changed, 67 insertions(+), 60 deletions(-) rename tests/micropython/{viper_ptr16_store_boundary_intbig.py => viper_ptr16_store_boundary.py} (57%) rename tests/micropython/{viper_ptr16_store_boundary_intbig.py.exp => viper_ptr16_store_boundary.py.exp} (100%) rename tests/micropython/{viper_ptr32_store_boundary_intbig.py => viper_ptr32_store_boundary.py} (60%) rename tests/micropython/{viper_ptr32_store_boundary_intbig.py.exp => viper_ptr32_store_boundary.py.exp} (100%) rename tests/micropython/{viper_ptr8_store_boundary_intbig.py => viper_ptr8_store_boundary.py} (55%) rename tests/micropython/{viper_ptr8_store_boundary_intbig.py.exp => viper_ptr8_store_boundary.py.exp} (100%) diff --git a/tests/micropython/viper_ptr16_store_boundary_intbig.py b/tests/micropython/viper_ptr16_store_boundary.py similarity index 57% rename from tests/micropython/viper_ptr16_store_boundary_intbig.py rename to tests/micropython/viper_ptr16_store_boundary.py index 2193eddae1385..3501a05685ec3 100644 --- a/tests/micropython/viper_ptr16_store_boundary_intbig.py +++ b/tests/micropython/viper_ptr16_store_boundary.py @@ -15,6 +15,23 @@ def set{off}(dest: ptr16): MASK = (1 << (8 * SIZE)) - 1 +next_int = 1 +test_buffer = bytearray(SIZE) + + +def next_value() -> uint: + global next_int + global test_buffer + for index in range(1, SIZE): + test_buffer[index - 1] = test_buffer[index] + test_buffer[SIZE - 1] = next_int + next_int += 1 + output = 0 + for byte in test_buffer: + output = (output << 8) | byte + return output & MASK + + @micropython.viper def set_index(dest: ptr16, i: int, val: uint): saved = dest @@ -27,8 +44,6 @@ def get_index(src, i): buffer = bytearray(((1 << max(BIT_THRESHOLDS) + 1) // 1024) * 1024) -next = 1 -val = 0 for bit in BIT_THRESHOLDS: print("---", bit) pre, idx, post = ( @@ -36,22 +51,10 @@ def get_index(src, i): (((1 << bit) - (1 * SIZE)) // SIZE), ((1 << bit) // SIZE), ) - val = (val << 8) + next - next += 1 - set_index(buffer, pre, val & MASK) - val = (val << 8) + next - next += 1 - set_index(buffer, idx, val & MASK) - val = (val << 8) + next - next += 1 - set_index(buffer, post, val & MASK) - val = (val << 8) + next - next += 1 + set_index(buffer, pre, next_value()) + set_index(buffer, idx, next_value()) + set_index(buffer, post, next_value()) print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post))) - exec(SET_TEMPLATE.format(off=pre, val=val & MASK)) - val = (val << 8) + next - next += 1 - exec(SET_TEMPLATE.format(off=idx, val=val & MASK)) - val = (val << 8) + next - next += 1 - exec(SET_TEMPLATE.format(off=post, val=val & MASK)) + exec(SET_TEMPLATE.format(off=pre, val=next_value())) + exec(SET_TEMPLATE.format(off=idx, val=next_value())) + exec(SET_TEMPLATE.format(off=post, val=next_value())) diff --git a/tests/micropython/viper_ptr16_store_boundary_intbig.py.exp b/tests/micropython/viper_ptr16_store_boundary.py.exp similarity index 100% rename from tests/micropython/viper_ptr16_store_boundary_intbig.py.exp rename to tests/micropython/viper_ptr16_store_boundary.py.exp diff --git a/tests/micropython/viper_ptr32_store_boundary_intbig.py b/tests/micropython/viper_ptr32_store_boundary.py similarity index 60% rename from tests/micropython/viper_ptr32_store_boundary_intbig.py rename to tests/micropython/viper_ptr32_store_boundary.py index b44f31b00af4d..8c207278ec538 100644 --- a/tests/micropython/viper_ptr32_store_boundary_intbig.py +++ b/tests/micropython/viper_ptr32_store_boundary.py @@ -14,6 +14,22 @@ def set{off}(dest: ptr32): SIZE = 4 MASK = (1 << (8 * SIZE)) - 1 +next_int = 1 +test_buffer = bytearray(SIZE) + + +def next_value() -> uint: + global next_int + global test_buffer + for index in range(1, SIZE): + test_buffer[index - 1] = test_buffer[index] + test_buffer[SIZE - 1] = next_int + next_int += 1 + output = 0 + for byte in test_buffer: + output = (output << 8) | byte + return output & MASK + @micropython.viper def set_index(dest: ptr32, i: int, val: uint): @@ -32,8 +48,6 @@ def get_index(src, i): buffer = bytearray(((1 << max(BIT_THRESHOLDS) + 1) // 1024) * 1024) -next = 1 -val = 0 for bit in BIT_THRESHOLDS: print("---", bit) pre, idx, post = ( @@ -41,22 +55,10 @@ def get_index(src, i): (((1 << bit) - (1 * SIZE)) // SIZE), ((1 << bit) // SIZE), ) - val = (val << 8) + next - next += 1 - set_index(buffer, pre, val & MASK) - val = (val << 8) + next - next += 1 - set_index(buffer, idx, val & MASK) - val = (val << 8) + next - next += 1 - set_index(buffer, post, val & MASK) - val = (val << 8) + next - next += 1 + set_index(buffer, pre, next_value()) + set_index(buffer, idx, next_value()) + set_index(buffer, post, next_value()) print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post))) - exec(SET_TEMPLATE.format(off=pre, val=val & MASK)) - val = (val << 8) + next - next += 1 - exec(SET_TEMPLATE.format(off=idx, val=val & MASK)) - val = (val << 8) + next - next += 1 - exec(SET_TEMPLATE.format(off=post, val=val & MASK)) + exec(SET_TEMPLATE.format(off=pre, val=next_value())) + exec(SET_TEMPLATE.format(off=idx, val=next_value())) + exec(SET_TEMPLATE.format(off=post, val=next_value())) diff --git a/tests/micropython/viper_ptr32_store_boundary_intbig.py.exp b/tests/micropython/viper_ptr32_store_boundary.py.exp similarity index 100% rename from tests/micropython/viper_ptr32_store_boundary_intbig.py.exp rename to tests/micropython/viper_ptr32_store_boundary.py.exp diff --git a/tests/micropython/viper_ptr8_store_boundary_intbig.py b/tests/micropython/viper_ptr8_store_boundary.py similarity index 55% rename from tests/micropython/viper_ptr8_store_boundary_intbig.py rename to tests/micropython/viper_ptr8_store_boundary.py index d22a062743321..d3cba17e16318 100644 --- a/tests/micropython/viper_ptr8_store_boundary_intbig.py +++ b/tests/micropython/viper_ptr8_store_boundary.py @@ -14,6 +14,22 @@ def set{off}(dest: ptr8): SIZE = 1 MASK = (1 << (8 * SIZE)) - 1 +next_int = 1 +test_buffer = bytearray(SIZE) + + +def next_value() -> uint: + global next_int + global test_buffer + for index in range(1, SIZE): + test_buffer[index - 1] = test_buffer[index] + test_buffer[SIZE - 1] = next_int + next_int += 1 + output = 0 + for byte in test_buffer: + output = (output << 8) | byte + return output & MASK + @micropython.viper def set_index(dest: ptr8, i: int, val: uint): @@ -27,27 +43,13 @@ def get_index(src: ptr8, i: int): buffer = bytearray(((1 << max(BIT_THRESHOLDS) + 1) // 1024) * 1024) -next = 1 -val = 0 for bit in BIT_THRESHOLDS: print("---", bit) pre, idx, post = (((1 << bit) - (2 * SIZE)), ((1 << bit) - (1 * SIZE)), (1 << bit)) - val = (val << 8) + next - next += 1 - set_index(buffer, pre, val & MASK) - val = (val << 8) + next - next += 1 - set_index(buffer, idx, val & MASK) - val = (val << 8) + next - next += 1 - set_index(buffer, post, val & MASK) - val = (val << 8) + next - next += 1 + set_index(buffer, pre, next_value()) + set_index(buffer, idx, next_value()) + set_index(buffer, post, next_value()) print(hex(get_index(buffer, pre)), hex(get_index(buffer, idx)), hex(get_index(buffer, post))) - exec(SET_TEMPLATE.format(off=pre, val=val & MASK)) - val = (val << 8) + next - next += 1 - exec(SET_TEMPLATE.format(off=idx, val=val & MASK)) - val = (val << 8) + next - next += 1 - exec(SET_TEMPLATE.format(off=post, val=val & MASK)) + exec(SET_TEMPLATE.format(off=pre, val=next_value())) + exec(SET_TEMPLATE.format(off=idx, val=next_value())) + exec(SET_TEMPLATE.format(off=post, val=next_value())) diff --git a/tests/micropython/viper_ptr8_store_boundary_intbig.py.exp b/tests/micropython/viper_ptr8_store_boundary.py.exp similarity index 100% rename from tests/micropython/viper_ptr8_store_boundary_intbig.py.exp rename to tests/micropython/viper_ptr8_store_boundary.py.exp From 0ee3f99da24e678a415a1d0b4575c38ad51430fe Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Thu, 10 Jul 2025 00:50:15 +0200 Subject: [PATCH 091/143] py/asmrv32: Make lt/le comparisons emitter shorter. This commit simplifies the emitter code in charge of generating opcodes performing less-than and less-than-or-equal comparisons. By rewriting the SLT/SLTU opcode generator (handling less-than comparisons) and de-inlining the less-than comparison generator call in the less-than-or-equal generator, the output binary is ~80 bytes smaller (measurements taken from the QEMU port). Signed-off-by: Alessandro Gatti --- py/asmrv32.c | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/py/asmrv32.c b/py/asmrv32.c index 158b5521917a6..723d32cdd7c89 100644 --- a/py/asmrv32.c +++ b/py/asmrv32.c @@ -573,12 +573,8 @@ void asm_rv32_meta_comparison_ne(asm_rv32_t *state, mp_uint_t rs1, mp_uint_t rs2 } void asm_rv32_meta_comparison_lt(asm_rv32_t *state, mp_uint_t rs1, mp_uint_t rs2, mp_uint_t rd, bool unsigned_comparison) { - // slt(u) rd, rs1, rs2 - if (unsigned_comparison) { - asm_rv32_opcode_sltu(state, rd, rs1, rs2); - } else { - asm_rv32_opcode_slt(state, rd, rs1, rs2); - } + // slt|sltu rd, rs1, rs2 + asm_rv32_emit_word_opcode(state, RV32_ENCODE_TYPE_R(0x33, (0x02 | (unsigned_comparison ? 1 : 0)), 0x00, rd, rs1, rs2)); } void asm_rv32_meta_comparison_le(asm_rv32_t *state, mp_uint_t rs1, mp_uint_t rs2, mp_uint_t rd, bool unsigned_comparison) { @@ -588,11 +584,7 @@ void asm_rv32_meta_comparison_le(asm_rv32_t *state, mp_uint_t rs1, mp_uint_t rs2 // ... ; PC + 8 asm_rv32_opcode_cli(state, rd, 1); asm_rv32_opcode_beq(state, rs1, rs2, 8); - if (unsigned_comparison) { - asm_rv32_opcode_sltu(state, rd, rs1, rs2); - } else { - asm_rv32_opcode_slt(state, rd, rs1, rs2); - } + asm_rv32_meta_comparison_lt(state, rs1, rs2, rd, unsigned_comparison); } #endif // MICROPY_EMIT_RV32 From 4614ee9e682dfffa26a4226645baca0208ecfac6 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 21 Jul 2025 13:31:59 -0500 Subject: [PATCH 092/143] py/binary: Add MICROPY_PY_STRUCT_UNSAFE_TYPECODES. This adds a compile-time flag to disable some "unsafe" and non-standard typecodes in struct, array and related modules. This is useful to turn off when fuzzing, as improper use of these typecodes can crash MicroPython. Signed-off-by: Jeff Epler --- py/binary.c | 20 ++++++++++++++++---- py/mpconfig.h | 7 +++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/py/binary.c b/py/binary.c index 48d3421bca963..ef2857b4318dc 100644 --- a/py/binary.c +++ b/py/binary.c @@ -69,11 +69,13 @@ size_t mp_binary_get_size(char struct_type, char val_type, size_t *palign) { case 'Q': size = 8; break; + #if MICROPY_PY_STRUCT_UNSAFE_TYPECODES case 'P': case 'O': case 'S': size = sizeof(void *); break; + #endif case 'e': size = 2; break; @@ -119,12 +121,14 @@ size_t mp_binary_get_size(char struct_type, char val_type, size_t *palign) { align = alignof(long long); size = sizeof(long long); break; + #if MICROPY_PY_STRUCT_UNSAFE_TYPECODES case 'P': case 'O': case 'S': align = alignof(void *); size = sizeof(void *); break; + #endif case 'e': align = 2; size = 2; @@ -280,12 +284,14 @@ mp_obj_t mp_binary_get_val_array(char typecode, void *p, size_t index) { case 'd': return mp_obj_new_float_from_d(((double *)p)[index]); #endif - // Extension to CPython: array of objects + // Extension to CPython: array of objects + #if MICROPY_PY_STRUCT_UNSAFE_TYPECODES case 'O': return ((mp_obj_t *)p)[index]; // Extension to CPython: array of pointers case 'P': return mp_obj_new_int((mp_int_t)(uintptr_t)((void **)p)[index]); + #endif } return MP_OBJ_NEW_SMALL_INT(val); } @@ -334,9 +340,9 @@ mp_obj_t mp_binary_get_val(char struct_type, char val_type, byte *p_base, byte * long long val = mp_binary_get_int(size, is_signed(val_type), (struct_type == '>'), p); - if (val_type == 'O') { + if (MICROPY_PY_STRUCT_UNSAFE_TYPECODES && val_type == 'O') { return (mp_obj_t)(mp_uint_t)val; - } else if (val_type == 'S') { + } else if (MICROPY_PY_STRUCT_UNSAFE_TYPECODES && val_type == 'S') { const char *s_val = (const char *)(uintptr_t)(mp_uint_t)val; return mp_obj_new_str_from_cstr(s_val); #if MICROPY_PY_BUILTINS_FLOAT @@ -407,9 +413,11 @@ void mp_binary_set_val(char struct_type, char val_type, mp_obj_t val_in, byte *p mp_uint_t val; switch (val_type) { + #if MICROPY_PY_STRUCT_UNSAFE_TYPECODES case 'O': val = (mp_uint_t)val_in; break; + #endif #if MICROPY_PY_BUILTINS_FLOAT case 'e': val = mp_encode_half_float(mp_obj_get_float_to_f(val_in)); @@ -474,10 +482,12 @@ void mp_binary_set_val_array(char typecode, void *p, size_t index, mp_obj_t val_ ((double *)p)[index] = mp_obj_get_float_to_d(val_in); break; #endif + #if MICROPY_PY_STRUCT_UNSAFE_TYPECODES // Extension to CPython: array of objects case 'O': ((mp_obj_t *)p)[index] = val_in; break; + #endif default: #if MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_NONE if (mp_obj_is_exact_type(val_in, &mp_type_int)) { @@ -534,9 +544,11 @@ void mp_binary_set_val_array_from_int(char typecode, void *p, size_t index, mp_i ((double *)p)[index] = (double)val; break; #endif - // Extension to CPython: array of pointers + // Extension to CPython: array of pointers + #if MICROPY_PY_STRUCT_UNSAFE_TYPECODES case 'P': ((void **)p)[index] = (void *)(uintptr_t)val; break; + #endif } } diff --git a/py/mpconfig.h b/py/mpconfig.h index dd41ff6a70867..5fe0e822f775c 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -1601,6 +1601,13 @@ typedef time_t mp_timestamp_t; #define MICROPY_PY_STRUCT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES) #endif +// Whether struct module provides unsafe and non-standard typecodes O, P, S. +// These typecodes are not in CPython and can cause crashes by accessing arbitrary +// memory. +#ifndef MICROPY_PY_STRUCT_UNSAFE_TYPECODES +#define MICROPY_PY_STRUCT_UNSAFE_TYPECODES (1) +#endif + // Whether to provide "sys" module #ifndef MICROPY_PY_SYS #define MICROPY_PY_SYS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES) From 744270ac1b9ed3929cd41d1a6e1f6ea0e785745d Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 6 Aug 2025 10:35:22 +1000 Subject: [PATCH 093/143] py/misc: Add explicit dependency on py/mpconfig.h. Macros in misc.h depend on values defined by including mpconfig.h. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- py/misc.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py/misc.h b/py/misc.h index 86ac2ec9a1061..5c1cc2f7a836b 100644 --- a/py/misc.h +++ b/py/misc.h @@ -26,6 +26,8 @@ #ifndef MICROPY_INCLUDED_PY_MISC_H #define MICROPY_INCLUDED_PY_MISC_H +#include "py/mpconfig.h" + // a mini library of useful types and functions /** types *******************************************************/ From f493075d88be9ce851c4d401d041b801701975c3 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 10 Aug 2025 22:23:26 +1000 Subject: [PATCH 094/143] tests/run-tests.py: Automatically include float tests when possible. This simplifies the code by removing the explicit addition of the "float/" test directory for certain targets. It also means the tests won't be added incorrectly, eg on a unix build without float. Signed-off-by: Damien George --- tests/feature_check/float.py | 13 ----------- tests/feature_check/float.py.exp | 1 - tests/feature_check/target_info.py | 13 ++++++++++- tests/run-tests.py | 37 +++++++++++++----------------- 4 files changed, 28 insertions(+), 36 deletions(-) delete mode 100644 tests/feature_check/float.py delete mode 100644 tests/feature_check/float.py.exp diff --git a/tests/feature_check/float.py b/tests/feature_check/float.py deleted file mode 100644 index d6d2a99d2429d..0000000000000 --- a/tests/feature_check/float.py +++ /dev/null @@ -1,13 +0,0 @@ -# detect how many bits of precision the floating point implementation has - -try: - float -except NameError: - print(0) -else: - if float("1.0000001") == float("1.0"): - print(30) - elif float("1e300") == float("inf"): - print(32) - else: - print(64) diff --git a/tests/feature_check/float.py.exp b/tests/feature_check/float.py.exp deleted file mode 100644 index 900731ffd51ff..0000000000000 --- a/tests/feature_check/float.py.exp +++ /dev/null @@ -1 +0,0 @@ -64 diff --git a/tests/feature_check/target_info.py b/tests/feature_check/target_info.py index 9501d808ef214..6d95f8b163c09 100644 --- a/tests/feature_check/target_info.py +++ b/tests/feature_check/target_info.py @@ -22,4 +22,15 @@ ][sys_mpy >> 10] thread = getattr(sys.implementation, "_thread", None) -print(platform, arch, thread) +# Detect how many bits of precision the floating point implementation has. +try: + if float("1.0000001") == float("1.0"): + float_prec = 30 + elif float("1e300") == float("inf"): + float_prec = 32 + else: + float_prec = 64 +except NameError: + float_prec = 0 + +print(platform, arch, thread, float_prec) diff --git a/tests/run-tests.py b/tests/run-tests.py index 859f16f484a85..498370f70e90c 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -292,12 +292,13 @@ def detect_test_platform(pyb, args): output = run_feature_check(pyb, args, "target_info.py") if output.endswith(b"CRASH"): raise ValueError("cannot detect platform: {}".format(output)) - platform, arch, thread = str(output, "ascii").strip().split() + platform, arch, thread, float_prec = str(output, "ascii").strip().split() if arch == "None": arch = None inlineasm_arch = detect_inline_asm_arch(pyb, args) if thread == "None": thread = None + float_prec = int(float_prec) args.platform = platform args.arch = arch @@ -305,6 +306,7 @@ def detect_test_platform(pyb, args): args.mpy_cross_flags = "-march=" + arch args.inlineasm_arch = inlineasm_arch args.thread = thread + args.float_prec = float_prec print("platform={}".format(platform), end="") if arch: @@ -313,6 +315,8 @@ def detect_test_platform(pyb, args): print(" inlineasm={}".format(inlineasm_arch), end="") if thread: print(" thread={}".format(thread), end="") + if float_prec: + print(" float={}-bit".format(float_prec), end="") print() @@ -685,8 +689,6 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): has_complex = True has_coverage = False - upy_float_precision = 32 - if True: # Even if we run completely different tests in a different directory, # we need to access feature_checks from the same directory as the @@ -774,11 +776,6 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add("cmdline/repl_words_move.py") upy_byteorder = run_feature_check(pyb, args, "byteorder.py") - upy_float_precision = run_feature_check(pyb, args, "float.py") - try: - upy_float_precision = int(upy_float_precision) - except ValueError: - upy_float_precision = 0 has_complex = run_feature_check(pyb, args, "complex.py") == b"complex\n" has_coverage = run_feature_check(pyb, args, "coverage.py") == b"coverage\n" cpy_byteorder = subprocess.check_output( @@ -814,7 +811,7 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): # fails with stack overflow on Debug builds skip_tests.add("misc/sys_settrace_features.py") - if upy_float_precision == 0: + if args.float_prec == 0: skip_tests.add("extmod/uctypes_le_float.py") skip_tests.add("extmod/uctypes_native_float.py") skip_tests.add("extmod/uctypes_sizeof_float.py") @@ -822,7 +819,7 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add("extmod/json_loads_float.py") skip_tests.add("extmod/random_extra_float.py") skip_tests.add("misc/rge_sm.py") - if upy_float_precision < 32: + if args.float_prec < 32: skip_tests.add( "float/float2int_intbig.py" ) # requires fp32, there's float2int_fp30_intbig.py instead @@ -832,7 +829,7 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add("float/bytes_construct.py") # requires fp32 skip_tests.add("float/bytearray_construct.py") # requires fp32 skip_tests.add("float/float_format_ints_power10.py") # requires fp32 - if upy_float_precision < 64: + if args.float_prec < 64: skip_tests.add("float/float_divmod.py") # tested by float/float_divmod_relaxed.py instead skip_tests.add("float/float2int_doubleprec_intbig.py") skip_tests.add("float/float_struct_e_doubleprec.py") @@ -1348,26 +1345,25 @@ def main(): test_dirs += ("inlineasm/{}".format(args.inlineasm_arch),) if args.thread is not None: test_dirs += ("thread",) + if args.float_prec > 0: + test_dirs += ("float",) if args.platform == "pyboard": # run pyboard tests - test_dirs += ("float", "stress", "ports/stm32") + test_dirs += ("stress", "ports/stm32") elif args.platform == "mimxrt": - test_dirs += ("float", "stress") + test_dirs += ("stress",) elif args.platform == "renesas-ra": - test_dirs += ("float", "ports/renesas-ra") + test_dirs += ("ports/renesas-ra") elif args.platform == "rp2": - test_dirs += ("float", "stress", "ports/rp2") + test_dirs += ("stress", "ports/rp2") elif args.platform == "esp32": - test_dirs += ("float", "stress") - elif args.platform in ("esp8266", "minimal", "samd", "nrf"): - test_dirs += ("float",) + test_dirs += ("stress",) elif args.platform == "WiPy": # run WiPy tests test_dirs += ("ports/cc3200",) elif args.platform in PC_PLATFORMS: # run PC tests test_dirs += ( - "float", "import", "io", "stress", @@ -1377,11 +1373,10 @@ def main(): ) elif args.platform == "qemu": test_dirs += ( - "float", "ports/qemu", ) elif args.platform == "webassembly": - test_dirs += ("float", "ports/webassembly") + test_dirs += ("ports/webassembly",) else: # run tests from these directories test_dirs = args.test_dirs From 656582795504cb6c9b025e716f20a2468e5d71ac Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 10 Aug 2025 22:25:15 +1000 Subject: [PATCH 095/143] tests/run-tests.py: Always include stress/ tests directory in tests. Ports that now run the stress tests, that didn't prior to this commit are: cc3200, esp8266, minimal, nrf, renesas-ra, samd, qemu, webassembly. Signed-off-by: Damien George --- tests/run-tests.py | 10 +++------- tests/stress/dict_copy.py | 7 ++++++- tests/stress/dict_create.py | 6 +++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/run-tests.py b/tests/run-tests.py index 498370f70e90c..21e0ab8872abd 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -1340,6 +1340,7 @@ def main(): "micropython", "misc", "extmod", + "stress", ) if args.inlineasm_arch is not None: test_dirs += ("inlineasm/{}".format(args.inlineasm_arch),) @@ -1349,15 +1350,11 @@ def main(): test_dirs += ("float",) if args.platform == "pyboard": # run pyboard tests - test_dirs += ("stress", "ports/stm32") - elif args.platform == "mimxrt": - test_dirs += ("stress",) + test_dirs += ("ports/stm32",) elif args.platform == "renesas-ra": test_dirs += ("ports/renesas-ra") elif args.platform == "rp2": - test_dirs += ("stress", "ports/rp2") - elif args.platform == "esp32": - test_dirs += ("stress",) + test_dirs += ("ports/rp2",) elif args.platform == "WiPy": # run WiPy tests test_dirs += ("ports/cc3200",) @@ -1366,7 +1363,6 @@ def main(): test_dirs += ( "import", "io", - "stress", "unicode", "cmdline", "ports/unix", diff --git a/tests/stress/dict_copy.py b/tests/stress/dict_copy.py index 73d3a5b51d601..f9b742e20f716 100644 --- a/tests/stress/dict_copy.py +++ b/tests/stress/dict_copy.py @@ -1,6 +1,11 @@ # copying a large dictionary -a = {i: 2 * i for i in range(1000)} +try: + a = {i: 2 * i for i in range(1000)} +except MemoryError: + print("SKIP") + raise SystemExit + b = a.copy() for i in range(1000): print(i, b[i]) diff --git a/tests/stress/dict_create.py b/tests/stress/dict_create.py index e9db40a8e6c5b..91a83a12f9d85 100644 --- a/tests/stress/dict_create.py +++ b/tests/stress/dict_create.py @@ -3,6 +3,10 @@ d = {} x = 1 while x < 1000: - d[x] = x + try: + d[x] = x + except MemoryError: + print("SKIP") + raise SystemExit x += 1 print(d[500]) From 1db71f9e553f2775b2c0326a3de778319a647f90 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 11:13:56 +1000 Subject: [PATCH 096/143] tests/run-tests.py: Generalise addition of port specific test directory. Signed-off-by: Damien George --- tests/run-tests.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/tests/run-tests.py b/tests/run-tests.py index 21e0ab8872abd..8735f93f2b3fe 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -102,6 +102,11 @@ def open(self, path, mode): # Platforms associated with the unix port, values of `sys.platform`. PC_PLATFORMS = ("darwin", "linux", "win32") +# Mapping from `sys.platform` to the port name, for special cases. +# See `platform_to_port()` function. +platform_to_port_map = {"pyboard": "stm32", "WiPy": "cc3200"} +platform_to_port_map.update({p: "unix" for p in PC_PLATFORMS}) + # Tests to skip for specific emitters. emitter_tests_to_skip = { # Some tests are known to fail with native emitter. @@ -252,6 +257,10 @@ def convert_regex_escapes(line): return bytes("".join(cs), "utf8") +def platform_to_port(platform): + return platform_to_port_map.get(platform, platform) + + def get_test_instance(test_instance, baudrate, user, password): if test_instance.startswith("port:"): _, port = test_instance.split(":", 1) @@ -1348,31 +1357,17 @@ def main(): test_dirs += ("thread",) if args.float_prec > 0: test_dirs += ("float",) - if args.platform == "pyboard": - # run pyboard tests - test_dirs += ("ports/stm32",) - elif args.platform == "renesas-ra": - test_dirs += ("ports/renesas-ra") - elif args.platform == "rp2": - test_dirs += ("ports/rp2",) - elif args.platform == "WiPy": - # run WiPy tests - test_dirs += ("ports/cc3200",) - elif args.platform in PC_PLATFORMS: + port_specific_test_dir = "ports/{}".format(platform_to_port(args.platform)) + if os.path.isdir(port_specific_test_dir): + test_dirs += (port_specific_test_dir,) + if args.platform in PC_PLATFORMS: # run PC tests test_dirs += ( "import", "io", "unicode", "cmdline", - "ports/unix", - ) - elif args.platform == "qemu": - test_dirs += ( - "ports/qemu", ) - elif args.platform == "webassembly": - test_dirs += ("ports/webassembly",) else: # run tests from these directories test_dirs = args.test_dirs From e2744ce679269692ceed2bed1e6f4f6a7840b49b Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 11:22:32 +1000 Subject: [PATCH 097/143] tests/run-tests.py: Autodetect if the target has unicode support. The unicode tests are now run on all targets that enable unicode. And other unicode tests (namely `extmod/json_loads.py`) are now properly skipped if the target doesn't have unicode support. Signed-off-by: Damien George --- tests/feature_check/target_info.py | 2 +- tests/run-tests.py | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/feature_check/target_info.py b/tests/feature_check/target_info.py index 6d95f8b163c09..962ebf4154b5c 100644 --- a/tests/feature_check/target_info.py +++ b/tests/feature_check/target_info.py @@ -33,4 +33,4 @@ except NameError: float_prec = 0 -print(platform, arch, thread, float_prec) +print(platform, arch, thread, float_prec, len("α") == 1) diff --git a/tests/run-tests.py b/tests/run-tests.py index 8735f93f2b3fe..e2bdf7c2c0286 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -301,13 +301,14 @@ def detect_test_platform(pyb, args): output = run_feature_check(pyb, args, "target_info.py") if output.endswith(b"CRASH"): raise ValueError("cannot detect platform: {}".format(output)) - platform, arch, thread, float_prec = str(output, "ascii").strip().split() + platform, arch, thread, float_prec, unicode = str(output, "ascii").strip().split() if arch == "None": arch = None inlineasm_arch = detect_inline_asm_arch(pyb, args) if thread == "None": thread = None float_prec = int(float_prec) + unicode = unicode == "True" args.platform = platform args.arch = arch @@ -316,6 +317,7 @@ def detect_test_platform(pyb, args): args.inlineasm_arch = inlineasm_arch args.thread = thread args.float_prec = float_prec + args.unicode = unicode print("platform={}".format(platform), end="") if arch: @@ -326,6 +328,8 @@ def detect_test_platform(pyb, args): print(" thread={}".format(thread), end="") if float_prec: print(" float={}-bit".format(float_prec), end="") + if unicode: + print(" unicode", end="") print() @@ -845,6 +849,9 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add("float/float_format_ints_doubleprec.py") skip_tests.add("float/float_parse_doubleprec.py") + if not args.unicode: + skip_tests.add("extmod/json_loads.py") # tests loading a utf-8 character + if not has_complex: skip_tests.add("float/complex1.py") skip_tests.add("float/complex1_intbig.py") @@ -870,6 +877,9 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): if args.platform not in PC_PLATFORMS: skip_tests.add("basics/exception_chain.py") # warning is not printed skip_tests.add("micropython/meminfo.py") # output is very different to PC output + skip_tests.add("unicode/file1.py") # requires local file access + skip_tests.add("unicode/file2.py") # requires local file access + skip_tests.add("unicode/file_invalid.py") # requires local file access # Skip emitter-specific tests. skip_tests.update(emitter_tests_to_skip.get(args.emit, ())) @@ -1357,6 +1367,8 @@ def main(): test_dirs += ("thread",) if args.float_prec > 0: test_dirs += ("float",) + if args.unicode: + test_dirs += ("unicode",) port_specific_test_dir = "ports/{}".format(platform_to_port(args.platform)) if os.path.isdir(port_specific_test_dir): test_dirs += (port_specific_test_dir,) @@ -1365,7 +1377,6 @@ def main(): test_dirs += ( "import", "io", - "unicode", "cmdline", ) else: From 54e6cfc6e3f3c2056bf2f06d32a58932d29f1ac3 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 11:45:55 +1000 Subject: [PATCH 098/143] tests/basics: Skip tests of io module individually using SKIP. Instead of using a feature check. This is more consistent with how other optional modules are skipped. Signed-off-by: Damien George --- tests/basics/io_buffered_writer.py | 6 +++--- tests/basics/io_bytesio_cow.py | 8 +++++++- tests/basics/io_bytesio_ext.py | 8 +++++++- tests/basics/io_bytesio_ext2.py | 7 ++++++- tests/basics/io_iobase.py | 7 ++++--- tests/basics/io_stringio1.py | 7 ++++++- tests/basics/io_stringio_base.py | 6 +++++- tests/basics/io_stringio_with.py | 7 ++++++- tests/basics/io_write_ext.py | 5 +++-- tests/feature_check/io_module.py | 6 ------ tests/feature_check/io_module.py.exp | 0 tests/run-tests.py | 8 -------- 12 files changed, 47 insertions(+), 28 deletions(-) delete mode 100644 tests/feature_check/io_module.py delete mode 100644 tests/feature_check/io_module.py.exp diff --git a/tests/basics/io_buffered_writer.py b/tests/basics/io_buffered_writer.py index 60cf2c837d10f..3cfee0103f777 100644 --- a/tests/basics/io_buffered_writer.py +++ b/tests/basics/io_buffered_writer.py @@ -1,9 +1,9 @@ -import io - try: + import io + io.BytesIO io.BufferedWriter -except AttributeError: +except (AttributeError, ImportError): print('SKIP') raise SystemExit diff --git a/tests/basics/io_bytesio_cow.py b/tests/basics/io_bytesio_cow.py index 2edb7136a9691..543c12ad42ab2 100644 --- a/tests/basics/io_bytesio_cow.py +++ b/tests/basics/io_bytesio_cow.py @@ -1,6 +1,12 @@ # Make sure that write operations on io.BytesIO don't # change original object it was constructed from. -import io + +try: + import io +except ImportError: + print("SKIP") + raise SystemExit + b = b"foobar" a = io.BytesIO(b) diff --git a/tests/basics/io_bytesio_ext.py b/tests/basics/io_bytesio_ext.py index 4d4c60c1363b7..92e715178116c 100644 --- a/tests/basics/io_bytesio_ext.py +++ b/tests/basics/io_bytesio_ext.py @@ -1,5 +1,11 @@ # Extended stream operations on io.BytesIO -import io + +try: + import io +except ImportError: + print("SKIP") + raise SystemExit + a = io.BytesIO(b"foobar") a.seek(10) print(a.read(10)) diff --git a/tests/basics/io_bytesio_ext2.py b/tests/basics/io_bytesio_ext2.py index 414ac90a3b083..f60a6a9a6041e 100644 --- a/tests/basics/io_bytesio_ext2.py +++ b/tests/basics/io_bytesio_ext2.py @@ -1,4 +1,9 @@ -import io +try: + import io +except ImportError: + print("SKIP") + raise SystemExit + a = io.BytesIO(b"foobar") try: a.seek(-10) diff --git a/tests/basics/io_iobase.py b/tests/basics/io_iobase.py index d3824c177f3b3..c01ca6a5073d6 100644 --- a/tests/basics/io_iobase.py +++ b/tests/basics/io_iobase.py @@ -1,8 +1,9 @@ -import io try: + import io + io.IOBase -except AttributeError: - print('SKIP') +except (AttributeError, ImportError): + print("SKIP") raise SystemExit diff --git a/tests/basics/io_stringio1.py b/tests/basics/io_stringio1.py index 7d355930f5a29..889e3ce697377 100644 --- a/tests/basics/io_stringio1.py +++ b/tests/basics/io_stringio1.py @@ -1,4 +1,9 @@ -import io +try: + import io +except ImportError: + print("SKIP") + raise SystemExit + a = io.StringIO() print('io.StringIO' in repr(a)) print(a.getvalue()) diff --git a/tests/basics/io_stringio_base.py b/tests/basics/io_stringio_base.py index 0f65fb3fabc3b..c8890dab73ab7 100644 --- a/tests/basics/io_stringio_base.py +++ b/tests/basics/io_stringio_base.py @@ -1,7 +1,11 @@ # Checks that an instance type inheriting from a native base that uses # MP_TYPE_FLAG_ITER_IS_STREAM will still have a getiter. -import io +try: + import io +except ImportError: + print("SKIP") + raise SystemExit a = io.StringIO() a.write("hello\nworld\nmicro\npython\n") diff --git a/tests/basics/io_stringio_with.py b/tests/basics/io_stringio_with.py index a3aa6ec84e066..0155ad5382dcd 100644 --- a/tests/basics/io_stringio_with.py +++ b/tests/basics/io_stringio_with.py @@ -1,4 +1,9 @@ -import io +try: + import io +except ImportError: + print("SKIP") + raise SystemExit + # test __enter__/__exit__ with io.StringIO() as b: b.write("foo") diff --git a/tests/basics/io_write_ext.py b/tests/basics/io_write_ext.py index 695abccef4421..5af1de7a6c3aa 100644 --- a/tests/basics/io_write_ext.py +++ b/tests/basics/io_write_ext.py @@ -1,10 +1,11 @@ # This tests extended (MicroPython-specific) form of write: # write(buf, len) and write(buf, offset, len) -import io try: + import io + io.BytesIO -except AttributeError: +except (AttributeError, ImportError): print('SKIP') raise SystemExit diff --git a/tests/feature_check/io_module.py b/tests/feature_check/io_module.py deleted file mode 100644 index 9094e605316ba..0000000000000 --- a/tests/feature_check/io_module.py +++ /dev/null @@ -1,6 +0,0 @@ -try: - import io - - print("io") -except ImportError: - print("no") diff --git a/tests/feature_check/io_module.py.exp b/tests/feature_check/io_module.py.exp deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/tests/run-tests.py b/tests/run-tests.py index e2bdf7c2c0286..958ddb1dc80e8 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -695,7 +695,6 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_async = False skip_const = False skip_revops = False - skip_io_module = False skip_fstring = False skip_endian = False skip_inlineasm = False @@ -752,11 +751,6 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): if output == b"TypeError\n": skip_revops = True - # Check if io module exists, and skip such tests if it doesn't - output = run_feature_check(pyb, args, "io_module.py") - if output != b"io\n": - skip_io_module = True - # Check if fstring feature is enabled, and skip such tests if it doesn't output = run_feature_check(pyb, args, "fstring.py") if output != b"a=1\n": @@ -925,7 +919,6 @@ def run_one_test(test_file): is_slice = test_name.find("slice") != -1 or test_name in misc_slice_tests is_async = test_name.startswith(("async_", "asyncio_")) or test_name.endswith("_async") is_const = test_name.startswith("const") - is_io_module = test_name.startswith("io_") is_fstring = test_name.startswith("string_fstring") is_inlineasm = test_name.startswith("asm") @@ -940,7 +933,6 @@ def run_one_test(test_file): skip_it |= skip_async and is_async skip_it |= skip_const and is_const skip_it |= skip_revops and "reverse_op" in test_name - skip_it |= skip_io_module and is_io_module skip_it |= skip_fstring and is_fstring skip_it |= skip_inlineasm and is_inlineasm From f9b6d8e6088545f4d934ca4b4c2baa69ee62e89e Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 12:26:40 +1000 Subject: [PATCH 099/143] github/workflows: Run webassembly and zephyr workflows if tests/ change. Because these ports run tests as part of CI, and need to be run if any of the tests change, to check those changes. Signed-off-by: Damien George --- .github/workflows/ports_webassembly.yml | 1 + .github/workflows/ports_zephyr.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ports_webassembly.yml b/.github/workflows/ports_webassembly.yml index 880f15ab34469..ceb35f83cd8f1 100644 --- a/.github/workflows/ports_webassembly.yml +++ b/.github/workflows/ports_webassembly.yml @@ -11,6 +11,7 @@ on: - 'shared/**' - 'lib/**' - 'ports/webassembly/**' + - 'tests/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/ports_zephyr.yml b/.github/workflows/ports_zephyr.yml index eb85af6a36154..ca3c36e004cca 100644 --- a/.github/workflows/ports_zephyr.yml +++ b/.github/workflows/ports_zephyr.yml @@ -11,6 +11,7 @@ on: - 'shared/**' - 'lib/**' - 'ports/zephyr/**' + - 'tests/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} From c16fe5b5ede3dde468bb65cc15e3e072101b0295 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 13:26:19 +1000 Subject: [PATCH 100/143] webassembly: Enable C-stack checking. This gets the recursive stress-tests working on this port. For relatively small Python functions the maximum recursive depth is about 150 nested calls. Signed-off-by: Damien George --- ports/webassembly/main.c | 5 +++++ ports/webassembly/mpconfigport.h | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ports/webassembly/main.c b/ports/webassembly/main.c index 770dfbe0ca5c7..e0e2d59ae6bd0 100644 --- a/ports/webassembly/main.c +++ b/ports/webassembly/main.c @@ -49,6 +49,9 @@ // the top-level call into C. static size_t external_call_depth = 0; +// Emscripten defaults to a 64k C-stack, so our limit should be less than that. +#define CSTACK_SIZE (32 * 1024) + #if MICROPY_GC_SPLIT_HEAP_AUTO static void gc_collect_top_level(void); #endif @@ -67,6 +70,8 @@ void external_call_depth_dec(void) { } void mp_js_init(int pystack_size, int heap_size) { + mp_cstack_init_with_sp_here(CSTACK_SIZE); + #if MICROPY_ENABLE_PYSTACK mp_obj_t *pystack = (mp_obj_t *)malloc(pystack_size * sizeof(mp_obj_t)); mp_pystack_init(pystack, pystack + pystack_size); diff --git a/ports/webassembly/mpconfigport.h b/ports/webassembly/mpconfigport.h index 544e76f80540d..0783aacbbcd24 100644 --- a/ports/webassembly/mpconfigport.h +++ b/ports/webassembly/mpconfigport.h @@ -44,7 +44,6 @@ #define MICROPY_READER_VFS (MICROPY_VFS) #define MICROPY_ENABLE_GC (1) #define MICROPY_ENABLE_PYSTACK (1) -#define MICROPY_STACK_CHECK (0) #define MICROPY_KBD_EXCEPTION (1) #define MICROPY_REPL_EVENT_DRIVEN (1) #define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_MPZ) From 0cb2c69b3f25816416f7141ca32697a3135d8e53 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 13:51:58 +1000 Subject: [PATCH 101/143] tests/misc/rge_sm.py: Remove unused code from the test. This cleans up the test to remove all unused code, making it smaller, a bit faster to deploy to a target to run, and also use less RAM on the target (which may help it run on targets that are just slightly out of memory running it). Signed-off-by: Damien George --- tests/misc/rge_sm.py | 49 -------------------------------------------- 1 file changed, 49 deletions(-) diff --git a/tests/misc/rge_sm.py b/tests/misc/rge_sm.py index f5b0910dd3a3f..56dad5749776e 100644 --- a/tests/misc/rge_sm.py +++ b/tests/misc/rge_sm.py @@ -39,14 +39,6 @@ def solve(self, finishtime): if not self.iterate(): break - def solveNSteps(self, nSteps): - for i in range(nSteps): - if not self.iterate(): - break - - def series(self): - return zip(*self.Trajectory) - # 1-loop RGES for the main parameters of the SM # couplings are: g1, g2, g3 of U(1), SU(2), SU(3); yt (top Yukawa), lambda (Higgs quartic) @@ -79,45 +71,6 @@ def series(self): ) -def drange(start, stop, step): - r = start - while r < stop: - yield r - r += step - - -def phaseDiagram(system, trajStart, trajPlot, h=0.1, tend=1.0, range=1.0): - tstart = 0.0 - for i in drange(0, range, 0.1 * range): - for j in drange(0, range, 0.1 * range): - rk = RungeKutta(system, trajStart(i, j), tstart, h) - rk.solve(tend) - # draw the line - for tr in rk.Trajectory: - x, y = trajPlot(tr) - print(x, y) - print() - # draw the arrow - continue - l = (len(rk.Trajectory) - 1) / 3 - if l > 0 and 2 * l < len(rk.Trajectory): - p1 = rk.Trajectory[l] - p2 = rk.Trajectory[2 * l] - x1, y1 = trajPlot(p1) - x2, y2 = trajPlot(p2) - dx = -0.5 * (y2 - y1) # orthogonal to line - dy = 0.5 * (x2 - x1) # orthogonal to line - # l = math.sqrt(dx*dx + dy*dy) - # if abs(l) > 1e-3: - # l = 0.1 / l - # dx *= l - # dy *= l - print(x1 + dx, y1 + dy) - print(x2, y2) - print(x1 - dx, y1 - dy) - print() - - def singleTraj(system, trajStart, h=0.02, tend=1.0): is_REPR_C = float("1.0000001") == float("1.0") tstart = 0.0 @@ -141,7 +94,5 @@ def singleTraj(system, trajStart, h=0.02, tend=1.0): print(tr_str) -# phaseDiagram(sysSM, (lambda i, j: [0.354, 0.654, 1.278, 0.8 + 0.2 * i, 0.1 + 0.1 * j]), (lambda a: (a[4], a[5])), h=0.1, tend=math.log(10**17)) - # initial conditions at M_Z singleTraj(sysSM, [0.354, 0.654, 1.278, 0.983, 0.131], h=0.5, tend=math.log(10**17)) # true values From 0f5f6484a2e6f330d03ee4786011ccb8062ac325 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 10 Aug 2025 12:05:32 +1000 Subject: [PATCH 102/143] tests/run-tests.py: Add support for .native.exp expected output files. There are currently a few tests that are excluded when using the native emitter because they test printing of exception tracebacks, which includes line numbers. And the native emitter doesn't store line numbers, so gets these tests wrong. But we'd still like to run these tests using the native emitter, because they test useful things even if the line number info is not in the traceback (eg that threads which crash print out their exception). This commit adds support for native-specific .exp files, which are of the form `.py.native.exp`. If such an .exp file exists then it take precedence over any normal `.py.exp` file. (Actually, the implementation here is general enough that it also supports `.py.bytecode.exp` as well, if bytecode ever needs a specific exp file.) Signed-off-by: Damien George --- tests/run-tests.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/run-tests.py b/tests/run-tests.py index 958ddb1dc80e8..d0a9121b4a4ca 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -994,7 +994,11 @@ def run_one_test(test_file): # Expected output is result of running unittest. output_expected = None else: - test_file_expected = test_file + ".exp" + # Prefer emitter-specific expected output. + test_file_expected = test_file + "." + args.emit + ".exp" + if not os.path.isfile(test_file_expected): + # Fall back to generic expected output. + test_file_expected = test_file + ".exp" if os.path.isfile(test_file_expected): # Expected output given by a file, so read that in. with open(test_file_expected, "rb") as f: @@ -1202,8 +1206,8 @@ def main(): {test_directory_description} When running tests, run-tests.py compares the MicroPython output of the test with the output -produced by running the test through CPython unless a .exp file is found, in which -case it is used as comparison. +produced by running the test through CPython unless a .exp file is found (or a +.native.exp file when using the native emitter), in which case it is used as comparison. If a test fails, run-tests.py produces a pair of .out and .exp files in the result directory with the MicroPython output and the expectations, respectively. From 95d1794afdbef4d0b57af321c0a5d69320dbe9fd Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 10 Aug 2025 12:04:58 +1000 Subject: [PATCH 103/143] tests/misc/print_exception.py: Use "raise e" instead of no-arg "raise". This allows the test to run with the native emitter. The test semantics remain the same. Signed-off-by: Damien George --- tests/misc/print_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/misc/print_exception.py b/tests/misc/print_exception.py index 92754733b58b1..d41478360faf3 100644 --- a/tests/misc/print_exception.py +++ b/tests/misc/print_exception.py @@ -71,7 +71,7 @@ def g(): except Exception as e: print("reraise") print_exc(e) - raise + raise e except Exception as e: print("caught") print_exc(e) From 3c72c3a1e60cecc6ab05d5a81a1b50f42999f10f Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 14:07:09 +1000 Subject: [PATCH 104/143] tests/micropython/opt_level_lineno.py: Force test func to use bytecode. So that the test can run the same on all targets when used with the native emitter. Signed-off-by: Damien George --- tests/micropython/opt_level_lineno.py | 13 ++++++++++++- tests/micropython/opt_level_lineno.py.exp | 2 +- tests/run-tests.py | 1 - 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/micropython/opt_level_lineno.py b/tests/micropython/opt_level_lineno.py index d8253e54b41f0..dda9092d868cf 100644 --- a/tests/micropython/opt_level_lineno.py +++ b/tests/micropython/opt_level_lineno.py @@ -3,4 +3,15 @@ # check that level 3 doesn't store line numbers # the expected output is that any line is printed as "line 1" micropython.opt_level(3) -exec("try:\n xyz\nexcept NameError as er:\n import sys\n sys.print_exception(er)") + +# force bytecode emitter, because native emitter doesn't store line numbers +exec(""" +@micropython.bytecode +def f(): + try: + xyz + except NameError as er: + import sys + sys.print_exception(er) +f() +""") diff --git a/tests/micropython/opt_level_lineno.py.exp b/tests/micropython/opt_level_lineno.py.exp index 469b90ba7938a..b50f0628c81fb 100644 --- a/tests/micropython/opt_level_lineno.py.exp +++ b/tests/micropython/opt_level_lineno.py.exp @@ -1,3 +1,3 @@ Traceback (most recent call last): - File "", line 1, in + File "", line 1, in f NameError: name 'xyz' isn't defined diff --git a/tests/run-tests.py b/tests/run-tests.py index d0a9121b4a4ca..f8174b2d3b0c2 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -131,7 +131,6 @@ def open(self, path, mode): "misc/print_exception.py", "micropython/emg_exc.py", "micropython/heapalloc_traceback.py", - "micropython/opt_level_lineno.py", "thread/thread_exc2.py", # These require stack-allocated slice optimisation. "micropython/heapalloc_slice.py", From a279c64046445c9dbde0a5d3d53d467cb7e43550 Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 10 Aug 2025 12:06:16 +1000 Subject: [PATCH 105/143] tests: Add .native.exp output files for tests that differ with native. Signed-off-by: Damien George --- tests/basics/sys_tracebacklimit.py.native.exp | 22 +++++++++++++++++++ tests/micropython/emg_exc.py.native.exp | 2 ++ .../heapalloc_traceback.py.native.exp | 3 +++ tests/misc/print_exception.py.native.exp | 18 +++++++++++++++ tests/run-tests.py | 6 ----- tests/thread/thread_exc2.py.native.exp | 3 +++ 6 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 tests/basics/sys_tracebacklimit.py.native.exp create mode 100644 tests/micropython/emg_exc.py.native.exp create mode 100644 tests/micropython/heapalloc_traceback.py.native.exp create mode 100644 tests/misc/print_exception.py.native.exp create mode 100644 tests/thread/thread_exc2.py.native.exp diff --git a/tests/basics/sys_tracebacklimit.py.native.exp b/tests/basics/sys_tracebacklimit.py.native.exp new file mode 100644 index 0000000000000..f9d30c058564b --- /dev/null +++ b/tests/basics/sys_tracebacklimit.py.native.exp @@ -0,0 +1,22 @@ +ValueError: value + +limit 4 +ValueError: value + +limit 3 +ValueError: value + +limit 2 +ValueError: value + +limit 1 +ValueError: value + +limit 0 +ValueError: value + +limit -1 +ValueError: value + +True +False diff --git a/tests/micropython/emg_exc.py.native.exp b/tests/micropython/emg_exc.py.native.exp new file mode 100644 index 0000000000000..9677c526a9cc8 --- /dev/null +++ b/tests/micropython/emg_exc.py.native.exp @@ -0,0 +1,2 @@ +ValueError: 1 + diff --git a/tests/micropython/heapalloc_traceback.py.native.exp b/tests/micropython/heapalloc_traceback.py.native.exp new file mode 100644 index 0000000000000..d6ac26aa829e1 --- /dev/null +++ b/tests/micropython/heapalloc_traceback.py.native.exp @@ -0,0 +1,3 @@ +StopIteration +StopIteration: + diff --git a/tests/misc/print_exception.py.native.exp b/tests/misc/print_exception.py.native.exp new file mode 100644 index 0000000000000..59e856ae3c44a --- /dev/null +++ b/tests/misc/print_exception.py.native.exp @@ -0,0 +1,18 @@ +caught +Exception: msg + +caught +Exception: fail + +finally +caught +Exception: fail + +reraise +Exception: fail + +caught +Exception: fail + +AttributeError: 'function' object has no attribute 'X' + diff --git a/tests/run-tests.py b/tests/run-tests.py index f8174b2d3b0c2..aeea0bad5e779 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -126,12 +126,6 @@ def open(self, path, mode): "basics/unboundlocal.py", # These require "raise from". "basics/exception_chain.py", - # These require proper traceback info. - "basics/sys_tracebacklimit.py", - "misc/print_exception.py", - "micropython/emg_exc.py", - "micropython/heapalloc_traceback.py", - "thread/thread_exc2.py", # These require stack-allocated slice optimisation. "micropython/heapalloc_slice.py", # These require running the scheduler. diff --git a/tests/thread/thread_exc2.py.native.exp b/tests/thread/thread_exc2.py.native.exp new file mode 100644 index 0000000000000..9b2e715ef8dfc --- /dev/null +++ b/tests/thread/thread_exc2.py.native.exp @@ -0,0 +1,3 @@ +Unhandled exception in thread started by +ValueError: +done From 2bba507148e8a751123257fa2b075c70d67e1160 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 12 Aug 2025 10:48:16 +1000 Subject: [PATCH 106/143] tests: Require SSL certificate file to be available for test to run. Previously, any test needing an SSL certificate file would automatically skip if the file could not be found. But that makes it too easy to accidentally skip tests. Instead, change it so that the test fails if the certificate file doesn't exist. That matches, for example, the fact that the test fails if networking (LAN, WiFi) is not active. Signed-off-by: Damien George --- tests/multi_net/asyncio_tls_server_client.py | 7 ------- .../asyncio_tls_server_client_cert_required_error.py | 7 ------- tests/multi_net/asyncio_tls_server_client_readline.py | 7 ------- tests/multi_net/asyncio_tls_server_client_verify_error.py | 7 ------- tests/multi_net/ssl_cert_ec.py | 7 ------- tests/multi_net/ssl_cert_rsa.py | 7 ------- tests/multi_net/sslcontext_check_hostname_error.py | 7 ------- tests/multi_net/sslcontext_getpeercert.py | 7 ------- tests/multi_net/sslcontext_server_client_files.py | 7 ------- tests/multi_net/sslcontext_verify_error.py | 7 ------- tests/multi_net/sslcontext_verify_time_error.py | 7 ------- tests/net_inet/asyncio_tls_open_connection_readline.py | 6 ------ tests/net_inet/test_sslcontext_client.py | 5 ----- 13 files changed, 88 deletions(-) diff --git a/tests/multi_net/asyncio_tls_server_client.py b/tests/multi_net/asyncio_tls_server_client.py index 98f15c6625f0e..016f57970c092 100644 --- a/tests/multi_net/asyncio_tls_server_client.py +++ b/tests/multi_net/asyncio_tls_server_client.py @@ -14,13 +14,6 @@ cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - async def handle_connection(reader, writer): data = await reader.read(100) diff --git a/tests/multi_net/asyncio_tls_server_client_cert_required_error.py b/tests/multi_net/asyncio_tls_server_client_cert_required_error.py index 178ad39274032..eac9a98beae6c 100644 --- a/tests/multi_net/asyncio_tls_server_client_cert_required_error.py +++ b/tests/multi_net/asyncio_tls_server_client_cert_required_error.py @@ -14,13 +14,6 @@ cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - async def handle_connection(reader, writer): print("handle connection") diff --git a/tests/multi_net/asyncio_tls_server_client_readline.py b/tests/multi_net/asyncio_tls_server_client_readline.py index da5f1afee2ad6..6093628ce5b7b 100644 --- a/tests/multi_net/asyncio_tls_server_client_readline.py +++ b/tests/multi_net/asyncio_tls_server_client_readline.py @@ -14,13 +14,6 @@ cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - async def handle_connection(reader, writer): data = await reader.readline() diff --git a/tests/multi_net/asyncio_tls_server_client_verify_error.py b/tests/multi_net/asyncio_tls_server_client_verify_error.py index 362f0fc8ecf72..6dbff0482c8ae 100644 --- a/tests/multi_net/asyncio_tls_server_client_verify_error.py +++ b/tests/multi_net/asyncio_tls_server_client_verify_error.py @@ -14,13 +14,6 @@ cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - async def handle_connection(reader, writer): print("handle connection") diff --git a/tests/multi_net/ssl_cert_ec.py b/tests/multi_net/ssl_cert_ec.py index 2c5734e052661..8ecbd4d34f6e8 100644 --- a/tests/multi_net/ssl_cert_ec.py +++ b/tests/multi_net/ssl_cert_ec.py @@ -13,13 +13,6 @@ certfile = "ec_cert.der" keyfile = "ec_key.der" -try: - os.stat(certfile) - os.stat(keyfile) -except OSError: - print("SKIP") - raise SystemExit - with open(certfile, "rb") as cf: cert = cadata = cf.read() diff --git a/tests/multi_net/ssl_cert_rsa.py b/tests/multi_net/ssl_cert_rsa.py index d148c8ebcfea8..b99493b0ae5d8 100644 --- a/tests/multi_net/ssl_cert_rsa.py +++ b/tests/multi_net/ssl_cert_rsa.py @@ -13,13 +13,6 @@ certfile = "rsa_cert.der" keyfile = "rsa_key.der" -try: - os.stat(certfile) - os.stat(keyfile) -except OSError: - print("SKIP") - raise SystemExit - with open(certfile, "rb") as cf: cert = cadata = cf.read() diff --git a/tests/multi_net/sslcontext_check_hostname_error.py b/tests/multi_net/sslcontext_check_hostname_error.py index d85363f00bd63..6bd911ddfd7cc 100644 --- a/tests/multi_net/sslcontext_check_hostname_error.py +++ b/tests/multi_net/sslcontext_check_hostname_error.py @@ -14,13 +14,6 @@ cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/sslcontext_getpeercert.py b/tests/multi_net/sslcontext_getpeercert.py index e9d96be248c70..e2d2a4251163b 100644 --- a/tests/multi_net/sslcontext_getpeercert.py +++ b/tests/multi_net/sslcontext_getpeercert.py @@ -15,13 +15,6 @@ cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/sslcontext_server_client_files.py b/tests/multi_net/sslcontext_server_client_files.py index 64a4215c75b44..48ff6376fad9b 100644 --- a/tests/multi_net/sslcontext_server_client_files.py +++ b/tests/multi_net/sslcontext_server_client_files.py @@ -14,13 +14,6 @@ cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/sslcontext_verify_error.py b/tests/multi_net/sslcontext_verify_error.py index 5dc461e7708a8..07dcc690b7ccc 100644 --- a/tests/multi_net/sslcontext_verify_error.py +++ b/tests/multi_net/sslcontext_verify_error.py @@ -14,13 +14,6 @@ cert = cafile = "ec_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/multi_net/sslcontext_verify_time_error.py b/tests/multi_net/sslcontext_verify_time_error.py index fbefdecf9f430..7b986322d15bd 100644 --- a/tests/multi_net/sslcontext_verify_time_error.py +++ b/tests/multi_net/sslcontext_verify_time_error.py @@ -14,13 +14,6 @@ cert = cafile = "expired_cert.der" key = "ec_key.der" -try: - os.stat(cafile) - os.stat(key) -except OSError: - print("SKIP") - raise SystemExit - # Server def instance0(): diff --git a/tests/net_inet/asyncio_tls_open_connection_readline.py b/tests/net_inet/asyncio_tls_open_connection_readline.py index 70145d91a794b..a0df88be4af1b 100644 --- a/tests/net_inet/asyncio_tls_open_connection_readline.py +++ b/tests/net_inet/asyncio_tls_open_connection_readline.py @@ -20,12 +20,6 @@ ca_cert_chain = "isrg.der" -try: - os.stat(ca_cert_chain) -except OSError: - print("SKIP") - raise SystemExit - with open(ca_cert_chain, "rb") as ca: cadata = ca.read() diff --git a/tests/net_inet/test_sslcontext_client.py b/tests/net_inet/test_sslcontext_client.py index 0c83abb733378..77f68da496a77 100644 --- a/tests/net_inet/test_sslcontext_client.py +++ b/tests/net_inet/test_sslcontext_client.py @@ -13,11 +13,6 @@ # $ openssl x509 -in mpycert.pem -out mpycert.der -outform DER ca_cert_chain = "mpycert.der" -try: - os.stat(ca_cert_chain) -except OSError: - print("SKIP") - raise SystemExit def main(use_stream=True): From e15219800e4874a6c2653cfbdd0553c37610aeff Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Tue, 5 Aug 2025 17:19:02 -0500 Subject: [PATCH 107/143] extmod/modframebuf: Fix crash in scroll() for large inputs. If mp_int_t is wider than int, then the tests such as `xend < 0` can fail even when the amount of scrolling requested is out of range. This resulted in a segmentation fault when attempting an out-of-bounds access to the framebuffer. Signed-off-by: Jeff Epler --- extmod/modframebuf.c | 29 +++++++++++++++-------------- tests/extmod/framebuf_scroll.py | 5 +++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/extmod/modframebuf.c b/extmod/modframebuf.c index 5c4b9abf0c0b8..16606109ccdc4 100644 --- a/extmod/modframebuf.c +++ b/extmod/modframebuf.c @@ -787,39 +787,40 @@ static mp_obj_t framebuf_scroll(mp_obj_t self_in, mp_obj_t xstep_in, mp_obj_t ys mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(self_in); mp_int_t xstep = mp_obj_get_int(xstep_in); mp_int_t ystep = mp_obj_get_int(ystep_in); - int sx, y, xend, yend, dx, dy; + unsigned int sx, y, xend, yend; + int dx, dy; if (xstep < 0) { - sx = 0; - xend = self->width + xstep; - if (xend <= 0) { + if (-xstep >= self->width) { return mp_const_none; } + sx = 0; + xend = self->width + (int)xstep; dx = 1; } else { - sx = self->width - 1; - xend = xstep - 1; - if (xend >= sx) { + if (xstep >= self->width) { return mp_const_none; } + sx = self->width - 1; + xend = (int)xstep - 1; dx = -1; } if (ystep < 0) { - y = 0; - yend = self->height + ystep; - if (yend <= 0) { + if (-ystep >= self->height) { return mp_const_none; } + y = 0; + yend = self->height + (int)ystep; dy = 1; } else { - y = self->height - 1; - yend = ystep - 1; - if (yend >= y) { + if (ystep >= self->height) { return mp_const_none; } + y = self->height - 1; + yend = (int)ystep - 1; dy = -1; } for (; y != yend; y += dy) { - for (int x = sx; x != xend; x += dx) { + for (unsigned x = sx; x != xend; x += dx) { setpixel(self, x, y, getpixel(self, x - xstep, y - ystep)); } } diff --git a/tests/extmod/framebuf_scroll.py b/tests/extmod/framebuf_scroll.py index db9b6ea1e9649..d7c07b1c65772 100644 --- a/tests/extmod/framebuf_scroll.py +++ b/tests/extmod/framebuf_scroll.py @@ -42,4 +42,9 @@ def prepare_buffer(): fbuf.scroll(15, 7) fbuf.scroll(10, -1) fbuf.scroll(1, -10) +try: + fbuf.scroll(1000000000000, -1) +except OverflowError: + # When mp_int_t is 32 bits, this throws OverflowError. + pass printbuf() From 803da9645f735edd3d1a80a3f920039e0f0298f2 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 11 Aug 2025 11:48:34 -0500 Subject: [PATCH 108/143] extmod/modframebuf: Save code size in setpixel. There's a slight code size increase paid for by using setpixel_checked for the last pixel of a line, instead of repeating the checks inline. Signed-off-by: Jeff Epler --- extmod/modframebuf.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/extmod/modframebuf.c b/extmod/modframebuf.c index 16606109ccdc4..593125aa16f91 100644 --- a/extmod/modframebuf.c +++ b/extmod/modframebuf.c @@ -483,9 +483,7 @@ static void line(const mp_obj_framebuf_t *fb, mp_int_t x1, mp_int_t y1, mp_int_t e += 2 * dy; } - if (0 <= x2 && x2 < fb->width && 0 <= y2 && y2 < fb->height) { - setpixel(fb, x2, y2, col); - } + setpixel_checked(fb, x2, y2, col, 1); } static mp_obj_t framebuf_line(size_t n_args, const mp_obj_t *args_in) { From 0615d13963ba33650a5b2070368584c7aec9c9dd Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 6 Aug 2025 10:15:57 -0500 Subject: [PATCH 109/143] py/objringio: Detect incorrect constructor calls. ringbuffer.size must be at least 2, and is a 16-bit quantity. This fixes several cases including the one the fuzzer discovered, which would lead to a fatal signal when accessing the object. Fixes issue #17847. Signed-off-by: Jeff Epler --- py/objringio.c | 19 ++++++++----------- tests/micropython/ringio.py | 18 ++++++++++++++++++ tests/micropython/ringio.py.exp | 3 +++ tests/micropython/ringio_big.py | 29 +++++++++++++++++++++++++++++ tests/micropython/ringio_big.py.exp | 2 ++ 5 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 tests/micropython/ringio_big.py create mode 100644 tests/micropython/ringio_big.py.exp diff --git a/py/objringio.c b/py/objringio.c index ba1ec25307ea4..0025b26be3b07 100644 --- a/py/objringio.c +++ b/py/objringio.c @@ -39,22 +39,19 @@ typedef struct _micropython_ringio_obj_t { static mp_obj_t micropython_ringio_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { mp_arg_check_num(n_args, n_kw, 1, 1, false); - mp_int_t buff_size = -1; mp_buffer_info_t bufinfo = {NULL, 0, 0}; if (!mp_get_buffer(args[0], &bufinfo, MP_BUFFER_RW)) { - buff_size = mp_obj_get_int(args[0]); + bufinfo.len = mp_obj_get_int(args[0]) + 1; + bufinfo.buf = m_new(uint8_t, bufinfo.len); } - micropython_ringio_obj_t *self = mp_obj_malloc(micropython_ringio_obj_t, type); - if (bufinfo.buf != NULL) { - // buffer passed in, use it directly for ringbuffer. - self->ringbuffer.buf = bufinfo.buf; - self->ringbuffer.size = bufinfo.len; - self->ringbuffer.iget = self->ringbuffer.iput = 0; - } else { - // Allocate new buffer, add one extra to buff_size as ringbuf consumes one byte for tracking. - ringbuf_alloc(&(self->ringbuffer), buff_size + 1); + if (bufinfo.len < 2 || bufinfo.len > UINT16_MAX) { + mp_raise_ValueError(NULL); } + micropython_ringio_obj_t *self = mp_obj_malloc(micropython_ringio_obj_t, type); + self->ringbuffer.buf = bufinfo.buf; + self->ringbuffer.size = bufinfo.len; + self->ringbuffer.iget = self->ringbuffer.iput = 0; return MP_OBJ_FROM_PTR(self); } diff --git a/tests/micropython/ringio.py b/tests/micropython/ringio.py index a0102ef63dafa..4109288798448 100644 --- a/tests/micropython/ringio.py +++ b/tests/micropython/ringio.py @@ -46,3 +46,21 @@ micropython.RingIO(None) except TypeError as ex: print(type(ex)) + +try: + # Buffer may not be empty + micropython.RingIO(bytearray(0)) +except ValueError as ex: + print(type(ex)) + +try: + # Buffer may not be too small + micropython.RingIO(bytearray(1)) +except ValueError as ex: + print(type(ex)) + +try: + # Size may not be too small + micropython.RingIO(0) +except ValueError as ex: + print(type(ex)) diff --git a/tests/micropython/ringio.py.exp b/tests/micropython/ringio.py.exp index 65bae06472f8d..c391529a41e54 100644 --- a/tests/micropython/ringio.py.exp +++ b/tests/micropython/ringio.py.exp @@ -14,3 +14,6 @@ b'\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01' 0 b'\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01' + + + diff --git a/tests/micropython/ringio_big.py b/tests/micropython/ringio_big.py new file mode 100644 index 0000000000000..d55c4c00b7c0d --- /dev/null +++ b/tests/micropython/ringio_big.py @@ -0,0 +1,29 @@ +# Check that micropython.RingIO works correctly. + +import micropython + +try: + micropython.RingIO +except AttributeError: + print("SKIP") + raise SystemExit + +try: + # The maximum possible size + micropython.RingIO(bytearray(65535)) + micropython.RingIO(65534) + + try: + # Buffer may not be too big + micropython.RingIO(bytearray(65536)) + except ValueError as ex: + print(type(ex)) + + try: + # Size may not be too big + micropython.RingIO(65535) + except ValueError as ex: + print(type(ex)) +except MemoryError: + print("SKIP") + raise SystemExit diff --git a/tests/micropython/ringio_big.py.exp b/tests/micropython/ringio_big.py.exp new file mode 100644 index 0000000000000..72af34b383872 --- /dev/null +++ b/tests/micropython/ringio_big.py.exp @@ -0,0 +1,2 @@ + + From 141f7d0c35516403c39ce6266be7c6fb4551ef6f Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 12 Aug 2025 11:51:53 +0100 Subject: [PATCH 110/143] py/mkrules.cmake: Clean genhdr and frozen_mpy dirs. CMake builds relied upon the parent Makefile removing the entire build directory to successfully clean build artifacts. py/mkrules.cmake: Add ADDITIONAL_CLEAN_FILES properties to ensure a "make clean" from within the build directory removes the genhdr and frozen_mpy directories. Signed-off-by: Phil Howard --- py/mkrules.cmake | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/py/mkrules.cmake b/py/mkrules.cmake index 27d8b24f67ac8..e3d769cc59b5c 100644 --- a/py/mkrules.cmake +++ b/py/mkrules.cmake @@ -100,6 +100,12 @@ if(MICROPY_ROM_TEXT_COMPRESSION) ) endif() +# Ensure genhdr directory is removed on clean + +set_property(TARGET ${MICROPY_TARGET} APPEND PROPERTY ADDITIONAL_CLEAN_FILES + "${MICROPY_GENHDR_DIR}" +) + # Command to force the build of another command # Generate mpversion.h @@ -244,6 +250,10 @@ add_custom_command( if(MICROPY_FROZEN_MANIFEST) set(MICROPY_FROZEN_CONTENT "${CMAKE_BINARY_DIR}/frozen_content.c") + set_property(TARGET ${MICROPY_TARGET} APPEND PROPERTY ADDITIONAL_CLEAN_FILES + "${CMAKE_BINARY_DIR}/frozen_mpy" + ) + target_sources(${MICROPY_TARGET} PRIVATE ${MICROPY_FROZEN_CONTENT} ) From 3efbd726eb89a1514c01a51857100b2718f12a50 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 14:44:48 +1000 Subject: [PATCH 111/143] py/parse: Remove explicit checks for invalid folding operations. They are instead checked by `binary_op_maybe()`, which catches exceptions for invalid int/float operations. This is a follow-up to 69ead7d98ef30df3b6bd4485633490e80fca1718 Signed-off-by: Damien George --- py/parse.c | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/py/parse.c b/py/parse.c index 6260987b28a35..1a50b13b5c790 100644 --- a/py/parse.c +++ b/py/parse.c @@ -771,12 +771,6 @@ static bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) { if (!mp_parse_node_get_number_maybe(pn, &arg1)) { return false; } - #if !MICROPY_COMP_CONST_FLOAT - if (op == MP_BINARY_OP_POWER && mp_obj_int_sign(arg1) < 0) { - // ** can't have negative rhs - return false; - } - #endif if (!binary_op_maybe(op, arg0, arg1, &arg0)) { return false; } @@ -796,16 +790,6 @@ static bool fold_constants(parser_t *parser, uint8_t rule_id, size_t num_args) { return false; } mp_token_kind_t tok = MP_PARSE_NODE_LEAF_ARG(peek_result(parser, i)); - if (tok == MP_TOKEN_OP_AT) { - // Can't fold @ - return false; - } - #if !MICROPY_COMP_CONST_FLOAT - if (tok == MP_TOKEN_OP_SLASH) { - // Can't fold / - return false; - } - #endif mp_binary_op_t op = MP_BINARY_OP_LSHIFT + (tok - MP_TOKEN_OP_DBL_LESS); if (!binary_op_maybe(op, arg0, arg1, &arg0)) { return false; From 40cc4e4f74b1f39076100136bc2cb8db5ff24ea5 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 16:35:16 +1000 Subject: [PATCH 112/143] py/objtype: Make mp_obj_new_type a static function. It's only used once, in the same file it's defined, and making it static reduces code size. Along with this, the associated example code comment in `ports/unix/main.c` has been removed. Signed-off-by: Damien George --- ports/unix/main.c | 13 ------------- py/obj.h | 1 - py/objtype.c | 3 ++- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/ports/unix/main.c b/ports/unix/main.c index 530e20a3863b4..5f6b99f989a3a 100644 --- a/ports/unix/main.c +++ b/ports/unix/main.c @@ -616,19 +616,6 @@ MP_NOINLINE int main_(int argc, char **argv) { } #endif - // Here is some example code to create a class and instance of that class. - // First is the Python, then the C code. - // - // class TestClass: - // pass - // test_obj = TestClass() - // test_obj.attr = 42 - // - // mp_obj_t test_class_type, test_class_instance; - // test_class_type = mp_obj_new_type(qstr_from_str("TestClass"), mp_const_empty_tuple, mp_obj_new_dict(0)); - // mp_store_name(qstr_from_str("test_obj"), test_class_instance = mp_call_function_0(test_class_type)); - // mp_store_attr(test_class_instance, qstr_from_str("attr"), mp_obj_new_int(42)); - /* printf("bytes:\n"); printf(" total %d\n", m_get_total_bytes_allocated()); diff --git a/py/obj.h b/py/obj.h index 4ac0cc0c611e5..69d6e626db3c1 100644 --- a/py/obj.h +++ b/py/obj.h @@ -987,7 +987,6 @@ void *mp_obj_malloc_with_finaliser_helper(size_t num_bytes, const mp_obj_type_t bool mp_obj_is_dict_or_ordereddict(mp_obj_t o); #define mp_obj_is_fun(o) (mp_obj_is_obj(o) && (((mp_obj_base_t *)MP_OBJ_TO_PTR(o))->type->name == MP_QSTR_function)) -mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict); static inline mp_obj_t mp_obj_new_bool(mp_int_t x) { return x ? mp_const_true : mp_const_false; } diff --git a/py/objtype.c b/py/objtype.c index 818ceeb0589fb..25c2dee994d74 100644 --- a/py/objtype.c +++ b/py/objtype.c @@ -44,6 +44,7 @@ #define ENABLE_SPECIAL_ACCESSORS \ (MICROPY_PY_DESCRIPTORS || MICROPY_PY_DELATTR_SETATTR || MICROPY_PY_BUILTINS_PROPERTY) +static mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict); static mp_obj_t mp_obj_is_subclass(mp_obj_t object, mp_obj_t classinfo); static mp_obj_t static_class_method_make_new(const mp_obj_type_t *self_in, size_t n_args, size_t n_kw, const mp_obj_t *args); @@ -1164,7 +1165,7 @@ MP_DEFINE_CONST_OBJ_TYPE( attr, type_attr ); -mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) { +static mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) { // Verify input objects have expected type if (!mp_obj_is_type(bases_tuple, &mp_type_tuple)) { mp_raise_TypeError(NULL); From b3cd1a355ebf50e06ec9f0c1594e47abd7ee339c Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 12 Aug 2025 11:47:52 +1000 Subject: [PATCH 113/143] extmod/modtime: Move tuple creation to common localtime implementation. This factors code out of the ports and into the common `time.localtime` implementation in `extmod/modtime.c`. That helps to reduce code duplication, prevent errors in implementation, and reduce code size on some ports (mimxrt and stm32 at least). Signed-off-by: Damien George --- extmod/modtime.c | 26 +++++++++++++------------- ports/cc3200/mods/modtime.c | 19 +++---------------- ports/esp32/modtime.c | 18 +++--------------- ports/esp8266/modtime.c | 18 +++--------------- ports/mimxrt/modtime.c | 23 ++++++++++------------- ports/renesas-ra/modtime.c | 23 ++++++++++------------- ports/rp2/modtime.c | 18 +++--------------- ports/samd/modtime.c | 22 +++++----------------- ports/stm32/modtime.c | 23 ++++++++++------------- ports/webassembly/modtime.c | 18 +++--------------- 10 files changed, 63 insertions(+), 145 deletions(-) diff --git a/extmod/modtime.c b/extmod/modtime.c index 999b81230bcb9..ee898828a4ab1 100644 --- a/extmod/modtime.c +++ b/extmod/modtime.c @@ -53,26 +53,26 @@ // - weekday is 0-6 for Mon-Sun // - yearday is 1-366 static mp_obj_t time_localtime(size_t n_args, const mp_obj_t *args) { + timeutils_struct_time_t tm; if (n_args == 0 || args[0] == mp_const_none) { // Get current date and time. - return mp_time_localtime_get(); + mp_time_localtime_get(&tm); } else { // Convert given seconds to tuple. mp_timestamp_t seconds = timeutils_obj_get_timestamp(args[0]); - timeutils_struct_time_t tm; timeutils_seconds_since_epoch_to_struct_time(seconds, &tm); - mp_obj_t tuple[8] = { - tuple[0] = mp_obj_new_int(tm.tm_year), - tuple[1] = mp_obj_new_int(tm.tm_mon), - tuple[2] = mp_obj_new_int(tm.tm_mday), - tuple[3] = mp_obj_new_int(tm.tm_hour), - tuple[4] = mp_obj_new_int(tm.tm_min), - tuple[5] = mp_obj_new_int(tm.tm_sec), - tuple[6] = mp_obj_new_int(tm.tm_wday), - tuple[7] = mp_obj_new_int(tm.tm_yday), - }; - return mp_obj_new_tuple(8, tuple); } + mp_obj_t tuple[8] = { + mp_obj_new_int(tm.tm_year), + mp_obj_new_int(tm.tm_mon), + mp_obj_new_int(tm.tm_mday), + mp_obj_new_int(tm.tm_hour), + mp_obj_new_int(tm.tm_min), + mp_obj_new_int(tm.tm_sec), + mp_obj_new_int(tm.tm_wday), + mp_obj_new_int(tm.tm_yday), + }; + return mp_obj_new_tuple(8, tuple); } MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_time_localtime_obj, 0, 1, time_localtime); diff --git a/ports/cc3200/mods/modtime.c b/ports/cc3200/mods/modtime.c index 254678fb2ddc5..d111667001115 100644 --- a/ports/cc3200/mods/modtime.c +++ b/ports/cc3200/mods/modtime.c @@ -29,23 +29,10 @@ #include "shared/timeutils/timeutils.h" #include "pybrtc.h" -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { - timeutils_struct_time_t tm; - +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { // get the seconds from the RTC - timeutils_seconds_since_2000_to_struct_time(pyb_rtc_get_seconds(), &tm); - mp_obj_t tuple[8] = { - mp_obj_new_int(tm.tm_year), - mp_obj_new_int(tm.tm_mon), - mp_obj_new_int(tm.tm_mday), - mp_obj_new_int(tm.tm_hour), - mp_obj_new_int(tm.tm_min), - mp_obj_new_int(tm.tm_sec), - mp_obj_new_int(tm.tm_wday), - mp_obj_new_int(tm.tm_yday) - }; - return mp_obj_new_tuple(8, tuple); + timeutils_seconds_since_2000_to_struct_time(pyb_rtc_get_seconds(), tm); } // Returns the number of seconds, as an integer, since the Epoch. diff --git a/ports/esp32/modtime.c b/ports/esp32/modtime.c index 991f2cf578771..64f9359db69af 100644 --- a/ports/esp32/modtime.c +++ b/ports/esp32/modtime.c @@ -31,23 +31,11 @@ #include "py/obj.h" #include "shared/timeutils/timeutils.h" -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { struct timeval tv; gettimeofday(&tv, NULL); - timeutils_struct_time_t tm; - timeutils_seconds_since_epoch_to_struct_time(tv.tv_sec, &tm); - mp_obj_t tuple[8] = { - tuple[0] = mp_obj_new_int(tm.tm_year), - tuple[1] = mp_obj_new_int(tm.tm_mon), - tuple[2] = mp_obj_new_int(tm.tm_mday), - tuple[3] = mp_obj_new_int(tm.tm_hour), - tuple[4] = mp_obj_new_int(tm.tm_min), - tuple[5] = mp_obj_new_int(tm.tm_sec), - tuple[6] = mp_obj_new_int(tm.tm_wday), - tuple[7] = mp_obj_new_int(tm.tm_yday), - }; - return mp_obj_new_tuple(8, tuple); + timeutils_seconds_since_epoch_to_struct_time(tv.tv_sec, tm); } // Return the number of seconds since the Epoch. diff --git a/ports/esp8266/modtime.c b/ports/esp8266/modtime.c index e99d920fde80b..c0c1dccfe474d 100644 --- a/ports/esp8266/modtime.c +++ b/ports/esp8266/modtime.c @@ -29,22 +29,10 @@ #include "shared/timeutils/timeutils.h" #include "modmachine.h" -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { mp_uint_t seconds = pyb_rtc_get_us_since_epoch() / 1000u / 1000u; - timeutils_struct_time_t tm; - timeutils_seconds_since_epoch_to_struct_time(seconds, &tm); - mp_obj_t tuple[8] = { - tuple[0] = mp_obj_new_int(tm.tm_year), - tuple[1] = mp_obj_new_int(tm.tm_mon), - tuple[2] = mp_obj_new_int(tm.tm_mday), - tuple[3] = mp_obj_new_int(tm.tm_hour), - tuple[4] = mp_obj_new_int(tm.tm_min), - tuple[5] = mp_obj_new_int(tm.tm_sec), - tuple[6] = mp_obj_new_int(tm.tm_wday), - tuple[7] = mp_obj_new_int(tm.tm_yday), - }; - return mp_obj_new_tuple(8, tuple); + timeutils_seconds_since_epoch_to_struct_time(seconds, tm); } // Returns the number of seconds, as an integer, since the Epoch. diff --git a/ports/mimxrt/modtime.c b/ports/mimxrt/modtime.c index 3bcfac411c0b3..fe77b8a733be2 100644 --- a/ports/mimxrt/modtime.c +++ b/ports/mimxrt/modtime.c @@ -29,22 +29,19 @@ #include "shared/timeutils/timeutils.h" #include "fsl_snvs_lp.h" -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { // Get current date and time. snvs_lp_srtc_datetime_t t; SNVS_LP_SRTC_GetDatetime(SNVS, &t); - mp_obj_t tuple[8] = { - mp_obj_new_int(t.year), - mp_obj_new_int(t.month), - mp_obj_new_int(t.day), - mp_obj_new_int(t.hour), - mp_obj_new_int(t.minute), - mp_obj_new_int(t.second), - mp_obj_new_int(timeutils_calc_weekday(t.year, t.month, t.day)), - mp_obj_new_int(timeutils_year_day(t.year, t.month, t.day)), - }; - return mp_obj_new_tuple(8, tuple); + tm->tm_year = t.year; + tm->tm_mon = t.month; + tm->tm_mday = t.day; + tm->tm_hour = t.hour; + tm->tm_min = t.minute; + tm->tm_sec = t.second; + tm->tm_wday = timeutils_calc_weekday(t.year, t.month, t.day); + tm->tm_yday = timeutils_year_day(t.year, t.month, t.day); } // Return the number of seconds since the Epoch. diff --git a/ports/renesas-ra/modtime.c b/ports/renesas-ra/modtime.c index e1358f82bc5a4..b778ab2fe5e2a 100644 --- a/ports/renesas-ra/modtime.c +++ b/ports/renesas-ra/modtime.c @@ -28,23 +28,20 @@ #include "shared/timeutils/timeutils.h" #include "rtc.h" -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { // get current date and time rtc_init_finalise(); ra_rtc_t time; ra_rtc_get_time(&time); - mp_obj_t tuple[8] = { - mp_obj_new_int(time.year), - mp_obj_new_int(time.month), - mp_obj_new_int(time.date), - mp_obj_new_int(time.hour), - mp_obj_new_int(time.minute), - mp_obj_new_int(time.second), - mp_obj_new_int(time.weekday - 1), - mp_obj_new_int(timeutils_year_day(time.year, time.month, time.date)), - }; - return mp_obj_new_tuple(8, tuple); + tm->tm_year = time.year; + tm->tm_mon = time.month; + tm->tm_mday = time.date; + tm->tm_hour = time.hour; + tm->tm_min = time.minute; + tm->tm_sec = time.second; + tm->tm_wday = time.weekday - 1; + tm->tm_yday = timeutils_year_day(time.year, time.month, time.date); } // Returns the number of seconds, as an integer, since the Epoch. diff --git a/ports/rp2/modtime.c b/ports/rp2/modtime.c index 7d6dd8fd97112..a860903285e55 100644 --- a/ports/rp2/modtime.c +++ b/ports/rp2/modtime.c @@ -28,23 +28,11 @@ #include "shared/timeutils/timeutils.h" #include "pico/aon_timer.h" -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { struct timespec ts; aon_timer_get_time(&ts); - timeutils_struct_time_t tm; - timeutils_seconds_since_epoch_to_struct_time(ts.tv_sec, &tm); - mp_obj_t tuple[8] = { - mp_obj_new_int(tm.tm_year), - mp_obj_new_int(tm.tm_mon), - mp_obj_new_int(tm.tm_mday), - mp_obj_new_int(tm.tm_hour), - mp_obj_new_int(tm.tm_min), - mp_obj_new_int(tm.tm_sec), - mp_obj_new_int(tm.tm_wday), - mp_obj_new_int(tm.tm_yday), - }; - return mp_obj_new_tuple(8, tuple); + timeutils_seconds_since_epoch_to_struct_time(ts.tv_sec, tm); } // Return the number of seconds since the Epoch. diff --git a/ports/samd/modtime.c b/ports/samd/modtime.c index 0bed3cb83a868..6168c645d6313 100644 --- a/ports/samd/modtime.c +++ b/ports/samd/modtime.c @@ -30,23 +30,11 @@ static uint64_t time_us_64_offset_from_epoch; -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { - timeutils_struct_time_t tm; - rtc_gettime(&tm); - tm.tm_wday = timeutils_calc_weekday(tm.tm_year, tm.tm_mon, tm.tm_mday); - tm.tm_yday = timeutils_year_day(tm.tm_year, tm.tm_mon, tm.tm_mday); - mp_obj_t tuple[8] = { - tuple[0] = mp_obj_new_int(tm.tm_year), - tuple[1] = mp_obj_new_int(tm.tm_mon), - tuple[2] = mp_obj_new_int(tm.tm_mday), - tuple[3] = mp_obj_new_int(tm.tm_hour), - tuple[4] = mp_obj_new_int(tm.tm_min), - tuple[5] = mp_obj_new_int(tm.tm_sec), - tuple[6] = mp_obj_new_int(tm.tm_wday), - tuple[7] = mp_obj_new_int(tm.tm_yday), - }; - return mp_obj_new_tuple(8, tuple); +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { + rtc_gettime(tm); + tm->tm_wday = timeutils_calc_weekday(tm->tm_year, tm->tm_mon, tm->tm_mday); + tm->tm_yday = timeutils_year_day(tm->tm_year, tm->tm_mon, tm->tm_mday); } // Returns the number of seconds, as an integer, since the Epoch. diff --git a/ports/stm32/modtime.c b/ports/stm32/modtime.c index 87a4536b04374..e7051065187bc 100644 --- a/ports/stm32/modtime.c +++ b/ports/stm32/modtime.c @@ -28,8 +28,8 @@ #include "shared/timeutils/timeutils.h" #include "rtc.h" -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { // get current date and time // note: need to call get time then get date to correctly access the registers rtc_init_finalise(); @@ -37,17 +37,14 @@ static mp_obj_t mp_time_localtime_get(void) { RTC_TimeTypeDef time; HAL_RTC_GetTime(&RTCHandle, &time, RTC_FORMAT_BIN); HAL_RTC_GetDate(&RTCHandle, &date, RTC_FORMAT_BIN); - mp_obj_t tuple[8] = { - mp_obj_new_int(2000 + date.Year), - mp_obj_new_int(date.Month), - mp_obj_new_int(date.Date), - mp_obj_new_int(time.Hours), - mp_obj_new_int(time.Minutes), - mp_obj_new_int(time.Seconds), - mp_obj_new_int(date.WeekDay - 1), - mp_obj_new_int(timeutils_year_day(2000 + date.Year, date.Month, date.Date)), - }; - return mp_obj_new_tuple(8, tuple); + tm->tm_year = 2000 + date.Year; + tm->tm_mon = date.Month; + tm->tm_mday = date.Date; + tm->tm_hour = time.Hours; + tm->tm_min = time.Minutes; + tm->tm_sec = time.Seconds; + tm->tm_wday = date.WeekDay - 1; + tm->tm_yday = timeutils_year_day(tm->tm_year, date.Month, date.Date); } // Returns the number of seconds, as an integer, since 1/1/2000. diff --git a/ports/webassembly/modtime.c b/ports/webassembly/modtime.c index 1b1e63d4ddf3a..b6c0cda96da12 100644 --- a/ports/webassembly/modtime.c +++ b/ports/webassembly/modtime.c @@ -28,21 +28,9 @@ #include "shared/timeutils/timeutils.h" #include "library.h" -// Return the localtime as an 8-tuple. -static mp_obj_t mp_time_localtime_get(void) { - timeutils_struct_time_t tm; - timeutils_seconds_since_epoch_to_struct_time(mp_hal_time_ms() / 1000, &tm); - mp_obj_t tuple[8] = { - mp_obj_new_int(tm.tm_year), - mp_obj_new_int(tm.tm_mon), - mp_obj_new_int(tm.tm_mday), - mp_obj_new_int(tm.tm_hour), - mp_obj_new_int(tm.tm_min), - mp_obj_new_int(tm.tm_sec), - mp_obj_new_int(tm.tm_wday), - mp_obj_new_int(tm.tm_yday), - }; - return mp_obj_new_tuple(8, tuple); +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { + timeutils_seconds_since_epoch_to_struct_time(mp_hal_time_ms() / 1000, tm); } // Returns the number of seconds, as a float, since the Epoch. From e58848a21eb6dafce53df991c52ac8db52354daa Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 12 Aug 2025 13:00:11 +1000 Subject: [PATCH 114/143] alif/machine_rtc: Implement RTC.datetime to get and set the RTC. The LPRTC peripheral is a 32-bit counter with a 16-bit prescaler. It's configured here to count at 1Hz (to get maximum date range) and then the prescaler value is used to get 30 microsecond resolution. That's essentially a 32+15=47-bit counter. Signed-off-by: Damien George --- ports/alif/machine_rtc.c | 104 ++++++++++++++++++++++++++++++++++++--- ports/alif/mphalport.h | 2 + 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/ports/alif/machine_rtc.c b/ports/alif/machine_rtc.c index 6473d1d80fbff..739fcab4b6cb0 100644 --- a/ports/alif/machine_rtc.c +++ b/ports/alif/machine_rtc.c @@ -28,9 +28,20 @@ #include "py/mphal.h" #include "py/mperrno.h" #include "extmod/modmachine.h" +#include "shared/timeutils/timeutils.h" #include "rtc.h" #include "sys_ctrl_rtc.h" +// The LPRTC (low-power real-time counter) is a 32-bit counter with a 16-bit prescaler, +// and usually clocked by a 32768Hz clock source. To get a large date range of around +// 136 years, the prescaler is set to 32768 and so the counter clocks at 1Hz. Then the +// counter counts the number of seconds since the 1970 Epoch. The prescaler is used to +// get the subseconds which are then converted to microseconds. +// +// The combined counter+prescaler counts starting at 0 from the year 1970 up to the year +// 2106, with a resolution of 30.52 microseconds. +#define LPRTC_PRESCALER_SETTING (32768) + typedef struct _machine_rtc_obj_t { mp_obj_base_t base; LPRTC_Type *rtc; @@ -44,24 +55,97 @@ void LPRTC_IRQHandler(void) { lprtc_interrupt_disable(machine_rtc.rtc); } +// Returns the number of seconds and microseconds since the Epoch. +uint32_t mp_hal_time_get(uint32_t *microseconds) { + uint32_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + uint32_t count = lprtc_get_count(machine_rtc.rtc); + if (microseconds == NULL) { + MICROPY_END_ATOMIC_SECTION(atomic_state); + return count; + } + uint32_t prescaler = machine_rtc.rtc->LPRTC_CPCVR; + uint32_t count2 = lprtc_get_count(machine_rtc.rtc); + if (count != count2) { + // The counter incremented during sampling of the prescaler, so resample the prescaler. + prescaler = machine_rtc.rtc->LPRTC_CPCVR; + } + MICROPY_END_ATOMIC_SECTION(atomic_state); + + // Compute the microseconds from the up-counting prescaler value. + MP_STATIC_ASSERT(LPRTC_PRESCALER_SETTING == 32768); + *microseconds = 15625UL * prescaler / 512UL; + + return count2; +} + static mp_obj_t machine_rtc_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { const machine_rtc_obj_t *self = &machine_rtc; // Check arguments. mp_arg_check_num(n_args, n_kw, 0, 0, false); - enable_lprtc_clk(); - lprtc_prescaler_disable(self->rtc); - lprtc_counter_wrap_disable(self->rtc); lprtc_interrupt_disable(self->rtc); lprtc_interrupt_unmask(self->rtc); + // Initialise the LPRTC if it's not already enabled. + if (!((VBAT->RTC_CLK_EN & RTC_CLK_ENABLE) + && (self->rtc->LPRTC_CCR & CCR_LPRTC_EN) + && (self->rtc->LPRTC_CPSR == LPRTC_PRESCALER_SETTING))) { + enable_lprtc_clk(); + self->rtc->LPRTC_CCR = 0; + lprtc_load_prescaler(self->rtc, LPRTC_PRESCALER_SETTING); + lprtc_load_count(self->rtc, 0); + self->rtc->LPRTC_CCR = CCR_LPRTC_PSCLR_EN | CCR_LPRTC_EN; + } + NVIC_SetPriority(LPRTC_IRQ_IRQn, IRQ_PRI_RTC); NVIC_ClearPendingIRQ(LPRTC_IRQ_IRQn); NVIC_EnableIRQ(LPRTC_IRQ_IRQn); + return MP_OBJ_FROM_PTR(self); } +static mp_obj_t machine_rtc_datetime(mp_uint_t n_args, const mp_obj_t *args) { + if (n_args == 1) { + // Get datetime. + uint32_t microseconds; + mp_timestamp_t s = mp_hal_time_get(µseconds); + timeutils_struct_time_t tm; + timeutils_seconds_since_epoch_to_struct_time(s, &tm); + mp_obj_t tuple[8] = { + mp_obj_new_int(tm.tm_year), + mp_obj_new_int(tm.tm_mon), + mp_obj_new_int(tm.tm_mday), + mp_obj_new_int(tm.tm_wday), + mp_obj_new_int(tm.tm_hour), + mp_obj_new_int(tm.tm_min), + mp_obj_new_int(tm.tm_sec), + mp_obj_new_int(microseconds), + }; + return mp_obj_new_tuple(8, tuple); + } else { + // Set datetime. + mp_obj_t *items; + mp_obj_get_array_fixed_n(args[1], 8, &items); + timeutils_struct_time_t tm = { + .tm_year = mp_obj_get_int(items[0]), + .tm_mon = mp_obj_get_int(items[1]), + .tm_mday = mp_obj_get_int(items[2]), + .tm_hour = mp_obj_get_int(items[4]), + .tm_min = mp_obj_get_int(items[5]), + .tm_sec = mp_obj_get_int(items[6]), + }; + mp_timestamp_t s = timeutils_seconds_since_epoch(tm.tm_year, tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); + + // Disable then re-enable the LPRTC so that the prescaler counter resets to 0. + machine_rtc.rtc->LPRTC_CCR = 0; + lprtc_load_count(machine_rtc.rtc, s); + machine_rtc.rtc->LPRTC_CCR = CCR_LPRTC_PSCLR_EN | CCR_LPRTC_EN; + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_rtc_datetime_obj, 1, 2, machine_rtc_datetime); + static mp_obj_t machine_rtc_alarm(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_id, ARG_time, ARG_repeat }; @@ -80,13 +164,18 @@ static mp_obj_t machine_rtc_alarm(size_t n_args, const mp_obj_t *pos_args, mp_ma if (mp_obj_is_int(args[ARG_time].u_obj)) { uint32_t seconds = mp_obj_get_int(args[1].u_obj) / 1000; - lprtc_counter_disable(self->rtc); - lprtc_load_count(self->rtc, 1); - lprtc_load_counter_match_register(self->rtc, seconds * 32768); + // Make sure we are guaranteed an interrupt: + // - if seconds = 0 it won't fire + // - if seconds = 1 it may miss if the counter rolls over just after it's read + // - if seconds >= 2 then it will always fire (when read/written close enough) + seconds = MAX(2, seconds); + // Configure the counter match as atomically as possible. + uint32_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); lprtc_interrupt_ack(self->rtc); + lprtc_load_counter_match_register(self->rtc, lprtc_get_count(self->rtc) + seconds); lprtc_interrupt_enable(self->rtc); - lprtc_counter_enable(self->rtc); + MICROPY_END_ATOMIC_SECTION(atomic_state); } else { mp_raise_ValueError(MP_ERROR_TEXT("invalid argument(s)")); } @@ -96,6 +185,7 @@ static mp_obj_t machine_rtc_alarm(size_t n_args, const mp_obj_t *pos_args, mp_ma static MP_DEFINE_CONST_FUN_OBJ_KW(machine_rtc_alarm_obj, 1, machine_rtc_alarm); static const mp_rom_map_elem_t machine_rtc_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_datetime), MP_ROM_PTR(&machine_rtc_datetime_obj) }, { MP_ROM_QSTR(MP_QSTR_alarm), MP_ROM_PTR(&machine_rtc_alarm_obj) }, }; static MP_DEFINE_CONST_DICT(machine_rtc_locals_dict, machine_rtc_locals_dict_table); diff --git a/ports/alif/mphalport.h b/ports/alif/mphalport.h index f03dc7c1c1f84..731ac14fc57e6 100644 --- a/ports/alif/mphalport.h +++ b/ports/alif/mphalport.h @@ -373,3 +373,5 @@ enum { void mp_hal_generate_laa_mac(int idx, uint8_t buf[6]); void mp_hal_get_mac(int idx, uint8_t buf[6]); void mp_hal_get_mac_ascii(int idx, size_t chr_off, size_t chr_len, char *dest); + +uint32_t mp_hal_time_get(uint32_t *microseconds); From 0feb4f5ea419c038c69a6fd0b295b788c09f06a2 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 12 Aug 2025 13:12:22 +1000 Subject: [PATCH 115/143] alif/mphalport: Implement mp_hal_time_ns. Signed-off-by: Damien George --- ports/alif/mphalport.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ports/alif/mphalport.c b/ports/alif/mphalport.c index 39528a4b9f272..3bf8bb0aed7e3 100644 --- a/ports/alif/mphalport.c +++ b/ports/alif/mphalport.c @@ -167,7 +167,9 @@ void mp_hal_delay_ms(mp_uint_t ms) { } uint64_t mp_hal_time_ns(void) { - return 0; + uint32_t microseconds; + uint32_t s = mp_hal_time_get(µseconds); + return (uint64_t)s * 1000000000ULL + (uint64_t)microseconds * 1000ULL; } void mp_hal_pin_config(const machine_pin_obj_t *pin, uint32_t mode, From 46b366d7b245360e37443ca096f120c5d601f11d Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 12 Aug 2025 13:04:20 +1000 Subject: [PATCH 116/143] alif/fatfs_port: Implement get_fattime. Signed-off-by: Damien George --- ports/alif/fatfs_port.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/ports/alif/fatfs_port.c b/ports/alif/fatfs_port.c index 5883c9f3b9447..20b7920ebf0f1 100644 --- a/ports/alif/fatfs_port.c +++ b/ports/alif/fatfs_port.c @@ -24,15 +24,12 @@ * THE SOFTWARE. */ +#include "py/mphal.h" +#include "shared/timeutils/timeutils.h" #include "lib/oofatfs/ff.h" DWORD get_fattime(void) { - // TODO - int year = 2024; - int month = 1; - int day = 1; - int hour = 0; - int min = 0; - int sec = 0; - return ((year - 1980) << 25) | (month << 21) | (day << 16) | (hour << 11) | (min << 5) | (sec / 2); + timeutils_struct_time_t tm; + timeutils_seconds_since_epoch_to_struct_time(mp_hal_time_get(NULL), &tm); + return ((tm.tm_year - 1980) << 25) | ((tm.tm_mon) << 21) | ((tm.tm_mday) << 16) | ((tm.tm_hour) << 11) | ((tm.tm_min) << 5) | (tm.tm_sec / 2); } From 326730d8b283245462f7c9dc7d191e7c01cb3756 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 12 Aug 2025 13:05:09 +1000 Subject: [PATCH 117/143] alif/mbedtls: Implement the mbedTLS time function. Signed-off-by: Damien George --- ports/alif/mbedtls/mbedtls_port.c | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/ports/alif/mbedtls/mbedtls_port.c b/ports/alif/mbedtls/mbedtls_port.c index a8c155e31ae65..14e24f021f9e9 100644 --- a/ports/alif/mbedtls/mbedtls_port.c +++ b/ports/alif/mbedtls/mbedtls_port.c @@ -24,15 +24,10 @@ * THE SOFTWARE. */ -#include "py/obj.h" +#include "py/mphal.h" #include "se_services.h" #include "mbedtls_config_port.h" -#if defined(MBEDTLS_HAVE_TIME) -#include "shared/timeutils/timeutils.h" -#include "mbedtls/platform_time.h" -#endif - int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen) { uint32_t val = 0; int n = 0; @@ -52,14 +47,7 @@ int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t #if defined(MBEDTLS_HAVE_TIME) time_t alif_mbedtls_time(time_t *timer) { - // TODO implement proper RTC time - unsigned int year = 2025; - unsigned int month = 1; - unsigned int date = 1; - unsigned int hours = 12; - unsigned int minutes = 0; - unsigned int seconds = 0; - return timeutils_seconds_since_epoch(year, month, date, hours, minutes, seconds); + return mp_hal_time_get(NULL); } #endif From f1462448d023107e34bdc193d6947a2a78af7d54 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 12 Aug 2025 13:05:39 +1000 Subject: [PATCH 118/143] alif/modtime: Implement the rest of the time module. Adds: `time.time()`, `time.time_ns()`, `time.localtime()`, `time.mktime()` and `time.gmtime()`. Signed-off-by: Damien George --- ports/alif/modtime.c | 39 +++++++++++++++++++++++++++++++++++++++ ports/alif/mpconfigport.h | 4 +++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 ports/alif/modtime.c diff --git a/ports/alif/modtime.c b/ports/alif/modtime.c new file mode 100644 index 0000000000000..6d40ec2cc37cf --- /dev/null +++ b/ports/alif/modtime.c @@ -0,0 +1,39 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/mphal.h" +#include "shared/timeutils/timeutils.h" + +// Get the localtime. +static void mp_time_localtime_get(timeutils_struct_time_t *tm) { + mp_timestamp_t s = mp_hal_time_get(NULL); + timeutils_seconds_since_epoch_to_struct_time(s, tm); +} + +// Return the number of seconds since the Epoch. +static mp_obj_t mp_time_time_get(void) { + return mp_obj_new_int_from_uint(mp_hal_time_get(NULL)); +} diff --git a/ports/alif/mpconfigport.h b/ports/alif/mpconfigport.h index 8a65721bb0ffc..6b30ea2e6245a 100644 --- a/ports/alif/mpconfigport.h +++ b/ports/alif/mpconfigport.h @@ -119,7 +119,9 @@ #define MICROPY_PY_OS_UNAME (1) #define MICROPY_PY_OS_URANDOM (1) #define MICROPY_PY_RANDOM_SEED_INIT_FUNC (se_services_rand64()) -#define MICROPY_PY_TIME (1) +#define MICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIME (1) +#define MICROPY_PY_TIME_TIME_TIME_NS (1) +#define MICROPY_PY_TIME_INCLUDEFILE "ports/alif/modtime.c" #define MICROPY_PY_MACHINE (1) #define MICROPY_PY_MACHINE_INCLUDEFILE "ports/alif/modmachine.c" #define MICROPY_PY_MACHINE_RESET (1) From f0f5abb7a3dd37551736fbbfbb5792875b0a539a Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 12 Aug 2025 13:30:09 +1000 Subject: [PATCH 119/143] alif/mpconfigport: Enable cryptolib and hashlib.md5/sha1. They are enabled when SSL/mbedTLS is included in the firmware. These new features cost around +1400 bytes of code size. Signed-off-by: Damien George --- ports/alif/mpconfigport.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ports/alif/mpconfigport.h b/ports/alif/mpconfigport.h index 6b30ea2e6245a..08b27e2786e75 100644 --- a/ports/alif/mpconfigport.h +++ b/ports/alif/mpconfigport.h @@ -112,6 +112,9 @@ // Extended modules #define MICROPY_EPOCH_IS_1970 (1) +#define MICROPY_PY_CRYPTOLIB (MICROPY_PY_SSL) +#define MICROPY_PY_HASHLIB_MD5 (MICROPY_PY_SSL) +#define MICROPY_PY_HASHLIB_SHA1 (MICROPY_PY_SSL) #define MICROPY_PY_OS_INCLUDEFILE "ports/alif/modos.c" #define MICROPY_PY_OS_DUPTERM (1) #define MICROPY_PY_OS_SEP (1) From b7cfafc1ee5c0bf8148cb27c6b82a597c3a1aae0 Mon Sep 17 00:00:00 2001 From: Damien George Date: Wed, 13 Aug 2025 10:39:44 +1000 Subject: [PATCH 120/143] alif/alif.mk: Add MPY_CROSS_FLAGS setting. The HP and HE CPUs have double-precision hardware floating point, so can use the armv7emdp architecture. This allows frozen code to use native/viper/asm_thumb decorators. Fixes issue #17896. Signed-off-by: Damien George --- ports/alif/alif.mk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ports/alif/alif.mk b/ports/alif/alif.mk index 265418aa07456..d9e7c32578ce8 100644 --- a/ports/alif/alif.mk +++ b/ports/alif/alif.mk @@ -22,6 +22,8 @@ include $(TOP)/extmod/extmod.mk ################################################################################ # Project specific settings and compiler/linker flags +MPY_CROSS_FLAGS += -march=armv7emdp + CROSS_COMPILE ?= arm-none-eabi- ALIF_DFP_REL_TOP ?= lib/alif_ensemble-cmsis-dfp ALIF_DFP_REL_HERE ?= $(TOP)/lib/alif_ensemble-cmsis-dfp From bba354201809dd715287fd32c85818a23f978d97 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 13 Aug 2025 12:37:23 -0500 Subject: [PATCH 121/143] extmod/modlwip: Remove unused include and functions. The lwIP includes are now port-specific. The `sys_arch_{,un}protect` functions are not used according to line 34 of `extmod/lwip-include/lwipopts_common.h`. Neither have been touched in nearly a decade. Signed-off-by: Thomas Watson --- extmod/lwip-include/arch/cc.h | 41 --------------------------------- extmod/lwip-include/arch/perf.h | 7 ------ extmod/lwip-include/lwipopts.h | 35 ---------------------------- extmod/modlwip.c | 12 ---------- 4 files changed, 95 deletions(-) delete mode 100644 extmod/lwip-include/arch/cc.h delete mode 100644 extmod/lwip-include/arch/perf.h delete mode 100644 extmod/lwip-include/lwipopts.h diff --git a/extmod/lwip-include/arch/cc.h b/extmod/lwip-include/arch/cc.h deleted file mode 100644 index 400dc6ec75de1..0000000000000 --- a/extmod/lwip-include/arch/cc.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_ARCH_CC_H -#define MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_ARCH_CC_H - -#include - -// Generate lwip's internal types from stdint - -typedef uint8_t u8_t; -typedef int8_t s8_t; -typedef uint16_t u16_t; -typedef int16_t s16_t; -typedef uint32_t u32_t; -typedef int32_t s32_t; - -typedef u32_t mem_ptr_t; - -#define U16_F "hu" -#define S16_F "hd" -#define X16_F "hx" -#define U32_F "u" -#define S32_F "d" -#define X32_F "x" - -#define X8_F "02x" -#define SZT_F "u" - -#define BYTE_ORDER LITTLE_ENDIAN - -#define LWIP_CHKSUM_ALGORITHM 2 - -#include -#define LWIP_PLATFORM_DIAG(x) -#define LWIP_PLATFORM_ASSERT(x) { assert(1); } - -//#define PACK_STRUCT_FIELD(x) x __attribute__((packed)) -#define PACK_STRUCT_FIELD(x) x -#define PACK_STRUCT_STRUCT __attribute__((packed)) -#define PACK_STRUCT_BEGIN -#define PACK_STRUCT_END - -#endif // MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_ARCH_CC_H diff --git a/extmod/lwip-include/arch/perf.h b/extmod/lwip-include/arch/perf.h deleted file mode 100644 index d310fc339f162..0000000000000 --- a/extmod/lwip-include/arch/perf.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_ARCH_PERF_H -#define MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_ARCH_PERF_H - -#define PERF_START /* null definition */ -#define PERF_STOP(x) /* null definition */ - -#endif // MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_ARCH_PERF_H diff --git a/extmod/lwip-include/lwipopts.h b/extmod/lwip-include/lwipopts.h deleted file mode 100644 index 2122f30f044e3..0000000000000 --- a/extmod/lwip-include/lwipopts.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_LWIPOPTS_H -#define MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_LWIPOPTS_H - -#include -#include -#include - -// We're running without an OS for this port. We don't provide any services except light protection. -#define NO_SYS 1 - -#define SYS_LIGHTWEIGHT_PROT 1 -#include -typedef uint32_t sys_prot_t; - -#define TCP_LISTEN_BACKLOG 1 - -// We'll put these into a proper ifdef once somebody implements an ethernet driver -#define LWIP_ARP 0 -#define LWIP_ETHERNET 0 - -#define LWIP_DNS 1 - -#define LWIP_NETCONN 0 -#define LWIP_SOCKET 0 - -#ifdef MICROPY_PY_LWIP_SLIP -#define LWIP_HAVE_SLIPIF 1 -#endif - -// For now, we can simply define this as a macro for the timer code. But this function isn't -// universal and other ports will need to do something else. It may be necessary to move -// things like this into a port-provided header file. -#define sys_now mp_hal_ticks_ms - -#endif // MICROPY_INCLUDED_EXTMOD_LWIP_INCLUDE_LWIPOPTS_H diff --git a/extmod/modlwip.c b/extmod/modlwip.c index 26aab6e81094a..4b1c1b8f3a5ec 100644 --- a/extmod/modlwip.c +++ b/extmod/modlwip.c @@ -1724,18 +1724,6 @@ static MP_DEFINE_CONST_OBJ_TYPE( locals_dict, &lwip_socket_locals_dict ); -/******************************************************************************/ -// Support functions for memory protection. lwIP has its own memory management -// routines for its internal structures, and since they might be called in -// interrupt handlers, they need some protection. -sys_prot_t sys_arch_protect() { - return (sys_prot_t)MICROPY_BEGIN_ATOMIC_SECTION(); -} - -void sys_arch_unprotect(sys_prot_t state) { - MICROPY_END_ATOMIC_SECTION((mp_uint_t)state); -} - /******************************************************************************/ // Polling callbacks for the interfaces connected to lwIP. Right now it calls // itself a "list" but isn't; we only support a single interface. From afa7265ffa911917f381ac6790ffe032b2b7baf8 Mon Sep 17 00:00:00 2001 From: Maureen Helm Date: Fri, 25 Jul 2025 16:35:27 -0500 Subject: [PATCH 122/143] zephyr: Upgrade to Zephyr v4.2.0. Updates the Zephyr port build instructions and CI to use the latest Zephyr release tag. Tested on max32690fthr and frdm_k64f. Signed-off-by: Maureen Helm --- docs/zephyr/tutorial/repl.rst | 4 ++-- ports/zephyr/README.md | 8 ++++---- tools/ci.sh | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/zephyr/tutorial/repl.rst b/docs/zephyr/tutorial/repl.rst index 199dda2b7aeee..1b16c5ad21d7b 100644 --- a/docs/zephyr/tutorial/repl.rst +++ b/docs/zephyr/tutorial/repl.rst @@ -31,8 +31,8 @@ With your serial program open (PuTTY, screen, picocom, etc) you may see a blank screen with a flashing cursor. Press Enter (or reset the board) and you should be presented with the following text:: - *** Booting Zephyr OS build v4.0.0 *** - MicroPython v1.24.0-preview.179.g5b85b24bd on 2024-08-05; zephyr-frdm_k64f with mk64f12 + *** Booting Zephyr OS build v4.2.0 *** + MicroPython v1.26.0-preview.451.gebc9525c9 on 2025-07-25; zephyr-frdm_k64f with mk64f12 Type "help()" for more information. >>> diff --git a/ports/zephyr/README.md b/ports/zephyr/README.md index 17c1f613de4ed..8384fabcf0261 100644 --- a/ports/zephyr/README.md +++ b/ports/zephyr/README.md @@ -5,8 +5,8 @@ This is a work-in-progress port of MicroPython to Zephyr RTOS (http://zephyrproject.org). This port tries to support all Zephyr versions supported upstream, -i.e. currently v3.7 (LTS), v4.0 and the development branch. The CI is -setup to use the latest version, i.e. v4.0. +i.e. currently v3.7 (LTS), v4.2 and the development branch. The CI is +setup to use the latest version, i.e. v4.2. All boards supported by Zephyr (with standard level of features support, like UART console) should work with MicroPython (but not all @@ -43,13 +43,13 @@ setup is correct. If you already have Zephyr installed but are having issues building the MicroPython port then try installing the correct version of Zephyr via: - $ west init zephyrproject -m https://github.com/zephyrproject-rtos/zephyr --mr v4.0.0 + $ west init zephyrproject -m https://github.com/zephyrproject-rtos/zephyr --mr v4.2.0 Alternatively, you don't have to redo the Zephyr installation to just switch from master to a tagged release, you can instead do: $ cd zephyrproject/zephyr - $ git checkout v4.0.0 + $ git checkout v4.2.0 $ west update With Zephyr installed you may then need to configure your environment, diff --git a/tools/ci.sh b/tools/ci.sh index 8f045639b80c9..9152c2e2dd699 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -886,9 +886,9 @@ function ci_windows_build { ######################################################################################## # ports/zephyr -ZEPHYR_DOCKER_VERSION=v0.27.4 -ZEPHYR_SDK_VERSION=0.17.0 -ZEPHYR_VERSION=v4.0.0 +ZEPHYR_DOCKER_VERSION=v0.28.1 +ZEPHYR_SDK_VERSION=0.17.2 +ZEPHYR_VERSION=v4.2.0 function ci_zephyr_setup { IMAGE=ghcr.io/zephyrproject-rtos/ci:${ZEPHYR_DOCKER_VERSION} From 1588c455c41b445bfd1c65b5055847ab63114c96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 02:37:54 +0000 Subject: [PATCH 123/143] github/workflows: Bump actions/checkout from 4 to 5. Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/biome.yml | 2 +- .github/workflows/code_formatting.yml | 2 +- .github/workflows/code_size.yml | 2 +- .github/workflows/codespell.yml | 2 +- .github/workflows/commit_formatting.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/examples.yml | 2 +- .github/workflows/mpremote.yml | 2 +- .github/workflows/mpy_format.yml | 2 +- .github/workflows/ports.yml | 2 +- .github/workflows/ports_alif.yml | 2 +- .github/workflows/ports_cc3200.yml | 2 +- .github/workflows/ports_esp32.yml | 2 +- .github/workflows/ports_esp8266.yml | 2 +- .github/workflows/ports_mimxrt.yml | 2 +- .github/workflows/ports_nrf.yml | 2 +- .github/workflows/ports_powerpc.yml | 2 +- .github/workflows/ports_qemu.yml | 4 +-- .github/workflows/ports_renesas-ra.yml | 2 +- .github/workflows/ports_rp2.yml | 2 +- .github/workflows/ports_samd.yml | 2 +- .github/workflows/ports_stm32.yml | 2 +- .github/workflows/ports_unix.yml | 38 ++++++++++++------------- .github/workflows/ports_webassembly.yml | 2 +- .github/workflows/ports_windows.yml | 6 ++-- .github/workflows/ports_zephyr.yml | 2 +- .github/workflows/ruff.yml | 2 +- 27 files changed, 48 insertions(+), 48 deletions(-) diff --git a/.github/workflows/biome.yml b/.github/workflows/biome.yml index 88744f16ca7d6..fea9c9c8bd7bc 100644 --- a/.github/workflows/biome.yml +++ b/.github/workflows/biome.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Biome uses: biomejs/setup-biome@v2 with: diff --git a/.github/workflows/code_formatting.yml b/.github/workflows/code_formatting.yml index 9f30f048cfdbe..5669779946e06 100644 --- a/.github/workflows/code_formatting.yml +++ b/.github/workflows/code_formatting.yml @@ -10,7 +10,7 @@ jobs: code-formatting: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 - name: Install packages run: source tools/ci.sh && ci_c_code_formatting_setup diff --git a/.github/workflows/code_size.yml b/.github/workflows/code_size.yml index 67261933798cb..8587ef0179442 100644 --- a/.github/workflows/code_size.yml +++ b/.github/workflows/code_size.yml @@ -25,7 +25,7 @@ jobs: build: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 100 - name: Install packages diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 1d6b1dc9d2b27..688134b42515c 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -6,7 +6,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # codespell version should be kept in sync with .pre-commit-config.yml - run: pip install --user codespell==2.4.1 tomli - run: codespell diff --git a/.github/workflows/commit_formatting.yml b/.github/workflows/commit_formatting.yml index fcbcaa7092ee2..2e1def95c36e8 100644 --- a/.github/workflows/commit_formatting.yml +++ b/.github/workflows/commit_formatting.yml @@ -10,7 +10,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 100 - uses: actions/setup-python@v5 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 62a6f69fc3924..f9d61125b50c1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 - name: Install Python packages run: pip install -r docs/requirements.txt diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 6613f106625a2..d16122b720b6c 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -18,7 +18,7 @@ jobs: embedding: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build run: make -C examples/embedding -f micropython_embed.mk && make -C examples/embedding - name: Run diff --git a/.github/workflows/mpremote.yml b/.github/workflows/mpremote.yml index ee91b6360b9b4..359d888286400 100644 --- a/.github/workflows/mpremote.yml +++ b/.github/workflows/mpremote.yml @@ -11,7 +11,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: # Setting this to zero means fetch all history and tags, # which hatch-vcs can use to discover the version tag. diff --git a/.github/workflows/mpy_format.yml b/.github/workflows/mpy_format.yml index b6768a46c3cdc..4043b63288a0c 100644 --- a/.github/workflows/mpy_format.yml +++ b/.github/workflows/mpy_format.yml @@ -17,7 +17,7 @@ jobs: test: runs-on: ubuntu-22.04 # use 22.04 to get python2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_mpy_format_setup - name: Test mpy-tool.py diff --git a/.github/workflows/ports.yml b/.github/workflows/ports.yml index 1f262b0ba4bee..d4e89bd1aa924 100644 --- a/.github/workflows/ports.yml +++ b/.github/workflows/ports.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build ports download metadata run: mkdir boards && ./tools/autobuild/build-downloads.py . ./boards diff --git a/.github/workflows/ports_alif.yml b/.github/workflows/ports_alif.yml index 0e96e7d816e50..a06b3f96ffae4 100644 --- a/.github/workflows/ports_alif.yml +++ b/.github/workflows/ports_alif.yml @@ -26,7 +26,7 @@ jobs: - alif_ae3_build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_alif_setup - name: Build ci_${{matrix.ci_func }} diff --git a/.github/workflows/ports_cc3200.yml b/.github/workflows/ports_cc3200.yml index f178a140587db..b60ff370daf9d 100644 --- a/.github/workflows/ports_cc3200.yml +++ b/.github/workflows/ports_cc3200.yml @@ -21,7 +21,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_cc3200_setup - name: Build diff --git a/.github/workflows/ports_esp32.yml b/.github/workflows/ports_esp32.yml index 36ab341cfcdd2..b86c6a76f82c7 100644 --- a/.github/workflows/ports_esp32.yml +++ b/.github/workflows/ports_esp32.yml @@ -28,7 +28,7 @@ jobs: - esp32_build_c2_c6 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - id: idf_ver name: Read the ESP-IDF version (including Python version) diff --git a/.github/workflows/ports_esp8266.yml b/.github/workflows/ports_esp8266.yml index 5236edf40b959..3293abed5980d 100644 --- a/.github/workflows/ports_esp8266.yml +++ b/.github/workflows/ports_esp8266.yml @@ -21,7 +21,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_esp8266_setup && ci_esp8266_path >> $GITHUB_PATH - name: Build diff --git a/.github/workflows/ports_mimxrt.yml b/.github/workflows/ports_mimxrt.yml index 7743e036ab377..ae9a80ec5806c 100644 --- a/.github/workflows/ports_mimxrt.yml +++ b/.github/workflows/ports_mimxrt.yml @@ -24,7 +24,7 @@ jobs: run: working-directory: 'micropython repo' # test build with space in path steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: path: 'micropython repo' - name: Install packages diff --git a/.github/workflows/ports_nrf.yml b/.github/workflows/ports_nrf.yml index 76727c9d1f6bd..ce86617af05ce 100644 --- a/.github/workflows/ports_nrf.yml +++ b/.github/workflows/ports_nrf.yml @@ -21,7 +21,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_nrf_setup - name: Build diff --git a/.github/workflows/ports_powerpc.yml b/.github/workflows/ports_powerpc.yml index c41b13e5ddffe..81f71ca8a96f4 100644 --- a/.github/workflows/ports_powerpc.yml +++ b/.github/workflows/ports_powerpc.yml @@ -21,7 +21,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_powerpc_setup - name: Build diff --git a/.github/workflows/ports_qemu.yml b/.github/workflows/ports_qemu.yml index ac09dde86408b..857645776629a 100644 --- a/.github/workflows/ports_qemu.yml +++ b/.github/workflows/ports_qemu.yml @@ -29,7 +29,7 @@ jobs: - thumb runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_qemu_setup_arm - name: Build and run test suite ci_qemu_build_arm_${{ matrix.ci_func }} @@ -41,7 +41,7 @@ jobs: build_and_test_rv32: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_qemu_setup_rv32 - name: Build and run test suite diff --git a/.github/workflows/ports_renesas-ra.yml b/.github/workflows/ports_renesas-ra.yml index b9fa74331dc0b..bf99ed25fedae 100644 --- a/.github/workflows/ports_renesas-ra.yml +++ b/.github/workflows/ports_renesas-ra.yml @@ -21,7 +21,7 @@ jobs: build_renesas_ra_board: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_renesas_ra_setup - name: Build diff --git a/.github/workflows/ports_rp2.yml b/.github/workflows/ports_rp2.yml index 748f38e143893..22d2a9688015f 100644 --- a/.github/workflows/ports_rp2.yml +++ b/.github/workflows/ports_rp2.yml @@ -24,7 +24,7 @@ jobs: run: working-directory: 'micropython repo' # test build with space in path steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: path: 'micropython repo' - name: Install packages diff --git a/.github/workflows/ports_samd.yml b/.github/workflows/ports_samd.yml index 5bf1826cd17bb..dbea255c79ad4 100644 --- a/.github/workflows/ports_samd.yml +++ b/.github/workflows/ports_samd.yml @@ -21,7 +21,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_samd_setup - name: Build diff --git a/.github/workflows/ports_stm32.yml b/.github/workflows/ports_stm32.yml index 8800f145189b8..43659a5d5dd5d 100644 --- a/.github/workflows/ports_stm32.yml +++ b/.github/workflows/ports_stm32.yml @@ -28,7 +28,7 @@ jobs: - stm32_misc_build runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_stm32_setup - name: Build ci_${{matrix.ci_func }} diff --git a/.github/workflows/ports_unix.yml b/.github/workflows/ports_unix.yml index f3f613a789af3..8fd8e1aec23d4 100644 --- a/.github/workflows/ports_unix.yml +++ b/.github/workflows/ports_unix.yml @@ -23,7 +23,7 @@ jobs: minimal: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build run: source tools/ci.sh && ci_unix_minimal_build - name: Run main test suite @@ -35,7 +35,7 @@ jobs: reproducible: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build with reproducible date run: source tools/ci.sh && ci_unix_minimal_build env: @@ -46,7 +46,7 @@ jobs: standard: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build run: source tools/ci.sh && ci_unix_standard_build - name: Run main test suite @@ -58,7 +58,7 @@ jobs: standard_v2: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build run: source tools/ci.sh && ci_unix_standard_v2_build - name: Run main test suite @@ -70,7 +70,7 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. @@ -105,7 +105,7 @@ jobs: coverage_32bit: runs-on: ubuntu-22.04 # use 22.04 to get libffi-dev:i386 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_unix_32bit_setup - name: Build @@ -123,7 +123,7 @@ jobs: nanbox: runs-on: ubuntu-22.04 # use 22.04 to get python2, and libffi-dev:i386 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_unix_32bit_setup - name: Build @@ -137,7 +137,7 @@ jobs: longlong: runs-on: ubuntu-22.04 # use 22.04 to get python2, and libffi-dev:i386 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_unix_32bit_setup - name: Build @@ -151,7 +151,7 @@ jobs: float: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build run: source tools/ci.sh && ci_unix_float_build - name: Run main test suite @@ -163,7 +163,7 @@ jobs: gil_enabled: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build run: source tools/ci.sh && ci_unix_gil_enabled_build - name: Run main test suite @@ -175,7 +175,7 @@ jobs: stackless_clang: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_unix_clang_setup - name: Build @@ -189,7 +189,7 @@ jobs: float_clang: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_unix_clang_setup - name: Build @@ -203,7 +203,7 @@ jobs: settrace_stackless: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. @@ -220,7 +220,7 @@ jobs: macos: runs-on: macos-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: '3.8' @@ -236,7 +236,7 @@ jobs: # ubuntu-22.04 is needed for older libffi. runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_unix_qemu_mips_setup - name: Build @@ -251,7 +251,7 @@ jobs: # ubuntu-22.04 is needed for older libffi. runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_unix_qemu_arm_setup - name: Build @@ -266,7 +266,7 @@ jobs: # ubuntu-22.04 is needed for older libffi. runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_unix_qemu_riscv64_setup - name: Build @@ -280,7 +280,7 @@ jobs: sanitize_address: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. @@ -305,7 +305,7 @@ jobs: sanitize_undefined: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 # Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests. # Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default. diff --git a/.github/workflows/ports_webassembly.yml b/.github/workflows/ports_webassembly.yml index ceb35f83cd8f1..14399950b569e 100644 --- a/.github/workflows/ports_webassembly.yml +++ b/.github/workflows/ports_webassembly.yml @@ -21,7 +21,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_webassembly_setup - name: Build diff --git a/.github/workflows/ports_windows.yml b/.github/workflows/ports_windows.yml index f33277d471d3c..6b492640a1fa0 100644 --- a/.github/workflows/ports_windows.yml +++ b/.github/workflows/ports_windows.yml @@ -58,7 +58,7 @@ jobs: - uses: microsoft/setup-msbuild@v2 with: vs-version: ${{ matrix.vs_version }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build mpy-cross.exe run: msbuild mpy-cross\mpy-cross.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} - name: Update submodules @@ -125,7 +125,7 @@ jobs: git diffutils path-type: inherit # Remove when setup-python is removed - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build mpy-cross.exe run: make -C mpy-cross -j2 - name: Update submodules @@ -143,7 +143,7 @@ jobs: cross-build-on-linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install packages run: source tools/ci.sh && ci_windows_setup - name: Build diff --git a/.github/workflows/ports_zephyr.yml b/.github/workflows/ports_zephyr.yml index ca3c36e004cca..9ce7034398669 100644 --- a/.github/workflows/ports_zephyr.yml +++ b/.github/workflows/ports_zephyr.yml @@ -30,7 +30,7 @@ jobs: large-packages: false docker-images: false swap-storage: false - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - id: versions name: Read Zephyr version run: source tools/ci.sh && echo "ZEPHYR=$ZEPHYR_VERSION" | tee "$GITHUB_OUTPUT" diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 4c4a2a3162ed6..633b0cdf82ef4 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -6,7 +6,7 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # ruff version should be kept in sync with .pre-commit-config.yaml & also micropython-lib - run: pipx install ruff==0.11.6 - run: ruff check --output-format=github . From 1b35116c920550ba7bfc0f06bd4bad7d20f33b21 Mon Sep 17 00:00:00 2001 From: Yuuki NAGAO Date: Tue, 22 Jul 2025 20:14:14 +0900 Subject: [PATCH 124/143] stm32/dac: Fix 12-bit DAC issue on STM32H5. For STM32H5, to use 12-bit DAC, the DMA parameter should set: - Actual DMA source datawidth to CTR1. - The length is the amount of data to be transferred from source to destination in bytes. Also, this commit modifies the (dummy) definition of DMA_CIRCULAR for STM32H5 to prevent conflict with data width specification. Signed-off-by: Yuuki NAGAO --- ports/stm32/dac.c | 7 +++++-- ports/stm32/dma.c | 4 ++-- ports/stm32/dma.h | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ports/stm32/dac.c b/ports/stm32/dac.c index 54d5acc6cea9d..0be0fd9300b12 100644 --- a/ports/stm32/dac.c +++ b/ports/stm32/dac.c @@ -174,7 +174,7 @@ static void dac_start_dma(uint32_t dac_channel, const dma_descr_t *dma_descr, ui // For STM32G4, DAC registers have to be accessed by words (32-bit). dma_align = DMA_MDATAALIGN_BYTE | DMA_PDATAALIGN_WORD; #elif defined(STM32H5) - dma_align = 0; + dma_align = DMA_SRC_DATAWIDTH_BYTE | DMA_DEST_DATAWIDTH_WORD; #else dma_align = DMA_MDATAALIGN_BYTE | DMA_PDATAALIGN_BYTE; #endif @@ -183,7 +183,7 @@ static void dac_start_dma(uint32_t dac_channel, const dma_descr_t *dma_descr, ui // For STM32G4, DAC registers have to be accessed by words (32-bit). dma_align = DMA_MDATAALIGN_HALFWORD | DMA_PDATAALIGN_WORD; #elif defined(STM32H5) - dma_align = 0; + dma_align = DMA_SRC_DATAWIDTH_HALFWORD | DMA_DEST_DATAWIDTH_WORD; #else dma_align = DMA_MDATAALIGN_HALFWORD | DMA_PDATAALIGN_HALFWORD; #endif @@ -490,7 +490,10 @@ mp_obj_t pyb_dac_write_timed(size_t n_args, const mp_obj_t *pos_args, mp_map_t * align = DAC_ALIGN_8B_R; } else { align = DAC_ALIGN_12B_R; + // For STM32H5, the length is the amount of data to be transferred from source to destination in bytes. + #if !defined(STM32H5) bufinfo.len /= 2; + #endif } dac_start_dma(self->dac_channel, tx_dma_descr, args[2].u_int, self->bits, align, bufinfo.len, bufinfo.buf); diff --git a/ports/stm32/dma.c b/ports/stm32/dma.c index c252770740d01..c2923f7ae732c 100644 --- a/ports/stm32/dma.c +++ b/ports/stm32/dma.c @@ -1737,10 +1737,10 @@ void dma_nohal_init(const dma_descr_t *descr, uint32_t config) { dma->CCR = init->Priority; uint32_t ctr1reg = 0; - ctr1reg |= init->SrcDataWidth; + ctr1reg |= config & DMA_CTR1_SDW_LOG2_Msk; ctr1reg |= init->SrcInc; ctr1reg |= (((init->SrcBurstLength - 1) << DMA_CTR1_SBL_1_Pos)) & DMA_CTR1_SBL_1_Msk; - ctr1reg |= init->DestDataWidth; + ctr1reg |= config & DMA_CTR1_DDW_LOG2_Msk; ctr1reg |= init->DestInc; ctr1reg |= (((init->DestBurstLength - 1) << DMA_CTR1_DBL_1_Pos)) & DMA_CTR1_DBL_1_Msk; diff --git a/ports/stm32/dma.h b/ports/stm32/dma.h index f05b22b5d05cd..75e007bc9fa98 100644 --- a/ports/stm32/dma.h +++ b/ports/stm32/dma.h @@ -33,7 +33,7 @@ typedef struct _dma_descr_t dma_descr_t; #if defined(STM32H5) // STM32H5 GPDMA doesn't feature circular mode directly, so define doesn't exist in // stm32 driver header. Define it here to make users like DAC driver happy. -#define DMA_CIRCULAR 0x00000001 +#define DMA_CIRCULAR 0x20000000 #endif #if defined(STM32F0) || defined(STM32F4) || defined(STM32F7) || defined(STM32G0) || defined(STM32H5) || defined(STM32H7) From 152a0782e6ef8fc510b3e0106df82e27595847c2 Mon Sep 17 00:00:00 2001 From: Yuuki NAGAO Date: Fri, 25 Jul 2025 20:14:03 +0900 Subject: [PATCH 125/143] stm32/dac: Add support for DAC feature on STM32G0. DAC.write() and DAC.write_timed() are now available on STM32G0. Tested on NUCLEO_G0B1RE. Signed-off-by: Yuuki NAGAO --- ports/stm32/boards/NUCLEO_G0B1RE/mpconfigboard.h | 2 +- ports/stm32/dac.c | 2 +- ports/stm32/dma.c | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ports/stm32/boards/NUCLEO_G0B1RE/mpconfigboard.h b/ports/stm32/boards/NUCLEO_G0B1RE/mpconfigboard.h index 092ee177925eb..ead36ed6c763f 100644 --- a/ports/stm32/boards/NUCLEO_G0B1RE/mpconfigboard.h +++ b/ports/stm32/boards/NUCLEO_G0B1RE/mpconfigboard.h @@ -5,7 +5,7 @@ #define MICROPY_HW_HAS_FLASH (1) #define MICROPY_HW_ENABLE_RNG (0) #define MICROPY_HW_ENABLE_RTC (1) -#define MICROPY_HW_ENABLE_DAC (0) +#define MICROPY_HW_ENABLE_DAC (1) #define MICROPY_HW_ENABLE_USB (0) // can be enabled if USB cable connected to PA11/PA12 #define MICROPY_PY_PYB_LEGACY (0) diff --git a/ports/stm32/dac.c b/ports/stm32/dac.c index 0be0fd9300b12..040892553d38c 100644 --- a/ports/stm32/dac.c +++ b/ports/stm32/dac.c @@ -97,7 +97,7 @@ static uint32_t TIMx_Config(mp_obj_t timer) { // work out the trigger channel (only certain ones are supported) if (tim->Instance == TIM2) { return DAC_TRIGGER_T2_TRGO; - #if defined(TIM4) + #if defined(TIM4) && defined(DAC_TRIGGER_T4_TRGO) // G0B1 doesn't have this } else if (tim->Instance == TIM4) { return DAC_TRIGGER_T4_TRGO; #endif diff --git a/ports/stm32/dma.c b/ports/stm32/dma.c index c2923f7ae732c..ad199b1fb91d9 100644 --- a/ports/stm32/dma.c +++ b/ports/stm32/dma.c @@ -1657,7 +1657,7 @@ static void dma_idle_handler(uint32_t tick) { } #endif -#if defined(STM32F0) || defined(STM32G4) || defined(STM32L0) || defined(STM32L1) || defined(STM32L4) +#if defined(STM32F0) || defined(STM32G0) || defined(STM32G4) || defined(STM32L0) || defined(STM32L1) || defined(STM32L4) void dma_nohal_init(const dma_descr_t *descr, uint32_t config) { DMA_Channel_TypeDef *dma = descr->instance; @@ -1680,7 +1680,7 @@ void dma_nohal_init(const dma_descr_t *descr, uint32_t config) { } else { __HAL_DMA2_REMAP(descr->sub_instance); } - #elif defined(STM32G4) + #elif defined(STM32G0) || defined(STM32G4) uint32_t *dmamux_ctrl = (void *)(DMAMUX1_Channel0_BASE + 0x04 * descr->id); *dmamux_ctrl = (*dmamux_ctrl & ~(0x7f)) | descr->sub_instance; #elif defined(STM32L1) @@ -1807,7 +1807,7 @@ void dma_nohal_start(const dma_descr_t *descr, uint32_t src_addr, uint32_t dst_a dma->CCR |= DMA_CCR_EN; } -#elif defined(STM32G0) || defined(STM32N6) || defined(STM32WB) || defined(STM32WL) +#elif defined(STM32N6) || defined(STM32WB) || defined(STM32WL) // These functions are currently not implemented or needed for this MCU. From 365329cd540f631f1cfe805b475f792cf00257e0 Mon Sep 17 00:00:00 2001 From: Yuuki NAGAO Date: Sun, 27 Jul 2025 22:03:42 +0900 Subject: [PATCH 126/143] stm32/dac: Fix DAC write for MCUs that have D-Cache. To prevent wrong DAC output, clean D-cache before starting DMA. For more details, please refer to the following document: https://www.st.com/resource/en/application_note/DM00272913.pdf Signed-off-by: Yuuki NAGAO --- ports/stm32/dac.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ports/stm32/dac.c b/ports/stm32/dac.c index 040892553d38c..8022fd274ce97 100644 --- a/ports/stm32/dac.c +++ b/ports/stm32/dac.c @@ -485,6 +485,9 @@ mp_obj_t pyb_dac_write_timed(size_t n_args, const mp_obj_t *pos_args, mp_map_t * #endif } + // To prevent invalid dac output, clean D-cache before starting dma. + MP_HAL_CLEAN_DCACHE(bufinfo.buf, bufinfo.len); + uint32_t align; if (self->bits == 8) { align = DAC_ALIGN_8B_R; From c7ddf0c54fcc9c6abdb38db27a558ab635ba9763 Mon Sep 17 00:00:00 2001 From: iabdalkader Date: Mon, 11 Aug 2025 14:20:36 +0200 Subject: [PATCH 127/143] stm32/Makefile: Add .gc.blocks.table section to generated binary. The generated binary file was missing this section, which caused a hard fault when loading bin or dfu firmware (eg on ARDUINO_GIGA). Signed-off-by: iabdalkader --- ports/stm32/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ports/stm32/Makefile b/ports/stm32/Makefile index 37ce8fbd8383d..abca3a05f8c7a 100644 --- a/ports/stm32/Makefile +++ b/ports/stm32/Makefile @@ -611,7 +611,7 @@ TEXT0_ADDR ?= 0x08000000 ifeq ($(TEXT1_ADDR),) # No TEXT1_ADDR given so put all firmware at TEXT0_ADDR location -TEXT0_SECTIONS ?= .isr_vector .isr_extratext .text .data .ARM +TEXT0_SECTIONS ?= .isr_vector .isr_extratext .text .gc.blocks.table .data .ARM deploy-stlink: $(BUILD)/firmware.bin $(call RUN_STLINK,$^,$(TEXT0_ADDR)) @@ -629,7 +629,7 @@ else # TEXT0_ADDR and TEXT1_ADDR are specified so split firmware between these locations TEXT0_SECTIONS ?= .isr_vector .isr_extratext -TEXT1_SECTIONS ?= .text .data .ARM +TEXT1_SECTIONS ?= .text .gc.blocks.table .data .ARM deploy-stlink: $(BUILD)/firmware0.bin $(BUILD)/firmware1.bin $(call RUN_STLINK,$(word 1,$^),$(TEXT0_ADDR)) From 020eeba41246f6015fb56f830141077d43d2334b Mon Sep 17 00:00:00 2001 From: Tico06 Date: Mon, 11 Aug 2025 19:30:47 +0200 Subject: [PATCH 128/143] stm32/eth_phy: Fix typo in header guard macro. Typo on line 29 (PYH instead of PHY). Compilation failing, here is the output: eth_phy.h:28: error: header guard 'MICROPY_INCLUDED_STM32_PHY_H' followed by '#define' of a different macro [Werror=header-guard] 28 | #ifndef MICROPY_INCLUDED_STM32_PHY_H eth_phy.h:29: note: 'MICROPY_INCLUDED_STM32_PYH_H' is defined here; did you mean 'MICROPY_INCLUDED_STM32_PHY_H'? 29 | #define MICROPY_INCLUDED_STM32_PYH_H Signed-off-by: Tico06 --- ports/stm32/eth_phy.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/stm32/eth_phy.h b/ports/stm32/eth_phy.h index 5036905c1f582..dccfb7951add8 100644 --- a/ports/stm32/eth_phy.h +++ b/ports/stm32/eth_phy.h @@ -26,7 +26,7 @@ */ #ifndef MICROPY_INCLUDED_STM32_PHY_H -#define MICROPY_INCLUDED_STM32_PYH_H +#define MICROPY_INCLUDED_STM32_PHY_H #if defined(MICROPY_HW_ETH_MDC) From d81d56cc4d7c44978761f967841bcb6e8e84b54d Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Tue, 22 Jul 2025 10:56:22 +1000 Subject: [PATCH 129/143] tools/ci: Add UBSan to longlong CI build. Also rewrite the sanitizer argument variables to not assume a variant. longlong variant currently fails in this config, due to a bug fixed in follow-up commit. Signed-off-by: Angus Gratton --- tools/ci.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tools/ci.sh b/tools/ci.sh index 9152c2e2dd699..095d96cf39beb 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -518,13 +518,11 @@ CI_UNIX_OPTS_QEMU_RISCV64=( ) CI_UNIX_OPTS_SANITIZE_ADDRESS=( - VARIANT=coverage CFLAGS_EXTRA="-fsanitize=address --param asan-use-after-return=0" LDFLAGS_EXTRA="-fsanitize=address --param asan-use-after-return=0" ) CI_UNIX_OPTS_SANITIZE_UNDEFINED=( - VARIANT=coverage CFLAGS_EXTRA="-fsanitize=undefined -fno-sanitize=nonnull-attribute" LDFLAGS_EXTRA="-fsanitize=undefined -fno-sanitize=nonnull-attribute" ) @@ -699,7 +697,7 @@ function ci_unix_nanbox_run_tests { } function ci_unix_longlong_build { - ci_unix_build_helper VARIANT=longlong + ci_unix_build_helper VARIANT=longlong "${CI_UNIX_OPTS_SANITIZE_UNDEFINED[@]}" } function ci_unix_longlong_run_tests { @@ -765,23 +763,23 @@ function ci_unix_settrace_stackless_run_tests { function ci_unix_sanitize_undefined_build { make ${MAKEOPTS} -C mpy-cross make ${MAKEOPTS} -C ports/unix submodules - make ${MAKEOPTS} -C ports/unix "${CI_UNIX_OPTS_SANITIZE_UNDEFINED[@]}" + make ${MAKEOPTS} -C ports/unix VARIANT=coverage "${CI_UNIX_OPTS_SANITIZE_UNDEFINED[@]}" ci_unix_build_ffi_lib_helper gcc } function ci_unix_sanitize_undefined_run_tests { - MICROPY_TEST_TIMEOUT=60 ci_unix_run_tests_full_helper coverage "${CI_UNIX_OPTS_SANITIZE_UNDEFINED[@]}" + MICROPY_TEST_TIMEOUT=60 ci_unix_run_tests_full_helper coverage VARIANT=coverage "${CI_UNIX_OPTS_SANITIZE_UNDEFINED[@]}" } function ci_unix_sanitize_address_build { make ${MAKEOPTS} -C mpy-cross make ${MAKEOPTS} -C ports/unix submodules - make ${MAKEOPTS} -C ports/unix "${CI_UNIX_OPTS_SANITIZE_ADDRESS[@]}" + make ${MAKEOPTS} -C ports/unix VARIANT=coverage "${CI_UNIX_OPTS_SANITIZE_ADDRESS[@]}" ci_unix_build_ffi_lib_helper gcc } function ci_unix_sanitize_address_run_tests { - MICROPY_TEST_TIMEOUT=60 ci_unix_run_tests_full_helper coverage "${CI_UNIX_OPTS_SANITIZE_ADDRESS[@]}" + MICROPY_TEST_TIMEOUT=60 ci_unix_run_tests_full_helper coverage VARIANT=coverage "${CI_UNIX_OPTS_SANITIZE_ADDRESS[@]}" } function ci_unix_macos_build { From 3faf2298537ee34648d27a662529ff2aa73cec1c Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 6 Aug 2025 10:19:43 +1000 Subject: [PATCH 130/143] py/misc: Add a way to detect sanitizer builds. Clang and gcc>=14 can use __has_feature() to detect if a sanitizer is enabled, but older GCC has no mechanism - need to set a macro explicitly for this to be recognised. Necessary for increasing some resource limits in sanitizer builds. Important not to use to avoid real issues! This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- py/misc.h | 24 ++++++++++++++++++++++++ tools/ci.sh | 6 ++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/py/misc.h b/py/misc.h index 5c1cc2f7a836b..081163cadf9fd 100644 --- a/py/misc.h +++ b/py/misc.h @@ -43,6 +43,11 @@ typedef unsigned int uint; #ifndef __has_builtin #define __has_builtin(x) (0) #endif +#ifndef __has_feature +// This macro is supported by Clang and gcc>=14 +#define __has_feature(x) (0) +#endif + /** generic ops *************************************************/ @@ -538,4 +543,23 @@ inline static bool mp_sub_ll_overflow(long long int lhs, long long int rhs, long } #endif + +// Helper macros for detecting if sanitizers are enabled +// +// Use sparingly, not for masking issues reported by sanitizers! +// +// Can be detected automatically in Clang and gcc>=14, need to be +// set manually otherwise. +#ifndef MP_UBSAN +#define MP_UBSAN __has_feature(undefined_behavior_sanitizer) +#endif + +#ifndef MP_ASAN +#define MP_ASAN __has_feature(address_sanitizer) +#endif + +#ifndef MP_SANITIZER_BUILD +#define MP_SANITIZER_BUILD (MP_UBSAN || MP_ASAN) +#endif + #endif // MICROPY_INCLUDED_PY_MISC_H diff --git a/tools/ci.sh b/tools/ci.sh index 095d96cf39beb..e165cb2cf3f48 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -518,12 +518,14 @@ CI_UNIX_OPTS_QEMU_RISCV64=( ) CI_UNIX_OPTS_SANITIZE_ADDRESS=( - CFLAGS_EXTRA="-fsanitize=address --param asan-use-after-return=0" + # Macro MP_ASAN allows detecting ASan on gcc<=13 + CFLAGS_EXTRA="-fsanitize=address --param asan-use-after-return=0 -DMP_ASAN=1" LDFLAGS_EXTRA="-fsanitize=address --param asan-use-after-return=0" ) CI_UNIX_OPTS_SANITIZE_UNDEFINED=( - CFLAGS_EXTRA="-fsanitize=undefined -fno-sanitize=nonnull-attribute" + # Macro MP_UBSAN allows detecting UBSan on gcc<=13 + CFLAGS_EXTRA="-fsanitize=undefined -fno-sanitize=nonnull-attribute -DMP_UBSAN=1" LDFLAGS_EXTRA="-fsanitize=undefined -fno-sanitize=nonnull-attribute" ) From d8672f4cde83c0a5632f9e6ac5a55eb68fed64f9 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Wed, 6 Aug 2025 10:20:57 +1000 Subject: [PATCH 131/143] unix: Increase stack sizes if running with sanitizers. The specific problem seems to be that the runtime "Python stack frame" function call is several times more expensive in stack usage when running with UBSan on older GCC (observed on gcc 11.4 as used in CI, would get 'RuntimeError: maximum recursion depth exceeded' when running some tests with UBSan enabled.) Other stack usage (i.e. from pushing things on the stack in Python) stays the same. Whatever causes the usage seems to be mostly gone in later GCC versions. Includes a refactor to apply the same stack size multipliers for the default thread stack size same as the main stack size. This goes in a new port-specific header as it depends on macros in misc.h, so can't be in mpconfigport.h. A side effect of this is that the default thread stack size is now doubled on ARM, same as the main stack size. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/unix/main.c | 7 ++--- ports/unix/mpthreadport.c | 5 ++-- ports/unix/stack_size.h | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 ports/unix/stack_size.h diff --git a/ports/unix/main.c b/ports/unix/main.c index 5f6b99f989a3a..dd0788aaad96b 100644 --- a/ports/unix/main.c +++ b/ports/unix/main.c @@ -54,6 +54,7 @@ #include "extmod/vfs_posix.h" #include "genhdr/mpversion.h" #include "input.h" +#include "stack_size.h" // Command line options, with their defaults static bool compile_only = false; @@ -479,11 +480,7 @@ int main(int argc, char **argv) { #endif // Define a reasonable stack limit to detect stack overflow. - mp_uint_t stack_size = 40000 * (sizeof(void *) / 4); - #if defined(__arm__) && !defined(__thumb2__) - // ARM (non-Thumb) architectures require more stack. - stack_size *= 2; - #endif + mp_uint_t stack_size = 40000 * UNIX_STACK_MULTIPLIER; // We should capture stack top ASAP after start, and it should be // captured guaranteedly before any other stack variables are allocated. diff --git a/ports/unix/mpthreadport.c b/ports/unix/mpthreadport.c index 141cd0218d93e..a41b3ec9f4701 100644 --- a/ports/unix/mpthreadport.c +++ b/ports/unix/mpthreadport.c @@ -31,6 +31,7 @@ #include "py/runtime.h" #include "py/mpthread.h" #include "py/gc.h" +#include "stack_size.h" #if MICROPY_PY_THREAD @@ -244,9 +245,9 @@ void mp_thread_start(void) { } mp_uint_t mp_thread_create(void *(*entry)(void *), void *arg, size_t *stack_size) { - // default stack size is 8k machine-words + // default stack size if (*stack_size == 0) { - *stack_size = 8192 * sizeof(void *); + *stack_size = 32768 * UNIX_STACK_MULTIPLIER; } // minimum stack size is set by pthreads diff --git a/ports/unix/stack_size.h b/ports/unix/stack_size.h new file mode 100644 index 0000000000000..f6159bb69d529 --- /dev/null +++ b/ports/unix/stack_size.h @@ -0,0 +1,54 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2025 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_UNIX_STACK_SIZE_H +#define MICROPY_INCLUDED_UNIX_STACK_SIZE_H + +#include "py/misc.h" + +// Define scaling factors for the stack size (also applies to main thread) +#ifndef UNIX_STACK_MULTIPLIER + +#if defined(__arm__) && !defined(__thumb2__) +// ARM (non-Thumb) architectures require more stack. +#define UNIX_STACK_MUL_ARM 2 +#else +#define UNIX_STACK_MUL_ARM 1 +#endif + +#if MP_SANITIZER_BUILD +// Sanitizer features consume significant stack in some cases +// This multiplier can probably be removed when using GCC 12 or newer. +#define UNIX_STACK_MUL_SANITIZERS 4 +#else +#define UNIX_STACK_MUL_SANITIZERS 1 +#endif + +// Double the stack size for 64-bit builds, plus additional scaling +#define UNIX_STACK_MULTIPLIER ((sizeof(void *) / 4) * UNIX_STACK_MUL_ARM * UNIX_STACK_MUL_SANITIZERS) + +#endif // UNIX_STACK_MULTIPLIER + +#endif // MICROPY_INCLUDED_UNIX_STACK_SIZE_H From 22deeeb8db1305b292e9146b7b00a58207755693 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Tue, 12 Aug 2025 11:03:50 +1000 Subject: [PATCH 132/143] tests/stress/recursive_iternext: Rewrite to find its own limit. Necessary on the unix port when running with sanitizers, as the newly increased stack size can run all tests at N=5000 without raising RuntimeError, and increasing N to fix this causes issues on other configurations. This way the test progressively builds a deeper data structure until it fails with RuntimeError. This is theoretically slower, but not noticeably so in reality. Signed-off-by: Angus Gratton --- tests/stress/recursive_iternext.py | 64 ++++++++++-------------------- 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/tests/stress/recursive_iternext.py b/tests/stress/recursive_iternext.py index bbc389e726237..c737f1e36d70a 100644 --- a/tests/stress/recursive_iternext.py +++ b/tests/stress/recursive_iternext.py @@ -1,4 +1,8 @@ # This tests that recursion with iternext doesn't lead to segfault. +# +# This test segfaults CPython, but that's not a bug as CPython doesn't enforce +# limits on C recursion - see +# https://github.com/python/cpython/issues/58218#issuecomment-1093570209 try: enumerate filter @@ -9,49 +13,25 @@ print("SKIP") raise SystemExit -# We need to pick an N that is large enough to hit the recursion -# limit, but not too large that we run out of heap memory. -try: - # large stack/heap, eg unix - [0] * 80000 - N = 5000 -except: - try: - # medium, eg pyboard - [0] * 10000 - N = 1000 - except: - # small, eg esp8266 - N = 100 - -try: - x = (1, 2) - for i in range(N): - x = enumerate(x) - tuple(x) -except RuntimeError: - print("RuntimeError") -try: +# Progressively build a bigger nested iterator structure (10 at a time for speed), +# and then try to evaluate it via tuple(x) which makes deep recursive function calls. +# +# Eventually this should raise a RuntimeError as MicroPython runs out of stack. +# It shouldn't ever raise a MemoryError, if it does then somehow MicroPython has +# run out of heap (for the nested structure) before running out of stack. +def recurse_iternext(nested_fn): x = (1, 2) - for i in range(N): - x = filter(None, x) - tuple(x) -except RuntimeError: - print("RuntimeError") + while True: + for _ in range(10): + x = nested_fn(x) + try: + tuple(x) + except RuntimeError: + print("RuntimeError") + break -try: - x = (1, 2) - for i in range(N): - x = map(max, x, ()) - tuple(x) -except RuntimeError: - print("RuntimeError") -try: - x = (1, 2) - for i in range(N): - x = zip(x) - tuple(x) -except RuntimeError: - print("RuntimeError") +# Test on various nested iterator structures +for nested_fn in [enumerate, lambda x: filter(None, x), lambda x: map(max, x, ()), zip]: + recurse_iternext(nested_fn) From bd413d3d853e2c1fc0382b328176ff912d79da70 Mon Sep 17 00:00:00 2001 From: Alessandro Gatti Date: Sun, 17 Aug 2025 07:28:35 +0200 Subject: [PATCH 133/143] py/asmthumb: Fix T3 encoding of conditional branches. This commit fixes the encoding of conditional branch opcodes emitted for ARMv7-M targets, when the emitter decides to use the T3 encoding for said operation. Fields J1 and J2 are now present in the generated opcode word, along with correcting some minor issues in bitmasks and shifts computation. This fixes #17940. Signed-off-by: Alessandro Gatti --- py/asmthumb.c | 5 ++--- tests/micropython/viper_large_jump.py | 20 ++++++++++++++++++++ tests/micropython/viper_large_jump.py.exp | 1 + 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 tests/micropython/viper_large_jump.py create mode 100644 tests/micropython/viper_large_jump.py.exp diff --git a/py/asmthumb.c b/py/asmthumb.c index 93860d2fdc1a2..58cc7aea88085 100644 --- a/py/asmthumb.c +++ b/py/asmthumb.c @@ -267,9 +267,8 @@ bool asm_thumb_b_n_label(asm_thumb_t *as, uint label) { #define OP_BCC_N(cond, byte_offset) (0xd000 | ((cond) << 8) | (((byte_offset) >> 1) & 0x00ff)) -// all these bit-arithmetic operations need coverage testing! -#define OP_BCC_W_HI(cond, byte_offset) (0xf000 | ((cond) << 6) | (((byte_offset) >> 10) & 0x0400) | (((byte_offset) >> 14) & 0x003f)) -#define OP_BCC_W_LO(byte_offset) (0x8000 | ((byte_offset) & 0x2000) | (((byte_offset) >> 1) & 0x0fff)) +#define OP_BCC_W_HI(cond, byte_offset) (0xf000 | ((cond) << 6) | (((byte_offset) >> 10) & 0x0400) | (((byte_offset) >> 12) & 0x003f)) +#define OP_BCC_W_LO(byte_offset) (0x8000 | (((byte_offset) >> 5) & 0x2000) | (((byte_offset) >> 8) & 0x0800) | (((byte_offset) >> 1) & 0x07ff)) bool asm_thumb_bcc_nw_label(asm_thumb_t *as, int cond, uint label, bool wide) { mp_uint_t dest = get_label_dest(as, label); diff --git a/tests/micropython/viper_large_jump.py b/tests/micropython/viper_large_jump.py new file mode 100644 index 0000000000000..1c5913dec1ea2 --- /dev/null +++ b/tests/micropython/viper_large_jump.py @@ -0,0 +1,20 @@ +COUNT = 600 + + +try: + code = """ +@micropython.viper +def f() -> int: + x = 0 + while x < 10: +""" + for i in range(COUNT): + code += " x += 1\n" + code += " return x" + exec(code) +except MemoryError: + print("SKIP-TOO-LARGE") + raise SystemExit + + +print(f()) diff --git a/tests/micropython/viper_large_jump.py.exp b/tests/micropython/viper_large_jump.py.exp new file mode 100644 index 0000000000000..e9f960cf4ac4e --- /dev/null +++ b/tests/micropython/viper_large_jump.py.exp @@ -0,0 +1 @@ +600 From 36501966821db97feaf27a6b87086bee0eb27775 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 16:52:29 +1000 Subject: [PATCH 134/143] py/objtype: Use locals_ptr directly instead of getting it from the slot. This is a very minor code simplification, which reduces code size by about -8 bytes. It should have no functional change. Signed-off-by: Damien George --- py/objtype.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/py/objtype.c b/py/objtype.c index 25c2dee994d74..d40f619fae241 100644 --- a/py/objtype.c +++ b/py/objtype.c @@ -1292,8 +1292,7 @@ static mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals mp_raise_TypeError(MP_ERROR_TEXT("multiple bases have instance lay-out conflict")); } - mp_map_t *locals_map = &MP_OBJ_TYPE_GET_SLOT(o, locals_dict)->map; - mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(MP_QSTR___new__), MP_MAP_LOOKUP); + mp_map_elem_t *elem = mp_map_lookup(&locals_ptr->map, MP_OBJ_NEW_QSTR(MP_QSTR___new__), MP_MAP_LOOKUP); if (elem != NULL) { // __new__ slot exists; check if it is a function if (mp_obj_is_fun(elem->value)) { From 169d382248fcec1b62891db6339c349477020bd2 Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 17:00:44 +1000 Subject: [PATCH 135/143] py/mpconfig: Rename MICROPY_PY___FILE__ to MICROPY_MODULE___FILE__. For consistency with other module-related configuration options. Signed-off-by: Damien George --- mpy-cross/main.c | 2 +- mpy-cross/mpconfigport.h | 2 +- ports/esp8266/mpconfigport.h | 2 +- ports/nrf/mpconfigport.h | 2 +- ports/pic16bit/mpconfigport.h | 2 +- ports/powerpc/mpconfigport.h | 2 +- ports/unix/main.c | 2 +- py/builtinimport.c | 6 +++--- py/mpconfig.h | 10 +++++----- py/objmodule.c | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/mpy-cross/main.c b/mpy-cross/main.c index 16f749ae4dc35..b7771ce6e798f 100644 --- a/mpy-cross/main.c +++ b/mpy-cross/main.c @@ -81,7 +81,7 @@ static int compile_and_save(const char *file, const char *output_file, const cha source_name = qstr_from_str(source_file); } - #if MICROPY_PY___FILE__ + #if MICROPY_MODULE___FILE__ mp_store_global(MP_QSTR___file__, MP_OBJ_NEW_QSTR(source_name)); #endif diff --git a/mpy-cross/mpconfigport.h b/mpy-cross/mpconfigport.h index 81cbfc2eeb937..0a6478b4f3d24 100644 --- a/mpy-cross/mpconfigport.h +++ b/mpy-cross/mpconfigport.h @@ -85,7 +85,7 @@ #define MICROPY_GCREGS_SETJMP (1) #endif -#define MICROPY_PY___FILE__ (0) +#define MICROPY_MODULE___FILE__ (0) #define MICROPY_PY_ARRAY (0) #define MICROPY_PY_ATTRTUPLE (0) #define MICROPY_PY_COLLECTIONS (0) diff --git a/ports/esp8266/mpconfigport.h b/ports/esp8266/mpconfigport.h index 323fba67f5b07..675751d2b4064 100644 --- a/ports/esp8266/mpconfigport.h +++ b/ports/esp8266/mpconfigport.h @@ -23,6 +23,7 @@ #define MICROPY_OPT_MATH_FACTORIAL (0) #define MICROPY_REPL_EMACS_KEYS (0) #define MICROPY_PY_BUILTINS_COMPLEX (0) +#define MICROPY_MODULE___FILE__ (0) #define MICROPY_PY_DELATTR_SETATTR (0) #define MICROPY_PY_BUILTINS_STR_CENTER (0) #define MICROPY_PY_BUILTINS_STR_PARTITION (0) @@ -32,7 +33,6 @@ #define MICROPY_PY_BUILTINS_EXECFILE (0) #define MICROPY_PY_BUILTINS_NOTIMPLEMENTED (0) #define MICROPY_PY_BUILTINS_POW3 (0) -#define MICROPY_PY___FILE__ (0) #define MICROPY_PY_MATH_CONSTANTS (0) #define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (0) #define MICROPY_PY_MATH_FACTORIAL (0) diff --git a/ports/nrf/mpconfigport.h b/ports/nrf/mpconfigport.h index 963e1e8836db7..d944fc8a11e71 100644 --- a/ports/nrf/mpconfigport.h +++ b/ports/nrf/mpconfigport.h @@ -264,6 +264,7 @@ #define MICROPY_ERROR_REPORTING (2) #define MICROPY_FULL_CHECKS (1) #define MICROPY_GC_ALLOC_THRESHOLD (1) +#define MICROPY_MODULE___FILE__ (1) #define MICROPY_MODULE_GETATTR (1) #define MICROPY_MULTIPLE_INHERITANCE (1) #define MICROPY_PY_ARRAY (1) @@ -290,7 +291,6 @@ #define MICROPY_PY_STRUCT (1) #define MICROPY_PY_SYS (1) #define MICROPY_PY_SYS_PATH_ARGV_DEFAULTS (1) -#define MICROPY_PY___FILE__ (1) #endif #ifndef MICROPY_PY_UBLUEPY diff --git a/ports/pic16bit/mpconfigport.h b/ports/pic16bit/mpconfigport.h index 7e6e1c4e02b17..e95f25aa0b689 100644 --- a/ports/pic16bit/mpconfigport.h +++ b/ports/pic16bit/mpconfigport.h @@ -43,6 +43,7 @@ #define MICROPY_ENABLE_SOURCE_LINE (0) #define MICROPY_ENABLE_DOC_STRING (0) #define MICROPY_ERROR_REPORTING (MICROPY_ERROR_REPORTING_TERSE) +#define MICROPY_MODULE___FILE__ (0) #define MICROPY_PY_ASYNC_AWAIT (0) #define MICROPY_PY_BUILTINS_BYTEARRAY (0) #define MICROPY_PY_BUILTINS_MEMORYVIEW (0) @@ -51,7 +52,6 @@ #define MICROPY_PY_BUILTINS_SLICE (0) #define MICROPY_PY_BUILTINS_PROPERTY (0) #define MICROPY_PY_MICROPYTHON_MEM_INFO (1) -#define MICROPY_PY___FILE__ (0) #define MICROPY_PY_GC (1) #define MICROPY_PY_ARRAY (0) #define MICROPY_PY_COLLECTIONS (0) diff --git a/ports/powerpc/mpconfigport.h b/ports/powerpc/mpconfigport.h index 25d85c9e61a72..091e94bdafdf2 100644 --- a/ports/powerpc/mpconfigport.h +++ b/ports/powerpc/mpconfigport.h @@ -57,6 +57,7 @@ #define MICROPY_ERROR_REPORTING (MICROPY_ERROR_REPORTING_TERSE) #define MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG (0) #define MICROPY_PY_ASYNC_AWAIT (0) +#define MICROPY_MODULE___FILE__ (0) #define MICROPY_MODULE_BUILTIN_INIT (1) #define MICROPY_PY_BUILTINS_BYTEARRAY (1) #define MICROPY_PY_BUILTINS_DICT_FROMKEYS (1) @@ -73,7 +74,6 @@ #define MICROPY_PY_BUILTINS_STR_OP_MODULO (1) #define MICROPY_PY_BUILTINS_HELP (1) #define MICROPY_PY_BUILTINS_HELP_MODULES (1) -#define MICROPY_PY___FILE__ (0) #define MICROPY_PY_GC (1) #define MICROPY_PY_ARRAY (1) #define MICROPY_PY_COLLECTIONS (1) diff --git a/ports/unix/main.c b/ports/unix/main.c index dd0788aaad96b..51d99ce5f1510 100644 --- a/ports/unix/main.c +++ b/ports/unix/main.c @@ -139,7 +139,7 @@ static int execute_from_lexer(int source_kind, const void *source, mp_parse_inpu qstr source_name = lex->source_name; - #if MICROPY_PY___FILE__ + #if MICROPY_MODULE___FILE__ if (input_kind == MP_PARSE_FILE_INPUT) { mp_store_global(MP_QSTR___file__, MP_OBJ_NEW_QSTR(source_name)); } diff --git a/py/builtinimport.c b/py/builtinimport.c index 0611926fdd52a..a2737a03e8dc2 100644 --- a/py/builtinimport.c +++ b/py/builtinimport.c @@ -153,7 +153,7 @@ static mp_import_stat_t stat_top_level(qstr mod_name, vstr_t *dest) { #if MICROPY_MODULE_FROZEN_STR || MICROPY_ENABLE_COMPILER static void do_load_from_lexer(mp_module_context_t *context, mp_lexer_t *lex) { - #if MICROPY_PY___FILE__ + #if MICROPY_MODULE___FILE__ qstr source_name = lex->source_name; mp_store_attr(MP_OBJ_FROM_PTR(&context->module), MP_QSTR___file__, MP_OBJ_NEW_QSTR(source_name)); #endif @@ -166,7 +166,7 @@ static void do_load_from_lexer(mp_module_context_t *context, mp_lexer_t *lex) { #if (MICROPY_HAS_FILE_READER && MICROPY_PERSISTENT_CODE_LOAD) || MICROPY_MODULE_FROZEN_MPY static void do_execute_proto_fun(const mp_module_context_t *context, mp_proto_fun_t proto_fun, qstr source_name) { - #if MICROPY_PY___FILE__ + #if MICROPY_MODULE___FILE__ mp_store_attr(MP_OBJ_FROM_PTR(&context->module), MP_QSTR___file__, MP_OBJ_NEW_QSTR(source_name)); #else (void)source_name; @@ -225,7 +225,7 @@ static void do_load(mp_module_context_t *module_obj, vstr_t *file) { if (frozen_type == MP_FROZEN_MPY) { const mp_frozen_module_t *frozen = modref; module_obj->constants = frozen->constants; - #if MICROPY_PY___FILE__ + #if MICROPY_MODULE___FILE__ qstr frozen_file_qstr = qstr_from_str(file_str + frozen_path_prefix_len); #else qstr frozen_file_qstr = MP_QSTRnull; diff --git a/py/mpconfig.h b/py/mpconfig.h index 5fe0e822f775c..93d65f2b0e231 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -990,6 +990,11 @@ typedef time_t mp_timestamp_t; #define MICROPY_STREAMS_POSIX_API (0) #endif +// Whether to set __file__ on imported modules. +#ifndef MICROPY_MODULE___FILE__ +#define MICROPY_MODULE___FILE__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES) +#endif + // Whether modules can use MP_REGISTER_MODULE_DELEGATION() to delegate failed // attribute lookups to a custom handler function. #ifndef MICROPY_MODULE_ATTR_DELEGATION @@ -1422,11 +1427,6 @@ typedef time_t mp_timestamp_t; #define MICROPY_PY_BUILTINS_HELP_MODULES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) #endif -// Whether to set __file__ for imported modules -#ifndef MICROPY_PY___FILE__ -#define MICROPY_PY___FILE__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES) -#endif - // Whether to process __all__ when importing all public symbols from module #ifndef MICROPY_MODULE___ALL__ #define MICROPY_MODULE___ALL__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES) diff --git a/py/objmodule.c b/py/objmodule.c index 5ce373b83d3f1..5ee2f7dc86023 100644 --- a/py/objmodule.c +++ b/py/objmodule.c @@ -44,7 +44,7 @@ static void module_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kin module_name = mp_obj_str_get_str(elem->value); } - #if MICROPY_PY___FILE__ + #if MICROPY_MODULE___FILE__ // If we store __file__ to imported modules then try to lookup this // symbol to give more information about the module. elem = mp_map_lookup(&self->globals->map, MP_OBJ_NEW_QSTR(MP_QSTR___file__), MP_MAP_LOOKUP); From 989abae12c9210a0acf966bbc71e90f285d8795c Mon Sep 17 00:00:00 2001 From: Damien George Date: Mon, 11 Aug 2025 17:02:01 +1000 Subject: [PATCH 136/143] py/mpconfig: Move MICROPY_MODULE___ALL__ option to other module options. Signed-off-by: Damien George --- py/mpconfig.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/py/mpconfig.h b/py/mpconfig.h index 93d65f2b0e231..b5483ee901199 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -990,6 +990,11 @@ typedef time_t mp_timestamp_t; #define MICROPY_STREAMS_POSIX_API (0) #endif +// Whether to process __all__ when importing all public symbols from a module. +#ifndef MICROPY_MODULE___ALL__ +#define MICROPY_MODULE___ALL__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES) +#endif + // Whether to set __file__ on imported modules. #ifndef MICROPY_MODULE___FILE__ #define MICROPY_MODULE___FILE__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES) @@ -1427,11 +1432,6 @@ typedef time_t mp_timestamp_t; #define MICROPY_PY_BUILTINS_HELP_MODULES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) #endif -// Whether to process __all__ when importing all public symbols from module -#ifndef MICROPY_MODULE___ALL__ -#define MICROPY_MODULE___ALL__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES) -#endif - // Whether to provide mem-info related functions in micropython module #ifndef MICROPY_PY_MICROPYTHON_MEM_INFO #define MICROPY_PY_MICROPYTHON_MEM_INFO (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) From b5fcb33eaa682bb666c839cd4fb301175cc3564f Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 15 Aug 2025 12:56:16 +1000 Subject: [PATCH 137/143] py/mpconfig: Enable CRYPTOLIB, HASHLIB_MD5, HASHLIB_SHA1 if SSL enabled. This commit unifies the configuration of MICROPY_PY_CRYPTOLIB, MICROPY_PY_HASHLIB_MD5 and MICROPY_PY_HASHLIB_SHA1, so they are enabled by default if MICROPY_PY_SSL is enabled. This matches the existing configuration of most of the ports. With this change, all ports remain the same except: - reneses-ra now enables MICROPY_PY_CRYPTOLIB, MICROPY_PY_HASHLIB_MD5 and MICROPY_PY_HASHLIB_SHA1. - rp2 now enables MICROPY_PY_HASHLIB_MD5. Signed-off-by: Damien George --- ports/alif/mpconfigport.h | 3 --- ports/esp32/mpconfigport.h | 4 ---- ports/esp8266/boards/ESP8266_GENERIC/mpconfigboard.h | 6 +----- ports/esp8266/mpconfigport.h | 1 + ports/mimxrt/mpconfigport.h | 3 --- .../rp2/boards/ARDUINO_NANO_RP2040_CONNECT/mpconfigboard.h | 3 --- ports/rp2/mpconfigport.h | 2 -- ports/stm32/mpconfigport.h | 3 --- ports/unix/variants/mpconfigvariant_common.h | 6 ------ py/mpconfig.h | 6 +++--- 10 files changed, 5 insertions(+), 32 deletions(-) diff --git a/ports/alif/mpconfigport.h b/ports/alif/mpconfigport.h index 08b27e2786e75..6b30ea2e6245a 100644 --- a/ports/alif/mpconfigport.h +++ b/ports/alif/mpconfigport.h @@ -112,9 +112,6 @@ // Extended modules #define MICROPY_EPOCH_IS_1970 (1) -#define MICROPY_PY_CRYPTOLIB (MICROPY_PY_SSL) -#define MICROPY_PY_HASHLIB_MD5 (MICROPY_PY_SSL) -#define MICROPY_PY_HASHLIB_SHA1 (MICROPY_PY_SSL) #define MICROPY_PY_OS_INCLUDEFILE "ports/alif/modos.c" #define MICROPY_PY_OS_DUPTERM (1) #define MICROPY_PY_OS_SEP (1) diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index e47b333c73698..9b12dbd34a736 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -105,10 +105,6 @@ #define MICROPY_BLUETOOTH_NIMBLE (1) #define MICROPY_BLUETOOTH_NIMBLE_BINDINGS_ONLY (1) #endif -#define MICROPY_PY_HASHLIB_MD5 (1) -#define MICROPY_PY_HASHLIB_SHA1 (1) -#define MICROPY_PY_HASHLIB_SHA256 (1) -#define MICROPY_PY_CRYPTOLIB (1) #define MICROPY_PY_RANDOM_SEED_INIT_FUNC (esp_random()) #define MICROPY_PY_OS_INCLUDEFILE "ports/esp32/modos.c" #define MICROPY_PY_OS_DUPTERM (1) diff --git a/ports/esp8266/boards/ESP8266_GENERIC/mpconfigboard.h b/ports/esp8266/boards/ESP8266_GENERIC/mpconfigboard.h index 1f679961e8786..cea2267c7ccd5 100644 --- a/ports/esp8266/boards/ESP8266_GENERIC/mpconfigboard.h +++ b/ports/esp8266/boards/ESP8266_GENERIC/mpconfigboard.h @@ -12,8 +12,6 @@ #define MICROPY_READER_VFS (MICROPY_VFS) #define MICROPY_VFS (1) -#define MICROPY_PY_CRYPTOLIB (1) - #elif defined(MICROPY_ESP8266_1M) #define MICROPY_HW_BOARD_NAME "ESP module (1M)" @@ -28,9 +26,6 @@ #define MICROPY_READER_VFS (MICROPY_VFS) #define MICROPY_VFS (1) - -#define MICROPY_PY_CRYPTOLIB (1) - #elif defined(MICROPY_ESP8266_512K) #define MICROPY_HW_BOARD_NAME "ESP module (512K)" @@ -45,6 +40,7 @@ #define MICROPY_PY_SYS_STDIO_BUFFER (0) #define MICROPY_PY_ASYNCIO (0) #define MICROPY_PY_RE_SUB (0) +#define MICROPY_PY_CRYPTOLIB (0) #define MICROPY_PY_FRAMEBUF (0) #endif diff --git a/ports/esp8266/mpconfigport.h b/ports/esp8266/mpconfigport.h index 675751d2b4064..0321de45d7f22 100644 --- a/ports/esp8266/mpconfigport.h +++ b/ports/esp8266/mpconfigport.h @@ -56,6 +56,7 @@ #define MICROPY_REPL_EVENT_DRIVEN (0) #define MICROPY_USE_INTERNAL_ERRNO (1) #define MICROPY_PY_BUILTINS_HELP_TEXT esp_help_text +#define MICROPY_PY_HASHLIB_MD5 (0) #define MICROPY_PY_HASHLIB_SHA1 (MICROPY_PY_SSL && MICROPY_SSL_AXTLS) #define MICROPY_PY_RANDOM_SEED_INIT_FUNC (*WDEV_HWRNG) #define MICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIME (1) diff --git a/ports/mimxrt/mpconfigport.h b/ports/mimxrt/mpconfigport.h index 3cf6550d7b07b..d6694badbad95 100644 --- a/ports/mimxrt/mpconfigport.h +++ b/ports/mimxrt/mpconfigport.h @@ -145,9 +145,6 @@ uint32_t trng_random_u32(void); #define MICROPY_PY_WEBSOCKET (MICROPY_PY_LWIP) #define MICROPY_PY_WEBREPL (MICROPY_PY_LWIP) #define MICROPY_PY_LWIP_SOCK_RAW (MICROPY_PY_LWIP) -#define MICROPY_PY_HASHLIB_MD5 (MICROPY_PY_SSL) -#define MICROPY_PY_HASHLIB_SHA1 (MICROPY_PY_SSL) -#define MICROPY_PY_CRYPTOLIB (MICROPY_PY_SSL) #ifndef MICROPY_PY_NETWORK_PPP_LWIP #define MICROPY_PY_NETWORK_PPP_LWIP (MICROPY_PY_LWIP) #endif diff --git a/ports/rp2/boards/ARDUINO_NANO_RP2040_CONNECT/mpconfigboard.h b/ports/rp2/boards/ARDUINO_NANO_RP2040_CONNECT/mpconfigboard.h index 7783c0a17c64d..11aa663296f82 100644 --- a/ports/rp2/boards/ARDUINO_NANO_RP2040_CONNECT/mpconfigboard.h +++ b/ports/rp2/boards/ARDUINO_NANO_RP2040_CONNECT/mpconfigboard.h @@ -10,9 +10,6 @@ // Enable networking. #define MICROPY_PY_NETWORK (1) -// Enable MD5 hash. -#define MICROPY_PY_HASHLIB_MD5 (1) - // Disable internal error numbers. #define MICROPY_USE_INTERNAL_ERRNO (0) diff --git a/ports/rp2/mpconfigport.h b/ports/rp2/mpconfigport.h index c312293ace6e9..0c226538cda1a 100644 --- a/ports/rp2/mpconfigport.h +++ b/ports/rp2/mpconfigport.h @@ -150,8 +150,6 @@ #define MICROPY_PY_OS_URANDOM (1) #define MICROPY_PY_RE_MATCH_GROUPS (1) #define MICROPY_PY_RE_MATCH_SPAN_START_END (1) -#define MICROPY_PY_HASHLIB_SHA1 (1) -#define MICROPY_PY_CRYPTOLIB (1) #define MICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIME (1) #define MICROPY_PY_TIME_TIME_TIME_NS (1) #define MICROPY_PY_TIME_INCLUDEFILE "ports/rp2/modtime.c" diff --git a/ports/stm32/mpconfigport.h b/ports/stm32/mpconfigport.h index 191503cd4956f..fac261f7e2480 100644 --- a/ports/stm32/mpconfigport.h +++ b/ports/stm32/mpconfigport.h @@ -97,9 +97,6 @@ #endif // extended modules -#define MICROPY_PY_HASHLIB_MD5 (MICROPY_PY_SSL) -#define MICROPY_PY_HASHLIB_SHA1 (MICROPY_PY_SSL) -#define MICROPY_PY_CRYPTOLIB (MICROPY_PY_SSL) #define MICROPY_PY_OS_INCLUDEFILE "ports/stm32/modos.c" #define MICROPY_PY_OS_DUPTERM (3) #define MICROPY_PY_OS_DUPTERM_BUILTIN_STREAM (1) diff --git a/ports/unix/variants/mpconfigvariant_common.h b/ports/unix/variants/mpconfigvariant_common.h index 65c874317666a..1ac59c95572dd 100644 --- a/ports/unix/variants/mpconfigvariant_common.h +++ b/ports/unix/variants/mpconfigvariant_common.h @@ -104,12 +104,6 @@ #define MICROPY_PY_TIME_CUSTOM_SLEEP (1) #define MICROPY_PY_TIME_INCLUDEFILE "ports/unix/modtime.c" -#if MICROPY_PY_SSL -#define MICROPY_PY_HASHLIB_MD5 (1) -#define MICROPY_PY_HASHLIB_SHA1 (1) -#define MICROPY_PY_CRYPTOLIB (1) -#endif - // The "select" module is enabled by default, but disable select.select(). #define MICROPY_PY_SELECT_POSIX_OPTIMISATIONS (1) #define MICROPY_PY_SELECT_SELECT (0) diff --git a/py/mpconfig.h b/py/mpconfig.h index b5483ee901199..877b262c8b7ae 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -1860,11 +1860,11 @@ typedef time_t mp_timestamp_t; #endif #ifndef MICROPY_PY_HASHLIB_MD5 -#define MICROPY_PY_HASHLIB_MD5 (0) +#define MICROPY_PY_HASHLIB_MD5 (MICROPY_PY_SSL) #endif #ifndef MICROPY_PY_HASHLIB_SHA1 -#define MICROPY_PY_HASHLIB_SHA1 (0) +#define MICROPY_PY_HASHLIB_SHA1 (MICROPY_PY_SSL) #endif #ifndef MICROPY_PY_HASHLIB_SHA256 @@ -1872,7 +1872,7 @@ typedef time_t mp_timestamp_t; #endif #ifndef MICROPY_PY_CRYPTOLIB -#define MICROPY_PY_CRYPTOLIB (0) +#define MICROPY_PY_CRYPTOLIB (MICROPY_PY_SSL) #endif // Depends on MICROPY_PY_CRYPTOLIB From 2fc1eee0d65a7c117fb4c94fde4bccfeae8aadaa Mon Sep 17 00:00:00 2001 From: Vincent1-python Date: Fri, 22 Aug 2025 11:54:09 +0800 Subject: [PATCH 138/143] esp32: Update machine_i2c.c. Signed-off-by: Vincent1-python esp32: fix machine_i2c.c format. Signed-off-by: Vincent1-python esp32: Update machine_i2c.c. Signed-off-by: Vincent1-python --- ports/esp32/machine_i2c.c | 180 +++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 63 deletions(-) diff --git a/ports/esp32/machine_i2c.c b/ports/esp32/machine_i2c.c index a9e5f0d3b32cb..b28c73c55e527 100644 --- a/ports/esp32/machine_i2c.c +++ b/ports/esp32/machine_i2c.c @@ -30,7 +30,7 @@ #include "extmod/modmachine.h" #include "machine_i2c.h" -#include "driver/i2c.h" +#include "driver/i2c_master.h" #include "hal/i2c_ll.h" #if MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_SOFTI2C @@ -49,92 +49,159 @@ #define I2C_DEFAULT_TIMEOUT_US (50000) // 50ms +// ---------------- Internal data structures ---------------- typedef struct _machine_hw_i2c_obj_t { mp_obj_base_t base; - i2c_port_t port : 8; + i2c_master_bus_handle_t bus_handle; + i2c_master_dev_handle_t dev_handle; + uint8_t port : 8; gpio_num_t scl : 8; gpio_num_t sda : 8; } machine_hw_i2c_obj_t; static machine_hw_i2c_obj_t machine_hw_i2c_obj[I2C_NUM_MAX]; +// ---------------- Initialization ---------------- static void machine_hw_i2c_init(machine_hw_i2c_obj_t *self, uint32_t freq, uint32_t timeout_us, bool first_init) { - if (!first_init) { - i2c_driver_delete(self->port); + + // 1. If already initialized, uninstall the old driver first + if (!first_init && self->bus_handle) { + i2c_master_bus_rm_device(self->dev_handle); + i2c_del_master_bus(self->bus_handle); + self->bus_handle = NULL; + self->dev_handle = NULL; } - i2c_config_t conf = { - .mode = I2C_MODE_MASTER, - .sda_io_num = self->sda, - .sda_pullup_en = GPIO_PULLUP_ENABLE, + + // 2. Configure the bus + i2c_master_bus_config_t bus_cfg = { + .i2c_port = self->port, .scl_io_num = self->scl, - .scl_pullup_en = GPIO_PULLUP_ENABLE, - .master.clk_speed = freq, + .sda_io_num = self->sda, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, }; - i2c_param_config(self->port, &conf); - int timeout = I2C_SCLK_FREQ / 1000000 * timeout_us; - i2c_set_timeout(self->port, (timeout > I2C_LL_MAX_TIMEOUT) ? I2C_LL_MAX_TIMEOUT : timeout); - i2c_driver_install(self->port, I2C_MODE_MASTER, 0, 0, 0); + ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &self->bus_handle)); + + // 3. Add a device (placeholder address; will be changed dynamically later) + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = 0x00, // Placeholder + .scl_speed_hz = freq, + }; + ESP_ERROR_CHECK(i2c_master_bus_add_device(self->bus_handle, &dev_cfg, &self->dev_handle)); } int machine_hw_i2c_transfer(mp_obj_base_t *self_in, uint16_t addr, size_t n, mp_machine_i2c_buf_t *bufs, unsigned int flags) { machine_hw_i2c_obj_t *self = MP_OBJ_TO_PTR(self_in); - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + /* 0. Probe the address to see if any device responds */ + esp_err_t err = i2c_master_probe(self->bus_handle, addr, 1000); + if (err != ESP_OK) { + return -MP_ENODEV; /* No device at address, return immediately */ + } + /* 1. Create a temporary device handle for this transaction */ + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = 100000, /* Use bus frequency */ + }; + i2c_master_dev_handle_t dev_handle; + err = i2c_master_bus_add_device(self->bus_handle, &dev_cfg, &dev_handle); + if (err != ESP_OK) { + return -MP_ENODEV; + } + int data_len = 0; + /* 2. If WRITE1 segment exists, perform the write first */ if (flags & MP_MACHINE_I2C_FLAG_WRITE1) { - i2c_master_start(cmd); - i2c_master_write_byte(cmd, addr << 1, true); - i2c_master_write(cmd, bufs->buf, bufs->len, true); + if (bufs->len) { + err = i2c_master_transmit(dev_handle, bufs->buf, bufs->len, 1000); /* Block with 1 s timeout */ + if (err != ESP_OK) { + goto cleanup; + } + } data_len += bufs->len; --n; ++bufs; } + if (flags & MP_MACHINE_I2C_FLAG_READ) { + /* 3. Main loop: remaining segments */ + for (; n--; ++bufs) { + if (bufs->len == 0) { + continue; + } + err = i2c_master_receive(dev_handle, bufs->buf, bufs->len, 1000); + if (err != ESP_OK) { + break; + } + + data_len += bufs->len; + } + } else { + // Write operation logic + size_t total_len = 0; + mp_machine_i2c_buf_t *original_bufs = bufs; // Save original pointer + size_t yuann = n; - i2c_master_start(cmd); - i2c_master_write_byte(cmd, addr << 1 | (flags & MP_MACHINE_I2C_FLAG_READ), true); + // Calculate total length + for (; n--; ++bufs) { + total_len += bufs->len; + } - for (; n--; ++bufs) { - if (flags & MP_MACHINE_I2C_FLAG_READ) { - i2c_master_read(cmd, bufs->buf, bufs->len, n == 0 ? I2C_MASTER_LAST_NACK : I2C_MASTER_ACK); - } else { - if (bufs->len != 0) { - i2c_master_write(cmd, bufs->buf, bufs->len, true); - } + // Reset pointer + bufs = original_bufs; + // Reset n + n = yuann; + // Dynamically allocate write_buf + uint8_t *write_buf = (uint8_t *)malloc(total_len); + if (write_buf == NULL) { + return -MP_ENOMEM; + } + + // Copy data into write_buf + size_t index = 0; + for (; n--; ++bufs) { + memcpy(write_buf + index, bufs->buf, bufs->len); + index += bufs->len; + } + + // Transmit data + err = i2c_master_transmit(dev_handle, write_buf, total_len, 1000); + if (err != ESP_OK) { + goto cleanup; } - data_len += bufs->len; - } - if (flags & MP_MACHINE_I2C_FLAG_STOP) { - i2c_master_stop(cmd); + // Free dynamically allocated memory + free(write_buf); } - // TODO proper timeout - esp_err_t err = i2c_master_cmd_begin(self->port, cmd, 100 * (1 + data_len) / portTICK_PERIOD_MS); - i2c_cmd_link_delete(cmd); +cleanup: + /* 4. Immediately destroy the temporary handle */ + i2c_master_bus_rm_device(dev_handle); + /* 5. Map errors */ if (err == ESP_FAIL) { return -MP_ENODEV; - } else if (err == ESP_ERR_TIMEOUT) { + } + if (err == ESP_ERR_TIMEOUT) { return -MP_ETIMEDOUT; - } else if (err != ESP_OK) { + } + if (err != ESP_OK) { return -abs(err); } return data_len; } -/******************************************************************************/ -// MicroPython bindings for machine API - +// ---------------- Print ---------------- static void machine_hw_i2c_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { machine_hw_i2c_obj_t *self = MP_OBJ_TO_PTR(self_in); - int h, l; - i2c_get_period(self->port, &h, &l); - mp_printf(print, "I2C(%u, scl=%u, sda=%u, freq=%u)", - self->port, self->scl, self->sda, I2C_SCLK_FREQ / (h + l)); + mp_printf(print, "I2C(%u, scl=%u, sda=%u)", self->port, self->scl, self->sda); } +// ---------------- Constructor ---------------- mp_obj_t machine_hw_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { // Create a SoftI2C instance if no id is specified (or is -1) but other arguments are given if (n_args != 0) { @@ -153,33 +220,21 @@ mp_obj_t machine_hw_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_ mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); - // Get I2C bus mp_int_t i2c_id = args[ARG_id].u_int; - - // Check if the I2C bus is valid if (!(I2C_NUM_0 <= i2c_id && i2c_id < I2C_NUM_MAX)) { mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("I2C(%d) doesn't exist"), i2c_id); } - // Get static peripheral object - machine_hw_i2c_obj_t *self = (machine_hw_i2c_obj_t *)&machine_hw_i2c_obj[i2c_id]; + machine_hw_i2c_obj_t *self = &machine_hw_i2c_obj[i2c_id]; - bool first_init = false; - if (self->base.type == NULL) { - // Created for the first time, set default pins + bool first_init = (self->base.type == NULL); + if (first_init) { self->base.type = &machine_i2c_type; self->port = i2c_id; - if (self->port == I2C_NUM_0) { - self->scl = MICROPY_HW_I2C0_SCL; - self->sda = MICROPY_HW_I2C0_SDA; - } else { - self->scl = MICROPY_HW_I2C1_SCL; - self->sda = MICROPY_HW_I2C1_SDA; - } - first_init = true; + self->scl = (i2c_id == I2C_NUM_0) ? MICROPY_HW_I2C0_SCL : MICROPY_HW_I2C1_SCL; + self->sda = (i2c_id == I2C_NUM_0) ? MICROPY_HW_I2C0_SDA : MICROPY_HW_I2C1_SDA; } - // Set SCL/SDA pins if given if (args[ARG_scl].u_obj != MP_OBJ_NULL) { self->scl = machine_pin_get_id(args[ARG_scl].u_obj); } @@ -187,12 +242,11 @@ mp_obj_t machine_hw_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_ self->sda = machine_pin_get_id(args[ARG_sda].u_obj); } - // Initialise the I2C peripheral machine_hw_i2c_init(self, args[ARG_freq].u_int, args[ARG_timeout].u_int, first_init); - return MP_OBJ_FROM_PTR(self); } +// ---------------- Protocol table ---------------- static const mp_machine_i2c_p_t machine_hw_i2c_p = { .transfer_supports_write1 = true, .transfer = machine_hw_i2c_transfer, @@ -208,4 +262,4 @@ MP_DEFINE_CONST_OBJ_TYPE( locals_dict, &mp_machine_i2c_locals_dict ); -#endif +#endif // MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_SOFTI2C From 84fd0e9b2a7f9df807b9cdea6e9d4567993a2f77 Mon Sep 17 00:00:00 2001 From: Vincent1-python Date: Sun, 24 Aug 2025 12:28:59 +0800 Subject: [PATCH 139/143] Update machine_i2c.c Signed-off-by: Vincent1-python --- ports/esp32/machine_i2c.c | 177 +++++++++++--------------------------- 1 file changed, 51 insertions(+), 126 deletions(-) diff --git a/ports/esp32/machine_i2c.c b/ports/esp32/machine_i2c.c index b28c73c55e527..51b414160f878 100644 --- a/ports/esp32/machine_i2c.c +++ b/ports/esp32/machine_i2c.c @@ -4,6 +4,7 @@ * The MIT License (MIT) * * Copyright (c) 2019 Damien P. George + * Copyright (c) 2025 Vincent1-python * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -35,44 +36,27 @@ #if MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_SOFTI2C -#if SOC_I2C_SUPPORT_XTAL -#if CONFIG_XTAL_FREQ > 0 -#define I2C_SCLK_FREQ (CONFIG_XTAL_FREQ * 1000000) -#else -#error "I2C uses XTAL but no configured freq" -#endif // CONFIG_XTAL_FREQ -#elif SOC_I2C_SUPPORT_APB -#define I2C_SCLK_FREQ APB_CLK_FREQ -#else -#error "unsupported I2C for ESP32 SoC variant" -#endif - #define I2C_DEFAULT_TIMEOUT_US (50000) // 50ms -// ---------------- Internal data structures ---------------- typedef struct _machine_hw_i2c_obj_t { mp_obj_base_t base; i2c_master_bus_handle_t bus_handle; - i2c_master_dev_handle_t dev_handle; uint8_t port : 8; gpio_num_t scl : 8; gpio_num_t sda : 8; + uint32_t freq; + uint32_t timeout_us; } machine_hw_i2c_obj_t; static machine_hw_i2c_obj_t machine_hw_i2c_obj[I2C_NUM_MAX]; -// ---------------- Initialization ---------------- -static void machine_hw_i2c_init(machine_hw_i2c_obj_t *self, uint32_t freq, uint32_t timeout_us, bool first_init) { +static void machine_hw_i2c_init(machine_hw_i2c_obj_t *self, bool first_init) { - // 1. If already initialized, uninstall the old driver first if (!first_init && self->bus_handle) { - i2c_master_bus_rm_device(self->dev_handle); i2c_del_master_bus(self->bus_handle); self->bus_handle = NULL; - self->dev_handle = NULL; } - // 2. Configure the bus i2c_master_bus_config_t bus_cfg = { .i2c_port = self->port, .scl_io_num = self->scl, @@ -82,126 +66,60 @@ static void machine_hw_i2c_init(machine_hw_i2c_obj_t *self, uint32_t freq, uint3 .flags.enable_internal_pullup = true, }; ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &self->bus_handle)); - - // 3. Add a device (placeholder address; will be changed dynamically later) - i2c_device_config_t dev_cfg = { - .dev_addr_length = I2C_ADDR_BIT_LEN_7, - .device_address = 0x00, // Placeholder - .scl_speed_hz = freq, - }; - ESP_ERROR_CHECK(i2c_master_bus_add_device(self->bus_handle, &dev_cfg, &self->dev_handle)); } -int machine_hw_i2c_transfer(mp_obj_base_t *self_in, uint16_t addr, size_t n, mp_machine_i2c_buf_t *bufs, unsigned int flags) { +static int machine_i2c_transfer_single(mp_obj_base_t *self_in, uint16_t addr, size_t len, uint8_t *buf, unsigned int flags) { machine_hw_i2c_obj_t *self = MP_OBJ_TO_PTR(self_in); - /* 0. Probe the address to see if any device responds */ - esp_err_t err = i2c_master_probe(self->bus_handle, addr, 1000); + // 1. Probe the address to see if any device responds. + // This test uses a fixed scl freq of 100_000. + esp_err_t err = i2c_master_probe(self->bus_handle, addr, self->timeout_us / 1000); if (err != ESP_OK) { - return -MP_ENODEV; /* No device at address, return immediately */ + return -MP_ENODEV; // No device at address, return immediately } - /* 1. Create a temporary device handle for this transaction */ - i2c_device_config_t dev_cfg = { - .dev_addr_length = I2C_ADDR_BIT_LEN_7, - .device_address = addr, - .scl_speed_hz = 100000, /* Use bus frequency */ - }; - i2c_master_dev_handle_t dev_handle; - err = i2c_master_bus_add_device(self->bus_handle, &dev_cfg, &dev_handle); - if (err != ESP_OK) { - return -MP_ENODEV; - } - - int data_len = 0; - /* 2. If WRITE1 segment exists, perform the write first */ - if (flags & MP_MACHINE_I2C_FLAG_WRITE1) { - if (bufs->len) { - err = i2c_master_transmit(dev_handle, bufs->buf, bufs->len, 1000); /* Block with 1 s timeout */ - if (err != ESP_OK) { - goto cleanup; - } + if (len > 0) { + // 2. Create a temporary device handle for this transaction + // This step for every transaction can be omitted in + // esp idf v5.5+, which supports handle address changing. + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = self->freq, + }; + i2c_master_dev_handle_t dev_handle; + err = i2c_master_bus_add_device(self->bus_handle, &dev_cfg, &dev_handle); + if (err != ESP_OK) { + return -MP_ENODEV; } - data_len += bufs->len; - --n; - ++bufs; - } - if (flags & MP_MACHINE_I2C_FLAG_READ) { - /* 3. Main loop: remaining segments */ - for (; n--; ++bufs) { - if (bufs->len == 0) { - continue; - } - err = i2c_master_receive(dev_handle, bufs->buf, bufs->len, 1000); - if (err != ESP_OK) { - break; - } - - data_len += bufs->len; + // 3. Transfer data + if (flags & MP_MACHINE_I2C_FLAG_READ) { + err = i2c_master_receive(dev_handle, buf, len, self->timeout_us / 1000); + } else if (len > 0) { + err = i2c_master_transmit(dev_handle, buf, len, self->timeout_us / 1000); } - } else { - // Write operation logic - size_t total_len = 0; - mp_machine_i2c_buf_t *original_bufs = bufs; // Save original pointer - size_t yuann = n; - - // Calculate total length - for (; n--; ++bufs) { - total_len += bufs->len; + // 4. Destroy the temporary handle + i2c_master_bus_rm_device(dev_handle); + // 5. Map errors + if (err == ESP_FAIL) { + return -MP_ENODEV; } - - // Reset pointer - bufs = original_bufs; - // Reset n - n = yuann; - // Dynamically allocate write_buf - uint8_t *write_buf = (uint8_t *)malloc(total_len); - if (write_buf == NULL) { - return -MP_ENOMEM; + if (err == ESP_ERR_TIMEOUT) { + return -MP_ETIMEDOUT; } - - // Copy data into write_buf - size_t index = 0; - for (; n--; ++bufs) { - memcpy(write_buf + index, bufs->buf, bufs->len); - index += bufs->len; - } - - // Transmit data - err = i2c_master_transmit(dev_handle, write_buf, total_len, 1000); if (err != ESP_OK) { - goto cleanup; + return -abs(err); } - - // Free dynamically allocated memory - free(write_buf); - } - -cleanup: - /* 4. Immediately destroy the temporary handle */ - i2c_master_bus_rm_device(dev_handle); - - /* 5. Map errors */ - if (err == ESP_FAIL) { - return -MP_ENODEV; } - if (err == ESP_ERR_TIMEOUT) { - return -MP_ETIMEDOUT; - } - if (err != ESP_OK) { - return -abs(err); - } - - return data_len; + return len; } -// ---------------- Print ---------------- static void machine_hw_i2c_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { machine_hw_i2c_obj_t *self = MP_OBJ_TO_PTR(self_in); - mp_printf(print, "I2C(%u, scl=%u, sda=%u)", self->port, self->scl, self->sda); + mp_printf(print, "I2C(%u, scl=%u, sda=%u, freq=%u, timeout=%u)", + self->port, self->scl, self->sda, self->freq, self->timeout_us); } -// ---------------- Constructor ---------------- mp_obj_t machine_hw_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { // Create a SoftI2C instance if no id is specified (or is -1) but other arguments are given if (n_args != 0) { @@ -214,8 +132,8 @@ mp_obj_t machine_hw_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_ { MP_QSTR_id, MP_ARG_INT, {.u_int = I2C_NUM_0} }, { MP_QSTR_scl, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, { MP_QSTR_sda, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} }, - { MP_QSTR_freq, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 400000} }, - { MP_QSTR_timeout, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = I2C_DEFAULT_TIMEOUT_US} }, + { MP_QSTR_freq, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_timeout, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); @@ -233,6 +151,8 @@ mp_obj_t machine_hw_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_ self->port = i2c_id; self->scl = (i2c_id == I2C_NUM_0) ? MICROPY_HW_I2C0_SCL : MICROPY_HW_I2C1_SCL; self->sda = (i2c_id == I2C_NUM_0) ? MICROPY_HW_I2C0_SDA : MICROPY_HW_I2C1_SDA; + self->freq = 400000; + self->timeout_us = I2C_DEFAULT_TIMEOUT_US; } if (args[ARG_scl].u_obj != MP_OBJ_NULL) { @@ -241,15 +161,20 @@ mp_obj_t machine_hw_i2c_make_new(const mp_obj_type_t *type, size_t n_args, size_ if (args[ARG_sda].u_obj != MP_OBJ_NULL) { self->sda = machine_pin_get_id(args[ARG_sda].u_obj); } + if (args[ARG_freq].u_int != -1) { + self->freq = args[ARG_freq].u_int; + } + if (args[ARG_timeout].u_int != -1) { + self->timeout_us = args[ARG_timeout].u_int; + } - machine_hw_i2c_init(self, args[ARG_freq].u_int, args[ARG_timeout].u_int, first_init); + machine_hw_i2c_init(self, first_init); return MP_OBJ_FROM_PTR(self); } -// ---------------- Protocol table ---------------- static const mp_machine_i2c_p_t machine_hw_i2c_p = { - .transfer_supports_write1 = true, - .transfer = machine_hw_i2c_transfer, + .transfer = mp_machine_i2c_transfer_adaptor, + .transfer_single = machine_i2c_transfer_single, }; MP_DEFINE_CONST_OBJ_TYPE( From a7f69abd0f90ec1eddd66a1c5f7b3e4f2e9e75b2 Mon Sep 17 00:00:00 2001 From: Vincent1-python Date: Mon, 25 Aug 2025 13:57:07 +0800 Subject: [PATCH 140/143] esp32: Update sdkconfig.base. Signed-off-by: Vincent1-python --- ports/esp32/boards/sdkconfig.base | 1 - 1 file changed, 1 deletion(-) diff --git a/ports/esp32/boards/sdkconfig.base b/ports/esp32/boards/sdkconfig.base index 78b09ec6b24b0..a045a45b4283a 100644 --- a/ports/esp32/boards/sdkconfig.base +++ b/ports/esp32/boards/sdkconfig.base @@ -143,5 +143,4 @@ CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT=n CONFIG_LWIP_MAX_ACTIVE_TCP=12 # Enable new I2C slave API, and disable conflict check. -CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK=y CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y From 9d0273273d5d13af433f8abb994df3cd8dd10159 Mon Sep 17 00:00:00 2001 From: Vincent1-python Date: Mon, 25 Aug 2025 17:15:55 +0800 Subject: [PATCH 141/143] esp32: Update machine_i2c.c. Signed-off-by: Vincent1-python --- ports/esp32/machine_i2c.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/esp32/machine_i2c.c b/ports/esp32/machine_i2c.c index 51b414160f878..89d2cf502a5a8 100644 --- a/ports/esp32/machine_i2c.c +++ b/ports/esp32/machine_i2c.c @@ -95,7 +95,7 @@ static int machine_i2c_transfer_single(mp_obj_base_t *self_in, uint16_t addr, si // 3. Transfer data if (flags & MP_MACHINE_I2C_FLAG_READ) { err = i2c_master_receive(dev_handle, buf, len, self->timeout_us / 1000); - } else if (len > 0) { + } else { err = i2c_master_transmit(dev_handle, buf, len, self->timeout_us / 1000); } // 4. Destroy the temporary handle From 236458cf7cae503809a5c80f22b0ca77794de0e2 Mon Sep 17 00:00:00 2001 From: Vincent1-python Date: Mon, 25 Aug 2025 18:29:42 +0800 Subject: [PATCH 142/143] esp32: Update machine_i2c.c. Signed-off-by: Vincent1-python --- ports/esp32/machine_i2c.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/esp32/machine_i2c.c b/ports/esp32/machine_i2c.c index 89d2cf502a5a8..6a53f7ca4509d 100644 --- a/ports/esp32/machine_i2c.c +++ b/ports/esp32/machine_i2c.c @@ -185,6 +185,6 @@ MP_DEFINE_CONST_OBJ_TYPE( print, machine_hw_i2c_print, protocol, &machine_hw_i2c_p, locals_dict, &mp_machine_i2c_locals_dict - ); +); #endif // MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_SOFTI2C From 32eb1f4849dc774d00da97ec647bf75eafbdd111 Mon Sep 17 00:00:00 2001 From: Vincent1-python Date: Mon, 25 Aug 2025 18:31:03 +0800 Subject: [PATCH 143/143] esp32: Update machine_i2c.c. Signed-off-by: Vincent1-python --- ports/esp32/machine_i2c.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/esp32/machine_i2c.c b/ports/esp32/machine_i2c.c index 6a53f7ca4509d..89d2cf502a5a8 100644 --- a/ports/esp32/machine_i2c.c +++ b/ports/esp32/machine_i2c.c @@ -185,6 +185,6 @@ MP_DEFINE_CONST_OBJ_TYPE( print, machine_hw_i2c_print, protocol, &machine_hw_i2c_p, locals_dict, &mp_machine_i2c_locals_dict -); + ); #endif // MICROPY_PY_MACHINE_I2C || MICROPY_PY_MACHINE_SOFTI2C