Library — newlib
SkillFiles & storagez88dk newlib architecture: CRT m4 FILE* instantiation, character_00 vs console_01, static stdio heap sizing, asm_target_open disk path, dual-stack FCB vs FatFs, open_max/fopen_max. Use when migrating targets, adding serial or disk drivers, or debugging fopen/open on -clib=new.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Library — newlib skill
What this skill tells your AI
The instructions your AI receives, as published by z88dk/z88dk in .agents/skills/library-newlib/SKILL.md and read by ahel’s review.
2. Serial / character FILE* instantiation (newlib)
Layers
stdio (printf / FILE*)
→ console_01 (line-edited terminals; default on many CRTs)
→ character_00 (thin byte streams; good for multi-port + RDR/PUN/LST)
→ device (UART/SIO/ACIA/ASCI/HBIOS/BDOS CON…)
| Layer | Typical path |
|---|---|
| stdio core | libsrc/newlib/stdio |
| character_00 | libsrc/newlib/drivers/character/ |
| console_01 | libsrc/newlib/drivers/terminal/console_01/ |
| Target terminals | libsrc/target/<t>/driver/terminal/*.m4 + .asm |
| Devices | libsrc/target/<t>/device/… |
Defaults: most hardware CRTs still instantiate full console_01 / rc_01_* terminals. Thin character_00 is the additive multi-port pattern (keep m4_file_dup / optional crt_driver_instantiation.asm.m4).
CRT m4 wiring (where FILEs are born)
Startup *_crt_N.asm.m4 includes driver m4 macros inside:
clib_instantiate_begin.m4
m4_<driver>(_stdin, …)
m4_<driver>(_stdout, …)
m4_file_dup(_stderr, 0x80, __i_fcntl_fdstruct_1) ; often dup of stdout
… extra ports …
clib_instantiate_end.m4 ; builds fdtbl + FILE freelist + stdio heap
Each static driver m4 typically:
- Allocates a FILE + FDSTRUCT on the stdio heap sections.
- Pushes an entry into the fd table body.
- Chains a heap block header (
__i_fcntl_heap_N).
Multi-port and dups
| Pattern | Mechanism |
|---|---|
| Second console / teletype | Second input+output terminal pair → ttyin / ttyout; ttyerr = m4_file_dup of ttyout (same idea as stderr) |
| stderr | Almost always a dup of stdout’s FDSTRUCT (flag 0x80) |
| Extra static slots | m4_file_absent or more drivers |
Declared in newlib stdio.h even when a CRT does not instantiate them: stdrdr, stdpun, stdlst, ttyin, ttyout, ttyerr. Missing instantiation ≠ missing declaration — apps that reference an uninstantiated FILE* will fail at link or runtime.
CP/M character model (portable apps vs implementations)
Physical BDOS units (what drivers usually call):
| Unit | BDOS | Typical newlib FILE* |
|---|---|---|
| CON | 1/2/6/… | stdin / stdout / stderr |
| RDR | 3 | stdrdr |
| PUN | 4 | stdpun |
| LST | 5 | stdlst |
Logical names (CRT, TTY, LPT, PTR, PTP, BAT, U*) are selected by IOBYTE (page-0 $0003, BDOS 7/8). BIOS maps logical → physical UART/device.
| Audience | What to wire |
|---|---|
| CP/M implementation (e.g. CP/M-IDE) | CRT maps stdin/tty* to real ports; shell seeds IOBYTE; ASM BIOS interprets IOBYTE |
CP/M application (+cpm -clib=new) | BDOS only; FILE* + optional logical-name helpers; no UART registers |
Do not assume fopen("TTY:") is how implementations work — FILE selection + IOBYTE seed* is the real dual-port pattern.
Hybrid classic+newlib consoles (rc2014-8085 lesson)
When a CRT mixes classic fgetc_cons/fputc_cons with newlib-style startup:
- FILE init flags must match classic expectations (
18/20=_IOSYSTEM|_IOREAD/_IOWRITE). - Wrong flags (
19/21with spurious_IOUNGETC) made firstgetcharreturn NUL. - Hybrid clib lists must not pull full newlib fcntl/stdio/threads.
- Build: classic
<stdio.h>must win include order (-I…/includebefore_DEVELOPMENT/common) when the hybrid needs classicstdin/stdoutobjects.
Cooked line input: newlib vs classic (general)
| World | Line API | Who echoes / edits |
|---|---|---|
| Newlib | POSIX getline / getdelim | console_01 (line mode, echo, BS, CR/LF cook) via tied oterm |
| Classic | No getline | fgets on stdin → fgets_cons (echo, DEL, optional soft cursor) |
| Classic raw | fgetc / fgetc_cons | No line editor — app must implement if needed |
Rules of thumb
getlineis newlib-only. Never expect it on 8080/8085 classic products.- One cook layer only. If the driver/
fgets_consalready echoes, do not also echo in app code (double echo). - Hybrid CRTs that only bind
fgetc_cons/fputc_consare raw. App-level line readers (e.g. shellya_getline) are compensating for classic, not for the CPU. - Prefer
fgets/fgets_conson classic instead of reimplementing line edit. On serial targets, disable soft cursor if needed (CLIB_DISABLE_FGETS_CURSOR=1— already set forrc2014-8085). - Align dual-CPU apps (Z80 newlib + 8085 classic) at a single call site with
#ifdef, not by linking newlib stdio into 8085 images.
Dual-port FILE* vs classic ttyin macros
| Newlib CRT | Classic hybrid (e.g. uart85) | |
|---|---|---|
| Second port | Real drivers: m4_rc_01_input_uartb(_ttyin, …) etc. | Often only stdin/out/err → primary UART/ACIA |
ttyin / ttyout in headers | extern FILE * | Classic macros → _sgoioblk[3]… slots |
| Meaning | Instantiated streams | Declaration/slots ≠ working UARTB console |
fgetc on classic special-cases stdin → fgetc_cons. Assigning input = ttyin does not create a second cooked port unless the CRT initialises that slot and a driver path exists. For dual-port on hybrid: either an active-console global in fgetc_cons, or real second-stream CRT work — do not copy newlib’s input = ttyin pattern blindly.
CP/M IOBYTE seeds (firmware shells)
Shell may seed bios_iobyte before handing off to CCP; BIOS copies it to page-0 IOBYTE.
- CON is low 2 bits (CRT vs TTY, etc.).
- Hardware-specific high bits (e.g. 8085 module LST → SOD) may require seeds like
0x81/0x80, not bare1/0. Match the BIOSlist/constdecode, not “Z80 values”.
2b. Newlib static stdio heap sizing (FDSTRUCT committed)
Each static driver m4 places a heap block:
[next:2][committed:2][prev:2] + FDSTRUCT body (+ edit buffer)
\_____ 6-byte header _____/
committed(and__I_FCNTL_HEAP_SIZEadd) must equal header + body bytes.- Oversized committed → free = next − (block+committed) underflows → later
open/fopencan corrupt the next FDSTRUCT (e.g. stdout). Classic bug:cpm_00_input_consused$3+29instead of$3+27. - Undersized committed → free block accounting wrong (FZX once claimed 63 for a 64-byte block).
| Family | Body (typical) | committed |
|---|---|---|
| character_00 / simple out | 17 | 23 |
console_01 input + edit buf $4 | 28+$4 | $4+34 |
cpm_00_input_cons (BDOS buf $3+1) | 21+$3 | $3+27 |
| zx inkey / lastk | … | $4+41 / $4+36 |
Verify: map spans between __i_fcntl_heap_N and _N+1 must equal the formula (with default edit buf, often 64 → first span 91 or 98, etc.). Multi-arg defb \$a, $b`` counts as multiple bytes when hand-checking m4.
m4 comments: do not put unquoted commas in macro body text (breaks m4 argument parsing).
3. Disk / fcntl instantiation (newlib)
Open path
open / creat / fopen
→ asm_vopen
→ asm_target_open_p1 ; validate path; return EXTRA bytes for FDSTRUCT
→ heap_alloc(sizeof header + EXTRA) from __stdio_heap
→ asm_target_open_p2 ; fill FDSTRUCT; install JP to driver
Target must provide asm_target_open_p1 and asm_target_open_p2 (e.g. CP/M FCB driver cpm_01_file). If missing → link error:
undefined symbol: asm_target_open_p1
That is the classic newlib “stdio disk I/O missing” symptom (#3022-class), not a compiler bug.
CRT knobs that make or break open / fopen
Set in target crt_config.inc (TAR__clib_*):
| Knob | Role | Failure mode if wrong |
|---|---|---|
open_max | Size of fd table (static FDs + dynamic opens) | open_max=0 → only static fds; open() ENFILE / no room |
stdio_heap_size | Heap for FDSTRUCTs (FCB driver ~192 B each incl. 128 B sector buf) | Too small → heap_alloc fails on open |
fopen_max | Max FILE structures | Must be > static FILE count or freelist stays empty → fopen EMFILE even when open works |
Rule of thumb for CP/M-class newlib CRTs with 6 static streams (stdin…stdlst) + user files:
open_max = 16stdio_heap_size = 1024fopen_max = 10(or any value greater than static FILE count)
Dual-stack policy (when both exist)
| API | Backend |
|---|---|
Unprefixed open / read / write / lseek / close | Host / OS file driver (e.g. CP/M BDOS FCB) |
ChaN f_* | FatFs + target diskio (raw media) |
printf / console FILE* | Character/terminal drivers |
f_*is never an alias for FCB fcntl. Volumes stay independent.- Plain
+cpm -clib=new: BDOS FCB only is enough — no FatFs, no physicaldiskio. - Hardware
-subtype=cpm: FCB by default; optional-lffdual-stack.
One open owner per binary: do not mix classic libsrc/target/cpm/fcntl objects with newlib cpm_01_file in the same link.
Library list / rebuild traps
-
Driver must appear in the target
library/*_sccz80.lstchain (often viadriver/driver.lst). -
Newlib
Makefileoften depends only onconfig_private.inc— lst/source adds do not always rebuild. Force:rm -f lib/clibs/sccz80/<target>.lib lib/clibs/sdcc_ix/<target>.lib make -C libsrc/newlib <target> -
Prove the symbol is in the lib:
z88dk-z80nm lib/clibs/sccz80/<target>.lib | rg 'asm_target_open|cpm_01_file' -
Prove the app linked it:
rg 'cpm_01_file|asm_target_open|__fcntl_fdtbl_size' app.map
4. Testing I/O (test/suites/target_io)
Shared serial + disk suite for z88dk-ticks.
| File | Role |
|---|---|
io_tests.c | printf/scanf + creat/write/read/lseek/close/multi-fd |
fcntl_native.c | Native open/creat/… (CP/M BDOS / newlib FCB) |
fcntl_host.c + ticks_host_fcntl.asm | Host SYSCALL files (targets without OS fcntl) |
Makefile | Per-product recipes |
Design rules
- Shared tests call only
tio_*(io_port.h) — backends swap. - Classic CP/M breadth: default subtype only (
+cpmz80 /+cpm -clib=8085). Do not fan out to 150+ machine subtypes in this suite. - Newlib gates: plain
+cpm -clib=new(Z80) and hardware+… -subtype=cpm -clib=newwhere dual-stack FCB applies. Newlib CP/M is not an 8085 product — 8085 stays classic default CLIB. - Extend serial when CRTs expose more streams: RDR/PUN/LST (
stdrdr/stdpun/stdlst), latertty*if instantiated. - Extend disk when drivers claim flags: keep lseek (SET/END + overwrite); add
fopen/fread/fwritefor newlib stdio path; optional O_TRUNC/O_APPEND if implemented. - FatFs
f_*is a separate optional gate on hardware packages — not required for plain+cpm.
Run pattern
make -C test/suites/target_io # all recipes
make -C test/suites/target_io test_rc2014_cpm.com
# scanf tests need piped input (Makefile uses SCANF_INPUT)
CP/M outputs need .com for ticks CP/M mode; some newlib links produce *_CODE.bin that must be copied to .com.
What “green” means
- Suite:
N run, N passed, 0 failed - Map proof for newlib disk: driver +
__fcntl_fdtbl_size/open_max/ heap as expected - Classic default Z80 + 8085 still pass after mixed-tree moves (isolation)
5. Config / CRT pipelines (quick map)
| Pipeline | When | Outputs |
|---|---|---|
| Config m4 | library build | config_*_{private,public}.inc, config_*.h |
| CRT m4 | each zcc +target link | expand *_crt.asm.m4 + startup + drivers |
- Modes:
CFG_ASM_DEF/CFG_ASM_PUB/CFG_C_DEF. - zcc maps startup →
__STARTUP, pragmas →M4__*,-Itarget home +src/m4. - Edit only the CLIB lines that point at newlib CRT/lib paths when migrating; leave classic SUBTYPE lines alone unless intentional.
6. Agent checklist (new serial or disk work)
Serial
- Driver class:
character_00vsconsole_01(match peers) - CRT m4 instantiates FILE* + FDSTRUCT; dups for err streams
- Public
stdio.hnames match what the CRT actually builds - Multi-port: second triple uses
tty*+m4_file_dupfor err - Hybrid classic console: FILE flags 18/20, list isolation, classic include order
- Line input: newlib
getlinevs classicfgets_cons— one cook layer; no fakettyinon hybrid - Static heap: each m4
committed/HEAP_SIZE= body + 6 (map-span check)
Disk
-
asm_target_open_p1/_p2in target lib (nm proof) -
open_max,stdio_heap_size,fopen_maxsized for static + dynamic use - One
openowner; dual-stack docs if FatFs also present - Forced lib rebuild after lst changes; app map shows driver
Test
-
target_iorecipe for the product (native vs host fcntl) - Classic CP/M: default subtype only unless a specific machine is in scope
- Extend serial/disk cases only where the CRT/driver supports them
- ticks CPU model (
-m8085when relevant)
Dual-CPU firmware shells (Z80 newlib + 8085 classic)
- Shared app logic; platform glue only for CRT/IOBYTE/banners
- Do not link newlib stdio into 8085 images
- Align line-read at one
#ifdefcall site; verify echo/BS on both
7. Headers: edit proto, regenerate common
Newlib public headers live under include/_DEVELOPMENT/:
| Path | Role |
|---|---|
proto/*.h | Source of truth (m4 macros: __DPROTO, __D2PROTO, …) |
common/*.h | Generated: m4 proto/foo.h > common/foo.h |
cd include/_DEVELOPMENT
# one file:
make common/math.h
# or force:
make -B common/math.h
Do not hand-edit common/ for lasting changes — edit proto and regenerate.
Math32 sccz80 remaps (math.h, issue #3061)
Under #ifdef __MATH_MATH32 / #ifdef __SCCZ80, proto/math.h remaps unary
API names to *_fastcall (same idea as classic math/math_math32.h). Required
because math32.lib is built with -D__CLASSIC and plain sin/sqrt/… are
stack bridges, while sccz80 treats the plain name as DEHL fastcall.
Details and map proofs: library-math32. After header edits: suite
test_math32_rc2014_CODE.bin + remeasure newlib TIMER rows that call higher
math (Whetstone, n-body).
Related
- Classic:
library-classic - Float products / calling:
library-math32,library-math16 - Measure I/O:
test/suites/target_io(seemethodology-measure) - Targets:
target-cpm,target-rc2014
Signals
- GitHub stars
- 1k
- Forks
- 206
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
library-newlib- Source
- github.com/z88dk/z88dk