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 @@ +

+ Vertigo +

+

+ + CI status + + + Documentation + +

+ +# 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CurrentSensing + + + + + + + + + + + + + + Feedback + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SV-PWM + + + + + + + + + + + + + + + + + + + + + SV-PWM + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ClarkeTransform + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ParkTransform + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PI + + + + + + + + Inverse ParkTransform + + + + + + + + + + + + + + + + + + + + + + PI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CurrentSensing + + + + + + + + + + + + - + + + + + + - + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TPWM + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TPWM + + + + + + t + + + + + + _qa + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + qa + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _qb + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + qb + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _qc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + qc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + |iRa| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + |iRb| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + |iRc| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TNOISE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TRISE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DT + + + + + + + + + + + + + + + + + + + + + + + + + + + + TPWM - TA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TSAMPLE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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° + + + + + + Vd + + + + + + + + + + + + + + + + + + + + + + VsMAX + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TPWM + + + + + + qa + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + t + + + + + + + + + + + + + qb + + + + + + qc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + z0/2 + + + + + + z0/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 + + + + + + v0 + + + + + + v1 + + + + + + v0 + + + + + + v1 + + + + + + v3 + + + + + + v3 + + + + + + v7 + + + + + + + + + + + + + + z7 + + + + + + da + + + + + + db + + + + + + dc + + + + + + + + + + + + + + + + + + + 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: .