PX-42F App Manual

Writing native games and apps for the PX-42F calculator. Framework API version 4.

1. What an app is

An app is a program for the PX-42F itself, written in C. It's a single .bin file on the calculator's drive, in /BIN/GAMES or /BIN/APPS, and it starts from Settings → Games & Apps. While it runs it has the whole calculator: the screen, the keyboard, the speaker, the clock, the files on the drive, the USB serial port, the infrared LED and the battery.

The template app: Hello!, last key, run number, a bouncing dot
The template app, HELLO.bin.
Snake: the snake and an apple, score and best in the status bar
Snake (PX-42F-Snake).
IR Remote: a map of the keyboard with each key's label
IR Remote (PX-42F-IR-Remote).

An app talks to the calculator only through one table of functions, px. Its definition is px42f/px_app.h, the contract between the firmware and every app. This manual explains each function in it. The rest of the template project is the build setup, a runtime, and a simulator that runs your app on your computer.

2. Quick start

What you need

Make a new app

  1. Copy the PX-42F-App-Template folder and give the copy your app's name, for example PX-42F-Tetris. Open it in VS Code.
  2. In the Makefile, set two things:
    • NAME: the file name, which is also the name in the menu. Use up to 28 letters, digits, _ or -.
    • KIND: GAMES or APPS, the folder it goes in.
  3. Write your app in src/app.c. You can add more .c and .h files in src/; they're all built.
  4. Build it: ⇧⌘B in VS Code, or make. The result is build/NAME.bin.
  5. Try it on your computer: the Simulate task, or make sim. Screenshots are saved in build/sim-out.
  6. Put it on the calculator:
    1. On the calculator, open Settings → USB Drive → Connect to Computer.
    2. Run the Install on the PX-42F task, or make install. It copies the file to BIN/GAMES or BIN/APPS on the drive named PX-42F.
    3. Eject the drive on the computer, then press EXIT on the calculator.
  7. Start it: Settings → Games & Apps → Games (or Apps), choose it, and press ENTER.

The project

PathWhat it is
src/Your app. The template has one file, app.c.
px42f/px_app.hThe contract with the firmware: every function and constant. It's copied from the firmware; don't edit it.
px42f/px.hWhat your files include: px_app.h, stdbool.h, stdint.h, the table px and app_main().
px42f/runtime.cThe .bin's header and entry, the memory malloc() uses, and printf() to the console.
px42f/app.ldThe linker script: places the app at 0x20060000, where the calculator loads it.
px42f/pxbin.pyTurns the linked program into the .bin: fills in its size and checksum.
sim/The simulator (sim.c), the calculator's fonts and PNG writer, the live simulator (live.py, live.html), and drive/, the simulated drive.
MakefileNAME, KIND, SIM_SCRIPT, and the targets all, sim, install and clean.
.vscode/The VS Code tasks (Build, Simulate, Install on the PX-42F, Clean) and IntelliSense set up for the ARM compiler.

3. Your first app

Every app includes px.h and defines app_main(). The firmware calls it with the table px already set. This app shows a greeting and waits until you leave:

#include "px.h"

int app_main(void) {
    px->clear();                                        /* a white screen */
    px->text(4, 4, "Hello, PX-42F!", PX_FONT_LARGE, 0); /* black text at (4, 4) */
    px->show();                                         /* send it to the panel */

    while (px->key(-1) != PX_KEY_QUIT)                  /* wait, asleep, for ON */
        ;
    return PX_EXIT_MENU;                                /* back to the menu */
}

Three things to notice:

4. How an app runs

Starting

When you choose an app, the calculator:

  1. Reads the whole .bin and checks its header and checksum. A file that fails gets a message (section 19) and nothing else happens.
  2. Saves the calculator's memory (the Plus42 stack, registers and programs) to the drive.
  3. Copies the app into RAM at 0x20060000, sets its variables to zero, and calls app_main() on processor core 1.

When the app returns, the calculator takes its memory back and goes to the menu, or turns off.

Rules every app follows

RuleWhy
Wait in px->key() or px->sleep(), never in a busy loop.The calculator sleeps while waiting there, which keeps the battery current low. And if 100 ms pass without any call into px, the watchdog restarts the calculator, assuming the app has hung.
Return when px->key() gives PX_KEY_QUIT.That's ON, the Sleep Timer running out, or an empty battery. After a quit, every further call to px->key() returns PX_KEY_QUIT straight away. An app with a way out of its own can take ON as an ordinary key instead: on_key(), below.
Keep within 128 KB.Code, constants, variables and the malloc() heap share 128 KB of RAM. make prints how much is left for the heap.
Keep big arrays static.The stack is the firmware's: 32 KB, part of it already in use. A large local array can overflow it.
Save to files rarely.Files are in flash memory, which wears out with writing. Save a high score at the end of a game, not every frame.

If an app crashes

The calculator's memory was saved to the drive before the app started, so nothing is lost. A crash or a hang restarts the calculator. The start screen then shows where it stopped: see Debugging.

