diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 7cc080b..6635720 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -36,13 +36,18 @@ pio run # if platformio.ini exists, all default envs ## 3. simavr ```bash -make sim-test # run the firmware on an emulated ATmega328P, compare to baseline -make sim-test-update # regenerate test/uno-simavr.txt after intentional trace changes +make sim-test # run the firmware on an emulated ATmega328P, compare serial trace to baseline +make sim-test-update # regenerate test/uno-simavr.txt after intentional trace changes +make sim-display-test # render the emulated SSD1306 framebuffer, compare pixels to baseline +make sim-display-test-update # regenerate test/uno-display.txt after intentional display changes ``` -This catches boot crashes and memory exhaustion that compilation cannot (it already -caught a display-buffer OOM and an out-of-bounds ADC mode). Requires simavr and -PlatformIO (both in the devcontainer). +`sim-test` catches boot crashes and memory exhaustion that compilation cannot (it +already caught a display-buffer OOM and an out-of-bounds ADC mode). `sim-display-test` +attaches a virtual SSD1306 over TWI and compares the rendered VRAM (ASCII art) +pixel-for-pixel against `test/uno-display.txt`, catching rendering regressions the serial +trace misses. Both require PlatformIO and simavr; `sim-display-test` also needs +`libsimavr-dev`, `libelf-dev`, and `gcc` (all in the devcontainer). ## 4. Hardware caveat diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 775c1ea..1f7c22b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,9 +4,12 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ make \ curl \ + gcc \ python3-pip \ python3-venv \ simavr \ + libsimavr-dev \ + libelf-dev \ udev \ && apt-get clean && rm -rf /var/lib/apt/lists/* diff --git a/.github/workflows/platformio.yml b/.github/workflows/platformio.yml index 3e1a26f..c8ff124 100644 --- a/.github/workflows/platformio.yml +++ b/.github/workflows/platformio.yml @@ -48,6 +48,9 @@ jobs: - name: Install simavr run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends simavr - - name: Run firmware under simavr + sudo apt-get install -y --no-install-recommends \ + simavr libsimavr-dev libelf-dev gcc + - name: Run firmware under simavr (serial trace) run: make sim-test + - name: Run firmware under simavr (OLED framebuffer) + run: make sim-display-test diff --git a/.gitignore b/.gitignore index 0fcab4f..066cb21 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ build/ # PlatformIO .pio/ + +# simavr display test host binary (built by `make sim-display-test`) +test/sim_display/sim_display diff --git a/AGENTS.md b/AGENTS.md index 77b1af5..63ca81f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,10 @@ tiny_scope/ ├── .devcontainer/ # Dev container: arduino-cli, PlatformIO, simavr ├── .claude/skills/ # Project skills for agent work (building, verify) ├── .github/workflows/ # CI: arduino.yml + platformio.yml build matrices + simavr -├── test/ # simavr smoke test: runner script + golden baseline +├── test/ # simavr tests: +│ # simavr-run.sh + uno-simavr.txt (serial smoke test) +│ # sim-display-run.sh + uno-display.txt (OLED render test) +│ # sim_display/ (render runner + vendored GPL SSD1306 part) ├── Makefile # arduino-cli build automation ├── arduino-cli.yaml # arduino-cli config (board indexes, .arduino dirs) ├── platformio.ini # PlatformIO envs: uno, teensy31, teensylc, @@ -98,10 +101,27 @@ buffers) and runs it under simavr via `test/simavr-run.sh`. The comparison strip so value drift doesn't fail the test but OK/FAIL/YES/NO verdict flips do. The trace code in `tiny_scope.ino` is compiled out of production builds. +### simavr display test + +```bash +make sim-display-test # render the emulated OLED framebuffer, compare to golden +make sim-display-test-update # regenerate test/uno-display.txt after intentional changes +``` + +Runs the same `uno_sim` firmware under simavr with a **virtual SSD1306 OLED attached +over TWI**, waits for the `sim: TESTS COMPLETE` marker, then dumps the display VRAM as +ASCII art (64x128, `#`/`.`) and compares it **pixel-for-pixel** (no digit stripping) +against `test/uno-display.txt`. This catches rendering regressions the serial smoke test +cannot. The runner is `test/sim_display/sim_display.c`; it links `ssd1306_virt.c/.h`, +**vendored GPLv3 from simavr** and used as test tooling only (never in the firmware) — +see `test/sim_display/README.md`. Needs `libsimavr-dev`, `libelf-dev`, and `gcc` (all in +the devcontainer); the Makefile builds the host binary. + ### Verification expectations Before declaring a change done, build for **at least** `arduino:avr:uno` and one ARM -target, and run `make sim-test` — see `.claude/skills/verify`. `--warnings all` is on; do not introduce new +target, and run `make sim-test` and `make sim-display-test` — see `.claude/skills/verify`. +`--warnings all` is on; do not introduce new warnings. Display/ADC behavior can only be fully confirmed on hardware; call out runtime-affecting changes (display init, ADC timing, pin changes) in the PR description so the maintainer can flash-test. diff --git a/Makefile b/Makefile index b5050cf..c8f9567 100644 --- a/Makefile +++ b/Makefile @@ -79,4 +79,30 @@ sim-test-update: # Run under simavr and regenerate the golden baseline pio run -e uno_sim test/simavr-run.sh --update -.PHONY: clean %.hex all core setup setup-pio sim-test sim-test-update +# simavr display test: renders the emulated SSD1306 framebuffer to ASCII art +# and compares it pixel-for-pixel to the golden baseline. Needs libsimavr-dev +# and libelf-dev (in the devcontainer). ssd1306_virt.c is GPLv3 test tooling +# vendored from simavr - see test/sim_display/README.md. +SIM_DISPLAY_INC ?= /usr/include/simavr +SIM_DISPLAY_DIR = test/sim_display + +# Our runner is held to -Wall -Wextra; the vendored simavr part needs +# -Wno-unused-parameter (its IRQ hooks ignore the irq/param args), so compile +# the two as separate objects. +$(SIM_DISPLAY_DIR)/sim_display: $(SIM_DISPLAY_DIR)/sim_display.c $(SIM_DISPLAY_DIR)/ssd1306_virt.c $(SIM_DISPLAY_DIR)/ssd1306_virt.h + gcc -O2 -Wall -Wextra -I$(SIM_DISPLAY_INC) -c -o $(SIM_DISPLAY_DIR)/sim_display.o $(SIM_DISPLAY_DIR)/sim_display.c + gcc -O2 -Wall -Wextra -Wno-unused-parameter -I$(SIM_DISPLAY_INC) -c -o $(SIM_DISPLAY_DIR)/ssd1306_virt.o $(SIM_DISPLAY_DIR)/ssd1306_virt.c + gcc -o $@ $(SIM_DISPLAY_DIR)/sim_display.o $(SIM_DISPLAY_DIR)/ssd1306_virt.o -lsimavr -lelf + +sim-display-test: # Render the emulated OLED framebuffer and compare to the golden baseline +sim-display-test: test/sim_display/sim_display + pio run -e uno_sim + test/sim-display-run.sh + +sim-display-test-update: # Render the emulated OLED framebuffer and regenerate the golden baseline +sim-display-test-update: test/sim_display/sim_display + pio run -e uno_sim + test/sim-display-run.sh --update + +.PHONY: clean %.hex all core setup setup-pio sim-test sim-test-update \ + sim-display-test sim-display-test-update diff --git a/test/sim-display-run.sh b/test/sim-display-run.sh new file mode 100755 index 0000000..79e04de --- /dev/null +++ b/test/sim-display-run.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# sim-display-run.sh - Run the tiny_scope firmware on a simulated ATmega328P +# with a virtual SSD1306 OLED attached over TWI, render its framebuffer to +# ASCII art, and compare it pixel-for-pixel against a golden baseline. +# +# Usage: +# test/sim-display-run.sh [firmware.elf] Run and compare against the golden file. +# test/sim-display-run.sh --update Run and (re)write the golden file instead +# of comparing (also: UPDATE=1 env var). +# +# The default firmware path is .pio/build/uno_sim/firmware.elf (built with +# `pio run -e uno_sim`). The runner binary (test/sim_display/sim_display) is +# built by the Makefile - run `make sim-display-test` rather than this script +# directly, or build the runner first. +# +# Unlike the serial smoke test (simavr-run.sh) this is a pixel test: the ASCII +# art is compared verbatim, with no digit stripping. +# +# Environment variables: +# RUNNER sim_display binary (default: test/sim_display/sim_display) +# TIMEOUT max simulated seconds (default: 30) + +set -u + +FIRMWARE="${1:-.pio/build/uno_sim/firmware.elf}" +TIMEOUT="${TIMEOUT:-30}" + +UPDATE="${UPDATE:-0}" +if [ "${FIRMWARE}" = "--update" ]; then + UPDATE=1 + FIRMWARE=".pio/build/uno_sim/firmware.elf" +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUNNER="${RUNNER:-${SCRIPT_DIR}/sim_display/sim_display}" +GOLDEN="${SCRIPT_DIR}/uno-display.txt" + +if [ ! -x "${RUNNER}" ]; then + echo "ERROR: runner not built: ${RUNNER}" >&2 + echo "Build it first, e.g. with: make sim-display-test" >&2 + exit 1 +fi + +if [ ! -f "${FIRMWARE}" ]; then + echo "ERROR: firmware not found: ${FIRMWARE}" >&2 + echo "Build it first, e.g. with: pio run -e uno_sim" >&2 + exit 1 +fi + +TMP_OUT="$(mktemp)" +cleanup() { + rm -f "${TMP_OUT}" +} +trap cleanup EXIT + +if ! "${RUNNER}" "${FIRMWARE}" "${TMP_OUT}" --timeout "${TIMEOUT}"; then + echo "ERROR: sim_display run failed (see output above)." >&2 + exit 1 +fi + +if [ "${UPDATE}" -eq 1 ]; then + cp "${TMP_OUT}" "${GOLDEN}" + echo "Updated golden file: ${GOLDEN}" + cat "${GOLDEN}" + exit 0 +fi + +if [ ! -f "${GOLDEN}" ]; then + echo "ERROR: golden file not found: ${GOLDEN}" >&2 + echo "Run with --update first to generate it." >&2 + exit 1 +fi + +if diff -q "${GOLDEN}" "${TMP_OUT}" >/dev/null; then + echo "PASS: rendered display matches the golden baseline." + exit 0 +fi + +echo "FAIL: rendered display does not match the golden baseline (${GOLDEN})." +echo "----- diff (golden vs capture) -----" +diff "${GOLDEN}" "${TMP_OUT}" +echo "----- captured display -----" +cat "${TMP_OUT}" +exit 1 diff --git a/test/sim_display/README.md b/test/sim_display/README.md new file mode 100644 index 0000000..998969d --- /dev/null +++ b/test/sim_display/README.md @@ -0,0 +1,41 @@ +# sim_display - emulated SSD1306 OLED render test + +This is a headless display test for tiny_scope. It runs the firmware on an +emulated ATmega328P (via [simavr](https://github.com/buserror/simavr)) with a +**virtual SSD1306 OLED** attached over TWI (I2C), waits for the firmware's +`sim: TESTS COMPLETE` marker on the serial port, then dumps the display's video +RAM as ASCII art (`#` = lit pixel, `.` = dark, 64 rows x 128 columns). That +render is compared pixel-for-pixel against the golden baseline +`test/uno-display.txt`, so it catches display regressions that the serial-only +smoke test (`test/simavr-run.sh`) cannot. + +## Files + +- `sim_display.c` - the runner (loads the ELF, wires up UART capture and the + virtual OLED, steps the CPU to the marker, writes the ASCII-art dump). This is + our own code. +- `ssd1306_virt.c` / `ssd1306_virt.h` - the virtual SSD1306 part, **vendored + verbatim from simavr v1.6** (`examples/parts/ssd1306_virt.{c,h}`). + +## Licensing + +> `ssd1306_virt.c` / `ssd1306_virt.h` are **GPLv3** (Copyright Michel Pollet and +> Doug Szumski), unlike the rest of tiny_scope which is MIT. They are **test +> tooling only** and are compiled solely into the `sim_display` host binary - +> they are **never** linked into the firmware. Keep their GPL headers intact. + +## Running + +Requires `gcc`, `libsimavr-dev`, and `libelf-dev` (all present in the +devcontainer). From the repo root: + +```bash +make sim-display-test # build uno_sim + runner, render and compare +make sim-display-test-update # regenerate test/uno-display.txt after an + # intentional display change +``` + +The Makefile builds the `sim_display` binary +(`gcc ... -I/usr/include/simavr -lsimavr -lelf`) and the `uno_sim` PlatformIO +firmware, then `test/sim-display-run.sh` runs the render and diff. Override the +simulated-seconds timeout with `TIMEOUT=`. diff --git a/test/sim_display/sim_display.c b/test/sim_display/sim_display.c new file mode 100644 index 0000000..20b333a --- /dev/null +++ b/test/sim_display/sim_display.c @@ -0,0 +1,166 @@ +/* + * sim_display.c - Run tiny_scope firmware on a simulated ATmega328P with a + * virtual SSD1306 OLED attached over TWI (I2C), then dump the display's VRAM + * as ASCII art for a pixel-exact golden comparison. + * + * Usage: + * sim_display [--timeout ] + * + * The firmware (built as the PlatformIO `uno_sim` env) prints + * "sim: TESTS COMPLETE" on UART0 right after its 5th display.display(), so we + * step the CPU until that marker appears and snapshot the framebuffer then - + * this makes the rendered output deterministic. A crash or timeout dumps the + * captured UART text to stderr and exits nonzero. + * + * The virtual display part (ssd1306_virt.c/.h) is vendored from simavr's + * examples and is GPLv3 - see README.md. This file is test tooling only and is + * never linked into the MIT-licensed firmware. + */ + +#include +#include +#include + +#include "sim_avr.h" +#include "sim_elf.h" +#include "sim_core.h" +#include "avr_uart.h" + +#include "ssd1306_virt.h" + +#define DEFAULT_MCU "atmega328p" +#define DEFAULT_FREQ 16000000 +#define DEFAULT_TIMEOUT_S 30 +#define MARKER "sim: TESTS COMPLETE" + +/* OLED reset pin as wired by the firmware (scope.cpp: OLED_RESET 4 -> PD4). */ +#define OLED_RESET_PORT 'D' +#define OLED_RESET_PIN 4 + +/* Ring-free growable capture of UART0 output so we can scan for the marker. */ +static char uart_text[8192]; +static size_t uart_len = 0; +static int marker_seen = 0; + +static void +uart_out_hook (struct avr_irq_t *irq, uint32_t value, void *param) +{ + (void) irq; + (void) param; + if (uart_len < sizeof(uart_text) - 1) { + uart_text[uart_len++] = (char) value; + uart_text[uart_len] = '\0'; + if (!marker_seen && strstr(uart_text, MARKER)) + marker_seen = 1; + } +} + +/* + * Dump the display framebuffer to fp: 64 rows of 128 chars, '#' lit, '.' dark. + * VRAM is pages x columns, one bit per pixel vertically. The firmware applies + * setRotation(2) in software before writing VRAM, so we dump it verbatim. + */ +static void +dump_vram (FILE *fp, ssd1306_t *oled) +{ + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 128; x++) { + int bit = (oled->vram[y / 8][x] >> (y % 8)) & 1; + fputc(bit ? '#' : '.', fp); + } + fputc('\n', fp); + } +} + +int +main (int argc, char *argv[]) +{ + const char *firmware_path = NULL; + const char *output_path = NULL; + long timeout_s = DEFAULT_TIMEOUT_S; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--timeout") == 0 && i + 1 < argc) { + timeout_s = strtol(argv[++i], NULL, 10); + } else if (!firmware_path) { + firmware_path = argv[i]; + } else if (!output_path) { + output_path = argv[i]; + } + } + + if (!firmware_path || !output_path) { + fprintf(stderr, + "usage: %s [--timeout ]\n", + argv[0]); + return 2; + } + + elf_firmware_t fw; + memset(&fw, 0, sizeof(fw)); + if (elf_read_firmware(firmware_path, &fw) < 0) { + fprintf(stderr, "ERROR: could not read firmware: %s\n", firmware_path); + return 2; + } + + const char *mcu = fw.mmcu[0] ? fw.mmcu : DEFAULT_MCU; + uint32_t freq = fw.frequency ? fw.frequency : DEFAULT_FREQ; + + avr_t *avr = avr_make_mcu_by_name(mcu); + if (!avr) { + fprintf(stderr, "ERROR: unknown MCU: %s\n", mcu); + return 2; + } + avr_init(avr); + avr->frequency = freq; + avr_load_firmware(avr, &fw); + + /* Capture UART0 output and stop simavr echoing it to our stdout. */ + avr_irq_t *uart_out = + avr_io_getirq(avr, AVR_IOCTL_UART_GETIRQ('0'), UART_IRQ_OUTPUT); + if (uart_out) + avr_irq_register_notify(uart_out, uart_out_hook, NULL); + + uint32_t uart_flags = 0; + avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uart_flags); + uart_flags &= ~AVR_UART_FLAG_STDIO; + avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uart_flags); + + /* Attach the virtual SSD1306 over TWI (connect_twi wires the TWI IRQs and + * the reset pin itself, given part->avr set by ssd1306_init). */ + ssd1306_t oled; + ssd1306_init(avr, &oled, 128, 64); + ssd1306_wiring_t wiring; + memset(&wiring, 0, sizeof(wiring)); + wiring.reset.port = OLED_RESET_PORT; + wiring.reset.pin = OLED_RESET_PIN; + ssd1306_connect_twi(&oled, &wiring); + + /* Deadline in CPU cycles. */ + uint64_t deadline = (uint64_t) freq * (uint64_t) timeout_s; + + int state = cpu_Running; + while (!marker_seen && avr->cycle < deadline) { + state = avr_run(avr); + if (state == cpu_Done || state == cpu_Crashed) + break; + } + + if (!marker_seen) { + fprintf(stderr, + "ERROR: marker '%s' never appeared (state=%d, cycle=%llu, timeout=%lds).\n", + MARKER, state, (unsigned long long) avr->cycle, timeout_s); + fprintf(stderr, "----- captured UART output -----\n%s\n", uart_text); + return 1; + } + + FILE *fp = fopen(output_path, "w"); + if (!fp) { + fprintf(stderr, "ERROR: cannot open output file: %s\n", output_path); + return 2; + } + dump_vram(fp, &oled); + fclose(fp); + + return 0; +} diff --git a/test/sim_display/ssd1306_virt.c b/test/sim_display/ssd1306_virt.c new file mode 100644 index 0000000..84d25aa --- /dev/null +++ b/test/sim_display/ssd1306_virt.c @@ -0,0 +1,503 @@ +/* + ssd1306_virt.c + + Copyright 2011 Michel Pollet + Copyright 2014 Doug Szumski + + This file is part of simavr. + + simavr is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simavr is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simavr. If not, see . + */ + +#include +#include +#include +#include "sim_time.h" + +#include "ssd1306_virt.h" +#include "avr_spi.h" +#include "avr_twi.h" +#include "avr_ioport.h" + +/* + * Write a byte at the current cursor location and then scroll the cursor. + */ +static void +ssd1306_write_data (ssd1306_t *part) +{ + part->vram[part->cursor.page][part->cursor.column] = part->spi_data; + + // Scroll the cursor + if (++(part->cursor.column) >= SSD1306_VIRT_COLUMNS) + { + part->cursor.column = 0; + if ( part->addr_mode == SSD1306_ADDR_MODE_HORZ && + (++(part->cursor.page) >= SSD1306_VIRT_PAGES)) + { + part->cursor.page = 0; + } + } + + ssd1306_set_flag (part, SSD1306_FLAG_DIRTY, 1); +} + +/* + * Called on the first command byte sent. For setting single + * byte commands and initiating multi-byte commands. + */ +void +ssd1306_update_command_register (ssd1306_t *part) +{ + part->reg_write_sz = 1; + switch (part->spi_data) + { + case SSD1306_VIRT_CHARGE_PUMP: + case SSD1306_VIRT_SET_CONTRAST: + part->command_register = part->spi_data; + //printf ("SSD1306: CONTRAST SET COMMAND: 0x%02x\n", part->spi_data); + return; + case SSD1306_VIRT_DISP_NORMAL: + ssd1306_set_flag (part, SSD1306_FLAG_DISPLAY_INVERTED, + 0); + ssd1306_set_flag (part, SSD1306_FLAG_DIRTY, 1); + //printf ("SSD1306: DISPLAY NORMAL\n"); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_DISP_INVERTED: + ssd1306_set_flag (part, SSD1306_FLAG_DISPLAY_INVERTED, + 1); + ssd1306_set_flag (part, SSD1306_FLAG_DIRTY, 1); + //printf ("SSD1306: DISPLAY INVERTED\n"); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_DISP_SUSPEND: + ssd1306_set_flag (part, SSD1306_FLAG_DISPLAY_ON, 0); + ssd1306_set_flag (part, SSD1306_FLAG_DIRTY, 1); + //printf ("SSD1306: DISPLAY SUSPENDED\n"); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_DISP_ON: + ssd1306_set_flag (part, SSD1306_FLAG_DISPLAY_ON, 1); + ssd1306_set_flag (part, SSD1306_FLAG_DIRTY, 1); + //printf ("SSD1306: DISPLAY ON\n"); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SET_PAGE_START_ADDR + ... SSD1306_VIRT_SET_PAGE_START_ADDR + + SSD1306_VIRT_PAGES - 1: + part->cursor.page = part->spi_data + - SSD1306_VIRT_SET_PAGE_START_ADDR; + //printf ("SSD1306: SET PAGE ADDRESS: 0x%02x\n", part->spi_data); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SET_COLUMN_LOW_NIBBLE + ... SSD1306_VIRT_SET_COLUMN_LOW_NIBBLE + 0xF: + part->spi_data -= SSD1306_VIRT_SET_COLUMN_LOW_NIBBLE; + if (part->addr_mode == SSD1306_ADDR_MODE_PAGE) { + part->cursor.column = (part->cursor.column & 0xF0) + | (part->spi_data & 0xF); + } + //printf ("SSD1306: SET COLUMN LOW NIBBLE: 0x%02x\n",part->spi_data); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SET_COLUMN_HIGH_NIBBLE + ... SSD1306_VIRT_SET_COLUMN_HIGH_NIBBLE + 0xF: + part->spi_data -= SSD1306_VIRT_SET_COLUMN_HIGH_NIBBLE; + if (part->addr_mode == SSD1306_ADDR_MODE_PAGE) { + part->cursor.column = (part->cursor.column & 0xF) + | ((part->spi_data & 0xF) << 4); + } + //printf ("SSD1306: SET COLUMN HIGH NIBBLE: 0x%02x\n", part->spi_data); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SET_SEG_REMAP_0: + ssd1306_set_flag (part, SSD1306_FLAG_SEGMENT_REMAP_0, + 1); + //printf ("SSD1306: SET COLUMN ADDRESS 0 TO OLED SEG0 to \n"); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SET_SEG_REMAP_127: + ssd1306_set_flag (part, SSD1306_FLAG_SEGMENT_REMAP_0, + 0); + //printf ("SSD1306: SET COLUMN ADDRESS 127 TO OLED SEG0 to \n"); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SET_COM_SCAN_NORMAL: + ssd1306_set_flag (part, SSD1306_FLAG_COM_SCAN_NORMAL, + 1); + //printf ("SSD1306: SET COM OUTPUT SCAN DIRECTION NORMAL \n"); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SET_COM_SCAN_INVERTED: + ssd1306_set_flag (part, SSD1306_FLAG_COM_SCAN_NORMAL, + 0); + //printf ("SSD1306: SET COM OUTPUT SCAN DIRECTION REMAPPED \n"); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SCROLL_RIGHT: + case SSD1306_VIRT_SCROLL_LEFT: + part->command_register = part->spi_data; + part->reg_write_sz = 6; + return; + case SSD1306_VIRT_SCROLL_VR: + case SSD1306_VIRT_SCROLL_VL: + part->command_register = part->spi_data; + part->reg_write_sz = 5; + return; + case SSD1306_VIRT_SET_RATIO_OSC: + case SSD1306_VIRT_MULTIPLEX: + case SSD1306_VIRT_SET_OFFSET: + case SSD1306_VIRT_MEM_ADDRESSING: + case SSD1306_VIRT_SET_LINE: + case SSD1306_VIRT_SET_PADS: + case SSD1306_VIRT_SET_CHARGE: + case SSD1306_VIRT_SET_VCOM: + part->command_register = part->spi_data; + return; + case SSD1306_VIRT_VERT_SCROLL_A: + case SSD1306_VIRT_SET_PAGE_ADDR: + case SSD1306_VIRT_SET_COL_ADDR: + part->reg_write_sz = 2; + part->command_register = part->spi_data; + return; + case SSD1306_VIRT_SCROLL_ON: + case SSD1306_VIRT_SCROLL_OFF: + case SSD1306_VIRT_RESUME_TO_RAM_CONTENT: + SSD1306_CLEAR_COMMAND_REG(part); + return; + default: + printf ("SSD1306: WARNING: unknown/not implemented command %x\n", part->spi_data); + // Unknown command + return; + } +} + +/* + * Multi-byte command setting + */ +void +ssd1306_update_setting (ssd1306_t *part) +{ + switch (part->command_register) + { + case SSD1306_VIRT_SET_CONTRAST: + part->contrast_register = part->spi_data; + ssd1306_set_flag (part, SSD1306_FLAG_DIRTY, 1); + SSD1306_CLEAR_COMMAND_REG(part); + //printf ("SSD1306: CONTRAST SET: 0x%02x\n", part->contrast_register); + return; + case SSD1306_VIRT_SET_PAGE_ADDR: + switch (--part->reg_write_sz) { + case 1: + part->cursor.page = part->spi_data; + break; + case 0: + //TODO handle virtual page end + SSD1306_CLEAR_COMMAND_REG(part); + } + return; + + case SSD1306_VIRT_SET_COL_ADDR: + switch (--part->reg_write_sz) { + case 1: + part->cursor.column = part->spi_data; + break; + case 0: + //TODO handle virtual col end + SSD1306_CLEAR_COMMAND_REG(part); + } + return; + case SSD1306_VIRT_MEM_ADDRESSING: + if (part->spi_data > SSD1306_ADDR_MODE_PAGE) + printf ("SSD1306: error ADDRESSING_MODE invalid value %x\n", part->spi_data); + part->addr_mode = part->spi_data; + //printf ("SSD1306: ADDRESSING MODE: 0x%02x\n", part->addr_mode); + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SET_LINE: + case SSD1306_VIRT_SET_RATIO_OSC: + case SSD1306_VIRT_MULTIPLEX: + case SSD1306_VIRT_SET_OFFSET: + case SSD1306_VIRT_SET_CHARGE: + case SSD1306_VIRT_SET_VCOM: + case SSD1306_VIRT_SET_PADS: + case SSD1306_VIRT_CHARGE_PUMP: + SSD1306_CLEAR_COMMAND_REG(part); + return; + case SSD1306_VIRT_SCROLL_RIGHT: + case SSD1306_VIRT_SCROLL_LEFT: + case SSD1306_VIRT_SCROLL_VR: + case SSD1306_VIRT_SCROLL_VL: + case SSD1306_VIRT_VERT_SCROLL_A: + if (! --part->reg_write_sz) + SSD1306_CLEAR_COMMAND_REG(part); + return; + default: + // Unknown command + printf("SSD1306: error: unknown update command %x\n",part->command_register); + return; + } +} + +/* + * Determines whether a new command has been sent, or + * whether we are in the process of setting a multi- + * byte command. + */ +static void +ssd1306_write_command (ssd1306_t *part) +{ + if (!part->command_register) + { + // Single byte or start of multi-byte command + ssd1306_update_command_register (part); + } else + { + // Multi-byte command setting + ssd1306_update_setting (part); + } +} + +/* + * Called when a TWI byte is sent + */ +static void +ssd1306_twi_hook (struct avr_irq_t * irq, uint32_t value, void * param) +{ + ssd1306_t * p = (ssd1306_t*) param; + avr_twi_msg_irq_t v; + v.u.v = value; + + if (v.u.twi.msg & TWI_COND_STOP) + p->twi_selected = 0; + + if (v.u.twi.msg & TWI_COND_START) { + p->twi_selected = 0; + p->twi_index = 0; + if (((v.u.twi.addr>>1) & SSD1306_I2C_ADDRESS_MASK) == SSD1306_I2C_ADDRESS) { + p->twi_selected = v.u.twi.addr; + avr_raise_irq(p->irq + IRQ_SSD1306_TWI_IN, + avr_twi_irq_msg(TWI_COND_ACK, p->twi_selected, 1)); + } + } + + if (p->twi_selected) { + if (v.u.twi.msg & TWI_COND_WRITE) { + avr_raise_irq(p->irq + IRQ_SSD1306_TWI_IN, + avr_twi_irq_msg(TWI_COND_ACK, p->twi_selected, 1)); + + if (p->twi_index == 0) { // control byte + if ((v.u.twi.data & (~(1<<6))) != 0) { + printf("%s COND_WRITE %x\n", __FUNCTION__, v.u.twi.data); + printf("%s ALERT: unhandled Co bit\n", __FUNCTION__); + abort(); + } + p->di_pin = v.u.twi.data ? SSD1306_VIRT_DATA : SSD1306_VIRT_INSTRUCTION; + } else { + p->spi_data = v.u.twi.data; + + switch (p->di_pin) + { + case SSD1306_VIRT_DATA: + ssd1306_write_data (p); + break; + case SSD1306_VIRT_INSTRUCTION: + ssd1306_write_command (p); + break; + default: + // Invalid value + break; + } + } + p->twi_index++; + } + + // SSD1306 doesn't support read on serial interfaces + // just return 0 + if (v.u.twi.msg & TWI_COND_READ) { + uint8_t data = 0; + avr_raise_irq(p->irq + IRQ_SSD1306_TWI_IN, + avr_twi_irq_msg(TWI_COND_READ, p->twi_selected, data)); + p->twi_index++; + } + } +} + +/* + * Called when a SPI byte is sent + */ +static void +ssd1306_spi_in_hook (struct avr_irq_t * irq, uint32_t value, void * param) +{ + ssd1306_t * part = (ssd1306_t*) param; + + // Chip select should be pulled low to enable + if (part->cs_pin) + return; + + part->spi_data = value & 0xFF; + + switch (part->di_pin) + { + case SSD1306_VIRT_DATA: + ssd1306_write_data (part); + break; + case SSD1306_VIRT_INSTRUCTION: + ssd1306_write_command (part); + break; + default: + // Invalid value + break; + } +} + +/* + * Called when chip select changes + */ +static void +ssd1306_cs_hook (struct avr_irq_t * irq, uint32_t value, void * param) +{ + ssd1306_t * p = (ssd1306_t*) param; + p->cs_pin = value & 0xFF; + //printf ("SSD1306: CHIP SELECT: 0x%02x\n", value); + +} + +/* + * Called when data/instruction changes + */ +static void +ssd1306_di_hook (struct avr_irq_t * irq, uint32_t value, void * param) +{ + ssd1306_t * part = (ssd1306_t*) param; + part->di_pin = value & 0xFF; + //printf ("SSD1306: DATA / INSTRUCTION: 0x%08x\n", value); +} + +/* + * Called when a RESET signal is sent + */ +static void +ssd1306_reset_hook (struct avr_irq_t * irq, uint32_t value, void * param) +{ + //printf ("SSD1306: RESET\n"); + ssd1306_t * part = (ssd1306_t*) param; + if (irq->value && !value) + { + // Falling edge + memset (part->vram, 0, part->rows * part->pages); + part->cursor.column = 0; + part->cursor.page = 0; + part->flags = 0; + part->command_register = 0x00; + part->contrast_register = 0x7F; + part->addr_mode = SSD1306_ADDR_MODE_PAGE; + ssd1306_set_flag (part, SSD1306_FLAG_COM_SCAN_NORMAL, 1); + ssd1306_set_flag (part, SSD1306_FLAG_SEGMENT_REMAP_0, 1); + } + +} + +static const char * irq_names[IRQ_SSD1306_COUNT] = +{ [IRQ_SSD1306_SPI_BYTE_IN] = "=ssd1306.SDIN", [IRQ_SSD1306_RESET + ] = "avr, AVR_IOCTL_SPI_GETIRQ(0), + SPI_IRQ_OUTPUT), + part->irq + IRQ_SSD1306_SPI_BYTE_IN); + + avr_connect_irq ( + avr_io_getirq (part->avr, + AVR_IOCTL_IOPORT_GETIRQ( + wiring->chip_select.port), + wiring->chip_select.pin), + part->irq + IRQ_SSD1306_ENABLE); + + avr_connect_irq ( + avr_io_getirq (part->avr, + AVR_IOCTL_IOPORT_GETIRQ( + wiring->data_instruction.port), + wiring->data_instruction.pin), + part->irq + IRQ_SSD1306_DATA_INSTRUCTION); + + avr_connect_irq ( + avr_io_getirq (part->avr, + AVR_IOCTL_IOPORT_GETIRQ( + wiring->reset.port), + wiring->reset.pin), + part->irq + IRQ_SSD1306_RESET); +} + +void +ssd1306_connect_twi (ssd1306_t * part, ssd1306_wiring_t * wiring) +{ + avr_connect_irq ( + avr_io_getirq (part->avr, AVR_IOCTL_TWI_GETIRQ(0), TWI_IRQ_OUTPUT), + part->irq + IRQ_SSD1306_TWI_OUT); + + avr_connect_irq ( + part->irq + IRQ_SSD1306_TWI_IN, + avr_io_getirq (part->avr, AVR_IOCTL_TWI_GETIRQ(0), TWI_IRQ_INPUT)); + + avr_connect_irq ( + avr_io_getirq (part->avr, + AVR_IOCTL_IOPORT_GETIRQ( + wiring->reset.port), + wiring->reset.pin), + part->irq + IRQ_SSD1306_RESET); +} + +void +ssd1306_init (struct avr_t *avr, struct ssd1306_t * part, int width, int height) +{ + if (!avr || !part) + return; + + memset (part, 0, sizeof(*part)); + part->avr = avr; + part->columns = width; + part->rows = height; + part->pages = height / 8; // 8 pixels per page + + /* + * Register callbacks on all our IRQs + */ + part->irq = avr_alloc_irq (&avr->irq_pool, 0, IRQ_SSD1306_COUNT, + irq_names); + + avr_irq_register_notify (part->irq + IRQ_SSD1306_SPI_BYTE_IN, + ssd1306_spi_in_hook, part); + avr_irq_register_notify (part->irq + IRQ_SSD1306_RESET, + ssd1306_reset_hook, part); + avr_irq_register_notify (part->irq + IRQ_SSD1306_ENABLE, + ssd1306_cs_hook, part); + avr_irq_register_notify (part->irq + IRQ_SSD1306_DATA_INSTRUCTION, + ssd1306_di_hook, part); + avr_irq_register_notify (part->irq + IRQ_SSD1306_TWI_OUT, + ssd1306_twi_hook, part); + + printf ("SSD1306: %duS is %d cycles for your AVR\n", 37, + (int) avr_usec_to_cycles (avr, 37)); + printf ("SSD1306: %duS is %d cycles for your AVR\n", 1, + (int) avr_usec_to_cycles (avr, 1)); +} diff --git a/test/sim_display/ssd1306_virt.h b/test/sim_display/ssd1306_virt.h new file mode 100644 index 0000000..fdf8103 --- /dev/null +++ b/test/sim_display/ssd1306_virt.h @@ -0,0 +1,204 @@ +/* + ssd1306_virt.h + + Copyright 2011 Michel Pollet + Copyright 2014 Doug Szumski + + This file is part of simavr. + + simavr is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simavr is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simavr. If not, see . + */ + +/* + * This "Part" simulates the SSD1306 OLED display driver. + * + * The following functions are currently supported: + * + * > Display reset + * > Display on / suspend + * > Setting of the contrast + * > Inversion of the display + * > Rotation of the display + * > Writing to the VRAM using horizontal addressing mode + * + * It has been tested on a "JY MCU v1.5 OLED" in 4 wire SPI mode + * with the E/RD and R/W lines hard wired low as per the datasheet. + * + */ + +#ifndef __SSD1306_VIRT_H__ +#define __SSD1306_VIRT_H__ + +#include "sim_irq.h" + +#define SSD1306_VIRT_DATA 1 +#define SSD1306_VIRT_INSTRUCTION 0 + +#define SSD1306_I2C_ADDRESS 0x3C +#define SSD1306_I2C_ADDRESS_MASK 0xfe + +#define SSD1306_VIRT_PAGES 8 +#define SSD1306_VIRT_COLUMNS 128 + +/* Fundamental commands. */ +#define SSD1306_VIRT_SET_CONTRAST 0x81 +#define SSD1306_VIRT_RESUME_TO_RAM_CONTENT 0xA4 +#define SSD1306_VIRT_IGNORE_RAM_CONTENT 0xA5 +#define SSD1306_VIRT_DISP_NORMAL 0xA6 +#define SSD1306_VIRT_DISP_INVERTED 0xA7 +#define SSD1306_VIRT_DISP_SUSPEND 0xAE +#define SSD1306_VIRT_DISP_ON 0xAF + +/* Scrolling commands */ +#define SSD1306_VIRT_SCROLL_RIGHT 0x26 +#define SSD1306_VIRT_SCROLL_LEFT 0x27 +#define SSD1306_VIRT_SCROLL_VR 0x29 +#define SSD1306_VIRT_SCROLL_VL 0x2A +#define SSD1306_VIRT_SCROLL_OFF 0x2E +#define SSD1306_VIRT_SCROLL_ON 0x2F +#define SSD1306_VIRT_VERT_SCROLL_A 0xA3 + +/* Address setting commands */ +#define SSD1306_VIRT_SET_COLUMN_LOW_NIBBLE 0x00 +#define SSD1306_VIRT_SET_COLUMN_HIGH_NIBBLE 0x10 +#define SSD1306_VIRT_MEM_ADDRESSING 0x20 +#define SSD1306_VIRT_SET_COL_ADDR 0x21 +#define SSD1306_VIRT_SET_PAGE_ADDR 0x22 +#define SSD1306_VIRT_SET_PAGE_START_ADDR 0xB0 + +/* Hardware config. commands */ +#define SSD1306_VIRT_SET_LINE 0x40 +#define SSD1306_VIRT_SET_SEG_REMAP_0 0xA0 +#define SSD1306_VIRT_SET_SEG_REMAP_127 0xA1 +#define SSD1306_VIRT_MULTIPLEX 0xA8 +#define SSD1306_VIRT_SET_COM_SCAN_NORMAL 0xC0 +#define SSD1306_VIRT_SET_COM_SCAN_INVERTED 0xC8 +#define SSD1306_VIRT_SET_OFFSET 0xD3 +#define SSD1306_VIRT_SET_PADS 0xDA + +/* Timing & driving scheme setting commands */ +#define SSD1306_VIRT_SET_RATIO_OSC 0xD5 +#define SSD1306_VIRT_SET_CHARGE 0xD9 +#define SSD1306_VIRT_SET_VCOM 0xDB +#define SSD1306_VIRT_NOP 0xE3 + +/* Charge pump command table */ +#define SSD1306_VIRT_CHARGE_PUMP 0x8D +#define SSD1306_VIRT_PUMP_ON 0x14 + +#define SSD1306_CLEAR_COMMAND_REG(part) part->command_register = 0x00 + +enum +{ + //IRQ_SSD1306_ALL = 0, + IRQ_SSD1306_SPI_BYTE_IN, + IRQ_SSD1306_ENABLE, + IRQ_SSD1306_RESET, + IRQ_SSD1306_DATA_INSTRUCTION, + //IRQ_SSD1306_INPUT_COUNT, + IRQ_SSD1306_ADDR, // << For VCD + IRQ_SSD1306_TWI_IN, + IRQ_SSD1306_TWI_OUT, + IRQ_SSD1306_COUNT +//TODO: Add IRQs for VCD: Internal state etc. +}; + +enum +{ + SSD1306_FLAG_DISPLAY_INVERTED = 0, + SSD1306_FLAG_DISPLAY_ON, + SSD1306_FLAG_SEGMENT_REMAP_0, + SSD1306_FLAG_COM_SCAN_NORMAL, + + /* + * Internal flags, not SSD1306 + */ + SSD1306_FLAG_BUSY, // 1: Busy between instruction, 0: ready + SSD1306_FLAG_REENTRANT, // 1: Do not update pins + SSD1306_FLAG_DIRTY, // 1: Needs redisplay... +}; + +enum ssd1306_addressing_mode_t +{ + SSD1306_ADDR_MODE_HORZ = 0, + SSD1306_ADDR_MODE_VERT, + SSD1306_ADDR_MODE_PAGE +}; + +/* + * Cursor position in VRAM + */ +struct ssd1306_virt_cursor_t +{ + uint8_t page; + uint8_t column; +}; + +typedef struct ssd1306_t +{ + avr_irq_t * irq; + struct avr_t * avr; + uint8_t columns, rows, pages; + struct ssd1306_virt_cursor_t cursor; + uint8_t vram[SSD1306_VIRT_PAGES][SSD1306_VIRT_COLUMNS]; + uint16_t flags; + uint8_t command_register; + uint8_t contrast_register; + uint8_t cs_pin; + uint8_t di_pin; + uint8_t spi_data; + uint8_t reg_write_sz; + enum ssd1306_addressing_mode_t addr_mode; + + uint8_t twi_selected; + uint8_t twi_index; +} ssd1306_t; + +typedef struct ssd1306_pin_t +{ + char port; + uint8_t pin; +} ssd1306_pin_t; + +typedef struct ssd1306_wiring_t +{ + ssd1306_pin_t chip_select; + ssd1306_pin_t data_instruction; + ssd1306_pin_t reset; +} ssd1306_wiring_t; + +void +ssd1306_init (struct avr_t *avr, struct ssd1306_t * b, int width, int height); + +static inline int +ssd1306_set_flag (ssd1306_t *b, uint16_t bit, int val) +{ + int old = b->flags & (1 << bit); + b->flags = (b->flags & ~(1 << bit)) | (val ? (1 << bit) : 0); + return old != 0; +} + +static inline int +ssd1306_get_flag (ssd1306_t *b, uint16_t bit) +{ + return (b->flags & (1 << bit)) != 0; +} + +void +ssd1306_connect (ssd1306_t * part, ssd1306_wiring_t * wiring); + +void +ssd1306_connect_twi (ssd1306_t * part, ssd1306_wiring_t * wiring); + +#endif diff --git a/test/uno-display.txt b/test/uno-display.txt new file mode 100644 index 0000000..824998d --- /dev/null +++ b/test/uno-display.txt @@ -0,0 +1,64 @@ +................................................................................................................................ +.........##.....###...##....###................................................................#...#.#.#..###................... +.........##..#.#...#..##...#...#..............................................................#.#..#.#.#.#...#.................. +............#..#..##.......#..##.............................................................#...#.#.#.#.#..##.................. +...........#...#.#.#.......#.#.#.............................................................#...#.#.#.#.#.#.#.#####............ +..........#....##..#.......##..#.............................................................#...#..#.##.##..#.................. +.........#..##.#...#.......#...#.............................................................#...#.......#...#.................. +............##..###.........###..............................................................#...#........###................... +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +.................###..............................#########.........#########.........######............#########............... +.................###..............................#########.........#########.........######............#########............... +.................###..............................#########.........#########.........######............#########............... +..............###...###........................###.........###...###.........###......######.........###.........###............ +..............###...###........................###.........###...###.........###......######.........###.........###............ +..............###...###........................###.........###...###.........###......######.........###.........###............ +...........###.........###.....................###......######...###......######.....................###......######............ +...........###.........###.....................###......######...###......######.....................###......######............ +...........###.........###.....................###......######...###......######.....................###......######............ +...........###.........###.....................###...###...###...###...###...###.....................###...###...###............ +...........###.........###.....................###...###...###...###...###...###.....................###...###...###............ +...........###.........###.....................###...###...###...###...###...###.....................###...###...###............ +...........###.........###.....................######......###...######......###.....................######......###............ +...........###.........###.....................######......###...######......###.....................######......###............ +...........###.........###.....................######......###...######......###.....................######......###............ +...........###.........###.....................###.........###...###.........###.....................###.........###............ +...........###.........###.....................###.........###...###.........###.....................###.........###............ +...........###.........###.....................###.........###...###.........###.....................###.........###............ +...........###.........###........................#########.........#########...........................#########............... +...........###.........###........................#########.........#########...........................#########............... +...........###.........###........................#########.........#########...........................#########............... +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................ +................................................................................................................................