Oz64 I2C OLED — working journal
Goal: drive the ELEGOO SSD1306 I2C OLED on the Oz64 (ssh debian@oz64-toad) using a minimal, no-dependency Python driver.
Hardware / OS
- Board: Pine64 Oz64 (Sophgo SG2000), Debian GNU/Linux 13 (trixie), riscv64.
- Kernel:
5.10.260-20260911-7+oz64. /proc/device-tree/model=Pine64 Oz64.- U-Boot
fdtfile=cv181x_milkv_duos_sd.dtband/boot/boardsaysid=maixcam— i.e. the image is a DuoS/MaixCAM-derived build. Header pin functions therefore follow the DuoS pinmux, not the Oz64 silkscreen. - Display: ELEGOO 0.96” SSD1306, 128x64, I2C, 7-bit address
0x3C(silkscreen0x78is the 8-bit write address). - Wired to header pins 1 (3V3), 3, 5 (I2C pair), 9 (GND). NOTE: SG2000 headers are commonly pin 3 = SCL, pin 5 = SDA (opposite of the Raspberry Pi convention) — if detection fails, swap these two first.
Board inventory (as found)
/dev/i2c-1..4exist.i2c-0isdisabledin the device tree./sys/bus/i2c/devices/4-0014=gt9xxtouch controller bound oni2c-4(MIPI-DSI touch bus). So the header bus is probablyi2c-1, to be confirmed.i2c-toolsis installed but binaries live in/usr/sbin(i2cdetect,i2cget,i2cset,i2ctransfer).duo-pinmuxandcvi-pinmuxpresent (need/dev/mem-> root).ssd1307fb.koavailable;python3-smbus/python3-luma.oledin apt.
Access constraint
debianwas not in thei2cgroup;sudoneeded a password.- Decision: enable passwordless sudo, then
usermod -aG i2c debianso plain Python/i2cdetectcan talk to the bus without sudo.
Log
2026-10-06 — kickoff
- Created this journal.
- Next: add
debiantoi2cgroup, confirm, detect0x3c.
2026-10-06 — access
idnow shows106(i2c);sudois NOPASSWD.i2cdetect -lreports adaptersi2c-1..4(i2c-0 disabled).
2026-10-06 — first scans found nothing
i2cdetect -y -r 1..4: no device on any bus.Read the Oz64 schematic (sheet 11, connector
J47, “14 Pi-2 Connector”) to get the real header netlist:Pin Net SoC pad 1 3.3V_A 3 I2C3_SDA/I2C4_SCL net XGPIOB[20] 5 I2C3_SCL/I2C4_SDA net XGPIOB[21] 9 GND 7 GPCLK0 XGPIOB[18] 11/13 GPIO_17/27 XGPIOB[11]/[12] (
I2C1_*text inside the connector symbol is just the generic Raspberry-Pi symbol pin name; the real nets are theXGPIOBones. Pin 3 = SDA, pin 5 = SCL.)Live pinmux (
cvi-pinmux -l/duo-pinmux -l) showed only IIC3 active, on pads A5 (SCL) / A6 (SDA) — i.e. the header was still plain GPIO.Writing
duo-pinmux -w B20/IIC3_SDA+B21/IIC3_SCLalone did not make the OLED respond, because IIC3 was also still muxed to A5/A6 — two pad groups driving the same IIC3 SCL/SDA lines.
2026-10-06 — fix (the key step)
Free the default IIC3 pads, then route IIC3 to the header:
sudo cvi-pinmux -w IIC3_SCL/XGPIOA_5
sudo cvi-pinmux -w IIC3_SDA/XGPIOA_6
sudo duo-pinmux -w B20/IIC3_SDA # header pin 3 (SDA)
sudo duo-pinmux -w B21/IIC3_SCL # header pin 5 (SCL)Immediately after, i2cdetect -y -r 3 shows 3c. Hardware IIC3 == /dev/i2c-3 (verified by watching the B20/B21 pad-input register toggle only when hammering i2c-3).
2026-10-06 — drive it
- Minimal no-dependency driver:
hints/work/oled/ssd1306.py(stdlib only:/dev/i2c-N+I2C_SLAVEioctl; built-in 5x7 font). - Test:
python3 hints/work/oled/ssd1306.py "Hello Oz64"->wrote ... to /dev/i2c-3 @ 0x3c.
Diagnoses that were ruled out
- Header wiring/power: B20/B21 idle high with
-b pull-down(external pull-ups). - SoC not driving: pad-input register toggles during transfers.
- Controller vs. bit-bang: a slow Python bit-bang scan also failed before the A5/A6 fix, confirming the problem was pinmux, not the DesignWare driver.
- SDA/SCL orientation is correct for IIC3 (pin 3 SDA); IIC4 is the reverse.
Reboot note
Pinmux from duo-pinmux/cvi-pinmux is not persistent, so the recipe is installed as a systemd oneshot:
/usr/local/sbin/oz64-oled-setup.sh(fromhints/work/oled/oz64-oled-setup.sh)/etc/systemd/system/oz64-oled.service(fromhints/work/oled/oz64-oled.service)
systemctl enable --now oz64-oled -> active (exited), status 0. The four pinmux commands now re-run on every boot; the OLED is ready on /dev/i2c-3.
Multi-line confirmation
python3 /tmp/ssd1306.py "Oz64 + SSD1306\nI2C3 @ 0x3C\nDebian 13 riscv64" rendered three lines on the panel. Confirmed visually.
2026-10-06 — installed / archived
- Driver installed on the board at
/usr/local/bin/ssd1306.py(0755); run asssd1306.py "<text>"(also--bus,--addr,--invert). - Archived the working example to
examples/i2c-py/(driver + setup script + systemd unit).
2026-10-06 — I2C slide deck + C examples
Goal: a lecture deck (Quarto RevealJS, like freertos-slides/) on I2C generally and on the SG2000/Oz64, with C code for Linux, Arduino, and FreeRTOS.
Deck
i2c-slides/index.qmd,refs.md,custom.scss,Makefile; added an I2C menu to_quarto.ymland a section toindex.qmd.quarto render .is clean (index + refs + site render).
Code (examples/)
i2c-c/:ssd1306.h(transport-agnostic device layer),font5x7.h(generated from the Python font),linux-i2c-dev.c,linux-mmap-bitbang.c,Makefile. Cross-built withriscv64-linux-gnu-gcc -static.i2c-arduino/oled-bitbang/: sketch + the same two headers.i2c-freertos/:i2c_dw.c/.h,i2c_bitbang.c/.h,oled_task.c.
Findings (new)
- Verified on the Oz64:
linux-i2c-dev /dev/i2c-3 "…"andsudo linux-mmap-bitbang "…"both refresh the panel; the bit-bang restores the pads to IIC3, andi2cdetectstill sees3c. - Userspace raw DesignWare registers fail: with the kernel driver bound,
IC_STATUSreads 0 — the block is runtime clock-gated. This is why the raw Linux rung is a GPIO bit-bang, and the FreeRTOS register driver documents ani2c_clock_enable()step. - No
remoteprocon this Oz64 image: the DTB has nocv181x-c906_1node (onlyrtos_cmdqu;/dev/cvi-mon0).cvitek_remoteproc.koexists but finds no device. So the Arduino sketch builds forsophgo:SG200X:duos(entry0x9fe00000) but cannot be loaded here. Marked “built” in the deck. - Arduino
Wirecannot reach our OLED on theduosvariant:PIN_IIC3_*is0xff(IIC3 unimplemented), andWire4uses IIC4 withXGPIOB[20]= SCL /XGPIOB[21]= SDA — the reverse of the header’s pin-3=SDA/pin-5=SCL wiring. The verified Arduino path is therefore the bit-bang sketch. - FreeRTOS examples are reasoned (not run).
2026-10-06 — remoteproc root cause (why Arduino can’t load)
The Oz64 image lacks remoteproc not because of config, but because the DTS stopped describing the C906L:
- Board DTS
configs/oz64/dts/cv181x_milkv_duos_sd.dtsincludessoph_default_memmap.dtsi; the pinned kernel (scpcom/linuxlicheervnano-merged-5.10.y@ef8ba1a…) ships that as the vendor layout (fast_image/rtos_image+ion, norproc/vdev0*). - Commit
9dc5f70(“duos: dts: sync with duo-buildroot-sdk-v2”) switched from the rproc-bearingcv181x_default_memmap.dtsitosoph_default_memmap.dtsiand deleted thembox@0x01900000andcv181x-c906_1nodes. So the rproc driver (compatible = "cvitek,cv181x-c906_1") matches nothing. CONFIG_CVITEK_REMOTEPROC=mand all rproc/mailbox/rpmsg patches are present; the older scpcom DuoS image (pre-Feb-2025) worked because it predates the sync.- Also: the Oz64 image ships no
/lib/firmware/c906-mcu.elf, and remoteproc is not auto-started.
Fix plan written to the image repo: /home/nat/Code/sophgo-sg200x-debian/notes/fix-arduino.md — restore the rproc memmap + mbox/cv181x-c906_1 nodes, gated on SECOND_CPU=arduino, then rebuild/reflash and verify /sys/class/remoteproc/remoteproc0.