Speed and battery

The processor runs at 150 MHz while the app works and at 48 MHz while it waits in px->key() or px->sleep(), asleep between keys and timeouts. A game that waits there for each frame uses little more than the calculator idling. A loop that never waits uses about six times as much, and trips the watchdog anyway.

5. The screen

The screen is 250 × 122 pixels, black and white. (0, 0) is the top-left corner, x goes right and y goes down. Everything you draw goes into a picture in memory; px->show() sends it to the panel. Draw a whole frame, then show it once.

Text in the three fonts, inverse text, a line, filled rectangles, two bitmaps and dotted pixels
Text in the three fonts (with HP-42S symbols), lines, filled rectangles, bitmaps, pixels and inverse text.
ConstantValueMeaning
PX_SCREEN_W250Width in pixels.
PX_SCREEN_H122Height in pixels.
PX_SCREEN_STRIDE32Bytes in a row of px->screen.

Every drawing function leaves out whatever falls off the screen, so you can draw at any coordinates.

void show(void);

Sends the picture to the panel. It takes about 10 ms. Nothing you draw appears until you call it.

void clear(void);

Makes the whole picture white.

void pixel(int x, int y, int black);

Sets one pixel: black if black isn't 0, white if it is.

int get_pixel(int x, int y);

Returns 1 if the pixel is black, 0 if it's white or off the screen. Useful for collision tests against what's drawn.

void fill(int x, int y, int w, int h, int black);

Fills a w × h rectangle whose top-left corner is (x, y), black or white. A white fill erases. For a frame, fill black, then fill a smaller white rectangle inside it.

px->fill(0, 0, PX_SCREEN_W, 12, 1);   /* a black title bar */
px->fill(40, 30, 50, 20, 1);          /* a box with a 2-pixel border */
px->fill(42, 32, 46, 16, 0);
void line(int x0, int y0, int x1, int y1, int black);

Draws a 1-pixel line from (x0, y0) to (x1, y1), both ends included.

void bitmap(int x, int y, int w, int h, const uint8_t *bits, int stride);

Draws a 1-bit picture with its top-left corner at (x, y). The picture's 1 bits become black pixels. Its 0 bits leave the screen as it is, so a sprite's background is transparent: clear the area first for a solid one.

bits
h rows of stride bytes each. In each byte the top bit is the leftmost pixel.
stride
Bytes per row: at least (w + 7) / 8.
/* An apple, 5 x 5: one byte a row, the pixels in the top 5 bits. */
static const uint8_t apple[5] = {0x20, 0x70, 0xF8, 0xF8, 0x70};
px->bitmap(100, 50, 5, 5, apple, 1);
int text(int x, int y, const char *s, int font, int inverse);

Draws the text s with its top-left corner at (x, y), in one of the six fonts (next section). Each character fills its whole cell: with inverse 0 it's black on white, otherwise white on black. So text draws over whatever was there, without clearing first.

returns
The x just after the text, to continue on the same line.
int x = px->text(2, 20, "Score: ", PX_FONT_SMALL, 0);
px->text(x, 20, "42", PX_FONT_SMALL, 1);           /* the number inverted */
int text_width(const char *s, int font);
int font_height(int font);

The width text() would take for s, and a font's height. The fonts are monospaced, so the width is the character count times the font's advance. Use them to centre or right-align:

const char *t = "GAME OVER";
px->text((PX_SCREEN_W - px->text_width(t, PX_FONT_LARGE)) / 2, 50, t, PX_FONT_LARGE, 0);
uint8_t *screen;

The picture itself, for drawing faster than pixel by pixel: PX_SCREEN_H rows of PX_SCREEN_STRIDE bytes, 1 bit a pixel, 1 = black. In each byte the top bit is the leftmost pixel. Bits past x = 249 in each row are ignored.

/* Invert the whole screen. */
for (int i = 0; i < PX_SCREEN_H * PX_SCREEN_STRIDE; i++)
    px->screen[i] ^= 0xFF;
px->show();

6. Fonts and characters

FontCharacter cellAdvanceCharacters a line
PX_FONT_LARGE10 × 1611 pixels22
PX_FONT_SMALL8 × 128 pixels31
PX_FONT_MINI6 × 86 pixels41
PX_FONT_CHOSEN_LARGE, _SMALL, _MINIThe same sizes, in the fonts chosen in Settings → Fonts: the calculator screen's own fonts, which may be fonts the owner made. Use them to match the calculator's look.

The built-in fonts are always the same, so a layout drawn with them looks as you designed it on every calculator.

Character codes

Codes 32–126 are ASCII, except 94, which is ↑ (the HP-42S has no ^ there; ^ is code 30). Codes 0–31 and 127 are the HP-42S's symbols. Write them in C strings as \x escapes:

