A C compiler, assembler and linker for the MCS-51 (8051), written from scratch in Rust with no
dependencies. It reads the dialect SDCC accepts — address-space qualifiers, __interrupt, __at,
inline assembly — and stands in for sdcc in an existing build, but it compiles the whole program
at once and optimizes hard for code size.
On the minitel-native examples, against SDCC 4.6 for the
same sources and the same board (nfz330, bytes of ROM):
| example | SDCC | cc51 | |
|---|---|---|---|
| hello_world | 6130 | 4613 | 75% |
| video_stream | 6405 | 4475 | 70% |
| dino | 4005 | 2610 | 65% |
| dino_game | 9064 | 5642 | 62% |
Much of what is left in those numbers is data: of dino_game's 5642 bytes, 2247 are the splash screen, a CRC table and sprites, so the code itself is under half of SDCC's.
cargo build --release
This produces three binaries in target/release: cc51, and two simulators used for testing,
sim51 (a plain 8051) and minitel_sim (an 8051 with the video chip and keyboard of a Minitel).
cc51 main.c display.c -o firmware.ihx
Compilation and linking are one step: every source file named on the command line becomes part of one program, and all optimization happens with the whole program in view.
-o <file> output (.hex/.ihx: Intel HEX, .bin: binary, with -c: object)
-c compile only -E preprocess only
-I<dir> -D<n>[=v] -U<n> -w no warnings
-O0 / -O / -O2 optimization level (default -O2)
--code-loc <addr> --code-size <n> --iram-size <n>
--xram-loc <addr> --xram-size <n>
--map <file> --lst <file> --size
--float=fast|small soft float: faster, or smaller and slower (same results)
--dump-ir print the optimized IR and the register allocation
compat/bin holds sdcc, sdar and makebin as symlinks to the cc51 binary, which behaves as
whichever name it is invoked under. Put that directory first on PATH and a CMake or make build
written for SDCC uses cc51 instead:
PATH=/path/to/cc51/compat/bin:$PATH cmake /path/to/project && make
SDCC's own options (-mmcs51, --std-c23, --model-small, --opt-code-size, …) are accepted;
the ones that do not apply are ignored.
C17 with the C23 additions SDCC has (bool, nullptr, auto as a type specifier, _BitInt(N),
static_assert, typeof, digraphs and trigraphs, u8/u/U/L string and character literals),
plus SDCC's target extensions:
__data, __idata, __pdata, __xdata, __code, __near, __far, and
__bit, __sbit, __sfr, __sfr16, __sfr32 for hardware registers.__at(addr) on variables and inside declarators, __interrupt(n), __using(n), __naked,
__critical, __reentrant, __nonbanked.__asm ... __endasm blocks and __asm__("..."), assembled by cc51 itself; preprocessor
conditionals inside a block are evaluated before the assembler sees it.#pragma save/restore/nooverlay, #pragma std_c* and #pragma std_sdcc*.include/: the freestanding ones plus stdio.h (printf family),
stdlib.h, string.h, math.h, ctype.h, setjmp.h, errno.h, wchar.h, uchar.h,
stdbit.h, stdckdint.h, stdatomic.h, and 8051.h/8052.h/reg51.h/reg52.h.Whole program. All translation units are parsed into one program, so there is no point at which the compiler has to be pessimistic about what another file might do.
__naked functions use SDCC's convention so hand-written callers keep working.__xdata or __code pointers are two, and the three-byte generic form with
SDCC's space tag appears only where a pointer really can reach several spaces.On the IR (a non-SSA control-flow graph of byte-width virtual registers):
i < K on the latch becomes i != K, because an index stepping by one from a
smaller constant meets the bound exactly — one cjne instead of a cjne and a jc.a[i] = c becomes a pointer walk.Code generation.
add a,#1 to inc a where the flags are dead, moves
folded through dead intermediates, runs of one constant loaded once through the accumulator,
and djnz recognition.sjmp/ajmp/ljmp, inverted conditional plus
a jump where the target is out of reach), and absolute sections are placed without pushing
relocatable code around.-c writes a file with the expected .rel name, but it holds
preprocessed source, not machine code, because every decision is deferred to the link. cc51
neither reads nor writes SDCC's .rel/.lib format, so its output cannot be mixed with objects
or libraries built by SDCC. sdar produces a cc51 library, and the C library is built in._PARM_n globals. Assembly that calls a C function by hand therefore needs that function marked
__naked, which pins it to SDCC's convention.--model-small, --model-large and friends are accepted and ignored;
where a variable lives and how wide a pointer is are decided by inference instead. __reentrant
is accepted and inert: recursion works without it.printf("%f") prints the number. SDCC's small-model printf prints <NO FLOAT>; this is the
one deliberate difference in the regression results below. Soft float comes in two forms
(--float=fast, --float=small) that agree bit for bit.memcpy, printf or even
__fsmul replaces the library's version, including with a different signature..asm, .rst, .sym or .mem output;
--lst and --map describe the linked program, in which a function called once no longer
exists as a separate routine.__addressmod (user-defined address spaces), banked calls, SDCC's assembly
library routines and their calling convention, and macro bodies that expand to __asm text,
whose instructions stay on one line. Targets other than MCS-51 are out of scope by design.tests/all.sh # everything below except the SDCC suite and the size comparison
tests/run.sh # compile tests/c/*.c, simulate, compare with the expected output
tests/float_fuzz.sh # both soft-float variants against a Python reference model
tests/minitel_all.sh # build every minitel example with SDCC and cc51, report sizes
SDCC_REG=<sdcc>/support/regression tests/sdcc_reg/run.py # SDCC's regression suite
Current results:
| suite | result |
|---|---|
tests/c | 17 of 17 programs produce their expected output |
| soft float | 6400 operations per variant, bit-exact against the reference model |
| dino differential | video-register writes match the SDCC build on nfz330, nfz400 and 722039m |
| SDCC regression suite | 4066 pass, 6 expected failures, 0 failures |
The six expected failures are the features listed above: addrspace, bug3475990 and
genericnonintrinsicnaddr need __addressmod, libmullong_type_asm calls SDCC's assembly
_mullong with SDCC's convention, bug1505956 expands an __asm body from a macro, and
snprintf_type_FLOAT expects <NO FLOAT>. tests/sdcc_reg/run.py records results.txt and
prints a REGRESSION line for any test that passed before and no longer does.
The differential test compiles the dino game with both compilers and runs each in minitel_sim,
comparing every write to the video chip; it is what makes size changes safe to trust. It and the
size comparison need sdcc on PATH and a checkout of minitel-native ($HOME/Code/minitel-native,
or M=<path>); the regression runner needs the support/regression directory of an SDCC source
tree and Python 3.
src/pp, src/lex.rs | preprocessor (hidesets, digraphs, trigraphs) and C tokens |
src/parse, src/ast.rs | parser and type checker in one pass, address-space inference |
src/ir | lowering to the control-flow graph |
src/opt | the IR passes above |
src/cg | selection, allocation, peephole, outlining, RAM layout |
src/asm, src/link.rs | assembler, relaxation, section placement, Intel HEX and binary output |
runtime/rt.s, runtime/libc.c | assembly helpers and the C library, parsed last as a weak unit |
src/sim.rs, src/bin | the simulators the tests run against |
53 commits
Rust
86.9%
C
8.6%
Assembly
2.5%
Python
1.3%
A C compiler, assembler and linker for the MCS-51 (8051), written from scratch in Rust with no
dependencies. It reads the dialect SDCC accepts — address-space qualifiers, __interrupt, __at,
inline assembly — and stands in for sdcc in an existing build, but it compiles the whole program
at once and optimizes hard for code size.
On the minitel-native examples, against SDCC 4.6 for the
same sources and the same board (nfz330, bytes of ROM):
| example | SDCC | cc51 | |
|---|---|---|---|
| hello_world | 6130 | 4613 | 75% |
| video_stream | 6405 | 4475 | 70% |
| dino | 4005 | 2610 | 65% |
| dino_game | 9064 | 5642 | 62% |
Much of what is left in those numbers is data: of dino_game's 5642 bytes, 2247 are the splash screen, a CRC table and sprites, so the code itself is under half of SDCC's.
cargo build --release
This produces three binaries in target/release: cc51, and two simulators used for testing,
sim51 (a plain 8051) and minitel_sim (an 8051 with the video chip and keyboard of a Minitel).
cc51 main.c display.c -o firmware.ihx
Compilation and linking are one step: every source file named on the command line becomes part of one program, and all optimization happens with the whole program in view.
-o <file> output (.hex/.ihx: Intel HEX, .bin: binary, with -c: object)
-c compile only -E preprocess only
-I<dir> -D<n>[=v] -U<n> -w no warnings
-O0 / -O / -O2 optimization level (default -O2)
--code-loc <addr> --code-size <n> --iram-size <n>
--xram-loc <addr> --xram-size <n>
--map <file> --lst <file> --size
--float=fast|small soft float: faster, or smaller and slower (same results)
--dump-ir print the optimized IR and the register allocation
compat/bin holds sdcc, sdar and makebin as symlinks to the cc51 binary, which behaves as
whichever name it is invoked under. Put that directory first on PATH and a CMake or make build
written for SDCC uses cc51 instead:
PATH=/path/to/cc51/compat/bin:$PATH cmake /path/to/project && make
SDCC's own options (-mmcs51, --std-c23, --model-small, --opt-code-size, …) are accepted;
the ones that do not apply are ignored.
C17 with the C23 additions SDCC has (bool, nullptr, auto as a type specifier, _BitInt(N),
static_assert, typeof, digraphs and trigraphs, u8/u/U/L string and character literals),
plus SDCC's target extensions:
__data, __idata, __pdata, __xdata, __code, __near, __far, and
__bit, __sbit, __sfr, __sfr16, __sfr32 for hardware registers.__at(addr) on variables and inside declarators, __interrupt(n), __using(n), __naked,
__critical, __reentrant, __nonbanked.__asm ... __endasm blocks and __asm__("..."), assembled by cc51 itself; preprocessor
conditionals inside a block are evaluated before the assembler sees it.#pragma save/restore/nooverlay, #pragma std_c* and #pragma std_sdcc*.include/: the freestanding ones plus stdio.h (printf family),
stdlib.h, string.h, math.h, ctype.h, setjmp.h, errno.h, wchar.h, uchar.h,
stdbit.h, stdckdint.h, stdatomic.h, and 8051.h/8052.h/reg51.h/reg52.h.Whole program. All translation units are parsed into one program, so there is no point at which the compiler has to be pessimistic about what another file might do.
__naked functions use SDCC's convention so hand-written callers keep working.__xdata or __code pointers are two, and the three-byte generic form with
SDCC's space tag appears only where a pointer really can reach several spaces.On the IR (a non-SSA control-flow graph of byte-width virtual registers):
i < K on the latch becomes i != K, because an index stepping by one from a
smaller constant meets the bound exactly — one cjne instead of a cjne and a jc.a[i] = c becomes a pointer walk.Code generation.
add a,#1 to inc a where the flags are dead, moves
folded through dead intermediates, runs of one constant loaded once through the accumulator,
and djnz recognition.sjmp/ajmp/ljmp, inverted conditional plus
a jump where the target is out of reach), and absolute sections are placed without pushing
relocatable code around.-c writes a file with the expected .rel name, but it holds
preprocessed source, not machine code, because every decision is deferred to the link. cc51
neither reads nor writes SDCC's .rel/.lib format, so its output cannot be mixed with objects
or libraries built by SDCC. sdar produces a cc51 library, and the C library is built in._PARM_n globals. Assembly that calls a C function by hand therefore needs that function marked
__naked, which pins it to SDCC's convention.--model-small, --model-large and friends are accepted and ignored;
where a variable lives and how wide a pointer is are decided by inference instead. __reentrant
is accepted and inert: recursion works without it.printf("%f") prints the number. SDCC's small-model printf prints <NO FLOAT>; this is the
one deliberate difference in the regression results below. Soft float comes in two forms
(--float=fast, --float=small) that agree bit for bit.memcpy, printf or even
__fsmul replaces the library's version, including with a different signature..asm, .rst, .sym or .mem output;
--lst and --map describe the linked program, in which a function called once no longer
exists as a separate routine.__addressmod (user-defined address spaces), banked calls, SDCC's assembly
library routines and their calling convention, and macro bodies that expand to __asm text,
whose instructions stay on one line. Targets other than MCS-51 are out of scope by design.tests/all.sh # everything below except the SDCC suite and the size comparison
tests/run.sh # compile tests/c/*.c, simulate, compare with the expected output
tests/float_fuzz.sh # both soft-float variants against a Python reference model
tests/minitel_all.sh # build every minitel example with SDCC and cc51, report sizes
SDCC_REG=<sdcc>/support/regression tests/sdcc_reg/run.py # SDCC's regression suite
Current results:
| suite | result |
|---|---|
tests/c | 17 of 17 programs produce their expected output |
| soft float | 6400 operations per variant, bit-exact against the reference model |
| dino differential | video-register writes match the SDCC build on nfz330, nfz400 and 722039m |
| SDCC regression suite | 4066 pass, 6 expected failures, 0 failures |
The six expected failures are the features listed above: addrspace, bug3475990 and
genericnonintrinsicnaddr need __addressmod, libmullong_type_asm calls SDCC's assembly
_mullong with SDCC's convention, bug1505956 expands an __asm body from a macro, and
snprintf_type_FLOAT expects <NO FLOAT>. tests/sdcc_reg/run.py records results.txt and
prints a REGRESSION line for any test that passed before and no longer does.
The differential test compiles the dino game with both compilers and runs each in minitel_sim,
comparing every write to the video chip; it is what makes size changes safe to trust. It and the
size comparison need sdcc on PATH and a checkout of minitel-native ($HOME/Code/minitel-native,
or M=<path>); the regression runner needs the support/regression directory of an SDCC source
tree and Python 3.
src/pp, src/lex.rs | preprocessor (hidesets, digraphs, trigraphs) and C tokens |
src/parse, src/ast.rs | parser and type checker in one pass, address-space inference |
src/ir | lowering to the control-flow graph |
src/opt | the IR passes above |
src/cg | selection, allocation, peephole, outlining, RAM layout |
src/asm, src/link.rs | assembler, relaxation, section placement, Intel HEX and binary output |
runtime/rt.s, runtime/libc.c | assembly helpers and the C library, parsed last as a weak unit |
src/sim.rs, src/bin | the simulators the tests run against |
53 commits
Rust
86.9%
C
8.6%
Assembly
2.5%
Python
1.3%