Microcontroller Development Framework for SDCC — build & flash MCS-51 (8051), STM8, Z80 and MOS6502 projects directly from VS Code.
- 🧩 Board Manager — add / edit / duplicate / delete MCU definitions; stored globally, available in every project
- 📁 Project editor (Webview) —
sdcc-project.jsonwith visual UI, multi-root workspace support, validated on save - ⚙️ Build — per-source
.c→build/obj/**(mirroring source tree, no collisions) →main()first →.ihx→.hex(packihx) →.bin(makebin) - ⚡ Flash — configurable upload tool with variable expansion (
${outputHex},${config:comPort},${board.mcu},${board.target},${env:…}, …) - 🪝 Hooks —
pre_build/post_link/post_build/pre_upload/post_uploadwithcwdandcontinue_on_error; a${…}picker in the editor inserts any of the 40+ variables at the cursor (grouped, searchable, with tooltips) - 📚 Libraries + components — prebuilt
*.lib(linked as-is) and source libraries (compiled →sdar -rc→ linked) — ESP8266-IDFcomponents/pattern, manual §3.2.4/§3.2.5 - 🔌 Serial port auto-detection — Windows
SerialPort.GetPortNames, Linux/macOS/devscan - 🛠 Toolchain picker — auto-detects PATH /
SDCC_HOME/ standard install dirs, with native folder picker; companion tools (makebin,packihx,sdar) found next tosdcc - 🐚 Windows shell pinning — auto-detects your shell (PowerShell / cmd / bash / WSL) and pins it to the terminal so quoting and exit-code markers always match — no more "Syntax error in filename"
⚠️ Auto-warning for unrecognized shells (v0.11.2) — Nushell / elvish / xonsh users get a one-time actionable hint instead of a silent build hang
Verified against the official SDCC 4.4.1 Compiler User Guide — target flags (§3.3.1), memory models per port (§3.3.6–§3.3.12), compile/link flow (§3.2.3), packihx/makebin (§3.2.2, §2.9).
- SDCC 4.0+ (4.4.1 recommended) —
sdcconPATH, or set SDCC: Path in Settings, or run SDCC: Select Toolchain Path - A flash tool for your target:
stm8flash(STM8 via ST-Link),stcgal(STC89 USB/serial bootloader),ezp/nmflash(MCS-51) — configured per-project viaupload.toolandupload.args
- Install SDCC, make sure
sdccis onPATH(or run SDCC: Select Toolchain Path). - Run SDCC: Create New Project → enter name → pick a board → Create Project Here (native folder picker). The starter folders (
src/,include/,lib/,components/) andsrc/main.copen automatically. - SDCC: Build (
Ctrl+Alt+B) → configureuploadinsdcc-project.json→ SDCC: Flash (Ctrl+Alt+F).
- Open Extensions (
Ctrl+Shift+X/Cmd+Shift+X). - Search for SDCC-MDF.
- Click Install.
- Get
sdcc-mdf-0.11.2.vsix. - Extensions →
⋯→ Install from VSIX… → pick the file. — or —code --install-extension sdcc-mdf-0.11.2.vsix
A page to add / edit / duplicate / delete MCU definitions: target (all SDCC ports), memory model (filtered per target), IRAM / XRAM / Code / Heap / Stack sizes, f_cpu, extra flags, upload max size. Boards are stored globally in ~/.sdcc-mdf/boards (update-safe — survives extension updates, uninstalls/reinstalls and profile switches; boards still living in the legacy globalStorage are rescued automatically on activation); a project can keep its own .sdcc/boards/ overrides that shadow globals for the same id.
Backup: SDCC: Export User Boards (backup) copies the whole home boards folder into a timestamped folder anywhere you choose; SDCC: Import Boards from Backup… merges .json board files from any folder back (existing files are only overwritten when the imported copy is strictly newer — an import can never roll back newer edits).
Ships with: Generic 8051, STC89C52RC, Beken BK2461, STM8S103F3, STM8S105K4, Generic Z80.
sdcc-project.json-based projects with a visual editor (Webview), multi-root workspace support. The Board field is select-only: the ▾ button opens a native-<select>-style dropdown — it drops under the Board input, exactly as wide as the input, listing all known MCUs with the current one shown in bold; the height is resizable by dragging the bottom edge, the width stays locked to the field. Free text is not allowed, so a board id is always a valid existing definition (add missing MCUs via the Board Manager). Create New Project: enter name → pick board → Create Project Here — a native folder picker; you CREATE a new empty folder anywhere, it is added to the workspace and src/main.c opens automatically.
The extension never grabs a workspace folder on its own — the project picker always shows a QuickPick annotated with the detected state (✓ valid / ⚠ warnings / ✗ errors / not an SDCC project). The config file is validated (schema, path safety, board, sources, hooks): the tree shows live status, build/flash/clean are blocked on errors, and SDCC: Show Project Problems prints the full report. The active project survives VS Code restarts (subfolder projects included).
Auto-saves unsaved editors, compiles each .c into build/obj/** (mirroring the source tree — no basename collisions), links main() first into build/<name>.ihx, then generates .hex via packihx and .bin via makebin (buffer sized from the ihx — works for STM8's 0x8000+ layout). Verified against SDCC 4.2.0 / 4.4.1.
On failure you do not scroll the terminal: the failing command is replayed hidden once, its stderr parsed, and the first diagnostic lines appear right in the failure notification — with a Go to Error button that opens the exact file:line when one is parseable (the ASCII subst alias path is translated back to the real project path first). Assembler (?ASxxxx-Error…) and linker (?ASlink-Error…) failures show their text without a jump target.
Extra flags: build.flags (compile + link) and build.src_flags (compile-only) — PlatformIO-style. sdcc.buildMode = debug adds --debug (SDCC emits .sym/.rst debug info — manual §3.8) and defines DEBUG (inherited by library components). Flip the mode with the Release / Debug status-bar item — no need to open Settings.
build.output_dir and build.output_name support the variable table too (with ${…} buttons in the Project Editor): "output_dir": "build/${config:buildMode}" keeps debug and release artifacts apart. Build, Flash, Clean and hooks all resolve the same expanded paths.
pre_build, post_link, post_build, pre_upload, post_upload command hooks with optional cwd and continue_on_error, editable in the Project Editor (PlatformIO extra_scripts pre:/post: equivalent). Hook commands, args and cwd support the full variable table below.
In the Project Editor, every hook field, the upload tool / args, and output_dir / output_name have a ${…} button that opens a searchable, grouped variable picker — hover a variable for its description in the detail bar, click to insert it at the cursor. ${env:NAME} / ${config:SETTING} insert with the placeholder selected, so you just type over it. The picker opens to the LEFT of the button, like a flyout: its top-right corner sits at the button's top-left corner when it opens down (or its bottom-right corner at the button's bottom-left corner when it opens up), always with a small horizontal gap — never flush against the button and never glued to the window edges; when space is tight the list scrolls inside. It is resizable (drag the bottom-right corner; the size persists for the session). Both menus hide automatically when the editor window is resized or the page scrolls.
Hooks (command / args / cwd), the upload tool and args, and build.output_dir / build.output_name support 40+ variables. ${config:comPort} resolves sdcc.comPort; ${env:NAME} reads any environment variable; ${config:any.setting} reads any VS Code setting (short keys resolve in the sdcc.* namespace first). Unresolved ${env:…} / ${config:…} stay literal — you see them in the failing command instead of a silent empty string.
| Group | Variable | Value |
|---|---|---|
| Project | ${workspaceFolder} |
project root |
${workspaceFolderBasename} |
project folder name | |
${project.name} |
name from sdcc-project.json |
|
| Outputs | ${outputDir} |
build/ (resolved) |
${outputObjDir} |
build/obj/ |
|
${outputName} |
firmware name, no extension | |
${outputIhx} · ${outputHex} · ${outputBin} |
build/firmware.ihx/.hex/.bin |
|
${outputMap} · ${outputMem} |
linker .map / .mem report |
|
${outputLst} |
.lst listing of the main() source |
|
| Board | ${board.mcu} |
e.g. stm8s103f3 |
${board.target} |
SDCC target: mcs51 / stm8 / z80 / … |
|
${board.name} · ${board.cpu} |
board display name / CPU | |
${board.fcpu} |
build.f_cpu from the board definition |
|
${board.memoryModel} |
small / medium / large |
|
${board.iramSize} · ${board.xramSize} · ${board.codeSize} |
memory sizes | |
${board.uploadMaxSize} · ${board.uploadMaxRamSize} |
upload limits | |
| Toolchain | ${toolchain.sdcc} · ${toolchain.dir} |
sdcc path / its folder |
${toolchain.version} |
detected SDCC version | |
${toolchain.packihx} · ${toolchain.makebin} · ${toolchain.sdar} |
companion tools | |
| Git | ${git:hash} · ${git:branch} |
short hash / branch ('' outside a repo) |
| Date & time | ${date} · ${time} · ${datetime} · ${unixtime} |
filename-safe (colons → -) |
| System | ${pathSeparator} · ${userHome} |
\ vs / · home dir |
| Config | ${config:comPort} · ${config:buildMode} |
sdcc.comPort / sdcc.buildMode |
${config:any.setting} |
any VS Code setting (sdcc.* namespace first) |
|
${env:NAME} |
any environment variable |
Examples — version-stamped backup and multi-target flashing:
"build": { "hooks": { "post_build": [
{ "command": "cp", "args": ["${outputHex}", "release/${project.name}_${git:hash}_${date}.hex"] }
] } }"upload": {
"tool": "${board.target}.bat",
"args": ["-p", "${config:comPort}", "-f", "${outputHex}"]
}A project has four separate top-level folders: src/, include/, lib/, components/.
| kind | what | where | config entry |
|---|---|---|---|
| Prebuilt | a ready .lib linked as-is (§3.2.4) |
lib/ |
libraries: ["lib"] (folder → scanned for *.lib); add "vendor/foo.lib" for a single file |
| Source library | a folder of .c → compiled → sdar -rc → linked (§3.2.5) |
components/<name>/ |
components: ["components"] (each subfolder is one library); add a JSON object for a lib elsewhere |
The build is config-driven: a libraries[] / components[] entry that is a folder name is scanned; a .lib file path or JSON object is linked/compiled directly. No hardcoded scan — the entries ARE the scan list. Library include dirs are automatically visible to app code (#include "stm8s.h" just works).
An optional shared root for vendor SDKs that live outside the project — scanned the same way: <externalSdkPath>/lib/*.lib (prebuilt) and <externalSdkPath>/components/<subfolder>/ (source). Also editable in the Project Editor (External SDK folder block).
Configurable upload tool with variable expansion — the tool itself and every arg support the full variable table (${outputIhx}, ${outputBin}, ${outputHex}, ${config:comPort}, ${board.mcu}, ${board.target}, ${env:…}, …). Serial port auto-detection: Linux/macOS /dev scan (ttyUSB/ttyACM/ttyS/cu.usbserial/cu.usbmodem), Windows SerialPort.GetPortNames.
SDCC: Select Toolchain Path shows all auto-detected sdcc locations (PATH / SDCC_HOME / standard install dirs) and offers native browse dialogs for the executable or folder (validated: sdcc or bin/sdcc inside). Companion tools (makebin, packihx, sdar) are found next to sdcc.
SDCC: Configure IntelliSense (C/C++) generates/refreshes one configuration named SDCC in .vscode/c_cpp_properties.json: project include dirs, component include dirs, SDCC's own header roots (<sdcc>/include, <sdcc>/include/<cpu>, non-free tree) and the project + board defines — no more red squiggles for 8051.h / stm8s.h / your include/. Paths inside the project are written as ${workspaceFolder}/… (portable); a hand-written c_cpp_properties.json is never clobbered — only the SDCC entry is touched. New projects get it automatically on creation.
Status bar (Build / Flash / Clean / toolchain / port / build mode) and a tree view with all commands. Colored sidebar icons (charts.* theme colors): Build — green, Flash — blue, Clean — red, boards — purple, libraries — blue, components — cyan, sources/hooks — orange, folders — yellow.
Busy indicator (v0.13.23, ESP8266-IDF-style): while Build / Clean / Flash runs, a spinning ⟳ SDCC: <command> item with a live elapsed timer ((3s) → (1m 05s)) appears leftmost in the status bar, warns with the theme's warning background, explains in its tooltip that all commands are locked until it finishes, and focuses the build terminal on click. When a command completes, its button flashes the result for 4 seconds — ✓ Build OK / ✗ Build Failed, ✓ Flash OK / ✗ Flash Failed, ✓ Clean OK / ✗ Clean Failed — before reverting to the idle label.
my-sdcc-project/
src/ ← application sources (proj.sources → "src")
include/ ← application headers (proj.includes → "include")
lib/ ← prebuilt *.lib (proj.libraries → "lib") — linked as-is (§3.2.4)
components/ ← source libraries (proj.components→ "components") — compiled → sdar → linked (§3.2.5)
sdcc-project.json
.vscode/
Every textarea in the Project Editor shows its default folder name by default (Sources → src, Includes → include, Libraries → lib, Components → components). The build is config-driven: libraries[] and components[] entries are folder names (scanned) or .lib paths / JSON objects (linked directly). No hardcoded scan — the entries ARE the scan list.
{
"name": "blink",
"board": "generic-8051",
"sources": ["src"],
"includes": ["include"],
"defines": ["F_CPU=16000000UL"],
"libraries": ["lib"],
"components": ["components"],
"build": {
"output_dir": "build",
"output_name": "firmware",
"hooks": {
"post_build": [
{ "command": "echo", "args": ["done"], "continue_on_error": true }
]
}
},
"upload": {
"tool": "stm8flash",
"args": ["-c", "stlinkv2", "-p", "stm8s103f3", "-w", "${outputHex}"]
}
}- a string — a folder (scanned recursively for
*.c) or a single file { "path": "src", "pattern": "**/*.c" }{ "path": "src", "files": ["main.c", "utils.c"] }{ "exclude": ["src/test/**"] }
Two ways to attach the SDK:
Option A — as a JSON object in components (when the SDK lives outside components/):
{
"name": "stm8s-app",
"board": "stm8s103f3",
"sources": ["src"],
"includes": ["include"],
"defines": ["STM8S103", "USE_STDPERIPH_DRIVER"],
"libraries": ["lib"],
"components": [
"components",
{
"name": "stm8s_stdperiph",
"path": "vendor/STM8S_StdPeriph_Lib/Libraries/STM8S_StdPeriph_Driver",
"pattern": "src/**/*.c",
"includes": ["inc", "../CMSIS/STM8S"],
"defines": ["STM8S103"]
}
]
}Option B — as a subfolder of components/: copy the SDK under components/stm8s_stdperiph/ so the default components entry scans it automatically. Make sure the layout is components/stm8s_stdperiph/*.c + components/stm8s_stdperiph/inc/ (the inc/ folder is auto-added).
What happens on build (manual §3.2.4/§3.2.5):
- every SDK source is compiled into
build/obj/libs/stm8s_stdperiph/**.relwith the SDK's include dirs (inc,../CMSIS/…— resolved relative to the SDK folder) and defines, using the same target/memory model as the app; sdar -rc build/stm8s_stdperiph.libarchives them;- the app is linked with the library after its own objects.
Notes for the ST SDK specifically:
- copy
stm8s_conf.hfromUtilities/STM8S_StdPeriph_Templates/into yoursrc/and enable the peripherals you use; USE_STDPERIPH_DRIVER+ the device define (STM8S103) must be set (projectdefines— they propagate to library compilation);- the SDK headers are automatically available to your code (
#include "stm8s.h"just works); - a prebuilt
.libcan be linked without recompiling: drop it intolib/or add"vendor/my.lib"tolibraries.
| platformio.ini | sdcc-project.json |
|---|---|
board = stm8s003f3 |
"board": "stm8s103f3" (Board Manager id) |
board_build.f_cpu = 16000000ul |
board definition build.f_cpu (Board Manager) or defines: ["F_CPU=16000000UL"] |
build_flags = --opt-code-size |
"build": { "flags": ["--opt-code-size"] } (compile + link) |
build_src_flags = --opt-code-size |
"build": { "src_flags": ["--opt-code-size"] } (compile only) |
upload_command = flash -c stlinkv2 -w $BUILD_DIR\\firmware.hex |
"upload": { "tool": "flash", "args": ["-c","stlinkv2","-w","${outputHex}"] } |
extra_scripts = pre:use_stm8dce.py |
"build": { "hooks": { "pre_build": ["python use_stm8dce.py"] } } |
extra_scripts = post_build.py |
"build": { "hooks": { "post_build": ["python post_build.py ${outputBin}"] } } (e.g. CRC) |
build_src_filter = -<**/*.asm> |
sources exclude rules: {"exclude":["**/*.asm"]} |
| Setting | Default | Description |
|---|---|---|
sdcc.path |
"" |
Path to sdcc executable; empty = auto-detect |
sdcc.boardsDir |
"" |
Additional folder with board definition JSON files. Boards from it override built-in ones with the same id (priority: project .sdcc/boards/ → this folder → global boards dir → built-in boards/) |
sdcc.comPort |
"" |
Serial port used by ${config:comPort} |
sdcc.shellPath |
"" |
Windows terminal shell override. Leave empty to auto-detect: terminal.integrated.automationProfile.windows → terminal.integrated.defaultProfile.windows (recognizes "Command Prompt" / "PowerShell" / "pwsh" / "Git Bash" / "WSL") → powershell.exe. The detected shell is pinned to the terminal so quoting and exit-code markers always match — no more "Syntax error in filename" |
sdcc.reuseTerminal |
true |
Reuse the SDCC terminal between runs |
sdcc.saveSettingsToWorkspace |
true |
Save port etc. to workspace settings |
sdcc.autoHexBin |
true |
Generate .hex/.bin after build when tools are available |
sdcc.postBuildAction |
none |
flash = run flash automatically after a successful build |
sdcc.buildMode |
release |
debug adds --debug to compile & link flags (SDCC emits .sym/.rst debug info — manual §3.8) and defines DEBUG (inherited by library components). Exposed as ${config:buildMode} |
sdcc.externalSdkPath |
"" |
External SDK folder — scanned for lib/*.lib (prebuilt) and components/<subfolder>/ (source), in addition to the project's own lib/ and components/. Also editable in the Project Editor (External SDK folder block) |
The extension supports the shells it can actually drive with quoting and exit-code markers: PowerShell (5.1 and 7+), cmd.exe, bash, sh, zsh, fish, dash, ksh, csh, tcsh, WSL. On Linux/macOS the system $SHELL is used directly (no detection, no pinning — VS Code opens the terminal with whatever shell it would use anyway).
If the resolved shell is not one of these (e.g. Nushell, elvish, xonsh, ion), a one-time information message per session shows:
SDCC: Detected shell "nu.exe" is not officially supported. Build/flash commands may fail with quoting or exit-code markers. Set "sdcc.shellPath" to override.
with the buttons Configure shell (opens Settings filtered by sdcc.shellPath) and Ignore.
The warning fires at most once per VS Code session — getUserShell() is called many times per build via shellKind(), so without a latch the user would drown in popups. It is suppressed when sdcc.shellPath is set explicitly (you took control, no nudge).
The trade-off is intentional: supporting every shell on Earth is overkill, but a silent build failure (PowerShell syntax sent to a nushell terminal → no marker file → build hangs for 30 minutes) is worse than a one-time actionable hint.
This extension ships as a ready-to-install .vsix. To publish your own build:
- Create a publisher ID at https://marketplace.visualstudio.com/manage and replace
publisherinpackage.jsonwith your own ID. - Update
repository/bugs/homepageURLs to point to your fork. - Install the packaging CLI:
npm i -g @vscode/vsce. - Build the VSIX:
vsce package(producessdcc-mdf-0.11.2.vsix). - Publish:
vsce publish(needs a Personal Access Token from https://dev.azure.com — see the official guide).
MIT © Dzantemir