Code0123456789ABCDEF
0x00÷×√∫▒Σ▶π¿≤LF≥≠↵↓→
0x10←µ£°ÅÑÄ∡ᴇÆ…EscÖÜ^•
0x7F⊢ (the HP-42S's append sign)
A C trap. "\x05A" is one character, code 0x5A, because a \x escape takes every hex digit that follows it. End the string and start a new one after the escape: "\x05" "A". C joins the two pieces into one string.
px->text(2, 2, "\x05+ turns left", PX_FONT_SMALL, 0);   /* Σ+ turns left  ("+" isn't a hex digit) */
px->text(2, 16, "20\x13" "C", PX_FONT_SMALL, 0);        /* 20°C */
px->text(2, 30, "\x07r\x1e" "2", PX_FONT_SMALL, 0);     /* πr^2 */

The font maps

Every character of the built-in fonts. A character's code is its row (00 to 70, in hexadecimal) plus its column (0 to F).

Map of the large font, 10 by 16
PX_FONT_LARGE, 10 × 16.
Map of the small font, 8 by 12
PX_FONT_SMALL, 8 × 12.
Map of the mini font, 6 by 8
PX_FONT_MINI, 6 × 8.

7. The keyboard

Keys are named by where they sit: PX_KEY(row, col), row 0–6 from the top and column 0–5 from the left. The top two rows have 6 keys and the others 5. ENTER is PX_KEY(2, 0). The same places name keys in the IR Remote's files (R0C5 is XEQ).

Orange: the name after PX_KEY_ in px_app.h, for example PX_KEY_ENTER.

int key(int wait_ms);

Returns the next keyboard event, waiting at most wait_ms milliseconds for one. While it waits, the calculator sleeps.

wait_ms
0 to look without waiting, a positive number of milliseconds, or any negative number to wait as long as it takes.
returns
One of:
  • PX_KEY(row, col): a key went down.
  • PX_KEY(row, col) | PX_KEY_RELEASED: a key was let go.
  • PX_KEY_NONE (−1): the wait ran out.
  • PX_KEY_QUIT (−2): the app must return now.

Each call returns one event. When several keys change together, each comes out on its own call, straight away, without waiting. Two keys can be down at once, and each gets its own down and up.

Pressing ON makes key() return PX_KEY_QUIT; held for 2 seconds, it also turns the calculator off after the app returns. The Sleep Timer and an empty battery give PX_KEY_QUIT too, and turn it off. A computer connected over USB keeps the Sleep Timer from running out, as everywhere on the calculator.

MacroGives
PX_KEY(row, col)The key at that place.
PX_KEY_ROW(k), PX_KEY_COL(k)The row and column of a key event, with or without PX_KEY_RELEASED.
k & PX_KEY_RELEASEDNot 0 if the event is a key being let go.
PX_KEY_SIGMA, PX_KEY_XEQ, PX_KEY_ENTER, PX_KEY_BACKSPACE, PX_KEY_ARROW_UP, PX_KEY_ARROW_DN, PX_KEY_SHIFT, PX_KEY_RSNames for common keys (orange in the map).
void on_key(int on_is_key);    /* API version 3 */

With 1, a short press of ON comes to key() as PX_KEY_ON, down and then up, instead of ending the app. The app then needs its own way out: for example, asking "Leave?" and returning. PX-BASIC does this. Held for 2 seconds, ON still turns the calculator off, with PX_KEY_QUIT, and the Sleep Timer and an empty battery still end the app. Every app starts with 0.

Waiting for a key

/* The next key pressed (releases skipped), or PX_KEY_QUIT. */
static int wait_key(void) {
    while (1) {
        int k = px->key(-1);
        if (k == PX_KEY_QUIT || !(k & PX_KEY_RELEASED))
            return k;
    }
}

Keys that are held down

Track down and up events to know what's held, for example to move while a key is held:

static bool held[64];                  /* by PX_KEY(row, col) */

int k = px->key(0);
if (k >= 0)
    held[k & 63] = !(k & PX_KEY_RELEASED);
if (held[PX_KEY(4, 1)]) x--;           /* 4 held: left */
if (held[PX_KEY(4, 3)]) x++;           /* 6 held: right */

8. Time and random numbers

uint32_t millis(void);

Milliseconds since the calculator started. It wraps around after 49 days, so compare times by subtracting: (int32_t) (t - px->millis()) > 0 means t hasn't come yet.

void sleep(int ms);

Waits ms milliseconds, asleep, as key() does. It doesn't take key events: they wait for the next key(). A key pressed and let go during the sleep, though, is missed. It returns early if the Sleep Timer or an empty battery ends the app; the next key() then returns PX_KEY_QUIT.

void datetime(px_datetime_t *now);

The calculator's clock, the one set in Settings → Date & Time.

typedef struct {
    int32_t year;    /* 2026 */
    int32_t month;   /* 1-12 */
    int32_t day;     /* 1-31 */
    int32_t hour;    /* 0-23 */
    int32_t min;     /* 0-59 */
    int32_t sec;     /* 0-59 */
    int32_t ms;      /* 0-999 */
    int32_t weekday; /* 0 = Sunday ... 6 = Saturday */
} px_datetime_t;

px_datetime_t now;
px->datetime(&now);
char line[24];
snprintf(line, sizeof(line), "%02ld:%02ld:%02ld", (long) now.hour, (long) now.min, (long) now.sec);
uint32_t random(void);

A random 32-bit number. Each run of the app gets a different sequence, seeded from the clock. For a number from 0 to n − 1: px->random() % n.

9. Sound

The PX-42F has a small speaker, and plays one sound at a time: starting a sound stops the one playing. The calculator's second processor plays it, so a sound can play while the app carries on:

A sound still playing when the app ends is stopped. While a sound plays, the processor stays at full speed (the speaker's timing comes from its clock), so waiting in key() then saves less battery.

void tone(int tone);

Plays one of the calculator's sounds:

0 – 9
The HP-42S's TONE 0 to TONE 9, low to high: the files /SOUNDS/TONES/TONE0.wav to TONE9.wav.
10
The error sound, /SOUNDS/ERROR.wav.

The owner can replace these files, so they may not sound the same on every calculator.

int play(const char *path);

Plays a WAV file from the drive: PCM (8, 16, 24 or 32-bit) or 32-bit float, 1 to 8 channels, 4 to 192 kHz. The calculator mixes it to mono, resamples it to 32 kHz and normalizes its loudness, as for its own sounds.

path
See paths: "JUMP.wav" in the app's folder, or "/SOUNDS/ON.wav" from the top of the drive.
returns
0 when it has played, or −1 if the file can't be opened or isn't a WAV it can play.
px->play("/SOUNDS/ON.wav");     /* the calculator's turn-on sound */
px->play("COIN.wav");           /* /BIN/GAMES/MYGAME/COIN.wav */

play() holds the app until the sound ends: for a long WAV, use play_async(). Before a sound starts, the calculator reads its whole file once to set its loudness, so even play_async() takes a moment for a long file.

void tone_async(int tone);
int play_async(const char *path);

The same as tone() and play(), but they return as soon as the sound starts. play_async() returns 0, or −1 if the file can't be played.

if (ate_apple)
    px->tone_async(9);         /* a short high tone; the next frame doesn't wait */
int sound_playing(void);
void sound_stop(void);

sound_playing() returns 1 while a sound plays. sound_stop() stops it at once.

px->play_async("MUSIC.wav");
while (px->sound_playing() && px->key(100) != PX_KEY_QUIT)
    ;                          /* music until it ends or ON */
px->sound_stop();
int midi_play(const char *path);    /* API version 4 */
void midi_status(px_midi_status_t *status);

Plays a Standard MIDI File (.mid, format 0 or 1, up to 128 KB) on the calculator's own synthesizer: 24 notes at once on 16 channels. Channel 10 is the drum kit. It returns at once, like play_async(); sound_playing() says when the song is over and sound_stop() stops it. Starting any sound stops the song.

The songs live in /MIDI on the drive. The synthesizer picks one of its instruments (piano, bell, organ, plucked, bass, strings, brass, reed, flute, lead, pad) from each channel's General MIDI program. It follows note on and off, volume, expression, modulation, the sustain pedal and pitch bend (±2 semitones), and ignores the rest.

path
See paths.
returns
0, or PX_MIDI_NOT_FOUND (−1: no such file), PX_MIDI_NOT_MIDI (−2), PX_MIDI_TOO_BIG (−3) or PX_MIDI_NO_MEMORY (−4).
status
While a song plays: length_ms (to its last event), elapsed_ms, channels (bit n set while a note sounds on channel n+1) and voices (notes sounding). All 0 when no song plays.
if (px->midi_play("/MIDI/Canon.mid") == 0) {
    px_midi_status_t st;
    while (px->sound_playing() && px->key(100) != PX_KEY_QUIT) {
        px->midi_status(&st);
        /* draw st.elapsed_ms / st.length_ms */
    }
    px->sound_stop();
}

10. Files on the drive

The calculator's drive is a FAT disk in its flash memory, 12 MB. A computer sees the same files in USB Drive mode, so an app can read files you copy there (levels, pictures, sounds, data) and write files you can copy off.

Paths

Whole files

int load(const char *path, void *data, int size);

Reads a file into data, at most size bytes.

returns
The bytes read, or −1 if the file can't be opened. If it returns exactly size, the file may be longer.
int save(const char *path, const void *data, int size);

Writes size bytes as the whole file, replacing it if it exists, and makes sure they're in flash before returning.

returns
0, or −1 if it couldn't be written: the drive is full, or the folder doesn't exist.
/* A best score, kept as text so it can be read on a computer. */
static int load_best(void) {
    char text[12] = "";
    return px->load("BEST", text, sizeof(text) - 1) > 0 ? atoi(text) : 0;
}

static void save_best(int best) {
    char text[12];
    int n = snprintf(text, sizeof(text), "%d\n", best);
    px->save("BEST", text, n);
}

Reading and writing in pieces

int open(const char *path, const char *mode);
int read(int file, void *data, int size);
int write(int file, const void *data, int size);
int close(int file);

For files too big to hold whole, or written bit by bit.

open
mode "r" to read, or "w" to write a new, empty file (an old one is replaced). Returns a file number for the other three calls, or −1. At most PX_FILES_OPEN (4) files are open at once.
read
Up to size bytes: returns how many were read, 0 at the end of the file, −1 on an error.
write
Returns how many bytes were written, or −1. Only for files opened with "w".
close
Writes what's left to flash. Returns 0, or −1 if what was written is lost. Files still open when the app ends are closed for it.
/* Copy a log line by line to the console. */
int f = px->open("/LOG.TXT", "r");
char buf[256];
int n;
while (f >= 0 && (n = px->read(f, buf, sizeof(buf))) > 0)
    px->serial_write(buf, n);
if (f >= 0)
    px->close(f);

Folders

int list(const char *dir, const char *ext, char (*names)[PX_FILE_NAME_MAX + 1], int max);

The files in folder dir whose names end in ext, without it, sorted alphabetically. Folders aren't listed.

dir
"" for the app's folder, or any path.
ext
The ending to match, such as ".txt", not case-sensitive. "" lists every file.
names
Room for max names of up to PX_FILE_NAME_MAX (28) characters. Longer names are skipped.
returns
How many names it wrote, at most max.
static char levels[20][PX_FILE_NAME_MAX + 1];
int count = px->list("", ".lvl", levels, 20);    /* "1", "2", ... for 1.lvl, 2.lvl */
int mkdir(const char *path);
int remove(const char *path);

mkdir makes a folder; it's fine if it's already there. remove deletes a file or an empty folder. Both return 0, or −1.

Flash wears out. Every write ends up in flash memory, which takes on the order of 100,000 writes to each part before it fails. Save on purpose (a game over, a setting changed), never in a loop.

11. The USB serial console

Connected to a computer by USB, the calculator shows up as two serial ports. The first, "PX-42F Console", belongs to the app while it runs.

int serial_write(const void *data, int size);
int serial_read(void *data, int size);

serial_write sends bytes to the console and returns size. Without a computer listening, nothing is sent and it doesn't wait. serial_read returns what has arrived, at most size bytes; 0 if nothing has, without waiting.

printf() works too. The runtime sends stdout to the console a line at a time, so end lines with \n. The C library is newlib-nano: %f isn't supported by default. Print a float as two integers, or link with -u _printf_float (about 10 KB more).

printf("level %d, score %d\n", level, score);

To watch it on a Mac, open the first of its two ports in a terminal (ls /dev/cu.usbmodem* lists them): screen /dev/cu.usbmodem2101 115200, with your port's name. Leave with Ctrl-A then K. On Windows, use PuTTY or the Arduino IDE's serial monitor on the console's COM port.

12. Infrared

The PX-42F has an infrared LED on its top edge, the kind TV remotes use. It's driven by hardware, so an app can send any remote-control code with exact timing.

void ir_send(const px_ir_burst_t *burst);

Sends one transmission and returns when it's out. If the last one ended less than us[0] ago, it first waits until it did.

typedef struct {
    uint32_t carrier_hz;              /* the blinking while "on": 10000-100000, usually 38000 */
    int32_t count;                    /* how many times in us[]: 2-112 */
    uint32_t us[PX_IR_MAX_TIMINGS];   /* microseconds, alternately off and on */
} px_ir_burst_t;

us[0] is the gap before the transmission, counted from the end of the last one; the LED is off. Then us[1] is the first pulse (on), us[2] a space (off), and so on, ending with a pulse. A burst with a count or carrier_hz out of range sends nothing.

/* NEC: address 0x04, command 0x08 (a TV's power, for example). */
static void nec_send(uint8_t address, uint8_t command) {
    static px_ir_burst_t b;
    uint8_t bytes[4] = {address, (uint8_t) ~address, command, (uint8_t) ~command};
    b.carrier_hz = 38000;
    b.count = 0;
    b.us[b.count++] = 110000;                  /* gap since the last one */
    b.us[b.count++] = 9000;                    /* leader: on 9 ms */
    b.us[b.count++] = 4500;                    /*         off 4.5 ms */
    for (int i = 0; i < 32; i++) {
        b.us[b.count++] = 560;                 /* each bit: on 560 us */
        b.us[b.count++] = (bytes[i / 8] >> (i % 8)) & 1 ? 1690 : 560;   /* off: 1 long, 0 short */
    }
    b.us[b.count++] = 560;                     /* the final pulse */
    px->ir_send(&b);
}

For the common protocols, use the encoders in the PX-42F-IR-Remote project: src/irproto.c makes these bursts for Flipper's 14 protocols (NEC, Samsung, Sony SIRC, RC5, RC6 and others), tested against the Flipper Zero's own.

The PX-42F has no infrared receiver, so it can send codes but not learn them.

13. Power

int battery_mv(void);

The battery's voltage in millivolts. A full Li-ion cell is about 4200 mV. The calculator turns itself off at about 3450 mV (and key() returns PX_KEY_QUIT first). While charging, the reading is the charger's, a little higher.

int usb_connected(void);

1 if a computer is connected over USB and using the calculator. The calculator is then charging, and the Sleep Timer doesn't run.

Turning off. Return PX_EXIT_OFF from app_main() to turn the calculator off when the app ends. The calculator also turns off after ON is held for 2 seconds, when the Sleep Timer runs out, or when the battery is empty: key() returns PX_KEY_QUIT for each, then the app returns.

14. Decimal numbers

The calculator computes with IEEE 754 decimal128 numbers, from Intel's decimal library, as Plus42 does: 34 significant digits, exponents up to ±6144, and decimal, so 0.1 + 0.2 is exactly 0.3. An app can use the same numbers and functions through the table, with no library of its own. They need firmware with API version 2.

typedef struct { uint32_t w[4]; } px_dec_t;   /* 16 bytes, aligned to 16 */

A number. Copy it, pass it and keep it in arrays like any struct; compute with it only through the functions below.

int dec(int op, px_dec_t *r, const px_dec_t *a, const px_dec_t *b);

One operation: r = a op b, or r = op(a) for the one-argument ones (b is then ignored, and can be NULL). r can be the same as a or b. Returns 0, or −1 for an operation the firmware doesn't have (r is left alone).

OperationsDo
PX_DEC_ADD SUB MUL DIVa + b, a − b, a × b, a / b
PX_DEC_POWa to the power b
PX_DEC_REMthe IEEE remainder: a − b·n, n = a/b rounded to the nearest whole number
PX_DEC_ATAN2, PX_DEC_HYPOTthe angle of the point (b, a); √(a² + b²)
PX_DEC_NEG ABS SQRT−a, |a|, √a
PX_DEC_EXP LN LOG10 EXPM1 LN1Pea, ln a, log10 a, ea − 1, ln(1 + a)
PX_DEC_SIN COS TAN ASIN ACOS ATANtrigonometry, in radians
PX_DEC_SINH COSH TANH ASINH ACOSH ATANHhyperbolic
PX_DEC_GAMMAΓ(a), which is (a − 1)! for whole numbers
PX_DEC_FLOOR CEIL TRUNCto a whole number: down, up, toward 0

A result too big is ±infinity, and one with no answer (√−1, 0/0) is NaN, "not a number". Check with dec_class() before using a result.

void dec_from_int(px_dec_t *r, int32_t v);
int dec_to_int(int32_t *r, const px_dec_t *a);
int dec_from_string(px_dec_t *r, const char *s);
void dec_to_string(char buf[PX_DEC_STRING_MAX], const px_dec_t *a);
dec_to_int
Truncates toward 0. Returns 0, or −1 if a is out of range or not a number.
dec_from_string
Reads "12", "-1.5", ".5", "2E-3". Returns 0, or −1 if it isn't a number (r is then NaN).
dec_to_string
The exact value, as the library writes it: a sign, every digit, then the exponent. "+15E-4" is 0.0015; others are "+Inf" and "+NaN". To show a number to a person, round and place the decimal point yourself: the Tiny BASIC project's format_number() does it, in src/basic.c.
int dec_cmp(const px_dec_t *a, const px_dec_t *b);
int dec_class(const px_dec_t *a);

dec_cmp returns −1 (a < b), 0 or 1, and 2 if either is NaN. dec_class returns PX_DEC_FINITE, PX_DEC_INF or PX_DEC_NAN.

/* The square root of 2, to 34 digits, on the console. */
px_dec_t two, root;
char s[PX_DEC_STRING_MAX];
px->dec_from_int(&two, 2);
px->dec(PX_DEC_SQRT, &root, &two, NULL);
px->dec_to_string(s, &root);
printf("%s\n", s);      /* +1414213562373095048801688724209698E-33 */
In the simulator a number is the computer's binary double, with about 16 digits, so results can differ from the calculator's in the last digits, and 0.1 + 0.2 isn't quite 0.3. Round when you print, and compare results with a tolerance in tests.

15. Recipes

A game loop

Draw a frame, then wait for the next one in key(), handling the keys that come meanwhile. The calculator sleeps between frames, and the frames keep time even when keys are pressed.

#define FRAME_MS 50   /* 20 frames a second */

/* The next key before time t, or PX_KEY_NONE at t. A frame that's late
 * doesn't wait at all: a negative wait would mean "forever". */
static int key_before(uint32_t t) {
    int32_t left = (int32_t) (t - px->millis());
    return px->key(left > 0 ? (int) left : 0);
}

int app_main(void) {
    uint32_t next = px->millis();
    while (1) {
        update();             /* move things */
        draw();               /* draw the frame, then px->show() */
        next += FRAME_MS;
        int k;
        while ((k = key_before(next)) != PX_KEY_NONE) {
            if (k == PX_KEY_QUIT)
                return PX_EXIT_MENU;
            if (!(k & PX_KEY_RELEASED))
                handle_key(k);
        }
    }
}

A menu

/* Up and down move, ENTER chooses. Returns the item, or -1 to quit. */
static int menu(const char *title, const char *const *items, int count) {
    int sel = 0;
    while (1) {
        px->clear();
        px->fill(0, 0, PX_SCREEN_W, 12, 1);
        px->text(2, 0, title, PX_FONT_SMALL, 1);
        for (int i = 0; i < count; i++) {
            if (i == sel)
                px->fill(0, 14 + i * 13, PX_SCREEN_W, 13, 1);
            px->text(4, 15 + i * 13, items[i], PX_FONT_SMALL, i == sel);
        }
        px->show();
        int k = px->key(-1);
        if (k == PX_KEY_QUIT)
            return -1;
        if (k == PX_KEY_ENTER)
            return sel;
        if (k == PX_KEY_ARROW_UP)
            sel = (sel + count - 1) % count;
        if (k == PX_KEY_ARROW_DN)
            sel = (sel + 1) % count;
    }
}

Reading a text file line by line

static char text[16 * 1024];             /* static: too big for the stack */

int n = px->load("words.txt", text, sizeof(text) - 1);
if (n >= 0) {
    text[n] = 0;
    for (char *line = strtok(text, "\r\n"); line; line = strtok(NULL, "\r\n"))
        add_word(line);
}

Typing a number

The digit keys' places: 0 is PX_KEY(6, 1); 1, 2, 3 are row 5; 4, 5, 6 row 4; 7, 8, 9 row 3, columns 1 to 3.

/* The digit on a key, or -1. */
static int digit_of(int k) {
    int r = PX_KEY_ROW(k), c = PX_KEY_COL(k);
    if (k == PX_KEY(6, 1))
        return 0;
    if (r >= 3 && r <= 5 && c >= 1 && c <= 3)
        return (5 - r) * 3 + c;
    return -1;
}

16. The simulator

make sim (the Simulate task) builds your app for your computer, with a stand-in for the calculator, and runs it through a script of keys and pauses. It saves screenshots as PNG files: the quickest way to see a change, and to check that a game behaves, without copying anything to the calculator.

The script is SIM_SCRIPT in the Makefile. It's a list of words, run in order:

WordDoes
R0C0 … R6C5Presses the key at that place. key() returns it going down, then going up on the next call.
SIGMA XEQ ENTER BACKSPACE UP DOWN SHIFT RSPresses that key, by name.
250Lets 250 ms pass with no keys.
shot:NAMESaves the screen, as of the last show(), as build/sim-out/NAME.png.
serial:TEXTTEXT arrives on the USB console, then Enter: serial_read() returns it. _ stands for a space. Put a word with ( ) " ; in it in single quotes in the Makefile: 'serial:PRINT_SQR(2)'.
ONPresses ON: key() returns PX_KEY_QUIT, or PX_KEY_ON after on_key(1).
QUITEnds the app, as the Sleep Timer does, when it next waits for a key (a program still running finishes first). The end of the script does the same.
SIM_SCRIPT := shot:title ENTER 1500 shot:playing XEQ 900 SIGMA 2000 shot:later ON

Run the simulator yourself to try other scripts: build/sim OUT_DIR DRIVE_DIR WORD....

The live simulator

make live (the Simulate live task) runs the same simulator in your browser, at http://127.0.0.1:8042. Only your computer can reach that address. It uses the computer's clock, not a script: the page shows the screen as the app draws it, and you press the keys yourself, by clicking the keypad or typing (digits and . + - * /, Enter, Backspace, the arrow keys, Space for R/S, f for SHIFT, s for Σ+, x for XEQ and Esc for ON).

It needs python3, which the simulator needs already. Stop it with Ctrl-C in the terminal.

The simulator isn't the calculator. The 128 KB limit, the stack size, the watchdog and the speed only apply on the calculator. Try every app there before you share it.

17. Debugging

Print

printf() to the USB console (section 11) is the simplest tool: print what the app does, and watch it in a terminal.

A crash

When an app crashes, the calculator restarts. Its start screen shows a report until a key is pressed:

Last reset:
FAULT core1 PC=20060C41 LR=200608B5 ...

Addresses from 20060000 up are in your app. The app is linked at the address it runs at, so the toolchain can name the line:

~/.pico-sdk/toolchain/15_2_Rel1/bin/arm-none-eabi-addr2line -f -e build/NAME.elf 0x20060C41 0x200608B5

PC is where it stopped; LR is usually the function that called it. Build with -g (add it to CFLAGS) for exact lines. build/NAME.map lists where every function and variable is.

A hang

An app that runs 100 ms without calling into px is restarted by the watchdog. The start screen then says WATCHDOG RESET APP: the app was in its own code, in a loop that doesn't wait.

18. The .bin file and the build

A .bin is the app's memory image as it will be in RAM, from 0x20060000. It starts with a 24-byte header:

OffsetFieldContents
0magic0x50415850, "PXAP" in the file.
4api_versionThe PX_APP_API_VERSION it was built with.
8image_sizeThe whole file's size: header, code and data.
12bss_sizeZeroed RAM just after the image, for variables that start at 0.
16entryThe address of the entry function, with bit 0 set (Thumb code).
20crc32zlib's CRC-32 of everything after the header.

All fields are 32-bit little-endian (px_app_header_t in px_app.h).

The build. make does this:

  1. arm-none-eabi-gcc compiles and links src/*.c with px42f/runtime.c for the RP2350's Cortex-M33: Thumb code, -mfloat-abi=softfp as the firmware uses, -Os, and the newlib-nano C library without its start-up code.
  2. px42f/app.ld puts the header first, then code, constants and data, all at 0x20060000. The zeroed variables come after, then the heap. A section the script doesn't place stops the link (--orphan-handling=error), so nothing ends up outside the image.
  3. objcopy takes the image out of the .elf, and px42f/pxbin.py fills in the sizes and the CRC.

The C library is there: string.h, stdlib.h (with malloc), stdio.h's snprintf and printf, ctype.h. float runs on the processor's FPU; double works too, more slowly, in software. math.h needs -lm added to LDFLAGS.

19. Messages when an app won't start

MessageMeaning
Not a PX-42F appThe file doesn't start with a PX-42F app header, or its entry point is outside it. It wasn't built with this framework, or it was overwritten.
Needs a newer firmwareIt was built for a later PX_APP_API_VERSION than the calculator's firmware has. Update the firmware.
The file is damagedIts size or checksum is wrong: the copy was cut short or changed. Copy it again.
Too big (128 KB at most)Code, data and variables add up to more than 128 KB.
Can't save the memoryThe calculator couldn't save its own memory to the drive (it's full), so it won't risk it.
Can't open the file / Can't read the fileThe drive couldn't be read.

20. Versions

px_app.h has PX_APP_API_VERSION, now 4. Version 2 added the decimal numbers, version 3 on_key(), version 4 MIDI. New firmware only adds functions at the end of the table and raises the number, so:

px->version is the firmware's own version number, for an app that wants to use a newer function only where it exists.

21. All functions at a glance

FunctionDoes
Screen
void show(void)Sends the picture to the panel.
void clear(void)All white.
void pixel(x, y, black)One pixel.
int get_pixel(x, y)1 if black.
void fill(x, y, w, h, black)A rectangle.
void line(x0, y0, x1, y1, black)A line.
void bitmap(x, y, w, h, bits, stride)A 1-bit picture; its 0 bits are transparent.
int text(x, y, s, font, inverse)Text; returns the x after it.
int text_width(s, font)Text's width in pixels.
int font_height(font)A font's height.
uint8_t *screenThe picture's memory.
Keyboard
int key(wait_ms)The next key down or up, PX_KEY_NONE or PX_KEY_QUIT.
Time
uint32_t millis(void)Milliseconds since start.
void sleep(ms)Waits, asleep.
void datetime(now)The calendar clock.
uint32_t random(void)A random number.
Sound
void tone(tone)TONE 0–9, 10 = error, waiting for it to end.
int play(path)A WAV file, waiting for it to end.
void tone_async(tone), int play_async(path)The same, not waiting.
int sound_playing(void)1 while a sound plays.
void sound_stop(void)Stops it.
Files
int load(path, data, size)A whole file in.
int save(path, data, size)A whole file out.
int open(path, mode)Opens a file, "r" or "w".
int read(file, data, size)Reads from it.
int write(file, data, size)Writes to it.
int close(file)Closes it.
int remove(path)Deletes a file or an empty folder.
int mkdir(path)Makes a folder.
int list(dir, ext, names, max)Lists files.
Serial
int serial_write(data, size)To the USB console (also printf).
int serial_read(data, size)What has come in.
Infrared
void ir_send(burst)One transmission.
Power
int battery_mv(void)Battery voltage.
int usb_connected(void)1 on a computer's USB.
Decimal numbers (API version 2)
int dec(op, r, a, b)Arithmetic and functions: PX_DEC_ADD … PX_DEC_TRUNC.
void dec_from_int(r, v), int dec_to_int(r, a)From and to a whole number.
int dec_from_string(r, s), void dec_to_string(buf, a)From and to text.
int dec_cmp(a, b), int dec_class(a)Compare; finite, infinite or NaN.
Version 3
void on_key(on_is_key)ON as a key (PX_KEY_ON) instead of ending the app.
Version 4
int midi_play(path)A MIDI file on the calculator's synthesizer, not waiting.
void midi_status(status)Length, position, channels and notes sounding.
Return values of app_main()
PX_EXIT_MENU (0)Back to Settings → Games & Apps.
PX_EXIT_OFF (1)Turn the calculator off.