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.
HELLO.bin.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
- The ARM toolchain. Use the one the Raspberry Pi Pico extension for VS Code installs:
~/.pico-sdk/toolchain/15_2_Rel1. Another location works withmake PICO_TOOLCHAIN=/path/to/toolchain. - For the simulator:
make,python3and a C compiler for your computer (cc). On a Mac, they come with the Xcode Command Line Tools. - Optional: VS Code with the C/C++ extension. Everything also works with
makein a terminal.
Make a new app
- Copy the
PX-42F-App-Templatefolder and give the copy your app's name, for examplePX-42F-Tetris. Open it in VS Code. - 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:GAMESorAPPS, the folder it goes in.
- Write your app in
src/app.c. You can add more.cand.hfiles insrc/; they're all built. - Build it: ⇧⌘B in VS Code, or
make. The result isbuild/NAME.bin. - Try it on your computer: the Simulate task, or
make sim. Screenshots are saved inbuild/sim-out. - Put it on the calculator:
- On the calculator, open Settings → USB Drive → Connect to Computer.
- Run the Install on the PX-42F task, or
make install. It copies the file toBIN/GAMESorBIN/APPSon the drive namedPX-42F. - Eject the drive on the computer, then press EXIT on the calculator.
- Start it: Settings → Games & Apps → Games (or Apps), choose it, and press ENTER.
The project
| Path | What it is |
|---|---|
src/ | Your app. The template has one file, app.c. |
px42f/px_app.h | The contract with the firmware: every function and constant. It's copied from the firmware; don't edit it. |
px42f/px.h | What your files include: px_app.h, stdbool.h, stdint.h, the table px and app_main(). |
px42f/runtime.c | The .bin's header and entry, the memory malloc() uses, and printf() to the console. |
px42f/app.ld | The linker script: places the app at 0x20060000, where the calculator loads it. |
px42f/pxbin.py | Turns 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. |
Makefile | NAME, 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:
- Drawing happens in memory. Nothing reaches the panel until
px->show(). px->key(-1)waits for the next key, asleep. It returnsPX_KEY_QUITwhen you press ON, and the app must then return.- The return value says where to go.
PX_EXIT_MENUgoes back to the list;PX_EXIT_OFFturns the calculator off.
4. How an app runs
Starting
When you choose an app, the calculator:
- Reads the whole
.binand checks its header and checksum. A file that fails gets a message (section 19) and nothing else happens. - Saves the calculator's memory (the Plus42 stack, registers and programs) to the drive.
- Copies the app into RAM at
0x20060000, sets its variables to zero, and callsapp_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
| Rule | Why |
|---|---|
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.
| Constant | Value | Meaning |
|---|---|---|
PX_SCREEN_W | 250 | Width in pixels. |
PX_SCREEN_H | 122 | Height in pixels. |
PX_SCREEN_STRIDE | 32 | Bytes 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
hrows ofstridebytes 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
| Font | Character cell | Advance | Characters a line |
|---|---|---|---|
PX_FONT_LARGE | 10 × 16 | 11 pixels | 22 |
PX_FONT_SMALL | 8 × 12 | 8 pixels | 31 |
PX_FONT_MINI | 6 × 8 | 6 pixels | 41 |
PX_FONT_CHOSEN_LARGE, _SMALL, _MINI | The 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:
| Code | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0x00 | ÷ | × | √ | ∫ | ▒ | Σ | ▶ | π | ¿ | ≤ | LF | ≥ | ≠ | ↵ | ↓ | → |
| 0x10 | ← | µ | £ | ° | Å | Ñ | Ä | ∡ | ᴇ | Æ | … | Esc | Ö | Ü | ^ | • |
| 0x7F | ⊢ (the HP-42S's append sign) | |||||||||||||||
"\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).
PX_FONT_LARGE, 10 × 16.PX_FONT_SMALL, 8 × 12.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.
| Macro | Gives |
|---|---|
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_RELEASED | Not 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_RS | Names 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:
tone()andplay()wait until the sound has played. Use them where the next thing should come after it.tone_async()andplay_async()start the sound and return at once. Use them in a game: the frames keep coming while it plays.
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 0toTONE 9, low to high: the files/SOUNDS/TONES/TONE0.wavtoTONE9.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) orPX_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) andvoices(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
- From the top of the drive: a path starting with
/, such as"/SOUNDS/ON.wav"or"/STATE". - In the app's own folder: any other path, such as
"BEST"or"levels/1.txt". The folder is next to the app and named after it:/BIN/GAMES/SNAKE/for/BIN/GAMES/SNAKE.bin. It's made the first time the app writes a file in it. ""is the app's folder itself, forlist().- A path has at most
PX_PATH_MAX(96) characters. Names aren't case-sensitive, as on any FAT drive.
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 mostPX_FILES_OPEN(4) files are open at once.- read
- Up to
sizebytes: 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
maxnames of up toPX_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.
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).
| Operations | Do |
|---|---|
PX_DEC_ADD SUB MUL DIV | a + b, a − b, a × b, a / b |
PX_DEC_POW | a to the power b |
PX_DEC_REM | the IEEE remainder: a − b·n, n = a/b rounded to the nearest whole number |
PX_DEC_ATAN2, PX_DEC_HYPOT | the angle of the point (b, a); √(a² + b²) |
PX_DEC_NEG ABS SQRT | −a, |a|, √a |
PX_DEC_EXP LN LOG10 EXPM1 LN1P | ea, ln a, log10 a, ea − 1, ln(1 + a) |
PX_DEC_SIN COS TAN ASIN ACOS ATAN | trigonometry, in radians |
PX_DEC_SINH COSH TANH ASINH ACOSH ATANH | hyperbolic |
PX_DEC_GAMMA | Γ(a), which is (a − 1)! for whole numbers |
PX_DEC_FLOOR CEIL TRUNC | to 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
ais 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 (ris 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'sformat_number()does it, insrc/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 */
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:
| Word | Does |
|---|---|
R0C0 … R6C5 | Presses the key at that place. key() returns it going down, then going up on the next call. |
SIGMA XEQ ENTER BACKSPACE UP DOWN SHIFT RS | Presses that key, by name. |
250 | Lets 250 ms pass with no keys. |
shot:NAME | Saves the screen, as of the last show(), as build/sim-out/NAME.png. |
serial:TEXT | TEXT 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)'. |
ON | Presses ON: key() returns PX_KEY_QUIT, or PX_KEY_ON after on_key(1). |
QUIT | Ends 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
- Time is virtual. It moves on through pauses and the app's waits, and by 1 ms each time the app reads it, so a loop that never waits still sees time pass. A 10-second script runs in a moment, and the same script gives the same result every time. The random numbers are the same on each run too.
- The drive is a folder.
sim/driveholds what the calculator's drive would: put files the app reads there, at the same paths. Eachmake simworks on a fresh copy,build/sim-drive, so what the app writes doesn't pile up in your project. - What's printed. Sounds, IR transmissions and serial output are printed, with the virtual time.
- Memory checks. The simulator is built with AddressSanitizer: writing past an array stops it and says where.
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).
- Restart rebuilds the app and starts it again with a fresh copy of the drive: edit
src/app.c, press Restart, look. A build that fails shows its errors in the console. - Record writes down the keys you press, and the pauses between them, as a
SIM_SCRIPTline to paste into theMakefile: play the app by hand once, then run it as a test withmake sim. - The console shows what the simulator prints, and the sanitizers' report if the app crashes. The box under it sends text to the app's USB serial port, then Enter.
- Sounds play in the page.
play()andtone()use the WAV files in the simulated drive'sSOUNDSfolder (sim/drive/SOUNDS/ON.wav,TONES/TONE0.wav…), as on the calculator, and the app waits as long as the sound lasts. The browser keeps sound off until you click or press a key on the page once.make simstill only prints them.
It needs python3, which the simulator needs already. Stop it with Ctrl-C in the terminal.
17. Debugging
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:
| Offset | Field | Contents |
|---|---|---|
| 0 | magic | 0x50415850, "PXAP" in the file. |
| 4 | api_version | The PX_APP_API_VERSION it was built with. |
| 8 | image_size | The whole file's size: header, code and data. |
| 12 | bss_size | Zeroed RAM just after the image, for variables that start at 0. |
| 16 | entry | The address of the entry function, with bit 0 set (Thumb code). |
| 20 | crc32 | zlib'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:
arm-none-eabi-gcccompiles and linkssrc/*.cwithpx42f/runtime.cfor the RP2350's Cortex-M33: Thumb code,-mfloat-abi=softfpas the firmware uses,-Os, and the newlib-nano C library without its start-up code.px42f/app.ldputs the header first, then code, constants and data, all at0x20060000. 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.objcopytakes the image out of the.elf, andpx42f/pxbin.pyfills 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
| Message | Meaning |
|---|---|
| Not a PX-42F app | The 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 firmware | It was built for a later PX_APP_API_VERSION than the calculator's firmware has. Update the firmware. |
| The file is damaged | Its 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 memory | The 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 file | The 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:
- An app keeps working on every later firmware.
- A calculator with older firmware than the app was built for refuses it ("Needs a newer firmware"), rather than calling a function it doesn't have.
- To use functions a newer firmware adds, copy its
px_app.h(from the PX-42F-Plus firmware project) into your app'spx42f/folder and rebuild. Your app then needs that firmware too.
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
| Function | Does |
|---|---|
| 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 *screen | The 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. |