diff --git a/.checkpatch.conf b/.checkpatch.conf
new file mode 100644
index 0000000..188c910
--- /dev/null
+++ b/.checkpatch.conf
@@ -0,0 +1,20 @@
+--emacs
+--summary-file
+--show-types
+--max-line-length=100
+--min-conf-desc-length=1
+--typedefsfile=../zephyr/scripts/checkpatch/typedefsfile
+
+--ignore BRACES
+--ignore PRINTK_WITHOUT_KERN_LEVEL
+--ignore SPLIT_STRING
+--ignore VOLATILE
+--ignore CONFIG_EXPERIMENTAL
+--ignore AVOID_EXTERNS
+--ignore NETWORKING_BLOCK_COMMENT_STYLE
+--ignore DATE_TIME
+--ignore MINMAX
+--ignore CONST_STRUCT
+--ignore FILE_PATH_CHANGES
+--ignore NEW_TYPEDEFS
+--exclude ext
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..cff6340
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,151 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# clang-format configuration file. Intended for clang-format >= 4.
+#
+# For more information, see:
+#
+# Documentation/process/clang-format.rst
+# https://clang.llvm.org/docs/ClangFormat.html
+# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
+#
+---
+AccessModifierOffset: -4
+AlignAfterOpenBracket: Align
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+#AlignEscapedNewlines: Left # Unknown to clang-format-4.0
+AlignOperands: true
+AlignTrailingComments: false
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: false
+BinPackArguments: true
+BinPackParameters: true
+BraceWrapping:
+ AfterClass: false
+ AfterControlStatement: false
+ AfterEnum: false
+ AfterFunction: true
+ AfterNamespace: true
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ #AfterExternBlock: false # Unknown to clang-format-5.0
+ BeforeCatch: false
+ BeforeElse: false
+ IndentBraces: false
+ #SplitEmptyFunction: true # Unknown to clang-format-4.0
+ #SplitEmptyRecord: true # Unknown to clang-format-4.0
+ #SplitEmptyNamespace: true # Unknown to clang-format-4.0
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Custom
+#BreakBeforeInheritanceComma: false # Unknown to clang-format-4.0
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+#BreakConstructorInitializers: BeforeComma # Unknown to clang-format-4.0
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit: 80
+CommentPragmas: '^ IWYU pragma:'
+#CompactNamespaces: false # Unknown to clang-format-4.0
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 8
+ContinuationIndentWidth: 8
+Cpp11BracedListStyle: false
+DerivePointerAlignment: false
+DisableFormat: false
+ExperimentalAutoDetectBinPacking: false
+#FixNamespaceComments: false # Unknown to clang-format-4.0
+
+# Taken from:
+# git grep -h '^#define [^[:space:]]*for_each[^[:space:]]*(' include/ \
+# | sed "s,^#define \([^[:space:]]*for_each[^[:space:]]*\)(.*$, - '\1'," \
+# | sort | uniq
+ForEachMacros:
+ - 'FOR_EACH'
+ - 'for_each_linux_bus'
+ - 'for_each_linux_driver'
+ - 'metal_bitmap_for_each_clear_bit'
+ - 'metal_bitmap_for_each_set_bit'
+ - 'metal_for_each_page_size_down'
+ - 'metal_for_each_page_size_up'
+ - 'metal_list_for_each'
+ - 'RB_FOR_EACH'
+ - 'RB_FOR_EACH_CONTAINER'
+ - 'SYS_DLIST_FOR_EACH_CONTAINER'
+ - 'SYS_DLIST_FOR_EACH_CONTAINER_SAFE'
+ - 'SYS_DLIST_FOR_EACH_NODE'
+ - 'SYS_DLIST_FOR_EACH_NODE_SAFE'
+ - 'SYS_SFLIST_FOR_EACH_CONTAINER'
+ - 'SYS_SFLIST_FOR_EACH_CONTAINER_SAFE'
+ - 'SYS_SFLIST_FOR_EACH_NODE'
+ - 'SYS_SFLIST_FOR_EACH_NODE_SAFE'
+ - 'SYS_SLIST_FOR_EACH_CONTAINER'
+ - 'SYS_SLIST_FOR_EACH_CONTAINER_SAFE'
+ - 'SYS_SLIST_FOR_EACH_NODE'
+ - 'SYS_SLIST_FOR_EACH_NODE_SAFE'
+ - '_WAIT_Q_FOR_EACH'
+ - 'Z_GENLIST_FOR_EACH_CONTAINER'
+ - 'Z_GENLIST_FOR_EACH_CONTAINER_SAFE'
+ - 'Z_GENLIST_FOR_EACH_NODE'
+ - 'Z_GENLIST_FOR_EACH_NODE_SAFE'
+
+#IncludeBlocks: Preserve # Unknown to clang-format-5.0
+IncludeCategories:
+ - Regex: '.*'
+ Priority: 1
+IncludeIsMainRegex: '(Test)?$'
+IndentCaseLabels: false
+#IndentPPDirectives: None # Unknown to clang-format-5.0
+IndentWidth: 8
+IndentWrappedFunctionNames: false
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: Inner
+#ObjCBinPackProtocolList: Auto # Unknown to clang-format-5.0
+ObjCBlockIndentWidth: 8
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+
+# Taken from git's rules
+#PenaltyBreakAssignment: 10 # Unknown to clang-format-4.0
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakString: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+
+PointerAlignment: Right
+ReflowComments: false
+SortIncludes: false
+#SortUsingDeclarations: false # Unknown to clang-format-4.0
+SpaceAfterCStyleCast: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+#SpaceBeforeCtorInitializerColon: true # Unknown to clang-format-5.0
+#SpaceBeforeInheritanceColon: true # Unknown to clang-format-5.0
+SpaceBeforeParens: ControlStatements
+#SpaceBeforeRangeBasedForLoopColon: true # Unknown to clang-format-5.0
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+Standard: Cpp03
+TabWidth: 8
+UseTab: Always
+...
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ec54228
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,55 @@
+# EditorConfig: https://editorconfig.org/
+
+# top-most EditorConfig file
+root = true
+
+# All (Defaults)
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 100
+
+# Assembly
+[*.S]
+indent_style = tab
+indent_size = 8
+
+# C
+[*.{c,h}]
+indent_style = tab
+indent_size = 8
+
+# Python
+[*.py]
+indent_style = space
+indent_size = 4
+
+# YAML
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
+
+# Shell Script
+[*.sh]
+indent_style = space
+indent_size = 4
+
+# CMake
+[{CMakeLists.txt,*.cmake}]
+indent_style = space
+indent_size = 2
+
+# Makefile
+[Makefile]
+indent_style = tab
+
+# Kconfig
+[Kconfig]
+indent_style = tab
+
+# devicetree
+[*.{dts,dtsi,overlay}]
+indent_style = tab
+indent_size = 4
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..701cbec
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,42 @@
+# Copyright (c) 2021, Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+name: Build
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ container: teslabs/spinner:latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ path: spinner
+
+ - name: Cache Zephyr
+ uses: actions/cache@v2
+ with:
+ path: |
+ modules
+ zephyr
+ key: ${{ hashFiles('spinner/west.yml') }}
+
+ - name: Initialize
+ working-directory: spinner
+ run: |
+ pip3 install -U west
+ west init -l .
+ west update
+ pip3 install -r ../zephyr/scripts/requirements-base.txt
+ pip3 install -r docs/requirements.txt
+
+ - name: Lint code
+ run: |
+ git diff --name-only ${{ github.base_ref}} ${{ github.sha }} -- '*.[ch]' | xargs zephyr/scripts/checkpatch.pl
+
+ - name: Build
+ working-directory: spinner
+ run: |
+ west build -b p_nucleo_ihm002 -s spinner
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..e67a176
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,34 @@
+# Copyright (c) 2021, Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+name: Documentation
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install dependencies
+ run: |
+ sudo apt install -y cmake ninja-build python3 python3-pip doxygen graphviz
+ pip3 install -r docs/requirements.txt
+
+ - name: Build
+ run: |
+ cmake -Sdocs -Bbuild_docs
+ cmake --build build_docs -t doxygen
+ cmake --build build_docs -t html
+ touch build_docs/html/.nojekyll
+
+ - name: Deploy
+ uses: JamesIves/github-pages-deploy-action@4.1.3
+ if: github.ref == 'refs/heads/main'
+ with:
+ branch: gh-pages
+ folder: build_docs/html
+ single-commit: true
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..365ec3a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+# zephyr
+/build*
+
+# python
+.venv
+
+# editors
+*.swp
+.vscode
+.~lock*
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..ce3bdf6
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,7 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+add_subdirectory(drivers)
+add_subdirectory(lib)
+
+zephyr_include_directories(include)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..9f9a8bd
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,41 @@
+FROM ubuntu:20.04
+
+# install dependencies
+ARG DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ git \
+ cmake \
+ ninja-build \
+ gperf \
+ ccache \
+ dfu-util \
+ device-tree-compiler \
+ wget \
+ python3-dev \
+ python3-pip \
+ python3-setuptools \
+ python3-tk \
+ python3-wheel \
+ xz-utils \
+ file \
+ make \
+ gcc \
+ gcc-multilib \
+ g++-multilib \
+ libsdl2-dev \
+ doxygen \
+ && rm -rf /var/lib/apt/lists/*
+
+# install SDK
+ARG ZSDK_TOOL=toolchain-arm
+ARG ZSDK_VERSION=0.12.4
+RUN wget -q "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${ZSDK_VERSION}/zephyr-${ZSDK_TOOL}-${ZSDK_VERSION}-x86_64-linux-setup.run" && \
+ sh "zephyr-${ZSDK_TOOL}-${ZSDK_VERSION}-x86_64-linux-setup.run" --quiet -- -d /opt/toolchains/zephyr-${ZSDK_TOOL}-${ZSDK_VERSION} && \
+ rm "zephyr-${ZSDK_TOOL}-${ZSDK_VERSION}-x86_64-linux-setup.run"
+
+ENV ZEPHYR_TOOLCHAIN_VARIANT=zephyr
+ENV ZEPHYR_SDK_INSTALL_DIR=/opt/toolchains/zephyr-${ZSDK_TOOL}-${ZSDK_VERSION}
+
+# install West
+RUN pip3 install west
diff --git a/Kconfig b/Kconfig
new file mode 100644
index 0000000..112f82b
--- /dev/null
+++ b/Kconfig
@@ -0,0 +1,5 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+rsource "drivers/Kconfig"
+rsource "lib/Kconfig"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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/README.md b/README.md
new file mode 100644
index 0000000..b8d18f2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+# Introduction
+
+Spinner is a motor control firmware based on the Field Oriented Control (FOC)
+principles. The firmware is built on top of the [Zephyr RTOS](https://zephyrproject.org),
+a modern multi-platform RTOS. Spinner is still a proof of concept, so do not
+expect production grade stability or features.
+
+## Getting Started
+
+Before getting started, make sure you have a proper Zephyr development
+environment. You can follow the official
+[Zephyr Getting Started Guide](https://docs.zephyrproject.org/latest/getting_started/index.html).
+
+### Initialization
+
+The first step is to initialize the `spinner` workspace folder where the
+source and all Zephyr modules will be cloned. You can do that by running:
+
+```shell
+# initialize spinner workspace
+west init -m git@github.com:teslabs/spinner --mr main spinner
+# update modules
+cd spinner
+west update
+```
+
+### Build & Run
+
+The application can be built by running:
+
+```shell
+west build -b $BOARD -s spinner
+```
+
+where `$BOARD` is the target board (see `boards`). Some other build
+configurations are also provided:
+
+- `debug.conf`: Enable debug-friendly build
+- `shell.conf`: Enable shell facilities
+
+They can be enabled by setting `OVERLAY_CONFIG`, e.g.
+
+```shell
+west build -b $BOARD -s spinner -- -DOVERLAY_CONFIG=debug.conf
+```
+
+Once you have built the application you can flash it by running:
+
+```shell
+west flash
+```
+
+## Documentation
+
+The documentation is based on Sphinx. Doxygen is used to extract the API
+docstrings, but its HTML output can also be used if preferred. A simple CMake
+script is provided in order to facilitate the documentation build process. In
+order to configure CMake you need to run:
+
+```shell
+cmake -Sdocs -Bbuild_docs
+```
+
+In order to build the Doxygen documentation you need to run:
+
+```shell
+cmake --build build_docs -t doxygen
+```
+
+Note that Doxygen output is required by Sphinx, so every time you change your
+API docstrings, remember to run the `doxygen` target. In order to build the
+Sphinx HTML documentation you need to run:
+
+```shell
+cmake --build build_docs -t html
+```
diff --git a/boards/arm/p_nucleo_ihm002/Kconfig.board b/boards/arm/p_nucleo_ihm002/Kconfig.board
new file mode 100644
index 0000000..102c7c0
--- /dev/null
+++ b/boards/arm/p_nucleo_ihm002/Kconfig.board
@@ -0,0 +1,6 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+config BOARD_P_NUCLEO_IHM002
+ bool "P-NUCLEO-IHM002"
+ depends on SOC_STM32F302X8
diff --git a/boards/arm/p_nucleo_ihm002/Kconfig.defconfig b/boards/arm/p_nucleo_ihm002/Kconfig.defconfig
new file mode 100644
index 0000000..07795db
--- /dev/null
+++ b/boards/arm/p_nucleo_ihm002/Kconfig.defconfig
@@ -0,0 +1,12 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+if BOARD_P_NUCLEO_IHM002
+
+config BOARD
+ default "p_nucleo_ihm002"
+
+config FPU
+ default y
+
+endif # BOARD_P_NUCLEO_IHM002
diff --git a/boards/arm/p_nucleo_ihm002/board.cmake b/boards/arm/p_nucleo_ihm002/board.cmake
new file mode 100644
index 0000000..e53d5ef
--- /dev/null
+++ b/boards/arm/p_nucleo_ihm002/board.cmake
@@ -0,0 +1,7 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+board_runner_args(jlink "--device=STM32F302R8" "--speed=4000")
+
+include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake)
+include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
diff --git a/boards/arm/p_nucleo_ihm002/p_nucleo_ihm002.dts b/boards/arm/p_nucleo_ihm002/p_nucleo_ihm002.dts
new file mode 100644
index 0000000..d1edede
--- /dev/null
+++ b/boards/arm/p_nucleo_ihm002/p_nucleo_ihm002.dts
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/dts-v1/;
+#include
+#include
+#include
+
+/ {
+ model = "P-NUCLEO-IHM002";
+
+ chosen {
+ zephyr,console = &usart2;
+ zephyr,shell-uart = &usart2;
+ zephyr,sram = &sram0;
+ zephyr,flash = &flash0;
+ };
+};
+
+&clk_hse {
+ hse-bypass;
+ clock-frequency = ; /* STLink 8MHz clock */
+ status = "okay";
+};
+
+&pll {
+ prediv = <1>;
+ mul = <9>;
+ clocks = <&clk_hse>;
+ status = "okay";
+};
+
+&rcc {
+ clocks = <&pll>;
+ clock-frequency = ;
+ ahb-prescaler = <1>;
+ apb1-prescaler = <2>;
+ apb2-prescaler = <1>;
+};
+
+&timers1 {
+ svpwm: svpwm {
+ compatible = "st,stm32-svpwm";
+ pinctrl-0 = <&tim1_ch1_pa8 &tim1_ch2_pa9 &tim1_ch3_pa10 &tim1_ocp_pa11>;
+
+ /* L6230 dead time (Table 6) */
+ t-dead-ns = <1000>;
+ /* L6230 rise time: tD(ON) + tRISE (Fig. 4) */
+ t-rise-ns = <1050>;
+ currsmp = <&currsmp>;
+ enable-gpios = <&gpioc 10 GPIO_ACTIVE_HIGH>,
+ <&gpioc 11 GPIO_ACTIVE_HIGH>,
+ <&gpioc 12 GPIO_ACTIVE_HIGH>;
+ };
+};
+
+&adc1 {
+ currsmp: currsmp {
+ compatible = "st,stm32-currsmp-shunt";
+ pinctrl-0 = <&adc1_in1_pa0 &adc1_in7_pc1 &adc1_in6_pc0>;
+
+ adc-channels = <1 7 6>;
+ adc-trigger = ;
+ };
+};
+
+&timers2 {
+ feedback: feedback {
+ compatible = "st,stm32-halls";
+
+ pinctrl-0 = <&tim2_ch1_pa15 &tim2_ch2_pb3 &tim2_ch3_pb10>;
+
+ h1-gpios = <&gpioa 15 0>;
+ h2-gpios = <&gpiob 3 0>;
+ h3-gpios = <&gpiob 10 0>;
+ phase-shift = <60>;
+ };
+};
+
+&usart2 {
+ status = "okay";
+ pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>;
+ current-speed = <115200>;
+};
+
+/* FIXME: missing upstream */
+&pinctrl {
+ tim1_ocp_pa11: tim1_ocp_pa11 {
+ pinmux = ;
+ };
+};
diff --git a/boards/arm/p_nucleo_ihm002/p_nucleo_ihm002_defconfig b/boards/arm/p_nucleo_ihm002/p_nucleo_ihm002_defconfig
new file mode 100644
index 0000000..d79d952
--- /dev/null
+++ b/boards/arm/p_nucleo_ihm002/p_nucleo_ihm002_defconfig
@@ -0,0 +1,9 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+CONFIG_SOC_SERIES_STM32F3X=y
+CONFIG_SOC_STM32F302X8=y
+
+CONFIG_PINMUX=y
+CONFIG_GPIO=y
+CONFIG_CLOCK_CONTROL=y
diff --git a/boards/arm/p_nucleo_ihm002/support/openocd.cfg b/boards/arm/p_nucleo_ihm002/support/openocd.cfg
new file mode 100644
index 0000000..e69af5e
--- /dev/null
+++ b/boards/arm/p_nucleo_ihm002/support/openocd.cfg
@@ -0,0 +1,15 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+source [find board/st_nucleo_f3.cfg]
+
+$_TARGETNAME configure -event gdb-attach {
+ echo "Debugger attaching: halting execution"
+ reset halt
+ gdb_breakpoint_override hard
+}
+
+$_TARGETNAME configure -event gdb-detach {
+ echo "Debugger detaching: resuming execution"
+ resume
+}
diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt
new file mode 100644
index 0000000..27046fe
--- /dev/null
+++ b/docs/CMakeLists.txt
@@ -0,0 +1,31 @@
+cmake_minimum_required(VERSION 3.13.1)
+project(docs LANGUAGES NONE)
+
+find_package(Doxygen REQUIRED)
+find_program(SPHINXBUILD NAMES sphinx-build)
+if(NOT SPHINXBUILD)
+ message(FATAL_ERROR "The 'sphinx-build' command was not found")
+endif()
+
+set(SPINNER_BASE ${CMAKE_SOURCE_DIR}/..)
+set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/html/doxygen)
+set(SPHINXOPTS "" CACHE STRING "Sphinx options")
+
+configure_file(Doxyfile.in ${CMAKE_BINARY_DIR}/Doxyfile)
+
+add_custom_target(
+ doxygen
+ COMMAND
+ ${CMAKE_COMMAND} -E make_directory ${DOXYGEN_OUTPUT_DIRECTORY}
+ COMMAND
+ ${DOXYGEN_EXECUTABLE} ${CMAKE_BINARY_DIR}/Doxyfile
+)
+
+add_custom_target(
+ html
+ COMMAND
+ ${CMAKE_COMMAND} -E env
+ SPINNER_BASE=${SPINNER_BASE}
+ DOXYGEN_OUTPUT_DIRECTORY=${DOXYGEN_OUTPUT_DIRECTORY}
+ ${SPHINXBUILD} -b html ${SPHINXOPTS} ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}/html
+)
diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in
new file mode 100644
index 0000000..14514e9
--- /dev/null
+++ b/docs/Doxyfile.in
@@ -0,0 +1,18 @@
+PROJECT_NAME = "Spinner"
+PROJECT_BRIEF = "FOC Motor Control Firmware"
+PROJECT_LOGO = @SPINNER_BASE@/docs/_static/images/logo.png
+INPUT = @SPINNER_BASE@/docs/doxygen.md @SPINNER_BASE@/include
+EXCLUDE = @SPINNER_BASE@/include/dts-bindings
+OUTPUT_DIRECTORY = @DOXYGEN_OUTPUT_DIRECTORY@
+STRIP_FROM_PATH = @SPINNER_BASE@/include
+FILE_PATTERNS = *.h
+RECURSIVE = YES
+GENERATE_HTML = YES
+GENERATE_XML = YES
+GENERATE_LATEX = NO
+GENERATE_TREEVIEW = YES
+EXTRACT_STATIC = YES
+OPTIMIZE_OUTPUT_FOR_C = YES
+TAB_SIZE = 8
+QUIET = YES
+WARN_AS_ERROR = YES
diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css
new file mode 100755
index 0000000..95b6eaf
--- /dev/null
+++ b/docs/_static/css/custom.css
@@ -0,0 +1,934 @@
+/**
+ * Copyright (c) 2019-2020, Juan Linietsky, Ariel Manzur and the Godot community
+ * Copyright (c) 2021, Teslabs Engineering S.L.
+ * SPDX-License-Identifier: CC-BY-3.0
+ *
+ * Various tweaks to the Read the Docs theme to better conform with Zephyr's
+ * visual identity. Many colors are also overridden to use CSS variables.
+ */
+
+body,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+input[type="text"],
+input[type="button"],
+input[type="reset"],
+input[type="submit"],
+textarea,
+legend,
+.btn,
+.rst-content .toctree-wrapper p.caption,
+.rst-versions {
+ /* Use a system font stack for better performance (no Web fonts required) */
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+legend,
+.rst-content .toctree-wrapper p.caption {
+ /* Use a lighter font for headers (Medium instead of Bold) */
+ font-weight: 500;
+}
+
+.rst-content div.figure p.caption {
+ /* Tweak caption styling to be closer to typical captions */
+ text-align: center;
+ margin-top: 8px;
+ opacity: 0.75;
+}
+
+.rst-content div.figure.figure-w480 {
+ max-width: 480px;
+}
+
+p,
+article ul,
+article ol,
+.wy-plain-list-disc,
+.wy-plain-list-decimal,
+.rst-content ol.arabic,
+.rst-content .section ul,
+.rst-content .toctree-wrapper ul,
+.rst-content .section ol {
+ /* Increase the line height slightly to account for the different font */
+ line-height: 25px;
+}
+
+body,
+.rst-content table.docutils thead {
+ color: var(--body-color);
+}
+
+a {
+ color: var(--link-color);
+}
+
+a:hover {
+ color: var(--link-color-hover);
+ text-decoration: underline;
+}
+
+a:active {
+ /* Add visual feedback when clicking on a link */
+ color: var(--link-color-active);
+}
+
+a:visited {
+ color: var(--link-color-visited);
+}
+
+a.btn:hover {
+ text-decoration: none;
+}
+
+.sphinx-tabs .sphinx-menu a.item {
+ /* Original definition has `!important` */
+ color: var(--link-color) !important;
+}
+
+.rst-content .toc-backref {
+ color: var(--link-color);
+}
+
+/* Style external links differently to make them easier to distinguish from internal links. */
+.reference.external {
+ background-position: center right;
+ background-repeat: no-repeat;
+ background-image: var(--external-reference-icon);
+ padding-right: 13px;
+}
+
+li .reference.external {
+ background-image: none;
+}
+
+hr,
+#search-results .search li:first-child,
+#search-results .search li {
+ border-color: var(--hr-color);
+}
+
+/* JavaScript documentation directives */
+.rst-content dl:not(.docutils) dt {
+ background-color: var(--admonition-note-background-color) !important;
+ border-color: var(--admonition-note-title-background-color) !important;
+ color: var(--admonition-note-color) !important;
+}
+.rst-content dl:not(.docutils) dl dt {
+ background-color: var(--admonition-attention-background-color) !important;
+ border-color: var(--admonition-attention-title-background-color) !important;
+ color: var(--admonition-attention-color) !important;
+}
+.rst-content dl:not(.docutils).class dt,
+.rst-content dl:not(.docutils).function dt,
+.rst-content dl:not(.docutils).method dt,
+.rst-content dl:not(.docutils).attribute dt {
+ width: 100% !important;
+}
+.rst-content dl:not(.docutils).class > dt,
+.rst-content dl:not(.docutils).function > dt,
+.rst-content dl:not(.docutils).method > dt,
+.rst-content dl:not(.docutils).attribute > dt {
+ font-size: 100% !important;
+ font-weight: normal !important;
+ margin-bottom: 16px !important;
+ padding: 6px 8px !important;
+}
+.rst-content dl:not(.docutils) tt.descclassname,
+.rst-content dl:not(.docutils) code.descclassname {
+ color: var(--highlight-type2-color) !important;
+ font-weight: normal !important;
+}
+.rst-content dl:not(.docutils) tt.descname,
+.rst-content dl:not(.docutils) code.descname {
+ color: var(--highlight-function-color) !important;
+ font-weight: normal !important;
+}
+.rst-content dl:not(.docutils) .sig-paren,
+.rst-content dl:not(.docutils) .optional {
+ color: var(--highlight-operator-color) !important;
+ font-weight: normal !important;
+ padding: 0 2px !important;
+}
+.rst-content dl:not(.docutils) .optional {
+ font-style: italic !important;
+}
+.rst-content dl:not(.docutils) .sig-param,
+.rst-content dl:not(.docutils).class dt > em,
+.rst-content dl:not(.docutils).function dt > em,
+.rst-content dl:not(.docutils).method dt > em {
+ color: var(--code-literal-color) !important;
+ font-style: normal !important;
+ padding: 0 4px !important;
+}
+.rst-content dl:not(.docutils) .sig-param,
+.rst-content dl:not(.docutils).class dt > code,
+.rst-content dl:not(.docutils).function dt > code,
+.rst-content dl:not(.docutils).method dt > code {
+ padding: 0 4px !important;
+}
+.rst-content dl:not(.docutils) .sig-param,
+.rst-content dl:not(.docutils).class dt > .optional ~ em,
+.rst-content dl:not(.docutils).function dt > .optional ~ em,
+.rst-content dl:not(.docutils).method dt > .optional ~ em {
+ color: var(--highlight-number-color) !important;
+ font-style: italic !important;
+}
+.rst-content dl:not(.docutils).class dt > em.property {
+ color: var(--highlight-keyword-color) !important;
+}
+.rst-content dl:not(.docutils) dt a.headerlink {
+ color: var(--link-color) !important;
+}
+.rst-content dl:not(.docutils) dt a.headerlink:visited {
+ color: var(--link-color-visited) !important;
+}
+
+footer,
+#search-results .context {
+ color: var(--footer-color);
+}
+
+/* Icon tweaks */
+a.icon-home,
+a.icon-home:visited {
+ color: var(--navbar-level-1-color);
+}
+
+/* Sphinx Search extension */
+/* .wy-body-for-nav is used for higher rule specificity */
+
+/* Search popup body */
+.wy-body-for-nav .search__outer {
+ background-color: var(--content-background-color);
+ border: 2px solid var(--content-background-color);
+}
+.wy-body-for-nav .search__cross svg {
+ fill: var(--body-color);
+}
+
+.wy-body-for-nav .search__outer::-webkit-scrollbar-track {
+ border-radius: 10px;
+ background-color: var(--content-background-color);
+}
+.wy-body-for-nav .search__outer::-webkit-scrollbar {
+ width: 7px;
+ height: 7px;
+ background-color: var(--content-background-color);
+}
+.wy-body-for-nav .search__outer::-webkit-scrollbar-thumb {
+ border-radius: 10px;
+ background-color: var(--hr-color);
+}
+
+/* Search input */
+.wy-body-for-nav .search__outer__input {
+ background-color: var(--search-input-background-color);
+ background-image: none;
+ border-radius: 50px;
+ border: 2px solid transparent;
+ color: var(--body-color);
+ height: 36px;
+ padding: 6px 12px;
+}
+.wy-body-for-nav .search__outer__input:focus {
+ border-color: var(--input-focus-border-color);
+}
+.wy-body-for-nav .search__outer .bar:after,
+.wy-body-for-nav .search__outer .bar:before {
+ display: none;
+}
+
+/* Search results item */
+.wy-body-for-nav .search__result__single {
+ border-bottom-color: var(--hr-color);
+}
+/* Search item title */
+.wy-body-for-nav .search__result__title {
+ color: var(--link-color);
+ border-bottom: none;
+ font-size: 120%;
+ font-weight: 400;
+}
+
+/* Search item section */
+.wy-body-for-nav .outer_div_page_results:hover,
+.wy-body-for-nav .search__result__box .active {
+ background-color: var(--search-active-color);
+}
+.wy-body-for-nav .search__result__subheading{
+ color: var(--body-color);
+ font-size: 100%;
+ font-weight: 400;
+}
+.wy-body-for-nav .search__result__content {
+ color: var(--footer-color);
+}
+
+/* Search item matching substring */
+.wy-body-for-nav .search__outer .search__result__title span,
+.wy-body-for-nav .search__outer .search__result__content span {
+ color: var(--search-match-color);
+ border-bottom: 1px solid var(--search-match-color);
+ background-color: var(--search-match-background-color);
+ padding: 0 2px;
+}
+.wy-body-for-nav .search__result__subheading span {
+ border-bottom-color: var(--body-color);
+}
+
+/* Search empty results */
+/* The original styles are inlined, see https://github.com/readthedocs/readthedocs-sphinx-search/issues/48 */
+.wy-body-for-nav .search__result__box {
+ color: var(--body-color) !important;
+}
+
+/* Search footer & credits */
+.wy-body-for-nav .rtd__search__credits {
+ background-color: var(--search-credits-background-color);
+ border-color: var(--search-credits-background-color);
+ color: var(--search-credits-color);
+ padding: 4px 8px;
+}
+.wy-body-for-nav .rtd__search__credits a {
+ color: var(--search-credits-link-color);
+}
+
+/* Main sections */
+
+.wy-nav-content-wrap {
+ background-color: var(--content-wrap-background-color);
+}
+
+.wy-nav-content {
+ background-color: var(--content-background-color);
+}
+
+.wy-body-for-nav {
+ background-color: var(--content-wrap-background-color);
+}
+
+@media only screen and (min-width: 769px) {
+ .wy-nav-content {
+ max-width: 100%;
+ }
+
+ .rst-content {
+ max-width: 800px;
+ margin: 0 auto;
+ }
+}
+
+/* Table display tweaks */
+
+.rst-content table.docutils,
+.wy-table-bordered-all td,
+.rst-content table.docutils td,
+.wy-table thead th,
+.rst-content table.docutils thead th,
+.rst-content table.field-list thead th {
+ border-color: var(--code-border-color);
+}
+
+.wy-table-odd td,
+.wy-table-striped tr:nth-child(2n-1) td,
+.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td {
+ background-color: var(--table-row-odd-background-color);
+}
+
+/* Override table no-wrap */
+/* The first column cells are not verbose, no need to wrap them */
+.wy-table-responsive table td:not(:nth-child(1)),
+.wy-table-responsive table th:not(:nth-child(1)) {
+ white-space: normal;
+}
+
+/* Make sure not to wrap keyboard shortcuts */
+.wy-table-responsive table td kbd {
+ white-space: nowrap;
+}
+
+/* Force table content font-size in responsive tables to be 100%
+ * fixing larger font size for paragraphs in the kconfig tables */
+ .wy-table-responsive td p {
+ font-size: 100%;
+}
+
+/* Code display tweaks */
+
+code,
+.rst-content tt,
+.rst-content code {
+ font-size: 14px;
+ background-color: var(--code-background-color);
+ border: 1px solid var(--code-border-color);
+}
+
+.rst-content tt.literal,
+.rst-content code.literal {
+ color: var(--code-literal-color);
+}
+
+.rst-content div[class^="highlight"] {
+ border-color: none;
+ border: none;
+}
+
+.rst-content pre.literal-block,
+.rst-content div[class^="highlight"] pre,
+.rst-content .linenodiv pre {
+ /* Increase the font size and line height in code blocks */
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.rst-content pre.literal-block {
+ border: none;
+ border-radius: 0.25rem;
+ background-color: var(--code-background-color);
+}
+
+/* Code tab display tweaks */
+
+.ui.tabular.menu .active.item,
+.ui.segment,
+.sphinx-tabs-panel {
+ background-color: var(--code-background-color);
+}
+
+.sphinx-tabs-tab {
+ color: var(--link-color);
+}
+
+.sphinx-tabs-tab[aria-selected="true"] {
+ background-color: var(--code-background-color);
+ border-bottom: 1px solid var(--code-background-color);
+}
+
+/* Code literals */
+a.internal code.literal {
+ color: var(--link-color);
+}
+
+a.internal:visited code.literal {
+ color: var(--link-color-visited);
+}
+
+/* Syntax highlighting */
+
+.tab div[class^='highlight']:last-child {
+ margin-bottom: 1em;
+}
+
+.rst-content tt.literal, .rst-content code.literal, .highlight {
+ border-radius: 0.25rem;
+}
+
+.highlight {
+ background-color: var(--highlight-background-color);
+}
+
+/* Emphasized lines */
+.highlight .hll {
+ background-color: var(--highlight-background-emph-color);
+}
+
+.highlight .gh /* Generic.Heading */,
+.highlight .gu /* Generic.Subheading */,
+.highlight .go /* Generic.Output */,
+.highlight .gt /* Generic.Traceback */ {
+ color: var(--highlight-default-color);
+}
+
+.highlight .c /* Comment */,
+.highlight .c1 /* Comment.Single */,
+.highlight .cm /* Comment.Multiline */,
+.highlight .cs /* Comment.Special */ {
+ color: var(--highlight-comment-color);
+}
+
+.highlight .bp /* Name.Builtin.Pseudo */,
+.highlight .k /* Keyword */,
+.highlight .kc /* Keyword.Constant */,
+.highlight .kd /* Keyword.Declaration */,
+.highlight .kn /* Keyword.Namespace */,
+.highlight .kp /* Keyword.Pseudo */,
+.highlight .kr /* Keyword.Reserved */,
+.highlight .kt /* Keyword.Type */,
+.highlight .ow /* Operator.Word */ {
+ color: var(--highlight-keyword-color);
+}
+
+.highlight .ch /* Comment.Hashbang */,
+.highlight .cp /* Comment.Preproc */ {
+ color: var(--highlight-keyword2-color);
+}
+
+.highlight .m /* Literal.Number */,
+.highlight .mf /* Literal.Number.Float */,
+.highlight .mi /* Literal.Number.Integer */,
+.highlight .il /* Literal.Number.Integer.Long */,
+.highlight .mb /* Literal.Number.Bin */,
+.highlight .mh /* Literal.Number.Hex */,
+.highlight .mo /* Literal.Number.Oct */ {
+ color: var(--highlight-number-color);
+}
+
+.highlight .na /* Name.Attribute */,
+.highlight .nd /* Name.Decorator */,
+.highlight .ni /* Name.Entity */,
+.highlight .nl /* Name.Label */ {
+ color: var(--highlight-decorator-color);
+}
+
+.highlight .nb /* Name.Builtin */,
+.highlight .ne /* Name.Exception */ {
+ color: var(--highlight-type-color);
+}
+
+.highlight .nc /* Name.Class */,
+.highlight .nn /* Name.Namespace */,
+.highlight .no /* Name.Constant */,
+.highlight .nv /* Name.Variable */,
+.highlight .vc /* Name.Variable.Class */,
+.highlight .vg /* Name.Variable.Global */,
+.highlight .vi /* Name.Variable.Instance */,
+.highlight .vm /* Name.Variable.Magic */ {
+ color: var(--highlight-type2-color);
+}
+
+.highlight .nf /* Name.Function */,
+.highlight .fm /* Name.Function.Magic */,
+.highlight .nt /* Name.Tag */ {
+ color: var(--highlight-function-color);
+}
+
+.highlight .o /* Operator */,
+.highlight .si /* Literal.String.Interpol */,
+.highlight .sx /* Literal.String.Other */,
+.highlight .sr /* Literal.String.Regex */,
+.highlight .ss /* Literal.String.Symbol */ {
+ color: var(--highlight-operator-color);
+}
+
+.highlight .cpf/* Comment.PreprocFile */,
+.highlight .s /* Literal.String */,
+.highlight .s1 /* Literal.String.Single */,
+.highlight .s2 /* Literal.String.Double */,
+.highlight .sc /* Literal.String.Char */,
+.highlight .se /* Literal.String.Escape */,
+.highlight .sa /* Literal.String.Affix */,
+.highlight .sb /* Literal.String.Backtick */,
+.highlight .dl /* Literal.String.Delimiter */,
+.highlight .sd /* Literal.String.Doc */,
+.highlight .sh /* Literal.String.Heredoc */ {
+ color: var(--highlight-string-color);
+}
+
+/* Admonition tweaks */
+
+.rst-content .admonition.note,
+.rst-content .admonition.seealso {
+ background-color: var(--admonition-note-background-color);
+ color: var(--admonition-note-color);
+}
+
+.rst-content .admonition.note .admonition-title,
+.rst-content .admonition.seealso .admonition-title {
+ background-color: var(--admonition-note-title-background-color);
+ color: var(--admonition-note-title-color);
+}
+
+.rst-content .admonition.attention,
+.rst-content .admonition.caution,
+.rst-content .admonition.warning {
+ background-color: var(--admonition-attention-background-color);
+ color: var(--admonition-attention-color);
+}
+
+.rst-content .admonition.attention .admonition-title,
+.rst-content .admonition.caution .admonition-title,
+.rst-content .admonition.warning .admonition-title {
+ background-color: var(--admonition-attention-title-background-color);
+ color: var(--admonition-attention-title-color);
+}
+
+.rst-content .admonition.danger {
+ background-color: var(--admonition-danger-background-color);
+ color: var(--admonition-danger-color);
+}
+
+.rst-content .admonition.danger .admonition-title {
+ background-color: var(--admonition-danger-title-background-color);
+ color: var(--admonition-danger-title-color);
+}
+
+.rst-content .admonition.tip,
+.rst-content .admonition.important {
+ background-color: var(--admonition-tip-background-color);
+ color: var(--admonition-tip-color);
+}
+
+.rst-content .admonition.tip .admonition-title,
+.rst-content .admonition.important .admonition-title {
+ background-color: var(--admonition-tip-title-background-color);
+ color: var(--admonition-tip-title-color);
+}
+
+/* Keyboard shortcuts tweaks */
+kbd, .kbd {
+ background-color: var(--kbd-background-color);
+ border: 1px solid var(--kbd-outline-color);
+ border-radius: 3px;
+ box-shadow: inset 0 -1px 0 var(--kbd-shadow-color);
+ color: var(--kbd-text-color);
+ display: inline-block;
+ font-size: 12px;
+ line-height: 11px;
+ padding: 4px 5px;
+ vertical-align: middle;
+}
+
+/* Buttons */
+
+.btn-neutral {
+ background-color: var(--btn-neutral-background-color) !important;
+ color: var(--body-color) !important;
+}
+
+.btn-neutral:hover {
+ background-color: var(--btn-neutral-hover-background-color) !important;
+}
+
+.btn-neutral:visited {
+ color: var(--body-color) !important;
+}
+
+/* Navigation bar logo and search */
+
+.logo {
+ opacity: var(--logo-opacity);
+}
+
+.wy-side-nav-search {
+ background-color: var(--navbar-background-color);
+}
+
+.wy-side-nav-search.fixed {
+ position: fixed;
+}
+
+@media only screen and (min-width: 769px) {
+ /* Simulate a drop shadow that only affects the bottom edge */
+ /* This is used to indicate the search bar is fixed */
+ .wy-side-nav-search.fixed-and-scrolled::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ bottom: -8px;
+ width: 300px;
+ height: 8px;
+ pointer-events: none;
+ background: linear-gradient(hsla(0, 0%, 0%, 0.2), transparent);
+ }
+}
+
+.wy-side-nav-search > a:hover,
+.wy-side-nav-search .wy-dropdown > a:hover {
+ background-color: var(--navbar-background-color-hover);
+}
+
+.wy-side-nav-search > a:active,
+.wy-side-nav-search .wy-dropdown > a:active {
+ background-color: var(--navbar-background-color-active);
+}
+
+.wy-side-nav-search input[type="text"] {
+ background-color: var(--input-background-color);
+ color: var(--body-color);
+ /* Avoid reflowing when toggling the focus state */
+ border: 2px solid transparent;
+ box-shadow: none;
+ /* Make visual feedback instant */
+ transition: none;
+ font-size: 14px;
+}
+
+.wy-side-nav-search input[type="text"]:focus {
+ border: 2px solid var(--input-focus-border-color);
+}
+
+.wy-side-nav-search input[type="text"]::placeholder {
+ color: var(--body-color);
+ opacity: 0.55;
+}
+
+/* Navigation bar */
+
+.wy-nav-side {
+ background-color: var(--navbar-background-color);
+}
+
+.wy-menu-vertical header,
+.wy-menu-vertical p.caption {
+ color: var(--navbar-heading-color);
+
+ /* Improves the appearance of uppercase text */
+ letter-spacing: 0.75px;
+}
+
+/* Mobile navigation */
+
+.wy-nav-top,
+.wy-nav-top a {
+ background-color: var(--navbar-background-color);
+ color: var(--navbar-level-1-color);
+}
+
+/* Version branch label below the logo */
+.wy-side-nav-search > div.version {
+ color: var(--navbar-level-3-color);
+ opacity: 0.9;
+}
+
+/* First level of navigation items */
+
+.wy-menu-vertical a {
+ color: var(--navbar-level-1-color);
+}
+
+.wy-menu-vertical a:hover {
+ background-color: var(--navbar-background-color-hover);
+ color: var(--navbar-level-1-color);
+}
+
+.wy-menu-vertical a:active {
+ background-color: var(--navbar-background-color-active);
+}
+
+.wy-menu-vertical li.toctree-l1.current > a {
+ border: none;
+}
+
+.wy-side-nav-search, .wy-menu-vertical a, .wy-menu-vertical a span.toctree-expand,
+.wy-menu-vertical li.toctree-l2 a span.toctree-expand {
+ color: var(--navbar-level-3-color);
+ opacity: 0.9;
+ margin-right: 8px;
+}
+
+.wy-side-nav-search, .wy-menu-vertical a, .wy-menu-vertical a:hover span.toctree-expand,
+.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand {
+ color: var(--navbar-level-2-color);
+ opacity: 1;
+}
+
+.wy-side-nav-search, .wy-menu-vertical a, .wy-menu-vertical a:active span.toctree-expand,
+.wy-menu-vertical li.toctree-l2 a:active span.toctree-expand {
+ color: var(--navbar-level-1-color);
+ opacity: 1;
+}
+
+/* Second (and higher) levels of navigation items */
+
+.wy-menu-vertical li.current a {
+ /* Make long words always display on a single line, keep wrapping for multiple words */
+ /* This fixes the class reference titles' display with very long class names */
+ display: flex;
+}
+
+.wy-menu-vertical li.current a,
+.wy-menu-vertical li.toctree-l2.current > a,
+.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a,
+.wy-menu-vertical li.toctree-l2.current li.toctree-l4 > a {
+ background-color: var(--navbar-current-background-color);
+ color: var(--navbar-level-2-color);
+ border-color: var(--navbar-current-background-color);
+}
+
+.wy-menu-vertical li.current a:hover,
+.wy-menu-vertical li.toctree-l2.current > a:hover,
+.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a:hover,
+.wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a:hover {
+ background-color: var(--navbar-current-background-color-hover);
+}
+
+.wy-menu-vertical li.current a:active,
+.wy-menu-vertical li.toctree-l2.current > a:active,
+.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a:active,
+.wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a:active {
+ background-color: var(--navbar-current-background-color-active);
+}
+
+.wy-menu-vertical a {
+ /* This overrides 8px margin added in other multi-selector rules */
+ margin-right: 0;
+}
+
+/* Banner panel in sidebar */
+.wy-nav-side .ethical-rtd.fixed {
+ position: fixed;
+}
+
+/* Version selector (only visible on Read the Docs) */
+
+.rst-versions {
+ background-color: var(--navbar-current-background-color);
+}
+
+.rst-versions a,
+.rst-versions .rst-current-version,
+.rst-versions .rst-current-version .fa,
+.rst-versions .rst-other-versions dd a {
+ color: var(--navbar-level-1-color);
+}
+
+.rst-versions .rst-other-versions small {
+ color: var(--navbar-level-3-color);
+}
+
+.rst-versions .rst-other-versions dd a:hover {
+ text-decoration: underline;
+}
+
+.rst-versions .rst-other-versions {
+ color: var(--navbar-heading-color);
+}
+
+.rst-versions .rst-current-version {
+ background-color: var(--navbar-current-background-color);
+}
+
+.rst-versions .rst-current-version:hover {
+ background-color: var(--navbar-current-background-color-hover);
+}
+
+.rst-versions .rst-current-version:active {
+ background-color: var(--navbar-current-background-color-active);
+}
+
+.rst-versions.shift-up {
+ overflow-y: auto;
+}
+
+/* Hide the obnoxious automatic highlight in search results */
+.rst-content .highlighted {
+ background-color: transparent;
+ font-weight: inherit;
+ padding: 0;
+}
+
+/* Allows the scrollbar to be shown in the sidebar */
+@media only screen and (min-width: 769px) {
+ .wy-side-scroll {
+ overflow: hidden;
+ }
+
+ .wy-nav-side .wy-side-scroll .ethical-rtd {
+ width: calc(300px - 1.25em);
+ padding: 0 0 0 1em;
+ }
+}
+.wy-menu.wy-menu-vertical {
+ overflow-y: auto;
+ overflow-x: hidden;
+ max-height: calc(100% - 348px);
+}
+@media screen and (max-width: 768px) {
+ .wy-nav-side {
+ padding-bottom: 44px;
+ }
+ .wy-menu.wy-menu-vertical {
+ overflow-y: initial;
+ max-height: initial;
+ }
+}
+
+/* Scrollbar styling */
+.wy-menu.wy-menu-vertical {
+ scrollbar-color: var(--navbar-scrollbar-color) var(--navbar-scrollbar-background);
+}
+.wy-menu.wy-menu-vertical::-webkit-scrollbar {
+ width: .75rem;
+}
+.wy-menu.wy-menu-vertical::-webkit-scrollbar-track {
+ background-color: var(--navbar-scrollbar-background);
+}
+.wy-menu.wy-menu-vertical::-webkit-scrollbar-thumb {
+ background-color: var(--navbar-scrollbar-color);
+}
+/* Firefox does the dimming on hover automatically. We emulate it for Webkit-based browsers. */
+.wy-menu.wy-menu-vertical::-webkit-scrollbar-thumb:hover {
+ background-color: var(--navbar-scrollbar-hover-color);
+}
+.wy-menu.wy-menu-vertical::-webkit-scrollbar-thumb:active {
+ background-color: var(--navbar-scrollbar-active-color);
+}
+
+/* Breathe tweaks */
+
+.rst-content dl.group>dt, .rst-content dl.group>dd>p {
+ display:none !important;
+}
+
+.rst-content dl.group {
+ margin: 0 0 1rem 0;
+}
+
+.rst-content dl.group>dd {
+ margin-left: 0 !important;
+}
+
+.rst-content p.breathe-sectiondef-title {
+ font-size: 115%;
+ color: var(--link-color);
+}
+
+.rst-content div.breathe-sectiondef {
+ padding-left: 0 !important;
+}
+
+.rst-content dl:not(.docutils) dl dt, dl:not(.docutils,.rst-other-versions) dt {
+ background: var(--admonition-note-background-color) !important;
+ border-top: none !important;
+ border-left: none !important;
+}
+
+/* Misc tweaks */
+
+.rst-columns {
+ column-width: 18em;
+}
+
+.figure {
+ text-align: center;
+}
+
+.eqno {
+ float: right;
+}
+
+/* Topics */
+.rst-content .topic {
+ background-color: var(--admonition-note-background-color);
+ color: var(--admonition-note-color);
+ padding: 12px;
+ line-height: 24px;
+ margin-bottom: 24px;
+}
+
+.rst-content .topic-title {
+ background-color: var(--admonition-note-title-background-color);
+ color: var(--admonition-note-title-color);
+ padding: 3px 6px;
+ margin: -12px -12px 12px;
+}
diff --git a/docs/_static/css/light.css b/docs/_static/css/light.css
new file mode 100644
index 0000000..28b452b
--- /dev/null
+++ b/docs/_static/css/light.css
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2019-2020, Juan Linietsky, Ariel Manzur and the Godot community
+ * Copyright (c) 2021, Teslabs Engineering S.L.
+ * SPDX-License-Identifier: CC-BY-3.0
+ *
+ * Light theme colors
+ */
+
+:root {
+ --body-color: #404040;
+ --content-wrap-background-color: #efefef;
+ --content-background-color: #fcfcfc;
+ --logo-opacity: 1.0;
+ --navbar-background-color: #333f67;
+ --navbar-background-color-hover: #29355c;
+ --navbar-background-color-active: #212d51;
+ --navbar-current-background-color: #212d51;
+ --navbar-current-background-color-hover: #182343;
+ --navbar-current-background-color-active: #131e3b;
+ --navbar-level-1-color: #c3e3ff;
+ --navbar-level-2-color: #b8d6f0;
+ --navbar-level-3-color: #a3c4e1;
+ --navbar-heading-color: #90caf9;
+ --navbar-scrollbar-color: #2196f3;
+ --navbar-scrollbar-hover-color: #1976d2;
+ --navbar-scrollbar-active-color: #115494;
+ --navbar-scrollbar-background: #131e2b;
+
+ --link-color: #2980b9;
+ --link-color-hover: #3091d1;
+ --link-color-active: #105078;
+ --link-color-visited: #9b59b6;
+ --external-reference-icon: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMjk4MGI5Ij48cGF0aCBkPSJtNy41IDcuMXYzLjRoLTZ2LTZoMy40Ii8+PHBhdGggZD0ibTUuNzY1IDFoNS4yMzV2NS4zOWwtMS41NzMgMS41NDctMS4zMS0xLjMxLTIuNzI0IDIuNzIzLTIuNjktMi42ODggMi44MS0yLjgwOC0xLjMxMy0xLjMxeiIvPjwvZz48L3N2Zz4K");
+ --classref-badge-text-color: hsl(0, 0%, 45%);
+
+ --hr-color: #e1e4e5;
+ --table-row-odd-background-color: #f3f6f6;
+ --code-background-color: #fff;
+ --code-border-color: #e1e4e5;
+ --code-literal-color: #d04c60;
+ --input-background-color: #fcfcfc;
+ --input-focus-border-color: #5f8cff;
+
+ --search-input-background-color: #e6eef3; /* derived from --input-background-color */
+ --search-match-color: #2c6b96; /* derived from --link-color */
+ --search-match-background-color: #e3f2fd; /* derived from --link-color */
+ --search-active-color: #efefef;
+ --search-credits-background-color: #333f67; /* derived from --navbar-background-color */
+ --search-credits-color: #b3b3b3; /* derived from --footer-color */
+ --search-credits-link-color: #4392c5; /* derived from --link-color */
+
+ --highlight-background-color: #f0f2f4;
+ --highlight-background-emph-color: #dbe6c3;
+ --highlight-default-color: #404040;
+ --highlight-comment-color: #408090;
+ --highlight-keyword-color: #007020;
+ --highlight-keyword2-color: #902000;
+ --highlight-number-color: #208050;
+ --highlight-decorator-color: #4070a0;
+ --highlight-type-color: #007020;
+ --highlight-type2-color: #0e84b5;
+ --highlight-function-color: #06287e;
+ --highlight-operator-color: #666666;
+ --highlight-string-color: #4070a0;
+
+ --admonition-note-background-color: #e7f2fa;
+ --admonition-note-color: #404040;
+ --admonition-note-title-background-color: #6ab0de;
+ --admonition-note-title-color: #fff;
+ --admonition-attention-background-color: #ffedcc;
+ --admonition-attention-color: #404040;
+ --admonition-attention-title-background-color: #f0b37e;
+ --admonition-attention-title-color: #fff;
+ --admonition-danger-background-color: #fcf3f2;
+ --admonition-danger-color: #404040;
+ --admonition-danger-title-background-color: #e9a499;
+ --admonition-danger-title-color: #fff;
+ --admonition-tip-background-color: #dbfaf4;
+ --admonition-tip-color: #404040;
+ --admonition-tip-title-background-color: #1abc9c;
+ --admonition-tip-title-color: #fff;
+
+ --kbd-background-color: #fafbfc;
+ --kbd-outline-color: #d1d5da;
+ --kbd-shadow-color: #b0b7bf;
+ --kbd-text-color: #444d56;
+
+ --btn-neutral-background-color: #f3f6f6;
+ --btn-neutral-hover-background-color: #e5ebeb;
+ --footer-color: #808080;
+}
diff --git a/docs/_static/images/logo.png b/docs/_static/images/logo.png
new file mode 100644
index 0000000..5176230
Binary files /dev/null and b/docs/_static/images/logo.png differ
diff --git a/docs/_static/images/logo.svg b/docs/_static/images/logo.svg
new file mode 100644
index 0000000..d1c8d71
--- /dev/null
+++ b/docs/_static/images/logo.svg
@@ -0,0 +1,213 @@
+
+image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/bibliography.bib b/docs/bibliography.bib
new file mode 100755
index 0000000..fddd7a3
--- /dev/null
+++ b/docs/bibliography.bib
@@ -0,0 +1,17 @@
+@manual{rm0365,
+ title = {RM0365 STM32F302xB/C/D/E and STM32F302x6/8 advanced ARM®-based 32-bit MCUs},
+ author = {STMicroelectronics},
+ edition = {8th},
+ year = {2020},
+ url = {https://www.st.com/resource/en/reference_manual/dm00094349-stm32f302xb-c-d-e-and-stm32f302x6-8-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf},
+ note = {[Online; accessed 4-May-2021]}
+}
+
+@manual{an4013,
+ title = {AN4013 STM32 cross-series timer overview},
+ author = {STMicroelectronics},
+ edition = {8th},
+ year = {2019},
+ url = {https://www.st.com/resource/en/application_note/dm00042534-stm32-crossseries-timer-overview-stmicroelectronics.pdf},
+ note = {[Online; accessed 4-May-2021]}
+}
diff --git a/docs/components/cloop/images/cloop-only-schematic.odg b/docs/components/cloop/images/cloop-only-schematic.odg
new file mode 100644
index 0000000..9e8e50e
Binary files /dev/null and b/docs/components/cloop/images/cloop-only-schematic.odg differ
diff --git a/docs/components/cloop/images/cloop-only-schematic.svg b/docs/components/cloop/images/cloop-only-schematic.svg
new file mode 100644
index 0000000..4ea3d9e
--- /dev/null
+++ b/docs/components/cloop/images/cloop-only-schematic.svg
@@ -0,0 +1,844 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Current Sensing
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Feedback
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SV-PWM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SV-PWM
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ M
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Clarke Transform
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Park Transform
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PI
+
+
+
+
+
+
+
+ Inverse Park Transform
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Current Sensing
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/components/cloop/index.rst b/docs/components/cloop/index.rst
new file mode 100644
index 0000000..e4ed9c1
--- /dev/null
+++ b/docs/components/cloop/index.rst
@@ -0,0 +1,21 @@
+Current Loop
+============
+
+The current control loop is the component responsible of controlling the
+inverter input by providing the control signals in the :math:`\alpha,\beta`
+space. These are used by the SV-PWM driver to generate the PWM signals as well
+as adjust the current sampling time. The current loop makes use of the estimated phase
+currents, :math:`\hat{i}_a,~\hat{i}_b`, and the estimated electrical angle,
+:math:`\hat{\theta}` to apply the principles of Field Oriented Control (FOC).
+:numref:`cloop-schematic` shows a block diagram that illustrates how all the
+components are connected.
+
+.. _cloop-schematic:
+.. figure:: images/cloop-only-schematic.svg
+
+ Current loop (highlighted blocks)
+
+API
+---
+
+.. doxygengroup:: spinner_lib_control_cloop
diff --git a/docs/components/currsmp/images/intro-inverter-schematic-blocks.odg b/docs/components/currsmp/images/intro-inverter-schematic-blocks.odg
new file mode 100644
index 0000000..8a2b5a6
Binary files /dev/null and b/docs/components/currsmp/images/intro-inverter-schematic-blocks.odg differ
diff --git a/docs/components/currsmp/images/intro-inverter-schematic-blocks.png b/docs/components/currsmp/images/intro-inverter-schematic-blocks.png
new file mode 100644
index 0000000..a374908
Binary files /dev/null and b/docs/components/currsmp/images/intro-inverter-schematic-blocks.png differ
diff --git a/docs/components/currsmp/images/intro-inverter-schematic.odg b/docs/components/currsmp/images/intro-inverter-schematic.odg
new file mode 100644
index 0000000..db79eaa
Binary files /dev/null and b/docs/components/currsmp/images/intro-inverter-schematic.odg differ
diff --git a/docs/components/currsmp/images/intro-inverter-schematic.png b/docs/components/currsmp/images/intro-inverter-schematic.png
new file mode 100644
index 0000000..85b6ee1
Binary files /dev/null and b/docs/components/currsmp/images/intro-inverter-schematic.png differ
diff --git a/docs/components/currsmp/impl/stm32.rst b/docs/components/currsmp/impl/stm32.rst
new file mode 100755
index 0000000..6f891b5
--- /dev/null
+++ b/docs/components/currsmp/impl/stm32.rst
@@ -0,0 +1,6 @@
+STM32
+=====
+
+.. note::
+
+ This page will be available soon.
diff --git a/docs/components/currsmp/index.rst b/docs/components/currsmp/index.rst
new file mode 100644
index 0000000..768487f
--- /dev/null
+++ b/docs/components/currsmp/index.rst
@@ -0,0 +1,18 @@
+Current Sampling
+================
+
+Introduction
+------------
+
+API
+---
+
+.. doxygengroup:: spinner_drivers_currsmp
+
+Implementations
+---------------
+
+.. toctree::
+ :glob:
+
+ impl/*
diff --git a/docs/components/feedback/images/halls.odg b/docs/components/feedback/images/halls.odg
new file mode 100755
index 0000000..93db86f
Binary files /dev/null and b/docs/components/feedback/images/halls.odg differ
diff --git a/docs/components/feedback/images/halls.svg b/docs/components/feedback/images/halls.svg
new file mode 100644
index 0000000..7eaa130
--- /dev/null
+++ b/docs/components/feedback/images/halls.svg
@@ -0,0 +1,1345 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ H1
+
+
+
+
+
+ H2
+
+
+
+
+
+ H3
+
+
+
+
+
+ 120 º
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 270 º
+
+
+
+
+
+ 330 º
+
+
+
+
+
+ 30º
+
+
+
+
+
+ H1
+
+
+
+
+
+ H2
+
+
+
+
+
+ H3
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 5
+
+
+
+
+
+ 1
+
+
+
+
+
+ 3
+
+
+
+
+
+ 2
+
+
+
+
+
+ 6
+
+
+
+
+
+ 4
+
+
+
+
+
+ 90 º
+
+
+
+
+
+ 150 º
+
+
+
+
+
+ 210 º
+
+
+
+
+
+ 270 º
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/components/feedback/impl/stm32-halls.rst b/docs/components/feedback/impl/stm32-halls.rst
new file mode 100755
index 0000000..6cf579d
--- /dev/null
+++ b/docs/components/feedback/impl/stm32-halls.rst
@@ -0,0 +1,6 @@
+STM32 Halls
+===========
+
+.. note::
+
+ This page will be available soon.
diff --git a/docs/components/feedback/index.rst b/docs/components/feedback/index.rst
new file mode 100644
index 0000000..5803f54
--- /dev/null
+++ b/docs/components/feedback/index.rst
@@ -0,0 +1,48 @@
+Feedback
+========
+
+Introduction
+------------
+
+Feedback drivers are responsible of providing information of the rotor position
+or speed. One of the core principles of FOC is the knowledge of the rotor
+position, therefore, it is one of the core devices of the system. To be precise,
+FOC depends on the knowledge of the electrical angle of the motor, which is
+directly related to the rotor position via the number of pair poles.
+
+There is a wide variety of feedback sensors. Their choice depends on the end
+application or required control. For example, Halls may be a good choice for
+speed control as detailed above. However, for accurate position control digital
+encoders may be a better candidate.
+
+Halls
+*****
+
+Hall sensors are a common type of feedback based on the `Hall effect`_. The
+sensor is actually composed by three individual hall sensors equally distributed
+in the distance of one electrical revolution. The combination of the three
+output signals using the XOR function results in a square wave that provides an
+electrical angle resolution of 60 degress (:numref:`feedback-halls`). Because of
+their low resolution Hall sensors are frequently used for speed control. An
+important characteristic of Hall sensors is that their position feedback is
+absolute.
+
+.. _feedback-halls:
+.. figure:: images/halls.svg
+
+ Hall sensors signals versus the motor electrical angle.
+
+.. _Hall effect: https://en.wikipedia.org/wiki/Hall_effect_sensor
+
+API
+---
+
+.. doxygengroup:: spinner_drivers_feedback
+
+Implementations
+---------------
+
+.. toctree::
+ :glob:
+
+ impl/*
diff --git a/docs/components/svpwm/images/ps-schematic.odg b/docs/components/svpwm/images/ps-schematic.odg
new file mode 100644
index 0000000..b5adb39
Binary files /dev/null and b/docs/components/svpwm/images/ps-schematic.odg differ
diff --git a/docs/components/svpwm/images/ps-schematic.svg b/docs/components/svpwm/images/ps-schematic.svg
new file mode 100644
index 0000000..f159db2
--- /dev/null
+++ b/docs/components/svpwm/images/ps-schematic.svg
@@ -0,0 +1,483 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ M
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/components/svpwm/impl/images/stm32-cc-output.png b/docs/components/svpwm/impl/images/stm32-cc-output.png
new file mode 100644
index 0000000..510f262
Binary files /dev/null and b/docs/components/svpwm/impl/images/stm32-cc-output.png differ
diff --git a/docs/components/svpwm/impl/images/stm32-timer-brk.png b/docs/components/svpwm/impl/images/stm32-timer-brk.png
new file mode 100755
index 0000000..af93c6d
Binary files /dev/null and b/docs/components/svpwm/impl/images/stm32-timer-brk.png differ
diff --git a/docs/components/svpwm/impl/images/stm32-timer-brkconf.png b/docs/components/svpwm/impl/images/stm32-timer-brkconf.png
new file mode 100755
index 0000000..34ce222
Binary files /dev/null and b/docs/components/svpwm/impl/images/stm32-timer-brkconf.png differ
diff --git a/docs/components/svpwm/impl/images/stm32-timer-centeraligned.odg b/docs/components/svpwm/impl/images/stm32-timer-centeraligned.odg
new file mode 100755
index 0000000..13fd28b
Binary files /dev/null and b/docs/components/svpwm/impl/images/stm32-timer-centeraligned.odg differ
diff --git a/docs/components/svpwm/impl/images/stm32-timer-centeraligned.svg b/docs/components/svpwm/impl/images/stm32-timer-centeraligned.svg
new file mode 100644
index 0000000..1b41a22
--- /dev/null
+++ b/docs/components/svpwm/impl/images/stm32-timer-centeraligned.svg
@@ -0,0 +1,1475 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ T PWM
+
+
+
+
+
+ ARR
+
+
+
+
+
+ t
+
+
+
+
+
+ CCR1
+
+
+
+
+
+ CCR2
+
+
+
+
+
+ PWM1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PWM2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/components/svpwm/impl/images/stm32-timer-diagram.png b/docs/components/svpwm/impl/images/stm32-timer-diagram.png
new file mode 100755
index 0000000..944313c
Binary files /dev/null and b/docs/components/svpwm/impl/images/stm32-timer-diagram.png differ
diff --git a/docs/components/svpwm/impl/images/stm32-timer-repcnt.png b/docs/components/svpwm/impl/images/stm32-timer-repcnt.png
new file mode 100755
index 0000000..38c7b5a
Binary files /dev/null and b/docs/components/svpwm/impl/images/stm32-timer-repcnt.png differ
diff --git a/docs/components/svpwm/impl/stm32.rst b/docs/components/svpwm/impl/stm32.rst
new file mode 100755
index 0000000..773e6fd
--- /dev/null
+++ b/docs/components/svpwm/impl/stm32.rst
@@ -0,0 +1,157 @@
+STM32
+=====
+
+Introduction
+------------
+
+The timer peripheral is the core part of the SV-PWM driver. It generates the PWM
+signals that drive the inverter circuit and it also takes care of synchronizing
+ADC measurements made by the current sampling driver.
+
+The driver is designed to work using one of the *advanced control timers*,
+usually ``TIM1`` and ``TIM8``. They include specific functionalities that are
+crucial to have a performant and safe system :cite:`an4013`.
+:numref:`stm32-timer-diagram` shows the diagram of an advanced control timer.
+
+.. _stm32-timer-diagram:
+.. figure:: images/stm32-timer-diagram.png
+
+ Advanced control timer diagram :cite:`rm0365`.
+
+PWM generation
+--------------
+
+The timer peripheral clock, :math:`\mathrm{f_{TIM}}`, is as fundamental variable
+as it controls the timer counting rate. The timer clock is divided by the
+prescaler, which is controlled by the ``PSC`` register (16-bit). The counting
+rate, :math:`\mathrm{f_{CNT}}`, is defined as:
+
+.. math::
+
+ \mathrm{f_{CNT} = \frac{f_{TIM}}{PSC + 1}}
+
+STM32 timers have multiple PWM modes. The most interesting mode when doing motor
+control is the **center-aligned PWM mode**
+(:numref:`stm32-timer-centeraligned`). In this mode the counting direction
+(up/down) is automatically alternated by the timer. This mode provides an
+interesting feature when multiple PWM waveforms are generated such as in a
+3-phase inverter. Contrary to edge-aligned modes, in this mode the rising and
+falling edges of the PWM signals are not synchronized with the counter
+roll-over. Therefore, switching time varies with the duty cycle value and
+switching noise is spread. This is a key feature for electric motor drives,
+since it allows to double the frequency of the current ripple for a given
+switching frequency. For instance, a 10 kHz PWM will generate inaudible 20 kHz
+current ripple. This feature also minimizes the switching losses due to the PWM
+frequency while guaranteeing a silent operation.
+
+.. _stm32-timer-centeraligned:
+.. figure:: images/stm32-timer-centeraligned.svg
+
+ Timing diagram for a timer in center-aligned PWM mode.
+
+Using the above diagram, we can see that the PWM frequency
+(:math:`\mathrm{f_{PWM}}`) is given by:
+
+.. math::
+
+ \mathrm{f_{PWM} = \frac{f_{TIM}}{2 \cdot (ARR + 1) \cdot (PSC + 1)}}
+
+where ``ARR`` is the auto-reload register, a 16-bit value. In order to maximize
+PWM resolution ``PSC`` should be chosen so that ``ARR`` is maximized while
+fitting into its 16-bit register.
+
+.. topic:: ARR calculation example
+
+ Given :math:`\mathrm{f_{TIM} = 72~MHz}` and :math:`\mathrm{f_{PWM} = 30~KHz}`,
+ we start with :math:`\mathrm{PSC = 0}`, which leads to:
+
+ .. math::
+
+ \mathrm{ARR = \frac{72~MHz}{2 \cdot 30~KHz \cdot (0 + 1)} - 1 = 1199}.
+
+ As 1199 fits into a 16-bit register, we stick to :math:`\mathrm{PSC = 0}`.
+
+The PWM duty cycle is controlled via the ``CCRx`` registers (``x = 1, 2, ...``).
+``CCRx`` value is compared against ``CNT`` so that the PWM signal is active when
+``CNT < CCRx``. In case ``CCRx`` is set to zero, the PWM signal is always kept
+inactive.
+
+There is an important feature that has to be enabled for ``CCRx`` registers:
+pre-load. When pre-load is enabled, the register value is only updated when the
+timer update event occurs. This is particular useful for real-time control, as
+the new register values are applied synchronously. However in center aligned
+mode, we have two update events: overflow (at the end of up cycle) and underflow
+(at the end of down cycle). Update event happening on overflow should be avoided
+since it is the time when current sampling is likely going to occur, and so the
+regulation loop. Repetition counter feature comes to the rescue to solve this
+problem. In center-aligned mode, odd values of the repetition counter generate
+the update event either on overfow or underflow depending on when the repetition
+counter register ``RCR`` was written and the counter launched. If ``RCR`` was
+written before starting the counter, the update event will occur on underflow
+and on overflow if ``RCR`` was written after starting the counter.
+
+.. figure:: images/stm32-timer-repcnt.png
+
+ Example of repetition counter update event generation :cite:`rm0365`.
+
+Up to now all the details on the signal generation have been given. The only
+missing part is now how to expose these signals to the outside via the ``OCx``
+pins. This is controlled by the output stage of the capture/compare channel as
+seen on :numref:`stm32-cc-output`. In general it is necessary to control both
+high and low sides of each inverter leg. For this purpose complementary outputs
+can be enabled (``OCxN``). As described in the following section, it is also
+possible to insert dead-time when using complementary outputs. Some integrated
+drivers do not require complementary signals since they internally take care of
+their generation including dead-time insertion.
+
+.. _stm32-cc-output:
+.. figure :: images/stm32-cc-output.png
+
+ Output stage of capture/compare channel :cite:`rm0365`.
+
+ADC synchronization
+-------------------
+
+As detailed in the :doc:`/theory/currsmp` page, it is crucial to synchronize the
+current measurements with the PWM generation. For this purpose, the driver uses
+its fourth channel compare unit (``OC4``) to trigger the ADC. The value of the
+``CCR4`` register controlling the signal duty cycle is updated every time phase
+voltages are set so that currents are always sampled at an optimal point. The
+``OC4`` output is connected to the ``TRGO`` output signal. The ADC device
+managed by the current sampling driver is responsible to connect to this signal
+as a trigger source.
+
+Break function
+--------------
+
+The break function is used to protect the power stage driven by the PWM signals.
+There are two break inputs which are usually connected to fault signals
+generated by the power stage circuit (e.g. over-current). When any of the input
+is activated a hardware protection mechanism is triggered so that the PWM
+outputs are disabled, leaving them in a pre-programmed state. The break
+circuitry works asynchronously, that is, it does not depend on any system clock.
+This feature makes sure that the circuitry does not suffer from any clock
+propagation delay or system clock failures.
+
+.. _stm32-timer-brk:
+.. figure:: images/stm32-timer-brk.png
+
+ Break circuitry :cite:`rm0365`.
+
+As shown in :numref:`stm32-timer-brk` ``BRK`` is the result of either an
+external event (``BKIN``) or an internal event (``BRK_ACTH``) such as a clock
+failure event (refer to :cite:`rm0365` for more details). The first channel,
+``BRK``, has priority over ``BRK2``. ``BRK`` can also be configured to either
+disable (inactive) or force PWM outputs to a predefined safe state. Furthermore,
+a dead-time can be programmed to avoid potential shoot-through when activating
+the break functionality. This provides a dual-level protection scheme, where for
+instance a low priority protection with all switches off can be overridden by a
+higher priority protection with low-side switches active. Let’s consider for
+instance that the fault occurs when the high-side PWM is ON, while the safe
+state is programmed to have high-side switched OFF and low-side switched ON. At
+the time the fault occurs the system will first disable the high-side PWM, and
+insert a dead time before switching ON the low side.
+
+.. figure:: images/stm32-timer-brkconf.png
+
+ Typical break use case :cite:`rm0365`.
diff --git a/docs/components/svpwm/index.rst b/docs/components/svpwm/index.rst
new file mode 100644
index 0000000..5dca0e8
--- /dev/null
+++ b/docs/components/svpwm/index.rst
@@ -0,0 +1,40 @@
+SV-PWM
+======
+
+Introduction
+------------
+
+The SV-PWM driver is the responsible to control the power stage, that is, the
+circuit that powers the motor. The power stage is formed by three half-bridge
+circuits, one for each motor phase. Each half-bridge is composed by two
+*switches*: :math:`\mathrm{q_i}` and :math:`\mathrm{\bar{q_i}}`, where
+:math:`\mathrm{i} \in \mathrm{(a, b, c)}` (:numref:`ps-schematic`). These
+switches are implemented using transistors (e.g. MOSFET, GaN...).
+
+.. _ps-schematic:
+.. figure:: images/ps-schematic.svg
+
+ Schematic of the power stage *switches*
+
+As the notation suggests, the switches are by complementary PWM signals,
+sometimes with the insertion of dead-time. The modulation scheme implemented by
+the driver is known as *Space Vector PWM*, hence its name. The theoretical
+details of this modulation can be found at the :doc:`/theory/svpwm` page. When
+using shunt resistors, current sampling needs to be synchronized with the PWM
+generation. Because of this reason, the SV-PWM driver also takes care of current
+sampling synchronization. The theoretical details can be found at the
+:doc:`/theory/currsmp` page.
+
+API
+---
+
+.. doxygengroup:: spinner_drivers_svpwm
+
+
+Implementations
+---------------
+
+.. toctree::
+ :glob:
+
+ impl/*
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100755
index 0000000..c7b5542
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,60 @@
+from datetime import datetime
+from pathlib import Path
+import os
+import re
+
+SPINNER_BASE = os.environ["SPINNER_BASE"]
+DOXYGEN_OUTPUT_DIRECTORY = os.environ["DOXYGEN_OUTPUT_DIRECTORY"]
+
+# Project ----------------------------------------------------------------------
+
+project = "Spinner"
+author = "Teslabs Engineering S.L."
+year = 2021
+copyright = f"{year}, {author}"
+
+cmake = Path(SPINNER_BASE) / "spinner" / "CMakeLists.txt"
+with open(cmake) as f:
+ version = re.search(r"project\(.*VERSION\s+([0-9\.]+).*\)", f.read()).group(1)
+
+# Options ----------------------------------------------------------------------
+
+extensions = [
+ "sphinx.ext.intersphinx",
+ "breathe",
+ "sphinx.ext.mathjax",
+ "sphinxcontrib.bibtex",
+ "matplotlib.sphinxext.plot_directive",
+]
+
+numfig = True
+
+# HTML output ------------------------------------------------------------------
+
+html_theme = "sphinx_rtd_theme"
+html_theme_options = {
+ "logo_only": True,
+ "collapse_navigation": False,
+}
+html_static_path = ["_static"]
+html_logo = "_static/images/logo.svg"
+
+# Options for intersphinx ------------------------------------------------------
+
+intersphinx_mapping = {"zephyr": ("https://docs.zephyrproject.org/latest", None)}
+
+# Options for breathe ----------------------------------------------------------
+
+breathe_projects = {"app": str(Path(DOXYGEN_OUTPUT_DIRECTORY) / "xml")}
+breathe_default_project = "app"
+breathe_domain_by_extension = {"h": "c", "c": "c"}
+breathe_default_members = ("members", )
+
+# Options for sphinxcontrib.bibtex ---------------------------------------------
+
+bibtex_bibfiles = ["bibliography.bib"]
+
+
+def setup(app):
+ app.add_css_file("css/custom.css")
+ app.add_css_file("css/light.css")
diff --git a/docs/doxygen.md b/docs/doxygen.md
new file mode 100644
index 0000000..a7c1d6d
--- /dev/null
+++ b/docs/doxygen.md
@@ -0,0 +1,20 @@
+# Spinner API Documentation {#index}
+
+Welcome to the Spinner's API documentation! Spinner is a motor control firmware
+based on the Field Oriented Control (FOC) principles. The firmware is built on
+top of the [Zephyr RTOS](https://zephyrproject.org), a modern multi-platform
+RTOS. Spinner is still a proof of concept, so do not expect production grade
+stability or features.
+
+Some of the offered features are:
+
+- FOC based current control loop
+- Driver APIs for:
+
+ - Current sampling
+ - SV-PWM
+ - Feedbacks (e.g. Halls)
+
+These pages contain documentation for all the APIs used in the Spinner
+firmware. For more details on the Spinner architecture, refer to the contextual
+documentation pages.
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100755
index 0000000..348b8ba
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,43 @@
+=====================
+Spinner Documentation
+=====================
+
+Welcome to the Spinner's documentation! Spinner is a motor control firmware
+based on the Field Oriented Control (FOC) principles. The firmware is built
+on top of the `Zephyr RTOS`_, a modern multi-platform RTOS. Spinner is still
+a proof of concept, so do not expect production grade stability or features.
+
+Some of the offered features are:
+
+- FOC based current control loop
+- Driver APIs for:
+
+ - Current sampling
+ - SV-PWM
+ - Feedbacks (e.g. Halls)
+
+These documentation pages contain architecture details of the Spinner
+firmware as well as some driver implementation details.
+
+.. _Zephyr RTOS: https://zephyrproject.org
+
+.. toctree::
+ :caption: Theory
+ :glob:
+ :hidden:
+
+ theory/*
+
+.. toctree::
+ :caption: Components
+ :glob:
+ :hidden:
+
+ components/**/index
+
+.. toctree::
+ :caption: Reference
+ :hidden:
+
+ API
+ zbibliography
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100755
index 0000000..e0ad4d0
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,7 @@
+Sphinx<4
+sphinx_rtd_theme
+breathe
+sphinxcontrib-mermaid
+sphinxcontrib-bibtex
+matplotlib
+numpy
diff --git a/docs/theory/currsmp.rst b/docs/theory/currsmp.rst
new file mode 100755
index 0000000..4927859
--- /dev/null
+++ b/docs/theory/currsmp.rst
@@ -0,0 +1,141 @@
+Current sampling
+================
+
+Knowledge of phase currents is at the core of Field Oriented Control as they are
+the controlled variables. Multiple methods can be used in order to measure motor
+phase currents, being one of the most populars the usage of shunt resistors. We
+will also see that current sampling is tightly related to the PWM control
+signals and hence the modulation scheme.
+
+Shunt resistors
+---------------
+
+It can be demonstrated that current flows through the shunt resistor when the
+low transistor is turned on. Therefore, current measurements need to be
+synchronized with the PWM switching times.
+
+Only two phase currents are required to know the third one, as in a balanced
+system all currents sum zero. If we measure :math:`i_a` and :math:`i_b`,
+:math:`i_c` is also known. However, it is not always possible to sample the same
+currents as we are limited by the time the low side is active. The detailed
+analysis will be limited to the first sector case, for other sectors the same
+procedure can be followed.
+
+SV-PWM
+------
+
+When using the SV-PWM modulation technique we have that the duty cycles take a
+particular shape that will condition the sampling of the currents. If we look at
+the first sector we have that duty cycles look like the animation shown in
+:numref:`currsample-svpwm-anim` (dead-time ignored for simplicity).
+
+.. _currsample-svpwm-anim:
+.. figure:: images/currsample-svpwm-anim.gif
+
+ Duty cycles for the first sector when using SV-PWM.
+
+Actually, other sectors are just a combination of what we have in
+:numref:`currsample-svpwm-anim`, only having direction changes in the linearly
+varying phase.
+
+.. plot:: theory/images/svpwm-modulation.py
+
+ SV-PWM duty cycles shape.
+
+Summarizing, we will always have the following situation:
+
+1. A phase with a **high duty cycle**, with its maximum at half of the period.
+2. A phase that varies **linearly** either **increasing or decreasing** over a
+ wide range of duty cycles.
+3. A phase with a **low duty cycle**, with its minimum at half of the period.
+
+We will use this information in the next section when designing the sampling
+strategy.
+
+Sampling strategy
+-----------------
+
+Because phase currents flow through the shunt resistor when the low-side is ON
+it is clear that we need to synchronize the measurements with the PWM signals.
+As we are on a balanced system, we can just measure two phase currents instead
+of all three. We also need to consider the modulation scheme (SV-PWM) in order
+to understand the limitations we have. As usual we will focus on analyzing the
+first SV-PWM sector and extrapolate the results to other sectors.
+:numref:`currsample-mid` provides a timing diagram for the first sector which
+will be useful for the analysis.
+
+.. _currsample-mid:
+.. figure:: images/currsample-mid.svg
+
+ Duty cycles and current shapes for sector 1
+
+.. table::
+
+ =========================== ============================================
+ Variable Description
+ =========================== ============================================
+ :math:`\mathrm{T_{RISE}}` Time taken by the current signal to rise and
+ stabilize to its nominal value after a
+ bottom transistor switch-on event.
+ :math:`\mathrm{T_{NOISE}}` Time during which electric noise is present
+ on a phase due to another phase bottom
+ transistor switch-on event.
+ :math:`\mathrm{T_{SAMPLE}}` Time taken to sample the currents.
+ :math:`\mathrm{DT}` Dead-Time is a small time added to the PWM
+ signals so that upper and bottom transistors
+ do not change state at the same time thus
+ avoiding shoot-throughs.
+ =========================== ============================================
+
+In order to derive a simple sampling strategy we will assume that sampling is
+always started at the middle of the PWM period. When sampling currents we always
+need to skip the measurement of the phase that can be potentially OFF in the
+active sector. In case of sector one, this happens for phase a, meaning we will
+need to sample phases b and c. :numref:`currsample-svpwm-anim` provides animated
+duty cycle waveforms that can help on understanding the given concepts. The same
+reasoning can be performed for the other sectors, leading to the results shown
+in :numref:`currsample-phases`.
+
+.. _currsample-phases:
+.. table:: Phases to be sampled on each SV-PWM sector.
+ :align: center
+
+ ====== ====================
+ Sector Phases to be sampled
+ ====== ====================
+ 1 b, c
+ 2 a, c
+ 3 a, c
+ 4 a, b
+ 5 a, b
+ 6 b, c
+ ====== ====================
+
+The only condition we must fulfill is that the time the low side is ON is big
+enough to allow sampling the currents. The lowest time the low side is ON is
+given by:
+
+.. math::
+
+ \mathrm{t_{min} = T_{PWM} \cdot (1 - d_{max}) - DT}.
+
+If we take SV-PWM equations we have that the maximum PWM duty cycle for the
+phases to be sampled is:
+
+.. math::
+
+ \mathrm{d_{max}} = \frac{1}{2} + \frac{\sqrt{3}}{4}.
+
+As we sample at the middle of the period, we need then:
+
+.. math::
+
+ \mathrm{T_s \leq \frac{t_{min}}{2}}
+
+which results in:
+
+.. math::
+
+ \mathrm{T_s \leq \frac{T_{PWM} \cdot \left( \frac{1}{2} - \frac{\sqrt{3}}{4} \right) - DT}{2}}.
+
+If this condition is not met, PWM frequency should be reduced.
diff --git a/docs/theory/foc.rst b/docs/theory/foc.rst
new file mode 100755
index 0000000..ccd8337
--- /dev/null
+++ b/docs/theory/foc.rst
@@ -0,0 +1,158 @@
+Field Oriented Control
+======================
+
+Field Oriented Control (FOC) consists on controlling the stator currents
+represented by a vector in a 2-D time-invariant space :math:`dq`. The :math:`dq`
+space is an orthogonal space aligned with the rotor: flux is aligned with
+:math:`d` and torque is aligned with :math:`q`. A set of projections is used to
+transform from a three-phase speed and time dependent system to :math:`dq`. As
+the transformations are just projections the controlled magnitudes are
+instantaneous quantities, making the control structure valid for transient and
+steady state.
+
+It can be shown that in the :math:`dq` space we have:
+
+.. math::
+
+ T \propto \psi_R i_q
+
+that is, by maintaining the rotor flux constant :math:`\psi_R` we have that the
+generated torque :math:`T` is directly proportional to the :math:`i_q` stator
+current. We can then perform torque control by changing the :math:`i_q` current
+reference. Because the speed and time dependency is removed from the :math:`dq`
+space, the control strategy is also simplified as constant references are being
+controlled.
+
+Space Vector
+------------
+
+We have that for three-phase AC motors, voltages, currents and fluxes can be
+analyzed in terms of complex space vectors. First we define the :math:`abc`
+space, given by the following three unit vectors in the complex space:
+
+.. math::
+
+ \hat{a} &= e^{j0} \\
+ \hat{b} &= e^{j \frac{2 \pi}{3}} \\
+ \hat{c} &= e^{j \frac{4 \pi}{3}}. \\
+
+Then, the space vector for currents (same applies to other magnitudes) is
+defined as:
+
+.. math::
+ \vec{i_s} = i_a \hat{a} + i_b \hat{b} + i_c \hat{c}
+ = \vec{i_a} + \vec{i_b} + \vec{i_c}.
+
+The definition above may sound abstract, but with some more context it can be
+better understood. Let us start by looking at the currents shape. Given a
+three-phase balanced AC system, we have that phase currents are sinusoidal in
+steady state, i.e.:
+
+.. math::
+
+ i_a(t) &= I \cos(\omega t + \phi_0) \\
+ i_b(t) &= I \cos(\omega t - \frac{2 \pi}{3} + \phi_0) \\
+ i_c(t) &= I \cos(\omega t - \frac{4 \pi}{3} + \phi_0) \\
+
+where :math:`I` is the current magnitude, :math:`\omega` is the rotation speed
+in rad/s and :math:`\phi_0` is an arbitrary initial phase. :math:`\omega t` is
+the instantaneous position, :math:`\theta`. Note that both rotation speed and
+instantaneous position are always in electrical terms. We can read from the
+equations that :math:`i_b` lags :math:`i_a` by :math:`\frac{2 \pi}{3}` rad, and
+:math:`i_c` lags :math:`i_b` by the same amount. We can also observe that the
+following equality holds as the system is balanced:
+
+.. math::
+
+ i_a(t) + i_b(t) + i_c(t) = 0.
+
+Using the above equations we can plot the space vector and its components as a
+function of the rotor position (:numref:`foc-abcs-anim`). The space vector can
+be seen as a CCW rotating vector with rotation speed :math:`\omega` in the
+complex space.
+
+.. _foc-abcs-anim:
+.. figure:: images/foc-abcs-anim.gif
+
+ Animated visualization of the space vector.
+
+Clarke transform
+----------------
+
+Any non-orthogonal space indicates a redundancy in its axes. This is the case of
+the :math:`abc` space, which can be reduced to the complex space. The complex
+space is usually referred in the motor control literature as the :math:`\alpha
+\beta` space. In order to derive the transform from the :math:`abc` space to
+the :math:`\alpha \beta` space, we can take the projection of the space vector
+components into the :math:`\alpha \beta` axes, that is:
+
+.. math::
+
+ i_{\alpha} &= \Re(\vec{i_a} + \vec{i_b} + \vec{i_c})
+ = i_a + i_b \cos \left(\frac{2 \pi}{3}\right) + i_c \cos\left(\frac{4 \pi}{3}\right)
+ = i_a - \frac{1}{2} (i_b + i_c), \\
+ i_{\beta} &= \Im(\vec{i_a} + \vec{i_b} + \vec{i_c})
+ = i_b + \sin\left(\frac{2 \pi}{3}\right) + i_c \sin\left(\frac{4 \pi}{3}\right)
+ = \frac{\sqrt{3}}{2} (i_b - i_c).
+
+Using the equality :math:`i_a + i_b + i_c = 0`, we can further simplify the
+expressions:
+
+.. math::
+
+ i_{\alpha} &= i_a \\
+ i_{\beta} &= \left( \frac{1}{\sqrt{3}} i_a + \frac{2}{\sqrt{3}} i_b \right).
+
+This transform is known as the **Clarke transform**, which in matrix form is:
+
+.. math::
+
+ (a, b) \rightarrow (\alpha, \beta): \mathbf{C} =
+ \begin{bmatrix}
+ 1 && 0 \\
+ \frac{1}{\sqrt{3}} && \frac{2}{\sqrt{3}}
+ \end{bmatrix}.
+
+It is important to note that :math:`\det{\mathbf{C}} \neq 1`, so the transform
+is not power-invariant. Its inverse is defined as:
+
+.. math::
+
+ (\alpha, \beta) \rightarrow (a, b): \mathbf{C^{-1}} =
+ \begin{bmatrix}
+ 1 && 0 \\
+ -\frac{1}{2} && \frac{\sqrt{3}}{2}
+ \end{bmatrix}.
+
+Park transform
+--------------
+
+After the application of the Clarke transformation, we still have quantities
+that are speed and time dependent. Assuming we have knowledge of the rotor
+position, :math:`\theta = \omega t`, we can de-rotate the :math:`\alpha\beta`
+space, therefore removing the speed and time dependency. The new frame will
+actually be a **rotating frame** and it is known as the :math:`dq` space, the
+space mentioned at the beginning.
+
+In order to derive the transformation we need to again project the
+:math:`\alpha\beta` components to the rotating frame. The transform is known as
+the **Park transform** and it is actually the well-known 2-D rotation matrix
+in its inverse form as we are de-rotating or moving clock-wise.
+
+.. math::
+
+ (\alpha, \beta) \rightarrow (d, q): \mathbf{P} =
+ \begin{bmatrix}
+ \cos(\theta) && \sin(\theta) \\
+ -\sin(\theta) && \cos(\theta)
+ \end{bmatrix}.
+
+In its inverse form it is given by:
+
+.. math::
+
+ (\alpha, \beta) \rightarrow (d, q): \mathbf{P^{-1}} =
+ \begin{bmatrix}
+ \cos(\theta) && -\sin(\theta) \\
+ \sin(\theta) && \cos(\theta)
+ \end{bmatrix}.
diff --git a/docs/theory/images/currsample-mid.odg b/docs/theory/images/currsample-mid.odg
new file mode 100755
index 0000000..c4c2466
Binary files /dev/null and b/docs/theory/images/currsample-mid.odg differ
diff --git a/docs/theory/images/currsample-mid.svg b/docs/theory/images/currsample-mid.svg
new file mode 100644
index 0000000..22337fe
--- /dev/null
+++ b/docs/theory/images/currsample-mid.svg
@@ -0,0 +1,2109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ T PWM
+
+
+
+
+
+ t
+
+
+
+
+
+ _ q a
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ q a
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ _ q b
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ q b
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ _ q c
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ q c
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |i Ra |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |i Rb |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |i Rc |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ T NOISE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ T RISE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ T PWM - T A
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ T SAMPLE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sampling Window
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sampling Trigger
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/theory/images/currsample-svpwm-anim.gif b/docs/theory/images/currsample-svpwm-anim.gif
new file mode 100755
index 0000000..a87973c
Binary files /dev/null and b/docs/theory/images/currsample-svpwm-anim.gif differ
diff --git a/docs/theory/images/currsample-svpwm-anim.py b/docs/theory/images/currsample-svpwm-anim.py
new file mode 100755
index 0000000..1943756
--- /dev/null
+++ b/docs/theory/images/currsample-svpwm-anim.py
@@ -0,0 +1,186 @@
+import argparse
+
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.animation import FuncAnimation
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-o", "--output", help="Output file")
+parser.add_argument("-s", "--sector", type=int, default=1, help="Sector")
+args = parser.parse_args()
+
+
+def calc(sector, angle):
+ alpha = np.deg2rad(angle)
+
+ # amplitude assumed sqrt(3) / 2 (maximum)
+ a = np.sqrt(3) / 2 * np.cos(alpha) - 1 / 2 * np.sin(alpha)
+ b = np.sin(alpha)
+ c = -(a + b)
+
+ if sector == 1:
+ x = a
+ y = b
+ z = 1 - (x + y)
+
+ da = x + y + 0.5 * z
+ db = y + 0.5 * z
+ dc = 0.5 * z
+ elif sector == 2:
+ x = -c
+ y = -a
+ z = 1 - (x + y)
+
+ da = x + 0.5 * z
+ db = x + y + 0.5 * z
+ dc = 0.5 * z
+ elif sector == 3:
+ x = b
+ y = c
+ z = 1 - (x + y)
+
+ da = 0.5 * z
+ db = x + y + 0.5 * z
+ dc = y + 0.5 * z
+ elif sector == 4:
+ x = -a
+ y = -b
+ z = 1 - (x + y)
+
+ da = 0.5 * z
+ db = x + 0.5 * z
+ dc = x + y + 0.5 * z
+ elif sector == 5:
+ x = c
+ y = a
+ z = 1 - (x + y)
+
+ da = y + 0.5 * z
+ db = 0.5 * z
+ dc = x + y + 0.5 * z
+ elif sector == 6:
+ x = -b
+ y = -c
+ z = 1 - (x + y)
+
+ da = x + y + 0.5 * z
+ db = 0.5 * z
+ dc = x + 0.5 * z
+
+ return da, db, dc
+
+
+fig, axes = plt.subplots(nrows=7, sharex=True, figsize=(8, 6))
+
+axes[0].set_ylim([0, 1])
+
+for axis in axes[1:]:
+ axis.set_xlim([0, 1])
+ axis.set_ylim([-0.1, 1.1])
+ axis.set_yticks([])
+ axis.set_xticks([0, 0.5, 1])
+
+axes[1].set_ylabel(r"$q_a$")
+axes[2].set_ylabel(r"$\bar{q}_a$")
+axes[3].set_ylabel(r"$q_b$")
+axes[4].set_ylabel(r"$\bar{q}_b$")
+axes[5].set_ylabel(r"$q_c$")
+axes[6].set_ylabel(r"$\bar{q}_c$")
+
+axes[6].set_xlabel("Regulation periods")
+
+N = 200
+t = np.linspace(0, 1, N)
+
+# plot timer ramp
+trig = np.zeros(N)
+trig[: N // 2] = 0 + 2 * t[: N // 2]
+trig[N // 2 :] = 2 - 2 * t[N // 2 :]
+axes[0].plot(t, trig)
+
+(da_line,) = axes[0].plot((0, 0), (0, 0), "r-")
+(db_line,) = axes[0].plot((0, 0), (0, 0), "g-")
+(dc_line,) = axes[0].plot((0, 0), (0, 0), "b-")
+
+(qa_H_line,) = axes[1].step([], [], "r")
+(qa_L_line,) = axes[2].step([], [], "r-")
+(qb_H_line,) = axes[3].step([], [], "g")
+(qb_L_line,) = axes[4].step([], [], "g")
+(qc_H_line,) = axes[5].step([], [], "b")
+(qc_L_line,) = axes[6].step([], [], "b")
+
+qa_H_text = axes[1].text(0, 0, "")
+qa_L_text = axes[2].text(0, 0, "")
+qb_H_text = axes[3].text(0, 0, "")
+qb_L_text = axes[4].text(0, 0, "")
+qc_H_text = axes[5].text(0, 0, "")
+qc_L_text = axes[6].text(0, 0, "")
+
+
+def update(angle):
+ """Update plot with new angle."""
+ da, db, dc = calc(args.sector, angle)
+
+ # adjust trigger lines
+ da_line.set_data((0, 1), (da, da))
+ db_line.set_data((0, 1), (db, db))
+ dc_line.set_data((0, 1), (dc, dc))
+
+ qa = np.zeros(N)
+ qa[np.where(da > trig)] = 1
+
+ qb = np.zeros(N)
+ qb[np.where(db > trig)] = 1
+
+ qc = np.zeros(N)
+ qc[np.where(dc > trig)] = 1
+
+ qa_H_line.set_data(t, qa)
+ qa_L_line.set_data(t, 1 - qa)
+
+ qb_H_line.set_data(t, qb)
+ qb_L_line.set_data(t, 1 - qb)
+
+ qc_H_line.set_data(t, qc)
+ qc_L_line.set_data(t, 1 - qc)
+
+ qa_H_text.set_text("{:.2f} %".format(100 * da))
+ qa_L_text.set_text("{:.2f} %".format(100 * (1 - da)))
+ qb_H_text.set_text("{:.2f} %".format(100 * db))
+ qb_L_text.set_text("{:.2f} %".format(100 * (1 - db)))
+ qc_H_text.set_text("{:.2f} %".format(100 * dc))
+ qc_L_text.set_text("{:.2f} %".format(100 * (1 - dc)))
+
+ return [
+ da_line,
+ db_line,
+ dc_line,
+ qa_H_line,
+ qa_L_line,
+ qb_H_line,
+ qb_L_line,
+ qc_H_line,
+ qc_L_line,
+ qa_H_text,
+ qa_L_text,
+ qb_H_text,
+ qb_L_text,
+ qc_H_text,
+ qc_L_text,
+ ]
+
+
+ani = FuncAnimation(
+ fig,
+ update,
+ interval=50,
+ frames=np.arange(60 * (args.sector - 1), 60 * args.sector, 0.5),
+ blit=True,
+)
+
+
+if args.output:
+ ani.save(args.output, fps=25, writer="imagemagick")
+else:
+ plt.show()
diff --git a/docs/theory/images/foc-abcs-anim.gif b/docs/theory/images/foc-abcs-anim.gif
new file mode 100755
index 0000000..391cd37
Binary files /dev/null and b/docs/theory/images/foc-abcs-anim.gif differ
diff --git a/docs/theory/images/foc-abcs-anim.py b/docs/theory/images/foc-abcs-anim.py
new file mode 100755
index 0000000..74187b0
--- /dev/null
+++ b/docs/theory/images/foc-abcs-anim.py
@@ -0,0 +1,110 @@
+import argparse
+
+import matplotlib.pyplot as plt
+import matplotlib.lines as lines
+import matplotlib.patches as mpatches
+from matplotlib.animation import FuncAnimation
+import numpy as np
+
+
+def circle(ax, radius):
+ ax.add_artist(mpatches.Circle((0, 0), radius, fill=False, alpha=0.3))
+
+
+def vector(color):
+ return plt.quiver(0, 0, 0, 0, color=color, angles="xy", scale_units="xy", scale=1)
+
+
+# unit vectors
+a_un = np.exp(1j * 0)
+b_un = np.exp(1j * 2 * np.pi / 3)
+c_un = np.exp(1j * 4 * np.pi / 3)
+
+fig, ax = plt.subplots()
+ax.set_aspect("equal")
+ax.set_xlim([-1.75, 1.75])
+ax.set_ylim([-1.75, 1.75])
+ax.set_xticks([1, 1.5])
+ax.set_yticks([1, 1.5])
+
+# center axes (using left+bottom only)
+ax.spines["left"].set_position("center")
+ax.spines["bottom"].set_position("center")
+ax.spines["right"].set_color("none")
+ax.spines["top"].set_color("none")
+
+ax.xaxis.set_ticks_position("bottom")
+ax.yaxis.set_ticks_position("left")
+
+title = ax.text(0.95, 0.95, "", transform=ax.transAxes, ha="right")
+
+# enclosing circles
+circle(ax, 1)
+circle(ax, 3 / 2)
+
+# vectors (u, v, w, beta, alpha, space-vector)
+a_line = vector("red")
+a_line_label = ax.text(0, 0, r"$\vec{i_a}$", color="red")
+
+b_line = vector("green")
+b_line_label = ax.text(0, 0, r"$\vec{i_b}$", color="green")
+
+c_line = vector("blue")
+c_line_label = ax.text(0, 0, r"$\vec{i_c}$", color="blue")
+
+i_s_line = vector("black")
+i_s_line_label = ax.text(0, 0, r"$\vec{i_s}$", color="black")
+
+
+def update(angle):
+ """Update plot with new angle."""
+
+ i_a = np.cos(angle)
+ i_b = np.cos(angle - 2 * np.pi / 3)
+ i_c = np.cos(angle - 4 * np.pi / 3)
+
+ title.set_text("Rotor angle: {:.0f} deg".format(np.rad2deg(angle)))
+
+ a = a_un * i_a
+ a_line.set_UVC(np.real(a), np.imag(a))
+ a_line_label.set_position((np.real(a), np.imag(a)))
+
+ b = b_un * i_b
+ b_line.set_UVC(np.real(b), np.imag(b))
+ b_line_label.set_position((np.real(b), np.imag(b)))
+
+ c = c_un * i_c
+ c_line.set_UVC(np.real(c), np.imag(c))
+ c_line_label.set_position((1.1 * np.real(c), np.imag(c)))
+
+ i_s = a + b + c
+ i_s_line.set_UVC(np.real(i_s), np.imag(i_s))
+ i_s_line_label.set_position((np.real(i_s), np.imag(i_s)))
+
+ return [
+ title,
+ a_line,
+ a_line_label,
+ b_line,
+ b_line_label,
+ c_line,
+ c_line_label,
+ i_s_line,
+ i_s_line_label,
+ ]
+
+
+ani = FuncAnimation(
+ fig, update, interval=50, frames=np.arange(0, 2 * np.pi, 2 * np.pi / 180), blit=True
+)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-o", "--output", help="Output file")
+ args = parser.parse_args()
+
+ if args.output:
+ ani.save(args.output, fps=25, writer="imagemagick")
+ else:
+ plt.show()
diff --git a/docs/theory/images/svpwm-abc-signs.py b/docs/theory/images/svpwm-abc-signs.py
new file mode 100755
index 0000000..3528138
--- /dev/null
+++ b/docs/theory/images/svpwm-abc-signs.py
@@ -0,0 +1,38 @@
+import numpy as np
+import matplotlib.pyplot as plt
+
+
+fig, ax = plt.subplots()
+
+ax.set_xlabel("State space vector angle (deg)")
+ax.set_ylabel(r"$a, b, c$")
+
+alphad = np.arange(0, 360, 1)
+alpha = np.deg2rad(alphad)
+
+# amplitude assumed sqrt(3) / 2 (maximum)
+a = np.sqrt(3) / 2 * np.cos(alpha) - 1 / 2 * np.sin(alpha)
+b = np.sin(alpha)
+c = -(a + b)
+
+ax.plot(alphad, a, color="r", label="a")
+ax.plot(alphad, b, color="g", label="b")
+ax.plot(alphad, c, color="b", label="c")
+
+ax.set_xticks([0, 60, 120, 180, 240, 300, 360])
+ax.set_xlim([0, 360])
+
+ax.set_yticks([-1, 0, 1])
+ax.set_ylim([-1, 1])
+
+ax.axvline(0, ls="--")
+ax.axvline(60, ls="--")
+ax.axvline(120, ls="--")
+ax.axvline(180, ls="--")
+ax.axvline(240, ls="--")
+ax.axvline(300, ls="--")
+ax.axvline(360, ls="--")
+
+ax.legend(loc="upper right")
+
+plt.show()
diff --git a/docs/theory/images/svpwm-hexagon.py b/docs/theory/images/svpwm-hexagon.py
new file mode 100755
index 0000000..24863f9
--- /dev/null
+++ b/docs/theory/images/svpwm-hexagon.py
@@ -0,0 +1,45 @@
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.lines as lines
+
+
+def v_s(q_c, q_b, q_a):
+ return q_a + q_b * np.exp(1j * 2 * np.pi / 3) + q_c * np.exp(1j * 4 * np.pi / 3)
+
+
+def plot_vector(ax, v, label):
+ r, i = np.real(v), np.imag(v)
+ ax.quiver(0, 0, r, i, color="k", angles="xy", scale_units="xy", scale=1)
+ ha = "left" if r > 0 else "right"
+ va = "bottom" if i > 0 else "top"
+ ax.annotate(label, (r, i), ha=ha, va=va)
+
+
+fig, ax = plt.subplots()
+ax.set_aspect("equal")
+ax.set_axis_off()
+ax.set_xlim([-1, 1])
+ax.set_ylim([-1, 1])
+
+# space vectors
+plot_vector(ax, v_s(0, 0, 0), r"$\vec{v_0} (000)$")
+plot_vector(ax, v_s(0, 0, 1), r"$\vec{v_1} (001)$")
+plot_vector(ax, v_s(0, 1, 0), r"$\vec{v_2} (010)$")
+plot_vector(ax, v_s(0, 1, 1), r"$\vec{v_3} (011)$")
+plot_vector(ax, v_s(1, 0, 0), r"$\vec{v_4} (100)$")
+plot_vector(ax, v_s(1, 0, 1), r"$\vec{v_5} (101)$")
+plot_vector(ax, v_s(1, 1, 0), r"$\vec{v_6} (110)$")
+plot_vector(ax, v_s(1, 1, 1), r"$\vec{v_7} (111)$")
+
+# hexagon
+angles = np.linspace(0, 2 * np.pi, 7)
+for i in range(len(angles) - 1):
+ ax.add_line(
+ lines.Line2D(
+ [np.cos(angles[i]), np.cos(angles[i + 1])],
+ [np.sin(angles[i]), np.sin(angles[i + 1])],
+ color="k",
+ )
+ )
+
+plt.show()
diff --git a/docs/theory/images/svpwm-limit.odg b/docs/theory/images/svpwm-limit.odg
new file mode 100755
index 0000000..16c5685
Binary files /dev/null and b/docs/theory/images/svpwm-limit.odg differ
diff --git a/docs/theory/images/svpwm-limit.svg b/docs/theory/images/svpwm-limit.svg
new file mode 100644
index 0000000..ab76432
--- /dev/null
+++ b/docs/theory/images/svpwm-limit.svg
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30 °
+
+
+
+
+
+ V d
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ V sMAX
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/theory/images/svpwm-modulation.py b/docs/theory/images/svpwm-modulation.py
new file mode 100755
index 0000000..ed6825f
--- /dev/null
+++ b/docs/theory/images/svpwm-modulation.py
@@ -0,0 +1,90 @@
+import numpy as np
+import matplotlib.pyplot as plt
+
+
+def calc(sector, angle):
+ alpha = np.deg2rad(angle)
+
+ # amplitude assumed sqrt(3) / 2 (maximum)
+ a = np.sqrt(3) / 2 * np.cos(alpha) - 1 / 2 * np.sin(alpha)
+ b = np.sin(alpha)
+ c = -(a + b)
+
+ if sector == 1:
+ x = a
+ y = b
+ z = 1 - (x + y)
+
+ da = x + y + 0.5 * z
+ db = y + 0.5 * z
+ dc = 0.5 * z
+ elif sector == 2:
+ x = -c
+ y = -a
+ z = 1 - (x + y)
+
+ da = x + 0.5 * z
+ db = x + y + 0.5 * z
+ dc = 0.5 * z
+ elif sector == 3:
+ x = b
+ y = c
+ z = 1 - (x + y)
+
+ da = 0.5 * z
+ db = x + y + 0.5 * z
+ dc = y + 0.5 * z
+ elif sector == 4:
+ x = -a
+ y = -b
+ z = 1 - (x + y)
+
+ da = 0.5 * z
+ db = x + 0.5 * z
+ dc = x + y + 0.5 * z
+ elif sector == 5:
+ x = c
+ y = a
+ z = 1 - (x + y)
+
+ da = y + 0.5 * z
+ db = 0.5 * z
+ dc = x + y + 0.5 * z
+ elif sector == 6:
+ x = -b
+ y = -c
+ z = 1 - (x + y)
+
+ da = x + y + 0.5 * z
+ db = 0.5 * z
+ dc = x + 0.5 * z
+
+ return da, db, dc
+
+
+fig, ax = plt.subplots()
+
+ax.set_xlabel("State space vector angle (deg)")
+ax.set_ylabel("Duty cycle")
+
+alpha = np.array([])
+da = np.array([])
+db = np.array([])
+dc = np.array([])
+
+for sector in range(1, 7):
+ alpha_ = np.arange(60 * (sector - 1), 60 * sector, 1)
+ da_, db_, dc_ = calc(sector, alpha_)
+
+ alpha = np.append(alpha, alpha_)
+ da = np.append(da, da_)
+ db = np.append(db, db_)
+ dc = np.append(dc, dc_)
+
+ax.plot(alpha, da, color="r", label=r"$d_a$")
+ax.plot(alpha, db, color="g", label=r"$d_b$")
+ax.plot(alpha, dc, color="b", label=r"$d_c$")
+
+ax.legend(loc="upper right")
+
+plt.show()
diff --git a/docs/theory/images/svpwm-pwm-timing.odg b/docs/theory/images/svpwm-pwm-timing.odg
new file mode 100755
index 0000000..6861b94
Binary files /dev/null and b/docs/theory/images/svpwm-pwm-timing.odg differ
diff --git a/docs/theory/images/svpwm-pwm-timing.svg b/docs/theory/images/svpwm-pwm-timing.svg
new file mode 100644
index 0000000..751786a
--- /dev/null
+++ b/docs/theory/images/svpwm-pwm-timing.svg
@@ -0,0 +1,2367 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ T PWM
+
+
+
+
+
+ q a
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ t
+
+
+
+
+
+
+
+
+
+
+
+
+ q b
+
+
+
+
+
+ q c
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ z 0 /2
+
+
+
+
+
+ z 0 /2
+
+
+
+
+
+ x/2
+
+
+
+
+
+ x/2
+
+
+
+
+
+ y/2
+
+
+
+
+
+ y/2
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 0
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 0
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ 1
+
+
+
+
+
+ v 0
+
+
+
+
+
+ v 1
+
+
+
+
+
+ v 0
+
+
+
+
+
+ v 1
+
+
+
+
+
+ v 3
+
+
+
+
+
+ v 3
+
+
+
+
+
+ v 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+ z 7
+
+
+
+
+
+ d a
+
+
+
+
+
+ d b
+
+
+
+
+
+ d c
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/theory/svpwm.rst b/docs/theory/svpwm.rst
new file mode 100755
index 0000000..94ae80a
--- /dev/null
+++ b/docs/theory/svpwm.rst
@@ -0,0 +1,215 @@
+.. _theory-svpwm:
+
+SV-PWM
+======
+
+In a FOC based system we usually synthesize the voltage Space Vector
+:math:`\vec{v_s}` using an inverter. When using an inverter we can not generate
+all the voltage levels we want but only a discrete set. With a 2-level 3-phase
+inverter, the most common one, we can generate :math:`2^3 = 8` voltage levels,
+given by:
+
+.. math::
+
+ \vec{v_s} = V_d (q_a e^{j0} + q_b e^{j \frac{2\pi}{3}} + q_c e^{j \frac{4\pi}{3}})
+
+where :math:`V_d` is the line voltage and :math:`q_a, q_b, q_c \in (0 =
+\text{OFF}, 1 = \text{ON})` correspond to the the *switch* state of each phase.
+
+.. _table-space-vectors:
+.. table:: Synthesizable space vectors with a 2-level 3-phase inverter.
+ :align: center
+
+ ================================================ =========== =========== ===========
+ Space Vector :math:`q_c` :math:`q_b` :math:`q_a`
+ ================================================ =========== =========== ===========
+ :math:`\vec{v_0} = 0` 0 0 0
+ :math:`\vec{v_1} = V_d` 0 0 1
+ :math:`\vec{v_2} = V_d e^{j \frac{2 \pi}{3}}` 0 1 0
+ :math:`\vec{v_3} = V_d e^{j \frac{\pi}{3}}` 0 1 1
+ :math:`\vec{v_4} = V_d e^{-j \frac{2 \pi}{3}}` 1 0 0
+ :math:`\vec{v_5} = V_d e^{-j \frac{\pi}{3}}` 1 0 1
+ :math:`\vec{v_6} = -V_d` 1 1 0
+ :math:`\vec{v_7} = 0` 1 1 1
+ ================================================ =========== =========== ===========
+
+If we draw lines that go from one vector edge to the other we can observe that
+these lines form an hexagon as shown below.
+
+.. _svpwm-hexagon:
+.. plot:: theory/images/svpwm-hexagon.py
+
+ SV-PWM synthesizable state vectors.
+
+In order to synthesize an arbitrary voltage, there is a rather simple technique:
+given the sector in which the vector to be synthesized falls, we can quickly
+alternate between the two adjacent vectors. Taking a voltage falling in the
+first sector, i.e. :math:`\theta \in \left( 0, \frac{\pi}{3} \right)`, we have
+that the average space-vector :math:`\vec{v^a_s}` is given by:
+
+.. math::
+
+ \left.\vec{v^a_s}\right|_{\text{sector} = 1}
+ = \frac{1}{T} \left( x T \vec{v_1} + y T \vec{v_3} + z T \vec{0} \right)
+ = x \vec{v_1} + y \vec{v_3}
+
+where :math:`T` is the averaging period and :math:`x + y + z = 1`. Note that
+:math:`z` is the fraction of time where the actual voltage is zero (this happens
+for :math:`\vec{v_0}` and :math:`\vec{v_7}`). Replacing with values from
+:numref:`table-space-vectors` we have:
+
+.. math::
+ \hat{V_s} e^{j \theta_s} = x V_d e^{j 0} + y V_d e^{j \frac{\pi}{3}}
+
+and by equaling both real and imaginary components, we obtain:
+
+.. math::
+ x &= \frac{\hat{V_s}}{V_d} \left( \cos(\theta_s) - \frac{1}{\sqrt{3}} \sin(\theta_s) \right) \\
+ y &= \frac{\hat{V_s}}{V_d} \frac{2}{\sqrt{3}} \sin(\theta_s).
+
+In terms of the :math:`\alpha` and :math:`\beta` components, we have:
+
+.. math::
+ x &= \frac{1}{V_d} \left( \hat{v_{\alpha}} - \frac{1}{\sqrt{3}} \hat{v_{\beta}} \right) \\
+ y &= \frac{1}{V_d} \frac{2}{\sqrt{3}} \hat{v_{\beta}}.
+
+If we repeat the previous calculation for each sector we get similar results. If
+we take:
+
+.. math::
+ :label: eq-abc
+
+ a &= x \\
+ b &= y \\
+ c &= -(x + y)
+
+being :math:`x, y` the values obtained for the first sector, we can express the
+other values as a function of :math:`a, b, c`.
+
+.. _table-svpwm-XY:
+.. table:: SV-PWM X-Y
+ :align: center
+
+ ====== ========== ==========
+ Sector :math:`x` :math:`y`
+ ====== ========== ==========
+ 1 a b
+ 2 -c -a
+ 3 b c
+ 4 -a -b
+ 5 c a
+ 6 -b -c
+ ====== ========== ==========
+
+Sector determination
+--------------------
+
+As we have already seen, knowledge of the sector is essential to compute the
+:math:`x, y` values. If we are given the space vector in cartesian form, that
+is, :math:`v_{\alpha}` and :math:`v_{\beta}`, we can easily determine its angle
+by performing :math:`\arctan\left(\frac{v_{\beta}}{v_{\alpha}}\right)` and hence
+the sector. However, :math:`\arctan` is an expensive trigonometric computation.
+It turns out there is a faster way to determine the sector.
+
+If we take a look at the plot of Eq. :eq:`eq-abc` for all sectors, we have that
+each sector has a unique combination of signs:
+
+.. plot:: theory/images/svpwm-abc-signs.py
+
+ Eq. :eq:`eq-abc` signs.
+
+.. table::
+ :align: center
+
+ ====== ====================== ====================== ======================
+ Sector :math:`\text{sign}(a)` :math:`\text{sign}(b)` :math:`\text{sign}(c)`
+ ====== ====================== ====================== ======================
+ 1 ``+`` ``+`` ``-``
+ 2 ``-`` ``+`` ``-``
+ 3 ``-`` ``+`` ``+``
+ 4 ``-`` ``-`` ``+``
+ 5 ``+`` ``-`` ``+``
+ 6 ``+`` ``-`` ``-``
+ ====== ====================== ====================== ======================
+
+
+Amplitude limitation
+--------------------
+
+By looking at the hexagon we can quickly observe that vectors falling in the
+middle of a sector will not have the same average amplitude as the ones that can
+be perfectly generated :math:`\vec{v_i}, i \in (0, ..., 7)`. The worst case
+happens for the space vectors falling just in the middle of the sector as shown
+on the figure below.
+
+.. figure:: images/svpwm-limit.svg
+
+ Maximum synthesizable amplitude without distortion.
+
+Therefore, in order to avoid distortions the maximum average amplitude should be
+limited to:
+
+.. math::
+
+ \hat{V_s}_{MAX} = V_d \cos(30^{\circ}) = V_d \frac{\sqrt{3}}{2}.
+
+The previous value is actually the maximum line voltage we will be able to use
+when using SV-PWM.
+
+Duty cycles calculation
+-----------------------
+
+Finally, we need to compute the PWM duty cycles using the values calculated in
+:numref:`table-svpwm-XY`. When using a center-aligned PWM we have that the
+actual PWM output is "ON" when the control variable is over the trigger signal
+(a saw-tooth) and "OFF" otherwise.
+
+There is still one thing left: the zero or null vector. Right at the beginning
+of this section we saw that in the formation of the space vector there is a
+fraction of time, :math:`z`, where a *zero* vector is active. There are actually
+a couple of zero vectors, :math:`\vec{v_0}` and :math:`\vec{v_7}`. Both vectors
+are valid in order to produce the space vector, however, it is common to use the
+null-vector that only requires a single switch state change with respect to the
+previous or future state. This choice is also known as the **reverse-alternating
+sequence**. For example, if the next state is :math:`\vec{v_1}` (001), then the
+null-vector choice would be :math:`\vec{v_0}`. Below the waveforms on the first
+sector are shown when using such sequence.
+
+.. _svpwm-pwm-timing:
+.. figure:: images/svpwm-pwm-timing.svg
+
+ PWM waveforms in sector 1 (:math:`z = z_0 + z_7`).
+
+By looking at the timing diagram in :numref:`svpwm-pwm-timing`, we have that the
+duty cycles are for the first sector:
+
+.. math::
+ d_a &= x + y + \frac{z}{2} \\
+ d_b &= y + \frac{z}{2} \\
+ d_c &= \frac{z}{2}
+
+We can do a similar calculation for each sector, leading to the duty cycles
+listed in :numref:`table-svpwm-duties`.
+
+.. _table-svpwm-duties:
+.. table:: SV-PWM duty cycle equations
+ :align: center
+
+ ====== =========================== =========================== ===========================
+ Sector :math:`d_a` :math:`d_b` :math:`d_c`
+ ====== =========================== =========================== ===========================
+ 1 :math:`x + y + \frac{z}{2}` :math:`y + \frac{z}{2}` :math:`\frac{z}{2}`
+ 2 :math:`x + \frac{z}{2}` :math:`x + y + \frac{z}{2}` :math:`\frac{z}{2}`
+ 3 :math:`\frac{z}{2}` :math:`x + y + \frac{z}{2}` :math:`y + \frac{z}{2}`
+ 4 :math:`\frac{z}{2}` :math:`x + \frac{z}{2}` :math:`x + y + \frac{z}{2}`
+ 5 :math:`y + \frac{z}{2}` :math:`\frac{z}{2}` :math:`x + y + \frac{z}{2}`
+ 6 :math:`x + y + \frac{z}{2}` :math:`\frac{z}{2}` :math:`x + \frac{z}{2}`
+ ====== =========================== =========================== ===========================
+
+Using equations from :numref:`table-svpwm-XY` and :numref:`table-svpwm-duties`
+we can plot the duty cycle waveforms:
+
+.. _svpwm-modulation:
+.. plot:: theory/images/svpwm-modulation.py
+
+ SV-PWM duty cycle waveforms.
diff --git a/docs/zbibliography.rst b/docs/zbibliography.rst
new file mode 100755
index 0000000..fd69c1c
--- /dev/null
+++ b/docs/zbibliography.rst
@@ -0,0 +1,6 @@
+============
+Bibliography
+============
+
+.. bibliography:: bibliography.bib
+ :all:
diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt
new file mode 100644
index 0000000..b6e8ad8
--- /dev/null
+++ b/drivers/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+add_subdirectory_ifdef(CONFIG_SPINNER_CURRSMP currsmp)
+add_subdirectory_ifdef(CONFIG_SPINNER_FEEDBACK feedback)
+add_subdirectory_ifdef(CONFIG_SPINNER_SVPWM svpwm)
diff --git a/drivers/Kconfig b/drivers/Kconfig
new file mode 100644
index 0000000..531d94a
--- /dev/null
+++ b/drivers/Kconfig
@@ -0,0 +1,10 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+menu "Drivers"
+
+rsource "currsmp/Kconfig"
+rsource "feedback/Kconfig"
+rsource "svpwm/Kconfig"
+
+endmenu
\ No newline at end of file
diff --git a/drivers/currsmp/CMakeLists.txt b/drivers/currsmp/CMakeLists.txt
new file mode 100755
index 0000000..d1dfb58
--- /dev/null
+++ b/drivers/currsmp/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_library()
+zephyr_library_sources_ifdef(CONFIG_SPINNER_CURRSMP_SHUNT_STM32 currsmp_shunt_stm32.c)
+
diff --git a/drivers/currsmp/Kconfig b/drivers/currsmp/Kconfig
new file mode 100644
index 0000000..fa0572a
--- /dev/null
+++ b/drivers/currsmp/Kconfig
@@ -0,0 +1,23 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+menuconfig SPINNER_CURRSMP
+ bool "Current Sample Drivers"
+ help
+ Enable options for current sample drivers.
+
+if SPINNER_CURRSMP
+
+module = SPINNER_CURRSMP
+module-str = SPINNER_CURRSMP
+source "subsys/logging/Kconfig.template.log_config"
+
+config SPINNER_CURRSMP_INIT_PRIORITY
+ int "Current sampling init priority"
+ default 80
+ help
+ Current sampling initialization priority.
+
+rsource "Kconfig.stm32"
+
+endif # SPINNER_CURRSMP
diff --git a/drivers/currsmp/Kconfig.stm32 b/drivers/currsmp/Kconfig.stm32
new file mode 100644
index 0000000..90891b2
--- /dev/null
+++ b/drivers/currsmp/Kconfig.stm32
@@ -0,0 +1,28 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+config SPINNER_CURRSMP_SHUNT_STM32
+ bool "STM32 shunt current sampling driver"
+ default y if SOC_FAMILY_STM32
+ select SPINNER_UTILS_STM32
+ select USE_STM32_LL_ADC
+ select ZERO_LATENCY_IRQS
+ help
+ Enable shunt current sampling driver for STM32 SoCs
+
+if SPINNER_CURRSMP_SHUNT_STM32
+
+config SPINNER_CURRSMP_SHUNT_STM32_ADC_RES
+ int "ADC resolution"
+ default 12
+ help
+ ADC resolution in bits
+
+config SPINNER_CURRSMP_SHUNT_STM32_ADC_SMP_TIME
+ int "ADC sampling time"
+ default 2
+ help
+ ADC sampling time in cycles. Decimal sampling times must be rounded
+ up, e.g. 19.5 needs to be provided as 20.
+
+endif # SPINNER_CURRSMP_SHUNT_STM32
diff --git a/drivers/currsmp/currsmp_shunt_stm32.c b/drivers/currsmp/currsmp_shunt_stm32.c
new file mode 100755
index 0000000..26625fd
--- /dev/null
+++ b/drivers/currsmp/currsmp_shunt_stm32.c
@@ -0,0 +1,451 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT st_stm32_currsmp_shunt
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#include
+LOG_MODULE_REGISTER(currsmp_shunt_stm32, CONFIG_SPINNER_CURRSMP_LOG_LEVEL);
+
+/*******************************************************************************
+ * Private
+ ******************************************************************************/
+
+struct currsmp_shunt_stm32_config {
+ ADC_TypeDef *adc;
+ struct stm32_pclken pclken;
+ uint32_t adc_irq;
+ uint32_t adc_ch_a;
+ uint32_t adc_ch_b;
+ uint32_t adc_ch_c;
+ uint32_t adc_trigger;
+ const struct soc_gpio_pinctrl *pinctrl;
+ size_t pinctrl_len;
+};
+
+struct currsmp_shunt_stm32_data {
+ currsmp_regulation_cb_t regulation_cb;
+ void *regulation_ctx;
+ uint16_t i_a_offset;
+ uint16_t i_b_offset;
+ uint16_t i_c_offset;
+ uint8_t sector;
+ uint32_t jsqr[3];
+};
+
+ISR_DIRECT_DECLARE(adc_irq)
+{
+ const struct device *dev = DEVICE_DT_INST_GET(0);
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+ struct currsmp_shunt_stm32_data *data = dev->data;
+
+ if (LL_ADC_IsActiveFlag_JEOS(config->adc)) {
+ LL_ADC_ClearFlag_JEOS(config->adc);
+ data->regulation_cb(data->regulation_ctx);
+ }
+
+ return 0;
+}
+
+/**
+ * Compute the ADC injected sequence register (JSQR) for the given 2 channels.
+ *
+ * @param[in] trigger ADC trigger.
+ * @param[in] rank1_ch Rank 1 channel.
+ * @param[in] rank2_ch Rank 2 channel.
+ *
+ * @return Computed JSQR register value.
+ */
+static uint32_t adc_calc_jsqr(uint32_t trigger, uint32_t rank1_ch,
+ uint32_t rank2_ch)
+{
+ uint32_t jsqr;
+
+ uint8_t ch1 = __LL_ADC_CHANNEL_TO_DECIMAL_NB(rank1_ch) - 1U;
+ uint8_t ch2 = __LL_ADC_CHANNEL_TO_DECIMAL_NB(rank2_ch) - 1U;
+
+ jsqr = ((ch1 & ADC_INJ_RANK_ID_JSQR_MASK) << ADC_INJ_RANK_1_JSQR_BITOFFSET_POS) |
+ ((ch2 & ADC_INJ_RANK_ID_JSQR_MASK) << ADC_INJ_RANK_2_JSQR_BITOFFSET_POS) |
+ LL_ADC_INJ_TRIG_EXT_RISING | trigger | 1U;
+
+ return jsqr;
+}
+
+/**
+ * @brief Configure ADC.
+ *
+ * @param[in] dev Current sampling device.
+ *
+ * @return 0 on success, negative errno otherwise.
+ */
+static int adc_configure(const struct device *dev)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+
+ int ret;
+ const struct device *clk;
+ uint32_t smp;
+ LL_ADC_CommonInitTypeDef adc_cinit;
+ LL_ADC_InitTypeDef adc_init;
+ LL_ADC_REG_InitTypeDef adc_rinit;
+ LL_ADC_INJ_InitTypeDef adc_jinit;
+ uint32_t adc_clk;
+
+ /* enable ADC clock */
+ clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
+ ret = clock_control_on(clk, (clock_control_subsys_t *)&config->pclken);
+ if (ret < 0) {
+ LOG_ERR("Could not turn on ADC clock (%d)", ret);
+ return ret;
+ }
+
+ /* configure common ADC instance */
+ LL_ADC_CommonStructInit(&adc_cinit);
+ if (CONFIG_SPINNER_CURRSMP_SHUNT_STM32_ADC_RES == 6U) {
+ adc_cinit.CommonClock = LL_ADC_CLOCK_SYNC_PCLK_DIV2;
+ } else {
+ adc_cinit.CommonClock = LL_ADC_CLOCK_SYNC_PCLK_DIV4;
+ }
+ if (LL_ADC_CommonInit(__LL_ADC_COMMON_INSTANCE(config->adc),
+ &adc_cinit) != SUCCESS) {
+ LOG_ERR("Could not initialize common ADC");
+ return -EIO;
+ }
+
+ /* configure ADC */
+ LL_ADC_StructInit(&adc_init);
+
+ ret = stm32_adc_res_get(CONFIG_SPINNER_CURRSMP_SHUNT_STM32_ADC_RES,
+ &adc_init.Resolution);
+ if (ret < 0) {
+ LOG_ERR("Unsupported ADC resolution");
+ return ret;
+ }
+
+ if (LL_ADC_Init(config->adc, &adc_init) != SUCCESS) {
+ LOG_ERR("Could not initialize ADC");
+ return -EIO;
+ }
+
+ /* configure ADC (regular) */
+ LL_ADC_REG_StructInit(&adc_rinit);
+ adc_rinit.Overrun = LL_ADC_REG_OVR_DATA_PRESERVED;
+ if (LL_ADC_REG_Init(config->adc, &adc_rinit) != SUCCESS) {
+ LOG_ERR("Could not initialize ADC regular group");
+ return -EIO;
+ }
+
+ /* configure ADC (injected) */
+ LL_ADC_INJ_StructInit(&adc_jinit);
+ adc_jinit.TriggerSource = config->adc_trigger | LL_ADC_INJ_TRIG_EXT_RISING;
+ if (LL_ADC_INJ_Init(config->adc, &adc_jinit) != SUCCESS) {
+ LOG_ERR("Could not initialize ADC injected group");
+ return -EIO;
+ }
+
+ /* configure sampling time */
+ ret = stm32_adc_smp_get(CONFIG_SPINNER_CURRSMP_SHUNT_STM32_ADC_SMP_TIME, &smp);
+ if (ret < 0) {
+ LOG_ERR("Unsupported ADC sampling time");
+ return ret;
+ }
+
+ LL_ADC_SetChannelSamplingTime(config->adc, config->adc_ch_a, smp);
+ LL_ADC_SetChannelSamplingTime(config->adc, config->adc_ch_b, smp);
+ LL_ADC_SetChannelSamplingTime(config->adc, config->adc_ch_c, smp);
+
+ /* enable internal ADC regulator */
+ LL_ADC_EnableInternalRegulator(config->adc);
+ k_busy_wait(LL_ADC_DELAY_INTERNAL_REGUL_STAB_US);
+ if (!LL_ADC_IsInternalRegulatorEnabled(config->adc)) {
+ LOG_ERR("ADC internal regulator not enabled within expected time");
+ return -EIO;
+ }
+
+ /* calibrate ADC */
+ LL_ADC_StartCalibration(config->adc, LL_ADC_SINGLE_ENDED);
+ while (LL_ADC_IsCalibrationOnGoing(config->adc))
+ ;
+
+ /* wait to enable ADC after calibration */
+ ret = stm32_adc_clk_get(config->adc, &config->pclken, &adc_clk);
+ if (ret < 0) {
+ return ret;
+ }
+
+ k_busy_wait(MAX(1U, (uint32_t)((1e6 / (float)adc_clk) *
+ LL_ADC_DELAY_CALIB_ENABLE_ADC_CYCLES)));
+
+ /* enable ADC */
+ LL_ADC_Enable(config->adc);
+ while (LL_ADC_IsActiveFlag_ADRDY(config->adc) != 1U)
+ ;
+
+ /* configure ADC IRQ */
+ LL_ADC_EnableIT_JEOS(config->adc);
+
+ IRQ_DIRECT_CONNECT(DT_IRQ_BY_IDX(DT_PARENT(DT_DRV_INST(0)), 0, irq),
+ DT_IRQ_BY_IDX(DT_PARENT(DT_DRV_INST(0)), 0, priority),
+ adc_irq, IRQ_ZERO_LATENCY);
+ irq_enable(config->adc_irq);
+
+ return 0;
+}
+
+/**
+ * @brief Perform regular ADC read.
+ *
+ * @param dev Current sampling device
+ * @param channel ADC channel
+ *
+ * @return Sample value.
+ */
+static uint16_t adc_read(const struct device *dev, uint32_t channel)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+
+ /* configure sequencer: only one channel */
+ LL_ADC_REG_SetSequencerLength(config->adc, LL_ADC_REG_SEQ_SCAN_DISABLE);
+ LL_ADC_REG_SetSequencerRanks(config->adc, LL_ADC_REG_RANK_1, channel);
+
+ /* perform regular conversion */
+ LL_ADC_REG_StartConversion(config->adc);
+ while (LL_ADC_IsActiveFlag_EOS(config->adc) != 1U)
+ ;
+
+ LL_ADC_ClearFlag_EOS(config->adc);
+
+ return (uint16_t)LL_ADC_REG_ReadConversionData32(config->adc);
+}
+
+/*******************************************************************************
+ * API
+ ******************************************************************************/
+
+static void currsmp_shunt_stm32_configure(const struct device *dev,
+ currsmp_regulation_cb_t regulation_cb,
+ void *ctx)
+{
+ struct currsmp_shunt_stm32_data *data = dev->data;
+
+ data->regulation_cb = regulation_cb;
+ data->regulation_ctx = ctx;
+}
+
+static void currsmp_shunt_stm32_get_currents(const struct device *dev,
+ struct currsmp_curr *curr)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+ struct currsmp_shunt_stm32_data *data = dev->data;
+
+ uint16_t val_ch1;
+ uint16_t val_ch2;
+ int16_t i_a = 0, i_b = 0, i_c = 0;
+
+ val_ch1 = (uint16_t)LL_ADC_INJ_ReadConversionData32(config->adc,
+ LL_ADC_INJ_RANK_1);
+ val_ch2 = (uint16_t)LL_ADC_INJ_ReadConversionData32(config->adc,
+ LL_ADC_INJ_RANK_2);
+
+ switch (data->sector) {
+ case 1U:
+ i_b = data->i_b_offset - val_ch1;
+ i_c = data->i_c_offset - val_ch2;
+ i_a = -(i_b + i_c);
+ break;
+ case 2U:
+ i_a = data->i_a_offset - val_ch1;
+ i_c = data->i_c_offset - val_ch2;
+ i_b = -(i_a + i_c);
+ break;
+ case 3U:
+ i_a = data->i_a_offset - val_ch1;
+ i_c = data->i_c_offset - val_ch2;
+ i_b = -(i_a + i_c);
+ break;
+ case 4U:
+ i_a = data->i_a_offset - val_ch2;
+ i_b = data->i_b_offset - val_ch1;
+ i_c = -(i_a + i_b);
+ break;
+ case 5U:
+ i_a = data->i_a_offset - val_ch2;
+ i_b = data->i_b_offset - val_ch1;
+ i_c = -(i_a + i_b);
+ break;
+ case 6U:
+ i_b = data->i_b_offset - val_ch1;
+ i_c = data->i_c_offset - val_ch2;
+ i_a = -(i_b + i_c);
+ break;
+ default:
+ __ASSERT(NULL, "Unexpected sector");
+ break;
+ }
+
+ curr->i_a = (float)i_a / (2 << (CONFIG_SPINNER_CURRSMP_SHUNT_STM32_ADC_RES - 1));
+ curr->i_b = (float)i_b / (2 << (CONFIG_SPINNER_CURRSMP_SHUNT_STM32_ADC_RES - 1));
+ curr->i_c = (float)i_c / (2 << (CONFIG_SPINNER_CURRSMP_SHUNT_STM32_ADC_RES - 1));
+}
+
+static void currsmp_shunt_stm32_set_sector(const struct device *dev,
+ uint8_t sector)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+ struct currsmp_shunt_stm32_data *data = dev->data;
+
+ data->sector = sector;
+ config->adc->JSQR = data->jsqr[sector / 2U % 3U];
+}
+
+static uint32_t currsmp_shunt_stm32_get_smp_time(const struct device *dev)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+
+ int ret;
+ uint32_t clk;
+ float t_sar;
+
+ ret = stm32_adc_clk_get(config->adc, &config->pclken, &clk);
+ if (ret < 0) {
+ LOG_ERR("Could not obtain ADC clock rate");
+ return 0U;
+ }
+
+ ret = stm32_adc_t_sar_get(CONFIG_SPINNER_CURRSMP_SHUNT_STM32_ADC_RES,
+ &t_sar);
+ if (ret < 0) {
+ LOG_ERR("Could not obtain ADC SAR time");
+ return 0U;
+ }
+
+ return (uint32_t)((1.0e9 / (float)clk) *
+ (t_sar + 2.0f * CONFIG_SPINNER_CURRSMP_SHUNT_STM32_ADC_SMP_TIME));
+}
+
+static void currsmp_shunt_stm32_start(const struct device *dev)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+ struct currsmp_shunt_stm32_data *data = dev->data;
+
+ /* calibrate a, b, c offset */
+ LL_ADC_ClearFlag_EOS(config->adc);
+
+ data->i_a_offset = adc_read(dev, config->adc_ch_a);
+ data->i_b_offset = adc_read(dev, config->adc_ch_b);
+ data->i_c_offset = adc_read(dev, config->adc_ch_c);
+
+ /* start injected conversions (triggered by sv-pwm) */
+ LL_ADC_ClearFlag_JEOS(config->adc);
+ LL_ADC_INJ_StartConversion(config->adc);
+}
+
+static void currsmp_shunt_stm32_stop(const struct device *dev)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+
+ LL_ADC_INJ_StopConversion(config->adc);
+ while (LL_ADC_INJ_IsStopConversionOngoing(config->adc) != 0U)
+ ;
+
+ while (LL_ADC_INJ_IsConversionOngoing(config->adc) != 0U)
+ ;
+}
+
+static void currsmp_shunt_stm32_pause(const struct device *dev)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+
+ LL_ADC_DisableIT_JEOS(config->adc);
+}
+
+static void currsmp_shunt_stm32_resume(const struct device *dev)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+
+ LL_ADC_EnableIT_JEOS(config->adc);
+}
+
+static const struct currsmp_driver_api currsmp_shunt_stm32_driver_api = {
+ .configure = currsmp_shunt_stm32_configure,
+ .get_currents = currsmp_shunt_stm32_get_currents,
+ .set_sector = currsmp_shunt_stm32_set_sector,
+ .get_smp_time = currsmp_shunt_stm32_get_smp_time,
+ .start = currsmp_shunt_stm32_start,
+ .stop = currsmp_shunt_stm32_stop,
+ .pause = currsmp_shunt_stm32_pause,
+ .resume = currsmp_shunt_stm32_resume,
+};
+
+/*******************************************************************************
+ * Initialization
+ ******************************************************************************/
+
+static int currsmp_shunt_stm32_init(const struct device *dev)
+{
+ const struct currsmp_shunt_stm32_config *config = dev->config;
+ struct currsmp_shunt_stm32_data *data = dev->data;
+
+ int ret;
+
+ /* configure pinmux */
+ ret = stm32_dt_pinctrl_configure(config->pinctrl, config->pinctrl_len,
+ (uint32_t)config->adc);
+ if (ret < 0) {
+ LOG_ERR("pinctrl setup failed (%d)", ret);
+ return ret;
+ }
+
+ /* configure ADC */
+ ret = adc_configure(dev);
+ if (ret < 0) {
+ return ret;
+ }
+
+ /* pre-compute ADC injected sequences */
+ data->jsqr[0] = adc_calc_jsqr(config->adc_trigger, config->adc_ch_b,
+ config->adc_ch_c);
+ data->jsqr[1] = adc_calc_jsqr(config->adc_trigger, config->adc_ch_a,
+ config->adc_ch_c);
+ data->jsqr[2] = adc_calc_jsqr(config->adc_trigger, config->adc_ch_b,
+ config->adc_ch_a);
+
+ return 0;
+}
+
+static const struct soc_gpio_pinctrl adc_pins[] = ST_STM32_DT_INST_PINCTRL(0, 0);
+
+static const struct currsmp_shunt_stm32_config currsmp_shunt_stm32_config = {
+ .adc = (ADC_TypeDef *)DT_REG_ADDR(DT_PARENT(DT_DRV_INST(0))),
+ .pclken = {
+ .bus = DT_CLOCKS_CELL(DT_PARENT(DT_DRV_INST(0)), bus),
+ .enr = DT_CLOCKS_CELL(DT_PARENT(DT_DRV_INST(0)), bits)
+ },
+ .adc_irq = DT_IRQ_BY_IDX(DT_PARENT(DT_DRV_INST(0)), 0, irq),
+ .adc_ch_a = __LL_ADC_DECIMAL_NB_TO_CHANNEL(
+ DT_INST_PROP_BY_IDX(0, adc_channels, 0)),
+ .adc_ch_b = __LL_ADC_DECIMAL_NB_TO_CHANNEL(
+ DT_INST_PROP_BY_IDX(0, adc_channels, 1)),
+ .adc_ch_c = __LL_ADC_DECIMAL_NB_TO_CHANNEL(
+ DT_INST_PROP_BY_IDX(0, adc_channels, 2)),
+ .adc_trigger = DT_INST_PROP(0, adc_trigger),
+ .pinctrl = adc_pins,
+ .pinctrl_len = ARRAY_SIZE(adc_pins),
+};
+
+static struct currsmp_shunt_stm32_data currsmp_shunt_stm32_data;
+
+DEVICE_DT_INST_DEFINE(0, &currsmp_shunt_stm32_init, NULL,
+ &currsmp_shunt_stm32_data, &currsmp_shunt_stm32_config,
+ POST_KERNEL, CONFIG_SPINNER_CURRSMP_INIT_PRIORITY,
+ &currsmp_shunt_stm32_driver_api);
diff --git a/drivers/feedback/CMakeLists.txt b/drivers/feedback/CMakeLists.txt
new file mode 100755
index 0000000..9b9dc0d
--- /dev/null
+++ b/drivers/feedback/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_library()
+zephyr_library_sources_ifdef(CONFIG_SPINNER_FEEDBACK_HALLS_STM32 halls_stm32.c)
+
diff --git a/drivers/feedback/Kconfig b/drivers/feedback/Kconfig
new file mode 100644
index 0000000..e78df67
--- /dev/null
+++ b/drivers/feedback/Kconfig
@@ -0,0 +1,17 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+menuconfig SPINNER_FEEDBACK
+ bool "Feedback Drivers"
+ help
+ Enable options for feedback drivers.
+
+if SPINNER_FEEDBACK
+
+module = SPINNER_FEEDBACK
+module-str = SPINNER_FEEDBACK
+source "subsys/logging/Kconfig.template.log_config"
+
+rsource "Kconfig.stm32"
+
+endif # SPINNER_FEEDBACK
diff --git a/drivers/feedback/Kconfig.stm32 b/drivers/feedback/Kconfig.stm32
new file mode 100644
index 0000000..832a274
--- /dev/null
+++ b/drivers/feedback/Kconfig.stm32
@@ -0,0 +1,10 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+config SPINNER_FEEDBACK_HALLS_STM32
+ bool "STM32 halls feedback driver"
+ default y if SOC_FAMILY_STM32
+ select USE_STM32_LL_TIM
+ help
+ Enable halls driver for STM32 SoCs
+
diff --git a/drivers/feedback/halls_stm32.c b/drivers/feedback/halls_stm32.c
new file mode 100755
index 0000000..18137e8
--- /dev/null
+++ b/drivers/feedback/halls_stm32.c
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT st_stm32_halls
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#include
+LOG_MODULE_REGISTER(halls_stm32, CONFIG_SPINNER_FEEDBACK_LOG_LEVEL);
+
+/*******************************************************************************
+ * Private
+ ******************************************************************************/
+
+struct halls_stm32_config {
+ TIM_TypeDef *timer;
+ struct stm32_pclken pclken;
+ struct gpio_dt_spec h1;
+ struct gpio_dt_spec h2;
+ struct gpio_dt_spec h3;
+ uint32_t irq;
+ uint32_t phase_shift;
+ const struct soc_gpio_pinctrl *pinctrl;
+ size_t pinctrl_len;
+};
+
+struct halls_stm32_data {
+ uint16_t eangle;
+ uint8_t last_state;
+ uint32_t tfreq;
+ int32_t raw_speed;
+};
+
+static uint8_t halls_stm32_get_state(const struct device *dev)
+{
+ const struct halls_stm32_config *config = dev->config;
+
+ return (uint8_t)gpio_pin_get_raw(config->h3.port, config->h3.pin) << 2U |
+ (uint8_t)gpio_pin_get_raw(config->h2.port, config->h2.pin) << 1U |
+ (uint8_t)gpio_pin_get_raw(config->h1.port, config->h1.pin);
+}
+
+ISR_DIRECT_DECLARE(timer_irq)
+{
+ const struct device *dev = DEVICE_DT_INST_GET(0);
+ const struct halls_stm32_config *config = dev->config;
+ struct halls_stm32_data *data = dev->data;
+
+ uint8_t curr_state;
+ uint16_t eangle = 0U;
+ int8_t direction = 1;
+
+ if (LL_TIM_IsActiveFlag_CC1(config->timer) == 0U) {
+ return 0;
+ }
+
+ LL_TIM_ClearFlag_CC1(config->timer);
+
+ curr_state = halls_stm32_get_state(dev);
+
+ switch (curr_state) {
+ case 5U:
+ if (data->last_state == 4U) {
+ eangle = 0;
+ } else if (data->last_state == 1U) {
+ eangle = 60;
+ direction = -1;
+ }
+
+ break;
+ case 1U:
+ if (data->last_state == 5U) {
+ eangle = 60;
+ } else if (data->last_state == 3U) {
+ eangle = 120;
+ direction = -1;
+ }
+ break;
+ case 3U:
+ if (data->last_state == 1U) {
+ eangle = 120;
+ } else if (data->last_state == 2U) {
+ eangle = 180;
+ direction = -1;
+ }
+ break;
+ case 2U:
+ if (data->last_state == 3U) {
+ eangle = 180;
+ } else if (data->last_state == 6U) {
+ eangle = 240;
+ direction = -1;
+ }
+ break;
+ case 6U:
+ if (data->last_state == 2U) {
+ eangle = 240;
+ } else if (data->last_state == 4U) {
+ eangle = 300;
+ direction = -1;
+ }
+ break;
+ case 4U:
+ if (data->last_state == 6U) {
+ eangle = 300;
+ } else if (data->last_state == 5U) {
+ eangle = 0;
+ direction = -1;
+ }
+ break;
+ default:
+ __ASSERT(NULL, "Unexpected halls state: %d", curr_state);
+ return 0;
+ }
+
+ eangle += config->phase_shift;
+
+ data->eangle = eangle;
+ data->last_state = curr_state;
+ data->raw_speed = direction * (int32_t)LL_TIM_IC_GetCaptureCH1(config->timer);
+
+ return 0;
+}
+
+/*******************************************************************************
+ * API
+ ******************************************************************************/
+
+static float halls_stm32_get_eangle(const struct device *dev)
+{
+ struct halls_stm32_data *data = dev->data;
+
+ return (float)data->eangle;
+}
+
+static float halls_stm32_get_speed(const struct device *dev)
+{
+ struct halls_stm32_data *data = dev->data;
+
+ return (float)(data->tfreq / data->raw_speed / 6UL);
+}
+
+static const struct feedback_driver_api halls_stm32_driver_api = {
+ .get_eangle = halls_stm32_get_eangle,
+ .get_speed = halls_stm32_get_speed
+};
+
+/*******************************************************************************
+ * Init
+ ******************************************************************************/
+
+static int halls_stm32_init(const struct device *dev)
+{
+ const struct halls_stm32_config *config = dev->config;
+ struct halls_stm32_data *data = dev->data;
+
+ int ret;
+ const struct device *clk;
+ LL_TIM_InitTypeDef init;
+ LL_TIM_ENCODER_InitTypeDef enc_init;
+ uint8_t curr_state;
+
+ /* configure pinmux */
+ ret = stm32_dt_pinctrl_configure(config->pinctrl, config->pinctrl_len,
+ (uint32_t)config->timer);
+ if (ret < 0) {
+ LOG_ERR("pinctrl setup failed (%d)", ret);
+ return ret;
+ }
+
+ /* enable timer clock */
+ clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
+
+ ret = clock_control_on(clk, (clock_control_subsys_t *)&config->pclken);
+ if (ret < 0) {
+ LOG_ERR("Could not turn on timer clock (%d)", ret);
+ return ret;
+ }
+
+ /* initialize timer */
+ LL_TIM_StructInit(&init);
+ if (LL_TIM_Init(config->timer, &init) != SUCCESS) {
+ LOG_ERR("Could not initialize timer");
+ return -EIO;
+ }
+
+ /* configure encoder (halls) mode */
+ LL_TIM_SetClockSource(config->timer, LL_TIM_CLOCKSOURCE_INTERNAL);
+ LL_TIM_IC_EnableXORCombination(config->timer);
+ LL_TIM_SetTriggerInput(config->timer, LL_TIM_TS_TI1F_ED);
+
+ LL_TIM_ENCODER_StructInit(&enc_init);
+ enc_init.IC1ActiveInput = LL_TIM_ACTIVEINPUT_TRC;
+ if (LL_TIM_ENCODER_Init(config->timer, &enc_init) != SUCCESS) {
+ LOG_ERR("Could not initialize encoder mode");
+ return -EIO;
+ }
+
+ /* configure CC unit and timer update source */
+ LL_TIM_SetUpdateSource(config->timer, LL_TIM_UPDATESOURCE_COUNTER);
+ LL_TIM_CC_EnableChannel(config->timer, LL_TIM_CHANNEL_CH1);
+ LL_TIM_EnableIT_CC1(config->timer);
+
+ /* store timer frequency (used for speed calculations) */
+ ret = stm32_tim_clk_get(&config->pclken, &data->tfreq);
+ if (ret < 0) {
+ return ret;
+ }
+
+ /* check H1/H2/H3 GPIO readiness */
+ if (!device_is_ready(config->h1.port) ||
+ !device_is_ready(config->h2.port) ||
+ !device_is_ready(config->h3.port)) {
+ LOG_ERR("H1/H2/H3 GPIO device/s not ready");
+ return -ENODEV;
+ }
+
+ /* initialize electrical angle */
+ curr_state = halls_stm32_get_state(dev);
+ switch (curr_state) {
+ case 5U:
+ data->eangle = 0U;
+ break;
+ case 1U:
+ data->eangle = 60U;
+ break;
+ case 3U:
+ data->eangle = 120U;
+ break;
+ case 2U:
+ data->eangle = 180U;
+ break;
+ case 6U:
+ data->eangle = 240U;
+ break;
+ case 4U:
+ data->eangle = 300U;
+ break;
+ default:
+ break;
+ }
+
+ data->eangle += config->phase_shift;
+ data->last_state = curr_state;
+
+ /* connect and enable timer IRQ */
+ IRQ_DIRECT_CONNECT(
+ DT_IRQ_BY_NAME(DT_PARENT(DT_DRV_INST(0)), global, irq),
+ DT_IRQ_BY_NAME(DT_PARENT(DT_DRV_INST(0)), global, priority),
+ timer_irq, 0);
+ irq_enable(config->irq);
+
+ return 0;
+}
+
+static const struct soc_gpio_pinctrl halls_pins[] = ST_STM32_DT_INST_PINCTRL(0, 0);
+
+static const struct halls_stm32_config halls_stm32_config = {
+ .timer = (TIM_TypeDef *)DT_REG_ADDR(DT_PARENT(DT_DRV_INST(0))),
+ .pclken = {
+ .bus = DT_CLOCKS_CELL(DT_PARENT(DT_DRV_INST(0)), bus),
+ .enr = DT_CLOCKS_CELL(DT_PARENT(DT_DRV_INST(0)), bits)
+ },
+ .h1 = GPIO_DT_SPEC_INST_GET(0, h1_gpios),
+ .h2 = GPIO_DT_SPEC_INST_GET(0, h2_gpios),
+ .h3 = GPIO_DT_SPEC_INST_GET(0, h3_gpios),
+ .irq = DT_IRQ_BY_NAME(DT_PARENT(DT_DRV_INST(0)), global, irq),
+ .phase_shift = DT_INST_PROP(0, phase_shift),
+ .pinctrl = halls_pins,
+ .pinctrl_len = ARRAY_SIZE(halls_pins),
+};
+
+static struct halls_stm32_data halls_stm32_data;
+
+DEVICE_DT_INST_DEFINE(0, &halls_stm32_init, NULL,
+ &halls_stm32_data, &halls_stm32_config, POST_KERNEL,
+ CONFIG_KERNEL_INIT_PRIORITY_DEVICE,
+ &halls_stm32_driver_api);
diff --git a/drivers/svpwm/CMakeLists.txt b/drivers/svpwm/CMakeLists.txt
new file mode 100755
index 0000000..0512414
--- /dev/null
+++ b/drivers/svpwm/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_library()
+zephyr_library_sources_ifdef(CONFIG_SPINNER_SVPWM_STM32 svpwm_stm32.c)
+
diff --git a/drivers/svpwm/Kconfig b/drivers/svpwm/Kconfig
new file mode 100644
index 0000000..722eece
--- /dev/null
+++ b/drivers/svpwm/Kconfig
@@ -0,0 +1,23 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+menuconfig SPINNER_SVPWM
+ bool "SV-PWM Drivers"
+ help
+ Enable options for SV-PWM drivers.
+
+if SPINNER_SVPWM
+
+module = SPINNER_SVPWM
+module-str = SPINNER_SVPWM
+source "subsys/logging/Kconfig.template.log_config"
+
+config SPINNER_SVPWM_INIT_PRIORITY
+ int "SV-PWM init priority"
+ default 90
+ help
+ SV-PWM initialization priority.
+
+rsource "Kconfig.stm32"
+
+endif # SPINNER_SVPWM
diff --git a/drivers/svpwm/Kconfig.stm32 b/drivers/svpwm/Kconfig.stm32
new file mode 100755
index 0000000..453b1c6
--- /dev/null
+++ b/drivers/svpwm/Kconfig.stm32
@@ -0,0 +1,19 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+config SPINNER_SVPWM_STM32
+ bool "STM32 SV-PWM driver"
+ default y if SOC_FAMILY_STM32
+ select USE_STM32_LL_TIM
+ select SPINNER_SVM
+ select SPINNER_UTILS_STM32
+ help
+ Enable SV-PWM driver for STM32 SoCs
+
+config SPINNER_SVPWM_STM32_PWM_FREQ
+ int "PWM frequency"
+ default 30000
+ depends on SPINNER_SVPWM_STM32
+ help
+ PWM frequency (Hz)
+
diff --git a/drivers/svpwm/svpwm_stm32.c b/drivers/svpwm/svpwm_stm32.c
new file mode 100755
index 0000000..da33cb6
--- /dev/null
+++ b/drivers/svpwm/svpwm_stm32.c
@@ -0,0 +1,312 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT st_stm32_svpwm
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+LOG_MODULE_REGISTER(svpwm_stm32, CONFIG_SPINNER_SVPWM_LOG_LEVEL);
+
+/*******************************************************************************
+ * Private
+ ******************************************************************************/
+
+struct svpwm_stm32_config {
+ TIM_TypeDef *timer;
+ struct stm32_pclken pclken;
+ bool enable_comp_outputs;
+ uint32_t t_dead;
+ uint32_t t_rise;
+ const struct device *currsmp;
+ const struct gpio_dt_spec *enable;
+ size_t enable_len;
+ const struct soc_gpio_pinctrl *pinctrl;
+ size_t pinctrl_len;
+};
+
+struct svpwm_stm32_data {
+ uint32_t period;
+ svm_t svm;
+};
+
+/*******************************************************************************
+ * API
+ ******************************************************************************/
+
+static void svpwm_stm32_start(const struct device *dev)
+{
+ const struct svpwm_stm32_config *config = dev->config;
+ struct svpwm_stm32_data *data = dev->data;
+
+ svm_init(&data->svm);
+ data->svm.sector = 5U;
+ currsmp_set_sector(config->currsmp, data->svm.sector);
+
+ /* activate enable pins if available */
+ for (size_t i = 0U; i < config->enable_len; i++) {
+ gpio_pin_set(config->enable[i].port, config->enable[i].pin, 1);
+ }
+
+ /* configure timer OC for a, b, c */
+ LL_TIM_OC_SetCompareCH1(config->timer, data->period / 2U);
+ LL_TIM_OC_SetCompareCH2(config->timer, data->period / 2U);
+ LL_TIM_OC_SetCompareCH3(config->timer, data->period / 2U);
+
+ LL_TIM_CC_EnableChannel(config->timer, LL_TIM_CHANNEL_CH1);
+ LL_TIM_CC_EnableChannel(config->timer, LL_TIM_CHANNEL_CH2);
+ LL_TIM_CC_EnableChannel(config->timer, LL_TIM_CHANNEL_CH3);
+ if (config->enable_comp_outputs) {
+ LL_TIM_CC_EnableChannel(config->timer, LL_TIM_CHANNEL_CH1N);
+ LL_TIM_CC_EnableChannel(config->timer, LL_TIM_CHANNEL_CH2N);
+ LL_TIM_CC_EnableChannel(config->timer, LL_TIM_CHANNEL_CH3N);
+ }
+
+ /* configure timer OC for ADC trigger */
+ LL_TIM_CC_EnableChannel(config->timer, LL_TIM_CHANNEL_CH4);
+
+ /* start timer */
+ LL_TIM_EnableAllOutputs(config->timer);
+
+ LL_TIM_EnableCounter(config->timer);
+}
+
+static void svpwm_stm32_stop(const struct device *dev)
+{
+ const struct svpwm_stm32_config *config = dev->config;
+
+ /* stop timer */
+ LL_TIM_DisableCounter(config->timer);
+
+ LL_TIM_DisableAllOutputs(config->timer);
+
+ LL_TIM_CC_DisableChannel(config->timer, LL_TIM_CHANNEL_CH1);
+ LL_TIM_CC_DisableChannel(config->timer, LL_TIM_CHANNEL_CH2);
+ LL_TIM_CC_DisableChannel(config->timer, LL_TIM_CHANNEL_CH3);
+ if (config->enable_comp_outputs) {
+ LL_TIM_CC_DisableChannel(config->timer, LL_TIM_CHANNEL_CH1N);
+ LL_TIM_CC_DisableChannel(config->timer, LL_TIM_CHANNEL_CH2N);
+ LL_TIM_CC_DisableChannel(config->timer, LL_TIM_CHANNEL_CH3N);
+ }
+
+ LL_TIM_CC_DisableChannel(config->timer, LL_TIM_CHANNEL_CH4);
+
+ /* deactivate enable pins if available */
+ for (size_t i = 0U; i < config->enable_len; i++) {
+ gpio_pin_set(config->enable[i].port, config->enable[i].pin, 0);
+ }
+}
+
+static void svpwm_stm32_set_phase_voltages(const struct device *dev,
+ float v_alpha, float v_beta)
+{
+ const struct svpwm_stm32_config *config = dev->config;
+ struct svpwm_stm32_data *data = dev->data;
+
+ const svm_duties_t *duties = &data->svm.duties;
+
+ /* space-vector modulation */
+ svm_set(&data->svm, v_alpha, v_beta);
+
+ /* program duties */
+ LL_TIM_OC_SetCompareCH1(config->timer,
+ (uint32_t)(data->period * duties->a));
+ LL_TIM_OC_SetCompareCH2(config->timer,
+ (uint32_t)(data->period * duties->b));
+ LL_TIM_OC_SetCompareCH3(config->timer,
+ (uint32_t)(data->period * duties->c));
+
+ /* inform current sampling device about current sector */
+ currsmp_set_sector(config->currsmp, data->svm.sector);
+}
+
+static const struct svpwm_driver_api svpwm_stm32_driver_api = {
+ .start = svpwm_stm32_start,
+ .stop = svpwm_stm32_stop,
+ .set_phase_voltages = svpwm_stm32_set_phase_voltages,
+};
+
+/*******************************************************************************
+ * Initialization
+ ******************************************************************************/
+
+static int svpwm_stm32_init(const struct device *dev)
+{
+ const struct svpwm_stm32_config *config = dev->config;
+ struct svpwm_stm32_data *data = dev->data;
+
+ int ret;
+ uint32_t freq;
+ uint16_t psc;
+ const struct device *clk;
+ LL_TIM_InitTypeDef tim_init;
+ LL_TIM_OC_InitTypeDef tim_ocinit;
+ LL_TIM_BDTR_InitTypeDef brk_dt_init;
+
+ if (!device_is_ready(config->currsmp)) {
+ LOG_ERR("Current sampling device not ready");
+ return -ENODEV;
+ }
+
+ /* configure pinmux */
+ ret = stm32_dt_pinctrl_configure(config->pinctrl, config->pinctrl_len,
+ (uint32_t)config->timer);
+ if (ret < 0) {
+ LOG_ERR("pinctrl setup failed (%d)", ret);
+ return ret;
+ }
+
+ clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
+
+ /* enable timer clock */
+ ret = clock_control_on(clk, (clock_control_subsys_t *)&config->pclken);
+ if (ret < 0) {
+ LOG_ERR("Could not turn on timer clock (%d)", ret);
+ return ret;
+ }
+
+ /* compute ARR */
+ ret = stm32_tim_clk_get(&config->pclken, &freq);
+ if (ret < 0) {
+ return ret;
+ }
+
+ psc = 0U;
+ do {
+ data->period = __LL_TIM_CALC_ARR(
+ freq, psc, CONFIG_SPINNER_SVPWM_STM32_PWM_FREQ * 2U);
+ psc++;
+ } while (data->period > UINT16_MAX);
+
+ /* initialize timer
+ * NOTE: repetition counter set to 1, update will happen on underflow
+ */
+ LL_TIM_StructInit(&tim_init);
+ tim_init.CounterMode = LL_TIM_COUNTERMODE_CENTER_UP;
+ tim_init.Autoreload = data->period;
+ tim_init.RepetitionCounter = 1U;
+ if (LL_TIM_Init(config->timer, &tim_init) != SUCCESS) {
+ LOG_ERR("Could not initialize timer");
+ return -EIO;
+ }
+
+ /* initialize OC for a, b, c channels */
+ LL_TIM_OC_StructInit(&tim_ocinit);
+ tim_ocinit.OCMode = LL_TIM_OCMODE_PWM1;
+ tim_ocinit.CompareValue = data->period / 2U;
+
+ if (LL_TIM_OC_Init(config->timer, LL_TIM_CHANNEL_CH1, &tim_ocinit) != SUCCESS) {
+ LOG_ERR("Could not initialize timer OC for channel 1");
+ return -EIO;
+ }
+
+ if (LL_TIM_OC_Init(config->timer, LL_TIM_CHANNEL_CH2, &tim_ocinit) != SUCCESS) {
+ LOG_ERR("Could not initialize timer OC for channel 2");
+ return -EIO;
+ }
+
+ if (LL_TIM_OC_Init(config->timer, LL_TIM_CHANNEL_CH3, &tim_ocinit) != SUCCESS) {
+ LOG_ERR("Could not initialize timer OC for channel 3");
+ return -EIO;
+ }
+
+ /* initialize OC for ADC trigger channel */
+ tim_ocinit.OCMode = LL_TIM_OCMODE_PWM2;
+ tim_ocinit.CompareValue = data->period - 1U;
+ if (LL_TIM_OC_Init(config->timer, LL_TIM_CHANNEL_CH4, &tim_ocinit) != SUCCESS) {
+ LOG_ERR("Could not initialize timer OC for channel 4");
+ return -EIO;
+ }
+
+ LL_TIM_SetTriggerOutput(config->timer, LL_TIM_TRGO_OC4REF);
+
+ /* enable pre-load on all OC channels */
+ LL_TIM_OC_EnablePreload(config->timer, LL_TIM_CHANNEL_CH1);
+ LL_TIM_OC_EnablePreload(config->timer, LL_TIM_CHANNEL_CH2);
+ LL_TIM_OC_EnablePreload(config->timer, LL_TIM_CHANNEL_CH3);
+ LL_TIM_OC_EnablePreload(config->timer, LL_TIM_CHANNEL_CH4);
+
+ /* configure ADC sampling point (middle of the period) */
+ LL_TIM_OC_SetCompareCH4(config->timer, data->period - 1U);
+
+ /* setup break and dead-time if available */
+ LL_TIM_BDTR_StructInit(&brk_dt_init);
+ brk_dt_init.OSSRState = LL_TIM_OSSR_ENABLE;
+ brk_dt_init.OSSIState = LL_TIM_OSSI_ENABLE;
+ brk_dt_init.LockLevel = LL_TIM_LOCKLEVEL_1;
+ /* TODO: add support for dead-time */
+ brk_dt_init.DeadTime = 0U;
+ brk_dt_init.BreakState = LL_TIM_BREAK_ENABLE;
+ brk_dt_init.BreakPolarity = LL_TIM_BREAK_POLARITY_HIGH;
+ brk_dt_init.Break2State = LL_TIM_BREAK2_ENABLE;
+ if (LL_TIM_BDTR_Init(config->timer, &brk_dt_init) != SUCCESS) {
+ LOG_ERR("Could not initialize timer break");
+ return -EIO;
+ }
+
+ /* initialize enable GPIOs */
+ for (size_t i = 0U; i < config->enable_len; i++) {
+ const struct gpio_dt_spec *enable_gpio = &config->enable[i];
+
+ if (!device_is_ready(enable_gpio->port)) {
+ LOG_ERR("Enable GPIO not ready");
+ return -ENODEV;
+ }
+
+ ret = gpio_pin_configure_dt(enable_gpio, GPIO_OUTPUT_INACTIVE);
+ if (ret < 0) {
+ LOG_ERR("Could not configure enable GPIO");
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static const struct soc_gpio_pinctrl svpwm_pins[] = ST_STM32_DT_INST_PINCTRL(0, 0);
+
+#define ENABLE_GPIOS_ELEM(idx, _) \
+ GPIO_DT_SPEC_INST_GET_BY_IDX(0, enable_gpios, idx),
+
+static const struct gpio_dt_spec enable_pins[] = {
+ COND_CODE_1(
+ DT_INST_NODE_HAS_PROP(0, enable_gpios),
+ (UTIL_LISTIFY(DT_INST_PROP_LEN(0, enable_gpios), ENABLE_GPIOS_ELEM)),
+ ()
+ )
+};
+
+static const struct svpwm_stm32_config svpwm_stm32_config = {
+ .timer = (TIM_TypeDef *)DT_REG_ADDR(DT_PARENT(DT_DRV_INST(0))),
+ .pclken = {
+ .bus = DT_CLOCKS_CELL(DT_PARENT(DT_DRV_INST(0)), bus),
+ .enr = DT_CLOCKS_CELL(DT_PARENT(DT_DRV_INST(0)), bits)
+ },
+ .enable_comp_outputs = DT_INST_PROP_OR(0, enable_comp_outputs, false),
+ .t_dead = DT_INST_PROP_OR(0, t_dead_ns, 0),
+ .t_rise = DT_INST_PROP_OR(0, t_rise_ns, 0),
+ .currsmp = DEVICE_DT_GET(DT_INST_PHANDLE(0, currsmp)),
+ .enable = enable_pins,
+ .enable_len = ARRAY_SIZE(enable_pins),
+ .pinctrl = svpwm_pins,
+ .pinctrl_len = ARRAY_SIZE(svpwm_pins),
+};
+
+static struct svpwm_stm32_data svpwm_stm32_data;
+
+DEVICE_DT_INST_DEFINE(0, &svpwm_stm32_init, NULL,
+ &svpwm_stm32_data, &svpwm_stm32_config, POST_KERNEL,
+ CONFIG_SPINNER_SVPWM_INIT_PRIORITY,
+ &svpwm_stm32_driver_api);
diff --git a/dts/bindings/currsmp/st,stm32-currsmp-shunt.yaml b/dts/bindings/currsmp/st,stm32-currsmp-shunt.yaml
new file mode 100644
index 0000000..0ed7940
--- /dev/null
+++ b/dts/bindings/currsmp/st,stm32-currsmp-shunt.yaml
@@ -0,0 +1,47 @@
+# Copyright (c) 2021, Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+description: |
+ STM32 shunt current sampling driver.
+
+ The shunt current sampling device is expected to be a children of any STM32
+ ADC supporting injected conversions. Example usage:
+
+ &adc1 {
+ currsmp: currsmp {
+ compatible = "st,stm32-currsmp-shunt";
+ pinctrl-0 = <&adc1_in1_pa0 &adc1_in7_pc1 &adc1_in6_pc0>;
+
+ adc-channels = <1 7 6>;
+ adc-trigger = ;
+ };
+ };
+
+compatible: "st,stm32-currsmp-shunt"
+
+include: base.yaml
+
+properties:
+ pinctrl-0:
+ type: phandles
+ required: false
+ description: |
+ Pin configuration for ADC signal/s. We expect that the phandles will
+ reference pinctrl nodes, e.g.
+
+ pinctrl-0 = <&adc1_in1_pa0 &adc1_in7_pc1 ...>;
+
+ adc-channels:
+ type: array
+ required: true
+ description: |
+ ADC channels (a, b, c).
+
+ adc-trigger:
+ type: int
+ required: true
+ description: |
+ External trigger for the injected ADC conversions. The external trigger
+ must be an output of the timer used for SV-PWM.
+
+ Definitions available at dts-bindings/adc/stm32fxxx.h files.
diff --git a/dts/bindings/feedback/st,stm32-halls.yaml b/dts/bindings/feedback/st,stm32-halls.yaml
new file mode 100644
index 0000000..dbfefcf
--- /dev/null
+++ b/dts/bindings/feedback/st,stm32-halls.yaml
@@ -0,0 +1,61 @@
+# Copyright (c) 2021, Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+description: |
+ Halls sensor driver for STM32 microcontrollers.
+
+ The halls device is expected to be a children of any STM32 timer supporting
+ the HALLS interface. Example usage:
+
+ &timers2 {
+ status = "okay";
+
+ feedback: feedback {
+ compatible = "st,stm32-halls";
+
+ pinctrl-0 = <&tim2_ch1_pa15 &tim2_ch2_pb3 &tim2_ch3_pb10>;
+
+ h1-gpios = <&gpioa 15 0>;
+ h2-gpios = <&gpiob 3 0>;
+ h3-gpios = <&gpiob 10 0>;
+ phase-shift = <60>;
+ };
+ };
+
+compatible: "st,stm32-halls"
+
+include: base.yaml
+
+properties:
+ pinctrl-0:
+ type: phandles
+ required: false
+ description: |
+ GPIO pin configuration for HALLS signal/s. We expect that the phandles
+ will reference pinctrl nodes, e.g.
+ pinctrl-0 = <&tim2_ch1_pa15 &tim2_ch2_pb3 &tim2_ch3_pb10>;
+
+ h1-gpios:
+ type: phandle-array
+ required: true
+ description: |
+ H1 GPIO
+
+ h2-gpios:
+ type: phandle-array
+ required: true
+ description: |
+ H2 GPIO
+
+ h3-gpios:
+ type: phandle-array
+ required: true
+ description: |
+ H3 GPIO
+
+ phase-shift:
+ type: int
+ default: 0
+ description: |
+ Phase shift between the low to high transition of signal H1 and the
+ maximum of the Bemf induced on phase A.
diff --git a/dts/bindings/svpwm/st,stm32-svpwm.yaml b/dts/bindings/svpwm/st,stm32-svpwm.yaml
new file mode 100644
index 0000000..6a55a44
--- /dev/null
+++ b/dts/bindings/svpwm/st,stm32-svpwm.yaml
@@ -0,0 +1,60 @@
+# Copyright (c) 2021, Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+description: |
+ STM32 SV-PWM device.
+
+ The SV-PWM device is expected to be a children of any STM32 advanced control
+ timer. Example usage:
+
+ &timers1 {
+ svpwm: svpwm {
+ compatible = "st,stm32-svpwm";
+ pinctrl-0 = <&tim1_ch1_pa8 &tim1_ch2_pa9 &tim1_ch3_pa10 &tim1_ocp_pa11>;
+ ...
+ };
+ };
+
+compatible: "st,stm32-svpwm"
+
+include: base.yaml
+
+properties:
+ pinctrl-0:
+ type: phandles
+ required: false
+ description: |
+ GPIO pin configuration for inverter signal/s. We expect that the phandles
+ will reference pinctrl nodes, e.g.
+ pinctrl-0 = <&tim1_ch1_pa8 &tim1_ch2_pa9 ...>;
+
+ enable-comp-outputs:
+ type: boolean
+ description: |
+ Enable complementary outputs, used to control the low side channels of
+ each inverter leg.
+
+ t-dead-ns:
+ type: int
+ required: false
+ description: |
+ Dead time in nanoseconds. If using an integrated controller, i.e. without
+ complementary PWM signals, it still needs to be provided to configure
+ accurate current measurements.
+
+ t-rise-ns:
+ type: int
+ required: false
+ description: |
+ Rise time in nanoseconds.
+
+ currsmp:
+ type: phandle
+ required: true
+ description: |
+ Current sampling device.
+
+ enable-gpios:
+ type: phandle-array
+ description: |
+ Channel enable GPIOS (a, b, c).
diff --git a/include/dts-bindings/adc/stm32f3xx.h b/include/dts-bindings/adc/stm32f3xx.h
new file mode 100644
index 0000000..32b79fc
--- /dev/null
+++ b/include/dts-bindings/adc/stm32f3xx.h
@@ -0,0 +1,31 @@
+/**
+ * @file
+ *
+ * DT definitions for STM32 ADC (F3XX series)
+ *
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef _DTS_BINDINGS_INVERTER_STM32F3XX_H_
+#define _DTS_BINDINGS_INVERTER_STM32F3XX_H_
+
+/* Ref. RM0365, Rev. 8, Table 88. */
+
+#define _STM32_ADC_JEXT_POS 2U
+
+#define STM32_ADC_INJ_TRIG_TIM1_TRGO (0U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM1_CC4 (1U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM2_TRGO (2U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM2_CC1 (3U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM3_CC4 (4U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM4_TRGO (5U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_EXTI15 (6U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM1_TRGO2 (8U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM3_CC3 (11U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM3_TRGO (12U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM3_CC1 (13U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM6_TRGO (14U << _STM32_ADC_JEXT_POS)
+#define STM32_ADC_INJ_TRIG_TIM15_TRGO (15U << _STM32_ADC_JEXT_POS)
+
+#endif /* _DTS_BINDINGS_INVERTER_STM32F3XX_H_ */
diff --git a/include/spinner/control/cloop.h b/include/spinner/control/cloop.h
new file mode 100755
index 0000000..d7df4a6
--- /dev/null
+++ b/include/spinner/control/cloop.h
@@ -0,0 +1,45 @@
+/**
+ * @file
+ *
+ * Current loop API.
+ *
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef _SPINNER_LIB_CONTROL_CLOOP_H_
+#define _SPINNER_LIB_CONTROL_CLOOP_H_
+
+/**
+ * @defgroup spinner_lib_control Control APIs
+ * @{
+ * @}
+ */
+
+/**
+ * @defgroup spinner_lib_control_cloop Current Loop API
+ * @ingroup spinner_lib_control
+ * @{
+ */
+
+/**
+ * @brief Start current loop.
+ */
+void cloop_start(void);
+
+/**
+ * @brief Stop current loop.
+ */
+void cloop_stop(void);
+
+/**
+ * @brief Set current loop working point.
+ *
+ * @param[in] i_d i_d current value.
+ * @param[in] i_q i_q current value.
+ */
+void cloop_set_ref(float i_d, float i_q);
+
+/** @} */
+
+#endif /* _SPINNER_LIB_CONTROL_CLOOP_H_ */
diff --git a/include/spinner/drivers/currsmp.h b/include/spinner/drivers/currsmp.h
new file mode 100644
index 0000000..f31a513
--- /dev/null
+++ b/include/spinner/drivers/currsmp.h
@@ -0,0 +1,175 @@
+/**
+ * @file
+ *
+ * Current Sampling API.
+ *
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef _SPINNER_DRIVERS_CURRSMP_H_
+#define _SPINNER_DRIVERS_CURRSMP_H_
+
+#include
+#include
+
+/**
+ * @defgroup spinner_drivers Driver APIs
+ * @{
+ * @}
+ */
+
+/**
+ * @defgroup spinner_drivers_currsmp Current Sampling API
+ * @ingroup spinner_drivers
+ * @{
+ */
+
+/** @brief Current sampling regulation callback. */
+typedef void (*currsmp_regulation_cb_t)(void *ctx);
+
+/** @brief Current sampling currents. */
+struct currsmp_curr {
+ /** Phase a current. */
+ float i_a;
+ /** Phase b current. */
+ float i_b;
+ /** Phase c current. */
+ float i_c;
+};
+
+/** @cond INTERNAL_HIDDEN */
+
+struct currsmp_driver_api {
+ void (*configure)(const struct device *dev,
+ currsmp_regulation_cb_t regulation_cb, void *ctx);
+ void (*get_currents)(const struct device *dev,
+ struct currsmp_curr *curr);
+ void (*set_sector)(const struct device *dev, uint8_t sector);
+ uint32_t (*get_smp_time)(const struct device *dev);
+ void (*start)(const struct device *dev);
+ void (*stop)(const struct device *dev);
+ void (*pause)(const struct device *dev);
+ void (*resume)(const struct device *dev);
+};
+
+/** @endcond */
+
+/**
+ * @brief Configure current sampling device.
+ *
+ * @note This function needs to be called before calling currsmp_start().
+ *
+ * @param[in] dev Current sampling device.
+ * @param[in] regulation_cb Callback called on each regulation cycle.
+ * @param[in] ctx Callback context.
+ */
+static inline void currsmp_configure(const struct device *dev,
+ currsmp_regulation_cb_t regulation_cb,
+ void *ctx)
+{
+ const struct currsmp_driver_api *api = dev->api;
+
+ api->configure(dev, regulation_cb, ctx);
+}
+
+/**
+ * @brief Get phase currents.
+ *
+ * @param[in] dev Current sampling device.
+ * @param[out] curr Pointer where phase currents will be stored.
+ */
+static inline void currsmp_get_currents(const struct device *dev,
+ struct currsmp_curr *curr)
+{
+ const struct currsmp_driver_api *api = dev->api;
+
+ api->get_currents(dev, curr);
+}
+
+/**
+ * @brief Set SV-PWM sector.
+ *
+ * @param[in] dev Current sampling device.
+ * @param[in] sector SV-PWM sector.
+ */
+static inline void currsmp_set_sector(const struct device *dev, uint8_t sector)
+{
+ const struct currsmp_driver_api *api = dev->api;
+
+ api->set_sector(dev, sector);
+}
+
+/**
+ * @brief Obtain currents sampling time in nanoseconds.
+ *
+ * @param[in] dev Current sampling device.
+ *
+ * @return Sampling time in nanoseconds (zero indicates error).
+ */
+static inline uint32_t currsmp_get_smp_time(const struct device *dev)
+{
+ const struct currsmp_driver_api *api = dev->api;
+
+ return api->get_smp_time(dev);
+}
+
+/**
+ * @brief Start sampling currents.
+ *
+ * @param[in] dev Current sampling device.
+ *
+ * @see currsmp_stop()
+ */
+static inline void currsmp_start(const struct device *dev)
+{
+ const struct currsmp_driver_api *api = dev->api;
+
+ api->start(dev);
+}
+
+/**
+ * @brief Stop sampling currents.
+ *
+ * @param[in] dev Current sampling device.
+ */
+static inline void currsmp_stop(const struct device *dev)
+{
+ const struct currsmp_driver_api *api = dev->api;
+
+ api->stop(dev);
+}
+
+/**
+ * @brief Pause current sampling.
+ *
+ * @note This function can be used to prevent current sampling
+ * to call the regulation callback, thus allowing to adjust
+ * shared context.
+ *
+ * @param dev Current sampling device.
+ *
+ * @see currsmp_resume()
+ */
+static inline void currsmp_pause(const struct device *dev)
+{
+ const struct currsmp_driver_api *api = dev->api;
+
+ api->pause(dev);
+}
+
+/**
+ * @brief Resume current sampling.
+ *
+ * @param dev Current sampling device.
+ */
+static inline void currsmp_resume(const struct device *dev)
+{
+ const struct currsmp_driver_api *api = dev->api;
+
+ api->resume(dev);
+}
+
+/** @} */
+
+#endif /* _SPINNER_DRIVERS_CURRSMP_H_ */
diff --git a/include/spinner/drivers/feedback.h b/include/spinner/drivers/feedback.h
new file mode 100644
index 0000000..07a4e44
--- /dev/null
+++ b/include/spinner/drivers/feedback.h
@@ -0,0 +1,59 @@
+/**
+ * @file
+ *
+ * Feedback API.
+ *
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef _SPINNER_DRIVERS_FEEDBACK_H_
+#define _SPINNER_DRIVERS_FEEDBACK_H_
+
+#include
+#include
+
+/**
+ * @defgroup spinner_drivers_feedback Feedback API
+ * @ingroup spinner_drivers
+ * @{
+ */
+
+/** @cond INTERNAL_HIDDEN */
+
+struct feedback_driver_api {
+ float (*get_eangle)(const struct device *dev);
+ float (*get_speed)(const struct device *dev);
+};
+
+/** @endcond */
+
+/**
+ * @brief Get electrical angle.
+ *
+ * @param dev Feedback instance.
+ * @return Electrical angle.
+ */
+static inline float feedback_get_eangle(const struct device *dev)
+{
+ const struct feedback_driver_api *api = dev->api;
+
+ return api->get_eangle(dev);
+}
+
+/**
+ * @brief Get speed.
+ *
+ * @param dev Feedback instance.
+ * @return Speed.
+ */
+static inline float feedback_get_speed(const struct device *dev)
+{
+ const struct feedback_driver_api *api = dev->api;
+
+ return api->get_speed(dev);
+}
+
+/** @} */
+
+#endif /* _SPINNER_DRIVERS_FEEDBACK_H_ */
diff --git a/include/spinner/drivers/svpwm.h b/include/spinner/drivers/svpwm.h
new file mode 100644
index 0000000..bfd0765
--- /dev/null
+++ b/include/spinner/drivers/svpwm.h
@@ -0,0 +1,77 @@
+/**
+ * @file
+ *
+ * SV-PWM API.
+ *
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef _SPINNER_DRIVERS_SVPWM_H_
+#define _SPINNER_DRIVERS_SVPWM_H_
+
+#include
+#include
+
+/**
+ * @defgroup spinner_drivers_svpwm SV-PWM API
+ * @ingroup spinner_drivers
+ * @{
+ */
+
+/** @cond INTERNAL_HIDDEN */
+
+struct svpwm_driver_api {
+ void (*start)(const struct device *dev);
+ void (*stop)(const struct device *dev);
+ void (*set_phase_voltages)(const struct device *dev, float v_alpha,
+ float v_beta);
+};
+
+/** @endcond */
+
+/**
+ * @brief Start the SV-PWM controller.
+ *
+ * @note Current sampling device must be started prior to SV-PWM, since SV-PWM
+ * is the responsible to trigger current sampling measurements.
+ *
+ * @param[in] dev SV-PWM device.
+ */
+static inline void svpwm_start(const struct device *dev)
+{
+ const struct svpwm_driver_api *api = dev->api;
+
+ api->start(dev);
+}
+
+/**
+ * @brief Stop the SV-PWM controller.
+ *
+ * @param[in] dev SV-PWM device.
+ */
+static inline void svpwm_stop(const struct device *dev)
+{
+ const struct svpwm_driver_api *api = dev->api;
+
+ api->stop(dev);
+}
+
+/**
+ * @brief Set phase voltages.
+ *
+ * @param[in] dev SV-PWM device.
+ * @param[in] v_alpha Alpha voltage.
+ * @param[in] v_beta Beta voltage.
+ */
+static inline void svpwm_set_phase_voltages(const struct device *dev,
+ float v_alpha, float v_beta)
+{
+ const struct svpwm_driver_api *api = dev->api;
+
+ api->set_phase_voltages(dev, v_alpha, v_beta);
+}
+
+/** @} */
+
+#endif /* _SPINNER_DRIVERS_SVPWM_H_ */
diff --git a/include/spinner/svm/svm.h b/include/spinner/svm/svm.h
new file mode 100755
index 0000000..b96d6bf
--- /dev/null
+++ b/include/spinner/svm/svm.h
@@ -0,0 +1,62 @@
+/**
+ * @file
+ *
+ * Space Vector Modulation (SVM).
+ *
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef _SPINNER_LIB_SVM_SVM_H_
+#define _SPINNER_LIB_SVM_SVM_H_
+
+#include
+
+/**
+ * @defgroup spinner_control_svm Space Vector Modulation (SVM) API
+ * @{
+ */
+
+/** @brief SVM duty cycles. */
+typedef struct {
+ /** A channel duty cycle. */
+ float a;
+ /** B channel duty cycle. */
+ float b;
+ /** C channel duty cycle. */
+ float c;
+ /** Maximum duty cycle of a, b, c. */
+ float max;
+} svm_duties_t;
+
+/** @brief SVM state. */
+typedef struct svm {
+ /** SVM sector. */
+ uint8_t sector;
+ /** Duty cycles. */
+ svm_duties_t duties;
+ /** Minimum allowed duty cycle. */
+ float d_min;
+ /** Maximum allowed duty cycle. */
+ float d_max;
+} svm_t;
+
+/**
+ * @brief Initialize SVM.
+ *
+ * @param[in] svm SVM instance.
+ */
+void svm_init(svm_t *svm);
+
+/**
+ * @brief Set v_alpha and v_beta.
+ *
+ * @param[in] svm SVM instance.
+ * @param[in] va v_alpha value.
+ * @param[in] vb v_beta value.
+ */
+void svm_set(svm_t *svm, float va, float vb);
+
+/** @} */
+
+#endif /* _SPINNER_LIB_SVM_SVM_H_ */
diff --git a/include/spinner/utils/stm32_adc.h b/include/spinner/utils/stm32_adc.h
new file mode 100644
index 0000000..7bbdad6
--- /dev/null
+++ b/include/spinner/utils/stm32_adc.h
@@ -0,0 +1,76 @@
+/**
+ * @file
+ *
+ * STM32 ADC Utilities.
+ *
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef _SPINNER_LIB_UTILS_STM32_ADC_H_
+#define _SPINNER_LIB_UTILS_STM32_ADC_H_
+
+#include
+
+#include
+
+/**
+ * @defgroup spinner_lib_utils Utility APIs
+ * @{
+ * @}
+ */
+
+/**
+ * @defgroup spinner_utils_stm32_adc STM32 ADC Utilities
+ * @ingroup spinner_lib_utils
+ * @{
+ */
+
+/**
+ * @brief Obtain the RES fields in the ADC CFGR register given the sampling
+ * resolution in bits.
+ *
+ * @param[in] res_bits Sampling resolution in bits.
+ * @param[out] res Obtained RES register value.
+ * @return int 0 on success, -ENOTSUP if fiven bit resolution is not supported.
+ */
+int stm32_adc_res_get(uint8_t res_bits, uint32_t *res);
+
+/**
+ * @brief Obtain the value of the SMP fields in the ADC SMPR register given
+ * sampling time in cycles.
+ *
+ * @note For decimal sampling times, @p smp_time must be rounded up, e.g. 19.5
+ * cycles should be provided as 20.
+ *
+ * @param[in] smp_time ADC sampling time in cycles
+ * @param[out] smp Obtained SMP register value.
+ * @return int 0 on success, -ENOTSUP if given sampling time is not supported.
+ */
+int stm32_adc_smp_get(uint32_t smp_time, uint32_t *smp);
+
+/**
+ * Obtain ADC clock rate.
+ *
+ * @param[in] adc ADC instance.
+ * @param[in] pclken ADC clock control subsystem.
+ * @param[out] clk Where ADC clock (in Hz) will be stored.
+ *
+ * @return 0 on success, error code otherwise.
+ */
+int stm32_adc_clk_get(ADC_TypeDef *adc, const struct stm32_pclken *pclken,
+ uint32_t *clk);
+
+/**
+ * Obtain ADC clock SAR time.
+ *
+ * @param[in] res_bits Sampling resolution in bits.
+ * @param[out] t_sar Where computed SAR time (in cycles) time will be stored.
+ *
+ * @return 0 on success, error code otherwise.
+ */
+int stm32_adc_t_sar_get(uint8_t res_bits, float *t_sar);
+
+/** @} */
+
+#endif /* _SPINNER_LIB_UTILS_STM32_ADC_H_ */
diff --git a/include/spinner/utils/stm32_tim.h b/include/spinner/utils/stm32_tim.h
new file mode 100644
index 0000000..7ad1dc5
--- /dev/null
+++ b/include/spinner/utils/stm32_tim.h
@@ -0,0 +1,33 @@
+/**
+ * @file
+ *
+ * STM32 Timer Utilities.
+ *
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef _SPINNER_LIB_UTILS_STM32_TIM_H_
+#define _SPINNER_LIB_UTILS_STM32_TIM_H_
+
+#include
+
+/**
+ * @defgroup spinner_utils_stm32_tim STM32 Timer Utilities
+ * @ingroup spinner_lib_utils
+ * @{
+ */
+
+/**
+ * Obtain timer clock speed.
+ *
+ * @param[in] pclken Timer clock control subsystem.
+ * @param[out] tim_clk Where computed timer clock will be stored.
+ *
+ * @return 0 on success, error code otherwise.
+ */
+int stm32_tim_clk_get(const struct stm32_pclken *pclken, uint32_t *tim_clk);
+
+/** @} */
+
+#endif /* _SPINNER_LIB_UTILS_STM32_TIM_H_ */
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
new file mode 100644
index 0000000..0873c18
--- /dev/null
+++ b/lib/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+add_subdirectory(control)
+add_subdirectory(svm)
+add_subdirectory(utils)
diff --git a/lib/Kconfig b/lib/Kconfig
new file mode 100644
index 0000000..a0e26d9
--- /dev/null
+++ b/lib/Kconfig
@@ -0,0 +1,10 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+menu "Libraries"
+
+rsource "control/Kconfig"
+rsource "svm/Kconfig"
+rsource "utils/Kconfig"
+
+endmenu
diff --git a/lib/control/CMakeLists.txt b/lib/control/CMakeLists.txt
new file mode 100644
index 0000000..0d00986
--- /dev/null
+++ b/lib/control/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+if(CONFIG_SPINNER_CLOOP)
+ zephyr_library()
+ zephyr_library_sources(cloop.c)
+ zephyr_library_sources_ifdef(CONFIG_SPINNER_CLOOP_SHELL cloop_shell.c)
+endif()
+
diff --git a/lib/control/Kconfig b/lib/control/Kconfig
new file mode 100644
index 0000000..fa76f3c
--- /dev/null
+++ b/lib/control/Kconfig
@@ -0,0 +1,46 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+menuconfig SPINNER_CLOOP
+ bool "Current Loop"
+ select SPINNER_SVM
+ select CMSIS_DSP
+ select CMSIS_DSP_CONTROLLER
+ select CMSIS_DSP_TABLES_ARM_SIN_COS_F32
+ depends on SPINNER_CURRSMP && SPINNER_FEEDBACK && SPINNER_SVPWM
+
+if SPINNER_CLOOP
+
+config SPINNER_CLOOP_SHELL
+ bool "Control loop shell"
+ default y
+ depends on SHELL
+ help
+ Utility shell to test current loop.
+
+config SPINNER_CLOOP_T_KP
+ int "Torque PID proportional constant"
+ default 1500
+ help
+ Torque PID controller proportional (Kp) constant. Value is in thousands.
+
+config SPINNER_CLOOP_T_KI
+ int "Torque PID integral constant"
+ default 0
+ help
+ Torque PID controller Integral (Ki) constant. Value is in thousands.
+
+config SPINNER_CLOOP_F_KP
+ int "Flux PID proportional constant"
+ default 1500
+ help
+ Flux PID controller proportional (Kp) constant. Value is in thousands.
+
+config SPINNER_CLOOP_F_KI
+ int "Flux PID integral constant"
+ default 0
+ help
+ Flux PID controller integral (Ki) constant. Value is in thousands.
+
+endif # SPINNER_CLOOP
+
diff --git a/lib/control/cloop.c b/lib/control/cloop.c
new file mode 100755
index 0000000..de2c65a
--- /dev/null
+++ b/lib/control/cloop.c
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+#include
+
+#include
+#include
+#include
+
+struct cloop {
+ const struct device *currsmp;
+ const struct device *feedback;
+ const struct device *svpwm;
+ arm_pid_instance_f32 pid_i_q;
+ arm_pid_instance_f32 pid_i_d;
+ float i_q_ref;
+ float i_d_ref;
+};
+
+static struct cloop cloop;
+
+/**
+ * @brief Current regulation callback.
+ *
+ * This function is called after current sampling is completed.
+ *
+ * @warning It is called from the highest priority IRQ.
+ */
+static void regulate(void *ctx)
+{
+ struct currsmp_curr curr;
+ float eangle, sin_eangle, cos_eangle;
+ float i_alpha, i_beta;
+ float i_q, i_d;
+ float v_q, v_d;
+ float v_alpha, v_beta;
+
+ ARG_UNUSED(ctx);
+
+ currsmp_get_currents(cloop.currsmp, &curr);
+ eangle = feedback_get_eangle(cloop.feedback);
+ arm_sin_cos_f32(eangle, &sin_eangle, &cos_eangle);
+
+ /* i_a, i_b -> i_alpha, i_beta */
+ arm_clarke_f32(curr.i_a, curr.i_b, &i_alpha, &i_beta);
+ /* i_alpha, i_beta -> i_q, i_d */
+ arm_park_f32(i_alpha, i_beta, &i_d, &i_q, sin_eangle, cos_eangle);
+
+ /* PI (i_q, i_d -> v_q, v_d) */
+ v_q = arm_pid_f32(&cloop.pid_i_q, cloop.i_q_ref - i_q);
+ v_d = arm_pid_f32(&cloop.pid_i_d, cloop.i_d_ref - i_d);
+
+ /* v_q, v_d -> v_alpha, v_beta */
+ arm_inv_park_f32(v_d, v_q, &v_alpha, &v_beta, sin_eangle, cos_eangle);
+ svpwm_set_phase_voltages(cloop.svpwm, v_alpha, v_beta);
+}
+
+static int cloop_init(const struct device *dev)
+{
+ ARG_UNUSED(dev);
+
+ cloop.currsmp = DEVICE_DT_GET(DT_NODELABEL(currsmp));
+ cloop.svpwm = DEVICE_DT_GET(DT_NODELABEL(svpwm));
+ cloop.feedback = DEVICE_DT_GET(DT_NODELABEL(feedback));
+
+ cloop.i_q_ref = 0.0f;
+ cloop.i_d_ref = 0.0f;
+
+ cloop.pid_i_q.Kp = CONFIG_SPINNER_CLOOP_T_KP / 1000.0f;
+ cloop.pid_i_q.Ki = CONFIG_SPINNER_CLOOP_T_KI / 1000.0f;
+ cloop.pid_i_q.Kd = 0.0f;
+ arm_pid_init_f32(&cloop.pid_i_q, 1);
+
+ cloop.pid_i_d.Kp = CONFIG_SPINNER_CLOOP_F_KP / 1000.0f;
+ cloop.pid_i_d.Ki = CONFIG_SPINNER_CLOOP_F_KI / 1000.0f;
+ cloop.pid_i_d.Kd = 0.0f;
+ arm_pid_init_f32(&cloop.pid_i_d, 1);
+
+ currsmp_configure(cloop.currsmp, regulate, NULL);
+
+ return 0;
+}
+
+SYS_INIT(cloop_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
+
+/*******************************************************************************
+ * Public
+ ******************************************************************************/
+
+void cloop_start(void)
+{
+ arm_pid_reset_f32(&cloop.pid_i_q);
+ arm_pid_reset_f32(&cloop.pid_i_d);
+
+ currsmp_start(cloop.currsmp);
+ svpwm_start(cloop.svpwm);
+}
+
+void cloop_stop(void)
+{
+ svpwm_stop(cloop.svpwm);
+ currsmp_stop(cloop.currsmp);
+}
+
+void cloop_set_ref(float i_d, float i_q)
+{
+ currsmp_pause(cloop.currsmp);
+ cloop.i_d_ref = i_d;
+ cloop.i_q_ref = i_q;
+ currsmp_resume(cloop.currsmp);
+}
diff --git a/lib/control/cloop_shell.c b/lib/control/cloop_shell.c
new file mode 100644
index 0000000..6f14173
--- /dev/null
+++ b/lib/control/cloop_shell.c
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+
+#include
+
+#include
+
+static int cmd_cloop_start(const struct shell *shell, size_t argc, char **argv)
+{
+ ARG_UNUSED(shell);
+ ARG_UNUSED(argc);
+ ARG_UNUSED(argv);
+
+ cloop_start();
+
+ return 0;
+}
+
+static int cmd_cloop_stop(const struct shell *shell, size_t argc, char **argv)
+{
+ ARG_UNUSED(shell);
+ ARG_UNUSED(argc);
+ ARG_UNUSED(argv);
+
+ cloop_stop();
+
+ return 0;
+}
+
+static int cmd_cloop_set(const struct shell *shell, size_t argc, char **argv)
+{
+ if (argc != 2) {
+ shell_help(shell);
+ return -EINVAL;
+ }
+
+ /* NOTE: i_d = 0, assuming PMSM */
+ cloop_set_ref(0.0f, strtof(argv[1], NULL));
+
+ return 0;
+}
+
+SHELL_STATIC_SUBCMD_SET_CREATE(
+ sub_cloop,
+ SHELL_CMD(start, NULL, "Start current regulation loop", cmd_cloop_start),
+ SHELL_CMD(stop, NULL, "Stop current regulation loop", cmd_cloop_stop),
+ SHELL_CMD(set, NULL, "Set current regulation loop target", cmd_cloop_set),
+ SHELL_SUBCMD_SET_END
+);
+
+SHELL_CMD_REGISTER(cloop, &sub_cloop, "Current Loop Control", NULL);
diff --git a/lib/svm/CMakeLists.txt b/lib/svm/CMakeLists.txt
new file mode 100644
index 0000000..50655b4
--- /dev/null
+++ b/lib/svm/CMakeLists.txt
@@ -0,0 +1,8 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+if(CONFIG_SPINNER_SVM)
+ zephyr_library()
+ zephyr_library_sources(svm.c)
+endif()
+
diff --git a/lib/svm/Kconfig b/lib/svm/Kconfig
new file mode 100644
index 0000000..09e8303
--- /dev/null
+++ b/lib/svm/Kconfig
@@ -0,0 +1,10 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+config SPINNER_SVM
+ bool "Space Vector Modulator"
+ select CMSIS_DSP
+ select CMSIS_DSP_FASTMATH
+ help
+ Space Vector Modulator.
+
diff --git a/lib/svm/svm.c b/lib/svm/svm.c
new file mode 100755
index 0000000..3b183c0
--- /dev/null
+++ b/lib/svm/svm.c
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+
+#include
+
+/*******************************************************************************
+ * Private
+ ******************************************************************************/
+
+/** Value sqrt(3). */
+#define SQRT_3 1.7320508075688773f
+
+/**
+ * @brief Clip a value.
+ *
+ * @param[in] value Value to be clipped.
+ * @param[in] min Minimum value.
+ * @param[in] max Maximum value.
+ *
+ * @return Clipped value.
+ */
+static inline float clip(float value, float min, float max)
+{
+ if (value < min)
+ return min;
+
+ if (value > max)
+ return max;
+
+ return value;
+}
+
+/**
+ * @brief Obtain sector based on a, b, c vector values.
+ *
+ * @param[in] a a component value.
+ * @param[in] b b component value.
+ * @param[in] c c component value.
+
+ * @return Sector (1...6).
+ */
+static uint8_t get_sector(float a, float b, float c)
+{
+ uint8_t sector = 0u;
+
+ if (c <= 0) {
+ if (a <= 0) {
+ sector = 2u;
+ } else {
+ if (b <= 0) {
+ sector = 6u;
+ } else {
+ sector = 1u;
+ }
+ }
+ } else {
+ if (a <= 0) {
+ if (b <= 0) {
+ sector = 4u;
+ } else {
+ sector = 3u;
+ }
+ } else {
+ sector = 5u;
+ }
+ }
+
+ return sector;
+}
+
+/*******************************************************************************
+ * Public
+ ******************************************************************************/
+
+void svm_init(svm_t *svm)
+{
+ svm->sector = 0u;
+
+ svm->duties.a = 0.0f;
+ svm->duties.b = 0.0f;
+ svm->duties.c = 0.0f;
+ svm->duties.max = 0.0f;
+
+ svm->d_min = 0.0f;
+ svm->d_max = 1.0f;
+}
+
+void svm_set(svm_t *svm, float va, float vb)
+{
+ float a, b, c, mod;
+ float x, y, z;
+
+ /* limit maximum amplitude to avoid distortions */
+ (void)arm_sqrt_f32(va * va + vb * vb, &mod);
+ if (mod > SQRT_3 / 2.0f) {
+ va = va / mod * (SQRT_3 / 2.0f);
+ vb = vb / mod * (SQRT_3 / 2.0f);
+ }
+
+ a = va - 1.0f / SQRT_3 * vb;
+ b = 2.0f / SQRT_3 * vb;
+ c = -(a + b);
+
+ svm->sector = get_sector(a, b, c);
+
+ switch (svm->sector) {
+ case 1u:
+ x = a;
+ y = b;
+ z = 1.0f - (x + y);
+
+ svm->duties.a = x + y + z * 0.5f;
+ svm->duties.b = y + z * 0.5f;
+ svm->duties.c = z * 0.5f;
+
+ svm->duties.max = svm->duties.a;
+ break;
+
+ case 2u:
+ x = -c;
+ y = -a;
+ z = 1.0f - (x + y);
+
+ svm->duties.a = x + z * 0.5f;
+ svm->duties.b = x + y + z * 0.5f;
+ svm->duties.c = z * 0.5f;
+
+ svm->duties.max = svm->duties.b;
+ break;
+
+ case 3u:
+ x = b;
+ y = c;
+ z = 1.0f - (x + y);
+
+ svm->duties.a = z * 0.5f;
+ svm->duties.b = x + y + z * 0.5f;
+ svm->duties.c = y + z * 0.5f;
+
+ svm->duties.max = svm->duties.b;
+
+ break;
+
+ case 4u:
+ x = -a;
+ y = -b;
+ z = 1.0f - (x + y);
+
+ svm->duties.a = z * 0.5f;
+ svm->duties.b = x + z * 0.5f;
+ svm->duties.c = x + y + z * 0.5f;
+
+ svm->duties.max = svm->duties.c;
+
+ break;
+
+ case 5u:
+ x = c;
+ y = a;
+ z = 1.0f - (x + y);
+
+ svm->duties.a = y + z * 0.5f;
+ svm->duties.b = z * 0.5f;
+ svm->duties.c = x + y + z * 0.5f;
+
+ svm->duties.max = svm->duties.c;
+
+ break;
+
+ case 6u:
+ x = -b;
+ y = -c;
+ z = 1.0f - (x + y);
+
+ svm->duties.a = x + y + z * 0.5f;
+ svm->duties.b = z * 0.5f;
+ svm->duties.c = x + z * 0.5f;
+
+ svm->duties.max = svm->duties.a;
+
+ break;
+
+ default:
+ break;
+ }
+
+ svm->duties.a = clip(svm->duties.a, svm->d_min, svm->d_max);
+ svm->duties.b = clip(svm->duties.b, svm->d_min, svm->d_max);
+ svm->duties.c = clip(svm->duties.c, svm->d_min, svm->d_max);
+
+ svm->duties.max = clip(svm->duties.max, svm->d_min, svm->d_max);
+}
diff --git a/lib/utils/CMakeLists.txt b/lib/utils/CMakeLists.txt
new file mode 100644
index 0000000..1564cd6
--- /dev/null
+++ b/lib/utils/CMakeLists.txt
@@ -0,0 +1,7 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+if (CONFIG_SPINNER_UTILS_STM32)
+ zephyr_library()
+ zephyr_library_sources(stm32_tim.c stm32_adc.c)
+endif()
diff --git a/lib/utils/Kconfig b/lib/utils/Kconfig
new file mode 100644
index 0000000..07205c5
--- /dev/null
+++ b/lib/utils/Kconfig
@@ -0,0 +1,5 @@
+config SPINNER_UTILS_STM32
+ bool "STM32 utilities"
+ select USE_STM32_LL_RCC
+ help
+ Enable common utilities for STM32 SoCs.
diff --git a/lib/utils/stm32_adc.c b/lib/utils/stm32_adc.c
new file mode 100644
index 0000000..e7d3a41
--- /dev/null
+++ b/lib/utils/stm32_adc.c
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+
+#include
+
+int stm32_adc_res_get(uint8_t res_bits, uint32_t *res)
+{
+ switch (res_bits) {
+#ifdef CONFIG_SOC_SERIES_STM32F3X
+ case 6U:
+ *res = LL_ADC_RESOLUTION_6B;
+ break;
+ case 8U:
+ *res = LL_ADC_RESOLUTION_8B;
+ break;
+ case 10U:
+ *res = LL_ADC_RESOLUTION_10B;
+ break;
+ case 12U:
+ *res = LL_ADC_RESOLUTION_12B;
+ break;
+#endif
+ default:
+ return -ENOTSUP;
+ }
+
+ return 0;
+}
+
+int stm32_adc_smp_get(uint32_t smp_time, uint32_t *smp)
+{
+ switch(smp_time) {
+#ifdef CONFIG_SOC_SERIES_STM32F3X
+ case 2U:
+ *smp = LL_ADC_SAMPLINGTIME_1CYCLE_5;
+ break;
+ case 3U:
+ *smp = LL_ADC_SAMPLINGTIME_2CYCLES_5;
+ break;
+ case 5U:
+ *smp = LL_ADC_SAMPLINGTIME_4CYCLES_5;
+ break;
+ case 8U:
+ *smp = LL_ADC_SAMPLINGTIME_7CYCLES_5;
+ break;
+ case 20U:
+ *smp = LL_ADC_SAMPLINGTIME_19CYCLES_5;
+ break;
+ case 62U:
+ *smp = LL_ADC_SAMPLINGTIME_61CYCLES_5;
+ break;
+ case 181U:
+ *smp = LL_ADC_SAMPLINGTIME_181CYCLES_5;
+ break;
+ case 602U:
+ *smp = LL_ADC_SAMPLINGTIME_601CYCLES_5;
+ break;
+#endif
+ default:
+ return -ENOTSUP;
+ }
+
+ return 0;
+}
+
+int stm32_adc_clk_get(ADC_TypeDef *adc, const struct stm32_pclken *pclken,
+ uint32_t *clk)
+{
+ const struct device *clk_dev;
+ int ret;
+ uint32_t div;
+
+ /* obtain ADC clock rate */
+ clk_dev = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
+ ret = clock_control_get_rate(clk_dev, (clock_control_subsys_t *)pclken,
+ clk);
+ if (ret < 0) {
+ return ret;
+ }
+
+ /* obtain divisor */
+ div = LL_ADC_GetCommonClock(__LL_ADC_COMMON_INSTANCE(adc));
+ switch (div) {
+ case LL_ADC_CLOCK_SYNC_PCLK_DIV1:
+ break;
+ case LL_ADC_CLOCK_SYNC_PCLK_DIV2:
+ *clk = *clk >> 1U;
+ break;
+ case LL_ADC_CLOCK_SYNC_PCLK_DIV4:
+ *clk = *clk >> 2U;
+ break;
+ default:
+ return -ENOTSUP;
+ }
+
+ return 0;
+}
+
+int stm32_adc_t_sar_get(uint8_t res_bits, float *t_sar)
+{
+ /* obtain t_sar (in clock cycles) */
+ switch (res_bits) {
+#ifdef CONFIG_SOC_SERIES_STM32F3X
+ case 6U:
+ *t_sar = 6.5f;
+ break;
+ case 8U:
+ *t_sar = 8.5f;
+ break;
+ case 10U:
+ *t_sar = 10.5f;
+ break;
+ case 12U:
+ *t_sar = 12.5f;
+ break;
+#endif
+ default:
+ return -ENOTSUP;
+ }
+
+ return 0;
+}
diff --git a/lib/utils/stm32_tim.c b/lib/utils/stm32_tim.c
new file mode 100644
index 0000000..a5f7f36
--- /dev/null
+++ b/lib/utils/stm32_tim.c
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+
+#include
+
+int stm32_tim_clk_get(const struct stm32_pclken *pclken, uint32_t *tim_clk)
+{
+ int ret;
+ const struct device *clk;
+ uint32_t bus_clk, apb_psc;
+
+ clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
+
+ ret = clock_control_get_rate(clk, (clock_control_subsys_t *)pclken,
+ &bus_clk);
+ if (ret < 0) {
+ return ret;
+ }
+
+ if (pclken->bus == STM32_CLOCK_BUS_APB1) {
+ apb_psc = STM32_APB1_PRESCALER;
+ }
+ else {
+ apb_psc = STM32_APB2_PRESCALER;
+ }
+
+ /*
+ * If the APB prescaler equals 1, the timer clock frequencies
+ * are set to the same frequency as that of the APB domain.
+ * Otherwise, they are set to twice (×2) the frequency of the
+ * APB domain.
+ */
+ if (apb_psc == 1U) {
+ *tim_clk = bus_clk;
+ } else {
+ *tim_clk = bus_clk * 2U;
+ }
+
+ return 0;
+}
diff --git a/spinner/CMakeLists.txt b/spinner/CMakeLists.txt
new file mode 100644
index 0000000..123ab42
--- /dev/null
+++ b/spinner/CMakeLists.txt
@@ -0,0 +1,20 @@
+#-------------------------------------------------------------------------------
+# Spinner
+#
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+cmake_minimum_required(VERSION 3.13.1)
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+
+project(spinner LANGUAGES C VERSION 1.0.0)
+
+#-------------------------------------------------------------------------------
+# Options
+
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+#-------------------------------------------------------------------------------
+# Application
+
+target_sources(app PRIVATE main.c)
diff --git a/spinner/Kconfig b/spinner/Kconfig
new file mode 100644
index 0000000..f3f39d2
--- /dev/null
+++ b/spinner/Kconfig
@@ -0,0 +1,4 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+source "Kconfig.zephyr"
diff --git a/spinner/debug.conf b/spinner/debug.conf
new file mode 100644
index 0000000..fee12de
--- /dev/null
+++ b/spinner/debug.conf
@@ -0,0 +1,12 @@
+# compiler
+CONFIG_DEBUG_OPTIMIZATIONS=y
+
+# console
+CONFIG_CONSOLE=y
+
+# UART console
+CONFIG_SERIAL=y
+CONFIG_UART_CONSOLE=y
+
+# logging
+CONFIG_LOG=y
diff --git a/spinner/main.c b/spinner/main.c
new file mode 100644
index 0000000..ce4c28d
--- /dev/null
+++ b/spinner/main.c
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2021 Teslabs Engineering S.L.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include
+
+#include
+
+void main(void)
+{
+ cloop_start();
+ cloop_set_ref(0.0f, 0.25f);
+}
diff --git a/spinner/prj.conf b/spinner/prj.conf
new file mode 100644
index 0000000..33bc8f7
--- /dev/null
+++ b/spinner/prj.conf
@@ -0,0 +1,10 @@
+# CMSIS-DSP requires newlib-libc
+CONFIG_NEWLIB_LIBC=y
+
+# drivers
+CONFIG_SPINNER_CURRSMP=y
+CONFIG_SPINNER_FEEDBACK=y
+CONFIG_SPINNER_SVPWM=y
+
+# lib
+CONFIG_SPINNER_CLOOP=y
diff --git a/spinner/shell.conf b/spinner/shell.conf
new file mode 100644
index 0000000..e3d95ac
--- /dev/null
+++ b/spinner/shell.conf
@@ -0,0 +1 @@
+CONFIG_SHELL=y
diff --git a/west.yml b/west.yml
new file mode 100644
index 0000000..5d766f5
--- /dev/null
+++ b/west.yml
@@ -0,0 +1,21 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+manifest:
+ self:
+ path: spinner
+
+ remotes:
+ - name: zephyr
+ url-base: https://github.com/zephyrproject-rtos
+
+ projects:
+ - name: zephyr
+ remote: zephyr
+ repo-path: zephyr
+ revision: v2.6.0
+ import:
+ path-whitelist:
+ - modules/hal/cmsis
+ - modules/hal/stm32
+
diff --git a/zephyr/module.yml b/zephyr/module.yml
new file mode 100644
index 0000000..05145fc
--- /dev/null
+++ b/zephyr/module.yml
@@ -0,0 +1,9 @@
+# Copyright (c) 2021 Teslabs Engineering S.L.
+# SPDX-License-Identifier: Apache-2.0
+
+build:
+ kconfig: Kconfig
+ cmake: .
+ settings:
+ board_root: .
+ dts_root: .