Repository: tum-ei-eda/mlonmcu (language: python)

Issue:
Declare combinations of features and configs on the command line
A current limitation of the MLonMCU command line interface `mlonmcu flow run` is that all the supplied features and configs are applied globally to all created runs. Such an issue does not exist for the Python API.

The workaround for the described problem would be to either use the Python API or run multiple `mlonmcu flow run` commands after each other. The latter approach has the disadvantage that the runs can not be parallelized which may be relevant depending on the number of specified runs.

Let's discuss if we should get rid of this limitation. Here is my proposal:

- The current command line interface supports 2 ways to pass configs: Either via multiple `--config foo=bar --config a=b` (A) or a single `--config foo=bar a=b` (B).
- We can decide on dropping support for the latter one (B) for the sake of simplicity.
- Then we could a a new argument called i.e. `--configs a=b c=d ...` which behaves like (B) but each one of its arguments creates a new run.
- The `--config` flag can than be used as usual to pass global configs while the `--configs` generates combinations of runs
- The same could be done for the `--feature` flag by introducing a `--features` flag which accepts several arguments.

## Example usage

```
mlonmcu flow run tinymlperf --backend tflmi --target spike --target ovpsim --parallel=8 \
                                            --feature validate --feature debug --config foo=bar \
                                            --features _ muriscvnn cmsisnn \
                                            --configs vext.vlen=64 vext.vlen=128 vext.vlen=256
```


## Limitations
- How to tell that no features should be added in a run? (See example)
- It would not possible to apply multiple non-global features to a single run as long we do not allow something like `--features a,b c` which would be very messy
- We still have issues when for example enabling the `tflmi` backend while passing a TVM-only feature using the `--feature(s)` flag. An approach to solve this would be just skipping the creation of runs which would not be supported instead of failing.





Discussion / hints:
Thanks for putting some thought into this!

Your proposal looks good to me.

Your idea to solve limitation 1 seems reasonable.

We could use the possibility to pass multiple arguments for `--configs` to mitigate limitation 2 by treating each arguments as seperate multiplicative combination. In the example

    --features _ muriscvnn cmsisnn --configs vext.vlen=64 vext.vlen=128 vext.vlen=256 --configs _ tvmaot.target_device=arm_cpu

we would get 3x3x2 runs. Incompatible combinations should only produce an error on the runs, not the whole command. I was not yet aware of specifying multiple `--targets`. Does that also work with backends? How is it handled so far if some config/feature is not available for some backends/targets? Should we then for consistency also allow `--target spike ovpsim`?

The naming should probably be more explicit. How about `--feature_combinations` or `--add_feature_combinations`. Or we go another way by treating everything as combinations by default (like we already do with backend/target?) and add parameters for always-applied options - maybe `--fixed_config`.
@rafzi Thank you for your feedback!

I agree that some consistency would be nice.

> We could use the possibility to pass multiple arguments for --configs to mitigate limitation 2 by treating each arguments as seperate multiplicative combination. In the example

Good idea!

> Does that also work with backends?

As there are no incompatible combinations of targets and backends you can specify as many `--target` and `--backend` flags a you want in a single command. 

> Should we then for consistency also allow --target spike ovpsim?

This would be an option. However argparse might fail to parse sometimes with the arguments a bad way given that the number of arguments required by those flags is not fixed. See this example:

```
mlonmcu flow run sine_model --backend tflmi tvmaot  # works

mlonmcu flow run --backend tflmi tvmaot -v sine_model  # works

mlonmcu flow run --backend tflmi tvmaot sine_model  # does not work. Is sine_model a backend or a model?

mlonmcu flow run --backend tflmi tvmaot - sine_model  # Solution for the above issue
```

I agree that this issue exists anyway however as more we rely on those `nargs="+"` arguments the more likely it gets to run into this kind of stuff. As shown above there are proper workarounds for this (separating the models with and `-` or further arguments) so if we ant to support this, that should be possible.

> The naming should probably be more explicit. How about --feature_combinations or --add_feature_combinations. Or we go another way by treating everything as combinations by default (like we already do with backend/target?) and add parameters for always-applied options - maybe --fixed_config.

I think we can agree on a good naming. I am open for those proposals but would prefer if the shorthand flag for the global configs would still be (`-c`) which is way more comfortable to type.
> Incompatible combinations should only produce an error on the runs, not the whole command

Error occurring during the run will result in a `Failing` column being added to the report. If there was at least one failing run in the report, the return code of `mlonmcu flow run` is going to be non-zero. We could change this behavior if required. The initialization of features happens before the processing of the run an thus result in exceptions/assertions which might not be catched properly. So maybe we need to distinguish between incompatible and failing runs in the end.

> How is it handled so far if some config/feature is not available for some backends/targets?

Configs which do not apply to a given component (feature/target/backend/...) are just ignored.

Fix patch:
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
index 7cb8a0099..d94d380be 100644
--- a/.github/workflows/bench.yml
+++ b/.github/workflows/bench.yml
@@ -20,38 +20,72 @@ name: Run Benchmarks
 on:
   workflow_dispatch:
     inputs:
-      bench_image:
-        description: 'Base Docker Image'
+      template:
+        description: 'MLonMCU Environment Template'
         required: true
-        default: "ghcr.io/tum-ei-eda/mlonmcu-bench:latest"
-      bench_cmd:
-        description: 'Benchmarking Command'
+        default: "default"
+      benchmark:
+        description: 'Benchmark Name'
         required: true
-        default: "flow run tinymlperf"
+        default: ""
 
 jobs:
-  bench:
-    name: Run a single benchmark
+  demo:
     runs-on: ubuntu-latest
-    container: "${{ github.event.inputs.bench_image }}"
+    strategy:
+      matrix:
+        python-version: [3.8]
     steps:
-    - name: Run Benchmark command in container
-      run: python3 -m mlonmcu.cli.main ${{ github.event.inputs.bench_cmd }}
-    - name: Store cmdline
-      run: echo "${{ github.event.inputs.bench_cmd }}" >> /environment/results/cmd.txt
+    - uses: actions/checkout@v2
+      with:
+        submodules: recursive
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+    # TODO: caching
+    - name: Install APT packages
+      run: |
+        sudo apt -qq install -y g++ libboost-all-dev libboost-system-dev libboost-filesystem-dev libboost-program-options-dev graphviz doxygen libtinfo-dev zlib1g-dev texinfo unzip device-tree-compiler
+        sudo apt -qq install -y ninja-build
+        sudo apt remove clang-11* llvm-11*
+    - name: Initialize Virtualenv
+      run: |
+        python -m pip install --upgrade pip
+        python -m venv .venv
+    - name: Install dependencies
+      run: |
+        source .venv/bin/activate
+        pip install -r requirements.txt
+    - name: Run package creation
+      run: |
+        source .venv/bin/activate
+        make install
+    - name: Initialize MLonMCU environment
+      run: |
+        source .venv/bin/activate
+        mlonmcu init home/ --non-interactive --template ${{ github.event.inputs.template }}
+    - name: Setup MLonMCU dependencies
+      run: |
+        source .venv/bin/activate
+        mlonmcu setup -H home/ -v
+    - name: Run Benchmark
+      run: |
+        source .venv/bin/activate
+        cd scripts/
+        python bench.py ${{ github.event.inputs.benchmark }} out/
     - name: Archive reports
       uses: actions/upload-artifact@v2
       with:
         name: results
-        path: /environment/results/
-    # Pushing results to a branch is not possible right now as we are inside a docker container!
-    # - name: Get date
-    #   run: echo "timestamp=`date +%FT%T`" >> $GITHUB_ENV
-    # - name: Install RSync
-    #   run: apt install -y rsync
-    # - name: Upload results to a special branch
-    #   uses: JamesIves/github-pages-deploy-action@4.1.1
-    #   with:
-    #     folder: /environment/results/
-    #     branch: benchmark_results
-    #     target-folder: ${{ env.timestamp }}/
+        path: scripts/out/
+    - name: Get date
+      run: echo "timestamp=`date +%FT%T`" >> $GITHUB_ENV
+    - name: Install RSync
+      run: apt install -y rsync
+    - name: Upload results to a special branch
+      uses: JamesIves/github-pages-deploy-action@4.1.1
+      with:
+        folder: scripts/out/
+        branch: benchmark_results
+        target-folder: benchmarks/${{ github.event.inputs.benchmark }}/${{ env.timestamp }}/
diff --git a/.github/workflows/docker_bench.yml b/.github/workflows/docker_bench.yml
new file mode 100644
index 000000000..7cb8a0099
--- /dev/null
+++ b/.github/workflows/docker_bench.yml
@@ -0,0 +1,57 @@
+##
+## Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+##
+## This file is part of MLonMCU.
+## See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+##
+## Licensed under the Apache License, Version 2.0 (the "License");
+## you may not use this file except in compliance with the License.
+## You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+##
+name: Run Benchmarks
+on:
+  workflow_dispatch:
+    inputs:
+      bench_image:
+        description: 'Base Docker Image'
+        required: true
+        default: "ghcr.io/tum-ei-eda/mlonmcu-bench:latest"
+      bench_cmd:
+        description: 'Benchmarking Command'
+        required: true
+        default: "flow run tinymlperf"
+
+jobs:
+  bench:
+    name: Run a single benchmark
+    runs-on: ubuntu-latest
+    container: "${{ github.event.inputs.bench_image }}"
+    steps:
+    - name: Run Benchmark command in container
+      run: python3 -m mlonmcu.cli.main ${{ github.event.inputs.bench_cmd }}
+    - name: Store cmdline
+      run: echo "${{ github.event.inputs.bench_cmd }}" >> /environment/results/cmd.txt
+    - name: Archive reports
+      uses: actions/upload-artifact@v2
+      with:
+        name: results
+        path: /environment/results/
+    # Pushing results to a branch is not possible right now as we are inside a docker container!
+    # - name: Get date
+    #   run: echo "timestamp=`date +%FT%T`" >> $GITHUB_ENV
+    # - name: Install RSync
+    #   run: apt install -y rsync
+    # - name: Upload results to a special branch
+    #   uses: JamesIves/github-pages-deploy-action@4.1.1
+    #   with:
+    #     folder: /environment/results/
+    #     branch: benchmark_results
+    #     target-folder: ${{ env.timestamp }}/
diff --git a/README.md b/README.md
index 8f9d66548..29c9bdc6f 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,7 @@ make install  # Alternative: python setup.py install
 
 #### Docker (Any other OS)
 
-See [`./docker/README.md`](htps://github.com/tum-ei-eda/mlonmcu/blob/main/docker/README.md) for more details.
+See [`./docker/README.md`](https://github.com/tum-ei-eda/mlonmcu/blob/main/docker/README.md) for more details.
 
 This repository ships three different types of docker images based on Debian:
 
diff --git a/docs/components.rst b/docs/components.rst
index 10a42cc98..3d540da60 100644
--- a/docs/components.rst
+++ b/docs/components.rst
@@ -1,2 +1,13 @@
 .. include:: COMPONENTS.md
    :parser: myst_parser.sphinx_
+
+
+Components in Detail
+====================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Components:
+
+   components/cmsisnnbyoc
+   components/muriscvnnbyoc
diff --git a/docs/components/AUTO_VECTORIZE.md b/docs/components/AUTO_VECTORIZE.md
new file mode 100644
index 000000000..a9d2fef4e
--- /dev/null
+++ b/docs/components/AUTO_VECTORIZE.md
@@ -0,0 +1,27 @@
+# Auto-Vectorize Feature
+
+## Usage
+
+- Add `-f auto_vectorize` to the command line arguments
+
+## TODOs:
+
+- [ ] Configure loop and basic block vectorizer individually.
+
+## Warning
+- Auto vectorization is enables by default (if available) on the following optimization levels:
+
+  - GCC: `-O2`
+  - LLVM: `-O1`
+
+## Configuration
+
+- `auto_vectorize.enable`: Allows to turn off ne auto-vectorization completely (Default: `true`)
+- `auto_vectorize.verbose`: Print details about auto vectorization possibilities during compililation. Need to check the MLID stdout artifact or enable `mlif.print_outputs` to be effective (Default: `false`)
+
+## Compatibility
+
+- Only RISC-V targets is supported at the momemt
+- The supported MLIF toolchains are GCC and LLVM
+- A VLEN larger equals 128 is required for this feature
+- It seems like this currently needs a ELEN=64 and proper alignment (e.g. `tvmaot.alignment_bytes=8`) for the backend data. TFLMI seems to break with this.
diff --git a/docs/components/CMSISNNBYOC.md b/docs/components/CMSISNNBYOC.md
new file mode 100644
index 000000000..56908e2bb
--- /dev/null
+++ b/docs/components/CMSISNNBYOC.md
@@ -0,0 +1,9 @@
+# CMSIS-NN BYOC Feature
+
+## Usage
+
+- The used extensions have to be manually selected by setting `cmsisnnbyoc.mcpu` on the command line
+
+## Compatibility
+
+- The `cmsisnnbyoc` feature is not compatible with the `desired_layout` config for the TVM targets
diff --git a/docs/components/MURISCVNNBYOC.md b/docs/components/MURISCVNNBYOC.md
new file mode 100644
index 000000000..02a11c569
--- /dev/null
+++ b/docs/components/MURISCVNNBYOC.md
@@ -0,0 +1,10 @@
+# muRISCV-NN BYOC Feature
+
+## Usage
+
+- The used extensions have to be manually selected by setting `muriscvnnbyoc.mcpu` on the command line
+- Example mapping: `cortex-m55 -> VEXT`, `cortex-m33 -> PEXT`, `cortex-m0 -> No extensions`
+
+## Compatibility
+
+- The `muriscvnnbyoc` feature is not compatible with the `desired_layout` config for the TVM targets
diff --git a/docs/components/auto_vectorize.rst b/docs/components/auto_vectorize.rst
new file mode 100644
index 000000000..0390dcbf5
--- /dev/null
+++ b/docs/components/auto_vectorize.rst
@@ -0,0 +1,2 @@
+.. include:: AUTO_VECTORIZE.md
+   :parser: myst_parser.sphinx_
diff --git a/docs/components/cmsisnnbyoc.rst b/docs/components/cmsisnnbyoc.rst
new file mode 100644
index 000000000..a169c9667
--- /dev/null
+++ b/docs/components/cmsisnnbyoc.rst
@@ -0,0 +1,2 @@
+.. include:: CMSISNNBYOC.md
+   :parser: myst_parser.sphinx_
diff --git a/docs/components/muriscvnnbyoc.rst b/docs/components/muriscvnnbyoc.rst
new file mode 100644
index 000000000..7e3257a92
--- /dev/null
+++ b/docs/components/muriscvnnbyoc.rst
@@ -0,0 +1,2 @@
+.. include:: MURISCVNNBYOC.md
+   :parser: myst_parser.sphinx_
diff --git a/mlonmcu/cli/build.py b/mlonmcu/cli/build.py
index 07c6535fd..bdf2fc8ad 100644
--- a/mlonmcu/cli/build.py
+++ b/mlonmcu/cli/build.py
@@ -23,8 +23,13 @@
 from mlonmcu.cli.common import kickoff_runs
 from mlonmcu.cli.load import handle as handle_load, add_load_options
 from mlonmcu.session.run import RunStage
-from mlonmcu.platform.lookup import get_platforms_targets
-from .helper.parse import extract_backend_names, extract_target_names, extract_platform_names
+from mlonmcu.platform.lookup import get_platforms_targets, get_platforms_backends
+from .helper.parse import (
+    extract_backend_names,
+    extract_target_names,
+    extract_platform_names,
+    extract_config_and_feature_names,
+)
 
 
 def add_build_options(parser):
@@ -60,7 +65,9 @@ def _handle(args, context):
     targets = extract_target_names(args, context=None)
     platforms = extract_platform_names(args, context=context)
 
-    platform_targets = get_platforms_targets(context)  # This will be slow?
+    new_config, _, _, _ = extract_config_and_feature_names(args, context=context)
+    platform_backends = get_platforms_backends(context, config=new_config)  # This will be slow?
+    platform_targets = get_platforms_targets(context, config=new_config)  # This will be slow?
 
     assert len(context.sessions) > 0
     session = context.sessions[-1]
@@ -69,6 +76,23 @@ def _handle(args, context):
         for target_name in targets:
             for backend_name in backends:
                 new_run = run.copy()
+                if backend_name is not None:
+                    platform_name = None
+                    for platform in platforms:
+                        candidates = platform_backends[platform]
+                        if backend_name in candidates:
+                            try:
+                                platform_name = platform
+                                new_run.add_platform_by_name(platform_name, context=context)
+                                break
+                            except AssertionError:  # TODO: replace with incompatble error
+                                platform_name = None
+                                continue
+                    # assert (
+                    #     platform_name is not None
+                    # ), f"Unable to find a suitable platform for the backend '{target_name}'"
+                    # assert platform_name is not None
+
                 if target_name is not None:
                     platform_name = None
                     for platform in platforms:
diff --git a/mlonmcu/cli/common.py b/mlonmcu/cli/common.py
index 8ff9bb5ea..9e2808b29 100644
--- a/mlonmcu/cli/common.py
+++ b/mlonmcu/cli/common.py
@@ -90,18 +90,37 @@ def add_flow_options(parser):
         choices=get_available_feature_names(),
         help="Enabled features for target/framework/backend (choices: %(choices)s)",
     )
+    flow_parser.add_argument(
+        "--feature-gen",
+        dest="feature_gen",
+        type=str,
+        metavar="FEATURES",
+        nargs="+",
+        action="append",
+        help="Generator statement for features.",
+    )
     flow_parser.add_argument(
         "-c",
         "--config",
         metavar="KEY=VALUE",
         nargs="+",
         action="append",
-        help="Set a number of key-value pairs "
-        "(do not put spaces before or after the = sign). "
-        "If a value contains spaces, you should define "
-        "it with double quotes: "
-        'foo="this is a sentence". Note that '
-        "values are always treated as strings.",
+        help=(
+            "Set a number of key-value pairs "
+            "(do not put spaces before or after the = sign). "
+            "If a value contains spaces, you should define "
+            "it with double quotes: "
+            'foo="this is a sentence". Note that '
+            "values are always treated as strings."
+        ),
+    )
+    flow_parser.add_argument(
+        "--config-gen",
+        dest="config_gen",
+        metavar="KEY=VALUE",
+        nargs="+",
+        action="append",
+        help="Generator statement for configs.",
     )
     flow_parser.add_argument(
         "--parallel",
diff --git a/mlonmcu/cli/compile.py b/mlonmcu/cli/compile.py
index 78c72a85f..498b8b4ce 100644
--- a/mlonmcu/cli/compile.py
+++ b/mlonmcu/cli/compile.py
@@ -26,18 +26,12 @@
 )
 from mlonmcu.session.run import RunStage
 from mlonmcu.platform.lookup import get_platforms_targets
-from .helper.parse import extract_target_names, extract_platform_names
+from .helper.parse import extract_target_names, extract_platform_names, extract_config_and_feature_names
 
 
 def add_compile_options(parser):
     add_build_options(parser)
-    compile_parser = parser.add_argument_group("compile options")
-    compile_parser.add_argument(
-        "--num",
-        action="append",
-        type=int,
-        help="Number of runs in simulation (default: %(default)s)",
-    )
+    # compile_parser = parser.add_argument_group("compile options")
 
 
 def get_parser(subparsers):
@@ -52,32 +46,30 @@ def _handle(args, context):
     handle_build(args, ctx=context)
     targets = extract_target_names(args, context=context)  # This will eventually be ignored below
     platforms = extract_platform_names(args, context=context)
-    num = args.num if args.num else [1]
 
-    platform_targets = get_platforms_targets(context)  # This will slow?
+    new_config, _, _, _ = extract_config_and_feature_names(args, context=context)
+    platform_targets = get_platforms_targets(context, config=new_config)  # This will slow?
 
     assert len(context.sessions) > 0  # TODO: automatically request session if no active one is available
     session = context.sessions[-1]
     new_runs = []
     for run in session.runs:
-        for n in num:
-            if run.target is None:
-                assert run.compile_platform is None
-                targets_ = targets
-            else:
-                targets_ = [None]
-            for target_name in targets_:
-                new_run = run.copy()
-                if target_name is not None:
-                    platform_name = None
-                    for platform in platforms:
-                        candidates = platform_targets[platform]
-                        if target_name in candidates:
-                            platform_name = platform
-                    new_run.add_platform_by_name(platform_name, context=context)
-                    new_run.add_target_by_name(target_name, context=context)
-                new_run.num = n
-                new_runs.append(new_run)
+        if run.target is None:
+            assert run.compile_platform is None
+            targets_ = targets
+        else:
+            targets_ = [None]
+        for target_name in targets_:
+            new_run = run.copy()
+            if target_name is not None:
+                platform_name = None
+                for platform in platforms:
+                    candidates = platform_targets[platform]
+                    if target_name in candidates:
+                        platform_name = platform
+                new_run.add_platform_by_name(platform_name, context=context)
+                new_run.add_target_by_name(target_name, context=context)
+            new_runs.append(new_run)
     session.runs = new_runs
 
 
diff --git a/mlonmcu/cli/helper/parse.py b/mlonmcu/cli/helper/parse.py
index 81165a4b1..1936359cd 100644
--- a/mlonmcu/cli/helper/parse.py
+++ b/mlonmcu/cli/helper/parse.py
@@ -55,8 +55,20 @@ def parse_vars(items):  # TODO: this needs to be used in other subcommands as we
 
 def extract_feature_names(args):
     if args.feature:
-        return args.feature
-    return []
+        features = args.feature
+    else:
+        features = []
+    if hasattr(args, "feature_gen") and args.feature_gen:
+        gen = []
+        for x in args.feature_gen:
+            if "_" in x:
+                assert len(x) == 1
+                gen.append([])
+            else:
+                gen.append(x)
+    else:
+        gen = [[]]
+    return features, gen
 
 
 def extract_config(args):
@@ -65,14 +77,26 @@ def extract_config(args):
         configs = parse_vars(configs)
     else:
         configs = {}
-    return configs
+    if hasattr(args, "config_gen") and args.config_gen:
+        gen = []
+        for x in args.config_gen:
+            if "_" in x:
+                assert len(x) == 1
+                gen.append({})
+            else:
+                c = parse_vars(x)
+                gen.append(c)
+    else:
+        gen = [{}]
+
+    return configs, gen
 
 
 def extract_config_and_feature_names(args, context=None):
     # TODO: get features from context?
-    feature_names = extract_feature_names(args)
-    config = extract_config(args)
-    return config, feature_names
+    feature_names, feature_gen = extract_feature_names(args)
+    config, config_gen = extract_config(args)
+    return config, feature_names, config_gen, feature_gen
 
 
 def extract_frontend_names(args, context=None):
diff --git a/mlonmcu/cli/load.py b/mlonmcu/cli/load.py
index 849149d34..18a3eac80 100644
--- a/mlonmcu/cli/load.py
+++ b/mlonmcu/cli/load.py
@@ -60,18 +60,22 @@ def get_parser(subparsers):
 
 def _handle(args, context):
     config = context.environment.vars
-    new_config, features = extract_config_and_feature_names(args, context=context)
+    new_config, features, gen_config, gen_features = extract_config_and_feature_names(args, context=context)
     config.update(new_config)
     frontends = extract_frontend_names(args, context=context)
     postprocesses = extract_postprocess_names(args, context=context)
     session = context.get_session(label=args.label, resume=args.resume, config=config)
     models = apply_modelgroups(args.models, context=context)
     for model in models:
-        run = session.create_run(config=config)
-        run.add_features_by_name(features, context=context)  # TODO do this before load.py?
-        run.add_frontends_by_name(frontends, context=context)
-        run.add_model_by_name(model, context=context)
-        run.add_postprocesses_by_name(postprocesses, context=context)  # TODO do this before load.py?
+        for f in gen_features:
+            for c in gen_config:
+                all_config = {**config, **c}
+                run = session.create_run(config=all_config)
+                all_features = list(set(features + f))
+                run.add_features_by_name(all_features, context=context)  # TODO do this before load.py?
+                run.add_frontends_by_name(frontends, context=context)
+                run.add_model_by_name(model, context=context)
+                run.add_postprocesses_by_name(postprocesses, context=context)  # TODO do this before load.py?
 
 
 def handle(args, ctx=None):
diff --git a/mlonmcu/cli/setup.py b/mlonmcu/cli/setup.py
index 49dd18805..d8fa39665 100644
--- a/mlonmcu/cli/setup.py
+++ b/mlonmcu/cli/setup.py
@@ -48,12 +48,14 @@ def add_setup_options(parser):
         metavar="KEY=VALUE",
         nargs="+",
         action="append",
-        help="Set a number of key-value pairs "
-        "(do not put spaces before or after the = sign). "
-        "If a value contains spaces, you should define "
-        "it with double quotes: "
-        'foo="this is a sentence". Note that '
-        "values are always treated as strings.",
+        help=(
+            "Set a number of key-value pairs "
+            "(do not put spaces before or after the = sign). "
+            "If a value contains spaces, you should define "
+            "it with double quotes: "
+            'foo="this is a sentence". Note that '
+            "values are always treated as strings."
+        ),
     )
     setup_parser.add_argument(
         "-p",
@@ -80,6 +82,12 @@ def add_setup_options(parser):
         default=None,
         help="Invoke a single task manually by name (default: %(default)s)",
     )
+    setup_parser.add_argument(
+        "--visualize",
+        type=str,
+        nargs=1,
+        help="Export a .dot file for the dependency graph.",
+    )
 
 
 def get_parser(subparsers):
@@ -95,10 +103,12 @@ def get_parser(subparsers):
 def handle(args):
     with mlonmcu.context.MlonMcuContext(path=args.home, lock=True) as context:
         # config, features = extract_config_and_init_features(args)
-        config, _ = extract_config_and_feature_names(args)
+        config, _, _, _ = extract_config_and_feature_names(args)
         # installer = setup.Setup(features=features, config=config, context=context)
         installer = setup.Setup(config=config, context=context)
-        if args.list:
+        if args.visualize:
+            installer.visualize(args.visualize[0], ordered=False)
+        elif args.list:
             order = installer.get_dependency_order()
             print("The following tasks have been selected:")
             print("\n".join(["- " + key for key in order]))
diff --git a/mlonmcu/config.py b/mlonmcu/config.py
index 7f0a17d86..82ad287f2 100644
--- a/mlonmcu/config.py
+++ b/mlonmcu/config.py
@@ -53,7 +53,7 @@ def helper(key):
     return {helper(key): value for key, value in config.items() if f"{prefix}." in key and key not in skip}
 
 
-def filter_config(config, prefix, defaults, required_keys):
+def filter_config(config, prefix, defaults, optionals, required_keys):
     """Filter the global config for a given component prefix.
 
     Arguments
@@ -64,6 +64,8 @@ def filter_config(config, prefix, defaults, required_keys):
         The prefix for the component.
     defaults : dict
         The default values used if not overwritten by user.
+    optionals : dict
+        The optional keys for the component.
     required_keys : list
         The required keys for the component.
 
@@ -76,7 +78,7 @@ def filter_config(config, prefix, defaults, required_keys):
     ------
     AssertionError: If a required key is missing.
     """
-    cfg = remove_config_prefix(config, prefix, skip=required_keys)
+    cfg = remove_config_prefix(config, prefix, skip=required_keys + optionals)
     for required in required_keys:
         value = None
         if required in cfg:
@@ -86,6 +88,15 @@ def filter_config(config, prefix, defaults, required_keys):
             cfg[required] = value
         assert value is not None, f"Required config key can not be None: {required}"
 
+    for optional in optionals:
+        if optional in cfg:
+            continue
+        if optional in config:
+            value = config[optional]
+            cfg[optional] = value
+        else:
+            cfg[optional] = None
+
     for key in defaults:
         if key not in cfg:
             cfg[key] = defaults[key]
@@ -99,7 +110,7 @@ def filter_config(config, prefix, defaults, required_keys):
 
 
 def resolve_required_config(
-    required_keys, features=None, config=None, cache=None, hints=None
+    required_keys, optional=None, features=None, config=None, cache=None, hints=None, default_flags=None
 ):  # TODO: add framework, backend, and frontends as well?
     """Utility which iterates over a set of given config keys and
     resolves their values using the passed config and/or cache.
@@ -118,6 +129,9 @@ def resolve_required_config(
     hints : List[str]
         List of additional flags which can be provided as a hint to lookup a cache config.
 
+    default_flags : dict
+        User-provided mapping of cache flags for some cache entries.
+
     Returns
     -------
     result : dict
@@ -134,8 +148,11 @@ def get_sublists(in_list):
 
     hint_combinations = get_sublists(hints if hints else [])
 
-    def get_cache_flags(features):
+    def get_cache_flags(features, default):
         result = {}
+        if default:
+            # result.update({key: frozenset(value) for key, value in default.items()})
+            result.update(default)
         if features:
             for feature in features:
                 if FeatureType.SETUP in type(feature).types():
@@ -143,7 +160,7 @@ def get_cache_flags(features):
         return result
 
     ret = {}
-    cache_flags = get_cache_flags(features)
+    cache_flags = get_cache_flags(features, default_flags)
     for key in required_keys:
         if config is None or key not in config:
             assert cache is not None, "No dependency cache was provided. Either provide a cache or config."
@@ -168,6 +185,23 @@ def get_cache_flags(features):
             ret[key] = value
         else:
             ret[key] = config[key]
+    if optional:
+        for key in optional:
+            if config is None or key not in config:
+                assert cache is not None, "No dependency cache was provided. Either provide a cache or config."
+                if cache is None or len(cache) == 0:
+                    continue
+                flags = cache_flags.get(key, ())
+                value = None
+                for hint_combination in hint_combinations:
+                    if (key, tuple(list(flags) + hint_combination)) in cache:
+                        value = cache[key, flags]
+                        break
+                if value is None:
+                    continue
+                ret[key] = value
+            else:
+                ret[key] = config[key]
 
     return ret
 
diff --git a/mlonmcu/data/__init__.py b/mlonmcu/data/__init__.py
deleted file mode 100644
index db85df904..000000000
--- a/mlonmcu/data/__init__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
-#
-# This file is part of MLonMCU.
-# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
diff --git a/mlonmcu/environment/environment.py b/mlonmcu/environment/environment.py
index c173fe0d9..cd3448012 100644
--- a/mlonmcu/environment/environment.py
+++ b/mlonmcu/environment/environment.py
@@ -67,8 +67,10 @@ def __init__(self):
         self.frameworks = []
         self.frontends = []
         self.platforms = []
+        self.toolchains = []
         self.targets = []
         self.vars = {}
+        self.flags = {}
 
     def __str__(self):
         return self.__class__.__name__ + "(" + str(vars(self)) + ")"
@@ -288,6 +290,9 @@ def has_platform(self, name):
         configs = self.lookup_platform_configs(platform=name)
         return len(configs) > 0
 
+    def has_toolchain(self, name):
+        return self.toolchains.get(name, False)
+
     def has_target(self, name):
         configs = self.lookup_target_configs(target=name)
         return len(configs) > 0
@@ -420,6 +425,7 @@ def __init__(self):
         self.vars = {
             "TEST": "abc",
         }
+        self.flags = {}
         self.platforms = [
             PlatformConfig(
                 "mlif",
@@ -427,6 +433,7 @@ def __init__(self):
                 features=[PlatformFeatureConfig("debug", platform="mlif", supported=True)],
             )
         ]
+        self.toolchains = {}
         self.targets = [
             TargetConfig(
                 "etiss_pulpino",
@@ -458,8 +465,10 @@ def __init__(
         frameworks=None,
         frontends=None,
         platforms=None,
+        toolchains=None,
         targets=None,
         variables=None,
+        default_flags=None,
     ):
         super().__init__()
         self._home = home
@@ -481,7 +490,11 @@ def __init__(
             self.frontends = frontends
         if platforms:
             self.platforms = platforms
+        if toolchains:
+            self.toolchains = toolchains
         if targets:
             self.targets = targets
         if variables:
             self.vars = variables
+        if default_flags:
+            self.flags = default_flags
diff --git a/mlonmcu/environment/loader.py b/mlonmcu/environment/loader.py
index 24a79dfc9..5ff88ad51 100644
--- a/mlonmcu/environment/loader.py
+++ b/mlonmcu/environment/loader.py
@@ -194,6 +194,10 @@ def load_environment_from_file(filename, base):
                 platforms.append(PlatformConfig(key, enabled=enabled, features=platform_features))
         else:
             platforms = None
+        if "toolchains" in loaded:
+            toolchains = loaded["toolchains"]
+        else:
+            toolchains = None
         default_target = None
         if "targets" in loaded:
             targets = []
@@ -216,6 +220,10 @@ def load_environment_from_file(filename, base):
             variables = loaded["vars"]
         else:
             variables = None
+        if "flags" in loaded:
+            default_flags = loaded["flags"]
+        else:
+            default_flags = None
         defaults = DefaultsConfig(
             log_level=log_level,
             log_to_file=log_to_file,
@@ -234,7 +242,9 @@ def load_environment_from_file(filename, base):
             frameworks=frameworks,
             frontends=frontends,
             platforms=platforms,
+            toolchains=toolchains,
             targets=targets,
             variables=variables,
+            default_flags=default_flags,
         )
         return env
diff --git a/mlonmcu/environment/templates.py b/mlonmcu/environment/templates.py
index 3ae81868a..2cfa79805 100644
--- a/mlonmcu/environment/templates.py
+++ b/mlonmcu/environment/templates.py
@@ -47,7 +47,7 @@ def fill_template(name, data={}):
         if not isinstance(template_text, str):
             try:
                 template_text = template_text.decode("utf-8")
-            except (UnicodeDecodeError):
+            except UnicodeDecodeError:
                 pass
         tmpl = jinja2.Template(template_text)
         rendered = tmpl.render(**data)
diff --git a/mlonmcu/environment/writer.py b/mlonmcu/environment/writer.py
index dcb49542f..94175da79 100644
--- a/mlonmcu/environment/writer.py
+++ b/mlonmcu/environment/writer.py
@@ -92,6 +92,7 @@ def create_environment_dict(environment):
             for platform in environment.platforms
         },
     }
+    data["toolchains"] = environment.toolchains
     data["targets"] = {
         "default": environment.defaults.default_target,
         **{
@@ -103,6 +104,7 @@ def create_environment_dict(environment):
         },
     }
     data["vars"] = environment.vars
+    data["flags"] = environment.flags
     return data
 
 
diff --git a/mlonmcu/feature/feature.py b/mlonmcu/feature/feature.py
index 2b7b11e37..fa1166ae7 100644
--- a/mlonmcu/feature/feature.py
+++ b/mlonmcu/feature/feature.py
@@ -35,11 +35,12 @@ class FeatureBase(ABC):
 
     DEFAULTS = {"enabled": True}
     REQUIRED = []
+    OPTIONAL = []
 
     def __init__(self, name, features=None, config=None):
         self.name = name
         self.config = config if config else {}
-        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.REQUIRED)
+        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
         # assert features is None, "Features with features are currently not supported"
         # just ignore the supplied feature list for now
 
@@ -205,7 +206,8 @@ def add_required_cache_flags(self, required_flags):
         own_flags = self.get_required_cache_flags()
         for key, flags in own_flags.items():
             if key in required_flags:
-                required_flags[key].append(flags)
+                # remove duplicates along the way
+                required_flags[key] = list(set(required_flags[key] + flags))
             else:
                 required_flags[key] = flags
 
diff --git a/mlonmcu/feature/features.py b/mlonmcu/feature/features.py
index e3f59e281..032901e35 100644
--- a/mlonmcu/feature/features.py
+++ b/mlonmcu/feature/features.py
@@ -19,7 +19,7 @@
 """Definition of MLonMCU features and the feature registry."""
 
 import re
-from pathlib import Path
+import pandas as pd
 
 from mlonmcu.utils import is_power_of_two
 from mlonmcu.config import str2bool
@@ -38,6 +38,7 @@
 # from mlonmcu.flow import SUPPORTED_TVM_BACKENDS
 SUPPORTED_TVM_BACKENDS = [
     "tvmaot",
+    "tvmaotplus",
     "tvmrt",
     "tvmcg",
     "tvmllvm",
@@ -316,6 +317,7 @@ class Vext(SetupFeature, TargetFeature, PlatformFeature):
     DEFAULTS = {
         **FeatureBase.DEFAULTS,
         "vlen": 64,  # TODO; define reasonable default? (Or put defaults in target and overwrite of not None)
+        "elen": 32,
     }
 
     REQUIRED = []
@@ -327,13 +329,18 @@ def __init__(self, features=None, config=None):
     def vlen(self):
         return int(self.config["vlen"])
 
+    @property
+    def elen(self):
+        return int(self.config["elen"])
+
     def get_target_config(self, target):
         # TODO: enforce llvm toolchain using add_compile_config and CompileFeature?
-        assert target in ["spike", "ovpsim", "etiss_pulpino"]
+        assert target in ["spike", "ovpsim", "etiss_pulpino", "riscv_qemu"]
         assert is_power_of_two(self.vlen)
         return {
             f"{target}.enable_vext": True,
             f"{target}.vlen": self.vlen,
+            f"{target}.elen": self.elen,
         }
 
     # It would be great if we could enforce an llvm toolchain here
@@ -425,7 +432,7 @@ def port(self):
         return int(self.config["port"]) if self.config["port"] is not None else None
 
     def get_target_config(self, target):
-        assert target in ["host_x86", "etiss_pulpino"]
+        assert target in ["host_x86", "etiss_pulpino", "ovpsim"]
         return filter_none(
             {
                 f"{target}.gdbserver_enable": self.enabled,
@@ -454,6 +461,8 @@ def get_target_config(self, target):
 class Trace(TargetFeature):
     """Enable tracing of all memory accesses in ETISS."""
 
+    # TODO: support ovpsim --trace --tracemem SA
+
     def __init__(self, features=None, config=None):
         super().__init__("trace", features=features, config=config)
 
@@ -508,19 +517,6 @@ def get_frontend_config(self, frontend):
         return {f"{frontend}.pack_weights": self.enabled}
 
 
-@register_feature("memplan")
-class Memplan(FrameworkFeature):
-    """Custom TVM memory planning feature by (@rafzi)"""
-
-    def __init__(self, features=None, config=None):
-        super().__init__("memplan", features=features, config=config)
-
-    def get_framework_config(self, framework):
-        assert framework in ["tvm"], f"Unsupported feature '{self.name}' for framework '{framework}'"
-        raise NotImplementedError
-        return {"tvm.memplan_enable": self.enabled}
-
-
 @register_feature("usmp")
 class Usmp(BackendFeature):
     """Unified Static Memory Planning algorithm integrated in TVM"""
@@ -691,7 +687,7 @@ class Autotune(BackendFeature, RunFeature):
         "results_file": None,
         "append": None,
         "tuner": None,
-        "trial": None,
+        "trials": None,
         "early_stopping": None,
         "num_workers": None,
         "max_parallel": None,
@@ -762,35 +758,6 @@ def get_run_config(self):
         return {"run.tune_enabled": self.enabled}
 
 
-@register_feature("fallback")
-class Fallback(FrameworkFeature, PlatformFeature):
-    """(Unimplemented) TFLite Fallback for unsupported and custom operators in TVM."""
-
-    DEFAULTS = {
-        **FeatureBase.DEFAULTS,
-        "config_file": None,
-    }
-
-    def __init__(self, features=None, config=None):
-        super().__init__("fallback", features=features, config=config)
-
-    @property
-    def config_file(self):
-        return str(self.config["config_file"]) if "config_file" in self.config else None
-
-    def get_framework_config(self, framework):
-        assert framework in ["tvm"], f"Unsupported feature '{self.name}' for framework '{framework}'"
-        raise NotImplementedError
-        return filter_none(
-            {
-                f"{framework}.fallback_enable": self.enabled,
-                f"{framework}.fallback_config_file": self.config_file,
-            }
-        )
-
-    # -> hard to model..., preprocess for tflmc?
-
-
 @register_feature("disable_legalize")
 class DisableLegalize(BackendFeature, SetupFeature):
     """Enable transformation to reduces sizes of intermediate buffers by skipping legalization passes."""
@@ -826,7 +793,7 @@ def get_required_cache_flags(self):
 
 
 @register_feature("demo")
-class Demo(BackendFeature, SetupFeature):
+class Demo(PlatformFeature):
     """Run demo application instead of benchmarking code."""
 
     DEFAULTS = {
@@ -961,7 +928,8 @@ def cachesim_callback(stdout, metrics, artifacts):
                             continue
                     value = int(value) if "%" not in value else float(value[:-1]) / 100
                     if prefix in prefixes:
-                        metrics.add(f"{prefix}-Cache {label}", value)
+                        for m in metrics:
+                            m.add(f"{prefix}-Cache {label}", value)
 
             return cachesim_callback
 
@@ -970,6 +938,9 @@ def cachesim_callback(stdout, metrics, artifacts):
 class LogInstructions(TargetFeature):
     """Enable logging of the executed instructions of a simulator-based target."""
 
+    # TODO: support ovpsim via --trace. Example Output:
+    #   Info 'riscvOVPsim/cpu', 0x0000000000010b92(...): fdc42583 lw      a1,-36(s0)
+
     DEFAULTS = {**FeatureBase.DEFAULTS, "to_file": False}
 
     def __init__(self, features=None, config=None):
@@ -989,7 +960,7 @@ def add_target_config(self, target, config):
             extra_args_new.append("-l")
             # if self.to_file:
             #     extra_args_new.append("--log=?")
-            config.update({"extra_args": extra_args_new})
+            config.update({f"{target}.extra_args": extra_args_new})
         elif target == "etiss_pulpino":
             plugins_new = config.get("plugins", [])
             plugins_new.append("PrintInstruction")
@@ -1020,76 +991,6 @@ def log_instrs_callback(stdout, metrics, artifacts):
             return log_instrs_callback
 
 
-@register_feature("microtvm_etissvp")
-class MicrotvmEtissVp(PlatformFeature):
-    """Use ETISS VP for MicroTVM deployment in TVM."""
-
-    DEFAULTS = {
-        **FeatureBase.DEFAULTS,
-        "verbose": False,
-        "debug": False,
-        "transport": True,
-    }
-
-    REQUIRED = ["microtvm_etissvp.template", "etiss.install_dir", "etissvp.script", "riscv_gcc.install_dir"]
-
-    def __init__(self, features=None, config=None):
-        super().__init__("microtvm_etissvp", features=features, config=config)
-
-    @property
-    def microtvm_etissvp_template(self):
-        return self.config["microtvm_etissvp.template"]
-
-    @property
-    def etiss_install_dir(self):
-        return self.config["etiss.install_dir"]
-
-    @property
-    def etissvp_script(self):
-        return self.config["etissvp.script"]
-
-    @property
-    def riscv_gcc_install_dir(self):
-        return self.config["riscv_gcc.install_dir"]
-
-    @property
-    def verbose(self):
-        value = self.config["verbose"]
-        return str2bool(value) if not isinstance(value, (bool, int)) else value
-
-    @property
-    def debug(self):
-        value = self.config["debug"]
-        return str2bool(value) if not isinstance(value, (bool, int)) else value
-
-    @property
-    def transport(self):
-        value = self.config["transport"]
-        return str2bool(value) if not isinstance(value, (bool, int)) else value
-
-    def get_platform_config(self, platform):
-        assert platform == "microtvm", f"Unsupported feature '{self.name}' for platform '{platform}'"
-        etissvp_ini = Path(self.microtvm_etissvp_template) / "scripts" / "memsegs.ini"
-
-        project_options = {
-            "project_type": "host_driven",
-            "verbose": str(self.verbose).lower(),
-            "debug": str(self.debug).lower(),
-            "transport": str(self.transport).lower(),
-            "etiss_path": str(self.etiss_install_dir),
-            "riscv_path": str(self.riscv_gcc_install_dir),
-            "etissvp_script": str(self.etissvp_script),
-            "etissvp_script_args": f"plic clint uart v -i{etissvp_ini}",  # TODO: remove v
-        }
-
-        return filter_none(
-            {
-                f"{platform}.project_template": self.microtvm_etissvp_template,
-                f"{platform}.project_options": project_options,
-            }
-        )
-
-
 @register_feature("arm_mvei")
 class ArmMvei(SetupFeature, TargetFeature, PlatformFeature):
     """Enable MVEI extension for supported ARM targets"""
@@ -1150,3 +1051,186 @@ def __init__(self, features=None, config=None):
 
     def get_run_config(self):
         return {"run.target_to_backend": self.enabled}
+
+
+# Needs: vext
+# RISC-V only
+# Warning: Auto-vectorization is turned on by default quite low optimization levels
+# Therfore this feature is mainly for debugging the auto-vectorization procedure
+@register_feature("auto_vectorize")
+class AutoVectorize(PlatformFeature):
+    """Enable auto_vectorization for supported MLIF platform targets."""
+
+    DEFAULTS = {
+        **FeatureBase.DEFAULTS,
+        "verbose": False,
+        "loop": True,
+        "slp": True,
+    }
+
+    def __init__(self, features=None, config=None):
+        super().__init__("auto_vectorize", features=features, config=config)
+
+    @property
+    def verbose(self):
+        return str2bool(self.config["verbose"]) if isinstance(self.config["verbose"], str) else self.config["verbose"]
+
+    @property
+    def loop(self):
+        return str2bool(self.config["loop"]) if isinstance(self.config["loop"], str) else self.config["loop"]
+
+    @property
+    def slp(self):
+        return str2bool(self.config["slp"]) if isinstance(self.config["slp"], str) else self.config["slp"]
+
+    def get_platform_defs(self, platform):
+        return {
+            "RISCV_AUTO_VECTORIZE": self.enabled,
+            "RISCV_AUTO_VECTORIZE_VERBOSE": self.verbose,
+            "RISCV_AUTO_VECTORIZE_LOOP": self.loop and self.enabled,
+            "RISCV_AUTO_VECTORIZE_SLP": self.slp and self.enabled,
+        }
+
+
+@register_feature("benchmark")
+class Benchmark(PlatformFeature, TargetFeature):
+    """Profile code using supported platforms."""
+
+    # TODO: would make sense to move end_to_end here as well!
+
+    DEFAULTS = {
+        **FeatureBase.DEFAULTS,
+        "num_runs": 1,
+        "num_repeat": 1,
+        "total": False,
+        "aggregate": "avg",  # Allowed: avg, max, min, none, all
+    }
+
+    REQUIRED = []
+
+    def __init__(self, features=None, config=None):
+        super().__init__("benchmark", features=features, config=config)
+
+    @property
+    def num_runs(self):
+        return int(self.config["num_runs"])
+
+    @property
+    def num_repeat(self):
+        return int(self.config["num_repeat"])
+
+    @property
+    def total(self):
+        value = self.config["total"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def aggregate(self):
+        value = self.config["aggregate"]
+        assert value in ["avg", "all", "max", "min", "none"]
+        return value
+
+    def get_platform_config(self, platform):
+        supported = ["mlif", "tvm"]  # TODO: support microtvm and espidf
+        assert platform in supported, f"Unsupported feature '{self.name}' for platform '{platform}'"
+
+        if platform in ["tvm", "microtvm"]:
+            return {
+                f"{platform}.number": self.num_runs,
+                f"{platform}.repeat": self.num_repeat,
+                f"{platform}.aggregate": self.aggregate,
+                f"{platform}.total_time": self.total,
+            }
+        else:
+            return {}
+
+    def get_target_config(self, target):
+
+        return {
+            f"{target}.repeat": self.num_repeat,
+        }
+
+    def get_platform_defs(self, platform):
+        supported = ["mlif", "espidf", "tvm", "microtvm", "zephyr"]  # TODO: support microtvm and espidf
+        assert platform in supported, f"Unsupported feature '{self.name}' for platform '{platform}'"
+
+        if platform == "mlif":
+            return {"NUM_RUNS": self.num_runs}
+        elif platform == "espidf":
+            return {"MLONMCU_NUM_RUNS": self.num_runs}
+        else:
+            return {}
+
+    def get_target_callback(self, target):
+        if self.enabled:
+
+            def benchmark_callback(stdout, metrics, artifacts):
+                if len(metrics) <= 1:
+                    return
+                metrics_ = metrics[1:]  # drop first run (warmup)
+
+                # TODO: this currently processes all numeric metrics, should probably ignore stuff like MIPS etc.
+                data_ = [
+                    {
+                        key: (float(value) / self.num_runs) if self.num_runs > 1 else value
+                        for key, value in m.data.items()
+                        if "cycle" in key.lower() or "time" in key.lower()
+                    }
+                    for m in metrics_
+                ]
+
+                df = pd.DataFrame(data_)
+
+                if self.aggregate == "all":
+                    aggs = ["mean", "min", "max"]
+                elif self.aggregate in ["avg", "mean"]:
+                    aggs = ["mean"]
+                elif self.aggregate == "min":
+                    aggs = ["min"]
+                elif self.aggregate == "max":
+                    aggs = ["max"]
+                elif self.aggregate == "none":
+                    aggs = []
+
+                if len(aggs) == 0:
+                    data = {}
+                else:
+                    df_ = df.agg(aggs)
+
+                    # rename columns
+                    index_mapping = {
+                        "mean": "Average",
+                        "min": "Min",
+                        "max": "Max",
+                    }
+
+                    df_ = df_.rename(index=index_mapping)
+
+                    data = df_.to_dict()
+
+                    data = {f"{prefix} {key}": value for key, temp in data.items() for prefix, value in temp.items()}
+                if self.total:
+                    data.update(
+                        {
+                            f"Total {key}": ((value * self.num_runs) if self.num_runs > 1 else value)
+                            for key, value in data_[-1].items()
+                            if "cycle" in key.lower() or "time" in key.lower()
+                        }
+                    )
+                metrics_ = metrics_[-1]
+                metrics_.data.update(data)
+
+                for key in data_[-1].keys():
+                    if key in metrics_.order:
+                        if self.total:
+                            metrics_.order.append(f"Total {key}")
+                        metrics_.order.remove(key)
+                for key in data.keys():
+                    if key not in metrics_.order:
+                        metrics_.order.append(key)
+                metrics.clear()
+                metrics.append(metrics_)
+
+            benchmark_callback.priority = 0
+
+            return benchmark_callback
diff --git a/mlonmcu/flow/__init__.py b/mlonmcu/flow/__init__.py
index a33c97947..441f3a0d0 100644
--- a/mlonmcu/flow/__init__.py
+++ b/mlonmcu/flow/__init__.py
@@ -18,15 +18,17 @@
 #
 """Flow module for frameworks and backend."""
 
+from mlonmcu.flow.tflm.framework import TFLMFramework
+from mlonmcu.flow.tvm.framework import TVMFramework
+
 from mlonmcu.flow.tflm.backend.tflmc import TFLMCBackend
 from mlonmcu.flow.tflm.backend.tflmi import TFLMIBackend
 
-from mlonmcu.flow.tflm.framework import TFLMFramework
 from mlonmcu.flow.tvm.backend.tvmaot import TVMAOTBackend
+from mlonmcu.flow.tvm.backend.tvmaotplus import TVMAOTPlusBackend
 from mlonmcu.flow.tvm.backend.tvmcg import TVMCGBackend
 from mlonmcu.flow.tvm.backend.tvmllvm import TVMLLVMBackend
 from mlonmcu.flow.tvm.backend.tvmrt import TVMRTBackend
-from mlonmcu.flow.tvm.framework import TVMFramework
 
 SUPPORTED_FRAMEWORKS = {
     "tflm": TFLMFramework,
@@ -40,6 +42,7 @@
 
 SUPPORTED_TVM_BACKENDS = {
     "tvmaot": TVMAOTBackend,
+    "tvmaotplus": TVMAOTPlusBackend,
     "tvmrt": TVMRTBackend,
     "tvmcg": TVMCGBackend,
     "tvmllvm": TVMLLVMBackend,
diff --git a/mlonmcu/flow/backend.py b/mlonmcu/flow/backend.py
index e0ccad91e..7df20df22 100644
--- a/mlonmcu/flow/backend.py
+++ b/mlonmcu/flow/backend.py
@@ -38,6 +38,7 @@ class Backend(ABC):
     FEATURES = []
     DEFAULTS = {}
     REQUIRED = []
+    OPTIONAL = []
 
     def __init__(
         self,
@@ -48,7 +49,7 @@ def __init__(
         self.framework = framework
         self.config = config if config else {}
         self.features = self.process_features(features)
-        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.REQUIRED)
+        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
         self.artifacts = []
         self.supported_fmts = []
         self.tuner = None
diff --git a/mlonmcu/flow/framework.py b/mlonmcu/flow/framework.py
index e59b5c21c..f101c9b71 100644
--- a/mlonmcu/flow/framework.py
+++ b/mlonmcu/flow/framework.py
@@ -31,12 +31,12 @@ class Framework(ABC):
     FEATURES = []
     DEFAULTS = {}
     REQUIRED = ["tf.src_dir"]
+    OPTIONAL = []
 
     def __init__(self, features=None, config=None, backends={}):
         self.config = config if config else {}
         self.features = self.process_features(features)
-        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.REQUIRED)
-        self.backends = backends  # TODO: get rid of this
+        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
 
     def process_features(self, features):
         if features is None:
diff --git a/mlonmcu/flow/tvm/__init__.py b/mlonmcu/flow/tvm/__init__.py
index bba5ae4a6..c8826b4a0 100644
--- a/mlonmcu/flow/tvm/__init__.py
+++ b/mlonmcu/flow/tvm/__init__.py
@@ -18,6 +18,6 @@
 #
 """TVM framework module."""
 
-from .backend import TVMAOTBackend, TVMRTBackend, TVMCGBackend
+from .backend import TVMAOTBackend, TVMAOTPlusBackend, TVMRTBackend, TVMCGBackend
 
-__all__ = ["TVMAOTBackend", "TVMRTBackend", "TVMCGBackend"]
+__all__ = ["TVMAOTBackend", "TVMAOTPlusBackend", "TVMRTBackend", "TVMCGBackend"]
diff --git a/mlonmcu/flow/tvm/backend/__init__.py b/mlonmcu/flow/tvm/backend/__init__.py
index 57647bed9..5891c5b7a 100644
--- a/mlonmcu/flow/tvm/backend/__init__.py
+++ b/mlonmcu/flow/tvm/backend/__init__.py
@@ -18,8 +18,9 @@
 #
 from .backend import TVMBackend
 from .tvmaot import TVMAOTBackend
+from .tvmaotplus import TVMAOTPlusBackend
 from .tvmcg import TVMCGBackend
 from .tvmrt import TVMRTBackend
 from .tvmllvm import TVMLLVMBackend
 
-__all__ = ["TVMBackend", "TVMAOTBackend", "TVMCGBackend", "TVMRTBackend", "TVMLLVMBackend"]
+__all__ = ["TVMBackend", "TVMAOTBackend", "TVMAOTPlusBackend", "TVMCGBackend", "TVMRTBackend", "TVMLLVMBackend"]
diff --git a/mlonmcu/flow/tvm/backend/backend.py b/mlonmcu/flow/tvm/backend/backend.py
index e090c77e0..e8293bc3c 100644
--- a/mlonmcu/flow/tvm/backend/backend.py
+++ b/mlonmcu/flow/tvm/backend/backend.py
@@ -16,13 +16,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-import os
-
 from mlonmcu.flow.backend import Backend
 from mlonmcu.setup import utils
 from mlonmcu.config import str2bool
-from mlonmcu.models.model import ModelFormats
-from .model_info import get_tflite_model_info, get_relay_model_info, get_pb_model_info
+from .model_info import get_model_info
 from .tuner import TVMTuner
 from .python_utils import prepare_python_environment
 from .tvmc_utils import (
@@ -50,6 +47,9 @@ class TVMBackend(Backend):
         "target_mcpu": None,
         "target_march": None,
         "target_model": None,
+        "target_mtriple": None,
+        "target_mabi": None,
+        "target_mattr": None,
         "extra_target": None,
         "extra_target_mcpu": None,
         "desired_layout": None,  # optional: NCHW or NHWC
@@ -66,13 +66,17 @@ class TVMBackend(Backend):
 
     REQUIRED = ["tvm.build_dir", "tvm.pythonpath", "tvm.configs_dir"]
 
-    def __init__(self, features=None, config=None):
+    def __init__(self, target="c", executor=None, runtime="crt", fmt="mlf", features=None, config=None):
         super().__init__(framework="tvm", features=features, config=config)
 
         self.model = None  # Actual filename!
         self.model_info = None
         self.input_shapes = None
         self.supported_formats = [ModelFormats.TFLITE, ModelFormats.RELAY, ModelFormats.PB]
+        self.target = target
+        self.runtime = runtime
+        self.executor = executor
+        self.fmt = fmt
 
         self.prefix = "default"
         self.artifacts = (
@@ -132,6 +136,18 @@ def target_mcpu(self):
     def target_march(self):
         return self.config["target_march"]
 
+    @property
+    def target_mtriple(self):
+        return self.config["target_mtriple"]
+
+    @property
+    def target_mabi(self):
+        return self.config["target_mabi"]
+
+    @property
+    def target_mattr(self):
+        return self.config["target_mattr"]
+
     @property
     def target_model(self):
         return self.config["target_model"]
@@ -202,6 +218,12 @@ def get_target_details(self):
             ret["mcpu"] = self.target_mcpu
         if self.target_march:
             ret["march"] = self.target_march
+        if self.target_mtriple:
+            ret["mtriple"] = self.target_mtriple
+        if self.target_mabi:
+            ret["mabi"] = self.target_mabi
+        if self.target_mattr:
+            ret["mattr"] = self.target_mattr
         if self.target_model:
             ret["model"] = self.target_model
         return ret
@@ -212,17 +234,18 @@ def get_extra_target_details(self):
             ret["mcpu"] = self.extra_target_mcpu
         return ret
 
-    def get_tvmc_compile_args(self, out, executor=None, fmt="mlf", target="c", runtime="crt", dump=None):
-        assert executor in ["aot", "graph"], "Unsupported TVM executor"
+    def get_tvmc_compile_args(self, out, dump=None):
+        assert self.executor is not None
+        assert self.executor in ["aot", "graph"], "Unsupported TVM executor"
         args = [
             self.model,
             *get_target_tvmc_args(
-                target,
+                self.target,
                 extra_target=self.extra_target,
                 target_details=self.get_target_details(),
                 extra_target_details=self.get_extra_target_details(),
             ),
-            *get_runtime_executor_tvmc_args(runtime, executor),
+            *get_runtime_executor_tvmc_args(self.runtime, self.executor),
             *get_pass_config_tvmc_args(self.pass_config),
             *get_disabled_pass_tvmc_args(self.disabled_passes),
             *get_input_shapes_tvmc_args(self.input_shapes),
@@ -232,7 +255,7 @@ def get_tvmc_compile_args(self, out, executor=None, fmt="mlf", target="c", runti
             *self.tvmc_extra_args,
             *["--opt-level", str(self.opt_level)],
             *["--output", str(out)],
-            *["-f", fmt],
+            *["-f", self.fmt],
             *["--model-format", self.model_format],
         ]
         return args
@@ -261,23 +284,5 @@ def load_model(self, model):
         self.model = model
         # TODO: path model class instead of path!
         # fmt = self.model.formats[0]
-        ext = os.path.splitext(model)[1][1:]
-        fmt = ModelFormats.from_extension(ext)
-        if fmt == ModelFormats.TFLITE:
-            self.model_format = "tflite"
-            with open(model, "rb") as handle:
-                model_buf = handle.read()
-                self.model_info = get_tflite_model_info(model_buf)
-        elif fmt == ModelFormats.RELAY:
-            # Warning: the wrapper generateion does currently not work because of the
-            # missing possibility to get the relay models input names and shapes
-            self.model_format = "relay"
-            with open(model, "r") as handle:
-                mod_text = handle.read()
-            self.model_info = get_relay_model_info(mod_text)
-        elif fmt == ModelFormats.PB:
-            self.model_format = "pb"
-            self.model_info = get_pb_model_info(model)
-        else:
-            raise RuntimeError(f"Unsupported model format '{fmt.name}' for backend '{self.name}'")
+        self.model_format, self.model_info = get_model_info(model, fmt, backend_name=self.name)
         self.input_shapes = {tensor.name: tensor.shape for tensor in self.model_info.in_tensors}
diff --git a/mlonmcu/flow/tvm/backend/model_info.py b/mlonmcu/flow/tvm/backend/model_info.py
index f14424ed3..89b223f04 100644
--- a/mlonmcu/flow/tvm/backend/model_info.py
+++ b/mlonmcu/flow/tvm/backend/model_info.py
@@ -17,9 +17,12 @@
 # limitations under the License.
 #
 import re
+import os
 import tflite
 from tflite.TensorType import TensorType as TType
 
+from mlonmcu.models.model import ModelFormats
+
 
 class TensorInfo:
     def __init__(self, name, shape, dtype, fix_names=False):
@@ -173,6 +176,72 @@ def __init__(self, model_file):
         super().__init__(in_tensors, out_tensors)
 
 
+class ONNXModelInfo(ModelInfo):
+    def __init__(self, model_file):
+        from google.protobuf.json_format import MessageToDict
+        import onnx
+        from onnx.mapping import TENSOR_TYPE_TO_NP_TYPE
+
+        model = onnx.load(model_file)
+
+        def _helper(tensors):
+            ret = []
+            for tensor in tensors:
+                d = MessageToDict(tensor)
+                name = d["name"]
+                elem_type = d["type"]["tensorType"]["elemType"]
+                dims = d["type"]["elemType"]["shape"]["dim"]
+                shape = [int(x["dimValue"]) for x in dims]
+                dtype = str(TENSOR_TYPE_TO_NP_TYPE[elem_type])
+                ret.append(TensorInfo(name, shape, dtype))
+            return ret
+
+        in_tensors = _helper(model.graph.input)
+        out_tensors = _helper(model.graph.output)
+        super().__init__(in_tensors, out_tensors)
+
+
+class PaddleModelInfo(ModelInfo):
+    def __init__(self, model_file):
+        import paddle
+
+        paddle.enable_static()
+        paddle.disable_signal_handler()
+
+        exe = paddle.static.Executor(paddle.CPUPlace())
+        prefix = "".join(model_file.strip().split(".")[:-1])
+        program, _, _ = paddle.static.load_inference_model(prefix, exe)
+
+        output_names = list()
+        for block in program.blocks:
+            for op in block.ops:
+                if op.type == "fetch":
+                    output_names.append(op.input("X")[0])
+
+        def _convert_dtype_value(val):
+            """Converts a Paddle type id to a string."""
+            # See: https://github.com/apache/tvm/blob/cc769fdc951707b8be991949864817b955a4dbc7/python/tvm/relay/frontend/paddlepaddle.py#L70
+
+            convert_dtype_map = {
+                21: "int8",
+                20: "uint8",
+                6: "float64",
+                5: "float32",
+                4: "float16",
+                3: "int64",
+                2: "int32",
+                1: "int16",
+                0: "bool",
+            }
+            if val not in convert_dtype_map:
+                msg = "Paddle data type value %d is not handled yet." % (val)
+                raise NotImplementedError(msg)
+            return convert_dtype_map[val]
+
+        raise NotImplementedError
+        # super().__init__(in_tensors, out_tensors)
+
+
 def get_tflite_model_info(model_buf):
     tflite_model = tflite.Model.GetRootAsModel(model_buf, 0)
     model_info = TfLiteModelInfo(tflite_model)
@@ -187,3 +256,32 @@ def get_relay_model_info(mod_text):
 def get_pb_model_info(model_file):
     model_info = PBModelInfo(model_file)
     return model_info
+
+
+def get_paddle_model_info(model_file):
+    model_info = PaddleModelInfo(model_file)
+    return model_info
+
+
+def get_onnx_model_info(model_file):
+    model_info = ONNXModelInfo(model_file)
+    return model_info
+
+
+def get_model_info(model, fmt, backend_name="unknown"):
+    ext = os.path.splitext(model)[1][1:]
+    fmt = ModelFormats.from_extension(ext)
+    if fmt == ModelFormats.TFLITE:
+        with open(model, "rb") as handle:
+            model_buf = handle.read()
+            return "tflite", get_tflite_model_info(model_buf)
+    elif fmt == ModelFormats.RELAY:
+        # Warning: the wrapper generateion does currently not work because of the
+        # missing possibility to get the relay models input names and shapes
+        with open(model, "r") as handle:
+            mod_text = handle.read()
+        return "relay", get_relay_model_info(mod_text)
+    elif fmt == ModelFormats.PB:
+        return "pb", get_pb_model_info(model)
+    else:
+        raise RuntimeError(f"Unsupported model format '{fmt.name}' for backend '{backend_name}'")
diff --git a/mlonmcu/flow/tvm/backend/tuner.py b/mlonmcu/flow/tvm/backend/tuner.py
index f2c450260..f5534ccd1 100644
--- a/mlonmcu/flow/tvm/backend/tuner.py
+++ b/mlonmcu/flow/tvm/backend/tuner.py
@@ -79,7 +79,7 @@ def append(self):
     @property
     def tuner(self):
         tuner = str(self.config["tuner"])
-        assert tuner in ["ga", "gridsearch", "random,xgb", "xgb_knob", "xgb-rank"]
+        assert tuner in ["ga", "gridsearch", "random", "xgb", "xgb_knob", "xgb-rank"]
         return tuner
 
     @property
diff --git a/mlonmcu/flow/tvm/backend/tvmaot.py b/mlonmcu/flow/tvm/backend/tvmaot.py
index 2bd717528..1d648496c 100644
--- a/mlonmcu/flow/tvm/backend/tvmaot.py
+++ b/mlonmcu/flow/tvm/backend/tvmaot.py
@@ -48,6 +48,9 @@ class TVMAOTBackend(TVMBackend):
 
     name = "tvmaot"
 
+    def __init__(self, runtime="crt", fmt="mlf", features=None, config=None):
+        super().__init__(executor="aot", runtime=runtime, fmt=fmt, features=features, config=config)
+
     @property
     def arena_size(self):
         size = self.config["arena_size"]
@@ -66,9 +69,7 @@ def alignment_bytes(self):
         return int(self.config["alignment_bytes"])
 
     def get_tvmc_compile_args(self, out):
-        return super().get_tvmc_compile_args(out, executor="aot") + get_tvmaot_tvmc_args(
-            self.alignment_bytes, self.unpacked_api
-        )
+        return super().get_tvmc_compile_args(out) + get_tvmaot_tvmc_args(self.alignment_bytes, self.unpacked_api)
 
     def get_workspace_size_from_metadata(self, metadata):
         if "modules" in metadata:
diff --git a/mlonmcu/flow/tvm/backend/tvmaotplus.py b/mlonmcu/flow/tvm/backend/tvmaotplus.py
new file mode 100644
index 000000000..c57317219
--- /dev/null
+++ b/mlonmcu/flow/tvm/backend/tvmaotplus.py
@@ -0,0 +1,52 @@
+#
+# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+#
+# This file is part of MLonMCU.
+# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import sys
+
+from .tvmaot import TVMAOTBackend, main
+
+
+class TVMAOTPlusBackend(TVMAOTBackend):
+
+    FEATURES = [
+        *TVMAOTBackend.FEATURES,
+    ]
+
+    DEFAULTS = {
+        **TVMAOTBackend.DEFAULTS,
+        "arena_size": 0,
+        "unpacked_api": True,
+        "extra_pass_config": {
+            "tir.usmp.enable": True,
+            "tir.usmp.algorithm": "hill_climb",
+        },
+    }
+
+    name = "tvmaotplus"
+
+    def __init__(self, runtime="crt", fmt="mlf", features=None, config=None):
+        super().__init__(runtime=runtime, fmt=fmt, features=features, config=config)
+
+
+if __name__ == "__main__":
+    sys.exit(
+        main(
+            TVMAOTPlusBackend,
+            args=sys.argv[1:],
+        )
+    )  # pragma: no cover
diff --git a/mlonmcu/flow/tvm/backend/tvmc_utils.py b/mlonmcu/flow/tvm/backend/tvmc_utils.py
index 0e1b4867b..1bce833bb 100644
--- a/mlonmcu/flow/tvm/backend/tvmc_utils.py
+++ b/mlonmcu/flow/tvm/backend/tvmc_utils.py
@@ -43,8 +43,38 @@ def get_input_shapes_tvmc_args(input_shapes):
     return ["--input-shapes", arg]
 
 
+def check_allowed(target, name):
+    common = ["libs", "model", "tag", "mcpu", "device", "keys"]
+    if target == "c":
+        return name in ["constants-byte-alignment", "workspace-bytes-alignment", "march"] + common
+    elif target == "llvm":
+        return (
+            name
+            in [
+                "fast-math",
+                "opt-level",
+                "fast-math-ninf",
+                "mattr",
+                "num-cores",
+                "fast-math-nsz",
+                "fast-math-contract",
+                "mtriple",
+                "mfloat-abi",
+                "fast-math-arcp",
+                "fast-math-reassoc",
+                "mabi",
+            ]
+            + common
+        )
+
+    else:
+        return True
+
+
 def gen_target_details_args(target, target_details):
-    return sum([[f"--target-{target}-{key}", value] for key, value in target_details.items()], [])
+    return sum(
+        [[f"--target-{target}-{key}", value] for key, value in target_details.items() if check_allowed(target, key)], []
+    )
 
 
 def get_target_tvmc_args(target="c", extra_target=None, target_details={}, extra_target_details=None):
@@ -56,10 +86,9 @@ def get_target_tvmc_args(target="c", extra_target=None, target_details={}, extra
                 extra_target = [extra_target]
         # TODO: support multiple ones, currently only single one...
         assert len(extra_target) == 1
-        target = ",".join(extra_target + [target])
     return [
         "--target",
-        target,
+        ",".join((extra_target if extra_target else []) + [target]),
         # TODO: provide a feature which sets these automatically depending on the chosen target
         *gen_target_details_args(target, target_details),
         *(gen_target_details_args(extra_target[0], extra_target_details) if extra_target is not None else []),
@@ -98,11 +127,12 @@ def get_tvmaot_tvmc_args(alignment_bytes, unpacked_api):
     ]
 
 
-def get_tvmrt_tvmc_args():
-    return [
-        *["--runtime-crt-system-lib", str(1)],
-        *["--executor-graph-link-params", str(0)],
-    ]
+def get_tvmrt_tvmc_args(runtime="crt"):
+    ret = []
+    if runtime == "crt":
+        ret.extend(["--runtime-crt-system-lib", str(1)])
+    ret.extend(["--executor-graph-link-params", str(0)])
+    return ret
 
 
 def get_data_tvmc_args(mode=None, ins_file=None, outs_file=None, print_top=10):
diff --git a/mlonmcu/flow/tvm/backend/tvmcg.py b/mlonmcu/flow/tvm/backend/tvmcg.py
index 8a761b5ac..3320a4b7e 100644
--- a/mlonmcu/flow/tvm/backend/tvmcg.py
+++ b/mlonmcu/flow/tvm/backend/tvmcg.py
@@ -42,7 +42,12 @@ class TVMCGBackend(TVMRTBackend):
 
     def get_max_workspace_size_from_metadata(self, metadata):
         max_workspace = 0
-        for op in metadata["memory"]["functions"]["operator_functions"]:
+        if "modules" in metadata:
+            temp = metadata["modules"]["default"]["memory"]
+        else:
+            # backwards compatibility
+            temp = metadata["memory"]
+        for op in temp["functions"]["operator_functions"]:
             max_workspace = max(
                 max_workspace,
                 op["workspace"][0]["workspace_size_bytes"] if len(op["workspace"]) > 0 else 0,
@@ -65,7 +70,8 @@ def generate_code(self):
                     with open(mlf_path / "metadata.json") as handle:
                         metadata = json.load(handle)
                     tvmcg_exe = self.config["utvmcg.exe"]
-                    graph_json_file = mlf_path / "executor-config" / "graph" / "graph.json"
+                    # graph_json_file = mlf_path / "executor-config" / "graph" / "graph.json"
+                    graph_json_file = mlf_path / "executor-config" / "graph" / "default.graph"
                     params_bin_file = mlf_path / "parameters" / "default.params"
                     max_workspace_size = self.get_max_workspace_size_from_metadata(metadata)
                     args = []
diff --git a/mlonmcu/flow/tvm/backend/tvmllvm.py b/mlonmcu/flow/tvm/backend/tvmllvm.py
index 7c9281775..bc7645103 100644
--- a/mlonmcu/flow/tvm/backend/tvmllvm.py
+++ b/mlonmcu/flow/tvm/backend/tvmllvm.py
@@ -22,8 +22,10 @@
 from pathlib import Path
 
 from .backend import TVMBackend
+from .wrapper import generate_tvmrt_wrapper, generate_wrapper_header
 from mlonmcu.flow.backend import main
 from mlonmcu.artifact import Artifact, ArtifactFormat
+from .tvmc_utils import get_tvmrt_tvmc_args
 
 
 # Warning: This is only ment to be used with the TvmPlatform!
@@ -41,8 +43,22 @@ class TVMLLVMBackend(TVMBackend):
 
     name = "tvmllvm"
 
+    def __init__(self, runtime="crt", fmt="mlf", features=None, config=None):
+        super().__init__(target="llvm", executor="graph", runtime=runtime, fmt=fmt, features=features, config=config)
+
     def get_tvmc_compile_args(self, out):
-        return super().get_tvmc_compile_args(out, executor="graph", target="llvm", runtime="cpp", fmt="so")
+        return super().get_tvmc_compile_args(out) + get_tvmrt_tvmc_args(self.runtime)
+
+    def get_graph_and_params_from_mlf(self, path):
+        graph = None
+        # with open(Path(path) / "executor-config" / "graph" / "graph.json", "r") as handle:
+        with open(Path(path) / "executor-config" / "graph" / "default.graph", "r") as handle:
+            graph = handle.read()
+        params = None
+        with open(Path(path) / "parameters" / "default.params", "rb") as handle:
+            params = handle.read()
+
+        return graph, params
 
     def generate_code(self, verbose=False):
         artifacts = []
@@ -61,7 +77,7 @@ def generate_code(self, verbose=False):
                     Artifact(
                         f"{self.prefix}.tar",
                         raw=mlf_data,
-                        fmt=ArtifactFormat.SHARED_OBJECT,
+                        fmt=ArtifactFormat.SHARED_OBJECT if self.fmt == "so" else ArtifactFormat.MLF,
                         archive=True,
                     )
                 )
@@ -69,6 +85,15 @@ def generate_code(self, verbose=False):
             stdout_artifact = Artifact(
                 "tvmc_compile_out.log", content=out, fmt=ArtifactFormat.TEXT
             )  # TODO: rename to tvmllvm_out.log?
+            generate_wrapper = self.fmt == "mlf"
+            if generate_wrapper:
+                workspace_size = 2**20
+                assert workspace_size >= 0
+                graph, params = self.get_graph_and_params_from_mlf(tar_dir)
+                wrapper_src = generate_tvmrt_wrapper(graph, params, self.model_info, workspace_size, debug_arena=False)
+                artifacts.append(Artifact("rt_wrapper.c", content=wrapper_src, fmt=ArtifactFormat.SOURCE))
+                header_src = generate_wrapper_header()
+                artifacts.append(Artifact("tvm_wrapper.h", content=header_src, fmt=ArtifactFormat.SOURCE))
             artifacts.append(stdout_artifact)
         self.artifacts = artifacts
 
diff --git a/mlonmcu/flow/tvm/backend/tvmrt.py b/mlonmcu/flow/tvm/backend/tvmrt.py
index 1f54066d1..9185aacc6 100644
--- a/mlonmcu/flow/tvm/backend/tvmrt.py
+++ b/mlonmcu/flow/tvm/backend/tvmrt.py
@@ -46,6 +46,9 @@ class TVMRTBackend(TVMBackend):
 
     name = "tvmrt"
 
+    def __init__(self, runtime="crt", fmt="mlf", features=None, config=None):
+        super().__init__(executor="graph", runtime=runtime, fmt=fmt, features=features, config=config)
+
     @property
     def arena_size(self):
         size = self.config["arena_size"]
@@ -56,11 +59,11 @@ def debug_arena(self):
         return bool(self.config["debug_arena"])
 
     def get_tvmc_compile_args(self, out):
-        return super().get_tvmc_compile_args(out, executor="graph") + get_tvmrt_tvmc_args()
+        return super().get_tvmc_compile_args(out) + get_tvmrt_tvmc_args()
 
     def get_graph_and_params_from_mlf(self, path):
         graph = None
-        with open(Path(path) / "executor-config" / "graph" / "graph.json", "r") as handle:
+        with open(Path(path) / "executor-config" / "graph" / "default.graph", "r") as handle:
             graph = handle.read()
         params = None
         with open(Path(path) / "parameters" / "default.params", "rb") as handle:
diff --git a/mlonmcu/flow/tvm/framework.py b/mlonmcu/flow/tvm/framework.py
index 3c226c8c1..a34436a95 100644
--- a/mlonmcu/flow/tvm/framework.py
+++ b/mlonmcu/flow/tvm/framework.py
@@ -18,6 +18,8 @@
 #
 """Definitions for TVMFramework."""
 
+import os
+import pkg_resources
 from pathlib import Path
 
 from mlonmcu.flow.framework import Framework
@@ -25,6 +27,18 @@
 # from mlonmcu.flow.tvm import TVMBackend
 
 
+def get_crt_config_dir():
+    files = pkg_resources.resource_listdir(
+        "mlonmcu", os.path.join("..", "resources", "frameworks", "tvm", "crt_config")
+    )
+    if "crt_config.h" not in files:
+        return None
+    fname = pkg_resources.resource_filename(
+        "mlonmcu", os.path.join("..", "resources", "frameworks", "tvm", "crt_config")
+    )
+    return fname
+
+
 class TVMFramework(Framework):
     """TVM Framework specialization."""
 
@@ -35,7 +49,7 @@ class TVMFramework(Framework):
     DEFAULTS = {
         "extra_incs": [],
         "extra_libs": [],
-        "crt_config_dir": None,  # i.e. see mlonmcu/resources/frameworks/tvm/crt_config/
+        "crt_config_dir": get_crt_config_dir(),
     }
 
     REQUIRED = ["tvm.src_dir"]
diff --git a/mlonmcu/mlonmcu.py b/mlonmcu/mlonmcu.py
index 111e36969..adc34db50 100644
--- a/mlonmcu/mlonmcu.py
+++ b/mlonmcu/mlonmcu.py
@@ -16,4 +16,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+# pragma: no cover
 """Main module."""
diff --git a/mlonmcu/models/__init__.py b/mlonmcu/models/__init__.py
index 240cdaaf2..5aac630c0 100644
--- a/mlonmcu/models/__init__.py
+++ b/mlonmcu/models/__init__.py
@@ -17,7 +17,7 @@
 # limitations under the License.
 #
 from mlonmcu.models.lookup import print_summary
-from .frontend import PBFrontend, TfLiteFrontend, PackedFrontend, ONNXFrontend, RelayFrontend
+from .frontend import PBFrontend, TfLiteFrontend, PackedFrontend, ONNXFrontend, RelayFrontend, PaddleFrontend
 
 SUPPORTED_FRONTENDS = {
     "tflite": TfLiteFrontend,
@@ -25,6 +25,7 @@
     "packed": PackedFrontend,
     "onnx": ONNXFrontend,
     "pb": PBFrontend,
+    "paddle": PaddleFrontend,
 }  # TODO: use registry instead
 
 __all__ = ["print_summary", "TfLiteFrontend", "PackedFrontend", "ONNXFrontend", "PBFrontend", "SUPPORTED_FRONTENDS"]
diff --git a/mlonmcu/models/frontend.py b/mlonmcu/models/frontend.py
index 613ca5cf5..ecab547b0 100644
--- a/mlonmcu/models/frontend.py
+++ b/mlonmcu/models/frontend.py
@@ -44,6 +44,7 @@ class Frontend(ABC):
     }
 
     REQUIRED = []
+    OPTIONAL = []
 
     def __init__(self, name, input_formats=None, output_formats=None, features=None, config=None):
         self.name = name
@@ -51,7 +52,7 @@ def __init__(self, name, input_formats=None, output_formats=None, features=None,
         self.output_formats = output_formats if output_formats else []
         self.config = config if config else {}
         self.features = self.process_features(features)
-        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.REQUIRED)
+        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
 
     def __repr__(self):
         probs = []
@@ -548,3 +549,22 @@ def __init__(self, features=None, config=None):
             features=features,
             config=config,
         )
+
+
+class PaddleFrontend(SimpleFrontend):
+
+    FEATURES = Frontend.FEATURES
+
+    DEFAULTS = {
+        **Frontend.DEFAULTS,
+    }
+
+    REQUIRED = Frontend.REQUIRED + []
+
+    def __init__(self, features=None, config=None):
+        super().__init__(
+            "paddle",
+            ModelFormats.PADDLE,
+            features=features,
+            config=config,
+        )
diff --git a/mlonmcu/models/inout.py b/mlonmcu/models/inout.py
deleted file mode 100644
index db85df904..000000000
--- a/mlonmcu/models/inout.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
-#
-# This file is part of MLonMCU.
-# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
diff --git a/mlonmcu/models/model.py b/mlonmcu/models/model.py
index 5cc5f4866..12385ce26 100644
--- a/mlonmcu/models/model.py
+++ b/mlonmcu/models/model.py
@@ -50,6 +50,7 @@ def from_extension(cls, ext):
     ONNX = ModelFormat(4, ["onnx"])
     RELAY = ModelFormat(5, ["relay"])
     PB = ModelFormat(6, ["pb"])
+    PADDLE = ModelFormat(7, ["pdmodel"])
 
 
 def parse_metadata_from_path(path):
@@ -77,7 +78,7 @@ def __init__(self, name, paths, config=None, alt=None, formats=ModelFormats.TFLI
         self.formats = formats
         if not isinstance(self.formats, list):
             self.formats = [formats]
-        self.config = filter_config(config if config is not None else {}, self.name, self.DEFAULTS, [])
+        self.config = filter_config(config if config is not None else {}, self.name, self.DEFAULTS, [], [])
         self.metadata = parse_metadata_from_path(self.metadata_path)
 
     @property
diff --git a/mlonmcu/platform/_platform.py b/mlonmcu/platform/_platform.py
index 4368e9c3c..8ef651f9e 100644
--- a/mlonmcu/platform/_platform.py
+++ b/mlonmcu/platform/_platform.py
@@ -19,6 +19,7 @@
 from .platform import register_platform
 from .mlif import MlifPlatform
 from .espidf import EspIdfPlatform
+from .zephyr import ZephyrPlatform
 from .tvm import TvmPlatform
 from .microtvm import MicroTvmPlatform
 
@@ -26,5 +27,6 @@
 
 register_platform("mlif", MlifPlatform)
 register_platform("espidf", EspIdfPlatform)
+register_platform("zephyr", ZephyrPlatform)
 register_platform("tvm", TvmPlatform)
 register_platform("microtvm", MicroTvmPlatform)
diff --git a/mlonmcu/platform/espidf.py b/mlonmcu/platform/espidf.py
index 29d1e356c..8328aa6a0 100644
--- a/mlonmcu/platform/espidf.py
+++ b/mlonmcu/platform/espidf.py
@@ -38,7 +38,7 @@
 from mlonmcu.config import str2bool
 
 from .platform import CompilePlatform, TargetPlatform
-from .espidf_target import create_espidf_target
+from .espidf_target import create_espidf_platform_target
 
 logger = get_logger()
 
@@ -54,7 +54,7 @@ def get_project_template(name="project"):
 class EspIdfPlatform(CompilePlatform, TargetPlatform):
     """ESP-IDF Platform class."""
 
-    FEATURES = CompilePlatform.FEATURES + TargetPlatform.FEATURES
+    FEATURES = CompilePlatform.FEATURES + TargetPlatform.FEATURES + ["benchmark"]
 
     DEFAULTS = {
         **CompilePlatform.DEFAULTS,
@@ -164,7 +164,7 @@ def create_target(self, name):
             base = SUPPORTED_TARGETS[name]
         else:
             base = Target
-        return create_espidf_target(name, self, base=base)
+        return create_espidf_platform_target(name, self, base=base)
 
     @property
     def project_template(self):
@@ -186,7 +186,7 @@ def check(self):
         if not shutil.which(self.idf_exe):
             raise RuntimeError(f"It seems like '{self.idf_exe}' is not available. Make sure to setup your environment!")
 
-    def prepare(self, target, src, num=1):
+    def prepare(self, target, src):
         self.init_directory()
         self.check()
         template_dir = self.project_template
@@ -230,7 +230,6 @@ def write_defaults(filename):
                 watchdog_sec = 60
                 f.write(f"CONFIG_ESP_TASK_WDT_TIMEOUT_S={watchdog_sec}\n")
                 f.write(f'CONFIG_MLONMCU_CODEGEN_DIR="{src}"\n')
-                f.write(f"CONFIG_MLONMCU_NUM_RUNS={num}\n")
                 for key, value in defs.items():
                     if isinstance(value, bool):
                         value = "y" if value else "n"
@@ -252,10 +251,10 @@ def get_idf_cmake_args(self):
         cmake_defs = {"CMAKE_BUILD_TYPE": "Debug" if self.debug else "Release"}
         return [f"-D{key}={value}" for key, value in cmake_defs.items()]
 
-    def compile(self, target, src=None, num=1):
+    def compile(self, target, src=None):
         out = ""
         # TODO: build with cmake options
-        out += self.prepare(target, src, num=num)
+        out += self.prepare(target, src)
         # TODO: support self.num_threads (e.g. patch esp-idf)
         idfArgs = [
             "-C",
@@ -266,9 +265,9 @@ def compile(self, target, src=None, num=1):
         out += self.invoke_idf_exe(*idfArgs, live=self.print_outputs)
         return out
 
-    def generate_elf(self, src, target, model=None, num=1, data_file=None):
+    def generate_elf(self, src, target, model=None, data_file=None):
         artifacts = []
-        out = self.compile(target, src=src, num=num)
+        out = self.compile(target, src=src)
         elf_name = self.project_name + ".elf"
         elf_file = self.project_dir / "build" / elf_name
         # TODO: just use path instead of raw data?
diff --git a/mlonmcu/platform/espidf_target.py b/mlonmcu/platform/espidf_target.py
index b734c1295..03fb247f5 100644
--- a/mlonmcu/platform/espidf_target.py
+++ b/mlonmcu/platform/espidf_target.py
@@ -27,8 +27,8 @@
 logger = get_logger()
 
 
-def create_espidf_target(name, platform, base=Target):
-    class EspIdfTarget(base):
+def create_espidf_platform_target(name, platform, base=Target):
+    class EspIdfPlatformTarget(base):
 
         FEATURES = base.FEATURES + []
 
@@ -87,8 +87,7 @@ def parse_stdout(self, out):
                 time_us = int(float(cpu_time_us.group(1)))
             return cycles, time_us
 
-        def get_metrics(self, elf, directory, handle_exit=None, num=None):
-            assert num is None
+        def get_metrics(self, elf, directory, handle_exit=None):
             if self.print_outputs:
                 out = self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit)
             else:
@@ -98,7 +97,7 @@ def get_metrics(self, elf, directory, handle_exit=None, num=None):
             cycles, time_us = self.parse_stdout(out)
 
             metrics = Metrics()
-            metrics.add("Total Cycles", cycles)
+            metrics.add("Cycles", cycles)
             time_s = time_us / 1e6 if time_us is not None else time_us
             metrics.add("Runtime [s]", time_s)
 
@@ -107,4 +106,4 @@ def get_metrics(self, elf, directory, handle_exit=None, num=None):
         def get_arch(self):
             return "unkwown"
 
-    return EspIdfTarget
+    return EspIdfPlatformTarget
diff --git a/mlonmcu/platform/lookup.py b/mlonmcu/platform/lookup.py
index a92ca766d..8ffcab76a 100644
--- a/mlonmcu/platform/lookup.py
+++ b/mlonmcu/platform/lookup.py
@@ -26,23 +26,56 @@ def get_platform_names(context):
     return context.environment.lookup_platform_configs(names_only=True)
 
 
-def get_platforms_targets(context):
+def get_platforms_targets(context, config=None):
     platform_names = get_platform_names(context)
     platform_classes = get_platforms()
     # To initialize the platform we need to provide a config with required paths.
     platforms = []
+    config_ = {}
+    if context:
+        config_.update(context.environment.vars)
+    if config:
+        config_.update(config)
+
     for platform_name in platform_names:
         platform_cls = platform_classes[platform_name]
         required_keys = platform_cls.REQUIRED
-        config = resolve_required_config(
+        new_config = resolve_required_config(
             required_keys,
+            config=config_,
             cache=context.cache,
+            default_flags=context.environment.flags,
         )
-        platform = platform_cls(config=config)
+        platform = platform_cls(config=new_config)
         platforms.append(platform)
     return {platform.name: platform.get_supported_targets() for platform in platforms}
 
 
+def get_platforms_backends(context, config=None):
+    platform_names = get_platform_names(context)
+    platform_classes = get_platforms()
+    # To initialize the platform we need to provide a config with required paths.
+    platforms = []
+    config_ = {}
+    if context:
+        config_.update(context.environment.vars)
+    if config:
+        config_.update(config)
+
+    for platform_name in platform_names:
+        platform_cls = platform_classes[platform_name]
+        required_keys = platform_cls.REQUIRED
+        new_config = resolve_required_config(
+            required_keys,
+            config=config_,
+            cache=context.cache,
+            default_flags=context.environment.flags,
+        )
+        platform = platform_cls(config=new_config)
+        platforms.append(platform)
+    return {platform.name: platform.get_supported_backends() for platform in platforms}
+
+
 def print_platforms(platform_names):
     print("Platforms:")
     if len(platform_names) == 0:
@@ -67,9 +100,27 @@ def print_targets(platform_targets):
         print("  - " + target_name + " [" + ", ".join(target_platforms) + "]")
 
 
+def print_backends(platform_backends):
+    print("Backends:")
+    backend_names = set(itertools.chain(*platform_backends.values()))
+    if len(backend_names) == 0:
+        print("No targets found")
+        return
+    for backend_name in backend_names:
+        backend_platforms = [
+            platform_name
+            for platform_name, platform_backends in platform_backends.items()
+            if backend_name in platform_backends
+        ]
+        print("  - " + backend_name + " [" + ", ".join(backend_platforms) + "]")
+
+
 def print_summary(context):
     print("Platform Targets Summary\n")
     platform_targets = get_platforms_targets(context)
     print_platforms(platform_targets.keys())
     print()
+    platform_backends = get_platforms_backends(context)
+    print_backends(platform_backends)
+    print()
     print_targets(platform_targets)
diff --git a/mlonmcu/platform/microtvm.py b/mlonmcu/platform/microtvm.py
index 44d10f87b..78e402a3a 100644
--- a/mlonmcu/platform/microtvm.py
+++ b/mlonmcu/platform/microtvm.py
@@ -18,71 +18,38 @@
 #
 """MicroTVM Platform"""
 
+import re
 import tempfile
 from pathlib import Path
 
 from mlonmcu.setup import utils
+from mlonmcu.config import str2bool
 from mlonmcu.logging import get_logger
-from mlonmcu.target import SUPPORTED_TARGETS
-from mlonmcu.target.target import Target
 from mlonmcu.artifact import Artifact, ArtifactFormat
 
 from mlonmcu.flow.tvm.backend.python_utils import prepare_python_environment
-from mlonmcu.flow.tvm.backend.tvmc_utils import get_bench_tvmc_args, get_data_tvmc_args
+from mlonmcu.flow.tvm.backend.tvmc_utils import get_bench_tvmc_args, get_data_tvmc_args, get_target_tvmc_args
 
-from .platform import CompilePlatform, TargetPlatform
-from .microtvm_target import create_microtvm_target
+from .platform import CompilePlatform, TargetPlatform, BuildPlatform, TunePlatform
+from .microtvm_target import create_microtvm_platform_target, get_microtvm_platform_targets
+from .microtvm_backend import create_microtvm_platform_backend, get_microtvm_platform_backends
 
 logger = get_logger()
+# TODO: This file is very similar to the TVM platform -> Reuse as much as possible
+
 
+def parse_project_options_from_stdout(out):
+    return re.compile(r"^\s+([A-Za-z0-9_]+)=", re.MULTILINE).findall(out)
 
-# TODO: Replace this hardcoded dict which dynamic lookup
-ALLOWED_PROJECT_OPTIONS = {
-    "arduino": {
-        "create": ["arduino_cli_cmd", "project_type", "warning_as_error"],
-        "build": ["arduino_board", "arduino_cli_cmd", "verbose"],
-        "flash": ["arduino_board", "arduino_cli_cmd", "port", "verbose"],
-        "run": ["arduino_board", "arduino_cli_cmd", "port"],
-    },
-    "zephyr": {
-        "create": [
-            "extra_files_tar",
-            "project_type",
-            "config_main_stack_size",
-            "warning_as_error",
-            "compile_definitions",
-            "zephyr_base",
-            "zephyr_board",
-        ],
-        "build": ["verbose", "west_cmd", "zephyr_base", "zephyr_board"],
-        "flash": ["zephyr_board"],
-        "run": ["gdbserver_port", "nrfjprog_snr", "openocd_serial", "zephyr_base", "zephyr_board"],
-    },
-    # "etissvp": {
-    "template": {
-        "create": [
-            "extra_files_tar",
-            "project_type",
-            "config_main_stack_size",
-            "warning_as_error",
-            "compile_definitions",
-        ],
-        "build": ["verbose", "riscv_path", "etiss_path"],
-        "flash": ["etiss_path", "etissvp_script", "etissvp_script_args", "transport"],
-        "run": ["etissvp_script", "etissvp_script_args", "etiss_path"],
-    },
-}
+
+def filter_project_options(valid, options):
+    return {key: value for key, value in options.items() if key in valid}
 
 
 def get_project_option_args(template, stage, project_options):
     ret = []
-    # TODO: dynamically fetch allowed options per stage (create, build, run)
-    assert template[0] in ALLOWED_PROJECT_OPTIONS
-    assert stage in ALLOWED_PROJECT_OPTIONS[template[0]]
-    allowed = ALLOWED_PROJECT_OPTIONS[template[0]][stage]
     for key, value in project_options.items():
-        if key in allowed:
-            ret.append(f"{key}={value}")
+        ret.append(f"{key}={value}")
 
     if len(ret) > 0:
         ret = ["--project-option"] + ret
@@ -90,10 +57,7 @@ def get_project_option_args(template, stage, project_options):
     return ret
 
 
-# TODO: This file is very similar to the TVM platform -> Reuse as much as possible
-
-
-class MicroTvmPlatform(CompilePlatform, TargetPlatform):
+class MicroTvmPlatform(CompilePlatform, TargetPlatform, BuildPlatform, TunePlatform):
     """TVM Platform class."""
 
     FEATURES = CompilePlatform.FEATURES + TargetPlatform.FEATURES + ["microtvm_etissvp"]  # TODO: validate?
@@ -116,6 +80,8 @@ class MicroTvmPlatform(CompilePlatform, TargetPlatform):
         # "rpc_port": None,
         "tvmc_extra_args": [],
         "tvmc_custom_script": None,
+        "visualize_tuning": False,  # Needs upstream patches
+        "tune_tasks": None,  # Needs upstream patches
     }
 
     REQUIRED = ["tvm.build_dir", "tvm.pythonpath", "tvm.configs_dir"]
@@ -208,6 +174,17 @@ def tvm_configs_dir(self):
     def project_template(self):
         return self.config["project_template"]
 
+    @property
+    def visualize_tuning(self):
+        # Visualize the tuning progress via matplotlib
+        value = self.config["visualize_tuning"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def tune_tasks(self):
+        # Effectively select which tasks should be tuned in the session
+        return self.config["tune_tasks"]
+
     def init_directory(self, path=None, context=None):
         if self.project_dir is not None:
             self.project_dir.mkdir(exist_ok=True)
@@ -234,87 +211,131 @@ def init_directory(self, path=None, context=None):
                 logger.debug("Temporary project directory: %s", self.project_dir)
         self.project_dir.mkdir(exist_ok=True)
 
-    def get_supported_targets(self):
-        # TODO: get this via tvmc micro create-project --help
-        target_names = ["zephyr", "arduino", "template"]
+    def get_supported_backends(self):
+        backend_names = get_microtvm_platform_backends()
+        return backend_names
+
+    def create_backend(self, name):
+        supported = self.get_supported_backends()
+        assert name in supported, f"{name} is not a valid MicroTVM platform backend"
+        base = supported[name]
+        return create_microtvm_platform_backend(name, self, base=base)
 
-        return [f"microtvm_{name}" for name in target_names]
+    def get_supported_targets(self):
+        return get_microtvm_platform_targets()
 
     def create_target(self, name):
-        assert name in self.get_supported_targets(), f"{name} is not a valid MicroTVM device"
-        if name in SUPPORTED_TARGETS:
-            base = SUPPORTED_TARGETS[name]
-        else:
-            base = Target
-        return create_microtvm_target(name, self, base=base)
+        supported = self.get_supported_targets()
+        assert name in supported, f"{name} is not a valid MicroTVM device"
+        base = supported[name]
+        return create_microtvm_platform_target(name, self, base=base)
 
-    def get_tvmc_run_args(self, path, device, num=1):
-        return [
+    def get_tvmc_run_args(self, path, device, list_options=False):
+        ret = [
             path,
             *["--device", device],
             *get_data_tvmc_args(
                 mode=self.fill_mode, ins_file=self.ins_file, outs_file=self.outs_file, print_top=self.print_top
             ),
             *get_bench_tvmc_args(
-                # print_time=True, profile=self.profile, end_to_end=False, repeat=self.repeat, number=num
-                print_time=True,
+                # print_time=True,
+                print_time=False,
                 profile=False,
                 end_to_end=False,
-                repeat=self.repeat,
-                number=num,
+                # repeat=self.repeat,
+                # number=self.number,
             ),
             # *get_rpc_tvmc_args(self.use_rpc, self.rpc_key, self.rpc_hostname, self.rpc_port),
         ]
+        if list_options:
+            ret.append("--list-options")
+        return ret
 
-    def get_tvmc_micro_args(self, command, path, mlf_path, template):
+    def get_tvmc_micro_args(self, command, path, mlf_path, template, list_options=False, tune_args=None):
+        ret = [command]
         if "create" in command:
-            return [command, "--force", path, mlf_path, *template]
+            ret.extend(["--force", path, mlf_path])
+        elif command == "tune":
+            if tune_args:
+                ret.extend(tune_args)
+            if list_options:
+                ret.extend(["--output", "-", "_"])
         else:
-            return [command, path, *template]
+            ret.append(path)
+        ret.extend(template)
+        if list_options:
+            ret.append("--help")
+        return ret
 
-    def invoke_tvmc(self, command, *args):
+    def invoke_tvmc(self, command, *args, target=None):
         env = prepare_python_environment(self.tvm_pythonpath, self.tvm_build_dir, self.tvm_configs_dir)
+        if target:
+            target.update_environment(env)
         if self.tvmc_custom_script is None:
             pre = ["-m", "tvm.driver.tvmc"]
         else:
             pre = [self.tvmc_custom_script]
         return utils.python(*pre, command, *args, live=self.print_outputs, print_output=False, env=env)
 
-    def invoke_tvmc_micro(self, command, path, mlf_path, template, micro=True):
-        args = self.get_tvmc_micro_args(command, path, mlf_path, template)
-        args += get_project_option_args(template, command, self.project_options)
-        return self.invoke_tvmc("micro", *args)
+    def collect_available_project_options(self, command, path, mlf_path, template, micro=True):
+        args = self.get_tvmc_micro_args(command, path, mlf_path, template, list_options=True)
+        out = self.invoke_tvmc("micro", *args)
+        return parse_project_options_from_stdout(out)
+
+    def invoke_tvmc_micro(self, command, path, mlf_path, template, target, extra_args=None, micro=True, tune_args=None):
+        args = self.get_tvmc_micro_args(command, path, mlf_path, template, tune_args=tune_args)
+        options = filter_project_options(
+            self.collect_available_project_options(command, path, mlf_path, template), target.get_project_options()
+        )
+        args += get_project_option_args(template, command, options)
+        return self.invoke_tvmc("micro", *args, target=target)
+
+    def collect_available_run_project_options(self, path, device):
+        args = self.get_tvmc_run_args(path, device, list_options=True)
+        out = self.invoke_tvmc("run", *args)
+        return parse_project_options_from_stdout(out)
 
-    def invoke_tvmc_run(self, path, device, template, num=1, micro=True):
-        args = self.get_tvmc_run_args(path, device, num=num)
+    def invoke_tvmc_run(self, path, device, template, target, micro=True):
+        args = self.get_tvmc_run_args(path, device)
         if micro:
-            args.extend(get_project_option_args(template, "run", self.project_options))
-        return self.invoke_tvmc("run", *args)
+            options = filter_project_options(
+                self.collect_available_run_project_options(path, device), target.get_project_options()
+            )
+            args.extend(get_project_option_args(template, "run", options))
+        return self.invoke_tvmc("run", *args, target=target)
 
     def close(self):
         if self.tempdir:
             self.tempdir.cleanup()
 
     def get_template_args(self, target):
-        target_template = target.template
-        if target_template == "template":
-            assert self.project_template is not None
-            return (target_template, "--template-dir", self.project_template)
+        template = target.template
+        if target.template_path:
+            template = "template"
+            template_path = target.template_path
         else:
-            return (target_template,)
+            if template == "template":
+                assert self.project_template is not None
+                template_path = self.project_template
+            else:
+                template_path = None
+        if template_path:
+            return ("template", "--template-dir", template_path)
+        else:
+            return (template,)
 
     def prepare(self, mlf, target):
-        out = self.invoke_tvmc_micro("create", self.project_dir, mlf, self.get_template_args(target))
+        out = self.invoke_tvmc_micro("create", self.project_dir, mlf, self.get_template_args(target), target)
         return out
 
     def compile(self, target):
         out = ""
         # TODO: build with cmake options
-        out += self.invoke_tvmc_micro("build", self.project_dir, None, self.get_template_args(target))
+        out += self.invoke_tvmc_micro("build", self.project_dir, None, self.get_template_args(target), target)
         # TODO: support self.num_threads (e.g. patch esp-idf)
         return out
 
-    def generate_elf(self, src, target, model=None, num=1, data_file=None):
+    def generate_elf(self, src, target, model=None, data_file=None):
         # TODO: name missleading as we are not interested in the ELF
         src = Path(src) / "default.tar"  # TODO: lookup for *.tar file
         artifacts = []
@@ -333,13 +354,129 @@ def flash(self, elf, target, timeout=120):
             logger.debug("Ignoring ELF file for microtvm platform")
         # TODO: implement timeout
         logger.debug("Flashing target software using MicroTVM ProjectAPI")
-        output = self.invoke_tvmc_micro("flash", self.project_dir, None, self.get_template_args(target))
+        output = self.invoke_tvmc_micro("flash", self.project_dir, None, self.get_template_args(target), target)
         return output
 
-    def run(self, elf, target, timeout=120, num=1):
+    def run(self, elf, target, timeout=120):
         # TODO: implement timeout
         output = self.flash(elf, target)
         output += self.invoke_tvmc_run(
-            str(self.project_dir), "micro", self.get_template_args(target), num=num, micro=True
+            str(self.project_dir), "micro", self.get_template_args(target), target, micro=True
         )
         return output
+
+    def get_micro_tune_args(self, model, backend, out):
+
+        tuner = backend.config.get("autotuning_tuner", "ga")
+        assert tuner in ["ga", "gridsearch", "random", "xgb", "xgb_knob", "xgb-rank"]
+        trials = backend.config.get("autotuning_trials", 10)
+        if not isinstance(trials, int):
+            trials = int(trials)
+        early_stopping = backend.config.get("autotuning_early_stopping", None)
+        if early_stopping is None:
+            early_stopping = max(trials, 10)  # Let's see if this default works out...
+        early_stopping = int(early_stopping)
+        # max_parallel = backend.config.get("autotuning_max_parallel", 1)
+        # max_parallel = 1
+        timeout = backend.config.get("autotuning_timeout", 1000)
+        results_file = backend.config.get("autotuning_results_file", None)
+        desired_layout = backend.config.get("desired_layout", None)
+        ret = [
+            *get_target_tvmc_args(
+                # "llvm", extra_target=backend.extra_target, target_details=backend.get_target_details()
+                "c",
+                extra_target=backend.extra_target,
+                target_details=backend.get_target_details(),
+            ),
+            *(["--desired-layout", desired_layout] if desired_layout is not None else []),
+            # *get_rpc_tvmc_args(self.use_rpc, self.key, self.hostname, self.port),
+            # TODO: missing: pass config, disabled_pass, etc.
+            *["--tuner", tuner],
+            *(["--early-stopping", str(early_stopping)] if early_stopping > 0 else []),
+            # *["--parallel", str(max_parallel)],
+            # *["--timeout", str(timeout * max_parallel)],
+            *["--timeout", str(timeout)],
+            *["--trials", str(trials)],
+            # *["--number", str(100)],
+            *(["--tuning-records", results_file] if results_file is not None else []),
+            *["--output", str(out)],
+            *(["--visualize"] if self.visualize_tuning else []),
+            *(["--tasks", str(self.tune_tasks)] if self.tune_tasks is not None else []),
+            # "--target-c-link-params",
+            # "1",
+            model,
+        ]
+        return ret
+
+    def tune_model(self, model_path, backend, target):
+        enable = backend.config["autotuning_enable"]
+        results_file = backend.config["autotuning_results_file"]
+        append = backend.config["autotuning_append"]
+        artifacts = []
+        if self.print_outputs:
+            verbose = True
+
+        content = ""
+        if enable:
+
+            if append:
+                if results_file is not None:
+                    with open(results_file, "r") as handle:
+                        content = handle.read()
+
+            with tempfile.TemporaryDirectory() as tmp_dir:
+                out_file = Path(tmp_dir) / "tuning_results.log.txt"
+                with open(out_file, "w") as handle:
+                    handle.write(content)
+                    # TODO: newline or not?
+                # out = self.invoke_tvmc_tune(out_file, verbose=verbose)
+                tune_args = self.get_micro_tune_args(model_path, backend, out_file)
+                out = self.invoke_tvmc_micro(
+                    "tune", self.project_dir, None, self.get_template_args(target), target, tune_args=tune_args
+                )
+                with open(out_file, "r") as handle:
+                    content = handle.read()
+        else:
+            if results_file is None:
+                return []
+            assert Path(results_file).is_file()
+            with open(results_file, "r") as handle:
+                content = handle.read()
+
+        artifact = Artifact("tuning_results.log.txt", content=content, fmt=ArtifactFormat.TEXT)
+        artifacts.append(artifact)
+
+        # pick best records
+        def _pick_best(backend, records, verbose=False):
+            content_best = ""
+            with tempfile.TemporaryDirectory() as tmp_dir:
+                in_file = Path(tmp_dir) / "tuning_results.log.txt"
+                with open(in_file, "w") as handle:
+                    handle.write(records)
+                out_file = Path(tmp_dir) / "best_tuning_results.log.txt"
+                args = [
+                    "--mode",
+                    "pick",
+                    "--i",
+                    in_file,
+                    "--o",
+                    out_file,
+                ]
+                env = prepare_python_environment(backend.tvm_pythonpath, backend.tvm_build_dir, backend.tvm_configs_dir)
+                utils.python("-m", "tvm.autotvm.record", *args, live=verbose, env=env)
+                with open(out_file, "r") as handle:
+                    content_best = handle.read()
+            return content_best
+
+        content_best = _pick_best(backend, content, verbose=verbose)
+        if len(content_best) > 0:
+            artifact_ = Artifact("best_tuning_results.log.txt", content=content_best, fmt=ArtifactFormat.TEXT)
+            artifacts.append(artifact_)
+
+        if enable:
+            stdout_artifact = Artifact(
+                "tvmc_micro_tune_out.log", content=out, fmt=ArtifactFormat.TEXT
+            )  # TODO: rename to tvmaot_out.log?
+            artifacts.append(stdout_artifact)
+
+        return artifacts
diff --git a/mlonmcu/platform/microtvm_backend.py b/mlonmcu/platform/microtvm_backend.py
new file mode 100644
index 000000000..5db231887
--- /dev/null
+++ b/mlonmcu/platform/microtvm_backend.py
@@ -0,0 +1,62 @@
+#
+# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+#
+# This file is part of MLonMCU.
+# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from mlonmcu.logging import get_logger
+
+from mlonmcu.flow.tvm.backend.backend import TVMBackend
+from mlonmcu.flow.tvm.backend.tvmaot import TVMAOTBackend
+from mlonmcu.flow.tvm.backend.tvmllvm import TVMLLVMBackend
+from mlonmcu.flow.tvm.backend.tvmrt import TVMRTBackend
+
+logger = get_logger()
+
+MICROTVM_PLATFORM_BACKEND_REGISTRY = {}
+
+
+def register_microtvm_platform_backend(backend_name, b, override=False):
+    global MICROTVM_PLATFORM_BACKEND_REGISTRY
+
+    if backend_name in MICROTVM_PLATFORM_BACKEND_REGISTRY and not override:
+        raise RuntimeError(f"TVM platform backend  {backend_name} is already registered")
+    MICROTVM_PLATFORM_BACKEND_REGISTRY[backend_name] = b
+
+
+def get_microtvm_platform_backends():
+    return MICROTVM_PLATFORM_BACKEND_REGISTRY
+
+
+register_microtvm_platform_backend("tvmaot", TVMAOTBackend)
+register_microtvm_platform_backend("tvmrt", TVMRTBackend)
+register_microtvm_platform_backend("tvmllvm", TVMLLVMBackend)
+
+
+def create_microtvm_platform_backend(name, platform, base=TVMBackend):
+    class MicroTvmPlatformBackend(base):
+
+        FEATURES = base.FEATURES + []
+
+        DEFAULTS = {
+            **base.DEFAULTS,
+        }
+        REQUIRED = base.REQUIRED + []
+
+        def __init__(self, features=None, config=None):
+            super().__init__(runtime="crt", fmt="mlf", features=features, config=config)
+            self.platform = platform
+
+    return MicroTvmPlatformBackend
diff --git a/mlonmcu/platform/microtvm_target.py b/mlonmcu/platform/microtvm_target.py
index b81ca7ed6..8213417c8 100644
--- a/mlonmcu/platform/microtvm_target.py
+++ b/mlonmcu/platform/microtvm_target.py
@@ -18,9 +18,11 @@
 #
 import re
 import os
+from pathlib import Path
 
 from mlonmcu.target.target import Target
 from mlonmcu.target.metrics import Metrics
+from mlonmcu.feature.features import SUPPORTED_TVM_BACKENDS
 
 from mlonmcu.logging import get_logger
 
@@ -31,8 +33,367 @@ def name2template(name):
     return name.replace("microtvm_", "")
 
 
-def create_microtvm_target(name, platform, base=Target):
-    class MicroTvmTarget(base):
+MICROTVM_PLATFORM_TARGET_REGISTRY = {}
+
+
+def register_microtvm_platform_target(target_name, t, override=False):
+    global MICROTVM_PLATFORM_TARGET_REGISTRY
+
+    if target_name in MICROTVM_PLATFORM_TARGET_REGISTRY and not override:
+        raise RuntimeError(f"MicroTVM platform target {target_name} is already registered")
+    MICROTVM_PLATFORM_TARGET_REGISTRY[target_name] = t
+
+
+def get_microtvm_platform_targets():
+    return MICROTVM_PLATFORM_TARGET_REGISTRY
+
+
+class TemplateMicroTvmPlatformTarget(Target):
+
+    FEATURES = Target.FEATURES + []
+
+    DEFAULTS = {
+        **Target.DEFAULTS,
+    }
+    REQUIRED = Target.REQUIRED + ["tvm.build_dir"]
+
+    def __init__(self, name=None, features=None, config=None):
+        super().__init__(name=name, features=features, config=config)
+        self.template_path = None
+        self.option_names = []
+        # self.platform = platform
+        # self.template = name2template(name)
+
+    def get_project_options(self):
+        ret = {
+            key: str(value).lower() if isinstance(value, bool) else value
+            for key, value in self.config.items()
+            if key in self.option_names and value is not None
+        }
+        print("ret", ret)
+        return {
+            key: str(value).lower() if isinstance(value, bool) else value
+            for key, value in self.config.items()
+            if key in self.option_names and value is not None
+        }
+
+    def update_environment(self, env):
+        pass
+
+
+class ArduinoMicroTvmPlatformTarget(TemplateMicroTvmPlatformTarget):
+
+    FEATURES = Target.FEATURES + []
+
+    DEFAULTS = {
+        **Target.DEFAULTS,
+        "project_type": "host_driven",
+        "warning_as_error": False,
+        "arduino_board": "?",
+        # "arduino_cli_cmd": None,
+        "verbose": False,
+        "port": -1,
+    }
+    REQUIRED = Target.REQUIRED + ["arduino.install_dir"]
+
+    def __init__(self, name=None, features=None, config=None):
+        super().__init__(name=name, features=features, config=config)
+        self.template_path = None
+        # self.option_names = ["project_type", "warning_as_error", "arduino_board", "verbose", "port"]
+        self.option_names = ["project_type", "warning_as_error", "arduino_board", "port"]
+        # self.platform = platform
+        # self.template = name2template(name)
+
+    @property
+    def arduino_install_dir(self):
+        return Path(self.config["arduino.install_dir"])
+
+    @property
+    def port(self):
+        return Path(self.config["port"])
+
+    def get_project_options(self):
+        ret = super().get_project_options()
+        ret.update({"arduino_cli_cmd": self.arduino_install_dir / "arduino-cli"})
+        return ret
+
+    def update_environment(self, env):
+        super().update_environment(env)
+        if self.port:
+            env["ESPTOOL_PORT"] = self.port  # TODO: required?
+
+
+class ZephyrMicroTvmPlatformTarget(TemplateMicroTvmPlatformTarget):
+
+    FEATURES = Target.FEATURES + []
+
+    DEFAULTS = {
+        **Target.DEFAULTS,
+        "extra_files_tar": None,
+        "project_type": "host_driven",
+        "zephyr_board": "",
+        # "zephyr_base": "?",
+        # "west_cmd": "?",
+        "verbose": False,
+        "warning_as_error": True,
+        "compile_definitions": "",
+        # "config_main_stack_size": None,
+        "config_main_stack_size": "16384",
+        "gdbserver_port": None,
+        "nrfjprog_snr": None,
+        "openocd_serial": None,
+        "port": None,  # Workaround to overwrite esptool detection
+    }
+    REQUIRED = Target.REQUIRED + ["zephyr.install_dir", "zephyr.sdk_dir"]
+
+    def __init__(self, name=None, features=None, config=None):
+        super().__init__(name=name, features=features, config=config)
+        self.template_path = None
+        self.option_names = [
+            "extra_files_tar",
+            "project_type",
+            "zephyr_board",
+            # "verbose",
+            "warning_as_error",
+            "compile_definitions",
+            "config_main_stack_size",
+            "gdbserver_port",
+            "nrfjprog_snr",
+            "openocd_serial",
+        ]
+        # self.platform = platform
+        # self.template = name2template(name)
+
+    @property
+    def zephyr_install_dir(self):
+        return Path(self.config["zephyr.install_dir"])
+
+    @property
+    def port(self):
+        return self.config["port"]
+
+    @property
+    def zephyr_sdk_dir(self):
+        return Path(self.config["zephyr.sdk_dir"])
+
+    def get_project_options(self):
+        ret = super().get_project_options()
+        ret.update({"zephyr_base": self.zephyr_install_dir / "zephyr"})
+        return ret
+
+    def update_environment(self, env):
+        super().update_environment(env)
+        env["ZEPHYR_BASE"] = str(self.zephyr_install_dir / "zephyr")
+        env["ZEPHYR_SDK_INSTALL_DIR"] = str(self.zephyr_sdk_dir)
+        if self.port:
+            env["ESPTOOL_PORT"] = self.port
+
+        # TODO
+
+
+class HostMicroTvmPlatformTarget(TemplateMicroTvmPlatformTarget):
+
+    FEATURES = Target.FEATURES + []
+
+    DEFAULTS = {
+        **Target.DEFAULTS,
+        "verbose": False,
+    }
+    REQUIRED = Target.REQUIRED + ["tvm.build_dir"]
+
+    def __init__(self, name=None, features=None, config=None):
+        super().__init__(name=name, features=features, config=config)
+        self.template_path = self.tvm_build_dir / "standalone_crt" / "template" / "host"
+        # self.option_names = ["verbose"]
+
+    @property
+    def tvm_build_dir(self):
+        return Path(self.config["tvm.build_dir"])
+
+
+class EtissvpMicroTvmPlatformTarget(TemplateMicroTvmPlatformTarget):
+
+    FEATURES = Target.FEATURES + []
+
+    DEFAULTS = {
+        **Target.DEFAULTS,
+        "extra_files_tar": None,
+        "project_type": "?",
+        "verbose": False,
+        "warning_as_error": True,
+        "compile_definitions": "",
+        "config_main_stack_size": -1,
+        # "riscv_path": "?",
+        # "etiss_path": "?",
+        # "etissvp_script": "?",
+        "etissvp_script_args": "?",
+        "transport": True,
+    }
+    REQUIRED = Target.REQUIRED + ["microtvm_etissvp.src_dir", "riscv_gcc.install_dir", "etiss.install_dir"]
+
+    def __init__(self, name=None, features=None, config=None):
+        super().__init__(name=name, features=features, config=config)
+        self.template_path = self.microtvm_etissvp.src_dir / "template_project"
+        self.option_names = [
+            "extra_files_tar",
+            "project_type",
+            # "verbose",
+            "warning_as_error",
+            "compile_definitions",
+            "config_main_stack_size",
+            "etissvp_script_args",
+            "transport",
+        ]
+
+    @property
+    def microtvm_etissvp_src_dir(self):
+        return Path(self.config["microtvm_etissvp.src_dir"])
+
+    @property
+    def riscv_gcc_install_dir(self):
+        return Path(self.config["riscv_gcc_install_dir"])
+
+    @property
+    def etiss_install_dir(self):
+        return Path(self.config["etiss.install_dir"])
+
+    def get_project_options(self):
+        ret = super().get_project_options()
+        ret.update(
+            {
+                "riscv_path": self.riscv_gcc_install_dir,
+                "etiss_path": self.etiss_install_dir,
+                "etissvp_script": self.etiss_install_dir / "bin" / "run_helper.sh",
+            }
+        )
+        return ret
+
+    def get_backend_config(self, backend):
+        if backend in SUPPORTED_TVM_BACKENDS:
+            return {
+                "target_device": "riscv_cpu",
+                # "target_march": "TODO",
+                # "target_model": "TODO",
+                # "target_mtriple": "TODO",
+                # "target_mabi": "TODO",
+                # "target_mattr": "TODO",
+                # "target_mcpu": "TODO",
+            }
+        return {}
+
+
+# class EspidfMicroTvmPlatformTarget(TemplateMicroTvmPlatformTarget):
+#
+#     FEATURES = Target.FEATURES + []
+#
+#     DEFAULTS = {
+#         **Target.DEFAULTS,
+#         "verbose": False,
+#         "?": "?",  # TODO
+#     }
+#     REQUIRED = Target.REQUIRED + ["microtvm_espidf.src_dir", "espidf.src_dir", "espidf.install_dir"]
+#
+#     def __init__(self, name=None, features=None, config=None):
+#         super().__init__(name=name, features=features, config=config)
+#         self.template_path = None
+
+
+class SpikeMicroTvmPlatformTarget(TemplateMicroTvmPlatformTarget):
+
+    FEATURES = Target.FEATURES + []
+
+    DEFAULTS = {
+        **Target.DEFAULTS,
+        "verbose": True,
+        # "spike_exe": None,
+        # "spike_pk": None,
+        "arch": None,
+        "abi": None,
+        "spike_extra_args": None,
+        "pk_extra_args": None,
+        # "triple": None,
+    }
+    REQUIRED = Target.REQUIRED + ["spike.exe", "spike.pk", "riscv_gcc.name", "riscv_gcc.install_dir", "tvm.src_dir"]
+
+    def __init__(self, name=None, features=None, config=None):
+        super().__init__(name=name, features=features, config=config)
+        self.template_path = self.tvm_src_dir / "apps" / "microtvm" / "spike"
+        # TODO: interate into TVM build config
+        self.option_names = [
+            "verbose",
+            "spike_exe",
+            "spike_pk",
+            "arch",
+            "abi",
+            "triple",
+            "spike_extra_args",
+            "pk_extra_args",
+        ]
+
+    @property
+    def spike_exe(self):
+        return Path(self.config["spike.exe"])
+
+    @property
+    def spike_pk(self):
+        return Path(self.config["spike.pk"])
+
+    @property
+    def riscv_gcc_name(self):
+        return self.config["riscv_gcc.name"]
+
+    @property
+    def riscv_gcc_install_dir(self):
+        return Path(self.config["riscv_gcc.install_dir"])
+
+    @property
+    def tvm_src_dir(self):
+        return Path(self.config["tvm.src_dir"])
+
+    def get_project_options(self):
+        ret = super().get_project_options()
+        ret.update(
+            {
+                "spike_exe": str(self.spike_exe),
+                "spike_pk": str(self.spike_pk),
+                "triple": str(self.riscv_gcc_install_dir / "bin" / self.riscv_gcc_name),
+            }
+        )
+        return ret
+
+    def update_environment(self, env):
+        super().update_environment(env)
+        if "PATH" in env:
+            env["PATH"] = str(self.riscv_gcc_install_dir / "bin") + ":" + env["PATH"]
+        else:
+            env["PATH"] = str(self.riscv_gcc_install_dir / "bin")
+
+    def get_backend_config(self, backend):
+        if backend in SUPPORTED_TVM_BACKENDS:
+            return {
+                "target_device": "riscv_cpu",
+                "target_march": self.config.get("arch", None),
+                # "target_model": "TODO",
+                # "target_mtriple": "TODO",
+                "target_mtriple": self.riscv_gcc_name,
+                "target_mabi": self.config.get("abi", None),
+                # "target_mattr": "TODO",
+                # "target_mcpu": "TODO",
+            }
+        return {}
+
+
+# register_microtvm_platform_target("microtvm_template", ZephyrMicroTvmPlatformTarget)
+register_microtvm_platform_target("microtvm_zephyr", ZephyrMicroTvmPlatformTarget)
+register_microtvm_platform_target("microtvm_arduino", ArduinoMicroTvmPlatformTarget)
+register_microtvm_platform_target("microtvm_host", HostMicroTvmPlatformTarget)
+register_microtvm_platform_target("microtvm_etissvp", EtissvpMicroTvmPlatformTarget)
+# register_microtvm_platform_target("microtvm_espidf", EtissvpMicroTvmPlatformTarget)
+register_microtvm_platform_target("microtvm_spike", SpikeMicroTvmPlatformTarget)
+
+
+def create_microtvm_platform_target(name, platform, base=Target):
+    class MicroTvmPlatformTarget(base):
 
         FEATURES = base.FEATURES + []
 
@@ -51,7 +412,11 @@ def __init__(self, features=None, config=None):
         def timeout_sec(self):
             return int(self.config["timeout_sec"])
 
-        def exec(self, program, *args, num=1, cwd=os.getcwd(), **kwargs):
+        @property
+        def repeat(self):
+            return None  # This is handled at the platform level
+
+        def exec(self, program, *args, cwd=os.getcwd(), **kwargs):
             """Use target to execute a executable with given arguments"""
             if len(args) > 0:
                 raise RuntimeError("Program arguments are not supported for real hardware devices")
@@ -61,7 +426,7 @@ def exec(self, program, *args, num=1, cwd=os.getcwd(), **kwargs):
             if self.timeout_sec > 0:
                 raise NotImplementedError
 
-            ret = self.platform.run(program, self, num=num)
+            ret = self.platform.run(program, self)
             return ret
 
         def parse_stdout(self, out):
@@ -90,7 +455,7 @@ def parse_stdout(self, out):
                     found = True
             return mean_ms, median_ms, max_ms, min_ms, std_ms
 
-        def get_metrics(self, elf, directory, handle_exit=None, num=1):
+        def get_metrics(self, elf, directory, handle_exit=None):
             if self.print_outputs:
                 out = self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit)
             else:
@@ -100,7 +465,6 @@ def get_metrics(self, elf, directory, handle_exit=None, num=1):
                     live=False,
                     print_func=lambda *args, **kwargs: None,
                     handle_exit=handle_exit,
-                    num=num,
                 )
             mean_ms, _, _, _, _ = self.parse_stdout(out)
 
@@ -113,4 +477,4 @@ def get_metrics(self, elf, directory, handle_exit=None, num=1):
         def get_arch(self):
             return "unkwown"
 
-    return MicroTvmTarget
+    return MicroTvmPlatformTarget
diff --git a/mlonmcu/platform/mlif.py b/mlonmcu/platform/mlif.py
index f2e2249be..423d3574d 100644
--- a/mlonmcu/platform/mlif.py
+++ b/mlonmcu/platform/mlif.py
@@ -28,7 +28,7 @@
 from mlonmcu.target.target import Target
 
 from .platform import CompilePlatform, TargetPlatform
-from .mlif_target import get_mlif_targets, create_mlif_target
+from .mlif_target import get_mlif_platform_targets, create_mlif_platform_target
 
 logger = get_logger()
 
@@ -49,6 +49,8 @@ class MlifPlatform(CompilePlatform, TargetPlatform):
             "pext",
             "arm_mvei",
             "arm_dsp",
+            "auto_vectorize",
+            "benchmark",
         ]  # TODO: allow Feature-Features with automatic resolution of initialization order
     )
 
@@ -60,9 +62,11 @@ class MlifPlatform(CompilePlatform, TargetPlatform):
         "model_support_dir": None,
         "toolchain": "gcc",
         "prebuild_lib_path": None,
+        "optimize": None,  # values: 0,1,2,3,s
     }
 
     REQUIRED = ["mlif.src_dir"]
+    OPTIONAL = ["llvm.install_dir"]
 
     def __init__(self, features=None, config=None):
         super().__init__(
@@ -106,12 +110,16 @@ def create_target(self, name):
             base = SUPPORTED_TARGETS[name]
         else:
             base = Target
-        return create_mlif_target(name, self, base=base)
+        return create_mlif_platform_target(name, self, base=base)
 
     @property
     def mlif_dir(self):
         return Path(self.config["mlif.src_dir"])
 
+    @property
+    def llvm_dir(self):
+        return self.config["llvm.install_dir"]
+
     @property
     def ignore_data(self):
         return bool(self.config["ignore_data"])
@@ -136,24 +144,33 @@ def model_support_dir(self):
     def prebuild_lib_dir(self):
         return self.config["prebuild_lib_dir"]
 
+    @property
+    def optimize(self):
+        return self.config["optimize"]
+
     def get_supported_targets(self):
-        target_names = get_mlif_targets()
+        target_names = get_mlif_platform_targets()
         return target_names
 
     def close(self):
         if self.tempdir:
             self.tempdir.cleanup()
 
-    def get_common_cmake_args(self, num=1):
+    def get_common_cmake_args(self):
         args = []
-        args.append(f"-DNUM_RUNS={num}")
         args.append(f"-DTOOLCHAIN={self.toolchain}")
+        if self.toolchain == "llvm" and self.llvm_dir is None:
+            raise RuntimeError("Missing config variable: llvm.install_dir")
+        else:
+            args.append(f"-DLLVM_DIR={self.llvm_dir}")
+        if self.optimize:
+            args.append(f"-DOPTIMIZE={self.optimize}")
         return args
 
     def prepare(self):
         self.init_directory()
 
-    def configure(self, target, src, _model, num=1, data_file=None):
+    def configure(self, target, src, _model, data_file=None):
         del target
         if not isinstance(src, Path):
             src = Path(src)
@@ -162,7 +179,7 @@ def configure(self, target, src, _model, num=1, data_file=None):
             if isinstance(value, bool):
                 value = "ON" if value else "OFF"
             cmakeArgs.append(f"-D{key}={value}")
-        cmakeArgs.extend(self.get_common_cmake_args(num=num))
+        cmakeArgs.extend(self.get_common_cmake_args())
         if self.model_support_dir:
             cmakeArgs.append(f"-DMODEL_SUPPORT_DIR={self.model_support_dir}")
         else:
@@ -190,10 +207,10 @@ def configure(self, target, src, _model, num=1, data_file=None):
         )
         return out
 
-    def compile(self, target, src=None, model=None, num=1, data_file=None):
+    def compile(self, target, src=None, model=None, data_file=None):
         out = ""
         if src:
-            out += self.configure(target, src, model, num=num, data_file=data_file)
+            out += self.configure(target, src, model, data_file=data_file)
         out += utils.make(
             self.goal,
             cwd=self.build_dir,
@@ -202,9 +219,9 @@ def compile(self, target, src=None, model=None, num=1, data_file=None):
         )
         return out
 
-    def generate_elf(self, src, target, model=None, num=1, data_file=None):
+    def generate_elf(self, src, target, model=None, data_file=None):
         artifacts = []
-        out = self.compile(target, src=src, model=model, num=num, data_file=data_file)
+        out = self.compile(target, src=src, model=model, data_file=data_file)
         elf_file = self.build_dir / "bin" / "generic_mlif"
         # TODO: just use path instead of raw data?
         with open(elf_file, "rb") as handle:
diff --git a/mlonmcu/platform/mlif_target.py b/mlonmcu/platform/mlif_target.py
index dcdacd102..33aaf8dac 100644
--- a/mlonmcu/platform/mlif_target.py
+++ b/mlonmcu/platform/mlif_target.py
@@ -18,36 +18,40 @@
 #
 from enum import IntEnum
 
-from mlonmcu.target.target import Target
-from mlonmcu.target.host_x86 import HostX86Target
-from mlonmcu.target.etiss_pulpino import EtissPulpinoTarget
-from mlonmcu.target.corstone300 import Corstone300Target
-from mlonmcu.target.spike import SpikeTarget
-from mlonmcu.target.ovpsim import OVPSimTarget
+from mlonmcu.target import (
+    Target,
+    EtissPulpinoTarget,
+    SpikeTarget,
+    OVPSimTarget,
+    Corstone300Target,
+    HostX86Target,
+    RiscvQemuTarget,
+)
 from mlonmcu.logging import get_logger
 
 logger = get_logger()
 
-MLIF_TARGET_REGISTRY = {}
+MLIF_PLATFORM_TARGET_REGISTRY = {}
 
 
-def register_mlif_target(target_name, t, override=False):
-    global MLIF_TARGET_REGISTRY
+def register_mlif_platform_target(target_name, t, override=False):
+    global MLIF_PLATFORM_TARGET_REGISTRY
 
-    if target_name in MLIF_TARGET_REGISTRY and not override:
-        raise RuntimeError(f"MLIF target {target_name} is already registered")
-    MLIF_TARGET_REGISTRY[target_name] = t
+    if target_name in MLIF_PLATFORM_TARGET_REGISTRY and not override:
+        raise RuntimeError(f"MLIF platform target {target_name} is already registered")
+    MLIF_PLATFORM_TARGET_REGISTRY[target_name] = t
 
 
-def get_mlif_targets():
-    return MLIF_TARGET_REGISTRY
+def get_mlif_platform_targets():
+    return MLIF_PLATFORM_TARGET_REGISTRY
 
 
-register_mlif_target("host_x86", HostX86Target)
-register_mlif_target("etiss_pulpino", EtissPulpinoTarget)
-register_mlif_target("corstone300", Corstone300Target)
-register_mlif_target("spike", SpikeTarget)
-register_mlif_target("ovpsim", OVPSimTarget)
+register_mlif_platform_target("host_x86", HostX86Target)
+register_mlif_platform_target("etiss_pulpino", EtissPulpinoTarget)
+register_mlif_platform_target("corstone300", Corstone300Target)
+register_mlif_platform_target("spike", SpikeTarget)
+register_mlif_platform_target("ovpsim", OVPSimTarget)
+register_mlif_platform_target("riscv_qemu", RiscvQemuTarget)
 
 
 class MlifExitCode(IntEnum):
@@ -60,8 +64,8 @@ def values(cls):
         return list(map(int, cls))
 
 
-def create_mlif_target(name, platform, base=Target):
-    class MlifTarget(base):  # This is not ideal as we will have multiple different MlifTarget classes
+def create_mlif_platform_target(name, platform, base=Target):
+    class MlifPlatformTarget(base):
 
         FEATURES = base.FEATURES + []
 
@@ -75,8 +79,7 @@ def __init__(self, features=None, config=None):
             self.platform = platform
             self.validation_result = None
 
-        def get_metrics(self, elf, directory, handle_exit=None, num=None):
-            assert num is None
+        def get_metrics(self, elf, directory, handle_exit=None):
 
             # This is wrapper around the original exec function to catch special return codes thrown by the inout data
             # feature (TODO: catch edge cases: no input data available (skipped) and no return code (real hardware))
@@ -108,4 +111,4 @@ def get_platform_defs(self, platform):
             ret["TARGET_SYSTEM"] = target_system
             return ret
 
-    return MlifTarget
+    return MlifPlatformTarget
diff --git a/mlonmcu/platform/platform.py b/mlonmcu/platform/platform.py
index 2b01105a5..ab564a689 100644
--- a/mlonmcu/platform/platform.py
+++ b/mlonmcu/platform/platform.py
@@ -57,23 +57,27 @@ class Platform:
     }
 
     REQUIRED = []
+    OPTIONAL = []
 
-    # def __init__(self, name, framework, backend, target, features=None, config=None, context=None):
     def __init__(self, name, features=None, config=None):
         self.name = name
-        # self.framework = framework  # TODO: required? or self.target.framework?
-        # self.backend = backend
-        # self.target = target
         self.config = config if config else {}
         self.definitions = {}
         self.features = self.process_features(features)
-        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.REQUIRED)
-        # self.context = context
+        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
         self.artifacts = []
 
     def init_directory(self, path=None, context=None):
         raise NotImplementedError
 
+    @property
+    def supports_build(self):
+        return False
+
+    @property
+    def supports_tune(self):
+        return False
+
     @property
     def supports_compile(self):
         return False
@@ -101,10 +105,67 @@ def process_features(self, features):
             feature.add_platform_defs(self.name, self.definitions)
         return features
 
+    def get_supported_backends(self):
+        return []
+
     def get_supported_targets(self):
         return []
 
 
+class BuildPlatform(Platform):
+    """Abstract backend platform class."""
+
+    FEATURES = Platform.FEATURES + []
+
+    DEFAULTS = {
+        **Platform.DEFAULTS,
+    }
+
+    REQUIRED = []
+
+    @property
+    def supports_build(self):
+        return True
+
+    def export_elf(self, path):
+        assert len(self.artifacts) > 0, "No artifacts found, please run generate_elf() first"
+
+        if not isinstance(path, Path):
+            path = Path(path)
+        assert (
+            path.is_dir()
+        ), "The supplied path does not exists."  # Make sure it actually exists (we do not create it by default)
+        for artifact in self.artifacts:
+            artifact.export(path)
+
+
+class TunePlatform(Platform):
+    """Abstract backend platform class."""
+
+    FEATURES = Platform.FEATURES + []
+
+    DEFAULTS = {
+        **Platform.DEFAULTS,
+    }
+
+    REQUIRED = []
+
+    @property
+    def supports_tune(self):
+        return True
+
+    def export_elf(self, path):
+        assert len(self.artifacts) > 0, "No artifacts found, please run generate_elf() first"
+
+        if not isinstance(path, Path):
+            path = Path(path)
+        assert (
+            path.is_dir()
+        ), "The supplied path does not exists."  # Make sure it actually exists (we do not create it by default)
+        for artifact in self.artifacts:
+            artifact.export(path)
+
+
 class CompilePlatform(Platform):
     """Abstract compile platform class."""
 
@@ -119,17 +180,6 @@ class CompilePlatform(Platform):
 
     REQUIRED = []
 
-    def __init__(self, name, features=None, config=None):
-        super().__init__(
-            name,
-            features=features,
-            config=config,
-        )
-        self.name = name
-        self.config = config if config else {}
-        self.features = self.process_features(features)
-        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.REQUIRED)
-
     @property
     def supports_compile(self):
         return True
@@ -163,19 +213,11 @@ def get_metrics(self, elf):
         metrics.add("RAM zero-init data", ram_zdata)
         return metrics
 
-    def generate_elf(self, src, target, model=None, num=1, data_file=None):
+    def generate_elf(self, src, target, model=None, data_file=None):
         raise NotImplementedError
 
-    def export_elf(self, path):
-        assert len(self.artifacts) > 0, "No artifacts found, please run generate_elf() first"
-
-        if not isinstance(path, Path):
-            path = Path(path)
-        assert (
-            path.is_dir()
-        ), "The supplied path does not exists."  # Make sure it actually exists (we do not create it by default)
-        for artifact in self.artifacts:
-            artifact.export(path)
+    def tune_model(self, path):
+        raise NotImplementedError
 
 
 class TargetPlatform(Platform):
diff --git a/mlonmcu/platform/tvm.py b/mlonmcu/platform/tvm.py
index a2dbcb9f1..e15b697fc 100644
--- a/mlonmcu/platform/tvm.py
+++ b/mlonmcu/platform/tvm.py
@@ -18,7 +18,6 @@
 #
 """TVM Platform"""
 
-
 import tempfile
 from pathlib import Path
 
@@ -30,16 +29,17 @@
 from mlonmcu.flow.tvm.backend.tvmc_utils import get_bench_tvmc_args, get_data_tvmc_args, get_rpc_tvmc_args
 from mlonmcu.flow.tvm.backend.python_utils import prepare_python_environment
 
-from .platform import TargetPlatform
-from .tvm_target import create_tvm_target
+from .platform import TargetPlatform, BuildPlatform
+from .tvm_target import create_tvm_platform_target
+from .tvm_backend import create_tvm_platform_backend, get_tvm_platform_backends
 
 logger = get_logger()
 
 
-class TvmPlatform(TargetPlatform):
+class TvmPlatform(BuildPlatform, TargetPlatform):
     """TVM Platform class."""
 
-    FEATURES = TargetPlatform.FEATURES + []  # TODO: validate?
+    FEATURES = TargetPlatform.FEATURES + ["benchmark"]  # TODO: validate?
 
     DEFAULTS = {
         **TargetPlatform.DEFAULTS,
@@ -51,6 +51,9 @@ class TvmPlatform(TargetPlatform):
         "print_top": False,
         "profile": False,
         "repeat": 1,
+        "number": 1,
+        "aggregate": "none",  # Allowed: avg, max, min, none, all
+        "total_time": False,
         "use_rpc": False,
         "rpc_key": None,
         "rpc_hostname": None,
@@ -71,6 +74,12 @@ def __init__(self, features=None, config=None):
         self.project_name = "app"
         self.project_dir = None
 
+    def create_backend(self, name):
+        supported = self.get_supported_backends()
+        assert name in supported, f"{name} is not a valid TVM platform backend"
+        base = supported[name]
+        return create_tvm_platform_backend(name, self, base=base)
+
     @property
     def fill_mode(self):
         return self.config["fill_mode"]
@@ -96,6 +105,21 @@ def profile(self):
     def repeat(self):
         return self.config["repeat"]
 
+    @property
+    def number(self):
+        return self.config["number"]
+
+    @property
+    def aggregate(self):
+        value = self.config["aggregate"]
+        assert value in ["avg", "all", "max", "min", "none"]
+        return value
+
+    @property
+    def total_time(self):
+        value = self.config["total_time"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
     @property
     def use_rpc(self):
         value = self.config["use_rpc"]
@@ -159,6 +183,10 @@ def init_directory(self, path=None, context=None):
                 logger.debug("Temporary project directory: %s", self.project_dir)
         self.project_dir.mkdir(exist_ok=True)
 
+    def get_supported_backends(self):
+        backend_names = get_tvm_platform_backends()
+        return backend_names
+
     def get_supported_targets(self):
         # TODO: get this via tvmc run --help
         target_names = ["cpu", "cuda", "cl", "metal", "vulkan", "rocm", "micro"]
@@ -173,13 +201,13 @@ def create_target(self, name):
             base = SUPPORTED_TARGETS[name]
         else:
             base = Target
-        return create_tvm_target(name, self, base=base)
+        return create_tvm_platform_target(name, self, base=base)
 
     def close(self):
         if self.tempdir:
             self.tempdir.cleanup()
 
-    def get_tvmc_run_args(self, path, device, num=1):
+    def get_tvmc_run_args(self, path, device):
         return [
             path,
             *["--device", device],
@@ -187,7 +215,7 @@ def get_tvmc_run_args(self, path, device, num=1):
                 mode=self.fill_mode, ins_file=self.ins_file, outs_file=self.outs_file, print_top=self.print_top
             ),
             *get_bench_tvmc_args(
-                print_time=True, profile=self.profile, end_to_end=False, repeat=self.repeat, number=num
+                print_time=True, profile=self.profile, end_to_end=False, repeat=self.repeat, number=self.number
             ),
             *get_rpc_tvmc_args(self.use_rpc, self.rpc_key, self.rpc_hostname, self.rpc_port),
         ]
@@ -200,14 +228,15 @@ def invoke_tvmc(self, command, *args):
             pre = [self.tvmc_custom_script]
         return utils.python(*pre, command, *args, live=self.print_outputs, print_output=False, env=env)
 
-    def invoke_tvmc_run(self, path, device, num=1):
-        args = self.get_tvmc_run_args(path, device, num=num)
+    def invoke_tvmc_run(self, path, device):
+        args = self.get_tvmc_run_args(path, device)
         return self.invoke_tvmc("run", *args)
 
-    def run(self, elf, target, timeout=120, num=1):
+    def run(self, elf, target, timeout=120):
         # TODO: implement timeout
         # Here, elf is actually a directory
         # TODO: replace workaround with possibility to pass TAR directly
         tar_path = elf
-        output = self.invoke_tvmc_run(str(tar_path), target.device, num=num)
+        output = self.invoke_tvmc_run(str(tar_path), target.device)
+
         return output
diff --git a/mlonmcu/platform/tvm_backend.py b/mlonmcu/platform/tvm_backend.py
new file mode 100644
index 000000000..87e569f25
--- /dev/null
+++ b/mlonmcu/platform/tvm_backend.py
@@ -0,0 +1,58 @@
+#
+# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+#
+# This file is part of MLonMCU.
+# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from mlonmcu.logging import get_logger
+
+from mlonmcu.flow.tvm.backend.backend import TVMBackend
+from mlonmcu.flow.tvm.backend.tvmllvm import TVMLLVMBackend
+
+logger = get_logger()
+
+TVM_PLATFORM_BACKEND_REGISTRY = {}
+
+
+def register_tvm_platform_backend(backend_name, b, override=False):
+    global TVM_PLATFORM_BACKEND_REGISTRY
+
+    if backend_name in TVM_PLATFORM_BACKEND_REGISTRY and not override:
+        raise RuntimeError(f"TVM platform backend  {backend_name} is already registered")
+    TVM_PLATFORM_BACKEND_REGISTRY[backend_name] = b
+
+
+def get_tvm_platform_backends():
+    return TVM_PLATFORM_BACKEND_REGISTRY
+
+
+register_tvm_platform_backend("tvmllvm", TVMLLVMBackend)
+
+
+def create_tvm_platform_backend(name, platform, base=TVMBackend):
+    class TvmPlatformBackend(base):
+
+        FEATURES = base.FEATURES + []
+
+        DEFAULTS = {
+            **base.DEFAULTS,
+        }
+        REQUIRED = base.REQUIRED + []
+
+        def __init__(self, features=None, config=None):
+            super().__init__(runtime="cpp", fmt="so", features=features, config=config)
+            self.platform = platform
+
+    return TvmPlatformBackend
diff --git a/mlonmcu/platform/tvm_target.py b/mlonmcu/platform/tvm_target.py
index cb3a9505b..e44d41377 100644
--- a/mlonmcu/platform/tvm_target.py
+++ b/mlonmcu/platform/tvm_target.py
@@ -31,8 +31,8 @@ def name2device(name):
     return name.replace("tvm_", "")
 
 
-def create_tvm_target(name, platform, base=Target):
-    class TvmTarget(base):
+def create_tvm_platform_target(name, platform, base=Target):
+    class TvmPlatformTarget(base):
 
         FEATURES = base.FEATURES + []
 
@@ -51,7 +51,11 @@ def __init__(self, features=None, config=None):
         def timeout_sec(self):
             return int(self.config["timeout_sec"])
 
-        def exec(self, program, *args, num=1, cwd=os.getcwd(), **kwargs):
+        @property
+        def repeat(self):
+            return None  # This is handled at the platform level
+
+        def exec(self, program, *args, cwd=os.getcwd(), **kwargs):
             """Use target to execute a executable with given arguments"""
             if len(args) > 0:
                 raise RuntimeError("Program arguments are not supported for real hardware devices")
@@ -61,7 +65,7 @@ def exec(self, program, *args, num=1, cwd=os.getcwd(), **kwargs):
             if self.timeout_sec > 0:
                 raise NotImplementedError
 
-            ret = self.platform.run(program, self, num=num)
+            ret = self.platform.run(program, self)
             return ret
 
         def parse_stdout(self, out):
@@ -90,9 +94,9 @@ def parse_stdout(self, out):
                     found = True
             return mean_ms, median_ms, max_ms, min_ms, std_ms
 
-        def get_metrics(self, elf, directory, handle_exit=None, num=1):
+        def get_metrics(self, elf, directory, handle_exit=None):
             if self.print_outputs:
-                out = self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit, num=num)
+                out = self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit)
             else:
                 out = self.exec(
                     elf,
@@ -100,18 +104,36 @@ def get_metrics(self, elf, directory, handle_exit=None, num=1):
                     live=False,
                     print_func=lambda *args, **kwargs: None,
                     handle_exit=handle_exit,
-                    num=num,
                 )
-            mean_ms, _, _, _, _ = self.parse_stdout(out)
+            mean_ms, _, max_ms, min_ms, _ = self.parse_stdout(out)
 
             metrics = Metrics()
-            # metrics.add("Total Cycles", cycles)
-            time_s = mean_ms / 1e3 if mean_ms is not None else mean_ms
-            metrics.add("Mean Runtime [s]", time_s)
+            mean_s = mean_ms / 1e3 if mean_ms is not None else mean_ms
+            min_s = min_ms / 1e3 if min_ms is not None else min_ms
+            max_s = max_ms / 1e3 if max_ms is not None else max_ms
+            if (
+                self.platform.number == 1
+                and self.platform.repeat == 1
+                and (not self.platform.total_time or self.platform.aggregate != "none")
+            ):
+                metrics.add("Runtime [s]", mean_s)
+            else:
+                if self.platform.total_time:
+                    metrics.add("Total Runtime [s]", mean_s * self.platform.number)
+                if self.platform.aggregate == "all":
+                    metrics.add("Average Runtime [s]", mean_s)
+                    metrics.add("Min Runtime [s]", min_s)
+                    metrics.add("Max Runtime [s]", max_s)
+                elif self.platform.aggregate in ["avg", "mean"]:
+                    metrics.add("Average Runtime [s]", mean_s)
+                elif self.platform.aggregate == "min":
+                    metrics.add("Min Runtime [s]", min_s)
+                elif self.platform.aggregate == "max":
+                    metrics.add("Max Runtime [s]", max_s)
 
             return metrics, out, []
 
         def get_arch(self):
             return "unkwown"
 
-    return TvmTarget
+    return TvmPlatformTarget
diff --git a/mlonmcu/platform/zephyr.py b/mlonmcu/platform/zephyr.py
new file mode 100644
index 000000000..768e754a3
--- /dev/null
+++ b/mlonmcu/platform/zephyr.py
@@ -0,0 +1,446 @@
+#
+# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+#
+# This file is part of MLonMCU.
+# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Zephyr Platform"""
+
+import re
+import os
+import time
+import shutil
+import tempfile
+from pathlib import Path
+import pkg_resources
+
+import serial
+
+from mlonmcu.setup import utils
+from mlonmcu.artifact import Artifact, ArtifactFormat
+from mlonmcu.logging import get_logger
+from mlonmcu.target import SUPPORTED_TARGETS
+from mlonmcu.target.target import Target
+from mlonmcu.config import str2bool
+
+from .platform import CompilePlatform, TargetPlatform
+from .zephyr_target import create_zephyr_platform_target
+
+logger = get_logger()
+
+
+def get_project_template(name="project2"):  # Workaround which only support tvmaot!!!
+    zephyr_templates = pkg_resources.resource_listdir("mlonmcu", os.path.join("..", "resources", "platforms", "zephyr"))
+    if name not in zephyr_templates:
+        return None
+    fname = pkg_resources.resource_filename("mlonmcu", os.path.join("..", "resources", "platforms", "zephyr", name))
+    return fname
+
+
+class ZephyrPlatform(CompilePlatform, TargetPlatform):
+    """Zephyr Platform class."""
+
+    FEATURES = CompilePlatform.FEATURES + TargetPlatform.FEATURES + ["benchmark"]
+
+    DEFAULTS = {
+        **CompilePlatform.DEFAULTS,
+        **TargetPlatform.DEFAULTS,
+        "project_template": None,
+        "project_dir": None,
+        "port": None,
+        # "port": "/dev/ttyUSB0",
+        "baud": 115200,
+        "wait_for_user": True,
+        "flash_only": False,
+        "optimize": None,  # values: 0,1,2,3,s
+    }
+
+    REQUIRED = ["zephyr.install_dir", "zephyr.sdk_dir", "zephyr.venv_dir"]
+
+    def __init__(self, features=None, config=None):
+        super().__init__(
+            "zephyr",
+            features=features,
+            config=config,
+        )
+        self.tempdir = None
+        self.project_name = "app"
+        self.project_dir = None
+
+    @property
+    def zephyr_install_dir(self):
+        return Path(self.config["zephyr.install_dir"])
+
+    @property
+    def zephyr_sdk_dir(self):
+        return Path(self.config["zephyr.sdk_dir"])
+
+    @property
+    def zephyr_venv_dir(self):
+        return Path(self.config["zephyr.venv_dir"])
+
+    @property
+    def wait_for_user(self):
+        value = self.config["wait_for_user"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def flash_only(self):
+        # TODO: get rid of this
+        value = self.config["flash_only"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def optimize(self):
+        return self.config["optimize"]
+
+    def invoke_west(self, *args, **kwargs):
+        env = os.environ.copy()
+        env["ZEPHYR_BASE"] = str(self.zephyr_install_dir / "zephyr")
+        env["ZEPHYR_SDK_INSTALL_DIR"] = str(self.zephyr_sdk_dir)
+        cmd = ". " + str(self.zephyr_venv_dir / "bin" / "activate") + " && west " + " ".join([str(arg) for arg in args])
+        out = utils.exec_getout(
+            cmd, shell=True, env=env, **kwargs, executable="/bin/bash"
+        )  # TODO: using shell=True is insecure but right now we can not avoid it?
+        return out
+
+    def init_directory(self, path=None, context=None):
+        if self.project_dir is not None:
+            self.project_dir.mkdir(exist_ok=True)
+            logger.debug("Project directory already initialized")
+            return
+        dir_name = self.name
+        if path is not None:
+            self.project_dir = Path(path)
+        elif self.config["project_dir"] is not None:
+            self.project_dir = Path(self.config["project_dir"])
+        else:
+            if context:
+                assert "temp" in context.environment.paths
+                self.project_dir = (
+                    context.environment.paths["temp"].path / dir_name
+                )  # TODO: Need to lock this for parallel builds
+            else:
+                logger.debug(
+                    "Creating temporary directory because no context was available "
+                    "and 'zephyr.project_dir' was not supplied"
+                )
+                self.tempdir = tempfile.TemporaryDirectory()
+                self.project_dir = Path(self.tempdir.name) / dir_name
+                logger.debug("Temporary project directory: %s", self.project_dir)
+        self.project_dir.mkdir(exist_ok=True)
+
+    def get_supported_targets(self):
+        with tempfile.TemporaryDirectory() as temp:
+            f = Path(temp) / "CMakeLists.txt"
+            # f.touch()
+            with open(f, "w") as handle:
+                handle.write(
+                    """
+cmake_minimum_required(VERSION 3.22)
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(ProjectName)
+                """
+                )
+            b = Path(temp) / "build"
+
+            # This will fail
+            # Workaround used to get list of supported boards...
+            def _handle(code):
+                return 0
+
+            text = self.invoke_west(
+                "build", "-d", b, "-b", "help", temp, live=False, print_output=False, handle_exit=_handle
+            )
+        # Warning: This will fail if a python executable is NOT available in the system. Aliasing
+        # python3 to python will not work. Not sure how this would handle a system which only has python2 installed?
+        target_names = re.compile(r"^  (\S+)$", re.MULTILINE).findall(text)
+
+        return [
+            f"zephyr_{name}" for name in target_names if len(name) > 0 and " " not in name
+        ]  # TODO: consider dropping zephyr_ prefix
+
+    def create_target(self, name):
+        assert name in self.get_supported_targets(), f"{name} is not a valid Zephyr target"
+        if name in SUPPORTED_TARGETS:
+            base = SUPPORTED_TARGETS[name]
+        else:
+            base = Target
+        return create_zephyr_platform_target(name, self, base=base)
+
+    @property
+    def project_template(self):
+        return self.config["project_template"]
+
+    @property
+    def port(self):
+        return self.config["port"]
+
+    @property
+    def baud(self):
+        return self.config["baud"]
+
+    @property
+    def build_dir(self):
+        return self.project_dir / "build"
+
+    def close(self):
+        if self.tempdir:
+            self.tempdir.cleanup()
+
+    def get_west_cmake_args(self):
+        cmake_defs = {"CMAKE_BUILD_TYPE": "Debug" if self.debug else "Release"}
+        return [f"-D{key}={value}" for key, value in cmake_defs.items()]
+
+    def prepare(self, target, src):
+        self.init_directory()
+        template_dir = self.project_template
+        if template_dir is None:
+            template_dir = get_project_template()
+        else:
+            template_dir = Path(template_dir)
+            if not template_dir.is_dir():
+                template_dir = get_project_template(name=str(template_dir))
+        assert template_dir is not None, f"Provided project template does not exists: {template_dir}"
+        shutil.copytree(template_dir, self.project_dir, dirs_exist_ok=True)
+
+        def write_defaults(filename):
+            defs = self.definitions
+            with open(filename, "w", encoding="utf-8") as f:
+                f.write("CONFIG_CPLUSPLUS=y\n")
+                f.write("CONFIG_NEWLIB_LIBC=y\n")
+                f.write("CONFIG_REBOOT=y\n")
+                f.write("CONFIG_STD_CPP20=y\n")
+                f.write("CONFIG_LIB_CPLUSPLUS=y\n")
+                f.write("CONFIG_TIMING_FUNCTIONS=y\n")
+                # f.write("CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y\n")
+                # f.write("CONFIG_CBPRINTF_FP_SUPPORT=y\n")
+                if self.debug:
+                    f.write("CONFIG_DEBUG=y\n")
+                    if not self.optimize:
+                        f.write("CONFIG_DEBUG_OPTIMIZATIONS=y\n")
+                else:
+                    f.write("CONFIG_DEBUG=n\n")
+                    if self.optimize:
+                        if str(self.optimize) == "s":
+                            f.write("CONFIG_SIZE_OPTIMIZATIONS=y\n")
+                        elif str(self.optimize) == "0":
+                            f.write("CONFIG_NO_OPTIMIZATIONS=y\n")
+                        elif str(self.optimize) == "g":
+                            f.write("CONFIG_DEBUG_OPTIMIZATIONS=y\n")
+                        elif str(self.optimize) == "2":
+                            f.write("CONFIG_SPEED_OPTIMIZATIONS=y\n")
+                        elif str(self.optimize) == "3":
+                            f.write("CONFIG_SPEED_OPTIMIZATIONS=y\n")
+                            f.write('CONFIG_COMPILER_OPT="-O3"\n')
+                        else:
+                            raise RuntimeError(f"Unsupported optimization level for Zephyr platform: {self.optimize}")
+                # f.write("CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y\n")
+                # if self.debug:
+                #     f.write("CONFIG_OPTIMIZATION_LEVEL_DEBUG=y\n")
+                #     f.write("CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y\n")
+                # else:
+                #     # Trying to reduce the binary size as much as possible
+                #     # (https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/performance/size.html)
+                #     f.write("CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y\n")
+                #     f.write("CONFIG_BOOTLOADER_LOG_LEVEL=0\n")
+                #     f.write("CONFIG_BOOT_ROM_LOG_ALWAYS_OFF=y\n")
+                #     f.write("CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y\n")
+                #     f.write("CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=0\n")
+                #     f.write("CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT=y\n")
+                #     f.write("CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=0\n")
+                #     f.write("CONFIG_LOG_DEFAULT_LEVEL_NONE=y\n")
+                #     f.write("CONFIG_LOG_DEFAULT_LEVEL=0\n")
+                #     f.write("CONFIG_LOG_MAXIMUM_LEVEL=0\n")
+                #     f.write("CONFIG_NEWLIB_NANO_FORMAT=y\n")
+                #     f.write("CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE=y\n")
+                #     f.write("CONFIG_OPTIMIZATION_LEVEL_RELEASE=y\n")
+                #     optimize_for_size = True
+                #     if optimize_for_size:
+                #         f.write("CONFIG_COMPILER_OPTIMIZATION_SIZE=y\n")
+                #     else:
+                #         f.write("CONFIG_COMPILER_OPTIMIZATION_PERF=y\n")
+                # watchdog_sec = 60
+                # f.write(f"CONFIG_ESP_TASK_WDT_TIMEOUT_S={watchdog_sec}\n")
+                f.write(f'CONFIG_MLONMCU_CODEGEN_DIR="{src}"\n')
+                for key, value in defs.items():
+                    if isinstance(value, bool):
+                        value = "y" if value else "n"
+                    else:
+                        value = f'"{value}"'
+                    f.write(f"CONFIG_{key}={value}\n")
+
+        write_defaults(self.project_dir / "prj.conf")
+        zephyr_target = target.name.split("_", 1)[-1]
+        westArgs = [  # cmake only
+            "build",
+            "-d",
+            self.build_dir,
+            "-b",
+            zephyr_target,
+            self.project_dir,
+            *self.get_west_cmake_args(),
+            "-c",
+        ]
+        return self.invoke_west(*westArgs, live=self.print_outputs)
+
+    def compile(self, target, src=None):
+        out = self.prepare(target, src)
+        # TODO: build with cmake options
+        # TODO: support self.num_threads
+        # self.build_dir.mkdir()
+        zephyr_target = target.name.split("_", 1)[-1]
+        westArgs = [
+            "build",
+            "-d",
+            self.build_dir,
+            "-b",
+            zephyr_target,
+            self.project_dir,
+            f"-o=-j{self.num_threads}",
+        ]
+        out += self.invoke_west(*westArgs, live=self.print_outputs)
+        return out
+
+    def generate_elf(self, src, target, model=None, data_file=None):
+        artifacts = []
+        out = self.compile(target, src=src)
+        elf_name = "zephyr.elf"
+        elf_file = self.project_dir / "build" / "zephyr" / elf_name
+        # TODO: just use path instead of raw data?
+        if self.tempdir:
+            # Warning: The porject context will get destroyed afterwards wehen using  a temporory directory
+            with open(elf_file, "rb") as handle:
+                data = handle.read()
+                artifact = Artifact("generic_mlif", raw=data, fmt=ArtifactFormat.RAW)
+                artifacts.append(artifact)
+        else:
+            artifact = Artifact(elf_name, path=elf_file, fmt=ArtifactFormat.PATH)
+            artifacts.append(artifact)
+        metrics = self.get_metrics(elf_file)
+        content = metrics.to_csv(include_optional=True)  # TODO: store df instead?
+        metrics_artifact = Artifact("metrics.csv", content=content, fmt=ArtifactFormat.TEXT)
+        artifacts.append(metrics_artifact)
+        stdout_artifact = Artifact(
+            "zephyr_out.log", content=out, fmt=ArtifactFormat.TEXT  # TODO: split into one file per command
+        )  # TODO: rename to tvmaot_out.log?
+        artifacts.append(stdout_artifact)
+        self.artifacts = artifacts
+
+    def get_serial(self, target):
+        port = target.port
+        if port is None:
+            # Falling back to 'espidf.baud'
+            assert self.port is not None, f"Please provide '{target.name}.port'"
+            port = self.port
+        baud = target.baud
+        if baud is None:
+            # Falling back to 'espidf.baud'
+            assert self.baud is not None, f"Please provide '{target.name}.baud'"
+            baud = self.baud
+        return port, baud
+
+    def flash(self, elf, target, timeout=120):
+        # Ignore elf, as we use self.project_dir instead
+        # TODO: add alternative approach which allows passing elf instead
+        if elf is not None:
+            logger.debug("Ignoring ELF file for zephyr platform")
+        # TODO: implement timeout
+        # TODO: make sure that already compiled? -> error or just call compile routine?
+        if self.wait_for_user:  # INTERACTIVE
+            answer = input(
+                f"Make sure that the device '{target.name}' is connected before you press [Enter]"
+                + " (Type 'Abort' to cancel)"
+            )
+            if answer.lower() == "abort":
+                return ""
+        logger.debug("Flashing target software")
+
+        westArgs = [
+            "flash",
+            "-d",
+            self.build_dir,
+        ]
+        if "esp32" in target.name:  # TODO: implement this for other runners as well
+            port, baud = self.get_serial(target)
+            if port:
+                westArgs.extend(["--esp-device", port])
+            if baud:
+                westArgs.extend(["--esp-baud-rate", baud])
+        self.invoke_west(*westArgs, live=self.print_outputs)
+
+    def monitor(self, target, timeout=60):
+        if self.flash_only:
+            return ""
+
+        port, baud = self.get_serial(target)
+
+        def _monitor_helper(port, baud, verbose=False, start_match=None, end_match=None, timeout=60):
+            # start_match and end_match are inclusive
+            found_start = start_match is None
+            outStr = ""
+            if timeout:
+                pass  # TODO: implement timeout
+            # The following is a custom initialization sequence inspired by
+            # (https://github.com/espressif/esp-idf/blob/master/tools/idf_monitor_base/serial_reader.py)
+            # Required for esp32c3!
+            high = False
+            low = True
+            ser = serial.Serial(port, baud)
+            ser.close()
+            ser.dtr = False
+            ser.rts = False
+            time.sleep(1)
+            ser.dtr = low
+            ser.rts = high
+            ser.dtr = ser.dtr
+            ser.open()
+            ser.dtr = high
+            ser.rts = low
+            ser.dtr = ser.dtr
+            time.sleep(0.002)
+            ser.rts = high
+            ser.dtr = ser.dtr
+            try:
+                while True:
+                    try:
+                        ser_bytes = ser.readline()
+                        new_line = ser_bytes.decode("utf-8", errors="replace")
+                        if verbose:
+                            print(new_line.replace("\n", ""))
+                        if start_match and start_match in new_line:
+                            outStr = new_line
+                            found_start = True
+                        else:
+                            outStr = outStr + new_line
+                        if found_start:
+                            if end_match and end_match in new_line:
+                                break
+                    except KeyboardInterrupt:
+                        logger.warning("Stopped processing serial port (KeyboardInterrupt)")
+            finally:
+                ser.close()
+            return outStr
+
+        logger.debug("Monitoring target software")
+        # TODO: implement timeout
+        return _monitor_helper(
+            port,
+            baud,
+            verbose=self.print_outputs,
+            start_match="MLonMCU: START",
+            end_match="MLonMCU: STOP",
+            timeout=timeout,
+        )
diff --git a/mlonmcu/platform/zephyr_target.py b/mlonmcu/platform/zephyr_target.py
new file mode 100644
index 000000000..bfb1dad2a
--- /dev/null
+++ b/mlonmcu/platform/zephyr_target.py
@@ -0,0 +1,111 @@
+#
+# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+#
+# This file is part of MLonMCU.
+# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import re
+import os
+
+from mlonmcu.target.target import Target
+from mlonmcu.target.metrics import Metrics
+
+from mlonmcu.logging import get_logger
+
+logger = get_logger()
+
+
+def create_zephyr_platform_target(name, platform, base=Target):
+    class ZephyrPlatformTarget(base):
+
+        FEATURES = base.FEATURES + []
+
+        DEFAULTS = {
+            **base.DEFAULTS,
+            "timeout_sec": 0,  # disabled
+            "port": None,
+            "baud": None,
+        }
+        REQUIRED = base.REQUIRED + []
+
+        def __init__(self, features=None, config=None):
+            super().__init__(name=name, features=features, config=config)
+            self.platform = platform
+
+        @property
+        def timeout_sec(self):
+            return int(self.config["timeout_sec"])
+
+        @property
+        def port(self):
+            return self.config["port"]
+
+        @property
+        def baud(self):
+            return self.config["baud"]
+
+        def exec(self, program, *args, cwd=os.getcwd(), **kwargs):
+            """Use target to execute a executable with given arguments"""
+            if len(args) > 0:
+                raise RuntimeError("Program arguments are not supported for real hardware devices")
+
+            assert self.platform is not None, "Zephyr targets need a platform to execute programs"
+
+            if self.timeout_sec > 0:
+                raise NotImplementedError
+
+            # Zephyr actually wants a project directory, but we only get the elf now. As a workaround we
+            # assume the elf is right in the build directory inside the project directory
+
+            ret = self.platform.run(program, self)
+            return ret
+
+        def parse_stdout(self, out):
+            # cpu_cycles = re.search(r"Total Cycles: (.*)", out)
+            # if not cpu_cycles:
+            #     logger.warning("unexpected script output (cycles)")
+            #     cycles = None
+            # else:
+            #     cycles = int(float(cpu_cycles.group(1)))
+            cpu_time_us = re.search(r"Total Time: (.*) us", out)
+            if not cpu_time_us:
+                logger.warning("unexpected script output (time_us)")
+                time_us = None
+            else:
+                time_us = int(float(cpu_time_us.group(1)))
+            # return cycles, time_us
+            return time_us
+
+        def get_metrics(self, elf, directory, handle_exit=None):
+            if self.print_outputs:
+                out = self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit)
+            else:
+                out = self.exec(
+                    elf, cwd=directory, live=False, print_func=lambda *args, **kwargs: None, handle_exit=handle_exit
+                )
+            # cycles, time_us = self.parse_stdout(out)
+            time_us = self.parse_stdout(out)
+
+            metrics = Metrics()
+            # metrics.add("Cycles", cycles)
+            time_s = time_us / 1e6 if time_us is not None else time_us
+            metrics.add("Runtime [s]", time_s)
+
+            return metrics, out, []
+
+        def get_arch(self):
+            return "unkwown"
+
+    return ZephyrPlatformTarget
diff --git a/mlonmcu/session/postprocess/__init__.py b/mlonmcu/session/postprocess/__init__.py
index c2a5a1c22..f314c5286 100644
--- a/mlonmcu/session/postprocess/__init__.py
+++ b/mlonmcu/session/postprocess/__init__.py
@@ -19,8 +19,6 @@
 """MLonMCU postprocess submodule"""
 
 from .postprocesses import (
-    AverageCyclesPostprocess,
-    DetailedCyclesPostprocess,
     FilterColumnsPostprocess,
     RenameColumnsPostprocess,
     Features2ColumnsPostprocess,
@@ -32,8 +30,6 @@
 )
 
 SUPPORTED_POSTPROCESSES = {
-    "average_cycles": AverageCyclesPostprocess,
-    "detailed_cycles": DetailedCyclesPostprocess,
     "filter_cols": FilterColumnsPostprocess,
     "rename_cols": RenameColumnsPostprocess,
     "features2cols": Features2ColumnsPostprocess,
diff --git a/mlonmcu/session/postprocess/postprocess.py b/mlonmcu/session/postprocess/postprocess.py
index e59d2ed4f..8ad50a9a3 100644
--- a/mlonmcu/session/postprocess/postprocess.py
+++ b/mlonmcu/session/postprocess/postprocess.py
@@ -28,12 +28,13 @@ class Postprocess:
     DEFAULTS = {}
 
     REQUIRED = []
+    OPTIONAL = []
 
     def __init__(self, name, config=None, features=None):
         self.name = name
         self.config = config if config else {}
         self.features = self.process_features(features)
-        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.REQUIRED)
+        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
 
     def process_features(self, features):
         """Utility which handles postprocess_features."""
diff --git a/mlonmcu/session/postprocess/postprocesses.py b/mlonmcu/session/postprocess/postprocesses.py
index b0150c788..faf1eeea0 100644
--- a/mlonmcu/session/postprocess/postprocesses.py
+++ b/mlonmcu/session/postprocess/postprocesses.py
@@ -40,114 +40,6 @@ def match_rows(df, cols):
     return groups
 
 
-class AverageCyclesPostprocess(SessionPostprocess, RunPostprocess):
-    """Postprocess which averages the cycle counts of multiple runs into a single row."""
-
-    DEFAULTS = {
-        **SessionPostprocess.DEFAULTS,
-        "merge_rows": False,
-    }
-
-    def __init__(self, features=None, config=None):
-        super().__init__("average_cycles", features=features, config=config)
-
-    @property
-    def merge_rows(self):
-        """Get merge_rows property."""
-        return bool(self.config["merge_rows"])
-
-    def post_run(self, report, artifacts):
-        """Called at the end of a run."""
-        if not self.merge_rows:
-            if "Total Cycles" not in report.main_df or "Num" not in report.pre_df:
-                return
-            report.main_df["Average Cycles"] = report.main_df["Total Cycles"] / report.pre_df["Num"]
-
-    def post_session(self, report):
-        """Called at the end of a session."""
-        if self.merge_rows:
-            if "Total Cycles" not in report.main_df or "Num" not in report.pre_df:
-                return
-            ignore_cols = ["Session", "Run", "Num", "Comment"]
-            combined_df = pd.concat([report.pre_df, report.post_df], axis=1)
-            use_cols = combined_df.columns
-            use_cols = list(filter(lambda elem: elem not in ignore_cols, use_cols))
-            groups = match_rows(combined_df, use_cols)
-            to_drop = []
-            for group in groups:
-                total_num = report.pre_df["Num"][list(group)].sum()
-                total_cycles = report.main_df["Total Cycles"][list(group)].sum()
-                avg_cycles = total_cycles / total_num
-                report.pre_df["Num"][[group[0]]] = total_num
-                report.main_df.loc[group[0], "Average Cycles"] = avg_cycles
-                to_drop.extend(list(group[1:]))
-            report.pre_df.drop(to_drop, inplace=True)
-            report.main_df.drop(to_drop, inplace=True)
-            report.post_df.drop(to_drop, inplace=True)
-
-
-def get_detailed_cycles(low_num, low_cycles, high_num, high_cycles):
-    """Helper function to split the total cycles of two runs into the setup and incoke cycles."""
-    assert high_cycles > low_cycles
-    assert high_num > low_num
-    diff_cycles = high_cycles - low_cycles
-    diff_num = high_num - low_num
-    invoke_cycles = int(float(diff_cycles) / (diff_num))
-    setup_cycles = low_cycles - (invoke_cycles * low_num)
-    return setup_cycles, invoke_cycles
-
-
-class DetailedCyclesPostprocess(SessionPostprocess):
-    """Postprocess automatically determines the actual setup and inference cycles if enough information is available.
-
-    Condition: there exists at least 2 runs which only differ in their `Num` setting.
-    """
-
-    DEFAULTS = {
-        **SessionPostprocess.DEFAULTS,
-        "warn": False,
-    }
-
-    def __init__(self, features=None, config=None):
-        super().__init__("detailed_cycles", features=features, config=config)
-
-    @property
-    def warn(self):
-        """Get warn property."""
-        return bool(self.config["warn"])
-
-    def post_session(self, report):
-        """Called at the end of a session."""
-        if "Total Cycles" not in report.main_df or "Num" not in report.pre_df:
-            if self.warn:
-                logger.warning("Postprocess %s was not applied because of missing columns", self.name)
-            return
-        ignore_cols = ["Session", "Run", "Num", "Comment"]
-        combined_df = pd.concat([report.pre_df, report.post_df], axis=1)
-        use_cols = combined_df.columns
-        use_cols = list(filter(lambda elem: elem not in ignore_cols, use_cols))
-        groups = match_rows(combined_df, use_cols)
-        to_drop = []
-        for group in groups:
-            if len(group) == 1:
-                if self.warn:
-                    logger.warning("Unable to find a suitable pair for extracting detailed cycle counts")
-            max_idx = report.pre_df["Num"][list(group)].idxmax()
-            min_idx = report.pre_df["Num"][list(group)].idxmin()
-            assert max_idx != min_idx
-            max_cycles = report.main_df["Total Cycles"][max_idx]
-            min_cycles = report.main_df["Total Cycles"][min_idx]
-            max_num = report.pre_df["Num"][max_idx]
-            min_num = report.pre_df["Num"][min_idx]
-            setup_cycles, invoke_cycles = get_detailed_cycles(min_num, min_cycles, max_num, max_cycles)
-            report.main_df.loc[max_idx, "Setup Cycles"] = setup_cycles
-            report.main_df.loc[max_idx, "Invoke Cycles"] = invoke_cycles
-            to_drop.extend([idx for idx in group if idx != max_idx])
-        report.pre_df.drop(to_drop, inplace=True)
-        report.main_df.drop(to_drop, inplace=True)
-        report.post_df.drop(to_drop, inplace=True)
-
-
 class FilterColumnsPostprocess(SessionPostprocess):
     """Postprocess which can be used to drop unwanted columns from a report."""
 
@@ -359,7 +251,7 @@ def post_session(self, report):
         if self.format != "png":
             raise NotImplementedError("Currently only supports PNG")
 
-        COLS = ["Total Cycles", "Total ROM", "Total RAM"]
+        COLS = ["Cycles", "Total ROM", "Total RAM"]
         for col in COLS:
             if col not in report.main_df.columns:
                 return []
diff --git a/mlonmcu/session/run.py b/mlonmcu/session/run.py
index 3d94c9a57..40ea4382f 100644
--- a/mlonmcu/session/run.py
+++ b/mlonmcu/session/run.py
@@ -26,7 +26,7 @@
 
 from mlonmcu.logging import get_logger
 from mlonmcu.artifact import ArtifactFormat, lookup_artifacts
-from mlonmcu.platform.platform import CompilePlatform, TargetPlatform
+from mlonmcu.platform.platform import CompilePlatform, TargetPlatform, BuildPlatform, TunePlatform
 from mlonmcu.report import Report  # TODO: move to mlonmcu.session.report
 from mlonmcu.config import resolve_required_config, filter_config
 from mlonmcu.models.lookup import lookup_models
@@ -68,6 +68,7 @@ class Run:
     }
 
     REQUIRED = []
+    OPTIONAL = []
 
     @classmethod
     def from_file(cls, path):
@@ -86,7 +87,6 @@ def __init__(
         features=None,  # TODO: All features combined or explicit run-features -> postprocesses?
         config=None,  # TODO: All config combined or explicit run-config?
         postprocesses=None,
-        num=1,
         archived=False,
         session=None,
         comment="",
@@ -98,7 +98,6 @@ def __init__(
         self.backend = backend
         self.platforms = platforms if platforms is not None else []
         self.artifacts_per_stage = {}
-        self.num = num
         self.archived = archived
         self.session = session
         self.postprocesses = postprocesses if postprocesses else []
@@ -113,7 +112,7 @@ def __init__(
         self.features = features if features else []
         self.run_config = config if config else {}
         self.run_features = self.process_features(features)
-        self.run_config = filter_config(self.run_config, "run", self.DEFAULTS, self.REQUIRED)
+        self.run_config = filter_config(self.run_config, "run", self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
         self.result = None
         self.failing = False  # -> RunStatus
         # self.lock = threading.Lock()  # FIXME: use mutex instead of boolean
@@ -140,6 +139,30 @@ def target_to_backend(self):
         """Get target_to_backend property."""
         return bool(self.run_config["target_to_backend"])
 
+    @property
+    def build_platform(self):
+        """Get platform for build stage."""
+        if self.backend is not None and (
+            hasattr(self.backend, "platform") and isinstance(self.backend.platform, BuildPlatform)
+        ):
+            return self.backend.platform
+        for platform in self.platforms:
+            if isinstance(platform, BuildPlatform):
+                return platform
+        return None
+
+    @property
+    def tune_platform(self):
+        """Get platform for tune stage."""
+        if self.backend is not None and (
+            hasattr(self.backend, "platform") and isinstance(self.backend.platform, TunePlatform)
+        ):
+            return self.backend.platform
+        for platform in self.platforms:
+            if isinstance(platform, TunePlatform):
+                return platform
+        return None
+
     @property
     def compile_platform(self):
         """Get platform for compile stage."""
@@ -230,12 +253,15 @@ def copy(self):
     def init_component(self, component_cls, context=None):
         """Helper function to create and configure a MLonMCU component instance for this run."""
         required_keys = component_cls.REQUIRED
+        optional_keys = component_cls.OPTIONAL
         self.config.update(
             resolve_required_config(
                 required_keys,
+                optional=optional_keys,
                 features=self.features,
                 config=self.config,
                 cache=context.cache if context else None,
+                default_flags=context.environment.flags if context else None,
                 hints=self.cache_hints,
             )
         )
@@ -317,13 +343,13 @@ def add_feature(self, feature):
         """Setter for a feature instance."""
         self.features = [feature]
         self.run_features = self.process_features(self.features)
-        self.run_config = filter_config(self.run_config, "run", self.DEFAULTS, self.REQUIRED)
+        self.run_config = filter_config(self.run_config, "run", self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
 
     def add_features(self, features, append=False):
         """Setter for the list of features."""
         self.features = features if not append else self.features + features
         self.run_features = self.process_features(self.features)
-        self.run_config = filter_config(self.run_config, "run", self.DEFAULTS, self.REQUIRED)
+        self.run_config = filter_config(self.run_config, "run", self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
 
     def pick_model_frontend(self, model_hints, backend=None):
         assert len(model_hints) > 0
@@ -370,7 +396,10 @@ def add_backend_by_name(self, backend_name, context=None):
         assert context is not None and context.environment.has_backend(
             backend_name
         ), f"The backend '{backend_name}' is not enabled for this environment"
-        self.add_backend(self.init_component(SUPPORTED_BACKENDS[backend_name], context=context))
+        if self.build_platform:
+            self.add_backend(self.init_component(self.build_platform.create_backend(backend_name), context=context))
+        else:
+            self.add_backend(self.init_component(SUPPORTED_BACKENDS[backend_name], context=context))
         framework_name = self.backend.framework  # TODO: does this work?
         assert context.environment.has_framework(
             framework_name
@@ -445,8 +474,6 @@ def __repr__(self):
             probs.append(str(self.backend))
         if self.target:
             probs.append(str(self.target))
-        if self.num:
-            probs.append(str(self.num))
         if self.features and len(self.features) > 0:
             probs.append(str(self.features))
         if self.config and len(self.config) > 0:
@@ -458,6 +485,10 @@ def frontend(self):
         assert len(self.frontends) > 0, "Not frontend is available for this run."
         return self.frontends[0]
 
+    @property
+    def artifacts(self):
+        return list(itertools.chain(*self.artifacts_per_stage.values()))
+
     def toDict(self):
         """Utility not implemented yet. (TODO: remove?)"""
         raise NotImplementedError
@@ -487,11 +518,10 @@ def postprocess(self):
         # assert self.completed[RunStage.RUN]  # Alternative: allow to trigger previous stages recursively as a fallback
 
         self.artifacts_per_stage[RunStage.POSTPROCESS] = []
-        existing_artifacts = list(itertools.chain(*self.artifacts_per_stage.values()))
         temp_report = self.get_report()
         for postprocess in self.postprocesses:
             if isinstance(postprocess, RunPostprocess):
-                artifacts = postprocess.post_run(temp_report, existing_artifacts)
+                artifacts = postprocess.post_run(temp_report, self.artifacts)
                 if artifacts is not None:
                     self.artifacts_per_stage[RunStage.POSTPROCESS].extend(artifacts)
         self.report = temp_report
@@ -513,7 +543,7 @@ def run(self):
             assert self.completed[RunStage.BUILD]  # Used for tvm platform
             self.export_stage(RunStage.BUILD, optional=self.export_optional)
             shared_object_artifact = self.artifacts_per_stage[RunStage.BUILD][0]
-            self.target.generate_metrics(shared_object_artifact.path, num=self.num)
+            self.target.generate_metrics(shared_object_artifact.path)
         self.artifacts_per_stage[RunStage.RUN] = self.target.artifacts
 
         self.completed[RunStage.RUN] = True
@@ -532,7 +562,7 @@ def compile(self):
             if artifact.name == "data.c":
                 artifact.export(self.dir)
                 data_file = Path(self.dir) / "data.c"
-        self.compile_platform.generate_elf(codegen_dir, self.target, num=self.num, data_file=data_file)
+        self.compile_platform.generate_elf(codegen_dir, self.target, data_file=data_file)
         self.artifacts_per_stage[RunStage.COMPILE] = self.compile_platform.artifacts
 
         self.completed[RunStage.COMPILE] = True
@@ -584,14 +614,16 @@ def tune(self):
             model_artifact.export(self.dir)
 
         # TODO: allow raw data as well as filepath in backends
-        self.backend.load_model(model=model_artifact.path)
-        self.backend.tune_model()
-        if self.backend.tuner is not None:
-            res = self.backend.tuner.get_results()
-            if res:
-                self.artifacts_per_stage[RunStage.TUNE] = res
-            else:
-                self.artifacts_per_stage[RunStage.TUNE] = []
+        if self.tune_platform:
+            res = self.tune_platform.tune_model(model_artifact.path, self.backend, self.target)
+        else:
+            self.backend.load_model(model=model_artifact.path)
+            self.backend.tune_model()
+            res = []
+            if self.backend.tuner is not None:
+                res = self.backend.tuner.get_results()
+        if res:
+            self.artifacts_per_stage[RunStage.TUNE] = res
         else:
             self.artifacts_per_stage[RunStage.TUNE] = []
 
@@ -722,8 +754,8 @@ def get_frontend_name(self):
 
     def get_platform_name(self):
         """Return platform name(s) for this run."""
-        used = list(set([self.compile_platform, self.target_platform]))  # TODO: build_platform, tune_platform
-        ret = [platform.name for platform in used]
+        used = list(set([self.tune_platform, self.build_platform, self.compile_platform, self.target_platform]))
+        ret = [platform.name for platform in used if platform is not None]
         return ret[0] if len(ret) == 1 else ret
 
     def get_all_configs(self, omit_paths=False, omit_defaults=False, omit_globals=False):
@@ -773,6 +805,8 @@ def config_helper(obj, prefix=None):
             ret.update(config_helper(self.target))
         for platform in self.platforms:
             ret.update(config_helper(platform))
+        for feature in self.features:
+            ret.update(config_helper(feature))
         for postprocess in self.postprocesses:
             ret.update(config_helper(postprocess))
         return ret
@@ -803,10 +837,10 @@ def get_report(self):
             pre["Platform"] = self.get_platform_name()
         if self.target:
             pre["Target"] = self.target.name
-        pre["Num"] = self.num
         post = {}
         post["Features"] = self.get_all_feature_names()
-        post["Config"] = self.get_all_configs(omit_paths=True, omit_defaults=True, omit_globals=True)
+        # post["Config"] = self.get_all_configs(omit_paths=True, omit_defaults=True, omit_globals=True)
+        post["Config"] = self.get_all_configs(omit_paths=True, omit_defaults=False, omit_globals=True)
         post["Postprocesses"] = self.get_all_postprocess_names()
         post["Comment"] = self.comment if len(self.comment) > 0 else "-"
         if self.failing:
diff --git a/mlonmcu/session/session.py b/mlonmcu/session/session.py
index c7da4cc87..7f32d0ed3 100644
--- a/mlonmcu/session/session.py
+++ b/mlonmcu/session/session.py
@@ -62,7 +62,7 @@ def __init__(self, label="", idx=None, archived=False, dir=None, config=None):
         )  # TODO: decide if named sessions should also get a timestamp?
         self.idx = idx
         self.config = config if config else {}
-        self.config = filter_config(self.config, "session", self.DEFAULTS, [])
+        self.config = filter_config(self.config, "session", self.DEFAULTS, [], [])
         self.status = SessionStatus.CREATED
         self.opened_at = None
         self.closed_at = None
diff --git a/mlonmcu/setup/setup.py b/mlonmcu/setup/setup.py
index 86540a5b8..5373f44c9 100644
--- a/mlonmcu/setup/setup.py
+++ b/mlonmcu/setup/setup.py
@@ -43,11 +43,12 @@ class Setup:
     }
 
     REQUIRED = []
+    OPTIONAL = []
 
     def __init__(self, features=None, config=None, context=None, tasks_factory=Tasks):
         self.config = config if config else {}
         self.features = self.process_features(features)
-        self.config = filter_config(self.config, "setup", self.DEFAULTS, self.REQUIRED)
+        self.config = filter_config(self.config, "setup", self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
         self.context = context
         self.tasks_factory = tasks_factory
         self.verbose = bool(self.config["print_outputs"])
@@ -90,13 +91,17 @@ def process_features(self, features):
             feature.add_setup_config(self.config)
         return features
 
-    def get_dependency_order(self):
+    def _get_task_graph(self):
         self.tasks_factory.reset_changes()
         task_graph = TaskGraph(
             self.tasks_factory.registry.keys(),
             self.tasks_factory.dependencies,
             self.tasks_factory.providers,
         )
+        return task_graph
+
+    def get_dependency_order(self):
+        task_graph = self._get_task_graph()
         V, E = task_graph.get_graph()
         order = task_graph.get_order()
         order_str = " -> ".join(order)
@@ -121,10 +126,17 @@ def write_cache_file(self):
         cache_file = self.context.environment.paths["deps"].path / "cache.ini"
         self.context.cache.write_to_file(cache_file)
 
+    def visualize(self, path, ordered=False):
+        task_graph = self._get_task_graph()
+        task_graph.export_dot(path)
+        logger.debug("Written task graph to file: %s" % path)
+
     def invoke_single_task(self, name, progress=False, write_cache=True, rebuild=False):
         assert name in self.tasks_factory.registry, f"Invalid task name: {name}"
         func = self.tasks_factory.registry[name]
         func(self.context, progress=progress, rebuild=rebuild, verbose=self.verbose, threads=self.num_threads)
+        if write_cache:
+            self.write_cache_file()
 
     def install_dependencies(
         self,
diff --git a/mlonmcu/setup/task.py b/mlonmcu/setup/task.py
index b0e0509d8..f26b8e135 100644
--- a/mlonmcu/setup/task.py
+++ b/mlonmcu/setup/task.py
@@ -24,6 +24,7 @@
 import time
 from typing import List, Tuple
 import networkx as nx
+from networkx.drawing.nx_agraph import write_dot
 from tqdm import tqdm
 
 from mlonmcu.logging import get_logger
@@ -126,6 +127,16 @@ def get_order(self) -> list:
         order = list(nx.topological_sort(graph))
         return order
 
+    def export_dot(self, path):
+        """Visualize the task dependency graph."""
+        nodes, edges = self.get_graph()
+        graph = nx.DiGraph(edges)
+        graph.add_nodes_from(nodes)
+        # order = list(nx.topological_sort(graph))
+        # TODO: annotate with order
+        # TODO: also export order as extra graph
+        write_dot(graph, path)
+
 
 class TaskFactory:
     """Class which is used to register all available tasks and their annotations.
diff --git a/mlonmcu/setup/tasks.py b/mlonmcu/setup/tasks.py
index a62e2cdce..8b0d8fbc0 100644
--- a/mlonmcu/setup/tasks.py
+++ b/mlonmcu/setup/tasks.py
@@ -19,6 +19,7 @@
 """Definition of tasks used to dynamically install MLonMCU dependencies"""
 
 import os
+import venv
 import multiprocessing
 from pathlib import Path
 
@@ -145,7 +146,7 @@ def _validate_build_tflite_micro_compiler(context: MlonMcuContext, params=None):
 @Tasks.provides(["tflmc.build_dir", "tflmc.exe"])
 @Tasks.param("muriscvnn", [False, True])
 @Tasks.param("cmsisnn", [False, True])
-@Tasks.param("dbg", False)
+@Tasks.param("dbg", [False, True])
 @Tasks.param("arch", ["x86"])  # TODO: compile for arm/riscv in the future
 @Tasks.validate(_validate_build_tflite_micro_compiler)
 @Tasks.register(category=TaskType.BACKEND)
@@ -158,13 +159,13 @@ def build_tflite_micro_compiler(
     dbg = params.get("dbg", False)
     arch = params.get("arch", "x86")
     flags = utils.makeFlags((True, arch), (muriscvnn, "muriscvnn"), (cmsisnn, "cmsisnn"), (dbg, "dbg"))
-    flags_ = utils.makeFlags((dbg, "dbg"))
+    # flags_ = utils.makeFlags((dbg, "dbg"))
     tflmcName = utils.makeDirName("tflmc", flags=flags)
     tflmcBuildDir = context.environment.paths["deps"].path / "build" / tflmcName
     tflmcInstallDir = context.environment.paths["deps"].path / "install" / tflmcName
     tflmcExe = tflmcInstallDir / "compiler"
-    tfSrcDir = context.cache["tf.src_dir", flags_]
-    tflmcSrcDir = context.cache["tflmc.src_dir", flags_]
+    tfSrcDir = context.cache["tf.src_dir"]
+    tflmcSrcDir = context.cache["tflmc.src_dir"]
     if rebuild or not utils.is_populated(tflmcBuildDir) or not tflmcExe.is_file():
         cmakeArgs = [
             "-DTF_SRC=" + str(tfSrcDir),
@@ -181,12 +182,14 @@ def build_tflite_micro_compiler(
             flags__ = utils.makeFlags((True, arch), (dbg, "dbg"))
             cmsisnnLib = context.cache["cmsisnn.lib", flags__]
             cmsisDir = Path(context.cache["cmsisnn.dir"])
-            cmsisIncs = [
-                str(cmsisDir),
-                str(cmsisDir / "CMSIS" / "Core" / "Include"),
-                str(cmsisDir / "CMSIS" / "NN" / "Include"),
-                str(cmsisDir / "CMSIS" / "DSP" / "Include"),
-            ]
+            cmsisIncs = r"\;".join(
+                [
+                    str(cmsisDir),
+                    str(cmsisDir / "CMSIS" / "Core" / "Include"),
+                    str(cmsisDir / "CMSIS" / "NN" / "Include"),
+                    str(cmsisDir / "CMSIS" / "DSP" / "Include"),
+                ]
+            )
             cmakeArgs.append("-DTFLM_OPTIMIZED_KERNEL=cmsis_nn")
             cmakeArgs.append(f"-DTFLM_OPTIMIZED_KERNEL_LIB={cmsisnnLib}")
             cmakeArgs.append(f"-DTFLM_OPTIMIZED_KERNEL_INCLUDE_DIR={cmsisIncs}")
@@ -212,6 +215,8 @@ def build_tflite_micro_compiler(
 
 
 def _validate_riscv_gcc(context: MlonMcuContext, params=None):
+    if not context.environment.has_toolchain("gcc"):
+        return False
     if not (
         context.environment.has_target("etiss_pulpino")
         or context.environment.has_target("spike")
@@ -313,7 +318,7 @@ def _helper(url):
 
 def _validate_llvm(context: MlonMcuContext, params=None):
     # return context.environment.has_framework("tvm") or context.environment.has_target("etiss_pulpino")
-    return context.environment.has_framework("tvm")
+    return context.environment.has_toolchain("llvm") or context.environment.has_framework("tvm")
 
 
 @Tasks.provides(["llvm.install_dir"])
@@ -632,6 +637,8 @@ def _validate_muriscvnn(context: MlonMcuContext, params=None):
         assert "muriscvnn" in context.environment.repos, "Undefined repository: 'muriscvnn'"
     if params:
         toolchain = params.get("toolchain", "gcc")
+        if not context.environment.has_toolchain(toolchain):
+            return False
         target_arch = params.get("target_arch", "riscv")
         if target_arch == "riscv":
             if params.get("vext", False):
@@ -683,7 +690,8 @@ def clone_muriscvnn(
 @Tasks.needs(["muriscvnn.src_dir", "riscv_gcc.install_dir", "riscv_gcc.name"])
 # @Tasks.optional(["riscv_gcc.install_dir", "riscv_gcc.name", "arm_gcc.install_dir"])
 @Tasks.provides(["muriscvnn.build_dir", "muriscvnn.lib"])
-@Tasks.param("dbg", [False, True])
+# @Tasks.param("dbg", [False, True])
+@Tasks.param("dbg", [False])  # disable due to bug with vext gcc
 @Tasks.param("vext", [False, True])
 @Tasks.param("pext", [False, True])
 @Tasks.param("toolchain", ["gcc"])
@@ -945,7 +953,7 @@ def clone_cmsis(
 ):
     """CMSIS repository."""
     cmsisName = utils.makeDirName("cmsis")
-    cmsisSrcDir = context.environment.paths["deps"].path / "src" / cmsisName
+    cmsisSrcDir = Path(context.environment.paths["deps"].path) / "src" / cmsisName
     # TODO: allow to skip this if cmsisnn.dir+cmsisnn.lib are provided by the user and corstone is not used
     # -> move those checks to validate?
     if rebuild or not utils.is_populated(cmsisSrcDir):
@@ -958,7 +966,8 @@ def clone_cmsis(
 @Tasks.optional(["riscv_gcc.install_dir", "riscv_gcc.name", "arm_gcc.install_dir"])
 @Tasks.provides(["cmsisnn.lib"])
 @Tasks.param("dbg", [False, True])
-@Tasks.param("target_arch", ["x86", "riscv", "arm"])
+# @Tasks.param("target_arch", ["x86", "riscv", "arm"])
+@Tasks.param("target_arch", ["x86", "riscv"])  # Arm currently broken
 @Tasks.param("mvei", [False, True])
 @Tasks.param("dsp", [False, True])
 @Tasks.validate(_validate_cmsisnn)
@@ -980,7 +989,7 @@ def build_cmsisnn(
     cmsisnnBuildDir = context.environment.paths["deps"].path / "build" / cmsisnnName
     cmsisnnInstallDir = context.environment.paths["deps"].path / "install" / cmsisnnName
     cmsisnnLib = cmsisnnInstallDir / "libcmsis-nn.a"
-    cmsisSrcDir = context.cache["cmsisnn.dir"]
+    cmsisSrcDir = Path(context.cache["cmsisnn.dir"])
     cmsisnnSrcDir = cmsisSrcDir / "CMSIS" / "NN"
     if rebuild or not utils.is_populated(cmsisnnBuildDir) or not cmsisnnLib.is_file():
         utils.mkdirs(cmsisnnBuildDir)
@@ -1032,8 +1041,12 @@ def build_cmsisnn(
     context.cache["cmsisnn.lib", flags] = cmsisnnLib
 
 
+def _validate_arm_gcc(context: MlonMcuContext, params=None):
+    return context.environment.has_toolchain("gcc")
+
+
 def _validate_corstone300(context: MlonMcuContext, params=None):
-    return context.environment.has_target("corstone300")
+    return context.environment.has_target("corstone300") and _validate_arm_gcc(context, params=params)
 
 
 @Tasks.provides(["arm_gcc.install_dir"])
@@ -1205,7 +1218,7 @@ def download_tflite_vizualize(
 
 
 def _validate_microtvm_etissvp(context: MlonMcuContext, params=None):
-    return context.environment.has_feature("microtvm_etissvp")
+    return context.environment.has_target("microtvm_etissvp")
 
 
 @Tasks.provides(["microtvm_etissvp.src_dir", "microtvm_etissvp.template"])
@@ -1222,3 +1235,79 @@ def clone_microtvm_etissvp(
         utils.clone(repo.url, srcDir, branch=repo.ref, refresh=rebuild)
     context.cache["microtvm_etissvp.src_dir"] = srcDir
     context.cache["microtvm_etissvp.template"] = srcDir / "template_project"
+
+
+def _validate_zephyr(context: MlonMcuContext, params=None):
+    return context.environment.has_platform("zephyr")
+
+
+# @Tasks.needs([])
+@Tasks.provides(["zephyr.install_dir", "zephyr.sdk_dir", "zephyr.venv_dir"])
+@Tasks.validate(_validate_zephyr)
+@Tasks.register(category=TaskType.PLATFORM)
+def install_zephyr(
+    context: MlonMcuContext, params=None, rebuild=False, verbose=False, threads=multiprocessing.cpu_count()
+):
+    """Download and install support for the Zephyr Platform."""
+    zephyrName = utils.makeDirName("zephyr")
+    zephyrInstallDir = context.environment.paths["deps"].path / "install" / zephyrName
+    zephyrInstallDir.mkdir(exist_ok=True)
+    zephyrSdkDir = zephyrInstallDir / "sdk"
+    zephyrVenvDir = zephyrInstallDir / "venv"
+    zephyrModulesDir = zephyrInstallDir / "modules"
+    user_vars = context.environment.vars
+    if "zephyr.install_dir" in user_vars:  # TODO: also check command line flags?
+        assert "zephyr.sdk_dir" in user_vars
+        assert "zephyr.venv_dir" in user_vars
+        return False
+    # boards = ["espressif"]
+    # if "zephyr.boards" in user_vars:
+    #     boards = user_vars["zephyr.boards"]
+    # if not isinstance(boards, str):
+    #     assert isinstance(boards, list)
+    #     boards = ",".join(boards)
+    if (
+        not utils.is_populated(zephyrInstallDir)
+        or not utils.is_populated(zephyrSdkDir)
+        or not utils.is_populated(zephyrVenvDir)
+        or not utils.is_populated(zephyrModulesDir)
+        or rebuild
+    ):
+        zephyrVenvScript = zephyrVenvDir / "bin" / "activate"
+        if not utils.is_populated(zephyrVenvDir):
+            venv.create(zephyrVenvDir)
+        utils.exec_getout(f". {zephyrVenvScript} && pip install west", shell=True, print_output=False, live=verbose)
+        zephyrUrl = "https://github.com/zephyrproject-rtos/zephyr"
+        if not utils.is_populated(zephyrInstallDir / "zephyr"):
+            if "zephyr.version" in user_vars:
+                zephyrVersion = user_vars["zephyr.version"]
+            else:
+                zephyrVersion = "v3.1.0"
+            utils.exec_getout(
+                f". {zephyrVenvScript} && west init -m {zephyrUrl} --mr {zephyrVersion} {zephyrInstallDir}",
+                shell=True,
+                print_output=False,
+                live=verbose,
+            )
+        extra = zephyrInstallDir / "zephyr" / "scripts" / "requirements.txt"
+        utils.exec_getout(
+            f". {zephyrVenvScript} && pip install -r {extra}", shell=True, print_output=False, live=verbose
+        )
+        env = os.environ.copy()
+        env["ZEPHYR_BASE"] = str(zephyrInstallDir / "zephyr")
+        utils.exec_getout(f". {zephyrVenvScript} && west update", shell=True, print_output=False, live=verbose, env=env)
+        if "zephyr.sdk_version" in user_vars:
+            sdkVersion = user_vars["zephyr.sdk_version"]
+        else:
+            sdkVersion = "0.15.0-rc1"
+        sdkDist = "linux-x86_64"
+        sdkUrl = f"https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v{sdkVersion}/"
+        sdkArchive = f"zephyr-sdk-{sdkVersion}_{sdkDist}.tar.gz"
+        if not utils.is_populated(zephyrSdkDir):
+            utils.download_and_extract(sdkUrl, sdkArchive, zephyrSdkDir)
+        sdkScript = zephyrSdkDir / "setup.sh"
+        # TODO: allow to limit installed toolchains
+        utils.exec_getout(sdkScript, "-t", "all", "-h", print_output=False, live=verbose)
+    context.cache["zephyr.install_dir"] = zephyrInstallDir
+    context.cache["zephyr.sdk_dir"] = zephyrSdkDir
+    context.cache["zephyr.venv_dir"] = zephyrVenvDir
diff --git a/mlonmcu/target/__init__.py b/mlonmcu/target/__init__.py
index 2e0dfc359..b5004f8a4 100644
--- a/mlonmcu/target/__init__.py
+++ b/mlonmcu/target/__init__.py
@@ -18,18 +18,12 @@
 #
 """MLonMCU target submodule"""
 
-# from .target import Target
-#
-# SUPPORTED_TARGETS = {
-#     "etiss/pulpino": Target("etiss/pulpino"),
-#     "host": Target("host"),
-# }  # TODO
-
-from .etiss_pulpino import EtissPulpinoTarget
+from .target import Target
+from .riscv import EtissPulpinoTarget, SpikeTarget, OVPSimTarget, RiscvQemuTarget
+from .arm import Corstone300Target
 from .host_x86 import HostX86Target
-from .corstone300 import Corstone300Target
-from .spike import SpikeTarget
-from .ovpsim import OVPSimTarget
+
+__all__ = ["Target"]
 
 # from .esp32 import Esp32Target
 # from .esp32c3 import Esp32c3Target
@@ -40,6 +34,7 @@
     "corstone300": Corstone300Target,
     "spike": SpikeTarget,
     "ovpsim": OVPSimTarget,
+    "riscv_qemu": RiscvQemuTarget,
     # "esp32": Esp32Target,  # TODO: in the long term we should just fetch the supported esp-idf boards at runtime
     # "esp32c3": Esp32c3Target,  # TODO: in the long term we should just fetch the supported esp-idf boards at runtime
 }
diff --git a/mlonmcu/target/arm/__init__.py b/mlonmcu/target/arm/__init__.py
new file mode 100644
index 000000000..5a8460b23
--- /dev/null
+++ b/mlonmcu/target/arm/__init__.py
@@ -0,0 +1,3 @@
+from .corstone300 import Corstone300Target
+
+__all__ = ["Corstone300Target"]
diff --git a/mlonmcu/target/corstone300.py b/mlonmcu/target/arm/corstone300.py
similarity index 78%
rename from mlonmcu/target/corstone300.py
rename to mlonmcu/target/arm/corstone300.py
index ef1fcbb2b..52a9a32dd 100644
--- a/mlonmcu/target/corstone300.py
+++ b/mlonmcu/target/arm/corstone300.py
@@ -24,9 +24,11 @@
 
 from mlonmcu.logging import get_logger
 from mlonmcu.feature.features import SUPPORTED_TVM_BACKENDS
-from .common import cli, execute
-from .target import Target
-from .metrics import Metrics
+from mlonmcu.config import str2bool
+from mlonmcu.target import Target
+from mlonmcu.target.common import cli, execute
+from mlonmcu.target.metrics import Metrics
+from .util import resolve_cpu_features
 
 logger = get_logger()
 
@@ -38,16 +40,15 @@ class Corstone300Target(Target):
 
     DEFAULTS = {
         **Target.DEFAULTS,
-        # "model": "cortex-m55",  # Options: cortex-m4, cortex-m7, cortex-m55 (Frequency is fixed at 25MHz)
-        "model": None,  # Options: cortex-m4, cortex-m7, cortex-m55 (Frequency is fixed at 25MHz)
+        "model": "cortex-m55",  # Options: cortex-m{0,0plus,1,3,4,7,23,33,35p,55} (Frequency is fixed at 25MHz)
         # Warning: FVP is still M55 based!
         "timeout_sec": 0,  # disabled
         "enable_ethosu": False,
+        "enable_fpu": True,
         "enable_mvei": False,  # unused
         "enable_dsp": False,  # unused
         "ethosu_num_macs": 256,
         "extra_args": "",
-        "enable_vext": False,
     }
     REQUIRED = [
         "corstone300.exe",
@@ -60,25 +61,27 @@ def __init__(self, name="corstone300", features=None, config=None):
 
     @property
     def model(self):
-        if self.enable_mvei:
-            assert self.config["model"] is None, "corstone300.model was overwritten by the user"
-            return "cortex-m55"
-        elif self.enable_dsp:
-            assert self.config["model"] is None, "corstone300.model was overwritten by the user"
-            return "cortex-m33"
-        return "cortex-m0"  # Default chip
+        return self.config["model"]
 
     @property
     def enable_ethosu(self):
-        return bool(self.config["enable_ethosu"])
+        value = self.config["enable_ethosu"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def enable_fpu(self):
+        value = self.config["enable_fpu"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def enable_mvei(self):
-        return bool(self.config["enable_mvei"])
+        value = self.config["enable_mvei"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def enable_dsp(self):
-        return bool(self.config["enable_dsp"])
+        value = self.config["enable_dsp"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def ethosu_num_macs(self):
@@ -121,6 +124,12 @@ def get_default_fvp_args(self):
             "cpu0.CFGDTCMSZ=15",  # ?
             "-C",
             "cpu0.CFGITCMSZ=15",  # ?
+            "-C",
+            f"cpu0.FPU={int(self.enable_fpu)}",
+            "-C",
+            "cpu0.MVE={}".format(
+                2 if self.enable_mvei and self.enable_fpu else (1 if self.enable_mvei and not self.enable_fpu else 0)
+            ),
         ]
 
     def get_ethosu_fvp_args(self):
@@ -174,8 +183,7 @@ def parse_stdout(self, out, handle_exit=None):
         # mips = None  # TODO: parse mips?
         return cycles
 
-    def get_metrics(self, elf, directory, handle_exit=None, num=None):
-        assert num is None
+    def get_metrics(self, elf, directory, handle_exit=None):
         out = ""
         if self.print_outputs:
             out += self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit)
@@ -186,7 +194,7 @@ def get_metrics(self, elf, directory, handle_exit=None, num=None):
         cycles = self.parse_stdout(out, handle_exit=handle_exit)
 
         metrics = Metrics()
-        metrics.add("Total Cycles", cycles)
+        metrics.add("Cycles", cycles)
 
         return metrics, out, []
 
@@ -194,7 +202,12 @@ def get_platform_defs(self, platform):
         ret = super().get_platform_defs(platform)
         ret["CMSIS_PATH"] = self.cmsisnn_dir
         ret["ARM_COMPILER_PREFIX"] = self.gcc_prefix
-        ret["ARM_CPU"] = self.model
+        cpu, float_abi, fpu = resolve_cpu_features(
+            self.model, enable_fp=self.enable_fpu, enable_dsp=self.enable_dsp, enable_mve=self.enable_mvei
+        )
+        ret["ARM_CPU"] = cpu
+        ret["ARM_FLOAT_ABI"] = float_abi
+        ret["ARM_FPU"] = fpu
         return ret
 
     def get_arch(self):
@@ -206,14 +219,13 @@ def get_backend_config(self, backend):
                 "target_device": "arm_cpu",
                 # "target_march": self.get_arch(),
                 "target_model": "unknown",
-                "target_mtriple": self.riscv_basename,
+                "target_mtriple": "arm-none-eabi",
                 "target_mcpu": self.model,
                 # "target_mattr": "?",
                 # "target_mabi": self.abi,
             }
             if self.enable_dsp:
-                pass
-                # ret.update({"desired_layout": "NHWC,HWOI"})  # NOt yet supported by upstream TVMC
+                ret.update({"desired_layout": "NHWC:HWOI"})  # Not yet supported by upstream TVMC
             return ret
         return {}
 
diff --git a/mlonmcu/target/arm/util.py b/mlonmcu/target/arm/util.py
new file mode 100644
index 000000000..6a7357a5a
--- /dev/null
+++ b/mlonmcu/target/arm/util.py
@@ -0,0 +1,109 @@
+#
+# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+#
+# This file is part of MLonMCU.
+# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""MLonMCU ARM Cortex-M utilities"""
+
+DSP_TARGETS = ["cortex-m4", "cortex-m7", "cortex-m33", "cortex-35p", "cortex-m55"]
+
+MVE_TARGETS = ["cortex-m55"]
+
+FP_TARGETS = ["cortex-m4", "cortex-m7", "cortex-m33", "cortex-m35p", "cortex-m55"]
+FP_DP_TARGETS = ["cortex-m7", "cortex-m55"]
+
+ALLOWED_ATTRS = {
+    "cortex-m0": [],
+    "cortex-m0plus": [],
+    "cortex-m1": [],
+    "cortex-m3": [],
+    "cortex-m4": ["+nofp"],
+    "cortex-m7": ["+nofp", "+nofp.dp"],
+    "cortex-m23": [],
+    "cortex-m33": ["+nodsp", "+nofp"],
+    "cortex-m35p": ["+nodsp", "+nofp"],
+    "cortex-m55": ["+nomve", "+nomve.fp", "+nodsp", "+nofp"],
+}
+
+
+def resolve_cpu_features(model, enable_fp=None, enable_fp_dp=None, enable_dsp=None, enable_mve=None):
+    cpu = model
+    fpu = "auto"
+
+    splitted = cpu.split("+")
+    base = splitted[0]
+    attrs = [f"+{attr}" for attr in splitted[1:]]
+
+    if enable_fp is None:
+        enable_fp = base in FP_TARGETS and ("+nofp" not in ALLOWED_ATTRS[base] or "+nofp" not in attrs)
+    if enable_fp_dp is None:
+        if enable_fp:
+            enable_fp_dp = base in FP_DP_TARGETS and ("+nofp.dp" not in ALLOWED_ATTRS[base] or "+nofp.dp" not in attrs)
+        else:
+            enable_fp_dp = False
+    if enable_dsp is None:
+        enable_dsp = base in DSP_TARGETS and ("+nodsp" not in ALLOWED_ATTRS[base] or "+nodsp" not in attrs)
+    if enable_mve is None:
+        enable_mve = base in MVE_TARGETS and ("+nomve" not in ALLOWED_ATTRS[base] or "+nomve" not in attrs)
+
+    float_abi = "hard" if enable_fp else "soft"
+
+    if enable_fp_dp:
+        assert enable_fp, "ARM double precision floating point supports requires fp extension"
+    if enable_mve:
+        assert enable_dsp, "ARM MVEI extension implies DSP extension"
+
+    if len(attrs) > 0:
+        # User defined features explicitly, check for validity
+        if enable_fp and ("+nofp" not in ALLOWED_ATTRS[base] or "+nofp" not in attrs):
+            assert base in FP_TARGETS, f"Chosen cpu '{cpu}' does not support floating point"
+        if enable_fp_dp and ("+nofp.dp" not in ALLOWED_ATTRS[base] or "+nofp.dp" not in attrs):
+            assert base in FP_DP_TARGETS, f"Chosen cpu '{cpu}' does not support double precison floating point"
+        if enable_dsp and ("+nodsp" not in ALLOWED_ATTRS[base] or "+nodsp" not in attrs):
+            assert base in DSP_TARGETS, f"Chosen cpu '{cpu}' does not support dsp instructions"
+        if enable_mve and ("+nomve" not in ALLOWED_ATTRS[base] or "+nomve" not in attrs):
+            assert base in MVE_TARGETS, f"Chosen cpu '{cpu}' does not support mve (helium) instructions"
+    else:
+        # Define features automatically
+        if enable_fp:
+            assert base in FP_TARGETS, f"Chosen cpu '{cpu}' does not support floating point"
+        else:
+            if base in FP_TARGETS:
+                assert "+nofp" in ALLOWED_ATTRS[base], f"Floating point support can not be turned off for cpu '{cpu}'"
+                attrs.append("+nofp")
+        if enable_fp_dp:
+            assert base in FP_DP_TARGETS, f"Chosen cpu '{cpu}' does not support double precision floating point"
+        else:
+            if base in FP_DP_TARGETS and enable_fp:
+                assert (
+                    "+nofp.dp" in ALLOWED_ATTRS[base]
+                ), f"Double precision floating point support can not be turned off for cpu '{cpu}'"
+                attrs.append("+nofp.dp")
+        if enable_dsp:
+            assert base in DSP_TARGETS, f"Chosen cpu '{cpu}' does not support dsp instructions"
+        else:
+            if base in DSP_TARGETS:
+                assert "+nodsp" in ALLOWED_ATTRS[base], f"DSP extension can not be turned off for cpu '{cpu}'"
+                attrs.append("+nodsp")
+        if enable_mve:
+            assert base in MVE_TARGETS, f"Chosen cpu '{cpu}' does not support mve (helium) instructions"
+        else:
+            if base in MVE_TARGETS:
+                assert "+nomve" in ALLOWED_ATTRS[base], f"MVE (helium) extension can not be turned of for cpu '{cpu}'"
+                attrs.append("+nomve")
+
+    cpu = "".join([base] + attrs)
+    return cpu, float_abi, fpu
diff --git a/mlonmcu/target/common.py b/mlonmcu/target/common.py
index b8019cda4..dcbcb48cf 100644
--- a/mlonmcu/target/common.py
+++ b/mlonmcu/target/common.py
@@ -156,8 +156,8 @@ def cli(target, args: List[str] = None):
     exec_parser = subparsers.add_parser("exec", description="Run program with target")
 
     def _handle_execute(args):
-        config = extract_config(args)
-        feature_names = extract_feature_names(args)
+        config, _ = extract_config(args)
+        feature_names, _ = extract_feature_names(args)
         features = init_target_features(feature_names, config)
         target_inst = target(features=features, config=config)
         target_inst.exec(args.program, *args.extra_args, live=True)
@@ -170,8 +170,8 @@ def _handle_execute(args):
     inspect_parser = subparsers.add_parser("inspect", description="Inspect program with target")
 
     def _handle_inspect(args):
-        config = extract_config(args)
-        feature_names = extract_feature_names(args)
+        config, _ = extract_config(args)
+        feature_names, _ = extract_feature_names(args)
         features = init_target_features(feature_names, config)
         target_inst = target(features=features, config=config)
         target_inst.inspect(args.program)
diff --git a/mlonmcu/target/elf.py b/mlonmcu/target/elf.py
index 95fe22c9a..f0082b3cd 100644
--- a/mlonmcu/target/elf.py
+++ b/mlonmcu/target/elf.py
@@ -71,6 +71,29 @@ def parseElf(inFile):
         # Espressif
         ".flash.appdesc",
         ".iram0.text_end",  # ?
+        # QEMU
+        ".htif",
+        # Zephyr
+        ".mcuboot_header",
+        ".metadata",
+        "ctors",
+        "initlevel",
+        "devices",
+        "device_handles",
+        "sw_isr_table",
+        "device_states",
+        ".mcuboot_header",
+        ".metadata",
+        "ctors",
+        "initlevel",
+        "devices",
+        "device_handles",
+        "sw_isr_table",
+        "device_states",
+        ".xt.prop",
+        ".xt.lit",
+        "k_heap_area",
+        "datas",
     ]
     ignorePrefixes = [
         ".gcc_except",
@@ -88,6 +111,7 @@ def parseElf(inFile):
         ".table",
         "dummy",
         "heap_start",
+        "rom_start",
         ".info",
     ]
 
@@ -95,17 +119,18 @@ def parseElf(inFile):
         e = elffile.ELFFile(f)
 
         for s in e.iter_sections():
-            if s.name.startswith(".text") or s.name.endswith(".text"):
+            if s.name.startswith(".text") or s.name.endswith(".text") or s.name == "text":
                 m["rom_code"] += s.data_size
             elif s.name.startswith(".srodata"):
                 m["rom_rodata"] += s.data_size
             elif s.name.startswith(".sdata"):
                 m["ram_data"] += s.data_size
-            elif s.name.endswith(".rodata"):
+            elif s.name.endswith(".rodata") or s.name == "rodata":
                 m["rom_rodata"] += s.data_size
             elif s.name in [
                 ".vectors",
                 "iram0.vectors",
+                ".iram0.vectors",
                 ".init_array",
                 ".fini_array",
                 ".fini",
@@ -116,7 +141,15 @@ def parseElf(inFile):
                 m["rom_misc"] += s.data_size
             elif s.name.endswith(".data"):
                 m["ram_data"] += s.data_size
-            elif s.name == ".bss" or s.name == ".sbss" or s.name == ".shbss" or s.name.endswith(".bss"):
+            elif (
+                s.name == ".bss"
+                or s.name == "bss"
+                or s.name == ".sbss"
+                or s.name == ".shbss"
+                or s.name.endswith(".bss")
+                or s.name.startswith(".sbss")
+                or s.name == "noinit"
+            ):
                 m["ram_zdata"] += s.data_size
             elif s.name in ignoreSections:
                 pass
diff --git a/mlonmcu/target/host_x86.py b/mlonmcu/target/host_x86.py
index 73fcc71d6..d59190291 100644
--- a/mlonmcu/target/host_x86.py
+++ b/mlonmcu/target/host_x86.py
@@ -21,6 +21,7 @@
 import stat
 from pathlib import Path
 
+from mlonmcu.config import str2bool
 from .common import cli, execute
 from .target import Target
 
@@ -31,7 +32,7 @@ class HostX86Target(Target):
     Mainly interesting to easy testing and debugging because benchmarking is not possible.
     """
 
-    FEATURES = ["gdbserver"]
+    FEATURES = Target.FEATURES + ["gdbserver"]
 
     DEFAULTS = {
         **Target.DEFAULTS,
@@ -47,11 +48,13 @@ def __init__(self, name="host_x86", features=None, config=None):
 
     @property
     def gdbserver_enable(self):
-        return bool(self.config["gdbserver_enable"])
+        value = self.config["gdbserver_enable"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def gdbserver_attach(self):
-        return bool(self.config["gdbserver_attach"])
+        value = self.config["gdbserver_attach"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def gdbserver_port(self):
diff --git a/mlonmcu/target/riscv/__init__.py b/mlonmcu/target/riscv/__init__.py
new file mode 100644
index 000000000..ba9a480ed
--- /dev/null
+++ b/mlonmcu/target/riscv/__init__.py
@@ -0,0 +1,6 @@
+from .etiss_pulpino import EtissPulpinoTarget
+from .spike import SpikeTarget
+from .ovpsim import OVPSimTarget
+from .riscv_qemu import RiscvQemuTarget
+
+__all__ = ["EtissPulpinoTarget", "SpikeTarget", "OVPSimTarget", "RiscvQemuTarget"]
diff --git a/mlonmcu/target/etiss_pulpino.py b/mlonmcu/target/riscv/etiss_pulpino.py
similarity index 75%
rename from mlonmcu/target/etiss_pulpino.py
rename to mlonmcu/target/riscv/etiss_pulpino.py
index 39ac7b57b..173516820 100644
--- a/mlonmcu/target/etiss_pulpino.py
+++ b/mlonmcu/target/riscv/etiss_pulpino.py
@@ -24,11 +24,12 @@
 from pathlib import Path
 
 from mlonmcu.logging import get_logger
+from mlonmcu.config import str2bool
 from mlonmcu.artifact import Artifact, ArtifactFormat
 from mlonmcu.feature.features import SUPPORTED_TVM_BACKENDS
-from .common import cli, execute
+from mlonmcu.target.common import cli, execute
+from mlonmcu.target.metrics import Metrics
 from .riscv import RISCVTarget
-from .metrics import Metrics
 
 logger = get_logger()
 
@@ -36,7 +37,7 @@
 class EtissPulpinoTarget(RISCVTarget):
     """Target using a Pulpino-like VP running in the ETISS simulator"""
 
-    FEATURES = ["gdbserver", "etissdbg", "trace", "log_instrs", "pext", "vext"]
+    FEATURES = RISCVTarget.FEATURES + ["gdbserver", "etissdbg", "trace", "log_instrs", "pext", "vext"]
 
     DEFAULTS = {
         **RISCVTarget.DEFAULTS,
@@ -45,6 +46,7 @@ class EtissPulpinoTarget(RISCVTarget):
         "gdbserver_port": 2222,
         "debug_etiss": False,
         "trace_memory": False,
+        # "plugins": ["PrintInstruction"],
         "plugins": [],
         "verbose": False,
         "cpu_arch": None,
@@ -53,9 +55,13 @@ class EtissPulpinoTarget(RISCVTarget):
         "etissvp.ram_start": 0x800000,
         "etissvp.ram_size": 0x4000000,  # 64 MB
         "etissvp.cycle_time_ps": 31250,  # 32 MHz
+        "enable_fpu": True,  # WIP
         "enable_vext": False,
         "enable_pext": False,
+        "vlen": 0,  # vectorization=off
+        "elen": 32,
         "jit": None,
+        "end_to_end_cycles": False,
     }
     REQUIRED = RISCVTarget.REQUIRED + ["etiss.src_dir", "etiss.install_dir", "etissvp.script"]
 
@@ -77,11 +83,13 @@ def etiss_script(self):
 
     @property
     def gdbserver_enable(self):
-        return bool(self.config["gdbserver_enable"])
+        value = self.config["gdbserver_enable"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def gdbserver_attach(self):
-        return bool(self.config["gdbserver_attach"])
+        value = self.config["gdbserver_attach"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def gdbserver_port(self):
@@ -89,11 +97,13 @@ def gdbserver_port(self):
 
     @property
     def debug_etiss(self):
-        return bool(self.config["debug_etiss"])
+        value = self.config["debug_etiss"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def trace_memory(self):
-        return bool(self.config["trace_memory"])
+        value = self.config["trace_memory"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def plugins(self):
@@ -101,7 +111,8 @@ def plugins(self):
 
     @property
     def verbose(self):
-        return bool(self.config["verbose"])
+        value = self.config["verbose"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def rom_start(self):
@@ -132,31 +143,60 @@ def cpu_arch(self):
         else:
             return "RV32IMACFD"
 
+    @property
+    def enable_fpu(self):
+        value = self.config["enable_fpu"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
     @property
     def enable_vext(self):
-        return bool(self.config["enable_vext"])
+        value = self.config["enable_vext"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def enable_pext(self):
-        return bool(self.config["enable_pext"])
+        value = self.config["enable_pext"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def vlen(self):
+        return int(self.config["vlen"])
+
+    @property
+    def elen(self):
+        return int(self.config["elen"])
 
     @property
     def jit(self):
         return self.config["jit"]
 
     @property
-    def arch(self):
-        ret = str(self.config["arch"])
-        if self.enable_pext:
-            if "p" not in ret[2:]:
-                ret += "p"
-        if self.enable_vext:
-            if "v" not in ret[2:]:
-                ret += "v"
-
+    def extensions(self):
+        ret = super().extensions
+        if self.enable_pext and "p" not in ret:
+            ret.append("p")
+        if self.enable_vext and ("v" not in ret and "zve32x" not in ret and "zve32f" not in ret):
+            if self.elen == 32:  # Required to tell the compiler that EEW is not allowed...
+                # if self.enable_fpu:
+                if True:
+                    ret.append("zve32x")
+                else:
+                    ret.append("zve32f")
+            else:
+                ret.append("v")
         return ret
 
-    # TODO: other properties
+    @property
+    def attr(self):
+        attrs = super().attr.split(",")
+        if self.enable_vext and f"+zvl{self.vlen}b" not in attrs:
+            attrs.append(f"+zvl{self.vlen}b")
+        return ",".join(attrs)
+
+    @property
+    def end_to_end_cycles(self):
+        value = self.config["end_to_end_cycles"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     def write_ini(self, path):
         # TODO: Either create artifact for ini or prefer to use cmdline args.
@@ -169,12 +209,23 @@ def write_ini(self, path):
                 f.write(f"jit.type={self.jit}JIT\n")
             f.write("[IntConfigurations]\n")
             # f.write("etiss.max_block_size=100\n")
+            # f.write("etiss.max_block_size=500\n")
             f.write(f"simple_mem_system.memseg_origin_00={hex(self.rom_start)}\n")
             f.write(f"simple_mem_system.memseg_length_00={hex(self.rom_size)}\n")
             f.write(f"simple_mem_system.memseg_origin_01={hex(self.ram_start)}\n")
             f.write(f"simple_mem_system.memseg_length_01={hex(self.ram_size)}\n")
             f.write("\n")
             f.write(f"arch.cpu_cycle_time_ps={self.cycle_time_ps}\n")
+            if self.enable_fpu:
+                # TODO: do not hardcode cpu_arch
+                # TODO: i.e. use cpu_arch_lower
+                f.write("arch.rv32imacfdpv.mstatus_fs=1")
+            if self.enable_vext:
+                f.write("arch.rv32imacfdpv.mstatus_vs=1")
+                if self.vlen > 0:
+                    f.write(f"arch.rv32imacfdpv.vlen={self.vlen}")
+                # if self.elen > 0:
+                #     f.write(f"arch.rv32imacfdpv.elen={self.elen}")
 
             if self.gdbserver_enable:
                 f.write("[Plugin gdbserver]\n")
@@ -232,12 +283,17 @@ def parse_stdout(self, out, handle_exit=None):
             if exit_code != 0:
                 logger.error("Execution failed - " + out)
                 raise RuntimeError(f"unexpected exit code: {exit_code}")
+        else:
+            exit_code = 0
         error_match = re.search(r"ETISS: Error: (.*)", out)
         if error_match:
             error_msg = error_match.group(1)
             raise RuntimeError(f"An ETISS Error occured during simulation: {error_msg}")
 
-        cpu_cycles = re.search(r"CPU Cycles \(estimated\): (.*)", out)
+        if self.end_to_end_cycles:
+            cpu_cycles = re.search(r"CPU Cycles \(estimated\): (.*)", out)
+        else:
+            cpu_cycles = re.search(r"Total Cycles: (.*)", out)
         if not cpu_cycles:
             if exit_code == 0:
                 logger.warning("unexpected script output (cycles)")
@@ -254,8 +310,7 @@ def parse_stdout(self, out, handle_exit=None):
 
         return cycles, mips
 
-    def get_metrics(self, elf, directory, handle_exit=None, num=None):
-        assert num is None
+    def get_metrics(self, elf, directory, handle_exit=None):
         out = ""
         if self.trace_memory:
             trace_file = os.path.join(directory, "dBusAccess.csv")
@@ -295,7 +350,7 @@ def get_metrics(self, elf, directory, handle_exit=None, num=None):
             )
 
         metrics = Metrics()
-        metrics.add("Total Cycles", total_cycles)
+        metrics.add("Cycles", total_cycles)
         metrics.add("MIPS", mips, optional=True)
 
         metrics_file = os.path.join(directory, "metrics.csv")
@@ -364,12 +419,27 @@ def get_platform_defs(self, platform):
         ret["PULPINO_ROM_SIZE"] = self.rom_size
         ret["PULPINO_RAM_START"] = self.ram_start
         ret["PULPINO_RAM_SIZE"] = self.ram_size
+        if self.enable_pext:
+            ret["RISCV_RVP_MAJOR"] = "0"
+            ret["RISCV_RVP_MINOR"] = "96"
+        if self.enable_vext:
+            ret["RISCV_RVV_MAJOR"] = "1"
+            ret["RISCV_RVV_MINOR"] = "0"
+            ret["RISCV_RVV_VLEN"] = self.vlen
         return ret
 
     def get_backend_config(self, backend):
         ret = super().get_backend_config(backend)
         if backend in SUPPORTED_TVM_BACKENDS:
             ret.update({"target_model": "etissvp"})
+            if self.enable_pext or self.enable_vext:
+                ret.update(
+                    {
+                        # Warning: passing kernel layouts does not work with upstream TVM
+                        # TODO: allow passing map?
+                        "desired_layout": "NHWC:HWOI",
+                    }
+                )
         return ret
 
 
diff --git a/mlonmcu/target/ovpsim.py b/mlonmcu/target/riscv/ovpsim.py
similarity index 51%
rename from mlonmcu/target/ovpsim.py
rename to mlonmcu/target/riscv/ovpsim.py
index 178a34988..32d5ab5ad 100644
--- a/mlonmcu/target/ovpsim.py
+++ b/mlonmcu/target/riscv/ovpsim.py
@@ -23,17 +23,32 @@
 from pathlib import Path
 
 from mlonmcu.logging import get_logger
-from .common import cli, execute
-from .riscv import RISCVTarget
-from .metrics import Metrics
+from mlonmcu.config import str2bool
+from mlonmcu.feature.features import SUPPORTED_TVM_BACKENDS
+from mlonmcu.target.common import cli, execute
+from mlonmcu.target.metrics import Metrics
+from .riscv import RISCVTarget, sort_extensions_canonical
 
 logger = get_logger()
 
 
+def replace_unsupported(exts):
+    ret = []
+    for ext in exts:
+        if "ZVE" in ext or "ZVL" in ext:
+            ret.append("V")
+        elif ext == "P":
+            ret.append(ext)
+            ret.append("B")
+        else:
+            ret.append(ext)
+    return ret
+
+
 class OVPSimTarget(RISCVTarget):
     """Target using an ARM FVP (fixed virtual platform) based on a Cortex M55 with EthosU support"""
 
-    FEATURES = ["vext", "pext", "gdbserver"]
+    FEATURES = RISCVTarget.FEATURES + ["vext", "pext", "gdbserver"]
 
     DEFAULTS = {
         **RISCVTarget.DEFAULTS,
@@ -43,8 +58,10 @@ class OVPSimTarget(RISCVTarget):
         "enable_pext": False,
         "enable_fpu": True,
         "variant": "RVB32I",
-        # "extensions": "MAFDCV",
-        "extensions": "MAFDC",  # rv32gc
+        "end_to_end_cycles": False,
+        "gdbserver_enable": False,
+        "gdbserver_attach": False,
+        "gdbserver_port": 2222,
     }
     REQUIRED = RISCVTarget.REQUIRED + ["ovpsim.exe"]
 
@@ -57,20 +74,35 @@ def ovpsim_exe(self):
 
     @property
     def variant(self):
-        return str(self.config["variant"])
+        temp = self.config["variant"]
+        if temp:
+            return temp
+        else:
+            return f"RVB{self.xlen}I"
 
     @property
     def extensions(self):
-        ret = str(self.config["extensions"])
-
-        if self.enable_pext:
-            if "P" not in ret:
-                ret += "BP"
-        if self.enable_vext:
-            if "V" not in ret:
-                ret += "V"
+        ret = super().extensions
+        if self.enable_pext and "p" not in ret:
+            ret.append("p")
+        if self.enable_vext and ("v" not in ret and "zve32x" not in ret and "zve32f" not in ret):
+            if self.elen == 32:  # Required to tell the compiler that EEW is not allowed...
+                # if self.enable_fpu:
+                if True:
+                    ret.append("zve32x")
+                else:
+                    ret.append("zve32f")
+            else:
+                ret.append("v")
         return ret
 
+    @property
+    def attr(self):
+        attrs = super().attr.split(",")
+        if self.enable_vext and f"+zvl{self.vlen}b" not in attrs:
+            attrs.append(f"+zvl{self.vlen}b")
+        return ",".join(attrs)
+
     @property
     def vlen(self):
         return int(self.config["vlen"])
@@ -81,33 +113,47 @@ def elen(self):
 
     @property
     def enable_fpu(self):
-        return bool(self.config["enable_fpu"])
+        value = self.config["enable_fpu"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def enable_vext(self):
-        return bool(self.config["enable_vext"])
+        value = self.config["enable_vext"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def enable_pext(self):
-        return bool(self.config["enable_pext"])
+        value = self.config["enable_pext"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
-    def arch(self):
-        ret = str(self.config["arch"])
-        if self.enable_pext:
-            if "p" not in ret[2:]:
-                ret += "p"
-        if self.enable_vext:
-            if "v" not in ret[2:]:
-                ret += "v"
-        return ret
+    def end_to_end_cycles(self):
+        value = self.config["end_to_end_cycles"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def gdbserver_enable(self):
+        value = self.config["gdbserver_enable"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def gdbserver_attach(self):
+        value = self.config["gdbserver_attach"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    @property
+    def gdbserver_port(self):
+        return int(self.config["gdbserver_port"])
 
     def get_default_ovpsim_args(self):
+        extensions_before = sort_extensions_canonical(self.extensions, lower=False, unpack=True)
+        extensions_after = replace_unsupported(extensions_before)
+        extensions_str = "".join(sort_extensions_canonical(extensions_after))
         args = [
             "--variant",
             self.variant,
             "--override",
-            f"riscvOVPsim/cpu/add_Extensions={self.extensions}",
+            f"riscvOVPsim/cpu/add_Extensions={extensions_str}",
             "--override",
             "riscvOVPsim/cpu/unaligned=T",
         ]
@@ -121,7 +167,7 @@ def get_default_ovpsim_args(self):
                 ]
             )
         if self.enable_vext:
-            assert self.enable_fpu, "Spike V-Extension requires enabled FPU"
+            assert self.enable_fpu, "V-Extension requires enabled FPU"
             args.extend(
                 [
                     "--override",
@@ -134,14 +180,13 @@ def get_default_ovpsim_args(self):
             )
             args.extend(["--override", f"riscvOVPsim/cpu/mstatus_VS={int(self.enable_vext)}"])
         if self.enable_fpu:
-            assert "F" in self.extensions
-            # if "F" not in self.extensions:
-            #     self.extensions += "F"
+            assert "f" in self.extensions or "g" in self.extensions
         args.extend(["--override", f"riscvOVPsim/cpu/mstatus_FS={int(self.enable_fpu)}"])
-        if False:  # ?
-            args.append("--trace")
-            args.extend(["--port", "3333"])
-            args.append("--gdbconsole")
+        if self.gdbserver_enable:
+            # args.append("--trace")
+            args.extend(["--port", str(self.gdbserver_port)])
+            if self.gdbserver_attach:
+                args.append("--gdbconsole")
         return args
 
     def exec(self, program, *args, cwd=os.getcwd(), **kwargs):
@@ -167,7 +212,10 @@ def exec(self, program, *args, cwd=os.getcwd(), **kwargs):
 
     def parse_stdout(self, out):
         # cpi = 1
-        cpu_cycles = re.search(r"  Simulated instructions:(.*)", out)
+        if self.end_to_end_cycles:
+            cpu_cycles = re.search(r"  Simulated instructions:(.*)", out)
+        else:
+            cpu_cycles = re.search(r"Total Cycles: (.*)", out)
         if not cpu_cycles:
             raise RuntimeError("unexpected script output (cycles)")
             cycles = None
@@ -181,8 +229,7 @@ def parse_stdout(self, out):
                 mips = float(mips_str)
         return cycles, mips
 
-    def get_metrics(self, elf, directory, handle_exit=None, num=None):
-        assert num is None
+    def get_metrics(self, elf, directory, handle_exit=None):
         out = ""
         if self.print_outputs:
             out += self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit)
@@ -193,11 +240,37 @@ def get_metrics(self, elf, directory, handle_exit=None, num=None):
         cycles, mips = self.parse_stdout(out)
 
         metrics = Metrics()
-        metrics.add("Total Cycles", cycles)
-        metrics.add("MIPS", cycles, optional=True)
+        metrics.add("Cycles", cycles)
+        if mips:
+            metrics.add("MIPS", mips, optional=True)
 
         return metrics, out, []
 
+    def get_platform_defs(self, platform):
+        ret = super().get_platform_defs(platform)
+        if self.enable_pext:
+            ret["RISCV_RVP_MAJOR"] = "0"
+            ret["RISCV_RVP_MINOR"] = "96"
+        if self.enable_vext:
+            ret["RISCV_RVV_MAJOR"] = "1"
+            ret["RISCV_RVV_MINOR"] = "0"
+            ret["RISCV_RVV_VLEN"] = self.vlen
+        return ret
+
+    def get_backend_config(self, backend):
+        ret = super().get_backend_config(backend)
+        if backend in SUPPORTED_TVM_BACKENDS:
+            ret.update({"target_model": "ovpsim-rv32"})
+            if self.enable_pext or self.enable_vext:
+                ret.update(
+                    {
+                        # Warning: passing kernel layouts does not work with upstream TVM
+                        # TODO: allow passing map?
+                        "desired_layout": "NHWC:HWOI",
+                    }
+                )
+        return ret
+
 
 if __name__ == "__main__":
     cli(target=OVPSimTarget)
diff --git a/mlonmcu/target/riscv.py b/mlonmcu/target/riscv/riscv.py
similarity index 55%
rename from mlonmcu/target/riscv.py
rename to mlonmcu/target/riscv/riscv.py
index c7c83175e..9d1923506 100644
--- a/mlonmcu/target/riscv.py
+++ b/mlonmcu/target/riscv/riscv.py
@@ -22,62 +22,29 @@
 
 from mlonmcu.logging import get_logger
 from mlonmcu.feature.features import SUPPORTED_TVM_BACKENDS
-from .target import Target
+from mlonmcu.target import Target
+from .util import sort_extensions_canonical
 
 logger = get_logger()
 
 
-def sort_extensions_canonical(extensions):
-    """Utility to get the canonical architecture name string."""
-
-    # See: https://riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf#table.22.1
-    ORDER = [
-        "I",
-        "M",
-        "A",
-        "F",
-        "D",
-        "G",
-        "Q",
-        "L",
-        "C",
-        "B",
-        "J",
-        "T",
-        "P",
-        "V",
-        "X",
-        "S",
-        "SX",
-    ]  # What about Z* extensions?
-    extensions_new = extensions.copy()
-
-    def _get_index(x):
-        if x in ORDER:
-            return ORDER.index(x)
-        else:
-            for i, o in enumerate(ORDER):
-                if x.startswith(o):
-                    return i
-            return ORDER.index("X") - 0.5  # Insert unknown keys right before custom extensions
-
-    extensions_new.sort(key=lambda x: _get_index(x))
-    return extensions_new
-
-
 class RISCVTarget(Target):
     """Common base class for RISCV-like targets. Please do not use this as a target itself!"""
 
-    FEATURES = []
+    FEATURES = Target.FEATURES + []
 
     DEFAULTS = {
         **Target.DEFAULTS,
+        "xlen": 32,
+        "extensions": ["g", "c"],
         "timeout_sec": 0,  # disabled
         "extra_args": "",
-        "arch": "rv32gc",
-        "abi": "ilp32d",
+        "arch": None,
+        "abi": None,
+        "attr": "",
     }
     REQUIRED = ["riscv_gcc.install_dir", "riscv_gcc.name"]
+    OPTIONAL = ["llvm.install_dir"]
 
     @property
     def riscv_prefix(self):
@@ -87,13 +54,53 @@ def riscv_prefix(self):
     def riscv_basename(self):
         return Path(self.config["riscv_gcc.name"])
 
+    @property
+    def xlen(self):
+        return int(self.config["xlen"])
+
+    @property
+    def extensions(self):
+        exts = self.config.get("extensions", []).copy()
+        if not isinstance(self.config["extensions"], list):
+            exts = exts.split(",")
+        return exts
+
     @property
     def arch(self):
-        return str(self.config["arch"])
+        temp = self.config["arch"]  # TODO: allow underscores and versions
+        if temp:
+            return temp
+        else:
+            exts_str = "".join(sort_extensions_canonical(self.extensions, lower=True))
+            return f"rv{self.xlen}{exts_str}"
 
     @property
     def abi(self):
-        return str(self.config["abi"])
+        temp = self.config["abi"]
+        if temp:
+            return temp
+        else:
+            if self.xlen == 32:
+                temp = "ilp32"
+            elif self.xlen == 64:
+                temp = "lp64"
+            else:
+                raise RuntimeError(f"Invalid xlen: {self.xlen}")
+            if "d" in self.extensions or "g" in self.extensions:
+                temp += "d"
+            elif "f" in self.extensions:
+                temp += "f"
+            return temp
+
+    @property
+    def attr(self):
+        attrs = str(self.config["attr"]).split(",")
+        if len(attrs) == 1 and len(attrs[0]) == 0:
+            attrs = []
+        for ext in sort_extensions_canonical(self.extensions, lower=True, unpack=True):
+            attrs.append(f"+{ext}")
+        attrs = list(set(attrs))
+        return ",".join(attrs)
 
     @property
     def extra_args(self):
@@ -110,7 +117,7 @@ def timeout_sec(self):
         return int(self.config["timeout_sec"])
 
     def get_target_system(self):
-        return "generic_riscv"
+        return "generic_riscv"  # TODO: rename to generic-rv32 for compatibility with LLVM
 
     def get_platform_defs(self, platform):
         ret = super().get_platform_defs(platform)
@@ -118,6 +125,7 @@ def get_platform_defs(self, platform):
         ret["RISCV_ELF_GCC_BASENAME"] = self.riscv_basename
         ret["RISCV_ARCH"] = self.arch
         ret["RISCV_ABI"] = self.abi
+        ret["RISCV_ATTR"] = self.attr  # TODO: use for clang
         return ret
 
     def get_arch(self):
@@ -129,9 +137,9 @@ def get_backend_config(self, backend):
                 "target_device": "riscv_cpu",
                 "target_march": self.arch,
                 "target_model": "unknown",
-                "target_mtriple": self.riscv_basename,
-                # "target_mattr": "?",
-                # "target_mcpu": "?",
-                # "target_mabi": self.abi,
+                "target_mtriple": self.riscv_basename,  # TODO: riscv32-esp-elf for esp32c3!
+                "target_mabi": self.abi,
+                "target_mattr": self.attr,
+                "target_mcpu": f"generic-rv{self.xlen}",
             }
         return {}
diff --git a/mlonmcu/target/riscv/riscv_qemu.py b/mlonmcu/target/riscv/riscv_qemu.py
new file mode 100644
index 000000000..1be821771
--- /dev/null
+++ b/mlonmcu/target/riscv/riscv_qemu.py
@@ -0,0 +1,163 @@
+# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+#
+# This file is part of MLonMCU.
+# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""MLonMCU RISC-V QEMU Target definitions"""
+
+import os
+
+import re
+
+from mlonmcu.logging import get_logger
+from mlonmcu.config import str2bool
+from mlonmcu.target.common import cli, execute
+from mlonmcu.target.metrics import Metrics
+from .riscv import RISCVTarget
+
+logger = get_logger()
+
+# TODO: create (Riscv)QemuTarget with variable machine
+
+
+class RiscvQemuTarget(RISCVTarget):
+    """Target using a spike machine in the QEMU simulator"""
+
+    FEATURES = RISCVTarget.FEATURES + ["vext"]
+
+    DEFAULTS = {
+        **RISCVTarget.DEFAULTS,
+        "vlen": 32,  # TODO: check allowed range [128, 1024]
+        "elen": 32,
+        "enable_vext": False,
+    }
+    REQUIRED = RISCVTarget.REQUIRED + ["riscv32_qemu.exe"]  # TODO: 64 bit?
+
+    def __init__(self, name="riscv_qemu", features=None, config=None):
+        super().__init__(name, features=features, config=config)
+
+    @property
+    def riscv32_qemu_exe(self):
+        return self.config["riscv32_qemu.exe"]
+
+    @property
+    def extensions(self):
+        ret = super().extensions
+        if self.enable_vext and ("v" not in ret and "zve32x" not in ret and "zve32f" not in ret):
+            if self.elen == 32:  # Required to tell the compiler that EEW is not allowed...
+                # if not self.enable_fpu:
+                if True:
+                    ret.append("zve32x")
+                else:
+                    ret.append("zve32f")
+            else:
+                ret.append("v")
+        return ret
+
+    @property
+    def attr(self):
+        attrs = super().attr.split(",")
+        if self.enable_vext and f"+zvl{self.vlen}b" not in attrs:
+            attrs.append(f"+zvl{self.vlen}b")
+        return ",".join(attrs)
+
+    @property
+    def vlen(self):
+        return int(self.config["vlen"])
+
+    @property
+    def elen(self):
+        return int(self.config["elen"])
+
+    @property
+    def enable_vext(self):
+        value = self.config["enable_vext"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
+
+    def get_cpu_str(self):
+        cfg = {}
+        if self.enable_vext:
+            cfg["v"] = "true"
+            cfg["vlen"] = str(self.vlen)
+            cfg["elen"] = str(self.elen)
+            cfg["vext_spec"] = "v1.0"
+        return ",".join([f"rv{self.xlen}"] + [f"{key}={value}" for key, value in cfg.items()])
+
+    def get_qemu_args(self, program):
+        args = []
+        args.extend(["-machine", "spike"])
+        args.extend(["-bios", "none"])
+        args.extend(["-icount", "shift=1"])
+        args.extend(["-cpu", self.get_cpu_str()])
+        args.append("-nographic")
+        args.extend(["-kernel", program])
+        return args
+
+    def exec(self, program, *args, cwd=os.getcwd(), **kwargs):
+        """Use target to execute a executable with given arguments"""
+        assert len(args) == 0, "Qemu does not support passing arguments."
+        qemu_args = self.get_qemu_args(program)
+
+        if self.timeout_sec > 0:
+            raise NotImplementedError
+        else:
+            ret = execute(
+                self.riscv32_qemu_exe,
+                *qemu_args,
+                cwd=cwd,
+                **kwargs,
+            )
+        return ret
+
+    def parse_stdout(self, out, handle_exit=None):
+        cpu_cycles = re.search(r"Total Cycles: (.*)", out)
+        if not cpu_cycles:
+            logger.warning("unexpected script output (cycles)")
+            cycles = None
+        else:
+            cycles = int(float(cpu_cycles.group(1)))
+        return cycles
+
+    def get_metrics(self, elf, directory, handle_exit=None):
+        out = ""
+
+        if self.print_outputs:
+            out += self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit)
+        else:
+            out += self.exec(
+                elf, cwd=directory, live=False, print_func=lambda *args, **kwargs: None, handle_exit=handle_exit
+            )
+        total_cycles = self.parse_stdout(out, handle_exit=handle_exit)
+
+        metrics = Metrics()
+        metrics.add("Cycles", total_cycles)
+
+        return metrics, out, []
+
+    def get_target_system(self):
+        return self.name
+
+    def get_platform_defs(self, platform):
+        assert platform == "mlif"
+        ret = super().get_platform_defs(platform)
+        if self.enable_vext:
+            ret["RISCV_RVV_MAJOR"] = "1"
+            ret["RISCV_RVV_MINOR"] = "0"
+            ret["RISCV_RVV_VLEN"] = self.vlen
+        return ret
+
+
+if __name__ == "__main__":
+    cli(target=RiscvQemuTarget)
diff --git a/mlonmcu/target/spike.py b/mlonmcu/target/riscv/spike.py
similarity index 68%
rename from mlonmcu/target/spike.py
rename to mlonmcu/target/riscv/spike.py
index cb81ce102..646ff8b29 100644
--- a/mlonmcu/target/spike.py
+++ b/mlonmcu/target/riscv/spike.py
@@ -25,9 +25,9 @@
 from mlonmcu.logging import get_logger
 from mlonmcu.config import str2bool
 from mlonmcu.feature.features import SUPPORTED_TVM_BACKENDS
-from .common import cli, execute
+from mlonmcu.target.common import cli, execute
+from mlonmcu.target.metrics import Metrics
 from .riscv import RISCVTarget
-from .metrics import Metrics
 
 logger = get_logger()
 
@@ -35,7 +35,7 @@
 class SpikeTarget(RISCVTarget):
     """Target using the riscv-isa-sim (Spike) RISC-V simulator."""
 
-    FEATURES = ["vext", "pext", "cachesim", "log_instrs"]
+    FEATURES = RISCVTarget.FEATURES + ["vext", "pext", "cachesim", "log_instrs"]
 
     DEFAULTS = {
         **RISCVTarget.DEFAULTS,
@@ -61,24 +61,37 @@ def spike_pk(self):
 
     @property
     def enable_vext(self):
-        return bool(self.config["enable_vext"])
+        value = self.config["enable_vext"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
     def enable_pext(self):
-        return bool(self.config["enable_pext"])
+        value = self.config["enable_pext"]
+        return str2bool(value) if not isinstance(value, (bool, int)) else value
 
     @property
-    def arch(self):
-        ret = str(self.config["arch"])
-        if self.enable_pext:
-            if "p" not in ret[2:]:
-                ret += "p"
-        if self.enable_vext:
-            if "v" not in ret[2:]:
-                ret += "v"
-
+    def extensions(self):
+        ret = super().extensions
+        if self.enable_pext and "p" not in ret:
+            ret.append("p")
+        if self.enable_vext and ("v" not in ret and "zve32x" not in ret and "zve32f" not in ret):
+            if self.elen == 32:  # Required to tell the compiler that EEW is not allowed...
+                # if self.enable_fpu:
+                if True:
+                    ret.append("zve32x")
+                else:
+                    ret.append("zve32f")
+            else:
+                ret.append("v")
         return ret
 
+    @property
+    def attr(self):
+        attrs = super().attr.split(",")
+        if self.enable_vext and f"+zvl{self.vlen}b" not in attrs:
+            attrs.append(f"+zvl{self.vlen}b")
+        return ",".join(attrs)
+
     @property
     def vlen(self):
         return int(self.config["vlen"])
@@ -101,7 +114,8 @@ def exec(self, program, *args, cwd=os.getcwd(), **kwargs):
         spike_args = []
         spikepk_args = []
 
-        spike_args.append(f"--isa={self.arch}")
+        arch_after = self.arch.replace("zve32x", "v").replace("zve32f", "v")
+        spike_args.append(f"--isa={arch_after}")
 
         if len(self.extra_args) > 0:
             spike_args.extend(self.extra_args)
@@ -145,8 +159,7 @@ def parse_stdout(self, out):
         # mips = None  # TODO: parse mips?
         return cycles
 
-    def get_metrics(self, elf, directory, handle_exit=None, num=None):
-        assert num is None
+    def get_metrics(self, elf, directory, handle_exit=None):
         out = ""
         if self.print_outputs:
             out += self.exec(elf, cwd=directory, live=True, handle_exit=handle_exit)
@@ -157,16 +170,33 @@ def get_metrics(self, elf, directory, handle_exit=None, num=None):
         cycles = self.parse_stdout(out)
 
         metrics = Metrics()
-        metrics.add("Total Cycles", cycles)
+        metrics.add("Cycles", cycles)
 
         return metrics, out, []
 
+    def get_platform_defs(self, platform):
+        ret = super().get_platform_defs(platform)
+        if self.enable_pext:
+            ret["RISCV_RVP_MAJOR"] = "0"
+            ret["RISCV_RVP_MINOR"] = "96"
+        if self.enable_vext:
+            ret["RISCV_RVV_MAJOR"] = "1"
+            ret["RISCV_RVV_MINOR"] = "0"
+            ret["RISCV_RVV_VLEN"] = self.vlen
+        return ret
+
     def get_backend_config(self, backend):
         ret = super().get_backend_config(backend)
         if backend in SUPPORTED_TVM_BACKENDS:
             ret.update({"target_model": "spike-rv32"})
-            if self.enable_pext:
-                pass  # TODO: change graph layout to use SIMD kernels
+            if self.enable_pext or self.enable_vext:
+                ret.update(
+                    {
+                        # Warning: passing kernel layouts does not work with upstream TVM
+                        # TODO: allow passing map?
+                        "desired_layout": "NHWC:HWOI",
+                    }
+                )
         return ret
 
 
diff --git a/mlonmcu/target/riscv/util.py b/mlonmcu/target/riscv/util.py
new file mode 100644
index 000000000..fb72216aa
--- /dev/null
+++ b/mlonmcu/target/riscv/util.py
@@ -0,0 +1,70 @@
+#
+# Copyright (c) 2022 TUM Department of Electrical and Computer Engineering.
+#
+# This file is part of MLonMCU.
+# See https://github.com/tum-ei-eda/mlonmcu.git for further info.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""MLonMCU RISC-V utilities"""
+
+
+def sort_extensions_canonical(extensions, lower=False, unpack=False):
+    """Utility to get the canonical architecture name string."""
+
+    # See: https://riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf#table.22.1
+    ORDER = [
+        "I",
+        "M",
+        "A",
+        "F",
+        "D",
+        "G",
+        "Q",
+        "L",
+        "C",
+        "B",
+        "J",
+        "T",
+        "P",
+        "V",
+        "X",
+        "S",
+        "SX",
+    ]  # What about Z* extensions?
+    extensions_new = extensions.copy()
+
+    # make upper
+    extensions_new = [x.upper() for x in extensions_new]
+
+    if unpack:
+        # Convert G into IMAFD
+        if "G" in extensions_new:
+            extensions_new = [x for x in extensions_new if x != "G"] + ["I", "M", "A", "F", "D"]
+        # Remove duplicates
+        extensions_new = list(set(extensions_new))
+
+    def _get_index(x):
+        if x in ORDER:
+            return ORDER.index(x)
+        else:
+            for i, o in enumerate(ORDER):
+                if x.startswith(o):
+                    return i
+            return ORDER.index("X") - 0.5  # Insert unknown keys right before custom extensions
+
+    extensions_new.sort(key=lambda x: _get_index(x))
+
+    if lower:
+        extensions_new = [x.lower() for x in extensions_new]
+    return extensions_new
diff --git a/mlonmcu/target/target.py b/mlonmcu/target/target.py
index b0eebc51b..e15604c6f 100644
--- a/mlonmcu/target/target.py
+++ b/mlonmcu/target/target.py
@@ -57,12 +57,14 @@ class Target:
         Optinal map of environment variables
     """
 
-    FEATURES = []
+    FEATURES = ["benchmark"]
     DEFAULTS = {
         "print_outputs": False,
+        "repeat": None,
     }
 
     REQUIRED = []
+    OPTIONAL = []
 
     def __init__(
         self,
@@ -74,7 +76,7 @@ def __init__(
         self.config = config if config else {}
         self.callbacks = []
         self.features = self.process_features(features)
-        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.REQUIRED)
+        self.config = filter_config(self.config, self.name, self.DEFAULTS, self.OPTIONAL, self.REQUIRED)
         self.inspect_program = "readelf"
         self.inspect_program_args = ["--all"]
         self.env = os.environ
@@ -85,6 +87,10 @@ def print_outputs(self):
         value = self.config["print_outputs"]
         return str2bool(value) if not isinstance(value, (bool, int)) else value
 
+    @property
+    def repeat(self):
+        return self.config["repeat"]
+
     def __repr__(self):
         return f"Target({self.name})"
 
@@ -106,8 +112,7 @@ def inspect(self, program: Path, *args, **kwargs):
         """Use target to inspect a executable"""
         return execute(self.inspect_program, program, *self.inspect_program_args, *args, **kwargs)
 
-    def get_metrics(self, elf, directory, handle_exit=None, num=None):
-        assert num is None
+    def get_metrics(self, elf, directory, handle_exit=None):
         # This should not be accurate, just a fallback which should be overwritten
         start_time = time.time()
         if self.print_outputs:
@@ -125,17 +130,27 @@ def get_metrics(self, elf, directory, handle_exit=None, num=None):
 
         return metrics, out, []
 
-    def generate_metrics(self, elf, num=None):
+    def generate_metrics(self, elf):
         artifacts = []
-        with tempfile.TemporaryDirectory() as temp_dir:
-            metrics, out, artifacts_ = self.get_metrics(elf, temp_dir, num=num)
-            artifacts.extend(artifacts_)
-            for callback in self.callbacks:  # TODO: give priorities to determine order?
-                callback(out, metrics, artifacts)
-            content = metrics.to_csv(include_optional=True)  # TODO: store df instead?
-            artifact = Artifact("metrics.csv", content=content, fmt=ArtifactFormat.TEXT)
-            # Alternative: artifact = Artifact("metrics.csv", data=df/dict, fmt=ArtifactFormat.DATA)
-            artifacts.append(artifact)
+        metrics = []
+        total = 1 + (self.repeat if self.repeat else 0)
+        # We only save the stdout and artifacts of the last execution
+        # Callect metrics from all runs to aggregate them in a callback with high priority
+        for n in range(total):
+            with tempfile.TemporaryDirectory() as temp_dir:
+                metrics_, out, artifacts_ = self.get_metrics(elf, temp_dir)
+            metrics.append(metrics_)
+        for callback in self.callbacks:
+            callback(out, metrics, artifacts_)
+        artifacts.extend(artifacts_)
+        if len(metrics) > 1:
+            raise RuntimeError("Collected target metrics for multiple runs. Please aggregate them in a callback!")
+        assert len(metrics) == 1
+        metrics = metrics[0]
+        content = metrics.to_csv(include_optional=True)  # TODO: store df instead?
+        artifact = Artifact("metrics.csv", content=content, fmt=ArtifactFormat.TEXT)
+        # Alternative: artifact = Artifact("metrics.csv", data=df/dict, fmt=ArtifactFormat.DATA)
+        artifacts.append(artifact)
         stdout_artifact = Artifact(
             f"{self.name}_out.log", content=out, fmt=ArtifactFormat.TEXT
         )  # TODO: rename to tvmaot_out.log?
diff --git a/requirements.txt b/requirements.txt
index 98ba3c640..dbcd1bb0e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -72,3 +72,6 @@ itsdangerous<2.1
 
 # moiopt
 ortools
+
+# visualize tasks
+# graphviz  # Needs graphviz-dev APT pkg
diff --git a/resources/platforms/zephyr/project/CMakeLists.txt b/resources/platforms/zephyr/project/CMakeLists.txt
new file mode 100644
index 000000000..f4eceee10
--- /dev/null
+++ b/resources/platforms/zephyr/project/CMakeLists.txt
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: Apache-2.0
+
+cmake_minimum_required(VERSION 3.20.0)
+
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(mlonmcu_template)
+SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+
+target_sources(app PRIVATE src/main.c)
+
+INCLUDE(backends/${CONFIG_MLONMCU_BACKEND})
+INCLUDE(frameworks/${CONFIG_MLONMCU_FRAMEWORK})
+
+IF(CONFIG_TF_DIR)
+    INCLUDE(LinkTFLite)
+ENDIF()
+IF(CONFIG_TVM_DIR)
+    INCLUDE(LinkTVM)
+ENDIF()
+ADD_SUBDIRECTORY(lib)
+
+TARGET_LINK_LIBRARIES(app PUBLIC target_sw_lib)
diff --git a/resources/platforms/zephyr/project/Kconfig b/resources/platforms/zephyr/project/Kconfig
new file mode 100644
index 000000000..5363d49b7
--- /dev/null
+++ b/resources/platforms/zephyr/project/Kconfig
@@ -0,0 +1,101 @@
+mainmenu "MLonMCU"
+
+source "Kconfig.zephyr"
+menu "MLonMCU Options"
+
+    config MLONMCU_CODEGEN_DIR
+        string "Codegen Directory"
+        default "$(CODEGEN_SRC)"
+        help
+            Path to the directory with the codegen results.
+
+    # choice MLONMCU_FRAMEWORK
+    #     prompt "MLonMCU framework"
+    #     default MLONMCU_FRAMEWORK_TFLM
+    #     help
+    #         Choose a MLonMCU framework from the list.
+
+    #     config MLONMCU_FRAMEWORK_TFLM
+    #         bool "TFLite Micro"
+    #     config MLONMCU_FRAMEWORK_TVM
+    #         bool "TVM"
+    # endchoice
+
+    config MLONMCU_FRAMEWORK
+        string "Ued Framework"
+        # default "tflm" if MLONMCU_FRAMEWORK_TFLM
+        # default "tvm" if MLONMCU_FRAMEWORK_TVM
+        default "none"
+        help
+            TODO
+
+    # choice MLONMCU_BACKEND
+    #     prompt "MLonMCU backend"
+    #     default MLONMCU_BACKEND_TFLMI if MLONMCU_FRAMEWORK_TFLM
+    #     default MLONMCU_BACKEND_TVMAOT if MLONMCU_FRAMEWORK_TVM
+    #     help
+    #         Choose a MLonMCU backend for the chosen framwork.
+
+    #     config MLONMCU_BACKEND_TFLMI
+    #         bool "TFLite Micro Default Interpreter"
+    #         depends on MLONMCU_FRAMEWORK_TFLM
+    #     config MLONMCU_BACKEND_TFLMC
+    #         bool "TFLite Micro Compiler"
+    #         depends on MLONMCU_FRAMEWORK_TFLM
+    #     config MLONMCU_BACKEND_TVMAOT
+    #         bool "TVM AoT Executor"
+    #         depends on MLONMCU_FRAMEWORK_TVM
+    #     config MLONMCU_BACKEND_TVMRT
+    #         bool "TVM Graph Executor"
+    #         depends on MLONMCU_FRAMEWORK_TVM
+    #     config MLONMCU_BACKEND_TVMCG
+    #         bool "TVM Custom Codegenerator"
+    #         depends on MLONMCU_FRAMEWORK_TVM
+    # endchoice
+
+    config MLONMCU_BACKEND
+        string "Used backend"
+        default "none"
+        help
+            TODO
+
+    config MLONMCU_NUM_RUNS
+        int "Number of inferences"
+        default 1
+        help
+            How many times the network should be invoked. A negative value maps to a infinite number of runs.
+
+    config MLONMCU_VALIDATE
+        bool "Validate model output data"
+        default false
+        help
+            Compare model outputs with expected results (if available). Only works in DEBUG mode.
+
+    config MLONMCU_MODLE_DIR
+        string "Model Directory"
+        depends on MLONMCU_VALIDATE
+        default "$(MODEL_DIR)"
+        help
+            Directory where model support code and reference data can be found.
+endmenu
+
+menu "TVM Build Options"
+    config TVM_DIR
+        string "Path to TVM"
+        default "$(TVM_SRC)"
+        help
+            TVM directory path.
+    config TVM_CRT_CONFIG_DIR
+        string "Path to TVM CRT config directory"
+        default ""
+        help
+            TVM crt path.
+endmenu
+menu "TFLite Micro Build Options"
+
+    config TF_DIR
+        string "Path to TFLite Micro"
+        default "$(TF_SRC)"
+        help
+            TFLite Micro directory path.
+endmenu
diff --git a/resources/platforms/zephyr/project/README.rst b/resources/platforms/zephyr/project/README.rst
new file mode 100644
index 000000000..ce5423d6f
--- /dev/null
+++ b/resources/platforms/zephyr/project/README.rst
@@ -0,0 +1,33 @@
+.. _hello_world:
+
+Hello World
+###########
+
+Overview
+********
+
+A simple sample that can be used with any :ref:`supported board <boards>` and
+prints "Hello World" to the console.
+
+Building and Running
+********************
+
+This application can be built and executed on QEMU as follows:
+
+.. zephyr-app-commands::
+   :zephyr-app: samples/hello_world
+   :host-os: unix
+   :board: qemu_x86
+   :goals: run
+   :compact:
+
+To build for another board, change "qemu_x86" above to that board's name.
+
+Sample Output
+=============
+
+.. code-block:: console
+
+    Hello World! x86
+
+Exit QEMU by pressing :kbd:`CTRL+A` :kbd:`x`.
diff --git a/resources/platforms/zephyr/project/cmake/AddAllSubdirs.cmake b/resources/platforms/zephyr/project/cmake/AddAllSubdirs.cmake
new file mode 100644
index 000000000..e025203fc
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/AddAllSubdirs.cmake
@@ -0,0 +1,21 @@
+MACRO(AddAllSubdirs)
+    UNSET(LOCAL_SUBDIRS)
+    FILE(
+        GLOB LOCAL_SUBDIRS
+        RELATIVE ${CMAKE_CURRENT_LIST_DIR}
+        ${CMAKE_CURRENT_LIST_DIR}/*
+    )
+
+    FOREACH(subdir ${LOCAL_SUBDIRS})
+        IF(IS_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/${subdir})
+            IF(EXISTS ${CMAKE_CURRENT_LIST_DIR}/${subdir}/CMakeLists.txt)
+                ADD_SUBDIRECTORY(${CMAKE_CURRENT_LIST_DIR}/${subdir})
+                MESSAGE(STATUS "Including sub project ${subdir}.")
+            ELSE()
+                MESSAGE(
+                    WARNING "Directory ${subdir} is not added to the current build because it lacks a CMakeLists.txt"
+                )
+            ENDIF()
+        ENDIF()
+    ENDFOREACH()
+ENDMACRO()
diff --git a/resources/platforms/zephyr/project/cmake/ChooseTarget.cmake b/resources/platforms/zephyr/project/cmake/ChooseTarget.cmake
new file mode 100644
index 000000000..b03c65e38
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/ChooseTarget.cmake
@@ -0,0 +1,22 @@
+# New target architectures and systems should be added here Make sure to define the CMAKE_TOOLCHAIN_FILE for
+# cross-compilation
+
+# Default implementation of the macro points to the original ADD_LIBRARY function
+MACRO(COMMON_ADD_LIBRARY TARGET_NAME)
+    ADD_LIBRARY(${TARGET_NAME} ${ARGN})
+ENDMACRO()
+
+MACRO(COMMON_ADD_EXECUTABLE TARGET_NAME)
+    ADD_EXECUTABLE(${TARGET_NAME} ${ARGN})
+ENDMACRO()
+
+SET(TOOLCHAIN_FILE "")
+IF(TARGET_SYSTEM)
+    INCLUDE(targets/${TARGET_SYSTEM})
+    IF(TOOLCHAIN)
+        SET(TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/toolchains/${TOOLCHAIN}_${TARGET_SYSTEM}.cmake")
+        INCLUDE(${TOOLCHAIN_FILE})
+    ENDIF()
+ENDIF()
+
+# set(CMAKE_EXECUTABLE_SUFFIX_C .elf)
diff --git a/resources/platforms/zephyr/project/cmake/LinkTFLite.cmake b/resources/platforms/zephyr/project/cmake/LinkTFLite.cmake
new file mode 100644
index 000000000..20a91cd51
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/LinkTFLite.cmake
@@ -0,0 +1,223 @@
+SET(TF_DIR
+    "/data/work/code/tensorflow"
+    CACHE PATH "TensorFlow source directory"
+)
+
+SET(TFL_SRC ${CONFIG_TF_DIR}/tensorflow/lite)
+SET(TFLM_SRC ${TFL_SRC}/micro)
+SET(TFLD_SRC ${TFLM_SRC}/tools/make/downloads)
+
+SET(TFLM_EXTRA_KERNEL_LIBS "")
+SET(TFLM_EXTRA_KERNEL_INCS "")
+
+IF(CONFIG_TFLM_OPTIMIZED_KERNEL)
+    # Suboptimal but we do not want to hardcode every kernel which should be replaced...
+    FILE(GLOB TFLM_EXTRA_KERNEL_SRCS ${TFLM_SRC}/kernels/${CONFIG_TFLM_OPTIMIZED_KERNEL}/*.cc)
+    # LIST(APPEND TFLM_EXTRA_KERNEL_INCS ${TFLM_SRC}/kernels/${TFLM_OPTIMIZED_KERNEL}/)
+    STRING(TOUPPER "${CONFIG_TFLM_OPTIMIZED_KERNEL}" TFLM_OPTIMIZED_KERNEL_UPPER)
+ENDIF()
+
+IF(CONFIG_TFLM_OPTIMIZED_KERNEL_LIB)
+    LIST(APPEND TFLM_EXTRA_KERNEL_LIBS ${CONFIG_TFLM_OPTIMIZED_KERNEL_LIB})
+ENDIF()
+
+IF(CONFIG_TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR)
+    LIST(APPEND TFLM_EXTRA_KERNEL_INCS ${CONFIG_TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR})
+ENDIF()
+
+IF(CONFIG_TFLM_OPTIMIZED_KERNEL_DEPS)
+    LIST(APPEND TFLM_EXTRA_KERNEL_DEPS ${CONFIG_TFLM_OPTIMIZED_KERNEL_DEPS})
+ENDIF()
+
+SET(CUSTOM_QUANT_SRC ${TFL_SRC}/experimental/custom_quantization_util.cc)
+IF(EXISTS ${CUSTOM_QUANT_SRC})
+    SET(OPT_SRC ${CUSTOM_QUANT_SRC})
+ENDIF()
+
+SET(TFLM_REFERENCE_KERNEL_SRCS
+    ${TFLM_SRC}/kernels/softmax.cc
+    ${TFLM_SRC}/kernels/fully_connected.cc
+    ${TFLM_SRC}/kernels/pooling.cc
+    ${TFLM_SRC}/kernels/add.cc
+    ${TFLM_SRC}/kernels/mul.cc
+    ${TFLM_SRC}/kernels/conv.cc
+    ${TFLM_SRC}/kernels/depthwise_conv.cc
+    ${TFLM_SRC}/kernels/softmax.cc
+    ${TFLM_SRC}/kernels/fully_connected.cc
+    ${TFLM_SRC}/kernels/pooling.cc
+    ${TFLM_SRC}/kernels/add.cc
+    ${TFLM_SRC}/kernels/mul.cc
+    ${TFLM_SRC}/kernels/conv.cc
+    ${TFLM_SRC}/kernels/depthwise_conv.cc
+    ${TFLM_SRC}/kernels/logical.cc
+    ${TFLM_SRC}/kernels/logistic.cc
+    ${TFLM_SRC}/kernels/svdf.cc
+    ${TFLM_SRC}/kernels/concatenation.cc
+    ${TFLM_SRC}/kernels/ceil.cc
+    ${TFLM_SRC}/kernels/floor.cc
+    ${TFLM_SRC}/kernels/prelu.cc
+    ${TFLM_SRC}/kernels/neg.cc
+    ${TFLM_SRC}/kernels/elementwise.cc
+    ${TFLM_SRC}/kernels/maximum_minimum.cc
+    ${TFLM_SRC}/kernels/arg_min_max.cc
+    ${TFLM_SRC}/kernels/reshape.cc
+    ${TFLM_SRC}/kernels/comparisons.cc
+    ${TFLM_SRC}/kernels/round.cc
+    ${TFLM_SRC}/kernels/strided_slice.cc
+    ${TFLM_SRC}/kernels/pack.cc
+    ${TFLM_SRC}/kernels/pad.cc
+    ${TFLM_SRC}/kernels/split.cc
+    ${TFLM_SRC}/kernels/unpack.cc
+    ${TFLM_SRC}/kernels/quantize.cc
+    ${TFLM_SRC}/kernels/activations.cc
+    ${TFLM_SRC}/kernels/dequantize.cc
+    ${TFLM_SRC}/kernels/reduce.cc
+    ${TFLM_SRC}/kernels/sub.cc
+    ${TFLM_SRC}/kernels/resize_nearest_neighbor.cc
+    ${TFLM_SRC}/kernels/l2norm.cc
+    ${TFLM_SRC}/kernels/circular_buffer.cc
+    ${TFLM_SRC}/kernels/ethosu.cc
+    ${TFLM_SRC}/kernels/tanh.cc
+)
+
+# This files only exists in newer versions of TF
+IF(EXISTS ${TFLM_SRC}/kernels/conv_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/conv_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/depthwise_conv_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/depthwise_conv_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/fully_connected_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/fully_connected_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/quantize_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/quantize_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/softmax_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/softmax_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/svdf_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/svdf_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/pooling_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/pooling_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/add_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/add_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/dequantize_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/dequantize_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/detection_postprocess.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/detection_postprocess.cc)
+ENDIF()
+
+FOREACH(src ${TFLM_EXTRA_KERNEL_SRCS})
+    GET_FILENAME_COMPONENT(src_name ${src} NAME)
+    IF(${src_name} MATCHES ".*_test.*")
+        LIST(REMOVE_ITEM TFLM_EXTRA_KERNEL_SRCS ${src})
+    ELSE()
+        SET(src_path "${TFLM_SRC}/kernels/${src_name}")
+        LIST(FIND TFLM_REFERENCE_KERNEL_SRCS ${src_path} TFLM_KERNEL_SRCS_FOUND_INDEX)
+        IF(${TFLM_KERNEL_SRCS_FOUND_INDEX} GREATER_EQUAL 0)
+            MESSAGE(STATUS "Replacing TFLM version of ${src_name} by optimized variant...")
+            LIST(REMOVE_ITEM TFLM_REFERENCE_KERNEL_SRCS ${src_path})
+        ENDIF()
+    ENDIF()
+ENDFOREACH()
+
+# This files only exists in newer versions of TF
+IF(EXISTS ${TFL_SRC}/schema/schema_utils.cc)
+    LIST(APPEND OPT_SRC ${TFL_SRC}/schema/schema_utils.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/micro_context.cc)
+    LIST(APPEND OPT_SRC ${TFLM_SRC}/micro_context.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/micro_graph.cc)
+    LIST(APPEND OPT_SRC ${TFLM_SRC}/micro_graph.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/flatbuffer_utils.cc)
+    LIST(APPEND OPT_SRC ${TFLM_SRC}/flatbuffer_utils.cc)
+ENDIF()
+
+ADD_LIBRARY(
+    tflm STATIC
+    # Not really needed?
+    ${TFLM_SRC}/micro_error_reporter.cc
+    ${TFLM_SRC}/debug_log.cc
+    ${TFLM_SRC}/micro_string.cc
+    # For reporter->Report
+    ${CONFIG_TF_DIR}/tensorflow/lite/core/api/error_reporter.cc
+    # Kernels
+    ${TFLM_REFERENCE_KERNEL_SRCS}
+    ${TFLM_EXTRA_KERNEL_SRCS}
+    # Kernel deps
+    ${TFLM_SRC}/kernels/kernel_util.cc
+    ${TFLM_SRC}/all_ops_resolver.cc
+    ${TFLM_SRC}/micro_utils.cc
+    ${TFL_SRC}/kernels/internal/quantization_util.cc
+    ${TFL_SRC}/kernels/kernel_util.cc
+    ${TFLM_SRC}/micro_interpreter.cc
+    ${TFLM_SRC}/micro_allocator.cc
+    ${TFLM_SRC}/micro_allocation_info.cc
+    ${TFLM_SRC}/micro_resource_variable.cc
+    ${TFLM_SRC}/arena_allocator/simple_memory_allocator.cc
+    ${TFLM_SRC}/memory_helpers.cc
+    ${TFLM_SRC}/memory_planner/greedy_memory_planner.cc
+    ${TFL_SRC}/core/api/tensor_utils.cc
+    ${TFL_SRC}/core/api/flatbuffer_conversions.cc
+    ${TFL_SRC}/core/api/op_resolver.cc
+    ${TFL_SRC}/c/common.cc
+    ${OPT_SRC}
+)
+
+IF(TFLM_EXTRA_KERNEL_LIBS)
+    TARGET_LINK_LIBRARIES(tflm PUBLIC ${TFLM_EXTRA_KERNEL_LIBS})
+ENDIF()
+TARGET_LINK_LIBRARIES(tflm PUBLIC m)
+
+IF(TFLM_OPTIMIZED_KERNEL_DEPS)
+    ADD_DEPENDENCIES(tflm ${TFLM_OPTIMIZED_KERNEL_DEPS})
+ENDIF()
+
+# cmake-format: off
+TARGET_INCLUDE_DIRECTORIES(tflm PUBLIC
+    ${CONFIG_TF_DIR}
+    ${TFLD_SRC}/flatbuffers/include
+    ${TFLD_SRC}/gemmlowp
+    ${TFLD_SRC}/ruy
+    ${TFLM_EXTRA_KERNEL_INCS}
+)
+# cmake-format: on
+
+# cmake-format: off
+TARGET_COMPILE_DEFINITIONS(tflm PUBLIC
+    TF_LITE_USE_GLOBAL_CMATH_FUNCTIONS
+    TF_LITE_USE_GLOBAL_MAX
+    TF_LITE_USE_GLOBAL_MIN
+    TF_LITE_STATIC_MEMORY
+    TFLITE_EMULATE_FLOAT
+    "$<$<CONFIG:RELEASE>:TF_LITE_STRIP_ERROR_STRINGS>"
+    ${TFLM_OPTIMIZED_KERNEL_UPPER}
+)
+
+# Workaround for the following issue which does not envolve patching the tflite-micro codebase:
+
+# .../micro_error_reporter.cc: In function 'tflite::ErrorReporter* tflite::GetMicroErrorReporter()':
+# .../micro_error_reporter.cc:58:76: error: 'static void tflite::MicroErrorReporter::operator delete(void*)' is private within this context
+#    58 |     error_reporter_ = new (micro_error_reporter_buffer) MicroErrorReporter();
+#       |                                                                            ^
+# In file included from .../micro_error_reporter.h:21,
+#                  from .../micro_error_reporter.cc:16:
+# .../compatibility.h:27:8: note: declared private here
+#    27 |   void operator delete(void* p) {}
+#       |        ^~~~~~~~
+# .../micro_error_reporter.h:51:3: note: in expansion of macro 'TF_LITE_REMOVE_VIRTUAL_DELETE'
+#    51 |   TF_LITE_REMOVE_VIRTUAL_DELETE
+#
+# If disabling the exceptions has major disadvantages needs to be investigated.
+
+TARGET_COMPILE_OPTIONS(tflm PUBLIC
+    -fno-exceptions
+)
+# cmake-format: on
diff --git a/resources/platforms/zephyr/project/cmake/LinkTVM.cmake b/resources/platforms/zephyr/project/cmake/LinkTVM.cmake
new file mode 100644
index 000000000..d0b13ed21
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/LinkTVM.cmake
@@ -0,0 +1,68 @@
+SET(TVM_ALIGNMENT_BYTES 4)
+
+# cmake-format: off
+SET(TVM_HEADERS
+    ${CONFIG_TVM_DIR}/include
+    ${CONFIG_TVM_DIR}/3rdparty/dlpack/include
+    ${CONFIG_TVM_DIR}/apps/bundle_deploy
+    ${CONFIG_TVM_DIR}/src/runtime/crt/include
+)
+# cmake-format: on
+
+IF(CONFIG_TVM_CRT_CONFIG_DIR)
+    LIST(APPEND TVM_HEADERS ${CONFIG_TVM_CRT_CONFIG_DIR})
+ELSE()
+    LIST(APPEND TVM_HEADERS ${CONFIG_TVM_DIR}/apps/bundle_deploy/crt_config)
+ENDIF()
+
+# cmake-format: off
+ADD_LIBRARY(tvm_static_rt STATIC
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_runtime_api.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/memory/stack_allocator.c
+)
+# cmake-format: on
+
+TARGET_INCLUDE_DIRECTORIES(tvm_static_rt PUBLIC ${TVM_HEADERS})
+
+# cmake-format: off
+TARGET_COMPILE_DEFINITIONS(tvm_static_rt PUBLIC
+    -DTVM_RUNTIME_ALLOC_ALIGNMENT_BYTES=${TVM_ALIGNMENT_BYTES}
+)
+# cmake-format: on
+
+# cmake-format: off
+ADD_LIBRARY(tvm_graph_rt STATIC
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_backend_api.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_runtime_api.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/func_registry.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/memory/page_allocator.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/memory/stack_allocator.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/ndarray.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/packed_func.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/graph_executor/graph_executor.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/graph_executor/load_json.c
+)
+# cmake-format: on
+
+TARGET_INCLUDE_DIRECTORIES(tvm_graph_rt PUBLIC ${TVM_HEADERS})
+
+# cmake-format: off
+TARGET_COMPILE_DEFINITIONS(tvm_graph_rt PUBLIC
+    -DTVM_RUNTIME_ALLOC_ALIGNMENT_BYTES=${TVM_ALIGNMENT_BYTES}
+)
+# cmake-format: on
+
+# cmake-format: off
+ADD_LIBRARY(tvm_aot_rt STATIC
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_backend_api.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/memory/stack_allocator.c
+)
+# cmake-format: on
+
+TARGET_INCLUDE_DIRECTORIES(tvm_aot_rt PUBLIC ${TVM_HEADERS})
+
+# cmake-format: off
+TARGET_COMPILE_DEFINITIONS(tvm_aot_rt PUBLIC
+    -DTVM_RUNTIME_ALLOC_ALIGNMENT_BYTES=${TVM_ALIGNMENT_BYTES}
+)
+# cmake-format: on
diff --git a/resources/platforms/zephyr/project/cmake/LookupClang.cmake b/resources/platforms/zephyr/project/cmake/LookupClang.cmake
new file mode 100644
index 000000000..c5655ba18
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/LookupClang.cmake
@@ -0,0 +1,41 @@
+set(CLANG_VERSIONS 15 14)
+
+set(LLVM_DIR "" CACHE PATH "Lookup path for the llvm installation.")
+MESSAGE("LLVM_DIR=${LLVM_DIR}")
+
+# Find clang
+function(do_lookup program out)
+    set(VERSION_NAMES "")
+    foreach(version ${CLANG_VERSIONS})
+        set(NEW_NAME "${program}-${version}")
+        list(APPEND VERSION_NAMES ${NEW_NAME})
+    endforeach()
+    # Custom path, explicit versions
+    find_program(${program}_NAME_MATCH NAMES ${VERSION_NAMES} PATHS ${LLVM_DIR}/bin/ NO_DEFAULT_PATH)
+    IF(NOT ${program}_NAME_MATCH)
+        # System path, explicit versions
+        find_program(${program}_NAME_MATCH NAMES ${VERSION_NAMES})
+        IF(NOT ${program}_NAME_MATCH)
+            # Custom path, implicit version
+            find_program(${program}_NAME_MATCH NAMES ${program} PATHS ${LLVM_DIR}/bin/ NO_DEFAULT_PATH)
+            IF(NOT ${program}_NAME_MATCH)
+                # System path, implicit version
+                find_program(${program}_NAME_MATCH NAMES ${program})
+            ENDIF()
+        ENDIF()
+    ENDIF()
+    IF(NOT ${program}_NAME_MATCH)
+        MESSAGE(FATAL_ERROR "Unable to find ${program} executable")
+    ENDIF()
+    MESSAGE(STATUS "${program}_NAME_MATCH=${${program}_NAME_MATCH}")
+    set(${out} ${${program}_NAME_MATCH} PARENT_SCOPE)
+endfunction()
+
+
+do_lookup(clang CLANG_EXECUTABLE)
+do_lookup(clang++ CLANG++_EXECUTABLE)
+do_lookup(lld LLD_EXECUTABLE)
+
+MESSAGE(STATUS "CLANG_EXECUTABLE=${CLANG_EXECUTABLE}")
+MESSAGE(STATUS "CLANG++_EXECUTABLE=${CLANG++_EXECUTABLE}")
+MESSAGE(STATUS "LLD_EXECUTABLE=${LLD_EXECUTABLE}")
diff --git a/resources/platforms/zephyr/project/cmake/backends/tflmc.cmake b/resources/platforms/zephyr/project/cmake/backends/tflmc.cmake
new file mode 100644
index 000000000..5f08f021b
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/backends/tflmc.cmake
@@ -0,0 +1,5 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tflm")
+ENDIF()
+
+SET(TFLITE_REQUIRED_COMPONENTS a b c)
diff --git a/resources/platforms/zephyr/project/cmake/backends/tflmi.cmake b/resources/platforms/zephyr/project/cmake/backends/tflmi.cmake
new file mode 100644
index 000000000..5f08f021b
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/backends/tflmi.cmake
@@ -0,0 +1,5 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tflm")
+ENDIF()
+
+SET(TFLITE_REQUIRED_COMPONENTS a b c)
diff --git a/resources/platforms/zephyr/project/cmake/backends/tvmaot.cmake b/resources/platforms/zephyr/project/cmake/backends/tvmaot.cmake
new file mode 100644
index 000000000..c14f83de3
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/backends/tvmaot.cmake
@@ -0,0 +1,10 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DTVMCG_DEBUG_ALLOCATIONS")
+
+SET(TVM_LIB tvm_aot_rt)
+SET(TVM_WRAPPER_FILENAME aot_wrapper.c)
diff --git a/resources/platforms/zephyr/project/cmake/backends/tvmaotplus.cmake b/resources/platforms/zephyr/project/cmake/backends/tvmaotplus.cmake
new file mode 100644
index 000000000..c14f83de3
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/backends/tvmaotplus.cmake
@@ -0,0 +1,10 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DTVMCG_DEBUG_ALLOCATIONS")
+
+SET(TVM_LIB tvm_aot_rt)
+SET(TVM_WRAPPER_FILENAME aot_wrapper.c)
diff --git a/resources/platforms/zephyr/project/cmake/backends/tvmcg.cmake b/resources/platforms/zephyr/project/cmake/backends/tvmcg.cmake
new file mode 100644
index 000000000..509174438
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/backends/tvmcg.cmake
@@ -0,0 +1,10 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DTVMAOT_DEBUG_ALLOCATIONS")
+
+SET(TVM_LIB tvm_static_rt)
+SET(TVM_WRAPPER_FILENAME staticrt.c)
diff --git a/resources/platforms/zephyr/project/cmake/backends/tvmllvm.cmake b/resources/platforms/zephyr/project/cmake/backends/tvmllvm.cmake
new file mode 100644
index 000000000..faff7adda
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/backends/tvmllvm.cmake
@@ -0,0 +1,8 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(TVM_LIB tvm_graph_rt)
+SET(TVM_WRAPPER_FILENAME rt_wrapper.c)
diff --git a/resources/platforms/zephyr/project/cmake/backends/tvmrt.cmake b/resources/platforms/zephyr/project/cmake/backends/tvmrt.cmake
new file mode 100644
index 000000000..faff7adda
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/backends/tvmrt.cmake
@@ -0,0 +1,8 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(TVM_LIB tvm_graph_rt)
+SET(TVM_WRAPPER_FILENAME rt_wrapper.c)
diff --git a/resources/platforms/zephyr/project/cmake/extras.cmake b/resources/platforms/zephyr/project/cmake/extras.cmake
new file mode 100644
index 000000000..c4ce8e0bf
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/extras.cmake
@@ -0,0 +1,9 @@
+IF(DEFINED DEBUG_ARENA_USAGE)
+    IF(DEBUG_ARENA_USAGE)
+        SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DDEBUG_ARENA_USAGE=1")
+        SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG_ARENA_USAGE=1")
+    ELSE()
+        SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DDEBUG_ARENA_USAGE=0")
+        SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG_ARENA_USAGE=0")
+    ENDIF()
+ENDIF()
diff --git a/resources/platforms/zephyr/project/cmake/features/auto_vectorize_gcc.cmake b/resources/platforms/zephyr/project/cmake/features/auto_vectorize_gcc.cmake
new file mode 100644
index 000000000..737bc9879
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/features/auto_vectorize_gcc.cmake
@@ -0,0 +1,65 @@
+IF(NOT RISCV_VEXT)
+    MESSAGE(FATAL_ERROR "RISCV_AUTO_VECTORIZE requires RISCV_VEXT")
+ENDIF()
+IF(RISCV_RVV_VLEN)
+    SET(VLEN ${RISCV_RVV_VLEN})
+ELSE()
+    SET(VLEN "?")
+ENDIF()
+SET(CMAKE_CXX_FLAGS_RELEASE
+    "${CMAKE_CXX_FLAGS_RELEASE} \
+    -ftree-vectorize \
+    -mriscv-vector-bits=${VLEN} \
+"
+)
+SET(CMAKE_C_FLAGS_RELEASE
+    "${CMAKE_C_FLAGS_RELEASE} \
+    -ftree-vectorize \
+    -mriscv-vector-bits=${VLEN} \
+"
+)
+IF(DEFINED RISCV_AUTO_VECTORIZE_LOOP)
+    IF(NOT RISCV_AUTO_VECTORIZE_LOOP)
+        SET(CMAKE_CXX_FLAGS_RELEASE
+            "${CMAKE_CXX_FLAGS_RELEASE} \
+            -fno-tree-loop-vectorize \
+        "
+        )
+        SET(CMAKE_C_FLAGS_RELEASE
+            "${CMAKE_C_FLAGS_RELEASE} \
+            -fno-tree-loop-vectorize \
+        "
+        )
+    ENDIF()
+ENDIF()
+IF(DEFINED RISCV_AUTO_VECTORIZE_SLP)
+    IF(NOT RISCV_AUTO_VECTORIZE_SLP)
+        SET(CMAKE_CXX_FLAGS_RELEASE
+            "${CMAKE_CXX_FLAGS_RELEASE} \
+            -fno-tree-slp-vectorize \
+        "
+        )
+        SET(CMAKE_C_FLAGS_RELEASE
+            "${CMAKE_C_FLAGS_RELEASE} \
+            -fno-tree-slp-vectorize \
+        "
+        )
+    ENDIF()
+ENDIF()
+# Also interesting:
+# -mriscv-vector-lmul=<lmul>  Set the vf using lmul in auto-vectorization
+# -fsimd-cost-model=[unlimited|dynamic|cheap|very-cheap] Specifies the vectorization cost model for code marked with a simd directive
+IF(RISCV_AUTO_VECTORIZE_VERBOSE)
+    SET(CMAKE_CXX_FLAGS_RELEASE
+        "${CMAKE_CXX_FLAGS_RELEASE} \
+        -fopt-info-vec \
+        -fopt-info-vec-missed \
+    "
+    )
+    SET(CMAKE_C_FLAGS_RELEASE
+        "${CMAKE_C_FLAGS_RELEASE} \
+        -fopt-info-vec \
+        -fopt-info-vec-missed \
+    "
+    )
+ENDIF()
diff --git a/resources/platforms/zephyr/project/cmake/features/auto_vectorize_llvm.cmake b/resources/platforms/zephyr/project/cmake/features/auto_vectorize_llvm.cmake
new file mode 100644
index 000000000..9cbd1330a
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/features/auto_vectorize_llvm.cmake
@@ -0,0 +1,20 @@
+IF(NOT RISCV_VEXT)
+    MESSAGE(FATAL_ERROR "RISCV_AUTO_VECTORIZE requires RISCV_VEXT")
+ENDIF()
+IF(RISCV_RVV_VLEN)
+    SET(VLEN ${RISCV_RVV_VLEN})
+ELSE()
+    SET(VLEN "?")
+ENDIF()
+ SET(CMAKE_CXX_FLAGS_RELEASE
+     "${CMAKE_CXX_FLAGS_RELEASE} \
+     -mllvm \
+     --riscv-v-vector-bits-min=${VLEN} \
+ "
+ )
+ SET(CMAKE_C_FLAGS_RELEASE
+     "${CMAKE_C_FLAGS_RELEASE} \
+     -mllvm \
+     --riscv-v-vector-bits-min=${VLEN} \
+ "
+ )
diff --git a/resources/platforms/zephyr/project/cmake/features/cmsisnn.cmake b/resources/platforms/zephyr/project/cmake/features/cmsisnn.cmake
new file mode 100644
index 000000000..06c92dd02
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/features/cmsisnn.cmake
@@ -0,0 +1,86 @@
+IF(NOT CMSISNN_DIR)
+    MESSAGE(FATAL_ERROR "Missing value: CMSISNN_DIR")
+ENDIF()
+
+IF(NOT ARM_MVEI)
+    SET(USE_MVEI OFF)
+ELSE()
+    SET(USE_MVEI ON)
+ENDIF()
+
+IF(NOT ARM_DSP)
+    SET(USE_DSP OFF)
+ELSE()
+    SET(USE_DSP ON)
+ENDIF()
+
+SET(BUILD_FLAGS "")
+# IF(USE_MVEI) SET(BUILD_FLAGS "${BUILD_FLAGS} -D__ARM_FEATURE_MVE=1 -DARM_MATH_MVEI=1") ENDIF() IF(USE_DSP)
+# SET(BUILD_FLAGS "${BUILD_FLAGS} -D__ARM_FEATURE_DSP=1 -DARM_MATH_DSP=1") ENDIF()
+
+SET(CMSISNN_INCLUDE_DIRS ${CMSISNN_DIR} ${CMSISNN_DIR}/CMSIS/Core/Include ${CMSISNN_DIR}/CMSIS/NN/Include
+                         ${CMSISNN_DIR}/CMSIS/DSP/Include
+)
+
+# TODO: propagarting all toolchain specific vars does not scale well
+
+MESSAGE(STATUS "ARM_CPU=${ARM_CPU}")
+INCLUDE(ExternalProject)
+EXTERNALPROJECT_ADD(
+    cmsisnn
+    PREFIX cmsisnn
+    SOURCE_DIR ${CMSISNN_DIR}/CMSIS/NN/
+    CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+               -DCMAKE_C_FLAGS:STRING=${BUILD_FLAGS}
+               -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_FILE}
+               -DTC_PREFIX=${TC_PREFIX}
+               -DEXE_EXT=${EXE_EXT}
+               -DARM_CPU=${ARM_CPU}
+               -DARM_FLOAT_ABI=${ARM_FLOAT_ABI}
+               -DARM_FPU=${ARM_FPU}
+               -DRISCV_ARCH=${RISCV_ARCH}
+               -DRISCV_ABI=${RISCV_ABI}
+    BUILD_COMMAND "${CMAKE_COMMAND}" --build .
+    INSTALL_COMMAND ""
+)
+
+EXTERNALPROJECT_GET_PROPERTY(cmsisnn BINARY_DIR)
+SET(CMSISNN_LIB ${BINARY_DIR}/Source/libcmsis-nn.a)
+
+# TFLite integration
+IF(TFLM_OPTIMIZED_KERNEL_LIB)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_LIB ${CMSISNN_LIB})
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_LIB ${CMSISNN_LIB})
+ENDIF()
+
+IF(TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR ${CMSISNN_INCLUDE_DIRS})
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR ${CMSISNN_INCLUDE_DIRS})
+ENDIF()
+
+IF(TFLM_OPTIMIZED_KERNEL_DEPS)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_DEPS cmsisnn)
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_DEPS cmsisnn)
+ENDIF()
+
+# TVM integration
+IF(TVM_EXTRA_LIBS)
+    LIST(APPEND TVM_EXTRA_LIBS ${CMSISNN_LIB})
+ELSE()
+    SET(TVM_EXTRA_LIBS ${CMSISNN_LIB})
+ENDIF()
+
+IF(TVM_EXTRA_INCS)
+    LIST(APPEND TVM_EXTRA_INCS ${CMSISNN_INCLUDE_DIRS})
+ELSE()
+    SET(TVM_EXTRA_INCS ${CMSISNN_INCLUDE_DIRS})
+ENDIF()
+
+IF(TVM_EXTRA_DEPS)
+    LIST(APPEND TVM_EXTRA_DEPS cmsisnn)
+ELSE()
+    SET(TVM_EXTRA_DEPS cmsisnn)
+ENDIF()
diff --git a/resources/platforms/zephyr/project/cmake/features/muriscvnn.cmake b/resources/platforms/zephyr/project/cmake/features/muriscvnn.cmake
new file mode 100644
index 000000000..6f84489a4
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/features/muriscvnn.cmake
@@ -0,0 +1,87 @@
+IF(NOT MURISCVNN_DIR)
+    MESSAGE(FATAL_ERROR "Missing value: MURISCVNN_DIR")
+ENDIF()
+
+IF(NOT RISCV_VEXT)
+    SET(USE_VEXT OFF)
+ELSE()
+    SET(USE_VEXT ON)
+ENDIF()
+
+IF(NOT RISCV_PEXT)
+    SET(USE_PEXT OFF)
+ELSE()
+    SET(USE_PEXT ON)
+ENDIF()
+
+IF(NOT MURISCVNN_TOOLCHAIN)
+    SET(MURISCVVN_TOOLCHAIN GCC)
+    # SET(MURISCVVN_TOOLCHAIN NONE)
+ENDIF()
+
+SET(MURISCVNN_INCLUDE_DIRS ${MURISCVNN_DIR}/Include ${MURISCVNN_DIR}/Include/CMSIS/NN/Include)
+
+# TODO: propagarting all toolchain specific vars does not scale well
+
+INCLUDE(ExternalProject)
+EXTERNALPROJECT_ADD(
+    muriscvnn
+    PREFIX muriscvnn
+    SOURCE_DIR ${MURISCVNN_DIR}
+    CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+               -DUSE_VEXT=${USE_VEXT}
+               -DUSE_PEXT=${USE_PEXT}
+               -DTOOLCHAIN=${MURISCVNN_TOOLCHAIN}
+               -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_FILE}
+               -DENABLE_TESTS=OFF
+               -DTC_PREFIX=${TC_PREFIX}
+               -DEXE_EXT=${EXE_EXT}
+               -DRISCV_ARCH=${RISCV_ARCH}
+               -DRISCV_ABI=${RISCV_ABI}
+               -DARM_CPU=${ARM_CPU}
+               -DARM_FLOAT_ABI=${ARM_FLOAT_ABI}
+               -DARM_FPU=${ARM_FPU}
+    BUILD_COMMAND "${CMAKE_COMMAND}" --build .
+    INSTALL_COMMAND ""
+)
+
+EXTERNALPROJECT_GET_PROPERTY(muriscvnn BINARY_DIR)
+SET(MURISCVNN_LIB ${BINARY_DIR}/Source/libmuriscv_nn.a)
+
+# TFLite integration
+IF(TFLM_OPTIMIZED_KERNEL_LIB)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_LIB ${MURISCVNN_LIB})
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_LIB ${MURISCVNN_LIB})
+ENDIF()
+
+IF(TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR ${MURISCVNN_INCLUDE_DIRS})
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR ${MURISCVNN_INCLUDE_DIRS})
+ENDIF()
+
+IF(TFLM_OPTIMIZED_KERNEL_DEPS)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_DEPS muriscvnn)
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_DEPS muriscvnn)
+ENDIF()
+
+# TVM integration
+IF(TVM_EXTRA_LIBS)
+    LIST(APPEND TVM_EXTRA_LIBS ${MURISCVNN_LIB})
+ELSE()
+    SET(TVM_EXTRA_LIBS ${MURISCVNN_LIB})
+ENDIF()
+
+IF(TVM_EXTRA_INCS)
+    LIST(APPEND TVM_EXTRA_INCS ${MURISCVNN_INCLUDE_DIRS})
+ELSE()
+    SET(TVM_EXTRA_INCS ${MURISCVNN_INCLUDE_DIRS})
+ENDIF()
+
+IF(TVM_EXTRA_DEPS)
+    LIST(APPEND TVM_EXTRA_DEPS muriscvnn)
+ELSE()
+    SET(TVM_EXTRA_DEPS muriscvnn)
+ENDIF()
diff --git a/resources/platforms/zephyr/project/cmake/frameworks/tflm.cmake b/resources/platforms/zephyr/project/cmake/frameworks/tflm.cmake
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/frameworks/tflm.cmake
@@ -0,0 +1,1 @@
+
diff --git a/resources/platforms/zephyr/project/cmake/frameworks/tflm_mlif.cmake b/resources/platforms/zephyr/project/cmake/frameworks/tflm_mlif.cmake
new file mode 100644
index 000000000..614226b6a
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/frameworks/tflm_mlif.cmake
@@ -0,0 +1,8 @@
+SET(TF_OUT_DIR ${CONFIG_MLONMCU_CODEGEN_DIR}/)
+SET(GEN_SRC ${TF_OUT_DIR}/model.cc)
+GET_FILENAME_COMPONENT(GEN_SRC ${GEN_SRC} REALPATH)
+GET_FILENAME_COMPONENT(EXTRA_INC ${GEN_SRC} DIRECTORY)
+SET(EXTRA_SRC ml_interface_tflm.cc ${GEN_SRC})
+SET(EXTRA_LIBS tflm)
+SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DTFLMC_DEBUG_ALLOCATIONS")
+LIST(APPEND EXTRA_LIBS ${CUSTOM_OPS_LIBS})
diff --git a/resources/platforms/zephyr/project/cmake/frameworks/tvm.cmake b/resources/platforms/zephyr/project/cmake/frameworks/tvm.cmake
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/frameworks/tvm.cmake
@@ -0,0 +1,1 @@
+
diff --git a/resources/platforms/zephyr/project/cmake/frameworks/tvm_mlif.cmake b/resources/platforms/zephyr/project/cmake/frameworks/tvm_mlif.cmake
new file mode 100644
index 000000000..45e818360
--- /dev/null
+++ b/resources/platforms/zephyr/project/cmake/frameworks/tvm_mlif.cmake
@@ -0,0 +1,37 @@
+SET(TVM_OUT_DIR ${CONFIG_MLONMCU_CODEGEN_DIR}/codegen/host/)
+SET(EXTRA_SRC ml_interface_tvm.c)
+
+FILE(GLOB TVM_SRCS ${TVM_OUT_DIR}/src/*_lib*.c ${TVM_OUT_DIR}/src/*_lib*.cc)
+
+IF(NOT TVM_SRCS)
+    MESSAGE(STATUS "NO SOURCES")
+    FILE(GLOB TVM_OBJS ${TVM_OUT_DIR}/lib/*_lib*.o)
+    MESSAGE(STATUS "TVM_OBJS=${TVM_OBJS}")
+    ADD_LIBRARY(tvm_extension OBJECT IMPORTED)
+
+    SET_PROPERTY(TARGET tvm_extension PROPERTY
+        IMPORTED_OBJECTS ${TVM_OBJS}
+    )
+ELSE()
+    # Need this in extra target to avoid circular dependency .
+    ADD_LIBRARY(tvm_extension STATIC ${TVM_SRCS})
+    TARGET_INCLUDE_DIRECTORIES(tvm_extension PUBLIC ${TVM_HEADERS} ${TVM_OUT_DIR}/include ${CONFIG_MLONMCU_CODEGEN_DIR})
+    TARGET_LINK_LIBRARIES(tvm_extension PUBLIC m)
+    TARGET_LINK_LIBRARIES(tvm_extension PUBLIC ${TVM_LIB})
+ENDIF()
+
+SET(EXTRA_SRC ${EXTRA_SRC} ${CONFIG_MLONMCU_CODEGEN_DIR}/${TVM_WRAPPER_FILENAME})
+TARGET_LINK_LIBRARIES(${TVM_LIB} PUBLIC tvm_extension)
+SET(EXTRA_INC ${TVM_OUT_DIR}/include ${CONFIG_MLONMCU_CODEGEN_DIR})
+
+SET(EXTRA_LIBS tvm_extension ${TVM_LIB})
+
+FOREACH(ENTRY ${CONFIG_TVM_EXTRA_LIBS})
+    TARGET_LINK_LIBRARIES(tvm_extension PUBLIC ${ENTRY})
+ENDFOREACH()
+FOREACH(ENTRY ${CONFIG_TVM_EXTRA_INCS})
+    TARGET_INCLUDE_DIRECTORIES(tvm_extension PUBLIC ${ENTRY})
+ENDFOREACH()
+FOREACH(ENTRY ${CONFIG_TVM_EXTRA_DEPS})
+    ADD_DEPENDENCIES(tvm_extension ${ENTRY})
+ENDFOREACH()
diff --git a/resources/platforms/zephyr/project/lib/CMakeLists.txt b/resources/platforms/zephyr/project/lib/CMakeLists.txt
new file mode 100644
index 000000000..2cbc11ae3
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/CMakeLists.txt
@@ -0,0 +1,13 @@
+PROJECT(target_sw_lib)
+
+ADD_SUBDIRECTORY(support)
+ADD_SUBDIRECTORY(ml_interface)
+
+SET(TARGET_LIB_SOURCE targets/${TARGET_SYSTEM}.c)
+IF(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_LIB_SOURCE})
+    SET(TARGET_LIB_SOURCE targets/default.c)
+ENDIF()
+
+ADD_LIBRARY(${PROJECT_NAME} STATIC drivers/fake_input.c ${TARGET_LIB_SOURCE})
+TARGET_INCLUDE_DIRECTORIES(${PROJECT_NAME} PUBLIC lib/drivers lib/targets)
+TARGET_LINK_LIBRARIES(${PROJECT_NAME} PUBLIC support ml_interface)
diff --git a/resources/platforms/zephyr/project/lib/drivers/fake_input.c b/resources/platforms/zephyr/project/lib/drivers/fake_input.c
new file mode 100644
index 000000000..164127a67
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/drivers/fake_input.c
@@ -0,0 +1,10 @@
+#include "fake_input.h"
+#include <stdint.h>
+
+void FakeInput_GetData(void *out, size_t len) {
+  volatile uint8_t *p = 0xc0000000;
+
+  for (size_t i = 0; i < len; i++) {
+    *(uint8_t *)out = *(p + i);
+  }
+}
diff --git a/resources/platforms/zephyr/project/lib/drivers/fake_input.h b/resources/platforms/zephyr/project/lib/drivers/fake_input.h
new file mode 100644
index 000000000..04e47811a
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/drivers/fake_input.h
@@ -0,0 +1,8 @@
+#ifndef DRIVER_FAKE_INPUT_H
+#define DRIVER_FAKE_INPUT_H
+
+#include <stddef.h>
+
+void FakeInput_GetData(void *out, size_t len);
+
+#endif
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/CMakeLists.txt b/resources/platforms/zephyr/project/lib/ml_interface/CMakeLists.txt
new file mode 100644
index 000000000..5d7d90e63
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/CMakeLists.txt
@@ -0,0 +1,31 @@
+PROJECT(ml_interface)
+
+INCLUDE(extras)
+
+INCLUDE(frameworks/${CONFIG_MLONMCU_FRAMEWORK}_mlif)
+
+IF(CONFIG_MODEL_DIR)
+    GET_FILENAME_COMPONENT(MODEL_SUPPORT_DIR ${CONFIG_MODEL_DIR}/support REALPATH)
+    GET_FILENAME_COMPONENT(MODEL_NAME ${CONFIG_MODEL_DIR} NAME)
+ENDIF()
+
+IF(MODEL_SUPPORT_DIR AND EXISTS ${MODEL_SUPPORT_DIR})
+    ADD_SUBDIRECTORY(${MODEL_SUPPORT_DIR} model_support_bin_${MODEL_NAME})
+ELSE()
+    ADD_SUBDIRECTORY(default_model_support)
+ENDIF()
+
+IF(NOT CONFIG_DATA_SRC)
+    SET(CONFIG_DATA_SRC "default_data.c")
+ENDIF()
+
+ADD_LIBRARY(${PROJECT_NAME}_base STATIC ${CONFIG_DATA_SRC})
+TARGET_LINK_LIBRARIES(${PROJECT_NAME}_base PUBLIC support ${EXTRA_LIBS})
+TARGET_INCLUDE_DIRECTORIES(${PROJECT_NAME}_base PUBLIC . ${EXTRA_INC})
+
+ADD_LIBRARY(${PROJECT_NAME} STATIC ml_interface.c ${EXTRA_SRC})
+TARGET_LINK_LIBRARIES(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_base model_support)
+TARGET_LINK_LIBRARIES(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_base model_support zephyr)
+IF(CONFIG_NUM_RUNS)
+    TARGET_COMPILE_DEFINITIONS(${PROJECT_NAME} PUBLIC NUM_RUNS=${CONFIG_NUM_RUNS})
+ENDIF()
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/default_data.c b/resources/platforms/zephyr/project/lib/ml_interface/default_data.c
new file mode 100644
index 000000000..933b986ba
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/default_data.c
@@ -0,0 +1,8 @@
+#include "ml_interface.h"
+#include <stddef.h>
+const int num_data_buffers_in = 0;
+const int num_data_buffers_out = 0;
+const unsigned char *const data_buffers_in[] = {};
+const unsigned char *const data_buffers_out[] = {};
+const size_t data_size_in[] = {};
+const size_t data_size_out[] = {};
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/CMakeLists.txt b/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/CMakeLists.txt
new file mode 100644
index 000000000..ea88d5e34
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/CMakeLists.txt
@@ -0,0 +1,2 @@
+ADD_LIBRARY(model_support STATIC process_input.c process_output.c)
+TARGET_LINK_LIBRARIES(model_support PUBLIC ml_interface_base)
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/process_input.c b/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/process_input.c
new file mode 100644
index 000000000..57b180bc7
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/process_input.c
@@ -0,0 +1,17 @@
+#include <stdlib.h>
+#include <memory.h>
+
+#include "printing.h"
+#include "exit.h"
+
+void mlif_process_input(const void *in_data, size_t in_size, void *model_input_ptr, size_t model_input_sz) {
+  if (in_size != 0) {
+    if (in_size != model_input_sz) {
+      DBGPRINTF("MLIF: Given input size (%lu) does not match model input buffer size (%lu)!\n", in_size,
+                model_input_sz);
+      exit(EXIT_MLIF_INVALID_SIZE);
+    }
+  }
+
+  memcpy(model_input_ptr, in_data, in_size);
+}
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/process_output.c b/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/process_output.c
new file mode 100644
index 000000000..431d340f4
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/default_model_support/process_output.c
@@ -0,0 +1,27 @@
+#include <stdlib.h>
+#include <memory.h>
+
+#include "printing.h"
+#include "exit.h"
+
+void mlif_process_output(void *model_output_ptr, size_t model_output_sz, const void *expected_out_data,
+                         size_t expected_out_size) {
+#ifdef _DEBUG
+  if (model_output_sz >= 4) {
+    DBGPRINTF("MLIF: First float of output: %f\n", *(float *)model_output_ptr);
+  }
+  DBGPRINTF("MLIF: Model output data: ");
+  for (size_t i = 0; i < model_output_sz; i++) {
+    DBGPRINTF("\\x%02X", ((unsigned char *)model_output_ptr)[i]);
+    fflush(0);
+  }
+  DBGPRINTF("\n");
+#endif
+
+  if (model_output_sz == expected_out_size && memcmp(model_output_ptr, expected_out_data, model_output_sz) == 0) {
+    DBGPRINTF("MLIF: Output data matches expected data\n");
+  } else {
+    DBGPRINTF("MLIF: Wrong output data!\n");
+    exit(EXIT_MLIF_MISSMATCH);
+  }
+}
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/ml_interface.c b/resources/platforms/zephyr/project/lib/ml_interface/ml_interface.c
new file mode 100644
index 000000000..c572bf192
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/ml_interface.c
@@ -0,0 +1,36 @@
+#include "ml_interface.h"
+#include "printing.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+__attribute__((weak)) bool mlif_request_input(void *model_input_ptr, size_t model_input_sz) {
+  static int num_done = 0;
+  if (num_done == num_data_buffers_in) {
+    static bool run_once = true;
+    if (num_data_buffers_in == 0 && run_once) {
+      // Minimal run. Just run the model without data.
+      run_once = false;
+      return true;
+    }
+    return false;
+  }
+
+  mlif_process_input(data_buffers_in[num_done], data_size_in[num_done], model_input_ptr, model_input_sz);
+  num_done++;
+  return true;
+}
+
+__attribute__((weak)) void mlif_handle_result(void *model_output_ptr, size_t model_output_sz) {
+  static int num_done = 0;
+
+  if (num_data_buffers_out == 0) {
+    return;
+  }
+
+  if (num_done < num_data_buffers_out) {
+    mlif_process_output(model_output_ptr, model_output_sz, data_buffers_out[num_done], data_size_out[num_done]);
+  }
+
+  num_done++;
+}
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/ml_interface.h b/resources/platforms/zephyr/project/lib/ml_interface/ml_interface.h
new file mode 100644
index 000000000..dab3441c0
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/ml_interface.h
@@ -0,0 +1,47 @@
+#ifndef TARGETLIB_ML_INTERFACE_H
+#define TARGETLIB_ML_INTERFACE_H
+
+#include <stddef.h>
+#include <stdbool.h>
+
+#ifndef NUM_RUNS
+#define NUM_RUNS 1
+#endif /* NUM_RUNS */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// This runs the ML model using the callbacks above.
+// The default implementation will run with garbage data.
+void mlif_run();
+
+// These can be overridden by use code.
+
+// Provides input data for the model. The default implementation retrieves input from
+// the global variables below and fills the model input with mlif_process_input.
+bool mlif_request_input(void *model_input_ptr, size_t model_input_sz);
+// Is called when the output data is available. The default implementation
+void mlif_handle_result(void *model_output_ptr, size_t model_output_sz);
+
+// Callback for any preprocessing on the input data. Responsible for copying the data.
+void mlif_process_input(const void *in_data, size_t in_size, void *model_input_ptr, size_t model_input_sz);
+// Callback for any postprocessing on the output data. The default implementation prints
+// the output and verifies consistency with the expected output.
+void mlif_process_output(void *model_output_ptr, size_t model_output_sz, const void *expected_out_data,
+                         size_t expected_out_size);
+
+extern const int num_data_buffers_in;
+extern const int num_data_buffers_out;
+extern const unsigned char *const data_buffers_in[];
+extern const unsigned char *const data_buffers_out[];
+extern const size_t data_size_in[];
+extern const size_t data_size_out[];
+
+extern const int num;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/ml_interface_tflm.cc b/resources/platforms/zephyr/project/lib/ml_interface/ml_interface_tflm.cc
new file mode 100644
index 000000000..180552bea
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/ml_interface_tflm.cc
@@ -0,0 +1,22 @@
+#include "ml_interface.h"
+
+#include "model.cc.h"
+
+void mlif_run() {
+  model_init();
+
+  size_t input_num = 0;
+  size_t remaining = NUM_RUNS;
+  while (mlif_request_input(model_input_ptr(input_num), model_input_size(input_num)) || remaining) {
+    if (input_num == model_inputs() - 1) {
+      model_invoke();
+      for (size_t i = 0; i < model_outputs(); i++) {
+        mlif_handle_result(model_output_ptr(i), model_output_size(i));
+      }
+      input_num = 0;
+      remaining = remaining > 0 ? remaining - 1 : 0;
+    } else {
+      input_num++;
+    }
+  }
+}
diff --git a/resources/platforms/zephyr/project/lib/ml_interface/ml_interface_tvm.c b/resources/platforms/zephyr/project/lib/ml_interface/ml_interface_tvm.c
new file mode 100644
index 000000000..61ca8d2c8
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/ml_interface/ml_interface_tvm.c
@@ -0,0 +1,22 @@
+#include "ml_interface.h"
+#include "tvm_wrapper.h"
+
+void mlif_run() {
+  TVMWrap_Init();
+
+  size_t input_num = 0;
+  size_t remaining = NUM_RUNS;
+  // inout data will only be applied in the first run!
+  while (mlif_request_input(TVMWrap_GetInputPtr(input_num), TVMWrap_GetInputSize(input_num)) || remaining) {
+    if (input_num == TVMWrap_GetNumInputs() - 1) {
+      TVMWrap_Run();
+      for (size_t i = 0; i < TVMWrap_GetNumOutputs(); i++) {
+        mlif_handle_result(TVMWrap_GetOutputPtr(i), TVMWrap_GetOutputSize(i));
+      }
+      input_num = 0;
+      remaining = remaining > 0 ? remaining - 1 : 0;
+    } else {
+      input_num++;
+    }
+  }
+}
diff --git a/resources/platforms/zephyr/project/lib/support/CMakeLists.txt b/resources/platforms/zephyr/project/lib/support/CMakeLists.txt
new file mode 100644
index 000000000..0f9fa21fc
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/support/CMakeLists.txt
@@ -0,0 +1,2 @@
+ADD_LIBRARY(support STATIC dummy.c)
+TARGET_INCLUDE_DIRECTORIES(support PUBLIC .)
diff --git a/resources/platforms/zephyr/project/lib/support/dummy.c b/resources/platforms/zephyr/project/lib/support/dummy.c
new file mode 100644
index 000000000..e69de29bb
diff --git a/resources/platforms/zephyr/project/lib/support/exit.h b/resources/platforms/zephyr/project/lib/support/exit.h
new file mode 100644
index 000000000..bc52eef1b
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/support/exit.h
@@ -0,0 +1,8 @@
+#ifndef SUPPORT_EXIT_H
+#define SUPPORT_EXIT_H
+
+#define EXIT_MLIF_BASE (0x10)
+#define EXIT_MLIF_INVALID_SIZE (EXIT_MLIF_BASE + 1)
+#define EXIT_MLIF_MISSMATCH (EXIT_MLIF_BASE + 2)
+
+#endif  // SUPPORT_EXIT_H
diff --git a/resources/platforms/zephyr/project/lib/support/printing.h b/resources/platforms/zephyr/project/lib/support/printing.h
new file mode 100644
index 000000000..047a412c5
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/support/printing.h
@@ -0,0 +1,11 @@
+#ifndef SUPPORT_PRINTING_H
+#define SUPPORT_PRINTING_H
+
+#ifdef _DEBUG
+#include <stdio.h>
+#define DBGPRINTF(format, ...) printf(format, ##__VA_ARGS__)
+#else
+#define DBGPRINTF(format, ...)
+#endif
+
+#endif
diff --git a/resources/platforms/zephyr/project/lib/support/quantize.h b/resources/platforms/zephyr/project/lib/support/quantize.h
new file mode 100644
index 000000000..ce80acb71
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/support/quantize.h
@@ -0,0 +1,23 @@
+#ifndef SUPPORT_QUANTIZE_H
+#define SUPPORT_QUANTIZE_H
+
+#include <stdint.h>
+#include <limits.h>
+#include <math.h>
+
+inline float DequantizeInt8ToFloat(int8_t value, float scale, int zero_point) {
+  return static_cast<float>(value - zero_point) * scale;
+}
+
+inline int8_t QuantizeFloatToInt8(float value, float scale, int zero_point) {
+  int32_t result = round(value / scale) + zero_point;
+  if (result < INT8_MIN) {
+    result = INT8_MIN;
+  }
+  if (result > INT8_MAX) {
+    result = INT8_MAX;
+  }
+  return static_cast<int8_t>(result);
+}
+
+#endif
diff --git a/resources/platforms/zephyr/project/lib/targets/corstone300.c b/resources/platforms/zephyr/project/lib/targets/corstone300.c
new file mode 100644
index 000000000..225363643
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/targets/corstone300.c
@@ -0,0 +1,62 @@
+#include <stdio.h>
+
+// DWT (Data Watchpoint and Trace) registers, only exists on ARM Cortex with a
+// DWT unit.
+#define KIN1_DWT_CONTROL (*((volatile uint32_t*)0xE0001000))
+
+// DWT Control register.
+#define KIN1_DWT_CYCCNTENA_BIT (1UL << 0)
+
+// CYCCNTENA bit in DWT_CONTROL register.
+#define KIN1_DWT_CYCCNT (*((volatile uint32_t*)0xE0001004))
+
+// DWT Cycle Counter register.
+#define KIN1_DEMCR (*((volatile uint32_t*)0xE000EDFC))
+
+// DEMCR: Debug Exception and Monitor Control Register.
+#define KIN1_TRCENA_BIT (1UL << 24)
+
+// Trace enable bit in DEMCR register.
+#define KIN1_LAR (*((volatile uint32_t*)0xE0001FB0))
+
+// Unlock access to DWT (ITM, etc.)registers.
+#define KIN1_UnlockAccessToDWT() KIN1_LAR = 0xC5ACCE55;
+
+// TRCENA: Enable trace and debug block DEMCR (Debug Exception and Monitor
+// Control Register.
+#define KIN1_InitCycleCounter() KIN1_DEMCR |= KIN1_TRCENA_BIT
+
+#define KIN1_ResetCycleCounter() KIN1_DWT_CYCCNT = 0
+#define KIN1_EnableCycleCounter() KIN1_DWT_CONTROL |= KIN1_DWT_CYCCNTENA_BIT
+#define KIN1_DisableCycleCounter() KIN1_DWT_CONTROL &= ~KIN1_DWT_CYCCNTENA_BIT
+#define KIN1_GetCycleCounter() KIN1_DWT_CYCCNT
+
+int32_t ticks_per_second() { return 25e6; }
+
+int32_t GetCurrentTimeTicks() { return KIN1_GetCycleCounter(); }
+
+extern void uart_init(void);
+
+static int32_t start_cycles = 0;
+
+void init_target() {
+  uart_init();
+  KIN1_UnlockAccessToDWT();
+  KIN1_InitCycleCounter();
+  KIN1_ResetCycleCounter();
+  KIN1_EnableCycleCounter();
+  int32_t ticks = GetCurrentTimeTicks();
+  start_cycles = ticks;
+  printf("GetCurrentTimeTicks=%ld\n", ticks);
+}
+
+void deinit_target() {
+  int32_t stop_cycles = GetCurrentTimeTicks();
+  int32_t diff_cycles = stop_cycles - start_cycles;
+  // int32_t diff_ms = diff_cycles / (ticks_per_second() / 1000);
+  // printf("Total Time: %ld ms\n", diff_ms);
+  printf("Total Cycles: %ld\n", diff_cycles);
+  printf("EXITTHESIM\n");
+  while (1)
+    ;
+}
diff --git a/resources/platforms/zephyr/project/lib/targets/default.c b/resources/platforms/zephyr/project/lib/targets/default.c
new file mode 100644
index 000000000..e58e59286
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/targets/default.c
@@ -0,0 +1,2 @@
+__attribute__((weak)) void init_target() {}
+__attribute__((weak)) void deinit_target() {}
diff --git a/resources/platforms/zephyr/project/lib/targets/etiss_pulpino.c b/resources/platforms/zephyr/project/lib/targets/etiss_pulpino.c
new file mode 100644
index 000000000..f083b4863
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/targets/etiss_pulpino.c
@@ -0,0 +1,112 @@
+#include <stdio.h>
+
+/* How many cycles (rdcycle) per second (OVPsim and Spike). */
+#define RDCYCLE_PER_SECOND 100000000UL
+
+/**
+ * @brief Returns the number of clock cycles executed by the processor.
+ * Overflows after 42.9 seconds on a 100 MIPS and with CPI = 1 (OVPsim).
+ */
+static inline uint32_t rdcycle(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  __asm__ volatile("rdcycle %0" : "=r"(cycles));
+  return cycles;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the full 64bit register cycle register, which holds the
+ * number of clock cycles executed by the processor.
+ */
+static inline uint64_t rdcycle64() {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  uint32_t cyclesh1;
+  uint32_t cyclesh2;
+
+  /* Reads are not atomic. So ensure, that we are never reading inconsistent
+   * values from the 64bit hardware register. */
+  do {
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh1));
+    __asm__ volatile("rdcycle %0" : "=r"(cycles));
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh2));
+  } while (cyclesh1 != cyclesh2);
+
+  return (((uint64_t)cyclesh1) << 32) | cycles;
+#else
+  return 0;
+#endif
+}
+
+/* How many time cycles (rdtime) per second (OVPsim and Spike). */
+#define RDTIME_PER_SECOND 1000000UL
+
+/**
+ * @brief Returns wall-clock real time that has passed from an arbitrary start time in the past.
+ */
+static inline uint32_t rdtime(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t time;
+  __asm__ volatile("rdtime %0" : "=r"(time));
+  return time;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the number of instructions retired by the processor.
+ */
+static inline uint32_t rdinstret(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t instret;
+  __asm__ volatile("rdinstret %0" : "=r"(instret));
+  return instret;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_fext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_vext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<9+1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+static uint64_t start_cycles = 0;
+
+void init_target() {
+  // enable_fext();
+#ifdef USE_VEXT
+  // enable_vext();
+#endif
+  start_cycles = rdcycle64();
+}
+
+void deinit_target() {
+  uint64_t stop_cycles = rdcycle64();
+  uint64_t diff_cycles = stop_cycles - start_cycles;
+  float diff_ms = 0;  // unimplemented (see RDCYCLE_PER_SECOND)
+  printf("Total Cycles: %lld\n", stop_cycles - start_cycles);
+}
diff --git a/resources/platforms/zephyr/project/lib/targets/generic_riscv.c b/resources/platforms/zephyr/project/lib/targets/generic_riscv.c
new file mode 100644
index 000000000..f083b4863
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/targets/generic_riscv.c
@@ -0,0 +1,112 @@
+#include <stdio.h>
+
+/* How many cycles (rdcycle) per second (OVPsim and Spike). */
+#define RDCYCLE_PER_SECOND 100000000UL
+
+/**
+ * @brief Returns the number of clock cycles executed by the processor.
+ * Overflows after 42.9 seconds on a 100 MIPS and with CPI = 1 (OVPsim).
+ */
+static inline uint32_t rdcycle(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  __asm__ volatile("rdcycle %0" : "=r"(cycles));
+  return cycles;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the full 64bit register cycle register, which holds the
+ * number of clock cycles executed by the processor.
+ */
+static inline uint64_t rdcycle64() {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  uint32_t cyclesh1;
+  uint32_t cyclesh2;
+
+  /* Reads are not atomic. So ensure, that we are never reading inconsistent
+   * values from the 64bit hardware register. */
+  do {
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh1));
+    __asm__ volatile("rdcycle %0" : "=r"(cycles));
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh2));
+  } while (cyclesh1 != cyclesh2);
+
+  return (((uint64_t)cyclesh1) << 32) | cycles;
+#else
+  return 0;
+#endif
+}
+
+/* How many time cycles (rdtime) per second (OVPsim and Spike). */
+#define RDTIME_PER_SECOND 1000000UL
+
+/**
+ * @brief Returns wall-clock real time that has passed from an arbitrary start time in the past.
+ */
+static inline uint32_t rdtime(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t time;
+  __asm__ volatile("rdtime %0" : "=r"(time));
+  return time;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the number of instructions retired by the processor.
+ */
+static inline uint32_t rdinstret(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t instret;
+  __asm__ volatile("rdinstret %0" : "=r"(instret));
+  return instret;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_fext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_vext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<9+1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+static uint64_t start_cycles = 0;
+
+void init_target() {
+  // enable_fext();
+#ifdef USE_VEXT
+  // enable_vext();
+#endif
+  start_cycles = rdcycle64();
+}
+
+void deinit_target() {
+  uint64_t stop_cycles = rdcycle64();
+  uint64_t diff_cycles = stop_cycles - start_cycles;
+  float diff_ms = 0;  // unimplemented (see RDCYCLE_PER_SECOND)
+  printf("Total Cycles: %lld\n", stop_cycles - start_cycles);
+}
diff --git a/resources/platforms/zephyr/project/lib/targets/riscv_qemu.c b/resources/platforms/zephyr/project/lib/targets/riscv_qemu.c
new file mode 100644
index 000000000..7660d141d
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/targets/riscv_qemu.c
@@ -0,0 +1,112 @@
+#include <stdio.h>
+
+/* How many cycles (rdcycle) per second (OVPsim and Spike). */
+#define RDCYCLE_PER_SECOND 100000000UL
+
+/**
+ * @brief Returns the number of clock cycles executed by the processor.
+ * Overflows after 42.9 seconds on a 100 MIPS and with CPI = 1 (OVPsim).
+ */
+static inline uint32_t rdcycle(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  __asm__ volatile("rdcycle %0" : "=r"(cycles));
+  return cycles;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the full 64bit register cycle register, which holds the
+ * number of clock cycles executed by the processor.
+ */
+static inline uint64_t rdcycle64() {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  uint32_t cyclesh1;
+  uint32_t cyclesh2;
+
+  /* Reads are not atomic. So ensure, that we are never reading inconsistent
+   * values from the 64bit hardware register. */
+  do {
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh1));
+    __asm__ volatile("rdcycle %0" : "=r"(cycles));
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh2));
+  } while (cyclesh1 != cyclesh2);
+
+  return (((uint64_t)cyclesh1) << 32) | cycles;
+#else
+  return 0;
+#endif
+}
+
+/* How many time cycles (rdtime) per second (OVPsim and Spike). */
+#define RDTIME_PER_SECOND 1000000UL
+
+/**
+ * @brief Returns wall-clock real time that has passed from an arbitrary start time in the past.
+ */
+static inline uint32_t rdtime(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t time;
+  __asm__ volatile("rdtime %0" : "=r"(time));
+  return time;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the number of instructions retired by the processor.
+ */
+static inline uint32_t rdinstret(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t instret;
+  __asm__ volatile("rdinstret %0" : "=r"(instret));
+  return instret;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_fext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_vext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<9+1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+static uint64_t start_cycles = 0;
+
+void init_target() {
+  enable_fext();
+#ifdef USE_VEXT
+  enable_vext();
+#endif
+  start_cycles = rdcycle64();
+}
+
+void deinit_target() {
+  uint64_t stop_cycles = rdcycle64();
+  uint64_t diff_cycles = stop_cycles - start_cycles;
+  float diff_ms = 0;  // unimplemented (see RDCYCLE_PER_SECOND)
+  printf("Total Cycles: %lld\n", stop_cycles - start_cycles);
+}
diff --git a/resources/platforms/zephyr/project/lib/targets/target.h b/resources/platforms/zephyr/project/lib/targets/target.h
new file mode 100644
index 000000000..990a31ac5
--- /dev/null
+++ b/resources/platforms/zephyr/project/lib/targets/target.h
@@ -0,0 +1,2 @@
+void init_target();
+void deinit_target();
diff --git a/resources/platforms/zephyr/project/prj.conf b/resources/platforms/zephyr/project/prj.conf
new file mode 100644
index 000000000..b13efb231
--- /dev/null
+++ b/resources/platforms/zephyr/project/prj.conf
@@ -0,0 +1,11 @@
+CONFIG_NEWLIB_LIBC=n
+CONFIG_MLONMCU_FRAMEWORK="tvm"
+CONFIG_MLONMCU_BACKEND="tvmaot"
+CONFIG_TVM_DIR="/nas/ei/share/TUEIEDAscratch/ga87puy/work/mlonmcu/workspace/deps/src/tvm"
+CONFIG_TF_DIR="?"
+CONFIG_MLONMCU_CODEGEN_DIR="/work/tools/zephyr_stuff/run/"
+CONFIG_CPLUSPLUS=y
+CONFIG_STD_CPP17=y
+CONFIG_LIB_CPLUSPLUS=y
+CONFIG_EXCEPTIONS=y
+CONFIG_REBOOT=y
diff --git a/resources/platforms/zephyr/project/sample.yaml b/resources/platforms/zephyr/project/sample.yaml
new file mode 100644
index 000000000..2bdf00b93
--- /dev/null
+++ b/resources/platforms/zephyr/project/sample.yaml
@@ -0,0 +1,19 @@
+sample:
+  description: Hello World sample, the simplest Zephyr
+    application
+  name: hello world
+common:
+    tags: introduction
+    integration_platforms:
+      - native_posix
+    harness: console
+    harness_config:
+      type: one_line
+      regex:
+        - "Hello World! (.*)"
+tests:
+  sample.basic.helloworld:
+    tags: introduction
+  sample.basic.helloworld.uefi:
+    platform_allow: qemu_x86_64
+    extra_args: CONF_FILE=prj_uefi.conf
diff --git a/resources/platforms/zephyr/project/src/main.c b/resources/platforms/zephyr/project/src/main.c
new file mode 100644
index 000000000..77ee83b9e
--- /dev/null
+++ b/resources/platforms/zephyr/project/src/main.c
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2012-2014 Wind River Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr.h>
+#include <stdint.h>
+#include <sys/printk.h>
+#include <sys/reboot.h>
+#include <timing/timing.h>
+// #include <zephyr/kernel.h>
+
+// void init_target();
+// void deinit_target();
+
+int main() {
+  // init_target();
+  timing_init();
+  timing_start();
+  printk("MLonMCU: START\n");
+  // uint32_t start_time;
+  // uint32_t start_time_;
+  // uint32_t stop_time;
+  // uint32_t stop_time_;
+  // uint32_t cycles_spent;
+  // uint64_t cycles_spent_;
+  // uint32_t nanoseconds_spent;
+  // uint64_t nanoseconds_spent_;
+  /* capture initial time stamp */
+  // start_time = k_cycle_get_32();
+  // start_time_ = k_cycle_get_64();
+  timing_t start_time, end_time;
+  // printk("start_time_=%llu", start_time_);
+  start_time = timing_counter_get();
+  mlif_run();
+  // stop_time = k_cycle_get_32();
+  // stop_time_ = k_cycle_get_64();
+  end_time = timing_counter_get();
+  uint64_t cycles = timing_cycles_get(&start_time, &end_time);
+  uint64_t ns_spent = timing_cycles_to_ns(cycles);
+  // printk("stop_time_=%llu", stop_time_);
+
+  /* compute how long the work took (assumes no counter rollover) */
+  // if (start_time >= stop_time) {
+  //     cycles_spent_ = stop_time - start_time;
+  // } else {
+  //     cycles_spent_ = stop_time + ((1 << 31) - start_time);
+  // }
+  // cycles_spent_ = stop_time_ - start_time_;
+  // nanoseconds_spent = k_cyc_to_ns_ceil32(cycles_spent);
+  // nanoseconds_spent_ = k_cyc_to_ns_ceil64(cycles_spent_);
+  // printk("Total Time: %u us\n", (uint32_t) (nanoseconds_spent/1000));
+  printk("Total Time: %llu us\n", (ns_spent/1000));
+  // printk("Total Cycles: %u\n", cycles_spent);
+  printk("Total Cycles: %llu\n", cycles);
+  printk("MLonMCU: STOP\n");
+  // deinit_target();
+  sys_reboot(0);
+  return 0;
+}
+//
+// /* do work for some (short) period of time */
+// ...
+//
+// /* capture final time stamp */
+// stop_time = k_cycle_get_32();
+//
+// /* compute how long the work took (assumes no counter rollover) */
+// cycles_spent = stop_time - start_time;
+// nanoseconds_spent = SYS_CLOCK_HW_CYCLES_TO_NS(cycles_spent);
diff --git a/resources/platforms/zephyr/project2/CMakeLists.txt b/resources/platforms/zephyr/project2/CMakeLists.txt
new file mode 100644
index 000000000..88bf67dea
--- /dev/null
+++ b/resources/platforms/zephyr/project2/CMakeLists.txt
@@ -0,0 +1,72 @@
+# SPDX-License-Identifier: Apache-2.0
+
+cmake_minimum_required(VERSION 3.20.0)
+
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(mlonmcu_template)
+SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+
+SET(TVM_ALIGNMENT_BYTES 4)
+
+# cmake-format: off
+SET(TVM_HEADERS
+    ${CONFIG_TVM_DIR}/include
+    ${CONFIG_TVM_DIR}/3rdparty/dlpack/include
+    ${CONFIG_TVM_DIR}/apps/bundle_deploy
+    ${CONFIG_TVM_DIR}/src/runtime/crt/include
+)
+# cmake-format: on
+
+IF(CONFIG_TVM_CRT_CONFIG_DIR)
+    LIST(APPEND TVM_HEADERS ${CONFIG_TVM_CRT_CONFIG_DIR})
+ELSE()
+    LIST(APPEND TVM_HEADERS ${CONFIG_TVM_DIR}/apps/bundle_deploy/crt_config)
+ENDIF()
+
+INCLUDE(backends/${CONFIG_MLONMCU_BACKEND})
+INCLUDE(frameworks/${CONFIG_MLONMCU_FRAMEWORK})
+
+TARGET_INCLUDE_DIRECTORIES(app PUBLIC ${TVM_HEADERS})
+
+TARGET_COMPILE_DEFINITIONS(app PUBLIC
+    -DTVM_RUNTIME_ALLOC_ALIGNMENT_BYTES=${TVM_ALIGNMENT_BYTES}
+)
+
+
+
+INCLUDE(extras)
+
+INCLUDE(frameworks/${CONFIG_MLONMCU_FRAMEWORK}_mlif)
+
+IF(CONFIG_MODEL_DIR)
+    GET_FILENAME_COMPONENT(MODEL_SUPPORT_DIR ${CONFIG_MODEL_DIR}/support REALPATH)
+    GET_FILENAME_COMPONENT(MODEL_NAME ${CONFIG_MODEL_DIR} NAME)
+ENDIF()
+
+IF(MODEL_SUPPORT_DIR AND EXISTS ${MODEL_SUPPORT_DIR})
+    ADD_SUBDIRECTORY(${MODEL_SUPPORT_DIR} model_support_bin_${MODEL_NAME})
+ELSE()
+    ADD_SUBDIRECTORY(default_model_support)
+ENDIF()
+
+IF(NOT CONFIG_DATA_SRC)
+    SET(CONFIG_DATA_SRC "default_data.c")
+ENDIF()
+
+IF(CONFIG_NUM_RUNS)
+    TARGET_COMPILE_DEFINITIONS(${PROJECT_NAME} PUBLIC NUM_RUNS=${CONFIG_NUM_RUNS})
+ENDIF()
+
+TARGET_INCLUDE_DIRECTORIES(app PUBLIC ${TVM_HEADERS} ${TVM_OUT_DIR}/include ${CONFIG_MLONMCU_CODEGEN_DIR} lib/support)
+SET(TVM_WRAPPER_FILENAME aot_wrapper.c)
+SET(EXTRA_SRC ${EXTRA_SRC} ${CONFIG_MLONMCU_CODEGEN_DIR}/${TVM_WRAPPER_FILENAME})
+target_sources(app PRIVATE
+    src/main.c
+    ml_interface.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_backend_api.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/memory/stack_allocator.c
+    ${CONFIG_DATA_SRC}
+    ${EXTRA_SRC}
+    ${TVM_SRCS}
+)
+target_link_libraries(app PRIVATE model_support)
diff --git a/resources/platforms/zephyr/project2/Kconfig b/resources/platforms/zephyr/project2/Kconfig
new file mode 100644
index 000000000..5363d49b7
--- /dev/null
+++ b/resources/platforms/zephyr/project2/Kconfig
@@ -0,0 +1,101 @@
+mainmenu "MLonMCU"
+
+source "Kconfig.zephyr"
+menu "MLonMCU Options"
+
+    config MLONMCU_CODEGEN_DIR
+        string "Codegen Directory"
+        default "$(CODEGEN_SRC)"
+        help
+            Path to the directory with the codegen results.
+
+    # choice MLONMCU_FRAMEWORK
+    #     prompt "MLonMCU framework"
+    #     default MLONMCU_FRAMEWORK_TFLM
+    #     help
+    #         Choose a MLonMCU framework from the list.
+
+    #     config MLONMCU_FRAMEWORK_TFLM
+    #         bool "TFLite Micro"
+    #     config MLONMCU_FRAMEWORK_TVM
+    #         bool "TVM"
+    # endchoice
+
+    config MLONMCU_FRAMEWORK
+        string "Ued Framework"
+        # default "tflm" if MLONMCU_FRAMEWORK_TFLM
+        # default "tvm" if MLONMCU_FRAMEWORK_TVM
+        default "none"
+        help
+            TODO
+
+    # choice MLONMCU_BACKEND
+    #     prompt "MLonMCU backend"
+    #     default MLONMCU_BACKEND_TFLMI if MLONMCU_FRAMEWORK_TFLM
+    #     default MLONMCU_BACKEND_TVMAOT if MLONMCU_FRAMEWORK_TVM
+    #     help
+    #         Choose a MLonMCU backend for the chosen framwork.
+
+    #     config MLONMCU_BACKEND_TFLMI
+    #         bool "TFLite Micro Default Interpreter"
+    #         depends on MLONMCU_FRAMEWORK_TFLM
+    #     config MLONMCU_BACKEND_TFLMC
+    #         bool "TFLite Micro Compiler"
+    #         depends on MLONMCU_FRAMEWORK_TFLM
+    #     config MLONMCU_BACKEND_TVMAOT
+    #         bool "TVM AoT Executor"
+    #         depends on MLONMCU_FRAMEWORK_TVM
+    #     config MLONMCU_BACKEND_TVMRT
+    #         bool "TVM Graph Executor"
+    #         depends on MLONMCU_FRAMEWORK_TVM
+    #     config MLONMCU_BACKEND_TVMCG
+    #         bool "TVM Custom Codegenerator"
+    #         depends on MLONMCU_FRAMEWORK_TVM
+    # endchoice
+
+    config MLONMCU_BACKEND
+        string "Used backend"
+        default "none"
+        help
+            TODO
+
+    config MLONMCU_NUM_RUNS
+        int "Number of inferences"
+        default 1
+        help
+            How many times the network should be invoked. A negative value maps to a infinite number of runs.
+
+    config MLONMCU_VALIDATE
+        bool "Validate model output data"
+        default false
+        help
+            Compare model outputs with expected results (if available). Only works in DEBUG mode.
+
+    config MLONMCU_MODLE_DIR
+        string "Model Directory"
+        depends on MLONMCU_VALIDATE
+        default "$(MODEL_DIR)"
+        help
+            Directory where model support code and reference data can be found.
+endmenu
+
+menu "TVM Build Options"
+    config TVM_DIR
+        string "Path to TVM"
+        default "$(TVM_SRC)"
+        help
+            TVM directory path.
+    config TVM_CRT_CONFIG_DIR
+        string "Path to TVM CRT config directory"
+        default ""
+        help
+            TVM crt path.
+endmenu
+menu "TFLite Micro Build Options"
+
+    config TF_DIR
+        string "Path to TFLite Micro"
+        default "$(TF_SRC)"
+        help
+            TFLite Micro directory path.
+endmenu
diff --git a/resources/platforms/zephyr/project2/README.rst b/resources/platforms/zephyr/project2/README.rst
new file mode 100644
index 000000000..ce5423d6f
--- /dev/null
+++ b/resources/platforms/zephyr/project2/README.rst
@@ -0,0 +1,33 @@
+.. _hello_world:
+
+Hello World
+###########
+
+Overview
+********
+
+A simple sample that can be used with any :ref:`supported board <boards>` and
+prints "Hello World" to the console.
+
+Building and Running
+********************
+
+This application can be built and executed on QEMU as follows:
+
+.. zephyr-app-commands::
+   :zephyr-app: samples/hello_world
+   :host-os: unix
+   :board: qemu_x86
+   :goals: run
+   :compact:
+
+To build for another board, change "qemu_x86" above to that board's name.
+
+Sample Output
+=============
+
+.. code-block:: console
+
+    Hello World! x86
+
+Exit QEMU by pressing :kbd:`CTRL+A` :kbd:`x`.
diff --git a/resources/platforms/zephyr/project2/cmake/AddAllSubdirs.cmake b/resources/platforms/zephyr/project2/cmake/AddAllSubdirs.cmake
new file mode 100644
index 000000000..e025203fc
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/AddAllSubdirs.cmake
@@ -0,0 +1,21 @@
+MACRO(AddAllSubdirs)
+    UNSET(LOCAL_SUBDIRS)
+    FILE(
+        GLOB LOCAL_SUBDIRS
+        RELATIVE ${CMAKE_CURRENT_LIST_DIR}
+        ${CMAKE_CURRENT_LIST_DIR}/*
+    )
+
+    FOREACH(subdir ${LOCAL_SUBDIRS})
+        IF(IS_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/${subdir})
+            IF(EXISTS ${CMAKE_CURRENT_LIST_DIR}/${subdir}/CMakeLists.txt)
+                ADD_SUBDIRECTORY(${CMAKE_CURRENT_LIST_DIR}/${subdir})
+                MESSAGE(STATUS "Including sub project ${subdir}.")
+            ELSE()
+                MESSAGE(
+                    WARNING "Directory ${subdir} is not added to the current build because it lacks a CMakeLists.txt"
+                )
+            ENDIF()
+        ENDIF()
+    ENDFOREACH()
+ENDMACRO()
diff --git a/resources/platforms/zephyr/project2/cmake/ChooseTarget.cmake b/resources/platforms/zephyr/project2/cmake/ChooseTarget.cmake
new file mode 100644
index 000000000..b03c65e38
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/ChooseTarget.cmake
@@ -0,0 +1,22 @@
+# New target architectures and systems should be added here Make sure to define the CMAKE_TOOLCHAIN_FILE for
+# cross-compilation
+
+# Default implementation of the macro points to the original ADD_LIBRARY function
+MACRO(COMMON_ADD_LIBRARY TARGET_NAME)
+    ADD_LIBRARY(${TARGET_NAME} ${ARGN})
+ENDMACRO()
+
+MACRO(COMMON_ADD_EXECUTABLE TARGET_NAME)
+    ADD_EXECUTABLE(${TARGET_NAME} ${ARGN})
+ENDMACRO()
+
+SET(TOOLCHAIN_FILE "")
+IF(TARGET_SYSTEM)
+    INCLUDE(targets/${TARGET_SYSTEM})
+    IF(TOOLCHAIN)
+        SET(TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/toolchains/${TOOLCHAIN}_${TARGET_SYSTEM}.cmake")
+        INCLUDE(${TOOLCHAIN_FILE})
+    ENDIF()
+ENDIF()
+
+# set(CMAKE_EXECUTABLE_SUFFIX_C .elf)
diff --git a/resources/platforms/zephyr/project2/cmake/LinkTFLite.cmake b/resources/platforms/zephyr/project2/cmake/LinkTFLite.cmake
new file mode 100644
index 000000000..20a91cd51
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/LinkTFLite.cmake
@@ -0,0 +1,223 @@
+SET(TF_DIR
+    "/data/work/code/tensorflow"
+    CACHE PATH "TensorFlow source directory"
+)
+
+SET(TFL_SRC ${CONFIG_TF_DIR}/tensorflow/lite)
+SET(TFLM_SRC ${TFL_SRC}/micro)
+SET(TFLD_SRC ${TFLM_SRC}/tools/make/downloads)
+
+SET(TFLM_EXTRA_KERNEL_LIBS "")
+SET(TFLM_EXTRA_KERNEL_INCS "")
+
+IF(CONFIG_TFLM_OPTIMIZED_KERNEL)
+    # Suboptimal but we do not want to hardcode every kernel which should be replaced...
+    FILE(GLOB TFLM_EXTRA_KERNEL_SRCS ${TFLM_SRC}/kernels/${CONFIG_TFLM_OPTIMIZED_KERNEL}/*.cc)
+    # LIST(APPEND TFLM_EXTRA_KERNEL_INCS ${TFLM_SRC}/kernels/${TFLM_OPTIMIZED_KERNEL}/)
+    STRING(TOUPPER "${CONFIG_TFLM_OPTIMIZED_KERNEL}" TFLM_OPTIMIZED_KERNEL_UPPER)
+ENDIF()
+
+IF(CONFIG_TFLM_OPTIMIZED_KERNEL_LIB)
+    LIST(APPEND TFLM_EXTRA_KERNEL_LIBS ${CONFIG_TFLM_OPTIMIZED_KERNEL_LIB})
+ENDIF()
+
+IF(CONFIG_TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR)
+    LIST(APPEND TFLM_EXTRA_KERNEL_INCS ${CONFIG_TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR})
+ENDIF()
+
+IF(CONFIG_TFLM_OPTIMIZED_KERNEL_DEPS)
+    LIST(APPEND TFLM_EXTRA_KERNEL_DEPS ${CONFIG_TFLM_OPTIMIZED_KERNEL_DEPS})
+ENDIF()
+
+SET(CUSTOM_QUANT_SRC ${TFL_SRC}/experimental/custom_quantization_util.cc)
+IF(EXISTS ${CUSTOM_QUANT_SRC})
+    SET(OPT_SRC ${CUSTOM_QUANT_SRC})
+ENDIF()
+
+SET(TFLM_REFERENCE_KERNEL_SRCS
+    ${TFLM_SRC}/kernels/softmax.cc
+    ${TFLM_SRC}/kernels/fully_connected.cc
+    ${TFLM_SRC}/kernels/pooling.cc
+    ${TFLM_SRC}/kernels/add.cc
+    ${TFLM_SRC}/kernels/mul.cc
+    ${TFLM_SRC}/kernels/conv.cc
+    ${TFLM_SRC}/kernels/depthwise_conv.cc
+    ${TFLM_SRC}/kernels/softmax.cc
+    ${TFLM_SRC}/kernels/fully_connected.cc
+    ${TFLM_SRC}/kernels/pooling.cc
+    ${TFLM_SRC}/kernels/add.cc
+    ${TFLM_SRC}/kernels/mul.cc
+    ${TFLM_SRC}/kernels/conv.cc
+    ${TFLM_SRC}/kernels/depthwise_conv.cc
+    ${TFLM_SRC}/kernels/logical.cc
+    ${TFLM_SRC}/kernels/logistic.cc
+    ${TFLM_SRC}/kernels/svdf.cc
+    ${TFLM_SRC}/kernels/concatenation.cc
+    ${TFLM_SRC}/kernels/ceil.cc
+    ${TFLM_SRC}/kernels/floor.cc
+    ${TFLM_SRC}/kernels/prelu.cc
+    ${TFLM_SRC}/kernels/neg.cc
+    ${TFLM_SRC}/kernels/elementwise.cc
+    ${TFLM_SRC}/kernels/maximum_minimum.cc
+    ${TFLM_SRC}/kernels/arg_min_max.cc
+    ${TFLM_SRC}/kernels/reshape.cc
+    ${TFLM_SRC}/kernels/comparisons.cc
+    ${TFLM_SRC}/kernels/round.cc
+    ${TFLM_SRC}/kernels/strided_slice.cc
+    ${TFLM_SRC}/kernels/pack.cc
+    ${TFLM_SRC}/kernels/pad.cc
+    ${TFLM_SRC}/kernels/split.cc
+    ${TFLM_SRC}/kernels/unpack.cc
+    ${TFLM_SRC}/kernels/quantize.cc
+    ${TFLM_SRC}/kernels/activations.cc
+    ${TFLM_SRC}/kernels/dequantize.cc
+    ${TFLM_SRC}/kernels/reduce.cc
+    ${TFLM_SRC}/kernels/sub.cc
+    ${TFLM_SRC}/kernels/resize_nearest_neighbor.cc
+    ${TFLM_SRC}/kernels/l2norm.cc
+    ${TFLM_SRC}/kernels/circular_buffer.cc
+    ${TFLM_SRC}/kernels/ethosu.cc
+    ${TFLM_SRC}/kernels/tanh.cc
+)
+
+# This files only exists in newer versions of TF
+IF(EXISTS ${TFLM_SRC}/kernels/conv_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/conv_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/depthwise_conv_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/depthwise_conv_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/fully_connected_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/fully_connected_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/quantize_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/quantize_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/softmax_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/softmax_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/svdf_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/svdf_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/pooling_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/pooling_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/add_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/add_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/dequantize_common.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/dequantize_common.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/kernels/detection_postprocess.cc)
+    LIST(APPEND TFLM_REFERENCE_KERNEL_SRCS ${TFLM_SRC}/kernels/detection_postprocess.cc)
+ENDIF()
+
+FOREACH(src ${TFLM_EXTRA_KERNEL_SRCS})
+    GET_FILENAME_COMPONENT(src_name ${src} NAME)
+    IF(${src_name} MATCHES ".*_test.*")
+        LIST(REMOVE_ITEM TFLM_EXTRA_KERNEL_SRCS ${src})
+    ELSE()
+        SET(src_path "${TFLM_SRC}/kernels/${src_name}")
+        LIST(FIND TFLM_REFERENCE_KERNEL_SRCS ${src_path} TFLM_KERNEL_SRCS_FOUND_INDEX)
+        IF(${TFLM_KERNEL_SRCS_FOUND_INDEX} GREATER_EQUAL 0)
+            MESSAGE(STATUS "Replacing TFLM version of ${src_name} by optimized variant...")
+            LIST(REMOVE_ITEM TFLM_REFERENCE_KERNEL_SRCS ${src_path})
+        ENDIF()
+    ENDIF()
+ENDFOREACH()
+
+# This files only exists in newer versions of TF
+IF(EXISTS ${TFL_SRC}/schema/schema_utils.cc)
+    LIST(APPEND OPT_SRC ${TFL_SRC}/schema/schema_utils.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/micro_context.cc)
+    LIST(APPEND OPT_SRC ${TFLM_SRC}/micro_context.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/micro_graph.cc)
+    LIST(APPEND OPT_SRC ${TFLM_SRC}/micro_graph.cc)
+ENDIF()
+IF(EXISTS ${TFLM_SRC}/flatbuffer_utils.cc)
+    LIST(APPEND OPT_SRC ${TFLM_SRC}/flatbuffer_utils.cc)
+ENDIF()
+
+ADD_LIBRARY(
+    tflm STATIC
+    # Not really needed?
+    ${TFLM_SRC}/micro_error_reporter.cc
+    ${TFLM_SRC}/debug_log.cc
+    ${TFLM_SRC}/micro_string.cc
+    # For reporter->Report
+    ${CONFIG_TF_DIR}/tensorflow/lite/core/api/error_reporter.cc
+    # Kernels
+    ${TFLM_REFERENCE_KERNEL_SRCS}
+    ${TFLM_EXTRA_KERNEL_SRCS}
+    # Kernel deps
+    ${TFLM_SRC}/kernels/kernel_util.cc
+    ${TFLM_SRC}/all_ops_resolver.cc
+    ${TFLM_SRC}/micro_utils.cc
+    ${TFL_SRC}/kernels/internal/quantization_util.cc
+    ${TFL_SRC}/kernels/kernel_util.cc
+    ${TFLM_SRC}/micro_interpreter.cc
+    ${TFLM_SRC}/micro_allocator.cc
+    ${TFLM_SRC}/micro_allocation_info.cc
+    ${TFLM_SRC}/micro_resource_variable.cc
+    ${TFLM_SRC}/arena_allocator/simple_memory_allocator.cc
+    ${TFLM_SRC}/memory_helpers.cc
+    ${TFLM_SRC}/memory_planner/greedy_memory_planner.cc
+    ${TFL_SRC}/core/api/tensor_utils.cc
+    ${TFL_SRC}/core/api/flatbuffer_conversions.cc
+    ${TFL_SRC}/core/api/op_resolver.cc
+    ${TFL_SRC}/c/common.cc
+    ${OPT_SRC}
+)
+
+IF(TFLM_EXTRA_KERNEL_LIBS)
+    TARGET_LINK_LIBRARIES(tflm PUBLIC ${TFLM_EXTRA_KERNEL_LIBS})
+ENDIF()
+TARGET_LINK_LIBRARIES(tflm PUBLIC m)
+
+IF(TFLM_OPTIMIZED_KERNEL_DEPS)
+    ADD_DEPENDENCIES(tflm ${TFLM_OPTIMIZED_KERNEL_DEPS})
+ENDIF()
+
+# cmake-format: off
+TARGET_INCLUDE_DIRECTORIES(tflm PUBLIC
+    ${CONFIG_TF_DIR}
+    ${TFLD_SRC}/flatbuffers/include
+    ${TFLD_SRC}/gemmlowp
+    ${TFLD_SRC}/ruy
+    ${TFLM_EXTRA_KERNEL_INCS}
+)
+# cmake-format: on
+
+# cmake-format: off
+TARGET_COMPILE_DEFINITIONS(tflm PUBLIC
+    TF_LITE_USE_GLOBAL_CMATH_FUNCTIONS
+    TF_LITE_USE_GLOBAL_MAX
+    TF_LITE_USE_GLOBAL_MIN
+    TF_LITE_STATIC_MEMORY
+    TFLITE_EMULATE_FLOAT
+    "$<$<CONFIG:RELEASE>:TF_LITE_STRIP_ERROR_STRINGS>"
+    ${TFLM_OPTIMIZED_KERNEL_UPPER}
+)
+
+# Workaround for the following issue which does not envolve patching the tflite-micro codebase:
+
+# .../micro_error_reporter.cc: In function 'tflite::ErrorReporter* tflite::GetMicroErrorReporter()':
+# .../micro_error_reporter.cc:58:76: error: 'static void tflite::MicroErrorReporter::operator delete(void*)' is private within this context
+#    58 |     error_reporter_ = new (micro_error_reporter_buffer) MicroErrorReporter();
+#       |                                                                            ^
+# In file included from .../micro_error_reporter.h:21,
+#                  from .../micro_error_reporter.cc:16:
+# .../compatibility.h:27:8: note: declared private here
+#    27 |   void operator delete(void* p) {}
+#       |        ^~~~~~~~
+# .../micro_error_reporter.h:51:3: note: in expansion of macro 'TF_LITE_REMOVE_VIRTUAL_DELETE'
+#    51 |   TF_LITE_REMOVE_VIRTUAL_DELETE
+#
+# If disabling the exceptions has major disadvantages needs to be investigated.
+
+TARGET_COMPILE_OPTIONS(tflm PUBLIC
+    -fno-exceptions
+)
+# cmake-format: on
diff --git a/resources/platforms/zephyr/project2/cmake/LinkTVM.cmake b/resources/platforms/zephyr/project2/cmake/LinkTVM.cmake
new file mode 100644
index 000000000..35466aff6
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/LinkTVM.cmake
@@ -0,0 +1,68 @@
+SET(TVM_ALIGNMENT_BYTES 4)
+
+# cmake-format: off
+SET(TVM_HEADERS
+    ${CONFIG_TVM_DIR}/include
+    ${CONFIG_TVM_DIR}/3rdparty/dlpack/include
+    ${CONFIG_TVM_DIR}/apps/bundle_deploy
+    ${CONFIG_TVM_DIR}/src/runtime/crt/include
+)
+# cmake-format: on
+
+IF(CONFIG_TVM_CRT_CONFIG_DIR)
+    LIST(APPEND TVM_HEADERS ${CONFIG_TVM_CRT_CONFIG_DIR})
+ELSE()
+    LIST(APPEND TVM_HEADERS ${CONFIG_TVM_DIR}/apps/bundle_deploy/crt_config)
+ENDIF()
+
+# # cmake-format: off
+# ADD_LIBRARY(tvm_static_rt STATIC
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_runtime_api.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/memory/stack_allocator.c
+# )
+# # cmake-format: on
+#
+# TARGET_INCLUDE_DIRECTORIES(tvm_static_rt PUBLIC ${TVM_HEADERS})
+#
+# # cmake-format: off
+# TARGET_COMPILE_DEFINITIONS(tvm_static_rt PUBLIC
+#     -DTVM_RUNTIME_ALLOC_ALIGNMENT_BYTES=${TVM_ALIGNMENT_BYTES}
+# )
+# # cmake-format: on
+#
+# # cmake-format: off
+# ADD_LIBRARY(tvm_graph_rt STATIC
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_backend_api.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_runtime_api.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/common/func_registry.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/memory/page_allocator.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/memory/stack_allocator.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/common/ndarray.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/common/packed_func.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/graph_executor/graph_executor.c
+#     ${CONFIG_TVM_DIR}/src/runtime/crt/graph_executor/load_json.c
+# )
+# # cmake-format: on
+#
+# TARGET_INCLUDE_DIRECTORIES(tvm_graph_rt PUBLIC ${TVM_HEADERS})
+#
+# # cmake-format: off
+# TARGET_COMPILE_DEFINITIONS(tvm_graph_rt PUBLIC
+#     -DTVM_RUNTIME_ALLOC_ALIGNMENT_BYTES=${TVM_ALIGNMENT_BYTES}
+# )
+# # cmake-format: on
+#
+# cmake-format: off
+ADD_LIBRARY(tvm_aot_rt STATIC
+    ${CONFIG_TVM_DIR}/src/runtime/crt/common/crt_backend_api.c
+    ${CONFIG_TVM_DIR}/src/runtime/crt/memory/stack_allocator.c
+)
+# cmake-format: on
+
+TARGET_INCLUDE_DIRECTORIES(tvm_aot_rt PUBLIC ${TVM_HEADERS})
+
+# cmake-format: off
+TARGET_COMPILE_DEFINITIONS(tvm_aot_rt PUBLIC
+    -DTVM_RUNTIME_ALLOC_ALIGNMENT_BYTES=${TVM_ALIGNMENT_BYTES}
+)
+# cmake-format: on
diff --git a/resources/platforms/zephyr/project2/cmake/LookupClang.cmake b/resources/platforms/zephyr/project2/cmake/LookupClang.cmake
new file mode 100644
index 000000000..c5655ba18
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/LookupClang.cmake
@@ -0,0 +1,41 @@
+set(CLANG_VERSIONS 15 14)
+
+set(LLVM_DIR "" CACHE PATH "Lookup path for the llvm installation.")
+MESSAGE("LLVM_DIR=${LLVM_DIR}")
+
+# Find clang
+function(do_lookup program out)
+    set(VERSION_NAMES "")
+    foreach(version ${CLANG_VERSIONS})
+        set(NEW_NAME "${program}-${version}")
+        list(APPEND VERSION_NAMES ${NEW_NAME})
+    endforeach()
+    # Custom path, explicit versions
+    find_program(${program}_NAME_MATCH NAMES ${VERSION_NAMES} PATHS ${LLVM_DIR}/bin/ NO_DEFAULT_PATH)
+    IF(NOT ${program}_NAME_MATCH)
+        # System path, explicit versions
+        find_program(${program}_NAME_MATCH NAMES ${VERSION_NAMES})
+        IF(NOT ${program}_NAME_MATCH)
+            # Custom path, implicit version
+            find_program(${program}_NAME_MATCH NAMES ${program} PATHS ${LLVM_DIR}/bin/ NO_DEFAULT_PATH)
+            IF(NOT ${program}_NAME_MATCH)
+                # System path, implicit version
+                find_program(${program}_NAME_MATCH NAMES ${program})
+            ENDIF()
+        ENDIF()
+    ENDIF()
+    IF(NOT ${program}_NAME_MATCH)
+        MESSAGE(FATAL_ERROR "Unable to find ${program} executable")
+    ENDIF()
+    MESSAGE(STATUS "${program}_NAME_MATCH=${${program}_NAME_MATCH}")
+    set(${out} ${${program}_NAME_MATCH} PARENT_SCOPE)
+endfunction()
+
+
+do_lookup(clang CLANG_EXECUTABLE)
+do_lookup(clang++ CLANG++_EXECUTABLE)
+do_lookup(lld LLD_EXECUTABLE)
+
+MESSAGE(STATUS "CLANG_EXECUTABLE=${CLANG_EXECUTABLE}")
+MESSAGE(STATUS "CLANG++_EXECUTABLE=${CLANG++_EXECUTABLE}")
+MESSAGE(STATUS "LLD_EXECUTABLE=${LLD_EXECUTABLE}")
diff --git a/resources/platforms/zephyr/project2/cmake/backends/tflmc.cmake b/resources/platforms/zephyr/project2/cmake/backends/tflmc.cmake
new file mode 100644
index 000000000..5f08f021b
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/backends/tflmc.cmake
@@ -0,0 +1,5 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tflm")
+ENDIF()
+
+SET(TFLITE_REQUIRED_COMPONENTS a b c)
diff --git a/resources/platforms/zephyr/project2/cmake/backends/tflmi.cmake b/resources/platforms/zephyr/project2/cmake/backends/tflmi.cmake
new file mode 100644
index 000000000..5f08f021b
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/backends/tflmi.cmake
@@ -0,0 +1,5 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tflm")
+ENDIF()
+
+SET(TFLITE_REQUIRED_COMPONENTS a b c)
diff --git a/resources/platforms/zephyr/project2/cmake/backends/tvmaot.cmake b/resources/platforms/zephyr/project2/cmake/backends/tvmaot.cmake
new file mode 100644
index 000000000..c14f83de3
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/backends/tvmaot.cmake
@@ -0,0 +1,10 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DTVMCG_DEBUG_ALLOCATIONS")
+
+SET(TVM_LIB tvm_aot_rt)
+SET(TVM_WRAPPER_FILENAME aot_wrapper.c)
diff --git a/resources/platforms/zephyr/project2/cmake/backends/tvmaotplus.cmake b/resources/platforms/zephyr/project2/cmake/backends/tvmaotplus.cmake
new file mode 100644
index 000000000..c14f83de3
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/backends/tvmaotplus.cmake
@@ -0,0 +1,10 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DTVMCG_DEBUG_ALLOCATIONS")
+
+SET(TVM_LIB tvm_aot_rt)
+SET(TVM_WRAPPER_FILENAME aot_wrapper.c)
diff --git a/resources/platforms/zephyr/project2/cmake/backends/tvmcg.cmake b/resources/platforms/zephyr/project2/cmake/backends/tvmcg.cmake
new file mode 100644
index 000000000..509174438
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/backends/tvmcg.cmake
@@ -0,0 +1,10 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DTVMAOT_DEBUG_ALLOCATIONS")
+
+SET(TVM_LIB tvm_static_rt)
+SET(TVM_WRAPPER_FILENAME staticrt.c)
diff --git a/resources/platforms/zephyr/project2/cmake/backends/tvmllvm.cmake b/resources/platforms/zephyr/project2/cmake/backends/tvmllvm.cmake
new file mode 100644
index 000000000..faff7adda
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/backends/tvmllvm.cmake
@@ -0,0 +1,8 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(TVM_LIB tvm_graph_rt)
+SET(TVM_WRAPPER_FILENAME rt_wrapper.c)
diff --git a/resources/platforms/zephyr/project2/cmake/backends/tvmrt.cmake b/resources/platforms/zephyr/project2/cmake/backends/tvmrt.cmake
new file mode 100644
index 000000000..faff7adda
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/backends/tvmrt.cmake
@@ -0,0 +1,8 @@
+IF("${MLONMCU_FRAMEWORK}" STREQUAL "")
+    SET(MLONMCU_FRAMEWORK "tvm")
+ENDIF()
+
+SET(TVM_REQUIRED_COMPONENTS a b c)
+
+SET(TVM_LIB tvm_graph_rt)
+SET(TVM_WRAPPER_FILENAME rt_wrapper.c)
diff --git a/resources/platforms/zephyr/project2/cmake/extras.cmake b/resources/platforms/zephyr/project2/cmake/extras.cmake
new file mode 100644
index 000000000..c4ce8e0bf
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/extras.cmake
@@ -0,0 +1,9 @@
+IF(DEFINED DEBUG_ARENA_USAGE)
+    IF(DEBUG_ARENA_USAGE)
+        SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DDEBUG_ARENA_USAGE=1")
+        SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG_ARENA_USAGE=1")
+    ELSE()
+        SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DDEBUG_ARENA_USAGE=0")
+        SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG_ARENA_USAGE=0")
+    ENDIF()
+ENDIF()
diff --git a/resources/platforms/zephyr/project2/cmake/features/auto_vectorize_gcc.cmake b/resources/platforms/zephyr/project2/cmake/features/auto_vectorize_gcc.cmake
new file mode 100644
index 000000000..737bc9879
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/features/auto_vectorize_gcc.cmake
@@ -0,0 +1,65 @@
+IF(NOT RISCV_VEXT)
+    MESSAGE(FATAL_ERROR "RISCV_AUTO_VECTORIZE requires RISCV_VEXT")
+ENDIF()
+IF(RISCV_RVV_VLEN)
+    SET(VLEN ${RISCV_RVV_VLEN})
+ELSE()
+    SET(VLEN "?")
+ENDIF()
+SET(CMAKE_CXX_FLAGS_RELEASE
+    "${CMAKE_CXX_FLAGS_RELEASE} \
+    -ftree-vectorize \
+    -mriscv-vector-bits=${VLEN} \
+"
+)
+SET(CMAKE_C_FLAGS_RELEASE
+    "${CMAKE_C_FLAGS_RELEASE} \
+    -ftree-vectorize \
+    -mriscv-vector-bits=${VLEN} \
+"
+)
+IF(DEFINED RISCV_AUTO_VECTORIZE_LOOP)
+    IF(NOT RISCV_AUTO_VECTORIZE_LOOP)
+        SET(CMAKE_CXX_FLAGS_RELEASE
+            "${CMAKE_CXX_FLAGS_RELEASE} \
+            -fno-tree-loop-vectorize \
+        "
+        )
+        SET(CMAKE_C_FLAGS_RELEASE
+            "${CMAKE_C_FLAGS_RELEASE} \
+            -fno-tree-loop-vectorize \
+        "
+        )
+    ENDIF()
+ENDIF()
+IF(DEFINED RISCV_AUTO_VECTORIZE_SLP)
+    IF(NOT RISCV_AUTO_VECTORIZE_SLP)
+        SET(CMAKE_CXX_FLAGS_RELEASE
+            "${CMAKE_CXX_FLAGS_RELEASE} \
+            -fno-tree-slp-vectorize \
+        "
+        )
+        SET(CMAKE_C_FLAGS_RELEASE
+            "${CMAKE_C_FLAGS_RELEASE} \
+            -fno-tree-slp-vectorize \
+        "
+        )
+    ENDIF()
+ENDIF()
+# Also interesting:
+# -mriscv-vector-lmul=<lmul>  Set the vf using lmul in auto-vectorization
+# -fsimd-cost-model=[unlimited|dynamic|cheap|very-cheap] Specifies the vectorization cost model for code marked with a simd directive
+IF(RISCV_AUTO_VECTORIZE_VERBOSE)
+    SET(CMAKE_CXX_FLAGS_RELEASE
+        "${CMAKE_CXX_FLAGS_RELEASE} \
+        -fopt-info-vec \
+        -fopt-info-vec-missed \
+    "
+    )
+    SET(CMAKE_C_FLAGS_RELEASE
+        "${CMAKE_C_FLAGS_RELEASE} \
+        -fopt-info-vec \
+        -fopt-info-vec-missed \
+    "
+    )
+ENDIF()
diff --git a/resources/platforms/zephyr/project2/cmake/features/auto_vectorize_llvm.cmake b/resources/platforms/zephyr/project2/cmake/features/auto_vectorize_llvm.cmake
new file mode 100644
index 000000000..9cbd1330a
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/features/auto_vectorize_llvm.cmake
@@ -0,0 +1,20 @@
+IF(NOT RISCV_VEXT)
+    MESSAGE(FATAL_ERROR "RISCV_AUTO_VECTORIZE requires RISCV_VEXT")
+ENDIF()
+IF(RISCV_RVV_VLEN)
+    SET(VLEN ${RISCV_RVV_VLEN})
+ELSE()
+    SET(VLEN "?")
+ENDIF()
+ SET(CMAKE_CXX_FLAGS_RELEASE
+     "${CMAKE_CXX_FLAGS_RELEASE} \
+     -mllvm \
+     --riscv-v-vector-bits-min=${VLEN} \
+ "
+ )
+ SET(CMAKE_C_FLAGS_RELEASE
+     "${CMAKE_C_FLAGS_RELEASE} \
+     -mllvm \
+     --riscv-v-vector-bits-min=${VLEN} \
+ "
+ )
diff --git a/resources/platforms/zephyr/project2/cmake/features/cmsisnn.cmake b/resources/platforms/zephyr/project2/cmake/features/cmsisnn.cmake
new file mode 100644
index 000000000..06c92dd02
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/features/cmsisnn.cmake
@@ -0,0 +1,86 @@
+IF(NOT CMSISNN_DIR)
+    MESSAGE(FATAL_ERROR "Missing value: CMSISNN_DIR")
+ENDIF()
+
+IF(NOT ARM_MVEI)
+    SET(USE_MVEI OFF)
+ELSE()
+    SET(USE_MVEI ON)
+ENDIF()
+
+IF(NOT ARM_DSP)
+    SET(USE_DSP OFF)
+ELSE()
+    SET(USE_DSP ON)
+ENDIF()
+
+SET(BUILD_FLAGS "")
+# IF(USE_MVEI) SET(BUILD_FLAGS "${BUILD_FLAGS} -D__ARM_FEATURE_MVE=1 -DARM_MATH_MVEI=1") ENDIF() IF(USE_DSP)
+# SET(BUILD_FLAGS "${BUILD_FLAGS} -D__ARM_FEATURE_DSP=1 -DARM_MATH_DSP=1") ENDIF()
+
+SET(CMSISNN_INCLUDE_DIRS ${CMSISNN_DIR} ${CMSISNN_DIR}/CMSIS/Core/Include ${CMSISNN_DIR}/CMSIS/NN/Include
+                         ${CMSISNN_DIR}/CMSIS/DSP/Include
+)
+
+# TODO: propagarting all toolchain specific vars does not scale well
+
+MESSAGE(STATUS "ARM_CPU=${ARM_CPU}")
+INCLUDE(ExternalProject)
+EXTERNALPROJECT_ADD(
+    cmsisnn
+    PREFIX cmsisnn
+    SOURCE_DIR ${CMSISNN_DIR}/CMSIS/NN/
+    CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+               -DCMAKE_C_FLAGS:STRING=${BUILD_FLAGS}
+               -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_FILE}
+               -DTC_PREFIX=${TC_PREFIX}
+               -DEXE_EXT=${EXE_EXT}
+               -DARM_CPU=${ARM_CPU}
+               -DARM_FLOAT_ABI=${ARM_FLOAT_ABI}
+               -DARM_FPU=${ARM_FPU}
+               -DRISCV_ARCH=${RISCV_ARCH}
+               -DRISCV_ABI=${RISCV_ABI}
+    BUILD_COMMAND "${CMAKE_COMMAND}" --build .
+    INSTALL_COMMAND ""
+)
+
+EXTERNALPROJECT_GET_PROPERTY(cmsisnn BINARY_DIR)
+SET(CMSISNN_LIB ${BINARY_DIR}/Source/libcmsis-nn.a)
+
+# TFLite integration
+IF(TFLM_OPTIMIZED_KERNEL_LIB)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_LIB ${CMSISNN_LIB})
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_LIB ${CMSISNN_LIB})
+ENDIF()
+
+IF(TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR ${CMSISNN_INCLUDE_DIRS})
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR ${CMSISNN_INCLUDE_DIRS})
+ENDIF()
+
+IF(TFLM_OPTIMIZED_KERNEL_DEPS)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_DEPS cmsisnn)
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_DEPS cmsisnn)
+ENDIF()
+
+# TVM integration
+IF(TVM_EXTRA_LIBS)
+    LIST(APPEND TVM_EXTRA_LIBS ${CMSISNN_LIB})
+ELSE()
+    SET(TVM_EXTRA_LIBS ${CMSISNN_LIB})
+ENDIF()
+
+IF(TVM_EXTRA_INCS)
+    LIST(APPEND TVM_EXTRA_INCS ${CMSISNN_INCLUDE_DIRS})
+ELSE()
+    SET(TVM_EXTRA_INCS ${CMSISNN_INCLUDE_DIRS})
+ENDIF()
+
+IF(TVM_EXTRA_DEPS)
+    LIST(APPEND TVM_EXTRA_DEPS cmsisnn)
+ELSE()
+    SET(TVM_EXTRA_DEPS cmsisnn)
+ENDIF()
diff --git a/resources/platforms/zephyr/project2/cmake/features/muriscvnn.cmake b/resources/platforms/zephyr/project2/cmake/features/muriscvnn.cmake
new file mode 100644
index 000000000..6f84489a4
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/features/muriscvnn.cmake
@@ -0,0 +1,87 @@
+IF(NOT MURISCVNN_DIR)
+    MESSAGE(FATAL_ERROR "Missing value: MURISCVNN_DIR")
+ENDIF()
+
+IF(NOT RISCV_VEXT)
+    SET(USE_VEXT OFF)
+ELSE()
+    SET(USE_VEXT ON)
+ENDIF()
+
+IF(NOT RISCV_PEXT)
+    SET(USE_PEXT OFF)
+ELSE()
+    SET(USE_PEXT ON)
+ENDIF()
+
+IF(NOT MURISCVNN_TOOLCHAIN)
+    SET(MURISCVVN_TOOLCHAIN GCC)
+    # SET(MURISCVVN_TOOLCHAIN NONE)
+ENDIF()
+
+SET(MURISCVNN_INCLUDE_DIRS ${MURISCVNN_DIR}/Include ${MURISCVNN_DIR}/Include/CMSIS/NN/Include)
+
+# TODO: propagarting all toolchain specific vars does not scale well
+
+INCLUDE(ExternalProject)
+EXTERNALPROJECT_ADD(
+    muriscvnn
+    PREFIX muriscvnn
+    SOURCE_DIR ${MURISCVNN_DIR}
+    CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
+               -DUSE_VEXT=${USE_VEXT}
+               -DUSE_PEXT=${USE_PEXT}
+               -DTOOLCHAIN=${MURISCVNN_TOOLCHAIN}
+               -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_FILE}
+               -DENABLE_TESTS=OFF
+               -DTC_PREFIX=${TC_PREFIX}
+               -DEXE_EXT=${EXE_EXT}
+               -DRISCV_ARCH=${RISCV_ARCH}
+               -DRISCV_ABI=${RISCV_ABI}
+               -DARM_CPU=${ARM_CPU}
+               -DARM_FLOAT_ABI=${ARM_FLOAT_ABI}
+               -DARM_FPU=${ARM_FPU}
+    BUILD_COMMAND "${CMAKE_COMMAND}" --build .
+    INSTALL_COMMAND ""
+)
+
+EXTERNALPROJECT_GET_PROPERTY(muriscvnn BINARY_DIR)
+SET(MURISCVNN_LIB ${BINARY_DIR}/Source/libmuriscv_nn.a)
+
+# TFLite integration
+IF(TFLM_OPTIMIZED_KERNEL_LIB)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_LIB ${MURISCVNN_LIB})
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_LIB ${MURISCVNN_LIB})
+ENDIF()
+
+IF(TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR ${MURISCVNN_INCLUDE_DIRS})
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_INCLUDE_DIR ${MURISCVNN_INCLUDE_DIRS})
+ENDIF()
+
+IF(TFLM_OPTIMIZED_KERNEL_DEPS)
+    LIST(APPEND TFLM_OPTIMIZED_KERNEL_DEPS muriscvnn)
+ELSE()
+    SET(TFLM_OPTIMIZED_KERNEL_DEPS muriscvnn)
+ENDIF()
+
+# TVM integration
+IF(TVM_EXTRA_LIBS)
+    LIST(APPEND TVM_EXTRA_LIBS ${MURISCVNN_LIB})
+ELSE()
+    SET(TVM_EXTRA_LIBS ${MURISCVNN_LIB})
+ENDIF()
+
+IF(TVM_EXTRA_INCS)
+    LIST(APPEND TVM_EXTRA_INCS ${MURISCVNN_INCLUDE_DIRS})
+ELSE()
+    SET(TVM_EXTRA_INCS ${MURISCVNN_INCLUDE_DIRS})
+ENDIF()
+
+IF(TVM_EXTRA_DEPS)
+    LIST(APPEND TVM_EXTRA_DEPS muriscvnn)
+ELSE()
+    SET(TVM_EXTRA_DEPS muriscvnn)
+ENDIF()
diff --git a/resources/platforms/zephyr/project2/cmake/frameworks/tflm.cmake b/resources/platforms/zephyr/project2/cmake/frameworks/tflm.cmake
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/frameworks/tflm.cmake
@@ -0,0 +1,1 @@
+
diff --git a/resources/platforms/zephyr/project2/cmake/frameworks/tflm_mlif.cmake b/resources/platforms/zephyr/project2/cmake/frameworks/tflm_mlif.cmake
new file mode 100644
index 000000000..614226b6a
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/frameworks/tflm_mlif.cmake
@@ -0,0 +1,8 @@
+SET(TF_OUT_DIR ${CONFIG_MLONMCU_CODEGEN_DIR}/)
+SET(GEN_SRC ${TF_OUT_DIR}/model.cc)
+GET_FILENAME_COMPONENT(GEN_SRC ${GEN_SRC} REALPATH)
+GET_FILENAME_COMPONENT(EXTRA_INC ${GEN_SRC} DIRECTORY)
+SET(EXTRA_SRC ml_interface_tflm.cc ${GEN_SRC})
+SET(EXTRA_LIBS tflm)
+SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DTFLMC_DEBUG_ALLOCATIONS")
+LIST(APPEND EXTRA_LIBS ${CUSTOM_OPS_LIBS})
diff --git a/resources/platforms/zephyr/project2/cmake/frameworks/tvm.cmake b/resources/platforms/zephyr/project2/cmake/frameworks/tvm.cmake
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/frameworks/tvm.cmake
@@ -0,0 +1,1 @@
+
diff --git a/resources/platforms/zephyr/project2/cmake/frameworks/tvm_mlif.cmake b/resources/platforms/zephyr/project2/cmake/frameworks/tvm_mlif.cmake
new file mode 100644
index 000000000..927ba23e8
--- /dev/null
+++ b/resources/platforms/zephyr/project2/cmake/frameworks/tvm_mlif.cmake
@@ -0,0 +1,37 @@
+SET(TVM_OUT_DIR ${CONFIG_MLONMCU_CODEGEN_DIR}/codegen/host/)
+SET(EXTRA_SRC ml_interface_tvm.c)
+
+FILE(GLOB TVM_SRCS ${TVM_OUT_DIR}/src/*_lib*.c ${TVM_OUT_DIR}/src/*_lib*.cc)
+
+# IF(NOT TVM_SRCS)
+#     MESSAGE(STATUS "NO SOURCES")
+#     FILE(GLOB TVM_OBJS ${TVM_OUT_DIR}/lib/*_lib*.o)
+#     MESSAGE(STATUS "TVM_OBJS=${TVM_OBJS}")
+#     # ADD_LIBRARY(tvm_extension OBJECT IMPORTED)
+#
+#     # SET_PROPERTY(TARGET tvm_extension PROPERTY
+#     #     IMPORTED_OBJECTS ${TVM_OBJS}
+#     # )
+# ELSE()
+#     # Need this in extra target to avoid circular dependency .
+#     # ADD_LIBRARY(tvm_extension STATIC ${TVM_SRCS})
+#     TARGET_INCLUDE_DIRECTORIES(tvm_extension PUBLIC ${TVM_HEADERS} ${TVM_OUT_DIR}/include ${CONFIG_MLONMCU_CODEGEN_DIR})
+#     TARGET_LINK_LIBRARIES(tvm_extension PUBLIC m)
+#     TARGET_LINK_LIBRARIES(tvm_extension PUBLIC ${TVM_LIB})
+# ENDIF()
+#
+# SET(EXTRA_SRC ${EXTRA_SRC} ${CONFIG_MLONMCU_CODEGEN_DIR}/${TVM_WRAPPER_FILENAME})
+# TARGET_LINK_LIBRARIES(${TVM_LIB} PUBLIC tvm_extension)
+# SET(EXTRA_INC ${TVM_OUT_DIR}/include ${CONFIG_MLONMCU_CODEGEN_DIR})
+#
+# SET(EXTRA_LIBS tvm_extension ${TVM_LIB})
+#
+# FOREACH(ENTRY ${CONFIG_TVM_EXTRA_LIBS})
+#     TARGET_LINK_LIBRARIES(tvm_extension PUBLIC ${ENTRY})
+# ENDFOREACH()
+# FOREACH(ENTRY ${CONFIG_TVM_EXTRA_INCS})
+#     TARGET_INCLUDE_DIRECTORIES(tvm_extension PUBLIC ${ENTRY})
+# ENDFOREACH()
+# FOREACH(ENTRY ${CONFIG_TVM_EXTRA_DEPS})
+#     ADD_DEPENDENCIES(tvm_extension ${ENTRY})
+# ENDFOREACH()
diff --git a/resources/platforms/zephyr/project2/default_data.c b/resources/platforms/zephyr/project2/default_data.c
new file mode 100644
index 000000000..933b986ba
--- /dev/null
+++ b/resources/platforms/zephyr/project2/default_data.c
@@ -0,0 +1,8 @@
+#include "ml_interface.h"
+#include <stddef.h>
+const int num_data_buffers_in = 0;
+const int num_data_buffers_out = 0;
+const unsigned char *const data_buffers_in[] = {};
+const unsigned char *const data_buffers_out[] = {};
+const size_t data_size_in[] = {};
+const size_t data_size_out[] = {};
diff --git a/resources/platforms/zephyr/project2/default_model_support/CMakeLists.txt b/resources/platforms/zephyr/project2/default_model_support/CMakeLists.txt
new file mode 100644
index 000000000..11edd6589
--- /dev/null
+++ b/resources/platforms/zephyr/project2/default_model_support/CMakeLists.txt
@@ -0,0 +1,3 @@
+ADD_LIBRARY(model_support STATIC process_input.c process_output.c)
+# TARGET_LINK_LIBRARIES(model_support PUBLIC ml_interface_base)
+TARGET_INCLUDE_DIRECTORIES(model_support PUBLIC ../lib/support)
diff --git a/resources/platforms/zephyr/project2/default_model_support/process_input.c b/resources/platforms/zephyr/project2/default_model_support/process_input.c
new file mode 100644
index 000000000..57b180bc7
--- /dev/null
+++ b/resources/platforms/zephyr/project2/default_model_support/process_input.c
@@ -0,0 +1,17 @@
+#include <stdlib.h>
+#include <memory.h>
+
+#include "printing.h"
+#include "exit.h"
+
+void mlif_process_input(const void *in_data, size_t in_size, void *model_input_ptr, size_t model_input_sz) {
+  if (in_size != 0) {
+    if (in_size != model_input_sz) {
+      DBGPRINTF("MLIF: Given input size (%lu) does not match model input buffer size (%lu)!\n", in_size,
+                model_input_sz);
+      exit(EXIT_MLIF_INVALID_SIZE);
+    }
+  }
+
+  memcpy(model_input_ptr, in_data, in_size);
+}
diff --git a/resources/platforms/zephyr/project2/default_model_support/process_output.c b/resources/platforms/zephyr/project2/default_model_support/process_output.c
new file mode 100644
index 000000000..431d340f4
--- /dev/null
+++ b/resources/platforms/zephyr/project2/default_model_support/process_output.c
@@ -0,0 +1,27 @@
+#include <stdlib.h>
+#include <memory.h>
+
+#include "printing.h"
+#include "exit.h"
+
+void mlif_process_output(void *model_output_ptr, size_t model_output_sz, const void *expected_out_data,
+                         size_t expected_out_size) {
+#ifdef _DEBUG
+  if (model_output_sz >= 4) {
+    DBGPRINTF("MLIF: First float of output: %f\n", *(float *)model_output_ptr);
+  }
+  DBGPRINTF("MLIF: Model output data: ");
+  for (size_t i = 0; i < model_output_sz; i++) {
+    DBGPRINTF("\\x%02X", ((unsigned char *)model_output_ptr)[i]);
+    fflush(0);
+  }
+  DBGPRINTF("\n");
+#endif
+
+  if (model_output_sz == expected_out_size && memcmp(model_output_ptr, expected_out_data, model_output_sz) == 0) {
+    DBGPRINTF("MLIF: Output data matches expected data\n");
+  } else {
+    DBGPRINTF("MLIF: Wrong output data!\n");
+    exit(EXIT_MLIF_MISSMATCH);
+  }
+}
diff --git a/resources/platforms/zephyr/project2/lib/CMakeLists.txt b/resources/platforms/zephyr/project2/lib/CMakeLists.txt
new file mode 100644
index 000000000..156b9f1df
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/CMakeLists.txt
@@ -0,0 +1,13 @@
+PROJECT(target_sw_lib)
+
+ADD_SUBDIRECTORY(support)
+# ADD_SUBDIRECTORY(ml_interface)
+
+SET(TARGET_LIB_SOURCE targets/${TARGET_SYSTEM}.c)
+IF(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_LIB_SOURCE})
+    SET(TARGET_LIB_SOURCE targets/default.c)
+ENDIF()
+
+ADD_LIBRARY(${PROJECT_NAME} STATIC drivers/fake_input.c ${TARGET_LIB_SOURCE})
+TARGET_INCLUDE_DIRECTORIES(${PROJECT_NAME} PUBLIC lib/drivers lib/targets)
+# TARGET_LINK_LIBRARIES(${PROJECT_NAME} PUBLIC support ml_interface)
diff --git a/resources/platforms/zephyr/project2/lib/drivers/fake_input.c b/resources/platforms/zephyr/project2/lib/drivers/fake_input.c
new file mode 100644
index 000000000..164127a67
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/drivers/fake_input.c
@@ -0,0 +1,10 @@
+#include "fake_input.h"
+#include <stdint.h>
+
+void FakeInput_GetData(void *out, size_t len) {
+  volatile uint8_t *p = 0xc0000000;
+
+  for (size_t i = 0; i < len; i++) {
+    *(uint8_t *)out = *(p + i);
+  }
+}
diff --git a/resources/platforms/zephyr/project2/lib/drivers/fake_input.h b/resources/platforms/zephyr/project2/lib/drivers/fake_input.h
new file mode 100644
index 000000000..04e47811a
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/drivers/fake_input.h
@@ -0,0 +1,8 @@
+#ifndef DRIVER_FAKE_INPUT_H
+#define DRIVER_FAKE_INPUT_H
+
+#include <stddef.h>
+
+void FakeInput_GetData(void *out, size_t len);
+
+#endif
diff --git a/resources/platforms/zephyr/project2/lib/support/CMakeLists.txt b/resources/platforms/zephyr/project2/lib/support/CMakeLists.txt
new file mode 100644
index 000000000..0f9fa21fc
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/support/CMakeLists.txt
@@ -0,0 +1,2 @@
+ADD_LIBRARY(support STATIC dummy.c)
+TARGET_INCLUDE_DIRECTORIES(support PUBLIC .)
diff --git a/resources/platforms/zephyr/project2/lib/support/dummy.c b/resources/platforms/zephyr/project2/lib/support/dummy.c
new file mode 100644
index 000000000..e69de29bb
diff --git a/resources/platforms/zephyr/project2/lib/support/exit.h b/resources/platforms/zephyr/project2/lib/support/exit.h
new file mode 100644
index 000000000..bc52eef1b
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/support/exit.h
@@ -0,0 +1,8 @@
+#ifndef SUPPORT_EXIT_H
+#define SUPPORT_EXIT_H
+
+#define EXIT_MLIF_BASE (0x10)
+#define EXIT_MLIF_INVALID_SIZE (EXIT_MLIF_BASE + 1)
+#define EXIT_MLIF_MISSMATCH (EXIT_MLIF_BASE + 2)
+
+#endif  // SUPPORT_EXIT_H
diff --git a/resources/platforms/zephyr/project2/lib/support/printing.h b/resources/platforms/zephyr/project2/lib/support/printing.h
new file mode 100644
index 000000000..047a412c5
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/support/printing.h
@@ -0,0 +1,11 @@
+#ifndef SUPPORT_PRINTING_H
+#define SUPPORT_PRINTING_H
+
+#ifdef _DEBUG
+#include <stdio.h>
+#define DBGPRINTF(format, ...) printf(format, ##__VA_ARGS__)
+#else
+#define DBGPRINTF(format, ...)
+#endif
+
+#endif
diff --git a/resources/platforms/zephyr/project2/lib/support/quantize.h b/resources/platforms/zephyr/project2/lib/support/quantize.h
new file mode 100644
index 000000000..ce80acb71
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/support/quantize.h
@@ -0,0 +1,23 @@
+#ifndef SUPPORT_QUANTIZE_H
+#define SUPPORT_QUANTIZE_H
+
+#include <stdint.h>
+#include <limits.h>
+#include <math.h>
+
+inline float DequantizeInt8ToFloat(int8_t value, float scale, int zero_point) {
+  return static_cast<float>(value - zero_point) * scale;
+}
+
+inline int8_t QuantizeFloatToInt8(float value, float scale, int zero_point) {
+  int32_t result = round(value / scale) + zero_point;
+  if (result < INT8_MIN) {
+    result = INT8_MIN;
+  }
+  if (result > INT8_MAX) {
+    result = INT8_MAX;
+  }
+  return static_cast<int8_t>(result);
+}
+
+#endif
diff --git a/resources/platforms/zephyr/project2/lib/targets/corstone300.c b/resources/platforms/zephyr/project2/lib/targets/corstone300.c
new file mode 100644
index 000000000..225363643
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/targets/corstone300.c
@@ -0,0 +1,62 @@
+#include <stdio.h>
+
+// DWT (Data Watchpoint and Trace) registers, only exists on ARM Cortex with a
+// DWT unit.
+#define KIN1_DWT_CONTROL (*((volatile uint32_t*)0xE0001000))
+
+// DWT Control register.
+#define KIN1_DWT_CYCCNTENA_BIT (1UL << 0)
+
+// CYCCNTENA bit in DWT_CONTROL register.
+#define KIN1_DWT_CYCCNT (*((volatile uint32_t*)0xE0001004))
+
+// DWT Cycle Counter register.
+#define KIN1_DEMCR (*((volatile uint32_t*)0xE000EDFC))
+
+// DEMCR: Debug Exception and Monitor Control Register.
+#define KIN1_TRCENA_BIT (1UL << 24)
+
+// Trace enable bit in DEMCR register.
+#define KIN1_LAR (*((volatile uint32_t*)0xE0001FB0))
+
+// Unlock access to DWT (ITM, etc.)registers.
+#define KIN1_UnlockAccessToDWT() KIN1_LAR = 0xC5ACCE55;
+
+// TRCENA: Enable trace and debug block DEMCR (Debug Exception and Monitor
+// Control Register.
+#define KIN1_InitCycleCounter() KIN1_DEMCR |= KIN1_TRCENA_BIT
+
+#define KIN1_ResetCycleCounter() KIN1_DWT_CYCCNT = 0
+#define KIN1_EnableCycleCounter() KIN1_DWT_CONTROL |= KIN1_DWT_CYCCNTENA_BIT
+#define KIN1_DisableCycleCounter() KIN1_DWT_CONTROL &= ~KIN1_DWT_CYCCNTENA_BIT
+#define KIN1_GetCycleCounter() KIN1_DWT_CYCCNT
+
+int32_t ticks_per_second() { return 25e6; }
+
+int32_t GetCurrentTimeTicks() { return KIN1_GetCycleCounter(); }
+
+extern void uart_init(void);
+
+static int32_t start_cycles = 0;
+
+void init_target() {
+  uart_init();
+  KIN1_UnlockAccessToDWT();
+  KIN1_InitCycleCounter();
+  KIN1_ResetCycleCounter();
+  KIN1_EnableCycleCounter();
+  int32_t ticks = GetCurrentTimeTicks();
+  start_cycles = ticks;
+  printf("GetCurrentTimeTicks=%ld\n", ticks);
+}
+
+void deinit_target() {
+  int32_t stop_cycles = GetCurrentTimeTicks();
+  int32_t diff_cycles = stop_cycles - start_cycles;
+  // int32_t diff_ms = diff_cycles / (ticks_per_second() / 1000);
+  // printf("Total Time: %ld ms\n", diff_ms);
+  printf("Total Cycles: %ld\n", diff_cycles);
+  printf("EXITTHESIM\n");
+  while (1)
+    ;
+}
diff --git a/resources/platforms/zephyr/project2/lib/targets/default.c b/resources/platforms/zephyr/project2/lib/targets/default.c
new file mode 100644
index 000000000..e58e59286
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/targets/default.c
@@ -0,0 +1,2 @@
+__attribute__((weak)) void init_target() {}
+__attribute__((weak)) void deinit_target() {}
diff --git a/resources/platforms/zephyr/project2/lib/targets/etiss_pulpino.c b/resources/platforms/zephyr/project2/lib/targets/etiss_pulpino.c
new file mode 100644
index 000000000..f083b4863
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/targets/etiss_pulpino.c
@@ -0,0 +1,112 @@
+#include <stdio.h>
+
+/* How many cycles (rdcycle) per second (OVPsim and Spike). */
+#define RDCYCLE_PER_SECOND 100000000UL
+
+/**
+ * @brief Returns the number of clock cycles executed by the processor.
+ * Overflows after 42.9 seconds on a 100 MIPS and with CPI = 1 (OVPsim).
+ */
+static inline uint32_t rdcycle(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  __asm__ volatile("rdcycle %0" : "=r"(cycles));
+  return cycles;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the full 64bit register cycle register, which holds the
+ * number of clock cycles executed by the processor.
+ */
+static inline uint64_t rdcycle64() {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  uint32_t cyclesh1;
+  uint32_t cyclesh2;
+
+  /* Reads are not atomic. So ensure, that we are never reading inconsistent
+   * values from the 64bit hardware register. */
+  do {
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh1));
+    __asm__ volatile("rdcycle %0" : "=r"(cycles));
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh2));
+  } while (cyclesh1 != cyclesh2);
+
+  return (((uint64_t)cyclesh1) << 32) | cycles;
+#else
+  return 0;
+#endif
+}
+
+/* How many time cycles (rdtime) per second (OVPsim and Spike). */
+#define RDTIME_PER_SECOND 1000000UL
+
+/**
+ * @brief Returns wall-clock real time that has passed from an arbitrary start time in the past.
+ */
+static inline uint32_t rdtime(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t time;
+  __asm__ volatile("rdtime %0" : "=r"(time));
+  return time;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the number of instructions retired by the processor.
+ */
+static inline uint32_t rdinstret(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t instret;
+  __asm__ volatile("rdinstret %0" : "=r"(instret));
+  return instret;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_fext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_vext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<9+1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+static uint64_t start_cycles = 0;
+
+void init_target() {
+  // enable_fext();
+#ifdef USE_VEXT
+  // enable_vext();
+#endif
+  start_cycles = rdcycle64();
+}
+
+void deinit_target() {
+  uint64_t stop_cycles = rdcycle64();
+  uint64_t diff_cycles = stop_cycles - start_cycles;
+  float diff_ms = 0;  // unimplemented (see RDCYCLE_PER_SECOND)
+  printf("Total Cycles: %lld\n", stop_cycles - start_cycles);
+}
diff --git a/resources/platforms/zephyr/project2/lib/targets/generic_riscv.c b/resources/platforms/zephyr/project2/lib/targets/generic_riscv.c
new file mode 100644
index 000000000..f083b4863
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/targets/generic_riscv.c
@@ -0,0 +1,112 @@
+#include <stdio.h>
+
+/* How many cycles (rdcycle) per second (OVPsim and Spike). */
+#define RDCYCLE_PER_SECOND 100000000UL
+
+/**
+ * @brief Returns the number of clock cycles executed by the processor.
+ * Overflows after 42.9 seconds on a 100 MIPS and with CPI = 1 (OVPsim).
+ */
+static inline uint32_t rdcycle(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  __asm__ volatile("rdcycle %0" : "=r"(cycles));
+  return cycles;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the full 64bit register cycle register, which holds the
+ * number of clock cycles executed by the processor.
+ */
+static inline uint64_t rdcycle64() {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  uint32_t cyclesh1;
+  uint32_t cyclesh2;
+
+  /* Reads are not atomic. So ensure, that we are never reading inconsistent
+   * values from the 64bit hardware register. */
+  do {
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh1));
+    __asm__ volatile("rdcycle %0" : "=r"(cycles));
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh2));
+  } while (cyclesh1 != cyclesh2);
+
+  return (((uint64_t)cyclesh1) << 32) | cycles;
+#else
+  return 0;
+#endif
+}
+
+/* How many time cycles (rdtime) per second (OVPsim and Spike). */
+#define RDTIME_PER_SECOND 1000000UL
+
+/**
+ * @brief Returns wall-clock real time that has passed from an arbitrary start time in the past.
+ */
+static inline uint32_t rdtime(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t time;
+  __asm__ volatile("rdtime %0" : "=r"(time));
+  return time;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the number of instructions retired by the processor.
+ */
+static inline uint32_t rdinstret(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t instret;
+  __asm__ volatile("rdinstret %0" : "=r"(instret));
+  return instret;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_fext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_vext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<9+1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+static uint64_t start_cycles = 0;
+
+void init_target() {
+  // enable_fext();
+#ifdef USE_VEXT
+  // enable_vext();
+#endif
+  start_cycles = rdcycle64();
+}
+
+void deinit_target() {
+  uint64_t stop_cycles = rdcycle64();
+  uint64_t diff_cycles = stop_cycles - start_cycles;
+  float diff_ms = 0;  // unimplemented (see RDCYCLE_PER_SECOND)
+  printf("Total Cycles: %lld\n", stop_cycles - start_cycles);
+}
diff --git a/resources/platforms/zephyr/project2/lib/targets/riscv_qemu.c b/resources/platforms/zephyr/project2/lib/targets/riscv_qemu.c
new file mode 100644
index 000000000..7660d141d
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/targets/riscv_qemu.c
@@ -0,0 +1,112 @@
+#include <stdio.h>
+
+/* How many cycles (rdcycle) per second (OVPsim and Spike). */
+#define RDCYCLE_PER_SECOND 100000000UL
+
+/**
+ * @brief Returns the number of clock cycles executed by the processor.
+ * Overflows after 42.9 seconds on a 100 MIPS and with CPI = 1 (OVPsim).
+ */
+static inline uint32_t rdcycle(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  __asm__ volatile("rdcycle %0" : "=r"(cycles));
+  return cycles;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the full 64bit register cycle register, which holds the
+ * number of clock cycles executed by the processor.
+ */
+static inline uint64_t rdcycle64() {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t cycles;
+  uint32_t cyclesh1;
+  uint32_t cyclesh2;
+
+  /* Reads are not atomic. So ensure, that we are never reading inconsistent
+   * values from the 64bit hardware register. */
+  do {
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh1));
+    __asm__ volatile("rdcycle %0" : "=r"(cycles));
+    __asm__ volatile("rdcycleh %0" : "=r"(cyclesh2));
+  } while (cyclesh1 != cyclesh2);
+
+  return (((uint64_t)cyclesh1) << 32) | cycles;
+#else
+  return 0;
+#endif
+}
+
+/* How many time cycles (rdtime) per second (OVPsim and Spike). */
+#define RDTIME_PER_SECOND 1000000UL
+
+/**
+ * @brief Returns wall-clock real time that has passed from an arbitrary start time in the past.
+ */
+static inline uint32_t rdtime(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t time;
+  __asm__ volatile("rdtime %0" : "=r"(time));
+  return time;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Returns the number of instructions retired by the processor.
+ */
+static inline uint32_t rdinstret(void) {
+#if defined(__riscv) || defined(__riscv__)
+  uint32_t instret;
+  __asm__ volatile("rdinstret %0" : "=r"(instret));
+  return instret;
+#else
+  return 0;
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_fext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+/**
+ * @brief Enables the vector extension (as well as the floating point extension).
+ */
+static inline void enable_vext(void)
+{
+#if defined(__riscv) || defined(__riscv__)
+    __asm__ volatile("li t0, 1<<9+1<<13 \n"
+                     "csrs mstatus, t0 \n" ::
+                         : "t0");
+#endif
+}
+
+static uint64_t start_cycles = 0;
+
+void init_target() {
+  enable_fext();
+#ifdef USE_VEXT
+  enable_vext();
+#endif
+  start_cycles = rdcycle64();
+}
+
+void deinit_target() {
+  uint64_t stop_cycles = rdcycle64();
+  uint64_t diff_cycles = stop_cycles - start_cycles;
+  float diff_ms = 0;  // unimplemented (see RDCYCLE_PER_SECOND)
+  printf("Total Cycles: %lld\n", stop_cycles - start_cycles);
+}
diff --git a/resources/platforms/zephyr/project2/lib/targets/target.h b/resources/platforms/zephyr/project2/lib/targets/target.h
new file mode 100644
index 000000000..990a31ac5
--- /dev/null
+++ b/resources/platforms/zephyr/project2/lib/targets/target.h
@@ -0,0 +1,2 @@
+void init_target();
+void deinit_target();
diff --git a/resources/platforms/zephyr/project2/ml_interface.c b/resources/platforms/zephyr/project2/ml_interface.c
new file mode 100644
index 000000000..c572bf192
--- /dev/null
+++ b/resources/platforms/zephyr/project2/ml_interface.c
@@ -0,0 +1,36 @@
+#include "ml_interface.h"
+#include "printing.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+__attribute__((weak)) bool mlif_request_input(void *model_input_ptr, size_t model_input_sz) {
+  static int num_done = 0;
+  if (num_done == num_data_buffers_in) {
+    static bool run_once = true;
+    if (num_data_buffers_in == 0 && run_once) {
+      // Minimal run. Just run the model without data.
+      run_once = false;
+      return true;
+    }
+    return false;
+  }
+
+  mlif_process_input(data_buffers_in[num_done], data_size_in[num_done], model_input_ptr, model_input_sz);
+  num_done++;
+  return true;
+}
+
+__attribute__((weak)) void mlif_handle_result(void *model_output_ptr, size_t model_output_sz) {
+  static int num_done = 0;
+
+  if (num_data_buffers_out == 0) {
+    return;
+  }
+
+  if (num_done < num_data_buffers_out) {
+    mlif_process_output(model_output_ptr, model_output_sz, data_buffers_out[num_done], data_size_out[num_done]);
+  }
+
+  num_done++;
+}
diff --git a/resources/platforms/zephyr/project2/ml_interface.h b/resources/platforms/zephyr/project2/ml_interface.h
new file mode 100644
index 000000000..dab3441c0
--- /dev/null
+++ b/resources/platforms/zephyr/project2/ml_interface.h
@@ -0,0 +1,47 @@
+#ifndef TARGETLIB_ML_INTERFACE_H
+#define TARGETLIB_ML_INTERFACE_H
+
+#include <stddef.h>
+#include <stdbool.h>
+
+#ifndef NUM_RUNS
+#define NUM_RUNS 1
+#endif /* NUM_RUNS */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// This runs the ML model using the callbacks above.
+// The default implementation will run with garbage data.
+void mlif_run();
+
+// These can be overridden by use code.
+
+// Provides input data for the model. The default implementation retrieves input from
+// the global variables below and fills the model input with mlif_process_input.
+bool mlif_request_input(void *model_input_ptr, size_t model_input_sz);
+// Is called when the output data is available. The default implementation
+void mlif_handle_result(void *model_output_ptr, size_t model_output_sz);
+
+// Callback for any preprocessing on the input data. Responsible for copying the data.
+void mlif_process_input(const void *in_data, size_t in_size, void *model_input_ptr, size_t model_input_sz);
+// Callback for any postprocessing on the output data. The default implementation prints
+// the output and verifies consistency with the expected output.
+void mlif_process_output(void *model_output_ptr, size_t model_output_sz, const void *expected_out_data,
+                         size_t expected_out_size);
+
+extern const int num_data_buffers_in;
+extern const int num_data_buffers_out;
+extern const unsigned char *const data_buffers_in[];
+extern const unsigned char *const data_buffers_out[];
+extern const size_t data_size_in[];
+extern const size_t data_size_out[];
+
+extern const int num;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/resources/platforms/zephyr/project2/ml_interface_tflm.cc b/resources/platforms/zephyr/project2/ml_interface_tflm.cc
new file mode 100644
index 000000000..180552bea
--- /dev/null
+++ b/resources/platforms/zephyr/project2/ml_interface_tflm.cc
@@ -0,0 +1,22 @@
+#include "ml_interface.h"
+
+#include "model.cc.h"
+
+void mlif_run() {
+  model_init();
+
+  size_t input_num = 0;
+  size_t remaining = NUM_RUNS;
+  while (mlif_request_input(model_input_ptr(input_num), model_input_size(input_num)) || remaining) {
+    if (input_num == model_inputs() - 1) {
+      model_invoke();
+      for (size_t i = 0; i < model_outputs(); i++) {
+        mlif_handle_result(model_output_ptr(i), model_output_size(i));
+      }
+      input_num = 0;
+      remaining = remaining > 0 ? remaining - 1 : 0;
+    } else {
+      input_num++;
+    }
+  }
+}
diff --git a/resources/platforms/zephyr/project2/ml_interface_tvm.c b/resources/platforms/zephyr/project2/ml_interface_tvm.c
new file mode 100644
index 000000000..61ca8d2c8
--- /dev/null
+++ b/resources/platforms/zephyr/project2/ml_interface_tvm.c
@@ -0,0 +1,22 @@
+#include "ml_interface.h"
+#include "tvm_wrapper.h"
+
+void mlif_run() {
+  TVMWrap_Init();
+
+  size_t input_num = 0;
+  size_t remaining = NUM_RUNS;
+  // inout data will only be applied in the first run!
+  while (mlif_request_input(TVMWrap_GetInputPtr(input_num), TVMWrap_GetInputSize(input_num)) || remaining) {
+    if (input_num == TVMWrap_GetNumInputs() - 1) {
+      TVMWrap_Run();
+      for (size_t i = 0; i < TVMWrap_GetNumOutputs(); i++) {
+        mlif_handle_result(TVMWrap_GetOutputPtr(i), TVMWrap_GetOutputSize(i));
+      }
+      input_num = 0;
+      remaining = remaining > 0 ? remaining - 1 : 0;
+    } else {
+      input_num++;
+    }
+  }
+}
diff --git a/resources/platforms/zephyr/project2/prj.conf b/resources/platforms/zephyr/project2/prj.conf
new file mode 100644
index 000000000..64d8cd327
--- /dev/null
+++ b/resources/platforms/zephyr/project2/prj.conf
@@ -0,0 +1,13 @@
+CONFIG_CPLUSPLUS=y
+CONFIG_NEWLIB_LIBC=y
+CONFIG_REBOOT=y
+CONFIG_STD_CPP20=y
+CONFIG_LIB_CPLUSPLUS=y
+CONFIG_TIMING_FUNCTIONS=y
+CONFIG_DEBUG=n
+CONFIG_MLONMCU_CODEGEN_DIR="/nas/ei/share/TUEIEDAscratch/ga87puy/work/mlonmcu/workspace/temp/sessions/152/runs/0"
+CONFIG_MLONMCU_BACKEND="tvmaot"
+CONFIG_MLONMCU_FRAMEWORK="tvm"
+CONFIG_TVM_CRT_CONFIG_DIR="/nas/ei/share/TUEIEDAscratch/ga87puy/work/mlonmcu/mlonmcu/../resources/frameworks/tvm/crt_config"
+CONFIG_TVM_DIR="/nas/ei/share/TUEIEDAscratch/ga87puy/work/mlonmcu/workspace/deps/src/tvm"
+CONFIG_COMPILER_OPT="-O2"
diff --git a/resources/platforms/zephyr/project2/sample.yaml b/resources/platforms/zephyr/project2/sample.yaml
new file mode 100644
index 000000000..2bdf00b93
--- /dev/null
+++ b/resources/platforms/zephyr/project2/sample.yaml
@@ -0,0 +1,19 @@
+sample:
+  description: Hello World sample, the simplest Zephyr
+    application
+  name: hello world
+common:
+    tags: introduction
+    integration_platforms:
+      - native_posix
+    harness: console
+    harness_config:
+      type: one_line
+      regex:
+        - "Hello World! (.*)"
+tests:
+  sample.basic.helloworld:
+    tags: introduction
+  sample.basic.helloworld.uefi:
+    platform_allow: qemu_x86_64
+    extra_args: CONF_FILE=prj_uefi.conf
diff --git a/resources/platforms/zephyr/project2/settings.json b/resources/platforms/zephyr/project2/settings.json
new file mode 100644
index 000000000..811e6a903
--- /dev/null
+++ b/resources/platforms/zephyr/project2/settings.json
@@ -0,0 +1,6 @@
+{
+    // "cortex-debug.armToolchainPath": "/nfs/TUEIEDAscratch/ga87puy/work/mlonmcu/workspace/deps/install/zephyr/sdk/arm-zephyr-eabi/bin",
+    "cortex-debug.armToolchainPath": "/nfs/TUEIEDAscratch/ga87puy/work/mlonmcu/workspace/deps/install/arm_gcc/bin",
+    "cortex-debug.openocdPath": "/nfs/TUEIEDAscratch/ga87puy/work/mlonmcu/workspace/deps/install/zephyr/sdk/sysroots/x86_64-pokysdk-linux/usr/bin/openocd",
+    // "cortex-debug.gdbPath": "/nfs/TUEIEDAscratch/ga87puy/work/mlonmcu/workspace/deps/install/zephyr/sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb",
+}
\ No newline at end of file
diff --git a/resources/platforms/zephyr/project2/src/main.c b/resources/platforms/zephyr/project2/src/main.c
new file mode 100644
index 000000000..4e5403e30
--- /dev/null
+++ b/resources/platforms/zephyr/project2/src/main.c
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2012-2014 Wind River Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr.h>
+#include <stdint.h>
+#include <sys/printk.h>
+#include <sys/reboot.h>
+#include <timing/timing.h>
+// #include <zephyr/kernel.h>
+
+// void init_target();
+// void deinit_target();
+
+int main() {
+  // init_target();
+  timing_init();
+  timing_start();
+  printk("MLonMCU: START\n");
+  timing_t start_time, end_time;
+  start_time = timing_counter_get();
+  mlif_run();
+  end_time = timing_counter_get();
+  uint64_t cycles = timing_cycles_get(&start_time, &end_time);
+  uint64_t ns_spent = timing_cycles_to_ns(cycles);
+
+  printk("Total Time: %llu us\n", (ns_spent/1000));
+  printk("Total Cycles: %llu\n", cycles);
+  printk("MLonMCU: STOP\n");
+  // deinit_target();
+  sys_reboot(0);
+  return 0;
+}
diff --git a/resources/templates/default.yml.j2 b/resources/templates/default.yml.j2
index 82511ddab..4f81c11e5 100644
--- a/resources/templates/default.yml.j2
+++ b/resources/templates/default.yml.j2
@@ -56,7 +56,7 @@ repos:
     ref: 2efabd3e6604b8a9e8f70baf52f57696680c7855
   cmsis:
     url: "https://github.com/PhilippvK/CMSIS_5.git"
-    ref: 4a52cabc5830b4666589d4c63f3a228eef5b4efb
+    ref: 21c549c7fdd4f92fb5a16e98212fb9b4df2ab672
   mlif:
     url: "https://github.com/tum-ei-eda/mlonmcu-sw.git"
     ref: 3f8e66b956fe1cf0d85a18d39d684816e92e2751
@@ -65,7 +65,7 @@ repos:
     ref: release/v4.4
   microtvm_etissvp:
     url: "https://github.com/PhilippvK/microtvm-etissvp-template.git"
-    ref: 8b48a897ed4354602454f18cdd890aef6df4a554
+    ref: 41e591ea5479484154598fa74e3d7097ded3da86
 # Here all supported frameworks with their specific features are defined
 # Optionally disable unwanted or incomatible backends or features here
 # The configured defaults are used if no backend was specified in the command line options
@@ -104,6 +104,15 @@ frameworks:
           disable_legalize: true
           autotune: true
           autotuned: true
+      tvmaotplus:
+        enabled: true
+        features:
+          debug_arena: true
+          unpacked_api: true
+          usmp: true
+          disable_legalize: true
+          autotune: true
+          autotuned: true
       tvmllvm:
         enabled: false
         features:
@@ -141,6 +150,10 @@ frontends:
   onnx:
     enabled: false
   # TODO: saved_model (TF->TFLITE), ipynb (IPYNB->?)
+# Some targets/platforms support multiple toolchains
+toolchains:
+  gcc: true
+  llvm: true
 # Platform extend the number of supported targets
 platforms:
   mlif:
@@ -152,6 +165,10 @@ platforms:
     enabled: true
     features:
       debug: true
+  zephyr:
+    enabled: false
+    features:
+      debug: true
   tvm:
     enabled: false
     features: []
@@ -159,9 +176,8 @@ platforms:
       # validate: true
   microtvm:
     enabled: false
-    features:
+    features: []
       # validate: true
-      microtvm_etissvp: true
 # List of supported targets in the environment
 targets:
   default: etiss_pulpino
@@ -196,6 +212,10 @@ targets:
       ethosu: false
       arm_mvei: false
       arm_dsp: false
+  microtvm_etissvp:
+    enabled: true
+  microtvm_espidf:
+    enabled: false
 postprocesses:
   use:
   # - detailed_cycles
@@ -212,5 +232,8 @@ vars:
   llvm.distribution: "x86_64-linux-gnu-ubuntu-16.04"
   runs_per_stage: true
   riscv_gcc.dl_url: "https://syncandshare.lrz.de/dl/fiD9HnHYkb2V8kNbVQYv242m/rv32gc.tar.gz"
-  riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiPX7H4WRQL9GXbeoo2sEexj/rv32gcv.tar.gz"
+  riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiGp4r3f6SZaC5QyDi6QUiNQ/rv32gcv_new.tar.gz"
   riscv_gcc.dl_url_pext: "https://syncandshare.lrz.de/dl/fiNvP4mzVQ8uDvgT9Yf2bqNk/rv32gcp.tar.xz"
+flags:
+  tflmc.exe:
+  - x86
diff --git a/resources/templates/dev.yml.j2 b/resources/templates/dev.yml.j2
index 581b5ecaa..3a3495ac2 100644
--- a/resources/templates/dev.yml.j2
+++ b/resources/templates/dev.yml.j2
@@ -167,6 +167,14 @@ targets:
     enabled: true
   esp32c3:
     enabled: true
+  microtvm_etissvp:
+    enabled: true
+  microtvm_espidf:
+    enabled: false
+# Some targets/platforms support multiple toolchains
+toolchains:
+  gcc: true
+  llvm: true
 platforms:
   mlif:
     enabled: true
@@ -177,6 +185,10 @@ platforms:
     enabled: true
     features:
       debug: true
+  zephyr:
+    enabled: false
+    features:
+      debug: true
 postprocesses:
   use:
   # - detailed_cycles
@@ -192,3 +204,6 @@ vars:
   llvm.version: "11.1.0"
   runs_per_stage: true
   riscv_gcc.dl_url: "https://syncandshare.lrz.de/dl/fi89JQF89pEiWwP6aQh7cM4H/rv32gcv.tar.xz"
+flags:
+  tflmc.exe:
+  - x86
diff --git a/resources/templates/minimal.yml.j2 b/resources/templates/minimal.yml.j2
index 31ad992b6..f41e41d90 100644
--- a/resources/templates/minimal.yml.j2
+++ b/resources/templates/minimal.yml.j2
@@ -56,7 +56,7 @@ repos:
     ref: 2efabd3e6604b8a9e8f70baf52f57696680c7855
   cmsis:
     url: "https://github.com/PhilippvK/CMSIS_5.git"
-    ref: 4a52cabc5830b4666589d4c63f3a228eef5b4efb
+    ref: 21c549c7fdd4f92fb5a16e98212fb9b4df2ab672
   mlif:
     url: "https://github.com/tum-ei-eda/mlonmcu-sw.git"
     ref: 3f8e66b956fe1cf0d85a18d39d684816e92e2751
@@ -65,7 +65,7 @@ repos:
     ref: release/v4.4
   microtvm_etissvp:
     url: "https://github.com/PhilippvK/microtvm-etissvp-template.git"
-    ref: 8b48a897ed4354602454f18cdd890aef6df4a554
+    ref: 41e591ea5479484154598fa74e3d7097ded3da86
 # Here all supported frameworks with their specific features are defined
 # Optionally disable unwanted or incomatible backends or features here
 # The configured defaults are used if no backend was specified in the command line options
@@ -142,6 +142,10 @@ frontends:
   onnx:
     enabled: false
   # TODO: saved_model (TF->TFLITE), ipynb (IPYNB->?)
+# Some targets/platforms support multiple toolchains
+toolchains:
+  gcc: true
+  llvm: true
 # Platform extend the number of supported targets
 platforms:
   mlif:
@@ -153,6 +157,10 @@ platforms:
     enabled: false
     features:
       debug: true
+  zephyr:
+    enabled: false
+    features:
+      debug: true
   tvm:
     enabled: false
     features: []
@@ -160,9 +168,8 @@ platforms:
       # validate: true
   microtvm:
     enabled: false
-    features:
+    features: []
       # validate: true
-      microtvm_etissvp: true
 # List of supported targets in the environment
 targets:
   default: host_x86
@@ -197,6 +204,10 @@ targets:
       ethosu: false
       arm_mvei: false
       arm_dsp: false
+  microtvm_etissvp:
+    enabled: true
+  microtvm_espidf:
+    enabled: false
 postprocesses:
   use:
   # - detailed_cycles
@@ -213,5 +224,8 @@ vars:
   llvm.distribution: "x86_64-linux-gnu-ubuntu-16.04"
   runs_per_stage: true
   riscv_gcc.dl_url: "https://syncandshare.lrz.de/dl/fiD9HnHYkb2V8kNbVQYv242m/rv32gc.tar.gz"
-  # riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiPX7H4WRQL9GXbeoo2sEexj/rv32gcv.tar.gz"
+  # riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiGp4r3f6SZaC5QyDi6QUiNQ/rv32gcv_new.tar.gz"
   # riscv_gcc.dl_url_pext: "https://syncandshare.lrz.de/dl/fiNvP4mzVQ8uDvgT9Yf2bqNk/rv32gcp.tar.xz"
+flags:
+  tflmc.exe:
+  - x86
diff --git a/resources/templates/phi.yml.j2 b/resources/templates/phi.yml.j2
new file mode 100644
index 000000000..3840a2ebd
--- /dev/null
+++ b/resources/templates/phi.yml.j2
@@ -0,0 +1,253 @@
+---
+# The MLONMCU_HOME is filled in automatically when creating the environment
+home: "{{ home_dir }}"
+logging:
+  level: DEBUG
+  to_file: false
+  rotate: false
+cleanup:
+  auto: true
+  keep: 50
+# Default locations for certain directoriescan be changed here
+# Non-absolute paths will always be threated relative to the MLONMCU_HOME
+paths:
+  # Where the dependencies are downloaded and installed
+  deps: deps
+  # If logging to file is used keep logs in this directory
+  logs: logs
+  # Location where reports and artifacts are written to
+  results: results
+  # Directory for intermediate build products, should be located on a large enough drive
+  temp: temp
+  # A collection of models which will be used to look for models
+  # The paths will be checked in the order defined here stopping at the first match
+  # Non-existant paths will be skipped without throwing an error
+  models:
+    - "{{ home_dir }}/models"
+    - "{{ config_dir }}/models"
+# Here default clone_urls
+repos:
+  tensorflow:  # TODO: rename to tflite-micro?
+    url: "git@gitlab.lrz.de:de-tum-ei-eda-esl/tflite-micro.git"
+    ref: feature/tflmc_support2
+  tflite_micro_compiler:
+    url: "git@gitlab.lrz.de:philippvk/tflite_micro_compiler.git"
+    ref: philippvk_new2
+  tvm:
+    # url: "git@github.com:rafzi/privtvm.git"
+    # ref: moiopt
+    url: "git@github.com:PhilippvK/tvm.git"
+    ref: tumeda-philippk-new-rebased
+  utvm_staticrt_codegen:
+    url: "git@github.com:tum-ei-eda/utvm_staticrt_codegen.git"
+    ref: master
+  tvm_extensions:
+    url: "git@github.com:tum-ei-eda/tvm_extensions.git"
+    ref: main
+  muriscvnn:
+    url: "git@github.com:tum-ei-eda/muriscv-nn.git"
+    ref: master
+  etiss:
+    url: "git@github.com:PhilippvK/etiss.git"
+    ref: coredsl2-rvp-rvv-rebased2
+  packer:
+    url: "git@gitlab.lrz.de:de-tum-ei-eda-esl/tflite-pack.git"
+    ref: main
+  spike:
+    url: "https://github.com/riscv-software-src/riscv-isa-sim.git"
+    ref: caf5a420ef7414031368739e16d60f3f418aa6ac
+  spikepk:
+    url: "https://github.com/riscv-software-src/riscv-pk.git"
+    ref: 2efabd3e6604b8a9e8f70baf52f57696680c7855
+  cmsis:
+    url: "https://github.com/PhilippvK/CMSIS_5.git"
+    ref: 21c549c7fdd4f92fb5a16e98212fb9b4df2ab672
+  mlif:
+    url: "git@github.com:tum-ei-eda/mlonmcu-sw.git"
+    ref: philippvk
+  espidf:
+    url: "https://github.com/espressif/esp-idf.git"
+    ref: release/v4.4
+  microtvm_etissvp:
+    url: "git@github.com:PhilippvK/microtvm-etissvp-template.git"
+    ref: main
+
+# Here all supported frameworks with their specific features are defined
+# Optionally disable unwanted or incomatible backends or features here
+# The configured defaults are used if no backend was specified in the command line options
+frameworks:
+  default: tvm
+  tflm:
+    enabled: true
+    backends:
+      default: tflmi
+      tflmc:
+        enabled: true
+      tflmi:
+        enabled: true
+        features:
+          debug_arena: true
+    features:
+      muriscvnn: true
+      cmsisnn: true
+  tvm:
+    enabled: true
+    backends:
+      default: tvmaot
+      tvmrt:
+        enabled: true
+        features:
+          debug_arena: true
+          # disable_legalize: true
+          disable_legalize: false
+          autotune: true
+          autotuned: true
+          moiopt: true
+      tvmaot:
+        enabled: true
+        features:
+          debug_arena: true
+          unpacked_api: true
+          usmp: false
+          disable_legalize: true
+          autotune: true
+          autotuned: true
+          moiopt: true
+      tvmllvm:
+        enabled: true
+        features:
+          # unpacked_api: true
+          # disable_legalize: true
+          disable_legalize: false
+          autotune: true
+          autotuned: true
+      tvmcg:
+        enabled: true
+        features:
+          debug_arena: true
+          # disable_legalize: true
+          disable_legalize: false
+          autotune: true
+          autotuned: true
+    features:
+      cmsisnnbyoc: true
+      muriscvnnbyoc: true
+# Some frontends are experimental and therefore disabled here
+# Features like packing are only available in certain environments
+#
+frontends:
+  tflite:
+    enabled: true
+    features:
+      validate: true
+      visualize: true
+  relay:
+    enabled: false
+    features:
+      relayviz: true
+  packed:
+    enabled: false
+    features:
+      packing: true
+      packed: true
+  onnx:
+    enabled: false
+  # TODO: saved_model (TF->TFLITE), ipynb (IPYNB->?)
+# Some targets/platforms support multiple toolchains
+toolchains:
+  gcc: true
+  llvm: true
+# Platform extend the number of supported targets
+platforms:
+  mlif:
+    enabled: true
+    features:
+      debug: true
+      validate: true
+  # espidf:
+  #   enabled: false
+  #   features:
+  #     debug: true
+  zephyr:
+    enabled: true
+    features:
+      debug: true
+  # tvm:
+  #   enabled: true
+  #   features: []
+  #     # rpc: true
+  #     # validate: true
+  # microtvm:
+  #   enabled: true
+  #   features:
+  #     # validate: true
+  #     microtvm_etissvp: true
+# List of supported targets in the environment
+targets:
+  default: etiss_pulpino
+  etiss_pulpino:
+    enabled: true
+    features:
+      gdbserver: true
+      etissdbg: true
+      trace: true
+      log_instrs: true
+      # vext: true
+      # pext: true
+  host_x86:
+    enabled: true
+    features:
+      gdbserver: true
+  spike:
+    enabled: true
+    features:
+      vext: true
+      pext: true
+      cachesim: true
+      log_instrs: true
+  ovpsim:
+    enabled: false
+    features:
+      vext: true
+      pext: true
+  corstone300:
+    enabled: true
+    features:
+      ethosu: false
+      arm_mvei: false
+      arm_dsp: true
+  riscv_qemu:
+    enabled: true
+    features:
+      vext: true
+  microtvm_etissvp:
+    enabled: true
+  microtvm_espidf:
+    enabled: false
+postprocesses:
+  use:
+  # - detailed_cycles
+  # - average_cycles
+  # - filter_cols
+  # - features2cols
+  # - config2cols
+  # - bytes2kb
+  # - visualize
+# This is where further options such as specific versions of dependencies can be set in the furture
+vars:
+  # tvm.make_tool: "ninja"
+  llvm.version: "14.0.0"
+  runs_per_stage: true
+  riscv_gcc.dl_url: "https://syncandshare.lrz.de/dl/fiD9HnHYkb2V8kNbVQYv242m/rv32gc.tar.gz"
+  riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiGp4r3f6SZaC5QyDi6QUiNQ/rv32gcv_new.tar.gz"
+  riscv_gcc.dl_url_pext: "https://syncandshare.lrz.de/dl/fiNvP4mzVQ8uDvgT9Yf2bqNk/rv32gcp.tar.xz"
+  ovpsim.exe: "/nfs/TUEIEDAscratch/ga87puy/work/riscv-ovpsim-plus/bin/Linux64/riscvOVPsimPlus.exe"
+  # riscv32_qemu.exe: "/work/git/prj/qemu_experiments/newer/qemu/build/qemu-system-riscv32" # TODO: download/build
+  # riscv32_qemu.exe: qemu-system-riscv32
+  riscv32_qemu.exe: "/nfs/TUEIEDAscratch/ga87puy/work/qemu-system-riscv32"
+  # zephyr.install_dir: "/work/tools/zephyr_stuff2"
+  # zephyr.sdk_dir: "/work/tools/zephyr_stuff2/zephyr-sdk-0.15.0-rc1"
+  # zephyr.venv_dir: "/work/tools/zephyr_stuff2/.venv"
+flags:
+  tflmc.exe:
+  - x86
diff --git a/resources/templates/research.yml.j2 b/resources/templates/research.yml.j2
index 8949992f3..acc56ddf9 100644
--- a/resources/templates/research.yml.j2
+++ b/resources/templates/research.yml.j2
@@ -59,7 +59,7 @@ repos:
     ref: master
   cmsis:
     url: "https://github.com/PhilippvK/CMSIS_5.git"
-    ref: tumeda-patches
+    ref: tumeda-patches-new
   mlif:
     url: "git@github.com:tum-ei-eda/mlonmcu-sw.git"
     ref: main
@@ -146,6 +146,10 @@ frontends:
   onnx:
     enabled: false
   # TODO: saved_model (TF->TFLITE), ipynb (IPYNB->?)
+# Some targets/platforms support multiple toolchains
+toolchains:
+  gcc: true
+  llvm: true
 # Platform extend the number of supported targets
 platforms:
   mlif:
@@ -157,6 +161,10 @@ platforms:
     enabled: true
     features:
       debug: true
+  zephyr:
+    enabled: false
+    features:
+      debug: true
   tvm:
     enabled: true
     features: []
@@ -164,9 +172,8 @@ platforms:
       # validate: true
   microtvm:
     enabled: true
-    features:
+    features: []
       # validate: true
-      microtvm_etissvp: true
 # List of supported targets in the environment
 targets:
   default: etiss_pulpino
@@ -201,7 +208,9 @@ targets:
       ethosu: false
       arm_mvei: true
       arm_dsp: true
-  qemu_sifive:
+  microtvm_etissvp:
+    enabled: true
+  microtvm_espidf:
     enabled: false
 postprocesses:
   use:
@@ -218,5 +227,8 @@ vars:
   llvm.version: "11.1.0"
   runs_per_stage: true
   riscv_gcc.dl_url: "https://syncandshare.lrz.de/dl/fiD9HnHYkb2V8kNbVQYv242m/rv32gc.tar.gz"
-  riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiPX7H4WRQL9GXbeoo2sEexj/rv32gcv.tar.gz"
+  riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiGp4r3f6SZaC5QyDi6QUiNQ/rv32gcv_new.tar.gz"
   riscv_gcc.dl_url_pext: "https://syncandshare.lrz.de/dl/fiNvP4mzVQ8uDvgT9Yf2bqNk/rv32gcp.tar.xz"
+flags:
+  tflmc.exe:
+  - x86
diff --git a/resources/templates/tumeda.yml.j2 b/resources/templates/tumeda.yml.j2
index df90c6ba1..d21654da7 100644
--- a/resources/templates/tumeda.yml.j2
+++ b/resources/templates/tumeda.yml.j2
@@ -59,7 +59,7 @@ repos:
     ref: 2efabd3e6604b8a9e8f70baf52f57696680c7855
   cmsis:
     url: "https://github.com/PhilippvK/CMSIS_5.git"
-    ref: 4a52cabc5830b4666589d4c63f3a228eef5b4efb
+    ref: 21c549c7fdd4f92fb5a16e98212fb9b4df2ab672
   mlif:
     url: "git@github.com:tum-ei-eda/mlonmcu-sw.git"
     ref: 3f8e66b956fe1cf0d85a18d39d684816e92e2751
@@ -68,7 +68,7 @@ repos:
     ref: release/v4.4
   microtvm_etissvp:
     url: "git@github.com:PhilippvK/microtvm-etissvp-template.git"
-    ref: 8b48a897ed4354602454f18cdd890aef6df4a554
+    ref: 41e591ea5479484154598fa74e3d7097ded3da86
 
 # Here all supported frameworks with their specific features are defined
 # Optionally disable unwanted or incomatible backends or features here
@@ -148,6 +148,10 @@ frontends:
   onnx:
     enabled: false
   # TODO: saved_model (TF->TFLITE), ipynb (IPYNB->?)
+# Some targets/platforms support multiple toolchains
+toolchains:
+  gcc: true
+  llvm: true
 # Platform extend the number of supported targets
 platforms:
   mlif:
@@ -159,6 +163,10 @@ platforms:
     enabled: false
     features:
       debug: true
+  zephyr:
+    enabled: false
+    features:
+      debug: true
   tvm:
     enabled: false
     features: []
@@ -166,9 +174,8 @@ platforms:
       # validate: true
   microtvm:
     enabled: false
-    features:
+    features: []
       # validate: true
-      microtvm_etissvp: true
 # List of supported targets in the environment
 targets:
   default: etiss_pulpino
@@ -203,6 +210,10 @@ targets:
       ethosu: false
       arm_mvei: false
       arm_dsp: true
+  microtvm_etissvp:
+    enabled: true
+  microtvm_espidf:
+    enabled: false
 postprocesses:
   use:
   # - detailed_cycles
@@ -218,5 +229,8 @@ vars:
   llvm.version: "14.0.0"
   runs_per_stage: true
   riscv_gcc.dl_url: "https://syncandshare.lrz.de/dl/fiD9HnHYkb2V8kNbVQYv242m/rv32gc.tar.gz"
-  riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiPX7H4WRQL9GXbeoo2sEexj/rv32gcv.tar.gz"
+  riscv_gcc.dl_url_vext: "https://syncandshare.lrz.de/dl/fiGp4r3f6SZaC5QyDi6QUiNQ/rv32gcv_new.tar.gz"
   riscv_gcc.dl_url_pext: "https://syncandshare.lrz.de/dl/fiNvP4mzVQ8uDvgT9Yf2bqNk/rv32gcp.tar.xz"
+flags:
+  tflmc.exe:
+  - x86
diff --git a/scripts/bench.py b/scripts/bench.py
new file mode 100644
index 000000000..aa6019114
--- /dev/null
+++ b/scripts/bench.py
@@ -0,0 +1,30 @@
+import sys
+import subprocess
+from pathlib import Path
+import configparser
+
+config = configparser.ConfigParser()
+
+
+ini_path = Path("benchmarks.ini")
+
+config.read(ini_path)
+
+benchmarks = config.sections()
+
+assert len(sys.argv) >= 3
+name = sys.argv[1]
+out_dir = sys.argv[2]
+extra_args = sys.argv[3:]
+
+assert name in benchmarks
+data = config[name]
+
+script = data["script"]
+args = data["args"].split(" ")
+
+# Path(out_dir).mkdir(exist_ok=True)
+
+args = ["python", script, *args, "--out", out_dir, *extra_args]
+print("Executing:", " ".join(args))
+subprocess.run(args)
diff --git a/scripts/benchmarks.ini b/scripts/benchmarks.ini
new file mode 100644
index 000000000..81426a4e8
--- /dev/null
+++ b/scripts/benchmarks.ini
@@ -0,0 +1,31 @@
+[muriscvnn]
+script=gen_muriscnn_benchmarks.py
+args=--parallel --progress --autotuned --post
+
+[riscv_schedules]
+script=gen_tvm_riscv_benchmarks.py
+args=--parallel --progress --autotuned --post
+
+[tflmc]
+script=gen_tflmc_benchmarks.py
+args=--parallel --progress --post
+
+[tvm_backends]
+script=gen_tvm_backend_benchmarks.py
+args=--parallel --progress --post
+
+[usmp]
+script=gen_usmp_benchmarks.py
+args=--parallel --progress --post
+
+[auto_vectorize]
+script=gen_auto_vectorize_benchmarks.py
+args=--parallel --progress --post
+
+[riscv_toolchains]
+script=gen_riscv_toolchain_benchmarks.py
+args=--parallel --progress --post
+
+[simulators]
+script=gen_simulator_benchmarks.py
+args=--parallel --progress --post
diff --git a/scripts/gen_auto_vectorize_benchmarks.py b/scripts/gen_auto_vectorize_benchmarks.py
new file mode 100644
index 000000000..9ab4cc9c7
--- /dev/null
+++ b/scripts/gen_auto_vectorize_benchmarks.py
@@ -0,0 +1,329 @@
+import os
+import argparse
+import multiprocessing
+import logging
+
+# import mlonmcu
+import mlonmcu.context
+from mlonmcu.session.run import RunStage
+from mlonmcu.models.lookup import apply_modelgroups
+from mlonmcu.logging import get_logger, set_log_level
+
+logger = get_logger()
+
+# MURISCVNN_TOOLCHAIN = "gcc"
+
+FRONTEND = "tflite"
+
+TARGETS = [
+    "spike",
+    "ovpsim",
+    "etiss_pulpino",
+    "riscv_qemu",
+]
+
+AUTOTUNED_TARGETS = [
+    "spike",
+    "ovpsim",
+    "etiss_pulpino",
+    # "riscv_qemu",
+    # "corstone300",
+]
+
+DEFAULT_TARGETS = [
+    # "spike",
+    "ovpsim",
+    # "etiss_pulpino",
+    # "riscv_qemu",
+    # "corstone300",
+]
+
+PLATFORM = "mlif"
+
+TOOLCHAINS = ["gcc", "llvm"]
+
+DEFAULT_TOOLCHAINS = ["gcc", "llvm"]
+
+BACKENDS = ["tvmaot", "tflmi"]
+DEFAULT_BACKENDS = ["tvmaot", "tflmi"]
+
+VALIDATE_FEATURES = ["validate", "debug"]
+
+BACKEND_DEFAULT_FEATURES = {"tvmaot": ["unpacked_api", "usmp"], "tflmi": []}
+
+
+def get_backend_config(backend, features, enable_autotuned=False):
+    BACKEND_FEATURES = {
+        "tvmaot": [{"tvmaot.desired_layout": "NCHW"}, {"tvmaot.desired_layout": "NHWC"}],
+        "tflmi": [{}],
+    }
+    ret = BACKEND_FEATURES[backend]
+    return ret
+
+
+DEFAULT_CONFIG = {
+    "mlif.num_threads": 4,
+}
+
+BACKEND_DEFAULT_CONFIG = {
+    "tflmi": {},
+    "tvmaot": {"usmp.algorithm": "hill_climb"},  # Warning: usmp not enabled!
+}
+
+VLENS = [64, 128, 256, 512, 1024]
+
+DEFAULT_VLENS = [128, 256, 512, 1024]
+
+MODELS = [
+    # "sine_model",
+    # "magic_wand",
+    # "micro_speech",
+    # "cifar10",
+    # "simple_mnist",
+    "aww",
+    "vww",
+    "resnet",
+    "toycar",
+]
+
+POSTPROCESSES = [
+    "features2cols",
+    "config2cols",
+    "rename_cols",
+    "filter_cols",
+]
+
+POSTPROCESS_CONFIG = {
+    "filter_cols.keep": [
+        "Model",
+        "Backend",
+        "Target",
+        "Cycles",
+        "Runtime [s]",
+        "Total ROM",
+        "Total RAM",
+        # "ROM read-only",
+        # "ROM code",
+        # "ROM misc",
+        # "RAM data",
+        # "RAM zero-init data",
+        "Incomplete",
+        "Failing",
+        "Features",
+        # "Comment",
+        "Validation",
+        "Schedules",
+        "Extensions",
+        "VLEN",
+        "Layout",
+        "Toolchain",
+        "Vectorize",
+    ],
+    "rename_cols.mapping": {
+        "config_vext.vlen": "VLEN",
+        "config_vext.elen": "ELEN",
+        # "spike.vlen": "VLEN",
+        # "etiss_pulpino.vlen": "VLEN",
+        # "riscv_qemu.vlen": "VLEN",
+        # "ovpsim.elen": "ELEN",
+        # "spike.elen": "ELEN",
+        # "etiss_pulpino.elen": "ELEN",
+        # "riscv_qemu.elen": "ELEN",
+        "config_tvmaot.desired_layout": "Layout",
+        "config_mlif.toolchain": "Toolchain",
+        "feature_auto_vectorize": "Vectorize",
+    },
+    "filter_cols.drop_nan": True,
+}
+
+
+def gen_features(backend, features, validate=False):
+    ret = []
+    ret.extend(BACKEND_DEFAULT_FEATURES[backend])
+    if validate:
+        ret += VALIDATE_FEATURES
+    ret += features
+    return ret
+
+
+def gen_config(backend, backend_config, features, vlen, toolchain, enable_postprocesses=False):
+    ret = {}
+    ret.update(DEFAULT_CONFIG)
+    ret.update(BACKEND_DEFAULT_CONFIG[backend])
+    ret.update(backend_config)
+    ret.update({"mlif.toolchain": toolchain})
+    if enable_postprocesses:
+        ret.update(POSTPROCESS_CONFIG)
+    if "auto_vectorize" in features:
+        for feature in features:
+            if feature == "vext":
+                ret["vext.vlen"] = vlen
+    return ret
+
+
+def benchmark(args):
+    with mlonmcu.context.MlonMcuContext() as context:
+        user_config = context.environment.vars  # TODO: get rid of this workaround
+        session = context.create_session()
+        models = apply_modelgroups(args.models, context=context)
+        for model in models:
+            for backend in args.backend:
+                for target in args.target:
+                    for target_features in [[], ["vext", "auto_vectorize"]]:
+                        for backend_config in get_backend_config(backend, target_features):
+                            vlens = [0]
+                            if "vext" in target_features:
+                                vlens = args.vlen
+                            features = gen_features(backend, target_features, validate=args.validate)
+                            for vlen in vlens:
+                                for toolchain in args.toolchain:
+                                    if toolchain == "llvm" and "pext" in features:
+                                        continue  # TODO: move this check up!
+                                    config = gen_config(
+                                        backend,
+                                        backend_config,
+                                        features,
+                                        vlen,
+                                        toolchain,
+                                        enable_postprocesses=args.post,
+                                    )
+                                    config.update(user_config)  # TODO
+                                    # resolve_missing_configs(config, features, target, context)
+                                    # print("RUN", model, config, features, backend, target)
+                                    run = session.create_run(config=config)
+                                    run.add_features_by_name(features, context=context)
+                                    run.add_platform_by_name(PLATFORM, context=context)
+                                    run.add_frontend_by_name(FRONTEND, context=context)
+                                    run.add_model_by_name(model, context=context)
+                                    run.add_backend_by_name(backend, context=context)
+                                    run.add_target_by_name(target, context=context)
+                                    if args.post:
+                                        run.add_postprocesses_by_name(POSTPROCESSES)
+        if args.noop:
+            stage = RunStage.LOAD
+        else:
+            stage = RunStage.RUN
+        session.process_runs(until=stage, num_workers=args.parallel, progress=args.progress, context=context)
+        report = session.get_reports()
+        report_file = args.output
+        report.export(report_file)
+        print()
+        print(report.df)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "models",
+        metavar="model",
+        type=str,
+        # nargs="+",
+        nargs="*",
+        default=MODELS,
+        help="Model to process",
+    )
+    parser.add_argument(
+        "-b",
+        "--backend",
+        type=str,
+        action="append",
+        choices=BACKENDS,
+        # default=DEFAULT_BACKENDS,
+        default=[],
+        help=f"Backends to use (default: {DEFAULT_BACKENDS})",
+    )
+    parser.add_argument(
+        "-t",
+        "--target",
+        type=str,
+        action="append",
+        choices=TARGETS,
+        # default=DEFAULT_TARGETS,
+        default=[],
+        help=f"Targets to use (default: {DEFAULT_TARGETS}s)",
+    )
+    parser.add_argument(
+        "--vlen",
+        type=int,
+        action="append",
+        choices=VLENS,
+        # default=DEFAULT_VLENS,
+        default=[],
+        help=f"VLENS to use (RISC-V only) (default: {DEFAULT_VLENS})",
+    )
+    parser.add_argument(
+        "--toolchain",
+        type=str,
+        action="append",
+        choices=TOOLCHAINS,
+        # default=DEFAULT_TOOLCHAINS,
+        default=[],
+        help=f"Toolchain to use (default: {DEFAULT_TOOLCHAINS})",
+    )
+    parser.add_argument(
+        "--validate",
+        action="store_true",
+        help="Validate model outputs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--autotuned",
+        action="store_true",
+        help="Use tuning records, if available (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--post",
+        action="store_true",
+        help="Run postprocesses after the session (default: %(default)s)",
+    )
+    parser.add_argument(
+        "-p",
+        "--progress",
+        action="store_true",
+        help="Display progress bar (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--parallel",
+        metavar="THREADS",
+        nargs="?",
+        type=int,
+        const=multiprocessing.cpu_count(),
+        default=1,
+        help="Use multiple threads to process runs in parallel (%(const)s if specified, else %(default)s)",
+    )
+    parser.add_argument(
+        "--output",
+        "-o",
+        metavar="FILE",
+        type=str,
+        default=os.path.join(os.getcwd(), "out.csv"),
+        help="""Output CSV file (default: %(default)s)""",
+    )
+    parser.add_argument(
+        "--verbose",
+        "-v",
+        action="store_true",
+        help="Print detailed messages for easier debugging (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--noop",
+        action="store_true",
+        help="Skip processing runs. (default: %(default)s)",
+    )
+    args = parser.parse_args()
+    if not args.backend:
+        args.backend = DEFAULT_BACKENDS
+    if not args.target:
+        args.target = DEFAULT_TARGETS
+    if not args.vlen:
+        args.vlen = DEFAULT_VLENS
+    if not args.toolchain:
+        args.toolchain = DEFAULT_TOOLCHAINS
+    if args.verbose:
+        set_log_level(logging.DEBUG)
+    else:
+        set_log_level(logging.INFO)
+    benchmark(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/gen_muriscnn_benchmarks.py b/scripts/gen_muriscnn_benchmarks.py
index 36a91e5d5..d6c936487 100644
--- a/scripts/gen_muriscnn_benchmarks.py
+++ b/scripts/gen_muriscnn_benchmarks.py
@@ -223,7 +223,7 @@ def get_backend_config(backend, features, enable_autotuned=False):
         "Model",
         "Backend",
         "Target",
-        "Total Cycles",
+        "Cycles",
         "Runtime [s]",
         "Total ROM",
         "Total RAM",
@@ -484,6 +484,7 @@ def main():
         set_log_level(logging.DEBUG)
     else:
         set_log_level(logging.INFO)
+    print("args", args)
     benchmark(args)
 
 
diff --git a/scripts/gen_riscv_toolchain_benchmarks.py b/scripts/gen_riscv_toolchain_benchmarks.py
new file mode 100644
index 000000000..436b82353
--- /dev/null
+++ b/scripts/gen_riscv_toolchain_benchmarks.py
@@ -0,0 +1,300 @@
+import os
+import argparse
+import multiprocessing
+import logging
+
+# import mlonmcu
+import mlonmcu.context
+from mlonmcu.session.run import RunStage
+from mlonmcu.models.lookup import apply_modelgroups
+from mlonmcu.logging import get_logger, set_log_level
+
+logger = get_logger()
+
+
+FRONTEND = "tflite"
+
+TARGETS = [
+    "spike",
+    "ovpsim",
+    "riscv_qemu",
+]
+
+DEFAULT_TARGETS = [
+    "spike",
+    # "ovpsim",
+    # "riscv_qemu",
+]
+
+PLATFORM = "mlif"
+
+TOOLCHAINS = ["gcc", "llvm"]
+
+DEFAULT_TOOLCHAINS = ["gcc", "llvm"]
+
+BACKENDS = [
+    "tflmi",
+    "tvmaot",
+]
+DEFAULT_BACKENDS = [
+    "tflmi",
+    "tvmaot",
+]
+
+TUNING_RECORDS = os.path.join(
+    os.path.dirname(os.path.abspath(__file__)),
+    "..",
+    "resources",
+    "frameworks",
+    "tvm",
+    "tuning_records",
+    "riscv_cpu_v0.01.log.best",
+)
+
+
+VALIDATE_FEATURES = ["validate", "debug"]
+
+TARGET_ARCH = {
+    "spike": "riscv",
+    "ovpsim": "riscv",
+    "x86": "x86",
+    "etiss_pulpino": "riscv",
+    "corstone300": "arm",
+}
+
+BACKEND_DEFAULT_FEATURES = {
+    "tflmi": [],
+    "tvmaot": ["unpacked_api", "usmp"],
+}
+
+
+def get_backend_features(backend, target):
+    return [[]]
+
+
+DEFAULT_CONFIG = {
+    "mlif.num_threads": 4,
+}
+
+BACKEND_DEFAULT_CONFIG = {
+    "tflmi": {},
+    "tvmaot": {"usmp.algorithm": "hill_climb"},
+}
+
+VLENS = [64, 128, 256, 512, 1024]
+
+DEFAULT_VLENS = [64, 128, 256, 512, 1024]
+
+MODELS = [
+    # "sine_model",
+    # "magic_wand",
+    # "micro_speech",
+    # "cifar10",
+    # "simple_mnist",
+    "aww",
+    "vww",
+    "resnet",
+    "toycar",
+]
+
+POSTPROCESSES = [
+    "features2cols",
+    "config2cols",
+    "rename_cols",
+    "filter_cols",
+]
+
+POSTPROCESS_CONFIG = {
+    "filter_cols.keep": [
+        "Model",
+        "Backend",
+        "Target",
+        "Cycles",
+        "Runtime [s]",
+        "Total ROM",
+        "Total RAM",
+        # "ROM read-only",
+        # "ROM code",
+        # "ROM misc",
+        # "RAM data",
+        # "RAM zero-init data",
+        "Incomplete",
+        "Failing",
+        "Features",
+        # "Comment",
+        "Validation",
+        "Toolchain",
+        "Optimize",
+    ],
+    "rename_cols.mapping": {
+        "config_mlif.toolchain": "Toolchain",
+        "config_mlif.optimize": "Optimize",
+    },
+    "filter_cols.drop_nan": True,
+}
+
+
+def gen_features(backend, validate=False):
+    ret = []
+    ret.extend(BACKEND_DEFAULT_FEATURES[backend])
+    if validate:
+        ret += VALIDATE_FEATURES
+    return ret
+
+
+def gen_config(backend, toolchain, enable_postprocesses=False, optimize="3"):
+    ret = {}
+    ret.update(DEFAULT_CONFIG)
+    ret.update(BACKEND_DEFAULT_CONFIG[backend])
+    ret.update({"mlif.toolchain": toolchain})
+    ret.update({"mlif.optimize": optimize})
+    if enable_postprocesses:
+        ret.update(POSTPROCESS_CONFIG)
+    return ret
+
+
+def benchmark(args):
+    with mlonmcu.context.MlonMcuContext() as context:
+        user_config = context.environment.vars  # TODO: get rid of this workaround
+        session = context.create_session()
+        models = apply_modelgroups(args.models, context=context)
+        for model in models:
+            for backend in args.backend:
+                for target in args.target:
+                    levels = ["0", "1", "2", "3", "s", "fast"]
+                    for level in levels:
+                        features = gen_features(backend, validate=args.validate)
+                        for toolchain in args.toolchain:
+                            config = gen_config(
+                                backend,
+                                toolchain,
+                                enable_postprocesses=args.post,
+                                optimize=level,
+                            )
+                            config.update(user_config)  # TODO
+                            run = session.create_run(config=config)
+                            run.add_features_by_name(features, context=context)
+                            run.add_platform_by_name(PLATFORM, context=context)
+                            run.add_frontend_by_name(FRONTEND, context=context)
+                            run.add_model_by_name(model, context=context)
+                            run.add_backend_by_name(backend, context=context)
+                            run.add_target_by_name(target, context=context)
+                            if args.post:
+                                run.add_postprocesses_by_name(POSTPROCESSES)
+        if args.noop:
+            stage = RunStage.LOAD
+        else:
+            stage = RunStage.RUN
+        session.process_runs(until=stage, num_workers=args.parallel, progress=args.progress, context=context)
+        report = session.get_reports()
+        report_file = args.output
+        report.export(report_file)
+        print()
+        print(report.df)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "models",
+        metavar="model",
+        type=str,
+        # nargs="+",
+        nargs="*",
+        default=MODELS,
+        help="Model to process",
+    )
+    parser.add_argument(
+        "-b",
+        "--backend",
+        type=str,
+        action="append",
+        choices=BACKENDS,
+        # default=DEFAULT_BACKENDS,
+        default=[],
+        help=f"Backends to use (default: {DEFAULT_BACKENDS})",
+    )
+    parser.add_argument(
+        "-t",
+        "--target",
+        type=str,
+        action="append",
+        choices=TARGETS,
+        # default=DEFAULT_TARGETS,
+        default=[],
+        help=f"Targets to use (default: {DEFAULT_TARGETS}s)",
+    )
+    parser.add_argument(
+        "--toolchain",
+        type=str,
+        action="append",
+        choices=TOOLCHAINS,
+        # default=DEFAULT_TOOLCHAINS,
+        default=[],
+        help=f"Toolchain to use (default: {DEFAULT_TOOLCHAINS})",
+    )
+    parser.add_argument(
+        "--validate",
+        action="store_true",
+        help="Validate model outputs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--autotuned",
+        action="store_true",
+        help="Use tuning records, if available (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--post",
+        action="store_true",
+        help="Run postprocesses after the session (default: %(default)s)",
+    )
+    parser.add_argument(
+        "-p",
+        "--progress",
+        action="store_true",
+        help="Display progress bar (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--parallel",
+        metavar="THREADS",
+        nargs="?",
+        type=int,
+        const=multiprocessing.cpu_count(),
+        default=1,
+        help="Use multiple threads to process runs in parallel (%(const)s if specified, else %(default)s)",
+    )
+    parser.add_argument(
+        "--output",
+        "-o",
+        metavar="FILE",
+        type=str,
+        default=os.path.join(os.getcwd(), "out.csv"),
+        help="""Output CSV file (default: %(default)s)""",
+    )
+    parser.add_argument(
+        "--verbose",
+        "-v",
+        action="store_true",
+        help="Print detailed messages for easier debugging (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--noop",
+        action="store_true",
+        help="Skip processing runs. (default: %(default)s)",
+    )
+    args = parser.parse_args()
+    if not args.backend:
+        args.backend = DEFAULT_BACKENDS
+    if not args.target:
+        args.target = DEFAULT_TARGETS
+    if not args.toolchain:
+        args.toolchain = DEFAULT_TOOLCHAINS
+    if args.verbose:
+        set_log_level(logging.DEBUG)
+    else:
+        set_log_level(logging.INFO)
+    benchmark(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/gen_tflmc_benchmarks.py b/scripts/gen_tflmc_benchmarks.py
new file mode 100644
index 000000000..0e0f3c6cc
--- /dev/null
+++ b/scripts/gen_tflmc_benchmarks.py
@@ -0,0 +1,348 @@
+import os
+import argparse
+import multiprocessing
+import logging
+
+# import mlonmcu
+import mlonmcu.context
+from mlonmcu.session.run import RunStage
+from mlonmcu.session.postprocess.postprocess import SessionPostprocess
+from mlonmcu.models.lookup import apply_modelgroups
+from mlonmcu.logging import get_logger, set_log_level
+
+logger = get_logger()
+
+
+class CustomPostprocess(SessionPostprocess):  # RunPostprocess?
+    """TODO"""
+
+    def __init__(self, features=None, config=None):
+        super().__init__("custom", features=features, config=config)
+
+    def post_session(self, report):
+        """Called at the end of a session."""
+        df = report.post_df.copy()
+        df["Kernels"] = df.apply(
+            lambda row: "muRISCV-NN"
+            if row.get("feature_muriscvnn") or row.get("feature_muriscvnnbyoc")
+            else (
+                "CMSIS-NN"
+                if row.get("feature_cmsisnn") or row.get("feature_cmsisnnbyoc")
+                else ("Autotuned" if row.get("feature_autotuned") else "Default")
+            ),
+            axis=1,
+        )
+        report.post_df = df
+
+
+FRONTEND = "tflite"
+
+TARGETS = [
+    "spike",
+    "ovpsim",
+    "host_x86",
+    "etiss_pulpino",
+    "corstone300",
+]
+
+DEFAULT_TARGETS = [
+    # "spike",
+    # "ovpsim",
+    # "host_x86",
+    "etiss_pulpino",
+    # "corstone300",
+]
+
+PLATFORM = "mlif"
+
+BACKENDS = [
+    "tflmi",
+    "tflmc",
+]
+DEFAULT_BACKENDS = [
+    "tflmi",
+    "tflmc",
+]
+
+FEATURES = [
+    "muriscvnn",
+    "cmsisnn",
+    "none",
+]
+
+DEFAULT_FEATURES = [
+    "muriscvnn",
+    "cmsisnn",
+]
+
+TARGET_DEFAULT_FEATURES = {
+    "etiss_pulpino": ["trace"],
+}
+
+
+def get_target_features(target, enable_default=True, enable_muriscvnn=False, enable_cmsisnn=False):
+    return [
+        *([[]] if enable_default else []),
+        *([["muriscvnn"]] if enable_muriscvnn else []),
+        *([["cmsisnn"]] if enable_cmsisnn else []),
+    ]
+
+
+VALIDATE_FEATURES = ["validate", "debug"]
+
+BACKEND_DEFAULT_FEATURES = {}
+
+
+def get_backend_features(backend, target, enable_autotuned=False):
+    BACKEND_FEATURES = {
+        "tflmi": [[]],
+        "tflmc": [[]],
+    }
+    return BACKEND_FEATURES[backend]
+
+
+def get_backend_config(backend, features, enable_autotuned=False):
+    BACKEND_CONFIG = {
+        "tflmi": [{}],
+        "tflmc": [{}],
+    }
+    ret = BACKEND_CONFIG[backend]
+    return ret
+
+
+DEFAULT_CONFIG = {
+    "mlif.num_threads": 4,
+}
+
+BACKEND_DEFAULT_CONFIG = {
+    "tflmi": {},
+    "tflmc": {},
+}
+
+MODELS = [
+    "sine_model",
+    # "magic_wand",
+    # "micro_speech",
+    # "cifar10",
+    # "simple_mnist",
+    "aww",
+    "vww",
+    "resnet",
+    "toycar",
+]
+
+POSTPROCESSES_0 = [
+    "features2cols",
+    "config2cols",
+]
+
+POSTPROCESSES_1 = [
+    "rename_cols",
+    "filter_cols",
+]
+
+POSTPROCESS_CONFIG = {
+    "filter_cols.keep": [
+        "Model",
+        "Backend",
+        "Target",
+        "Cycles",
+        "Runtime [s]",
+        "Total ROM",
+        "Total RAM",
+        "ROM read-only",
+        "ROM code",
+        "ROM misc",
+        "RAM data",
+        "RAM zero-init data",
+        "Incomplete",
+        "Failing",
+        "Features",
+        # "Comment",
+        "Validation",
+        "Kernels",
+        "RAM stack",
+        "RAM heap",
+    ],
+    "rename_cols.mapping": {},
+    "filter_cols.drop_nan": True,
+}
+
+
+def gen_features(backend, target, features, validate=False):
+    ret = []
+    ret.extend(BACKEND_DEFAULT_FEATURES.get(backend, []))
+    ret.extend(TARGET_DEFAULT_FEATURES.get(target, []))
+    if validate:
+        ret += VALIDATE_FEATURES
+    ret += features
+    return ret
+
+
+def gen_config(backend, backend_config, features, enable_postprocesses=False):
+    ret = {}
+    ret.update(DEFAULT_CONFIG)
+    ret.update(BACKEND_DEFAULT_CONFIG[backend])
+    ret.update(backend_config)
+    if enable_postprocesses:
+        ret.update(POSTPROCESS_CONFIG)
+    return ret
+
+
+def benchmark(args):
+    with mlonmcu.context.MlonMcuContext() as context:
+        user_config = context.environment.vars  # TODO: get rid of this workaround
+        session = context.create_session()
+        models = apply_modelgroups(args.models, context=context)
+        for model in models:
+            for backend in args.backend:
+                for target in args.target:
+                    enable_default = not args.skip_default
+                    enable_muriscvnn = "muriscvnn" in args.feature
+                    enable_cmsisnn = "cmsisnn" in args.feature
+                    for target_features in get_target_features(
+                        target,
+                        enable_default=enable_default,
+                        enable_muriscvnn=enable_muriscvnn,
+                        enable_cmsisnn=enable_cmsisnn,
+                    ):
+                        for backend_features in get_backend_features(backend, target):
+                            features = list(set(target_features + backend_features))
+                            for backend_config in get_backend_config(backend, features):
+                                features = gen_features(backend, target, features, validate=args.validate)
+                                config = gen_config(backend, backend_config, features, enable_postprocesses=args.post)
+                                config.update(user_config)  # TODO
+                                # resolve_missing_configs(config, features, target, context)
+                                run = session.create_run(config=config)
+                                run.add_features_by_name(features, context=context)
+                                run.add_platform_by_name(PLATFORM, context=context)
+                                run.add_frontend_by_name(FRONTEND, context=context)
+                                run.add_model_by_name(model, context=context)
+                                run.add_backend_by_name(backend, context=context)
+                                run.add_target_by_name(target, context=context)
+                                if args.post:
+                                    run.add_postprocesses_by_name(POSTPROCESSES_0)
+                                    run.add_postprocess(CustomPostprocess(), append=True)
+                                    run.add_postprocesses_by_name(POSTPROCESSES_1, append=True)
+        if args.noop:
+            stage = RunStage.LOAD
+        else:
+            stage = RunStage.RUN
+        session.process_runs(until=stage, num_workers=args.parallel, progress=args.progress, context=context)
+        report = session.get_reports()
+        report_file = args.output
+        report.export(report_file)
+        print()
+        print(report.df)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "models",
+        metavar="model",
+        type=str,
+        # nargs="+",
+        nargs="*",
+        default=MODELS,
+        help="Model to process",
+    )
+    parser.add_argument(
+        "-b",
+        "--backend",
+        type=str,
+        action="append",
+        choices=BACKENDS,
+        # default=DEFAULT_BACKENDS,
+        default=[],
+        help=f"Backends to use (default: {DEFAULT_BACKENDS})",
+    )
+    parser.add_argument(
+        "-t",
+        "--target",
+        type=str,
+        action="append",
+        choices=TARGETS,
+        # default=DEFAULT_TARGETS,
+        default=[],
+        help=f"Targets to use (default: {DEFAULT_TARGETS}s)",
+    )
+    parser.add_argument(
+        "-f",
+        "--feature",
+        type=str,
+        action="append",
+        choices=FEATURES,
+        # default=default_features,
+        default=[],
+        help=f"features to use (default: {DEFAULT_FEATURES})",
+    )
+    parser.add_argument(
+        "--validate",
+        action="store_true",
+        help="Validate model outputs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--skip-default",
+        dest="skip_default",
+        action="store_true",
+        help="Do not generate benchmarks for reference runs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--post",
+        action="store_true",
+        help="Run postprocesses after the session (default: %(default)s)",
+    )
+    parser.add_argument(
+        "-p",
+        "--progress",
+        action="store_true",
+        help="Display progress bar (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--parallel",
+        metavar="THREADS",
+        nargs="?",
+        type=int,
+        const=multiprocessing.cpu_count(),
+        default=1,
+        help="Use multiple threads to process runs in parallel (%(const)s if specified, else %(default)s)",
+    )
+    parser.add_argument(
+        "--output",
+        "-o",
+        metavar="FILE",
+        type=str,
+        default=os.path.join(os.getcwd(), "out.csv"),
+        help="""Output CSV file (default: %(default)s)""",
+    )
+    parser.add_argument(
+        "--verbose",
+        "-v",
+        action="store_true",
+        help="Print detailed messages for easier debugging (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--noop",
+        action="store_true",
+        help="Skip processing runs. (default: %(default)s)",
+    )
+    args = parser.parse_args()
+    if not args.backend:
+        args.backend = DEFAULT_BACKENDS
+    if not args.target:
+        args.target = DEFAULT_TARGETS
+    if not args.feature:
+        args.feature = DEFAULT_FEATURES
+    if "none" in args.feature:
+        assert len(args.feature) == 1
+        args.feature = []
+    if args.verbose:
+        set_log_level(logging.DEBUG)
+    else:
+        set_log_level(logging.INFO)
+    benchmark(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/gen_tvm_backend_benchmarks.py b/scripts/gen_tvm_backend_benchmarks.py
new file mode 100644
index 000000000..1f80c2aee
--- /dev/null
+++ b/scripts/gen_tvm_backend_benchmarks.py
@@ -0,0 +1,299 @@
+import os
+import argparse
+import multiprocessing
+import logging
+
+# import mlonmcu
+import mlonmcu.context
+from mlonmcu.session.run import RunStage
+from mlonmcu.models.lookup import apply_modelgroups
+from mlonmcu.logging import get_logger, set_log_level
+
+logger = get_logger()
+
+FRONTEND = "tflite"
+
+TARGETS = [
+    "spike",
+    "ovpsim",
+    "host_x86",
+    "etiss_pulpino",
+    "corstone300",
+]
+
+DEFAULT_TARGETS = [
+    # "spike",
+    # "ovpsim",
+    # "host_x86",
+    "etiss_pulpino",
+    "corstone300",
+]
+
+PLATFORM = "mlif"
+
+BACKENDS = [
+    "tvmrt",
+    "tvmaot",
+    "tvmcg",
+    "tvmllvm",
+]
+DEFAULT_BACKENDS = [
+    "tvmrt",
+    "tvmaot",
+    "tvmcg",
+    "tvmllvm",
+]
+
+FEATURES = [
+    "unpacked_api",
+    "none",
+]
+
+DEFAULT_FEATURES = [
+    "unpacked_api",
+]
+
+
+VALIDATE_FEATURES = ["validate", "debug"]
+
+
+def get_backend_features(backend, enable_default=True, enable_unpacked_api=True):
+    backend_features = {
+        "tvmrt": [[]],
+        "tvmaot": [[], *([["unpacked_api"]] if enable_unpacked_api else [])],
+        "tvmcg": [[]],
+        "tvmllvm": [[]],
+    }
+    return backend_features[backend]
+
+
+def get_backend_config(backend, features, enable_unpacked_api=False):
+    return [{}]
+
+
+DEFAULT_CONFIG = {
+    "mlif.num_threads": 4,
+}
+
+BACKEND_DEFAULT_CONFIG = {}
+TARGET_DEFAULT_FEATURES = {
+    "etiss_pulpino": ["trace"],
+}
+
+MODELS = [
+    "sine_model",
+    # "magic_wand",
+    # "micro_speech",
+    # "cifar10",
+    # "simple_mnist",
+    "aww",
+    "vww",
+    "resnet",
+    "toycar",
+]
+
+POSTPROCESSES_0 = [
+    "features2cols",
+]
+
+POSTPROCESSES_1 = [
+    "rename_cols",
+    "filter_cols",
+]
+
+POSTPROCESS_CONFIG = {
+    "filter_cols.keep": [
+        "Model",
+        "Backend",
+        "Target",
+        "Cycles",
+        "Runtime [s]",
+        "Total ROM",
+        "Total RAM",
+        "ROM read-only",
+        "ROM code",
+        "ROM misc",
+        "RAM data",
+        "RAM zero-init data",
+        "Incomplete",
+        "Failing",
+        "Features",
+        # "Comment",
+        "Validation",
+        "Kernels",
+        "Unpacked API",
+        "RAM stack",
+        "RAM heap",
+    ],
+    "rename_cols.mapping": {"feature_unpacked_api": "Unpacked API"},
+    "filter_cols.drop_nan": True,
+}
+
+
+def gen_features(backend, target, features, validate=False):
+    ret = []
+    ret.extend(TARGET_DEFAULT_FEATURES.get(target, []))
+    if validate:
+        ret += VALIDATE_FEATURES
+    ret += features
+    return ret
+
+
+def gen_config(backend, backend_config, features, enable_postprocesses=False):
+    ret = {}
+    ret.update(DEFAULT_CONFIG)
+    ret.update(BACKEND_DEFAULT_CONFIG.get(backend, []))
+    ret.update(backend_config)
+    if enable_postprocesses:
+        ret.update(POSTPROCESS_CONFIG)
+    return ret
+
+
+def benchmark(args):
+    with mlonmcu.context.MlonMcuContext() as context:
+        user_config = context.environment.vars  # TODO: get rid of this workaround
+        session = context.create_session()
+        models = apply_modelgroups(args.models, context=context)
+        for model in models:
+            for backend in args.backend:
+                for target in args.target:
+                    enable_default = not args.skip_default
+                    enable_unpacked_api = "unpacked_api" in args.feature
+                    for backend_features in get_backend_features(
+                        backend, enable_default=enable_default, enable_unpacked_api=enable_unpacked_api
+                    ):
+                        features = gen_features(backend, target, backend_features, validate=args.validate)
+                        config = gen_config(backend, {}, features, enable_postprocesses=args.post)
+                        config.update(user_config)  # TODO
+                        run = session.create_run(config=config)
+                        run.add_features_by_name(features, context=context)
+                        run.add_platform_by_name(PLATFORM, context=context)
+                        run.add_frontend_by_name(FRONTEND, context=context)
+                        run.add_model_by_name(model, context=context)
+                        run.add_backend_by_name(backend, context=context)
+                        run.add_target_by_name(target, context=context)
+                        if args.post:
+                            run.add_postprocesses_by_name(POSTPROCESSES_0)
+                            run.add_postprocesses_by_name(POSTPROCESSES_1, append=True)
+        if args.noop:
+            stage = RunStage.LOAD
+        else:
+            stage = RunStage.RUN
+        session.process_runs(until=stage, num_workers=args.parallel, progress=args.progress, context=context)
+        report = session.get_reports()
+        report_file = args.output
+        report.export(report_file)
+        print()
+        print(report.df)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "models",
+        metavar="model",
+        type=str,
+        # nargs="+",
+        nargs="*",
+        default=MODELS,
+        help="Model to process",
+    )
+    parser.add_argument(
+        "-b",
+        "--backend",
+        type=str,
+        action="append",
+        choices=BACKENDS,
+        # default=DEFAULT_BACKENDS,
+        default=[],
+        help=f"Backends to use (default: {DEFAULT_BACKENDS})",
+    )
+    parser.add_argument(
+        "-t",
+        "--target",
+        type=str,
+        action="append",
+        choices=TARGETS,
+        # default=DEFAULT_TARGETS,
+        default=[],
+        help=f"Targets to use (default: {DEFAULT_TARGETS}s)",
+    )
+    parser.add_argument(
+        "-f",
+        "--feature",
+        type=str,
+        action="append",
+        choices=FEATURES,
+        # default=default_features,
+        default=[],
+        help=f"features to use (default: {DEFAULT_FEATURES})",
+    )
+    parser.add_argument(
+        "--validate",
+        action="store_true",
+        help="Validate model outputs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--skip-default",
+        dest="skip_default",
+        action="store_true",
+        help="Do not generate benchmarks for reference runs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--post",
+        action="store_true",
+        help="Run postprocesses after the session (default: %(default)s)",
+    )
+    parser.add_argument(
+        "-p",
+        "--progress",
+        action="store_true",
+        help="Display progress bar (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--parallel",
+        metavar="THREADS",
+        nargs="?",
+        type=int,
+        const=multiprocessing.cpu_count(),
+        default=1,
+        help="Use multiple threads to process runs in parallel (%(const)s if specified, else %(default)s)",
+    )
+    parser.add_argument(
+        "--output",
+        "-o",
+        metavar="FILE",
+        type=str,
+        default=os.path.join(os.getcwd(), "out.csv"),
+        help="""Output CSV file (default: %(default)s)""",
+    )
+    parser.add_argument(
+        "--verbose",
+        "-v",
+        action="store_true",
+        help="Print detailed messages for easier debugging (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--noop",
+        action="store_true",
+        help="Skip processing runs. (default: %(default)s)",
+    )
+    args = parser.parse_args()
+    if not args.backend:
+        args.backend = DEFAULT_BACKENDS
+    if not args.target:
+        args.target = DEFAULT_TARGETS
+    if not args.feature:
+        args.feature = DEFAULT_FEATURES
+    if "none" in args.feature:
+        assert len(args.feature) == 1
+        args.feature = []
+    if args.verbose:
+        set_log_level(logging.DEBUG)
+    else:
+        set_log_level(logging.INFO)
+    benchmark(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/gen_tvm_riscv_benchmarks.py b/scripts/gen_tvm_riscv_benchmarks.py
new file mode 100644
index 000000000..50fabd5ae
--- /dev/null
+++ b/scripts/gen_tvm_riscv_benchmarks.py
@@ -0,0 +1,541 @@
+import os
+import argparse
+import multiprocessing
+import logging
+
+# import mlonmcu
+import mlonmcu.context
+from mlonmcu.session.run import RunStage
+from mlonmcu.session.postprocess.postprocess import SessionPostprocess
+from mlonmcu.models.lookup import apply_modelgroups
+from mlonmcu.logging import get_logger, set_log_level
+
+logger = get_logger()
+
+# MURISCVNN_TOOLCHAIN = "gcc"
+
+
+class CustomPostprocess(SessionPostprocess):  # RunPostprocess?
+    """TODO"""
+
+    def __init__(self, features=None, config=None):
+        super().__init__("custom", features=features, config=config)
+
+    def post_session(self, report):
+        """Called at the end of a session."""
+        df = report.post_df.copy()
+        df["Schedules"] = df.apply(
+            lambda row: "RISC-V"
+            if row.get("feature_target_optimized")
+            else ("Autotuned" if row.get("feature_autotuned") else "Default"),
+            axis=1,
+        )
+        # TODO: allow combinations
+        df["Extensions"] = df.apply(
+            lambda row: "VEXT+PEXT"
+            if row.get("feature_vext") and row.get("feature_pext")
+            else ("VEXT" if row.get("feature_vext") else ("PEXT" if row.get("feature_pext") else (None))),
+            axis=1,
+        )
+        report.post_df = df
+
+
+FRONTEND = "tflite"
+
+TARGETS = [
+    "spike",
+    "ovpsim",
+    "etiss_pulpino",
+    "riscv_qemu",
+]
+
+AUTOTUNED_TARGETS = [
+    "spike",
+    "ovpsim",
+    "etiss_pulpino",
+    # "riscv_qemu",
+]
+
+DEFAULT_TARGETS = [
+    # "spike",
+    "ovpsim",
+    # "etiss_pulpino",
+    # "riscv_qemu",
+]
+
+PLATFORM = "mlif"
+
+TOOLCHAINS = ["gcc", "llvm"]
+
+DEFAULT_TOOLCHAINS = ["gcc", "llvm"]
+
+BACKENDS = ["tvmaot"]
+DEFAULT_BACKENDS = ["tvmaot"]
+
+FEATURES = [
+    "target_optimized",
+    "auto_vectorize",
+    "disable_legalize",
+    "none",
+]
+
+DEFAULT_FEATURES = [
+    "target_optimized",
+    "auto_vectorize",
+    # "disable_legalize",
+]
+
+TUNING_RECORDS = os.path.join(
+    os.path.dirname(os.path.abspath(__file__)),
+    "..",
+    "resources",
+    "frameworks",
+    "tvm",
+    "tuning_records",
+    "riscv_cpu_v0.01.log.best",
+)
+
+
+def get_target_features(target, enable_default=False, enable_target_optimized=False, enable_auto_vectorize=False):
+    TARGET_FEATURES = {
+        "spike": [
+            *(
+                [[], ["vext", "auto_vectorize"] if enable_auto_vectorize else ["vext"], ["pext"]]
+                if enable_default
+                else []
+            ),
+            *(
+                [
+                    ["target_optimized"],
+                    ["target_optimized", "vext", "auto_vectorize"]
+                    if enable_auto_vectorize
+                    else ["target_optimized", "vext"],
+                    ["target_optimized", "pext"],
+                ]
+                if enable_target_optimized
+                else []
+            ),
+        ],
+        "ovpsim": [
+            *(
+                [[], ["vext", "auto_vectorize"] if enable_auto_vectorize else ["vext"], ["pext"]]
+                if enable_default
+                else []
+            ),
+            *(
+                [
+                    ["target_optimized"],
+                    ["target_optimized", "vext", "auto_vectorize"]
+                    if enable_auto_vectorize
+                    else ["target_optimized", "vext"],
+                    ["target_optimized", "pext"],
+                ]
+                if enable_target_optimized
+                else []
+            ),
+        ],
+        "etiss_pulpino": [
+            *(
+                # [[], ["vext", "auto_vectorize"] if enable_auto_vectorize else ["vext"], ["pext"]]
+                [[], ["pext"]]
+                if enable_default
+                else []
+            ),
+            *(
+                [
+                    ["target_optimized"],
+                    # ["target_optimized", "vext", "auto_vectorize"]
+                    # if enable_auto_vectorize
+                    # else ["target_optimized", "vext"],
+                    ["target_optimized", "pext"],
+                ]
+                if enable_target_optimized
+                else []
+            ),
+        ],
+        "riscv_qemu": [
+            *([[], ["vext", "auto_vectorize"] if enable_auto_vectorize else ["vext"]] if enable_default else []),
+            *(
+                [
+                    ["target_optimized"],
+                    ["target_optimized", "vext", "auto_vectorize"]
+                    if enable_auto_vectorize
+                    else ["target_optimized", "vext"],
+                ]
+                if enable_target_optimized
+                else []
+            ),
+        ],
+    }
+    return TARGET_FEATURES[target]
+
+
+VALIDATE_FEATURES = ["validate", "debug"]
+
+BACKEND_DEFAULT_FEATURES = {
+    # "tvmaot": ["unpacked_api", "usmp"],
+    "tvmaot": ["unpacked_api"],
+}
+
+
+def get_backend_features(backend, target, enable_autotuned=False, enable_disable_legalize=False):
+    # print("get_backend_features", backend, target, enable_autotuned, enable_disable_legalize)
+    BACKEND_FEATURES = {
+        "tvmaot": [
+            [],
+            *([["autotuned"]] if enable_autotuned and target in AUTOTUNED_TARGETS else []),
+            *([["disable_legalize"]] if enable_disable_legalize else []),
+        ],
+    }
+    return BACKEND_FEATURES[backend]
+
+
+def get_backend_config(backend, features, enable_autotuned=False):
+    # print("get_backend_config", backend, features, enable_autotuned)
+    BACKEND_FEATURES = {
+        "tvmaot": [
+            *(
+                [{"tvmaot.desired_layout": "NCHW"}, {"tvmaot.desired_layout": "NHWC"}]
+                if "target_optimized" not in features
+                else [{}]
+            ),
+        ],
+    }
+    ret = BACKEND_FEATURES[backend]
+    if enable_autotuned and backend == "tvmaot":
+        for cfg in ret:
+            cfg.update({"autotuned.results_file": TUNING_RECORDS})
+    # print("ret", ret)
+    return ret
+
+
+DEFAULT_CONFIG = {
+    "mlif.num_threads": 4,
+}
+
+BACKEND_DEFAULT_CONFIG = {
+    "tflmi": {},
+    "tvmaot": {"usmp.algorithm": "hill_climb"},  # Warning: usmp not enabled!
+}
+
+VLENS = [64, 128, 256, 512, 1024]
+
+DEFAULT_VLENS = [128, 256, 512, 1024]
+
+MODELS = [
+    # "sine_model",
+    # "magic_wand",
+    # "micro_speech",
+    # "cifar10",
+    # "simple_mnist",
+    "aww",
+    "vww",
+    "resnet",
+    "toycar",
+]
+
+POSTPROCESSES_0 = [
+    "features2cols",
+    "config2cols",
+]
+
+POSTPROCESSES_1 = [
+    "rename_cols",
+    "filter_cols",
+]
+
+POSTPROCESS_CONFIG = {
+    "filter_cols.keep": [
+        "Model",
+        "Backend",
+        "Target",
+        "Cycles",
+        "Runtime [s]",
+        "Total ROM",
+        "Total RAM",
+        # "ROM read-only",
+        # "ROM code",
+        # "ROM misc",
+        # "RAM data",
+        # "RAM zero-init data",
+        "Incomplete",
+        "Failing",
+        "Features",
+        # "Comment",
+        "Validation",
+        "Schedules",
+        "Extensions",
+        "VLEN",
+        "ELEN",
+        "Layout",
+        "Toolchain",
+        "Vectorize",
+    ],
+    "rename_cols.mapping": {
+        "config_vext.vlen": "VLEN",
+        "config_vext.elen": "ELEN",
+        # "spike.vlen": "VLEN",
+        # "etiss_pulpino.vlen": "VLEN",
+        # "riscv_qemu.vlen": "VLEN",
+        # "ovpsim.elen": "ELEN",
+        # "spike.elen": "ELEN",
+        # "etiss_pulpino.elen": "ELEN",
+        # "riscv_qemu.elen": "ELEN",
+        "config_tvmaot.desired_layout": "Layout",
+        "config_mlif.toolchain": "Toolchain",
+        "feature_auto_vectorize": "Vectorize",
+    },
+    "filter_cols.drop_nan": True,
+}
+
+
+def gen_features(backend, features, validate=False):
+    ret = []
+    ret.extend(BACKEND_DEFAULT_FEATURES[backend])
+    if validate:
+        ret += VALIDATE_FEATURES
+    ret += features
+    return ret
+
+
+def gen_config(backend, backend_config, features, vlen, toolchain, enable_postprocesses=False):
+    ret = {}
+    ret.update(DEFAULT_CONFIG)
+    ret.update(BACKEND_DEFAULT_CONFIG[backend])
+    ret.update(backend_config)
+    ret.update({"mlif.toolchain": toolchain})
+    if enable_postprocesses:
+        ret.update(POSTPROCESS_CONFIG)
+    if "target_optimized" in features or "auto_vectorize" in features:
+        for feature in features:
+            if feature == "pext":
+                assert vlen == 0
+            elif feature == "vext":
+                ret["vext.vlen"] = vlen
+    return ret
+
+
+def benchmark(args):
+    # print("args", args)
+    with mlonmcu.context.MlonMcuContext() as context:
+        user_config = context.environment.vars  # TODO: get rid of this workaround
+        session = context.create_session()
+        models = apply_modelgroups(args.models, context=context)
+        for model in models:
+            # print("model", model)
+            for backend in args.backend:
+                # print("backend", backend)
+                for target in args.target:
+                    # print("target", target)
+                    enable_default = not args.skip_default
+                    enable_target_optimized = "target_optimized" in args.feature
+                    enable_auto_vectorize = "auto_vectorize" in args.feature
+                    for target_features in get_target_features(
+                        target,
+                        enable_default=enable_default,
+                        enable_target_optimized=enable_target_optimized,
+                        enable_auto_vectorize=enable_auto_vectorize,
+                    ):
+                        # print("target_features", target_features)
+                        enable_autotuned = False
+                        if args.autotuned:
+                            if "target_optimized" not in target_features and backend == "tvmaot":
+                                enable_autotuned = True
+                        enable_disable_legalize = (
+                            "disable_legalize" in args.feature and "target_optimized" not in target_features
+                        )
+                        for backend_features in get_backend_features(
+                            backend,
+                            target,
+                            enable_autotuned=enable_autotuned,
+                            enable_disable_legalize=enable_disable_legalize,
+                        ):
+                            # print("backend_features", backend_features)
+                            features = list(set(target_features + backend_features))
+                            # print("features", features)
+                            for backend_config in get_backend_config(
+                                backend, features, enable_autotuned=enable_autotuned
+                            ):
+                                vlens = [0]
+                                if "vext" in features:
+                                    if "target_optimized" in features or "auto_vectorize" in features:
+                                        vlens = args.vlen
+                                    else:
+                                        continue
+                                features = gen_features(backend, features, validate=args.validate)
+                                for vlen in vlens:
+                                    # print("vlen", vlen)
+                                    for toolchain in args.toolchain:
+                                        if toolchain == "llvm" and "pext" in features:
+                                            continue  # TODO: move this check up!
+                                        config = gen_config(
+                                            backend,
+                                            backend_config,
+                                            features,
+                                            vlen,
+                                            toolchain,
+                                            enable_postprocesses=args.post,
+                                        )
+                                        config.update(user_config)  # TODO
+                                        # resolve_missing_configs(config, features, target, context)
+                                        # print("RUN", model, config, features, backend, target)
+                                        run = session.create_run(config=config)
+                                        run.add_features_by_name(features, context=context)
+                                        run.add_platform_by_name(PLATFORM, context=context)
+                                        run.add_frontend_by_name(FRONTEND, context=context)
+                                        run.add_model_by_name(model, context=context)
+                                        run.add_backend_by_name(backend, context=context)
+                                        run.add_target_by_name(target, context=context)
+                                        if args.post:
+                                            run.add_postprocesses_by_name(POSTPROCESSES_0)
+                                            run.add_postprocess(CustomPostprocess(), append=True)
+                                            run.add_postprocesses_by_name(POSTPROCESSES_1, append=True)
+        if args.noop:
+            stage = RunStage.LOAD
+        else:
+            stage = RunStage.RUN
+        session.process_runs(until=stage, num_workers=args.parallel, progress=args.progress, context=context)
+        report = session.get_reports()
+        report_file = args.output
+        report.export(report_file)
+        print()
+        print(report.df)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "models",
+        metavar="model",
+        type=str,
+        # nargs="+",
+        nargs="*",
+        default=MODELS,
+        help="Model to process",
+    )
+    parser.add_argument(
+        "-b",
+        "--backend",
+        type=str,
+        action="append",
+        choices=BACKENDS,
+        # default=DEFAULT_BACKENDS,
+        default=[],
+        help=f"Backends to use (default: {DEFAULT_BACKENDS})",
+    )
+    parser.add_argument(
+        "-t",
+        "--target",
+        type=str,
+        action="append",
+        choices=TARGETS,
+        # default=DEFAULT_TARGETS,
+        default=[],
+        help=f"Targets to use (default: {DEFAULT_TARGETS}s)",
+    )
+    parser.add_argument(
+        "-f",
+        "--feature",
+        type=str,
+        action="append",
+        choices=FEATURES,
+        # default=default_features,
+        default=[],
+        help=f"features to use (default: {DEFAULT_FEATURES})",
+    )
+    parser.add_argument(
+        "--vlen",
+        type=int,
+        action="append",
+        choices=VLENS,
+        # default=DEFAULT_VLENS,
+        default=[],
+        help=f"VLENS to use (RISC-V only) (default: {DEFAULT_VLENS})",
+    )
+    parser.add_argument(
+        "--toolchain",
+        type=str,
+        action="append",
+        choices=TOOLCHAINS,
+        # default=DEFAULT_TOOLCHAINS,
+        default=[],
+        help=f"Toolchain to use (default: {DEFAULT_TOOLCHAINS})",
+    )
+    parser.add_argument(
+        "--validate",
+        action="store_true",
+        help="Validate model outputs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--autotuned",
+        action="store_true",
+        help="Use tuning records, if available (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--skip-default",
+        dest="skip_default",
+        action="store_true",
+        help="Do not generate benchmarks for reference runs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--post",
+        action="store_true",
+        help="Run postprocesses after the session (default: %(default)s)",
+    )
+    parser.add_argument(
+        "-p",
+        "--progress",
+        action="store_true",
+        help="Display progress bar (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--parallel",
+        metavar="THREADS",
+        nargs="?",
+        type=int,
+        const=multiprocessing.cpu_count(),
+        default=1,
+        help="Use multiple threads to process runs in parallel (%(const)s if specified, else %(default)s)",
+    )
+    parser.add_argument(
+        "--output",
+        "-o",
+        metavar="FILE",
+        type=str,
+        default=os.path.join(os.getcwd(), "out.csv"),
+        help="""Output CSV file (default: %(default)s)""",
+    )
+    parser.add_argument(
+        "--verbose",
+        "-v",
+        action="store_true",
+        help="Print detailed messages for easier debugging (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--noop",
+        action="store_true",
+        help="Skip processing runs. (default: %(default)s)",
+    )
+    args = parser.parse_args()
+    if not args.backend:
+        args.backend = DEFAULT_BACKENDS
+    if not args.target:
+        args.target = DEFAULT_TARGETS
+    if not args.feature:
+        args.feature = DEFAULT_FEATURES
+    if not args.vlen:
+        args.vlen = DEFAULT_VLENS
+    if not args.toolchain:
+        args.toolchain = DEFAULT_TOOLCHAINS
+    if "none" in args.feature:
+        assert len(args.feature) == 1
+        args.feature = []
+    if args.verbose:
+        set_log_level(logging.DEBUG)
+    else:
+        set_log_level(logging.INFO)
+    benchmark(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/gen_usmp_benchmarks.py b/scripts/gen_usmp_benchmarks.py
new file mode 100644
index 000000000..ac2ca375f
--- /dev/null
+++ b/scripts/gen_usmp_benchmarks.py
@@ -0,0 +1,329 @@
+import os
+import argparse
+import multiprocessing
+import logging
+
+# import mlonmcu
+import mlonmcu.context
+from mlonmcu.session.run import RunStage
+from mlonmcu.session.postprocess.postprocess import SessionPostprocess
+from mlonmcu.models.lookup import apply_modelgroups
+from mlonmcu.logging import get_logger, set_log_level
+
+logger = get_logger()
+
+
+class CustomPostprocess(SessionPostprocess):  # RunPostprocess?
+    """TODO"""
+
+    def __init__(self, features=None, config=None):
+        super().__init__("custom", features=features, config=config)
+
+    def post_session(self, report):
+        """Called at the end of a session."""
+        # df = report.post_df.copy()
+        # report.post_df = df
+
+
+FRONTEND = "tflite"
+
+TARGETS = [
+    "spike",
+    "ovpsim",
+    "host_x86",
+    "etiss_pulpino",
+    "corstone300",
+]
+
+DEFAULT_TARGETS = [
+    # "spike",
+    # "ovpsim",
+    # "host_x86",
+    "etiss_pulpino",
+    "corstone300",
+]
+
+PLATFORM = "mlif"
+
+BACKENDS = [
+    "tvmaot",
+]
+DEFAULT_BACKENDS = [
+    "tvmaot",
+]
+
+FEATURES = [
+    "unpacked_api",
+    "usmp",
+    "none",
+]
+
+DEFAULT_FEATURES = [
+    "unpacked_api",
+    "usmp",
+]
+
+
+VALIDATE_FEATURES = ["validate", "debug"]
+
+
+def get_backend_features(backend, enable_default=True, enable_unpacked_api=True, enable_usmp=True):
+    backend_features = {
+        "tvmaot": [
+            [],
+            *([["unpacked_api"]] if enable_unpacked_api else []),
+            *([["usmp"]] if enable_usmp else []),
+            *([["unpacked_api", "usmp"]] if enable_unpacked_api and enable_usmp else []),
+        ],
+    }
+    return backend_features[backend]
+
+
+def get_backend_config(backend, features):
+    return (
+        [
+            {"usmp.algorithm": "greedy_by_size"},
+            {"usmp.algorithm": "greedy_by_conflicts"},
+            {"usmp.algorithm": "hill_climb"},
+        ]
+        if "usmp" in features
+        else [{}]
+    )
+
+
+DEFAULT_CONFIG = {
+    "mlif.num_threads": 4,
+}
+
+BACKEND_DEFAULT_CONFIG = {
+    "tvmaot": {},
+}
+
+MODELS = [
+    "sine_model",
+    # "magic_wand",
+    # "micro_speech",
+    # "cifar10",
+    # "simple_mnist",
+    "aww",
+    "vww",
+    "resnet",
+    "toycar",
+]
+
+POSTPROCESSES_0 = [
+    "features2cols",
+    "config2cols",
+]
+
+POSTPROCESSES_1 = [
+    "rename_cols",
+    "filter_cols",
+]
+
+POSTPROCESS_CONFIG = {
+    "filter_cols.keep": [
+        "Model",
+        "Backend",
+        "Target",
+        "Cycles",
+        "Runtime [s]",
+        "Total ROM",
+        "Total RAM",
+        "ROM read-only",
+        "ROM code",
+        "ROM misc",
+        "RAM data",
+        "RAM zero-init data",
+        "Incomplete",
+        "Failing",
+        "Features",
+        # "Comment",
+        "Validation",
+        "Kernels",
+        "Unpacked API",
+        "USMP",
+        "Algorithm",
+        "RAM stack",
+        "RAM heap",
+    ],
+    "rename_cols.mapping": {
+        "feature_unpacked_api": "Unpacked API",
+        "feature_usmp": "USMP",
+        "config_usmp.algorithm": "Algorithm",
+    },
+    "filter_cols.drop_nan": True,
+}
+
+
+def gen_features(backend, features, validate=False):
+    ret = []
+    if validate:
+        ret += VALIDATE_FEATURES
+    ret += features
+    return ret
+
+
+def gen_config(backend, backend_config, features, enable_postprocesses=False):
+    ret = {}
+    ret.update(DEFAULT_CONFIG)
+    ret.update(BACKEND_DEFAULT_CONFIG[backend])
+    ret.update(backend_config)
+    if enable_postprocesses:
+        ret.update(POSTPROCESS_CONFIG)
+    return ret
+
+
+def benchmark(args):
+    with mlonmcu.context.MlonMcuContext() as context:
+        user_config = context.environment.vars  # TODO: get rid of this workaround
+        session = context.create_session()
+        models = apply_modelgroups(args.models, context=context)
+        for model in models:
+            for backend in args.backend:
+                for target in args.target:
+                    enable_default = not args.skip_default
+                    enable_unpacked_api = "unpacked_api" in args.feature
+                    enable_usmp = "usmp" in args.feature
+                    for backend_features in get_backend_features(
+                        backend,
+                        enable_default=enable_default,
+                        enable_unpacked_api=enable_unpacked_api,
+                        enable_usmp=enable_usmp,
+                    ):
+                        for backend_config in get_backend_config(backend, backend_features):
+                            features = gen_features(backend, backend_features, validate=args.validate)
+                            config = gen_config(backend, backend_config, features, enable_postprocesses=args.post)
+                            config.update(user_config)  # TODO
+                            run = session.create_run(config=config)
+                            run.add_features_by_name(features, context=context)
+                            run.add_platform_by_name(PLATFORM, context=context)
+                            run.add_frontend_by_name(FRONTEND, context=context)
+                            run.add_model_by_name(model, context=context)
+                            run.add_backend_by_name(backend, context=context)
+                            run.add_target_by_name(target, context=context)
+                            if args.post:
+                                run.add_postprocesses_by_name(POSTPROCESSES_0)
+                                run.add_postprocesses_by_name(POSTPROCESSES_1, append=True)
+        if args.noop:
+            stage = RunStage.LOAD
+        else:
+            stage = RunStage.RUN
+        session.process_runs(until=stage, num_workers=args.parallel, progress=args.progress, context=context)
+        report = session.get_reports()
+        report_file = args.output
+        report.export(report_file)
+        print()
+        print(report.df)
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "models",
+        metavar="model",
+        type=str,
+        # nargs="+",
+        nargs="*",
+        default=MODELS,
+        help="Model to process",
+    )
+    parser.add_argument(
+        "-b",
+        "--backend",
+        type=str,
+        action="append",
+        choices=BACKENDS,
+        # default=DEFAULT_BACKENDS,
+        default=[],
+        help=f"Backends to use (default: {DEFAULT_BACKENDS})",
+    )
+    parser.add_argument(
+        "-t",
+        "--target",
+        type=str,
+        action="append",
+        choices=TARGETS,
+        # default=DEFAULT_TARGETS,
+        default=[],
+        help=f"Targets to use (default: {DEFAULT_TARGETS}s)",
+    )
+    parser.add_argument(
+        "-f",
+        "--feature",
+        type=str,
+        action="append",
+        choices=FEATURES,
+        # default=default_features,
+        default=[],
+        help=f"features to use (default: {DEFAULT_FEATURES})",
+    )
+    parser.add_argument(
+        "--validate",
+        action="store_true",
+        help="Validate model outputs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--skip-default",
+        dest="skip_default",
+        action="store_true",
+        help="Do not generate benchmarks for reference runs (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--post",
+        action="store_true",
+        help="Run postprocesses after the session (default: %(default)s)",
+    )
+    parser.add_argument(
+        "-p",
+        "--progress",
+        action="store_true",
+        help="Display progress bar (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--parallel",
+        metavar="THREADS",
+        nargs="?",
+        type=int,
+        const=multiprocessing.cpu_count(),
+        default=1,
+        help="Use multiple threads to process runs in parallel (%(const)s if specified, else %(default)s)",
+    )
+    parser.add_argument(
+        "--output",
+        "-o",
+        metavar="FILE",
+        type=str,
+        default=os.path.join(os.getcwd(), "out.csv"),
+        help="""Output CSV file (default: %(default)s)""",
+    )
+    parser.add_argument(
+        "--verbose",
+        "-v",
+        action="store_true",
+        help="Print detailed messages for easier debugging (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--noop",
+        action="store_true",
+        help="Skip processing runs. (default: %(default)s)",
+    )
+    args = parser.parse_args()
+    if not args.backend:
+        args.backend = DEFAULT_BACKENDS
+    if not args.target:
+        args.target = DEFAULT_TARGETS
+    if not args.feature:
+        args.feature = DEFAULT_FEATURES
+    if "none" in args.feature:
+        assert len(args.feature) == 1
+        args.feature = []
+    if args.verbose:
+        set_log_level(logging.DEBUG)
+    else:
+        set_log_level(logging.INFO)
+    benchmark(args)
+
+
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 56db81a55..c2d67902a 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -25,7 +25,7 @@
 from io import BytesIO
 from zipfile import ZipFile
 
-import mlonmcu  # TODO: fix this bad?
+# import mlonmcu  # TODO: fix this bad?
 
 
 @pytest.fixture()
@@ -64,6 +64,8 @@ def __init__(self):
     class FakeEnvironment:
         def __init__(self):
             self.paths = {}  # TODO: get rid of PathConfig if possible?
+            self.flags = {}
+            self.vars = {}
 
     class FakeContext:
         def __init__(self):
@@ -85,6 +87,8 @@ def example_elf_file(request, tmp_path):
 
 @pytest.fixture()
 def user_context():
+    import mlonmcu.context
+
     with mlonmcu.context.MlonMcuContext() as context:
         yield context
 
diff --git a/tests/unit-tests/environment/test_environment_writer.py b/tests/unit-tests/environment/test_environment_writer.py
index 8a1cb6241..2a0e5dd57 100644
--- a/tests/unit-tests/environment/test_environment_writer.py
+++ b/tests/unit-tests/environment/test_environment_writer.py
@@ -8,6 +8,7 @@
 
 class MyEnvironment(Environment):
     def __init__(self):
+        super().__init__()
         self._home = "/foo/bar"
         self.defaults = DefaultsConfig(
             log_level=logging.DEBUG,
@@ -23,9 +24,11 @@ def __init__(self):
         self.repos = {"repo1": RepoConfig("repo1url"), "repo2": RepoConfig("repo2url", ref="repo2ref")}
         self.frameworks = []
         self.frontends = []
+        self.toolchains = {}
         self.platforms = []
         self.targets = []
         self.vars = {"key": "val"}
+        self.flags = {"my.var": ["foo", "bar"]}
 
 
 def test_create_environment_dict():
@@ -44,7 +47,9 @@ def test_create_environment_dict():
         "repos": {"repo1": {"url": "repo1url", "ref": None}, "repo2": {"url": "repo2url", "ref": "repo2ref"}},
         "frameworks": {"default": None},
         "frontends": {},
+        "toolchains": {},
         "platforms": {},
         "targets": {"default": None},
         "vars": {"key": "val"},
+        "flags": {"my.var": ["foo", "bar"]},
     }
diff --git a/tests/unit-tests/test_cli_parse.py b/tests/unit-tests/test_cli_parse.py
index 5e50aa431..fedef6779 100644
--- a/tests/unit-tests/test_cli_parse.py
+++ b/tests/unit-tests/test_cli_parse.py
@@ -42,17 +42,17 @@ def test_parse_vars():
 
 
 def test_extract_feature_names():
-    assert extract_feature_names(Namespace(feature=None)) == []
-    assert extract_feature_names(Namespace(feature=[])) == []
-    assert extract_feature_names(Namespace(feature=["x"])) == ["x"]
-    assert extract_feature_names(Namespace(feature=["x", "y"])) == ["x", "y"]
+    assert extract_feature_names(Namespace(feature=None, feature_gen=None))[0] == []
+    assert extract_feature_names(Namespace(feature=[], feature_gen=None))[0] == []
+    assert extract_feature_names(Namespace(feature=["x"], feature_gen=None))[0] == ["x"]
+    assert extract_feature_names(Namespace(feature=["x", "y"], feature_gen=None))[0] == ["x", "y"]
 
 
 def test_extract_config():
-    assert extract_config(Namespace(config=None)) == {}
-    assert extract_config(Namespace(config=[])) == {}
-    assert extract_config(Namespace(config=[[]])) == {}
-    assert extract_config(Namespace(config=[[], []])) == {}
-    assert extract_config(Namespace(config=[["foo=bar"]])) == {"foo": "bar"}
-    assert extract_config(Namespace(config=[["foo=bar", "a=b"]])) == {"foo": "bar", "a": "b"}
-    assert extract_config(Namespace(config=[["foo=bar"], ["a=b"]])) == {"foo": "bar", "a": "b"}
+    assert extract_config(Namespace(config=None, config_gen=None))[0] == {}
+    assert extract_config(Namespace(config=[], config_gen=None))[0] == {}
+    assert extract_config(Namespace(config=[[]], config_gen=None))[0] == {}
+    assert extract_config(Namespace(config=[[], []], config_gen=None))[0] == {}
+    assert extract_config(Namespace(config=[["foo=bar"]], config_gen=None))[0] == {"foo": "bar"}
+    assert extract_config(Namespace(config=[["foo=bar", "a=b"]], config_gen=None))[0] == {"foo": "bar", "a": "b"}
+    assert extract_config(Namespace(config=[["foo=bar"], ["a=b"]], config_gen=None))[0] == {"foo": "bar", "a": "b"}
diff --git a/tests/unit-tests/test_target.py b/tests/unit-tests/test_target.py
index f1b0d8a39..e86e076c8 100644
--- a/tests/unit-tests/test_target.py
+++ b/tests/unit-tests/test_target.py
@@ -21,8 +21,7 @@
 
 from mlonmcu.target.common import execute, cli
 from mlonmcu.target.target import Target
-from mlonmcu.target.etiss_pulpino import EtissPulpinoTarget
-from mlonmcu.target.host_x86 import HostX86Target
+from mlonmcu.target import EtissPulpinoTarget, HostX86Target
 
 
 class CustomTarget(Target):