- C 96.2%
- Makefile 3.8%
Adds a section explaining what ISO 14229 actually defines (generic session/security/block-transfer services) versus what it doesn't (which addresses hold which calibration parameters, units, or the checksum algorithm) -- that mapping is a separate "ECU definition" obtained via OEM docs, diffing dumps, or a tuning company's reverse-engineered database, not something the protocol itself exposes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| src | ||
| .gitignore | ||
| ECU-PROJECT.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
tmp.2026-07-25.2n4Z
playing with optimized C/C++ code on a pine64
See ECU-PROJECT.md for a separate, in-progress plan to use the GPIO/I2C header work below plus a CAN interface to dump/modify/ reflash a car ECU.
what's here
Five small, self-contained C programs, each demonstrating a different way to actually use the hardware on a 4-core Pine64 (Cortex-A53):
mandelbrot-- renders the Mandelbrot set, splitting rows acrosspthreadworker threads (no locks needed, since each thread only writes its own rows).-bruns a benchmark sweep from 1 thread up tonprocand prints a speedup table.-p block|stripepicks how rows are handed out, and-vprints each thread's row count and elapsed time.bench_crc32,bench_aes,bench_sha256-- each computes the same checksum/cipher/hash two ways: a portable software implementation, and a hardware version using the CPU's dedicated instructions (ARMv8 CRC/Crypto Extension on aarch64 via<arm_acle.h>/<arm_neon.h>; SSE4.2/AES-NI on x86_64/i386 for CRC32 and AES via<nmmintrin.h>/<wmmintrin.h>-- x86 SHA is deliberately not implemented, seesrc/sha256/sha256.c). Each one checks its own output against known-answer test vectors (FIPS-197 for AES, FIPS 180-4 for SHA-256) and checks software vs. hardware agree, before trusting the speedup number.hwcheck-- checks whether/dev/gpiochip*and/dev/i2c-*exist and are actually openable by the current user, without reading or writing any hardware state. Run this before writing any GPIO/I2C code against the board; see "GPIO / I2C access" below.gpio_tool,i2c_tool-- CLIs exercisingsrc/gpio/gpio.candsrc/i2c/i2c.c, thin wrappers around the Linux GPIO/I2C character-device ioctls (<linux/gpio.h>,<linux/i2c-dev.h>,<linux/i2c.h>) -- no vendor SDK, no libgpiod dependency. See "GPIO / I2C hardware access" below for the header pinout and library details.
All build the same way on any Linux box; the hardware paths are picked automatically for the arch you're building on (see the Makefile), so you can sanity-check CRC32/AES on a regular x86_64 dev machine before ever touching the board (SHA-256 hardware is aarch64-only here).
build & run
make # builds everything into bin/
./bin/mandelbrot -b # thread-count vs. speedup table
./bin/mandelbrot -o out.ppm # render one image (view with e.g. `feh out.ppm`)
./bin/bench_crc32 128 20 # 128 MiB buffer, 20 iterations
./bin/bench_aes 128 10
./bin/bench_sha256 128 10
./bin/hwcheck # can this user touch GPIO/I2C without sudo?
./bin/gpio_tool list # header pin table + live line state (read-only)
./bin/i2c_tool scan 1 # probe I2C1 header pins (bus 1) for devices
measured on the actual pine64 (Cortex-A53, ARMv8 Crypto Extension)
| benchmark | software | hardware | speedup |
|---|---|---|---|
| CRC32C (64 MiB) | 104.8 MiB/s | 975.8 MiB/s | 9.3x |
| AES-128 ECB (32 MiB) | 5.7 MiB/s | 197.7 MiB/s | 34.7x |
| SHA-256 (48 MiB) | 29.0 MiB/s | 153.0 MiB/s | 5.3x |
All three passed their known-answer self-tests on-device, and software vs. hardware output matched byte-for-byte in every run.
On the Pine64 (Armbian/Debian, Cortex-A53, Mali400 via lima): same
make, same commands, over ssh -- no cross-compiling needed since gcc
runs natively there.
a real finding: block partitioning doesn't scale monotonically
Running mandelbrot -b on the pine64 gives a non-monotonic result -- 3
threads is slower than 2:
threads seconds speedup
1 4.569 1.00x
2 2.285 2.00x
3 2.790 1.64x <- worse than 2 threads!
4 1.869 2.44x
-v shows why: with block partitioning, thread 1's row range straddles
the dense middle of the set (interior points run the full max_iter loop;
exterior points escape in a handful of iterations, and the set is
symmetric about the real axis), so it does 3x the work of its siblings
while they sit idle at pthread_join:
$ ./bin/mandelbrot -t 3 -p block -v -o /tmp/m.ppm
thread 0: 240 rows 0.896s
thread 1: 240 rows 2.790s <- straggler
thread 2: 240 rows 0.890s
-p stripe (round-robin row assignment, y % nthreads == thread_id
instead of contiguous chunks) spreads the expensive middle rows evenly
across every thread regardless of count, and fixes it:
threads seconds speedup
1 4.571 1.00x
2 2.279 2.01x
3 1.615 2.83x
4 1.159 3.95x <- essentially linear
The general lesson: for any embarrassingly-parallel workload, contiguous block partitioning only balances well if the work is uniform across the partition axis (or, by luck, if the split happens to land on a symmetry in the workload, the way a 2-way split does here). When cost is skewed, interleaved/striped partitioning is the standard fix.
Note: this specific board's Mali400 GPU only supports OpenGL ES 2.0
(graphics) via the mainlined lima driver -- there's no OpenCL or Vulkan
compute path on Utgard-generation Mali, so "GPU" work here means shader
graphics, not general compute. The CPU side is where the real, measurable
wins are: multithreading across the 4 cores, and the hardware-accelerated
CRC32/AES/SHA instructions exposed by the crc32 aes sha1 sha2 pmull
flags in lscpu -- see the results table above.
Possible next steps: a from-scratch, carefully-verified x86 SHA-NI path
(intentionally skipped for now, see src/sha256/sha256.c); or the EGL +
OpenGL ES 2.0 stretch goal to actually exercise the Mali400 via lima
(needs libegl1-mesa-dev libgles2-mesa-dev installed on the board first).
GPIO / I2C hardware access
Both are exposed as Linux character devices (/dev/gpiochip*,
/dev/i2c-*), controlled via ioctls from the kernel's own public headers
(<linux/gpio.h>, <linux/i2c-dev.h>, <linux/i2c.h>) -- no vendor SDK
needed. But on a stock Armbian image, neither is usable by a regular user
out of the box:
-
GPIO:
/dev/gpiochip*defaults toroot:root, mode0600-- nogpiogroup exists at all. Fixed with a udev rule plus a group:sudo groupadd -f gpio sudo usermod -aG gpio "$USER" sudo tee /etc/udev/rules.d/99-gpio.rules <<'EOF' SUBSYSTEM=="gpio", KERNEL=="gpiochip*", GROUP="gpio", MODE="0660" EOF sudo udevadm control --reload-rules sudo udevadm triggerThis board has three gpiochips --
gpiochip0/gpiochip1are the SoC's own pin controller,gpiochip2is the AXP803/AXP813 PMIC's GPIO lines (a separate driver,axp20x-gpio) -- and a singleudevadm triggermay not re-apply the rule to a chip that was already present before the rule was added; retrigger that one specifically ifhwcheckstill shows it as denied:sudo udevadm trigger --action=add <its /sys path>. -
I2C:
i2c-toolsalready ships a udev rule (60-i2c-tools.rules) that sets/dev/i2c-*to groupi2c, mode0660-- but you still have to be a member of that group yourself:sudo usermod -aG i2c "$USER" -
Either way, log back in (new SSH session, or
newgrp gpio/newgrp i2c) afterward -- group membership is read at login, so an already-open shell won't see the change.
Run ./bin/hwcheck after any of the above to confirm it actually worked
-- it reports each device as accessible / permission-denied / not found,
with exit status 0 only once everything's usable without sudo.
the library: no vendor SDK, just the kernel's own uAPI
src/gpio/gpio.c and src/i2c/i2c.c are thin wrappers around the same
ioctls any GPIO/I2C library uses under the hood -- built against the exact
struct/ioctl definitions in this board's own /usr/include/linux/gpio.h,
<linux/i2c-dev.h>, <linux/i2c.h>, not guessed from memory:
- GPIO uses the v2 character-device uAPI:
open()a/dev/gpiochipN,GPIO_V2_GET_LINE_IOCTLto request a specific line offset as input or output (for output, the initial value is set atomically as part of the same request, so the pin is never briefly left at some other state), thenGPIO_V2_LINE_GET_VALUES_IOCTL/_SET_VALUES_IOCTLon the returned line fd to read/write it.gpio_get_line_info()queries a line's name/ used-state without requesting it -- read-only, safe regardless of what's wired to the pin. - I2C uses
ioctl(fd, I2C_SLAVE, addr)to select a device, then plainread()/write(), orI2C_RDWRwith twostruct i2c_msgs for a combined register read with a proper repeated START (whati2c_read_reg()does -- a plainwrite()thenread()risks the adapter inserting a STOP in between, which most sensors don't expect).i2c_probe()uses an SMBus "quick" transaction (no data byte, just an address+direction ACK/NACK check) to detect devices, picking read vs. write direction per address range the same wayi2cdetect's default mode does, to avoid upsetting devices in ranges known to dislike a write-direction probe.
Pi-2 header pinout (Pine A64/A64+/A64-LTS)
src/gpio/pine64_pinout.c maps the 40-pin, Raspberry-Pi-layout header to
(chip, offset) pairs. Source: the "Pi-2/Euler/Ext Bus Connector Pin
Assignment" schematic linked from wiki.pine64.org, cross-checked against
this exact board's live gpioinfo (line counts and reserved-line
positions matched the offset arithmetic for every port-labeled pin):
- Ports B-H live on the main pin controller,
/dev/gpiochip0(256 lines):offset = bank*32 + pin, A=0, B=1, ..., H=7 (e.g.PC7-> offset 71). - Port L (the always-on domain) lives on
/dev/gpiochip1(32 lines):offset = pindirectly (e.g.PL10-> offset 10). - A few header pins were only labeled with a bare Raspberry-Pi-style BCM
GPIO number in the source schematic, with no Allwinner port name given
-- those are left unverified (
chip = NULL) rather than guessed at;gpio_tool listmarks them clearly.
Verified live on the board:
$ ./bin/gpio_tool list
pin signal port chip offset info
3 I2C1_SDA PH3 /dev/gpiochip0 227 free
5 I2C1_SCL PH2 /dev/gpiochip0 226 free
11 - PC7 /dev/gpiochip0 71 free
...
$ ./bin/gpio_tool get 11
pin 11 (PC7, /dev/gpiochip0 offset 71) = 1
$ ./bin/i2c_tool scan 1 # I2C1, physical pins 3/5 -- clean scan, nothing attached
gpio_tool set and i2c_tool read-reg actually drive a pin or address a
real device -- only point those at a physical pin/address you've confirmed
is safe to touch (nothing sensitive wired to it).