diff --git a/hw-tests/api-demo/api.h b/hw-tests/api-demo/api.h
new file mode 100644
index 0000000000000000000000000000000000000000..7283a779edf9a1d2325cab3c937b74e0330ba53e
--- /dev/null
+++ b/hw-tests/api-demo/api.h
@@ -0,0 +1,23 @@
+#ifndef _API_H
+#define _API_H
+
+#ifndef API
+#  define API(id, def) def
+#endif
+
+#define API_FOO 0x35c3
+API(API_FOO, void foo(short x, int y, char z, int w));
+
+#define API_BAR 0xc0ffee
+API(API_BAR, void bar(char*astr));
+
+typedef struct {
+    int foo;
+    int bar;
+    int baz;
+} qux_t;
+
+#define API_QUX 0xCCC
+API(API_QUX, void qux(qux_t q));
+
+#endif /* _API_H */
diff --git a/hw-tests/api-demo/genapi.py b/hw-tests/api-demo/genapi.py
new file mode 100644
index 0000000000000000000000000000000000000000..3316875efbb86241ed2ee2884518bd196ebbc2a5
--- /dev/null
+++ b/hw-tests/api-demo/genapi.py
@@ -0,0 +1,111 @@
+import argparse
+import contextlib
+import os
+import re
+import subprocess
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Generate the API stubs from a header file."
+    )
+    parser.add_argument(
+        "-H", "--header", required=True, help="The header to base the definitions on."
+    )
+    parser.add_argument(
+        "-c", "--client", required=True, help="The output client-side c source file."
+    )
+    parser.add_argument(
+        "-s", "--server", required=True, help="The output server-side c source file."
+    )
+    args = parser.parse_args()
+
+    with contextlib.ExitStack() as cx:
+        # Run the preprocessor on the header file to get the API definitions.
+        #
+        # For this, we first need a source to include the header which contains
+        # an alternative definition of the `API` macro that marks definitions in
+        # a way we can find later on.
+        api_src = """\
+#define API(id, def) __GENERATE_API $ __GEN_ID_##id $ def $
+#include "{header}"
+""".format(
+            header=os.path.relpath(args.header)
+        )
+
+        # Evaluate the preprocessor
+        source = subprocess.check_output(
+            ["gcc", "-E", "-"], input=api_src.encode()
+        ).decode()
+
+        # Parse the header for API definitions
+        matcher = re.compile(
+            r"__GENERATE_API \$ __GEN_ID_(?P<id>\w+) \$ (?P<decl>.+?)\((?P<args>.*?)\) \$",
+            re.DOTALL | re.MULTILINE,
+        )
+
+        args_matcher = re.compile(r"(?P<type>\w+(?:\*+|\s+))(?P<name>\w+),")
+
+        # Open output files
+        f_client = cx.enter_context(open(args.client, "w"))
+        f_server = cx.enter_context(open(args.server, "w"))
+
+        print('#include "{}"\n'.format(args.header))
+
+        for match in matcher.finditer(source):
+            api_id = match.group("id")
+            api_decl = match.group("decl")
+            api_args = match.group("args")
+
+            api_args_names = []
+            api_args_types = []
+            api_args_sizes = []
+
+            # Destructure args
+            for match in args_matcher.finditer(api_args + ","):
+                arg_type = match.group("type").strip()
+                arg_name = match.group("name")
+
+                api_args_names.append(arg_name)
+                api_args_types.append(arg_type)
+                api_args_sizes.append("sizeof({})".format(arg_type))
+
+            print(
+                """\
+/* Autogenerated stub for {id} */
+{cdecl}({cargs}) {{
+    const int size = {total_size};
+    unsigned char buffer[size];
+""".format(
+                    id=api_id,
+                    cdecl=api_decl,
+                    cargs=api_args,
+                    total_size=" + ".join(api_args_sizes),
+                )
+            )
+
+            for i, (arg, ty) in enumerate(zip(api_args_names, api_args_types)):
+                print(
+                    """    *({type}*)(buffer + {offset}) = {arg};""".format(
+                        type=ty,
+                        offset=" + ".join(api_args_sizes[:i]) if i > 0 else "0",
+                        arg=arg,
+                    )
+                )
+
+            print(
+                """
+    printf("{id}: ");
+    for (int i = 0; i < size; i++) {{
+        printf("0x%02x ", buffer[i]);
+    }}
+    printf("\\n");
+}}
+""".format(
+                    id=api_id
+                )
+            )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hw-tests/api-demo/main.c b/hw-tests/api-demo/main.c
new file mode 100644
index 0000000000000000000000000000000000000000..41cb01232c5dc181263ad040b01544926687f2ae
--- /dev/null
+++ b/hw-tests/api-demo/main.c
@@ -0,0 +1,22 @@
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "card10.h"
+#include "tmr_utils.h"
+
+int main(void)
+{
+    int count = 0;
+
+    card10_init();
+    card10_diag();
+
+    printf("API Test.\n");
+
+    while(1) {
+        printf("count = %d\n", count++);
+
+        TMR_Delay(MXC_TMR0, SEC(1), 0);
+    }
+}
diff --git a/hw-tests/api-demo/meson.build b/hw-tests/api-demo/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..071241b1f7f56b781f75f8e1519fdd9300729e1c
--- /dev/null
+++ b/hw-tests/api-demo/meson.build
@@ -0,0 +1,11 @@
+name = 'api-demo'
+
+executable(
+  name + '.elf',
+  'main.c',
+  dependencies: [libcard10, max32665_startup],
+  link_whole: [max32665_startup_lib, board_card10_lib],
+  link_args: [
+    '-Wl,-Map=' + meson.current_build_dir() + '/' + name + '.map',
+  ],
+)
diff --git a/hw-tests/meson.build b/hw-tests/meson.build
index 757dccd1a4e6773bf3aa7b909cf3cf427085922b..1c04f9e686ffbd17c7c3023fc443b3309beb379f 100644
--- a/hw-tests/meson.build
+++ b/hw-tests/meson.build
@@ -1,3 +1,4 @@
+subdir('api-demo/')
 subdir('bmatest/')
 subdir('bmetest/')
 subdir('dual-core/')