Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ jobs:
- name: Run all tests
run: make test

- name: Run all tests (small CRC table)
run: make clean && make test UR_CRC32_SLICE_BY_8=0
- name: Run all tests (slice-by-8 CRC)
run: make clean && make test UR_CRC32_SLICE_BY_8=1

esp-idf:
name: ESP-IDF ${{ matrix.idf }} build (${{ matrix.target }})
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ if(ESP_PLATFORM)
else()
# Plain host build (e.g. added via add_subdirectory from a simulator or
# test harness). Uses the bundled SHA-256 so it has no dependencies.
option(UR_CRC32_SLICE_BY_8 "CRC32: use slice-by-8 (faster, +8 KB flash)" ON)
option(UR_CRC32_SLICE_BY_8 "CRC32: use slice-by-8 (faster, +8 KB flash)" OFF)
add_library(ur STATIC ${UR_SRCS} "src/sha256/sha256.c")
set_target_properties(ur PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(ur PUBLIC "src")
Expand Down
24 changes: 14 additions & 10 deletions Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@ menu "cUR (Uniform Resources)"

config UR_CRC32_SLICE_BY_8
bool "CRC32: use slice-by-8 (faster, +8 KB flash)"
default y
default n
help
Use the slice-by-8 CRC32 implementation instead of the small
nibble table. About 2.7x faster on ESP32-P4, at the cost of 8 KB
of flash for a const lookup table.
Use the slice-by-8 CRC32 implementation instead of the default
64-byte nibble table. About 2.7x faster on ESP32-P4, at the cost
of 8 KB of flash for a const lookup table. UR CRCs run over
individual fragments (typically a few hundred bytes per QR
frame), so the small table is rarely a bottleneck in practice.

config UR_XOR_ESP32P4_SIMD
bool "Fountain XOR: use ESP32-P4 PIE 128-bit SIMD"
depends on IDF_TARGET_ESP32P4
default n
default y
help
Use the ESP32-P4 PIE (xesppie) 128-bit vector unit for the
fountain-code XOR reduction. About 16x faster than a byte loop
(and ~4x faster than the portable word-wise path) on aligned
runs; unaligned data transparently falls back to the word-wise
path. When enabled, cUR 16-byte-aligns its buffer allocations
so the vector path engages on them. ESP32-P4 only.
fountain-code XOR reduction. About 4x faster than the portable
word-wise path on aligned runs; unaligned data transparently
falls back to the word-wise path. When enabled, cUR
16-byte-aligns its buffer allocations so the vector path
engages on them. Only visible on ESP32-P4; enabled by default
there since it costs no RAM and little flash — disable it to
keep the library free of target-specific code paths.

config UR_ALLOC_PSRAM
bool "Allocate working buffers in PSRAM"
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ LDFLAGS =
INCLUDES = -Isrc
SRCDIR = src
OBJDIR = src/obj
UR_CRC32_SLICE_BY_8 ?= 1
UR_CRC32_SLICE_BY_8 ?= 0

# DEBUG=1 switches to -O0 with AddressSanitizer + UndefinedBehaviorSanitizer.
# Requires a full rebuild when toggling (sanitized and non-sanitized objects
Expand All @@ -22,7 +22,7 @@ else
CFLAGS += -O2
endif

# Defaults to the fast CRC path; set UR_CRC32_SLICE_BY_8=0 for the small table.
# Defaults to the small CRC table; set UR_CRC32_SLICE_BY_8=1 for slice-by-8.
ifeq ($(UR_CRC32_SLICE_BY_8),1)
CFLAGS += -DUR_CRC32_SLICE_BY_8
endif
Expand Down
56 changes: 31 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ A lightweight C implementation of the [Blockchain Commons Uniform Resources (UR)
specification — a method for encoding structured binary data optimized
for transport via QR codes.

Targets PC, ESP-IDF (e.g. ESP32, ESP32-P4), and the Kendryte K210
(Krux / MaixPy) from a single source tree. SHA-256 backend is selected
at compile time to use the platform's hardware when available.
Targets PC and ESP-IDF (e.g. ESP32, ESP32-P4) from a single source
tree. SHA-256 backend is selected at compile time to use the
platform's hardware when available.

The implementation favors being small and easy to review. Optional
performance paths exist, but every default is the simplest, smallest
variant; see [Build options](#build-options).

## Supported UR types

Expand Down Expand Up @@ -107,6 +111,21 @@ expected during scanning).
Type-specific helpers (`bytes_from_cbor`, `psbt_from_cbor`,
`output_from_descriptor_string`, etc.) live in `src/types/*.h`.

## Build options

All options default to the smallest, simplest implementation; the
performance variants are opt-in (or, for the P4 vector path, gated by
the CPU with an opt-out). Each option is a single flag with the same
name everywhere it appears — Makefile variable, CMake option, and
ESP-IDF Kconfig.

| Option | Default | Effect |
|--------|---------|--------|
| `UR_CRC32_SLICE_BY_8` | off | Slice-by-8 CRC32: ~2.7x faster, +8 KB flash for a const table. The default 64-byte nibble table is rarely a bottleneck (CRCs run per fragment, a few hundred bytes each). |
| `UR_XOR_ESP32P4_SIMD` | on (ESP32-P4 only) | PIE 128-bit vector XOR for fountain-code mixing, with transparent word-wise fallback on unaligned data. Only exists on ESP32-P4; Kconfig opt-out. |
| `UR_ALLOC_PSRAM` | on (ESP targets) | Route the library's buffers to PSRAM with internal-RAM fallback, keeping fountain-decoder churn out of scarce internal heap. Kconfig opt-out; no-op elsewhere. |
| `UR_ENVELOPE_ONLY` | off | CMake: build only the UR transport layer (bytewords, fountain, multi-part assembly), excluding the `src/types/` payload codecs, for integrators that do their own CBOR. |

## Platform integration

The SHA-256 dependency is the only platform-sensitive piece. A
Expand All @@ -116,7 +135,7 @@ compile-time flag picks the backend; see `src/sha256/sha256_compat.h`.
|------------------|-------------------------|-------------------------------|
| PC (default) | *(none)* | bundled `src/sha256/sha256.c` |
| ESP-IDF | `UR_USE_MBEDTLS_SHA256` | mbedTLS PSA (HW-accelerated) |
| Kendryte K210 | `UR_USE_K210_SHA256` | `sha256_hard_calculate` (HW) |
| K210 (legacy) | `UR_USE_K210_SHA256` | `sha256_hard_calculate` (HW) |

### ESP-IDF component

Expand All @@ -132,32 +151,19 @@ idf_component_register(SRCS "app_main.c" INCLUDE_DIRS "." REQUIRES cUR)

Then include headers normally: `#include "ur_encoder.h"`.

### Kendryte K210 (Krux / MaixPy)

MaixPy's port does **not** wire components via USERMOD/`py.mk`; it
uses a hardcoded `register_component()` + `append_srcs_dir()` block in
`components/micropython/CMakeLists.txt`. The `-DUR_USE_K210_SHA256`
flag must be added inside that block, or the link fails with
undefined references to `ur_bundled_sha256_*`:

```cmake
# Inside the CONFIG_MAIXPY_BC_UR_ENABLE block:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUR_USE_K210_SHA256")
```
### Legacy K210 port

This mirrors how `CONFIG_MAIXPY_SECP256K1_ENABLE` sets its own flags.
Without this flag the bundled SHA implementation is selected, but its
symbol names now live under the `ur_bundled_sha256_*` namespace
precisely to surface the missing flag as a link error rather than a
silent runtime hardfault.
Maintained for compatibility only. The build wiring lives in the
consuming project, which must pass `-DUR_USE_K210_SHA256`; without it
the link fails with undefined references to `ur_bundled_sha256_*`
(deliberate — a link error instead of a silent runtime hardfault).

### MicroPython wrapper (`uUR.c`)

`uUR.c` at the repo root wraps the C library as a MicroPython module
exposing `URDecoder`, `UREncoder`, `UR`, and `FountainEncoder`. Krux
consumes this file as-is (byte-identical). For vanilla MicroPython
USERMOD builds the repo-root `micropython.mk` provides the wiring;
MaixPy K210 ignores it and uses the CMakeLists route above.
exposing `URDecoder`, `UREncoder`, `UR`, and `FountainEncoder`. For
vanilla MicroPython USERMOD builds the repo-root `micropython.mk`
provides the wiring; legacy ports wire it in their own build instead.

## Repository layout

Expand Down
93 changes: 64 additions & 29 deletions src/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,56 @@
#include <stdlib.h>
#include <string.h>

// Word type for the portable XOR loops: pointer-width chunks, so 32-bit
// targets don't emulate 64-bit arithmetic with register pairs.
#if UINTPTR_MAX > 0xFFFFFFFFu
typedef uint64_t ur_xor_word_t;
#else
typedef uint32_t ur_xor_word_t;
#endif

// Portable word-wise XOR loops (byte tail). memcpy is the aliasing- and
// alignment-safe word access and compiles to plain loads/stores wherever the
// target allows them; pointer casts here would be undefined behavior under
// strict aliasing and fault on strict-alignment CPUs.
static void ur_xor_portable_inplace(uint8_t *restrict dst,
const uint8_t *restrict src, size_t n) {
size_t i = 0;
for (; i + sizeof(ur_xor_word_t) <= n; i += sizeof(ur_xor_word_t)) {
ur_xor_word_t a, b;
memcpy(&a, dst + i, sizeof(a));
memcpy(&b, src + i, sizeof(b));
a ^= b;
memcpy(dst + i, &a, sizeof(a));
}
for (; i < n; i++)
dst[i] ^= src[i];
}

static void ur_xor_portable(uint8_t *restrict out, const uint8_t *restrict a,
const uint8_t *restrict b, size_t n) {
size_t i = 0;
for (; i + sizeof(ur_xor_word_t) <= n; i += sizeof(ur_xor_word_t)) {
ur_xor_word_t x, y;
memcpy(&x, a + i, sizeof(x));
memcpy(&y, b + i, sizeof(y));
x ^= y;
memcpy(out + i, &x, sizeof(x));
}
for (; i < n; i++)
out[i] = a[i] ^ b[i];
}

#if defined(UR_XOR_ESP32P4_SIMD)
#if !defined(CONFIG_IDF_TARGET_ESP32P4)
#error "UR_XOR_ESP32P4_SIMD requires ESP32-P4"
#endif

// ESP32-P4 PIE (xesppie) implementations: scalar head bytes until the
// destination is 16-byte aligned, 128-bit vector body when the operands are
// co-aligned, portable word-wise loop for the tail (and as the fallback when
// co-alignment is impossible).

// Pointers are pinned because esp.vld/esp.vst require low address registers.
static inline void ur_xor128_inplace(uint8_t *dst, const uint8_t *src,
size_t chunks) {
Expand Down Expand Up @@ -53,15 +98,10 @@ static inline void ur_xor128_out(uint8_t *out, const uint8_t *a,
:
: "memory");
}
#endif

// In-place XOR: dst[i] ^= src[i] for n bytes. Word-wise (8 bytes/iter) with a
// scalar tail -- roughly 4x faster than a byte loop. On ESP32-P4 with
// UR_XOR_ESP32P4_SIMD, 16-byte-aligned runs use the PIE 128-bit path (~16x).
// Buffers must not overlap.
// In-place XOR: dst[i] ^= src[i] for n bytes. Buffers must not overlap.
void ur_xor_inplace(uint8_t *restrict dst, const uint8_t *restrict src,
size_t n) {
#if defined(UR_XOR_ESP32P4_SIMD)
size_t head = (size_t)((0u - (uintptr_t)dst) & 15u);
if (head > n)
head = n;
Expand All @@ -78,23 +118,12 @@ void ur_xor_inplace(uint8_t *restrict dst, const uint8_t *restrict src,
src += done;
n -= done;
}
#endif
size_t i = 0;
for (; i + 8 <= n; i += 8) {
uint64_t a, b;
memcpy(&a, dst + i, 8);
memcpy(&b, src + i, 8);
a ^= b;
memcpy(dst + i, &a, 8);
}
for (; i < n; i++)
dst[i] ^= src[i];
ur_xor_portable_inplace(dst, src, n);
}

// Out-of-place XOR: out[i] = a[i] ^ b[i] for n bytes. Buffers must not overlap.
void ur_xor(uint8_t *restrict out, const uint8_t *restrict a,
const uint8_t *restrict b, size_t n) {
#if defined(UR_XOR_ESP32P4_SIMD)
size_t head = (size_t)((0u - (uintptr_t)out) & 15u);
if (head > n)
head = n;
Expand All @@ -113,19 +142,25 @@ void ur_xor(uint8_t *restrict out, const uint8_t *restrict a,
b += done;
n -= done;
}
#endif
size_t i = 0;
for (; i + 8 <= n; i += 8) {
uint64_t x, y;
memcpy(&x, a + i, 8);
memcpy(&y, b + i, 8);
x ^= y;
memcpy(out + i, &x, 8);
}
for (; i < n; i++)
out[i] = a[i] ^ b[i];
ur_xor_portable(out, a, b, n);
}

#else // portable builds: the word-wise loops are the whole implementation

// In-place XOR: dst[i] ^= src[i] for n bytes. Buffers must not overlap.
void ur_xor_inplace(uint8_t *restrict dst, const uint8_t *restrict src,
size_t n) {
ur_xor_portable_inplace(dst, src, n);
}

// Out-of-place XOR: out[i] = a[i] ^ b[i] for n bytes. Buffers must not overlap.
void ur_xor(uint8_t *restrict out, const uint8_t *restrict a,
const uint8_t *restrict b, size_t n) {
ur_xor_portable(out, a, b, n);
}

#endif

/* On ESP32 targets, route allocations to PSRAM (falling back to internal RAM
* when absent). ESP-IDF's malloc() keeps small allocations in internal RAM,
* where the fountain decoder's churn of variably-sized part buffers fragments
Expand Down
Loading