From 084135bd4c8b58a7d3a164e1feb5413e4d790de1 Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Sat, 20 Jan 2024 15:14:01 +0530 Subject: [PATCH 01/57] initial commit --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 842e3c27..6fe2179a 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -10,4 +10,6 @@ - LPF - State Filter -- Observer \ No newline at end of file +- Observer + +Anirudh will added content here... \ No newline at end of file From d2652bf706485cc4a9f7f0cc1a2675a4938b708c Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Thu, 6 Jun 2024 00:21:36 -0500 Subject: [PATCH 02/57] Add content --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 6fe2179a..62ec9dfc 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -1,5 +1,9 @@ # Encoder Feedback +## Background + +Encoders provide the rotor position feedback to the control system in a motor drive. This document describes a method to convert the raw readings of an encoder into meaningful position information which can be used by the contrl algorithm. Consequently, few techniques to derive the rotor speed from the measured position is also proposed. For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). + ## Calibration - Converting from raw counts to "theta" From 1f1b8a018a870e40c06da1ba0fe62faa58d3de58 Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Wed, 19 Jun 2024 00:41:43 -0500 Subject: [PATCH 03/57] Update docs --- .../control-with-amdc/encoder-fb/index.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 62ec9dfc..82d49e6e 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -6,6 +6,73 @@ Encoders provide the rotor position feedback to the control system in a motor dr ## Calibration +Incremental encoders which are typically used with AMDC have a fixed number of counts per revolution (for example, 1024) and is denoted by `CPR`. The user needs needs to convert the count into usable angular information which maybe used in the code. + +### Converting from raw counts to angle information + +As a first step, the user may use the AMDC `enc` driver function `encoder_get_position()` to get the count of the encoder reading. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft, if it is feasible. This document assumes a positive rotor angular position in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. Care must be taken by the user to ensure that angle is within the bounds of `0` and `2 $\pi$` by appropriately wrapping the variable. + +Example code for when encoder count is increasing with CCW rotation of shaft: +```C +double task_get_theta_m(void) +{ + // Get raw encoder position + uint32_t position; + encoder_get_position(&position); + + // Encoder count per revolution + ENCODER_COUNT_PER_REV = 1024; + + // Angular position to be computed + double theta_m_enc; + + enc_theta_m_offset = 100; + + // Convert to radians + theta_m_enc = (double) PI2 * ( ( (double)position - enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); + + // Wrapping to ensure within bounds + while (theta_m_enc < 0) { + theta_m_enc += PI2; + } + return theta_m_enc; +} +``` + +Example code for when encoder count is decreasing with CCW rotation of shaft: +```C +double task_get_theta_m(void) +{ + // Get raw encoder position + uint32_t position; + encoder_get_position(&position); + + // Encoder count per revolution + ENCODER_COUNT_PER_REV = 1024; + + // Angular position to be computed + double theta_m_enc; + + enc_theta_m_offset = 100; + + // Convert to radians + theta_m_enc = (double) PI2 * ( ( (double)ENCODER_COUNT_PER_REV - (double)1 -(double)position + enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); + + + // Wrapping to ensure within bounds + while (theta_m_enc > PI2) { + theta_m_enc -= PI2; + } + return theta_m_enc; +} +``` + +### Finding the offset + +The example code shown above makes uses of an encoder offset value. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the rotor d-axis is aligned with the phase U of the stator. + +To get the rotor to align with the phase U of stator the user may have some current flow through phase U and out of phase V and phase W. Alternately, the user may also inject some Id current with the AMDC injection function but with the rotot position set to 0. + - Converting from raw counts to "theta" - Direction: +/- depending on phase connections - Sync machines: dq offset From d81c6918248b94f70c630e9842c667fd56be75ad Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Mon, 24 Jun 2024 01:00:48 -0500 Subject: [PATCH 04/57] Update doc --- .../control-with-amdc/encoder-fb/index.md | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 82d49e6e..1a202210 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -10,9 +10,9 @@ Incremental encoders which are typically used with AMDC have a fixed number of c ### Converting from raw counts to angle information -As a first step, the user may use the AMDC `enc` driver function `encoder_get_position()` to get the count of the encoder reading. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft, if it is feasible. This document assumes a positive rotor angular position in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. Care must be taken by the user to ensure that angle is within the bounds of `0` and `2 $\pi$` by appropriately wrapping the variable. +As a first step, the user may use the AMDC `enc` driver function `encoder_get_position()` to get the count of the encoder reading. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and logging the position reported. This document assumes a positive rotor angular position in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. Care must be taken by the user to ensure that angle is within the bounds of 0 and 2 $\pi$ by appropriately wrapping the variable. -Example code for when encoder count is increasing with CCW rotation of shaft: +Example code to convert encoder to angular position in radians: ```C double task_get_theta_m(void) { @@ -20,58 +20,53 @@ double task_get_theta_m(void) uint32_t position; encoder_get_position(&position); - // Encoder count per revolution + int ENCODER_COUNT_PER_REV, CCW; + double enc_theta_m_offset; + + // User to set encoder count per revolution ENCODER_COUNT_PER_REV = 1024; + // Set 1 if encoder count increases with CCW rotation of shaft, Set 0 if encoder count increases with CW rotation of shaft + CCW = 1; + // Angular position to be computed double theta_m_enc; + // User to set encoder offset enc_theta_m_offset = 100; - // Convert to radians - theta_m_enc = (double) PI2 * ( ( (double)position - enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); - // Wrapping to ensure within bounds - while (theta_m_enc < 0) { - theta_m_enc += PI2; + // Convert to radians + if (CCW){ + theta_m_enc = (double) PI2 * ( ( (double)position - enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); } + else{ + theta_m_enc = (double) PI2 * ( ( (double)ENCODER_COUNT_PER_REV - (double)1 -(double)position + enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); + } + + // Mod by 2 pi + theta_m_enc = fmod(theta_m_enc,PI2 ); return theta_m_enc; } ``` -Example code for when encoder count is decreasing with CCW rotation of shaft: -```C -double task_get_theta_m(void) -{ - // Get raw encoder position - uint32_t position; - encoder_get_position(&position); - // Encoder count per revolution - ENCODER_COUNT_PER_REV = 1024; - - // Angular position to be computed - double theta_m_enc; - enc_theta_m_offset = 100; - - // Convert to radians - theta_m_enc = (double) PI2 * ( ( (double)ENCODER_COUNT_PER_REV - (double)1 -(double)position + enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); +### Finding the offset +The example code shown above makes uses of an encoder offset value. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the rotor d-axis is aligned with the phase U of the stator. - // Wrapping to ensure within bounds - while (theta_m_enc > PI2) { - theta_m_enc -= PI2; - } - return theta_m_enc; -} -``` +To get the rotor to align with the phase U of stator the user may have some current flow through phase U and out of phase V and phase W. Alternately, the user may also inject some current along the d-axis using the AMDC injection function but with the rotor position overridden by the user to 0. After obtaining the offset, the user needs to set the variable `enc_theta_m_offset` to the appropirate value in the `task_get_theta_m()` function. -### Finding the offset +The encoder offset maybe validated during operation of a permanent magnet synchrnous motor as follows: +1. Spin the motor up-to a steady speed under no load conditions +1. Measure the d-axis voltage commanded by the current regulator +1. Repeat the experiment for a few different motor speeds +1. Plot the d-axis voltage against the motor speed +1. The d-axis voltage if the offset is tuned correctly should be close to zero for all speeds. +1. In-case there is an error in the offset value, a significant speed dependent voltage will appear on the d-axis voltage -The example code shown above makes uses of an encoder offset value. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the rotor d-axis is aligned with the phase U of the stator. -To get the rotor to align with the phase U of stator the user may have some current flow through phase U and out of phase V and phase W. Alternately, the user may also inject some Id current with the AMDC injection function but with the rotot position set to 0. - Converting from raw counts to "theta" - Direction: +/- depending on phase connections @@ -79,6 +74,12 @@ To get the rotor to align with the phase U of stator the user may have some curr ## Computing Speed from Position +The user needs to compute a rotor speed signal from the obtained position signal to be used in the control algorithm. There are several ways to do this. A simple and straightforward way to do this would be to compute the discrete time derivative of the position signal in the controller as shown below. This can be referred to as $\Omega_{raw}$. + + +$\Omega_{raw}$ will be a choppy signal due to the derivative operation. A low pass filter may be applied to this signal as shown below to a filtered speed, $\Omega_{lpf}$. The user may select an appropriate bandwidth for the low pass filter. However, this signal will always be a lagging estimate of the actual rotor speed due to low pass filter characterstics. + +An observer which implements a mechanical model of the rotor as shown below will produce a no-lag estimate of the rotor speed. - LPF - State Filter - Observer From e0fbf68fe1ba721915a846f12e7ca7771aba5d6d Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Wed, 26 Jun 2024 00:27:27 -0500 Subject: [PATCH 05/57] Update docs --- .../control-with-amdc/encoder-fb/index.md | 47 +- .../resources/EncoderCodeBlockDiargam.svg | 257 +++++++ .../encoder-fb/resources/ObserverFigure.svg | 635 ++++++++++++++++++ 3 files changed, 920 insertions(+), 19 deletions(-) create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/ObserverFigure.svg diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 1a202210..13bcd9de 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -2,15 +2,17 @@ ## Background -Encoders provide the rotor position feedback to the control system in a motor drive. This document describes a method to convert the raw readings of an encoder into meaningful position information which can be used by the contrl algorithm. Consequently, few techniques to derive the rotor speed from the measured position is also proposed. For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). +Encoders provide rotor position feedback to the control system in a motor drive. This document describes a method to convert the raw readings of an encoder into meaningful position information which can be used by the contrl algorithm. Consequently, few techniques to derive the rotor speed from the measured position are also proposed. For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). ## Calibration -Incremental encoders which are typically used with AMDC have a fixed number of counts per revolution (for example, 1024) and is denoted by `CPR`. The user needs needs to convert the count into usable angular information which maybe used in the code. +Incremental encoders which are typically used with AMDC have a fixed number of counts per revolution (for example, 1024) and is denoted by `CPR`. The user needs needs to write some code to obtaine the encoder count at a given instance and tconvert the count into usable angular information which can be used within the control code. -### Converting from raw counts to angle information +### Obtaining encoder count and translating it into rotor position -As a first step, the user may use the AMDC `enc` driver function `encoder_get_position()` to get the count of the encoder reading. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and logging the position reported. This document assumes a positive rotor angular position in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. Care must be taken by the user to ensure that angle is within the bounds of 0 and 2 $\pi$ by appropriately wrapping the variable. + + +As a first step, the user may use the AMDC `enc` driver function `encoder_get_position()` to get the count of the encoder reading. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. This document follows the convention of a positive rotor angle in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. The user must ensure that angle is within the bounds of 0 and 2 $\pi$ by appropriately wrapping the variable using the `mod` function. Example code to convert encoder to angular position in radians: ```C @@ -41,7 +43,7 @@ double task_get_theta_m(void) theta_m_enc = (double) PI2 * ( ( (double)position - enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); } else{ - theta_m_enc = (double) PI2 * ( ( (double)ENCODER_COUNT_PER_REV - (double)1 -(double)position + enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); + theta_m_enc = (double) PI2 * ( ( (double)ENCODER_COUNT_PER_REV - (double)1 -(double)position + enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); } // Mod by 2 pi @@ -54,34 +56,41 @@ double task_get_theta_m(void) ### Finding the offset -The example code shown above makes uses of an encoder offset value. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the rotor d-axis is aligned with the phase U of the stator. +The example code shown above makes uses of an encoder offset value. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding of the stator. + +To get the rotor to align with the phase U winding, the user may have some current flow through phase U and out of phase V and phase W. This can done by using a DC supply, with phase U connected to the positive terminal and phases V and W connected to the negative terminal. Alternately, the user may also inject some current along the d-axis using the AMDC injection function, but with the rotor position overridden by the user to 0. After obtaining the offset, the user needs to set the variable `enc_theta_m_offset` to the appropirate value in the `task_get_theta_m()` function. -To get the rotor to align with the phase U of stator the user may have some current flow through phase U and out of phase V and phase W. Alternately, the user may also inject some current along the d-axis using the AMDC injection function but with the rotor position overridden by the user to 0. After obtaining the offset, the user needs to set the variable `enc_theta_m_offset` to the appropirate value in the `task_get_theta_m()` function. +To ensure that the obtained encoder offset is correct, the user may perform additional validation. For a permanent magnet synchronous motor, this can be done as follows: -The encoder offset maybe validated during operation of a permanent magnet synchrnous motor as follows: 1. Spin the motor up-to a steady speed under no load conditions 1. Measure the d-axis voltage commanded by the current regulator 1. Repeat the experiment for a few different motor speeds 1. Plot the d-axis voltage against the motor speed -1. The d-axis voltage if the offset is tuned correctly should be close to zero for all speeds. +1. The d-axis voltage if the offset is tuned correctly, should be close to zero for all speeds. 1. In-case there is an error in the offset value, a significant speed dependent voltage will appear on the d-axis voltage -- Converting from raw counts to "theta" -- Direction: +/- depending on phase connections -- Sync machines: dq offset - ## Computing Speed from Position The user needs to compute a rotor speed signal from the obtained position signal to be used in the control algorithm. There are several ways to do this. A simple and straightforward way to do this would be to compute the discrete time derivative of the position signal in the controller as shown below. This can be referred to as $\Omega_{raw}$. +$$ +\Omega_\text{raw}[k] = \frac{\theta_m[k] - \theta_m[k-1]}{T_s} +$$ + + +$\Omega_\text{raw}$ will be a choppy and noisy signal due to the derivative operation. A low pass filter may be applied to this signal as shown below to get a filtered speed, $\Omega_\text{lpf}$. The user may select an appropriate bandwidth, $\omega_b$ for the low pass filter to eliminate the noise introduced by the derivative operation. However, this signal will always be a lagging estimate of the actual rotor speed due to the characterstics of a low pass filter. + +$$ + \Omega_\text{lpf}[k] = \Omega_\text{raw}[k](1 - e^{\omega_b T_s}) + \Omega_\text{lpf}[k-1]e^{\omega_b T_s} +$$ +An observer which implements a mechanical model of the rotor as shown below will produce a no-lag estimate of the rotor speed, denoted by $\Omega_\text{sf}$. To implement an observer, the user needs to know the system parameters - `J` - the inertia of the rotor shaft and `b` - the damping coefficient of the rotor shaft. Further, to obtain a no-lag estimate it is necessary to provide the electromechanical torque, $T_{em}$ as input to the mechanical model. The `P-I` part of the observer closes the loop on the speed with $\Omega_\text{raw}$ being the reference input. The recommended tuning approach is as follows: -$\Omega_{raw}$ will be a choppy signal due to the derivative operation. A low pass filter may be applied to this signal as shown below to a filtered speed, $\Omega_{lpf}$. The user may select an appropriate bandwidth for the low pass filter. However, this signal will always be a lagging estimate of the actual rotor speed due to low pass filter characterstics. +$$ +K_p = \omega_{sf}b, K_i = \omega_{sf}J +$$ -An observer which implements a mechanical model of the rotor as shown below will produce a no-lag estimate of the rotor speed. -- LPF -- State Filter -- Observer +This tuning ensures a pole zero cancellation in the closed transfer function, resulting in a unity transfer function for speed tracking under ideal parameter estimates of `J` and `b` -Anirudh will added content here... \ No newline at end of file + diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg new file mode 100644 index 00000000..8e5f6b1a --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + Determineencoder counts per revolution + Determineencoder offset + Determinedirectionality of encoder count + task_get_theta_m() + Get encodercount usingencoder_get_position()function + + + + + + θm + A,B,Z pulses + + diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/ObserverFigure.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/ObserverFigure.svg new file mode 100644 index 00000000..6306e1a2 --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/ObserverFigure.svg @@ -0,0 +1,635 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ki + + + + + 1 + s + + + + + + + + + + + + + + + + + + 1 + J + + + + + + + + + + b + + + + Kp + + + + + + + + + + + + + + + 1 + s + + + + + + + + + + + + + + + + + + + + + + + + Ωraw + Ωsf + Tem + + From cbc72d10a6603fac6419295ea4ca2a4e38362f3e Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Wed, 26 Jun 2024 00:28:57 -0500 Subject: [PATCH 06/57] Update docs --- source/getting-started/control-with-amdc/encoder-fb/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 13bcd9de..d703a3b5 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -10,7 +10,7 @@ Incremental encoders which are typically used with AMDC have a fixed number of c ### Obtaining encoder count and translating it into rotor position - + As a first step, the user may use the AMDC `enc` driver function `encoder_get_position()` to get the count of the encoder reading. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. This document follows the convention of a positive rotor angle in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. The user must ensure that angle is within the bounds of 0 and 2 $\pi$ by appropriately wrapping the variable using the `mod` function. @@ -74,6 +74,7 @@ To ensure that the obtained encoder offset is correct, the user may perform addi ## Computing Speed from Position The user needs to compute a rotor speed signal from the obtained position signal to be used in the control algorithm. There are several ways to do this. A simple and straightforward way to do this would be to compute the discrete time derivative of the position signal in the controller as shown below. This can be referred to as $\Omega_{raw}$. + $$ \Omega_\text{raw}[k] = \frac{\theta_m[k] - \theta_m[k-1]}{T_s} $$ @@ -93,4 +94,4 @@ $$ This tuning ensures a pole zero cancellation in the closed transfer function, resulting in a unity transfer function for speed tracking under ideal parameter estimates of `J` and `b` - + From 655f2b18976961e11d609572cea6ce36f7a0e5bc Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Fri, 28 Jun 2024 16:11:05 -0500 Subject: [PATCH 07/57] Update doc --- .../control-with-amdc/encoder-fb/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index d703a3b5..47617e66 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -6,13 +6,13 @@ Encoders provide rotor position feedback to the control system in a motor drive. ## Calibration -Incremental encoders which are typically used with AMDC have a fixed number of counts per revolution (for example, 1024) and is denoted by `CPR`. The user needs needs to write some code to obtaine the encoder count at a given instance and tconvert the count into usable angular information which can be used within the control code. +Incremental encoders which are typically used with AMDC have a fixed number of counts per revolution (for example, 1024) and is denoted by `CPR`. The user needs needs to write some code to obtain the encoder count at a given instance and convert the count into usable angular information which can be used within the control code. ### Obtaining encoder count and translating it into rotor position -As a first step, the user may use the AMDC `enc` driver function `encoder_get_position()` to get the count of the encoder reading. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. This document follows the convention of a positive rotor angle in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. The user must ensure that angle is within the bounds of 0 and 2 $\pi$ by appropriately wrapping the variable using the `mod` function. +As a first step, the user may use the AMDC `drv/encoder` driver module `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted the rotor position in radians. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. This document follows the convention of a positive rotor angle in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. The user must ensure that angle is within the bounds of 0 and 2 $\pi$ by appropriately wrapping the variable using the `mod` function. Example code to convert encoder to angular position in radians: ```C @@ -56,9 +56,9 @@ double task_get_theta_m(void) ### Finding the offset -The example code shown above makes uses of an encoder offset value. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding of the stator. +The example code shown above makes uses of an encoder offset value. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. -To get the rotor to align with the phase U winding, the user may have some current flow through phase U and out of phase V and phase W. This can done by using a DC supply, with phase U connected to the positive terminal and phases V and W connected to the negative terminal. Alternately, the user may also inject some current along the d-axis using the AMDC injection function, but with the rotor position overridden by the user to 0. After obtaining the offset, the user needs to set the variable `enc_theta_m_offset` to the appropirate value in the `task_get_theta_m()` function. +To get the rotor to align with the phase U winding, the user may have some current flow through phase U and out of phase V and phase W. This can done by using a DC supply, with phase U connected to the positive terminal and phases V and W connected to the negative terminal. Alternately, the user may also inject some current along the d-axis using the AMDC [Signal Injection](https://docs.amdc.dev/getting-started/user-guide/injection/index.html) module. While injecting d-axis current, the user must ensure that the rotor position is set to zero in the control code. After obtaining the offset, the user needs to set the variable `enc_theta_m_offset` to the appropriate value in the `task_get_theta_m()` function. To ensure that the obtained encoder offset is correct, the user may perform additional validation. For a permanent magnet synchronous motor, this can be done as follows: @@ -80,7 +80,7 @@ $$ $$ -$\Omega_\text{raw}$ will be a choppy and noisy signal due to the derivative operation. A low pass filter may be applied to this signal as shown below to get a filtered speed, $\Omega_\text{lpf}$. The user may select an appropriate bandwidth, $\omega_b$ for the low pass filter to eliminate the noise introduced by the derivative operation. However, this signal will always be a lagging estimate of the actual rotor speed due to the characterstics of a low pass filter. +$\Omega_\text{raw}$ will be a choppy and noisy signal due to the derivative operation and the digital nature of the incremental encoder. A low pass filter may be applied to this signal as shown below to get a filtered speed, $\Omega_\text{lpf}$. The user may select an appropriate bandwidth, $\omega_b$ for the low pass filter to eliminate the noise introduced by the derivative operation. A recommended bandwidth for the low pass filter is 10 Hz. However, this signal will always be a lagging estimate of the actual rotor speed due to the characterstics of a low pass filter. $$ \Omega_\text{lpf}[k] = \Omega_\text{raw}[k](1 - e^{\omega_b T_s}) + \Omega_\text{lpf}[k-1]e^{\omega_b T_s} From cbd2100c5b2a9731aaa78d36ee1552f8732e0a78 Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Sun, 30 Jun 2024 09:21:46 -0500 Subject: [PATCH 08/57] Update block diagram --- .../control-with-amdc/encoder-fb/index.md | 6 +- .../resources/EncoderCodeBlockDiargam.svg | 527 +++++++++++++----- 2 files changed, 394 insertions(+), 139 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 47617e66..413334a5 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -12,7 +12,7 @@ Incremental encoders which are typically used with AMDC have a fixed number of c -As a first step, the user may use the AMDC `drv/encoder` driver module `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted the rotor position in radians. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. This document follows the convention of a positive rotor angle in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. The user must ensure that angle is within the bounds of 0 and 2 $\pi$ by appropriately wrapping the variable using the `mod` function. +As a first step, the user may use the AMDC `drv/encoder` driver module `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted the rotor position in radians. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. This document follows the convention of a positive rotor angle in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. The user must ensure that angle is within the bounds of 0 and 2$\pi$ by appropriately wrapping the variable using the `mod` function. Example code to convert encoder to angular position in radians: ```C @@ -80,7 +80,7 @@ $$ $$ -$\Omega_\text{raw}$ will be a choppy and noisy signal due to the derivative operation and the digital nature of the incremental encoder. A low pass filter may be applied to this signal as shown below to get a filtered speed, $\Omega_\text{lpf}$. The user may select an appropriate bandwidth, $\omega_b$ for the low pass filter to eliminate the noise introduced by the derivative operation. A recommended bandwidth for the low pass filter is 10 Hz. However, this signal will always be a lagging estimate of the actual rotor speed due to the characterstics of a low pass filter. +$\Omega_\text{raw}$ will be a choppy and noisy signal due to the derivative operation and the digital nature of the incremental encoder. A low pass filter may be applied to this signal as shown below to get a filtered speed, $\Omega_\text{lpf}$. The user may select an appropriate bandwidth, $\omega_b$ for the low pass filter to eliminate the noise introduced by the derivative operation. A recommended bandwidth for the low pass filter is 10 Hz. This would be a good bandwidth to try for the user. The user can reduce the bandwidth if the filtered signal still has signficant noise for the application. However, $\Omega_\text{lpf}$ will always be a lagging estimate of the actual rotor speed due to the characterstics of a low pass filter. $$ \Omega_\text{lpf}[k] = \Omega_\text{raw}[k](1 - e^{\omega_b T_s}) + \Omega_\text{lpf}[k-1]e^{\omega_b T_s} @@ -92,6 +92,6 @@ $$ K_p = \omega_{sf}b, K_i = \omega_{sf}J $$ -This tuning ensures a pole zero cancellation in the closed transfer function, resulting in a unity transfer function for speed tracking under ideal parameter estimates of `J` and `b` +This tuning ensures a pole zero cancellation in the closed transfer function, resulting in a unity transfer function for speed tracking under ideal parameter estimates of `J` and `b`. A recommended bandwidth for the observer is 10 Hz. diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg index 8e5f6b1a..9c3b51c1 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg @@ -3,8 +3,8 @@ @@ -52,11 +52,11 @@ transform="scale(0.5)" style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" d="M 5.77,0 -2.88,5 V -5 Z" - id="path3250" /> + id="path135" /> + id="path135-7" /> + + + + + + + + + + + + + + + - - Determine + encoder counts per revolution + x="114.87061" + y="21.049625" + id="tspan2800" /> task_get_theta_m() + + + Get encodercount usingencoder_get_position()function + + + Determineencoder offset + x="203.05502" + y="18.744692" + id="tspan2672-3">θm Determinedirectionality of Encoder raw encoder count + x="10.457352" + y="24.011608" + id="tspan15343">signals + + + + 2π( (count - offset)/ENCODER_COUNT_PER_REV) + if CCW + else + 2π( (ENCODER_COUNT_PER_REV - 1 - count + offset)/ENCODER_COUNT_PER_REV) + + + + + + + + mod by 2π + + + + + + + task_get_theta_m() + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583" + x="69.787239" + y="20.100161" + id="tspan2672-3-7-8">count Get encoderoffset* count using + encoder_get_position()ENCODER_COUNT_PER_REV* function - - - - - + style="stroke-width:0.264583" + x="38.037342" + y="49.542728" + id="tspan15336" /> rotor θm + x="166.22415" + y="19.171022" + id="tspan15372">position + * User input + CCW* A,B,Z pulses + x="57.219265" + y="59.622509" + id="tspan15340-4" /> From d971d20e660e859b4d48ab047cc4da4fd8f9d706 Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya <90214161+anirudhupadhyaya@users.noreply.github.com> Date: Sun, 30 Jun 2024 12:23:06 -0500 Subject: [PATCH 09/57] Apply suggestions from code review Co-authored-by: Eric Severson --- .../control-with-amdc/encoder-fb/index.md | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 413334a5..4a36c66b 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -6,7 +6,7 @@ Encoders provide rotor position feedback to the control system in a motor drive. ## Calibration -Incremental encoders which are typically used with AMDC have a fixed number of counts per revolution (for example, 1024) and is denoted by `CPR`. The user needs needs to write some code to obtain the encoder count at a given instance and convert the count into usable angular information which can be used within the control code. +Incremental encoders are typically used with the AMDC and have a fixed number of counts per revolution `CPR` (for example, `CPR = 1024`). The user needs needs to provide code that interfaces to the AMDC's drivers to read the encoder count and convert it into usable angular information that is suitable for use within the control code. ### Obtaining encoder count and translating it into rotor position @@ -73,25 +73,44 @@ To ensure that the obtained encoder offset is correct, the user may perform addi ## Computing Speed from Position -The user needs to compute a rotor speed signal from the obtained position signal to be used in the control algorithm. There are several ways to do this. A simple and straightforward way to do this would be to compute the discrete time derivative of the position signal in the controller as shown below. This can be referred to as $\Omega_{raw}$. +The user needs to compute a rotor speed signal from the obtained position signal to be used in the control algorithm. There are several ways to do this. + +### Difference Equation Approach + +A simple, but naive, way to do this would be to compute the discrete time derivative of the position signal in the controller as shown below. This can be referred to as $\Omega_{raw}$. $$ \Omega_\text{raw}[k] = \frac{\theta_m[k] - \theta_m[k-1]}{T_s} $$ -$\Omega_\text{raw}$ will be a choppy and noisy signal due to the derivative operation and the digital nature of the incremental encoder. A low pass filter may be applied to this signal as shown below to get a filtered speed, $\Omega_\text{lpf}$. The user may select an appropriate bandwidth, $\omega_b$ for the low pass filter to eliminate the noise introduced by the derivative operation. A recommended bandwidth for the low pass filter is 10 Hz. This would be a good bandwidth to try for the user. The user can reduce the bandwidth if the filtered signal still has signficant noise for the application. However, $\Omega_\text{lpf}$ will always be a lagging estimate of the actual rotor speed due to the characterstics of a low pass filter. +Unfortunately, using this approach results in noise in $\Omega_\text{raw}$ due to the derivative operation and the digital nature of the incremental encoder. + +### Low Pass Filter Approach + +To solve this, _a low pass filter_ may be applied to this signal. This is shown below to obtain a filtered speed, $\Omega_\text{lpf}$. $$ \Omega_\text{lpf}[k] = \Omega_\text{raw}[k](1 - e^{\omega_b T_s}) + \Omega_\text{lpf}[k-1]e^{\omega_b T_s} $$ -An observer which implements a mechanical model of the rotor as shown below will produce a no-lag estimate of the rotor speed, denoted by $\Omega_\text{sf}$. To implement an observer, the user needs to know the system parameters - `J` - the inertia of the rotor shaft and `b` - the damping coefficient of the rotor shaft. Further, to obtain a no-lag estimate it is necessary to provide the electromechanical torque, $T_{em}$ as input to the mechanical model. The `P-I` part of the observer closes the loop on the speed with $\Omega_\text{raw}$ being the reference input. The recommended tuning approach is as follows: +### Observer Approach + +To obtain a no-lag estimate of the rotor speed, users may create an observer which implements a mechanical model of the rotor as shown below. + + + +The estimate of rotor speed is denoted by $\Omega_\text{sf}$. To implement this observer, the user needs to know the system parameters: +- `J`: the inertia of the rotor +- `b` the damping coefficient of the rotor. + +It is also necessary to provide the electromechanical torque, $T_{em}$ as input to the mechanical model. + +The `PI` portion of the observer closes the loop on the speed, with $\Omega_\text{raw}$ being the reference input. The recommended tuning approach is as follows: $$ K_p = \omega_{sf}b, K_i = \omega_{sf}J $$ -This tuning ensures a pole zero cancellation in the closed transfer function, resulting in a unity transfer function for speed tracking under ideal parameter estimates of `J` and `b`. A recommended bandwidth for the observer is 10 Hz. +This tuning ensures a pole zero cancellation in the closed transfer function, resulting in a unity transfer function for speed tracking under ideal parameter estimates of `J` and `b`. An observer bandwidth of 10 Hz is typical of most systems, but similar to the low pass filter approach, users may need to alter this based on the unique aspects of their system. - From 63600c65d2a44e5a57752b17e5d80587119f9578 Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Sun, 30 Jun 2024 13:42:56 -0500 Subject: [PATCH 10/57] Update doc to address review comments --- .../control-with-amdc/encoder-fb/index.md | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 4a36c66b..34b693a8 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -2,17 +2,23 @@ ## Background -Encoders provide rotor position feedback to the control system in a motor drive. This document describes a method to convert the raw readings of an encoder into meaningful position information which can be used by the contrl algorithm. Consequently, few techniques to derive the rotor speed from the measured position are also proposed. For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). +Encoders provide rotor position feedback to the control system in a motor drive. This document describes a method to convert the raw readings of an encoder into meaningful position information which can be used by the contrl algorithm. Next, useful methods to obtain rotor speed from the measured position is presented. For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). ## Calibration -Incremental encoders are typically used with the AMDC and have a fixed number of counts per revolution `CPR` (for example, `CPR = 1024`). The user needs needs to provide code that interfaces to the AMDC's drivers to read the encoder count and convert it into usable angular information that is suitable for use within the control code. +Incremental encoders are typically used with the AMDC and have a fixed number of counts per revolution `CPR` (for example, `CPR = 1024`). The user needs to provide code that interfaces to the AMDC's drivers to read the encoder count and convert it into usable angular information that is suitable for use within the control code. ### Obtaining encoder count and translating it into rotor position +The recommended approach to reading the shaft position from the encoder is illustrated in the figure below: + -As a first step, the user may use the AMDC `drv/encoder` driver module `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted the rotor position in radians. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done by manually rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. This document follows the convention of a positive rotor angle in the counter clockwise (CCW) direction of shaft rotation. Using this information, along with the offset and total encoder counts per revolution, the obtained count can be translated into angular position using a simple linear equation. The user must ensure that angle is within the bounds of 0 and 2$\pi$ by appropriately wrapping the variable using the `mod` function. +As a first step, the user may use the AMDC `drv/encoder` driver module `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted to rotor position in radians. + + Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done manually by rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. In the figure, the signal `CCW` indicates this directionality. It must be set to `1`, if the encoder count increases with the counter clockwise rotation of shaft and `0` otherwise. Additionally, the user needs to provide the encoder offset, `offset` and the encoder counts per revolution, `ENCODER_COUNT_PER_REV`. A method to get the value of offset is described in the next [subsection](#finding-the-offset). Using all of this quantities, the obtained count can be translated into angular position using a simple linear equation. Note that, this document follows the convention of a positive rotor angle in the counter clockwise direction of shaft rotation while caclulating rotor position from the count signal. + + Finally, the user must ensure that angle is within the bounds of $0$ and $2\pi$ by appropriately wrapping the `rotor position` signal using the `mod` function. This is shown in the final block in the diagram. Example code to convert encoder to angular position in radians: ```C @@ -47,7 +53,7 @@ double task_get_theta_m(void) } // Mod by 2 pi - theta_m_enc = fmod(theta_m_enc,PI2 ); + theta_m_enc = fmod(theta_m_enc,PI2); return theta_m_enc; } ``` @@ -56,18 +62,20 @@ double task_get_theta_m(void) ### Finding the offset -The example code shown above makes uses of an encoder offset value. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. +The example code shown above makes uses of an encoder offset value, `enc_theta_m_offset`. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. + +To determine the appropriate offset value, eliminate any source of load torque on the shaft and apply a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). This should cause the rotor to align with the phase U winding axis. The offset value can now be obtained as `enc_theta_m_offset = encoder_get_position()`. Alternately, the user may also inject a current along the d-axis using the AMDC [Signal Injection](https://docs.amdc.dev/getting-started/user-guide/injection/index.html) module. While injecting d-axis current, the user must ensure that the rotor position is set to zero in the control code. -To get the rotor to align with the phase U winding, the user may have some current flow through phase U and out of phase V and phase W. This can done by using a DC supply, with phase U connected to the positive terminal and phases V and W connected to the negative terminal. Alternately, the user may also inject some current along the d-axis using the AMDC [Signal Injection](https://docs.amdc.dev/getting-started/user-guide/injection/index.html) module. While injecting d-axis current, the user must ensure that the rotor position is set to zero in the control code. After obtaining the offset, the user needs to set the variable `enc_theta_m_offset` to the appropriate value in the `task_get_theta_m()` function. +After obtaining the offset, the user needs to set the variable `enc_theta_m_offset` to the appropriate value in the `task_get_theta_m()` function. To ensure that the obtained encoder offset is correct, the user may perform additional validation. For a permanent magnet synchronous motor, this can be done as follows: 1. Spin the motor up-to a steady speed under no load conditions 1. Measure the d-axis voltage commanded by the current regulator -1. Repeat the experiment for a few different motor speeds -1. Plot the d-axis voltage against the motor speed -1. The d-axis voltage if the offset is tuned correctly, should be close to zero for all speeds. -1. In-case there is an error in the offset value, a significant speed dependent voltage will appear on the d-axis voltage +1. Repeat the experiment for a few different rotor speeds +1. Plot the d-axis voltage against the rotor speed +1. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly +1. In-case there is an error in the offset value, a significant speed dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. @@ -96,7 +104,7 @@ $$ ### Observer Approach -To obtain a no-lag estimate of the rotor speed, users may create an observer which implements a mechanical model of the rotor as shown below. +To obtain a no-lag estimate of the rotor speed, users may create an observer [[1]](#1), which implements a mechanical model of the rotor as shown below. @@ -114,3 +122,7 @@ $$ This tuning ensures a pole zero cancellation in the closed transfer function, resulting in a unity transfer function for speed tracking under ideal parameter estimates of `J` and `b`. An observer bandwidth of 10 Hz is typical of most systems, but similar to the low pass filter approach, users may need to alter this based on the unique aspects of their system. + +# References + 1. R. D. Lorenz and K. W. Van Patten, "High-resolution velocity estimation for all-digital, AC servo drives," in IEEE Transactions on Industry Applications, vol. 27, no. 4, pp. 701-705, July-Aug. 1991, doi: 10.1109/28.85485. keywords: {Servomechanisms;Optical feedback;Optical signal processing;Transducers;Signal resolution;Velocity measurement;Position control;Feedback loop;Velocity control;Noise reduction} + From 3a1ca7bf9e6a2a553e35a9022d5a3b1e41de6a69 Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya Date: Sun, 30 Jun 2024 18:39:44 -0500 Subject: [PATCH 11/57] Add line on low pass filter --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 34b693a8..493bf6d3 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -102,6 +102,8 @@ $$ \Omega_\text{lpf}[k] = \Omega_\text{raw}[k](1 - e^{\omega_b T_s}) + \Omega_\text{lpf}[k-1]e^{\omega_b T_s} $$ +10 Hz is a good bandwidth for the low-pass filter to start with, but users may need to alter this based on the unique aspects of their system. However, this approach will always produce a lagging speed estimate. + ### Observer Approach To obtain a no-lag estimate of the rotor speed, users may create an observer [[1]](#1), which implements a mechanical model of the rotor as shown below. From 04c349060b11e8cf059ee4a14cbb43f42bbffcdd Mon Sep 17 00:00:00 2001 From: Anirudh Upadhyaya <90214161+anirudhupadhyaya@users.noreply.github.com> Date: Sun, 30 Jun 2024 19:30:07 -0500 Subject: [PATCH 12/57] Update source/getting-started/control-with-amdc/encoder-fb/index.md Co-authored-by: Eric Severson --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 493bf6d3..3a6d4c1d 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -102,7 +102,9 @@ $$ \Omega_\text{lpf}[k] = \Omega_\text{raw}[k](1 - e^{\omega_b T_s}) + \Omega_\text{lpf}[k-1]e^{\omega_b T_s} $$ -10 Hz is a good bandwidth for the low-pass filter to start with, but users may need to alter this based on the unique aspects of their system. However, this approach will always produce a lagging speed estimate. +Here, $T_{\rm s}$ is the control sample rate and $\omega_b$ is the low pass filter bandwidth. The user must select this bandwidth to obtain a sufficiently clean speed signal. The optimal bandwidth to use is going to vary based on the motor system. Typically, a bandwidth of 10 Hz is a reasonable starting point. This can be reduced if the speed signal remains too noisy, or increased for higher speed controls. + +Note that this low pass filter approach will always produce a lagging speed estimate due to phase delay in the filter transfer function. This may be unacceptable higher performance motor control algorithms. ### Observer Approach From 7bdd0aa9c11da1020725c980b78e88086c2d5766 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Fri, 1 Nov 2024 20:26:40 -0500 Subject: [PATCH 13/57] Edit background --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 3a6d4c1d..b977a3ac 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -2,7 +2,9 @@ ## Background -Encoders provide rotor position feedback to the control system in a motor drive. This document describes a method to convert the raw readings of an encoder into meaningful position information which can be used by the contrl algorithm. Next, useful methods to obtain rotor speed from the measured position is presented. For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). +Encoders provide rotor position feedback to the control system in a motor drive. This document describes a method to convert the raw readings of an encoder into meaningful position information which can be used by the control algorithm. Methods to obtain rotor speed from the measured position are also presented. + +For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). ## Calibration From c249ab3d82dc4a07dbaa80dbc7b371feb3bd4d8b Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Fri, 1 Nov 2024 20:27:18 -0500 Subject: [PATCH 14/57] Fix file name type-o --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- ...{EncoderCodeBlockDiargam.svg => EncoderCodeBlockDiagram.svg} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename source/getting-started/control-with-amdc/encoder-fb/resources/{EncoderCodeBlockDiargam.svg => EncoderCodeBlockDiagram.svg} (100%) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index b977a3ac..f3e1e0d2 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -14,7 +14,7 @@ Incremental encoders are typically used with the AMDC and have a fixed number of The recommended approach to reading the shaft position from the encoder is illustrated in the figure below: - + As a first step, the user may use the AMDC `drv/encoder` driver module `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted to rotor position in radians. diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiagram.svg similarity index 100% rename from source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiargam.svg rename to source/getting-started/control-with-amdc/encoder-fb/resources/EncoderCodeBlockDiagram.svg From 52556e65cae8dabb65c15f93baa0c62221ad75fd Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Fri, 1 Nov 2024 21:58:42 -0500 Subject: [PATCH 15/57] Fix type-os --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index f3e1e0d2..8510093c 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -16,9 +16,9 @@ The recommended approach to reading the shaft position from the encoder is illus -As a first step, the user may use the AMDC `drv/encoder` driver module `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted to rotor position in radians. +As a first step, the user may use the AMDC `drv/encoder` driver module function `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted to rotor position in radians. - Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done manually by rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. In the figure, the signal `CCW` indicates this directionality. It must be set to `1`, if the encoder count increases with the counter clockwise rotation of shaft and `0` otherwise. Additionally, the user needs to provide the encoder offset, `offset` and the encoder counts per revolution, `ENCODER_COUNT_PER_REV`. A method to get the value of offset is described in the next [subsection](#finding-the-offset). Using all of this quantities, the obtained count can be translated into angular position using a simple linear equation. Note that, this document follows the convention of a positive rotor angle in the counter clockwise direction of shaft rotation while caclulating rotor position from the count signal. + Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done manually by rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. In the figure, the signal `CCW` indicates this directionality. It must be set to `1` if the encoder count increases with counter clockwise rotation of shaft and `0` otherwise. Additionally, the user needs to provide the encoder offset, `offset`, and the encoder counts per revolution, `ENCODER_COUNT_PER_REV`. A method to get the value of offset is described in the next [subsection](#finding-the-offset). Using all of these quantities, the obtained count can be translated into angular position using a simple linear equation. Note that this document follows the convention of a positive rotor angle in the counter clockwise direction of shaft rotation while calculating rotor position from the count signal. Finally, the user must ensure that angle is within the bounds of $0$ and $2\pi$ by appropriately wrapping the `rotor position` signal using the `mod` function. This is shown in the final block in the diagram. From 5ac7ce6056da6e764fe7be2687d9a9f98b18b181 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Fri, 1 Nov 2024 22:19:41 -0500 Subject: [PATCH 16/57] Add config instructions --- .../control-with-amdc/encoder-fb/index.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 8510093c..4a813be3 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -6,10 +6,25 @@ Encoders provide rotor position feedback to the control system in a motor drive. For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). -## Calibration +## Obtaining Position Incremental encoders are typically used with the AMDC and have a fixed number of counts per revolution `CPR` (for example, `CPR = 1024`). The user needs to provide code that interfaces to the AMDC's drivers to read the encoder count and convert it into usable angular information that is suitable for use within the control code. +### Configuring the encoder +Upon powerup, the AMDC configures the encoder to a default number of pulses per revolution. This is handled in `encoder.c`, which is part of the core AMDC-Firmware. When using an encoder that has a different number pulses per revolution, the user must inform the driver by calling `encoder_set_pulses_per_rev()`. + +Example code for a 10 bit encoder: + +``` C +#define USER_ENCODER_PULSES_PER_REV_BITS (10) +#define USER_ENCODER_PULSES_PER_REV (1 << USER_ENCODER_PULSES_PER_REV_BITS) + +int task_user_app_init(void) +{ + encoder_set_pulses_per_rev_bits(USER_ENCODER_PULSES_PER_REV_BITS); + ... +``` + ### Obtaining encoder count and translating it into rotor position The recommended approach to reading the shaft position from the encoder is illustrated in the figure below: From 1532f0a15f86b8e17ff825718f044670df14e25e Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sat, 2 Nov 2024 21:09:33 -0500 Subject: [PATCH 17/57] Edit first 1/3 of document --- .../control-with-amdc/encoder-fb/index.md | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 4a813be3..3bf68649 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -2,16 +2,20 @@ ## Background -Encoders provide rotor position feedback to the control system in a motor drive. This document describes a method to convert the raw readings of an encoder into meaningful position information which can be used by the control algorithm. Methods to obtain rotor speed from the measured position are also presented. +Encoders are used to determine the rotor position and speed, and are the typical method of feedback to the control system in a motor drive. This document explains how to use the AMDC's encoder interface to extract high quality rotor position and speed data. -For more information on how an encoder works and how they may be interfaced with the AMDC please refer to this [document](https://docs.amdc.dev/hardware/subsystems/encoder.html#). +For more information: -## Obtaining Position +- on how encoders work and are interfaced with the AMDC, see the [encoder hardware subsystem page](https://docs.amdc.dev/hardware/subsystems/encoder.html#) +- on the driver functionality included with the AMDC firmware, see the [encoder driver architecture page](https://docs.amdc.dev/firmware/arch/drivers/encoder.html). -Incremental encoders are typically used with the AMDC and have a fixed number of counts per revolution `CPR` (for example, `CPR = 1024`). The user needs to provide code that interfaces to the AMDC's drivers to read the encoder count and convert it into usable angular information that is suitable for use within the control code. +## Rotor Position + +The AMDC supports [incremental encoders with quadrature ABZ outputs](https://en.wikipedia.org/wiki/Incremental_encoder#Quadrature_outputs) and a fixed number of counts per revolution `CPR` (for example, `CPR = 1024`). The user needs to provide code that interfaces to the AMDC's drivers to read the encoder count and convert it into usable angular information that is suitable for use within the control code. ### Configuring the encoder -Upon powerup, the AMDC configures the encoder to a default number of pulses per revolution. This is handled in `encoder.c`, which is part of the core AMDC-Firmware. When using an encoder that has a different number pulses per revolution, the user must inform the driver by calling `encoder_set_pulses_per_rev()`. + +Upon powerup, the AMDC configures the encoder to a default number of pulses per revolution. This is handled in `encoder.c` as part of the standard firmware package. When using an encoder that has a different number pulses per revolution, the user must inform the driver by calling `encoder_set_pulses_per_rev()`. Example code for a 10 bit encoder: @@ -21,11 +25,17 @@ Example code for a 10 bit encoder: int task_user_app_init(void) { - encoder_set_pulses_per_rev_bits(USER_ENCODER_PULSES_PER_REV_BITS); - ... + encoder_set_pulses_per_rev(USER_ENCODER_PULSES_PER_REV); + + // other user app one-time initialization code + // ... +``` + +```{tip} +The AMDC provides a convenience function that can be used as an alternate to `encoder_set_pulses_per_rev()` when the encoder is specified as a number of bits: `encoder_set_pulses_per_rev_bits(USER_ENCODER_PULSES_PER_REV_BITS).` ``` -### Obtaining encoder count and translating it into rotor position +### Converting the encoder count into rotor position The recommended approach to reading the shaft position from the encoder is illustrated in the figure below: From 5b5a78be438b21cfdaa2745d516357a8bf39ee99 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Tue, 10 Dec 2024 15:22:31 -0600 Subject: [PATCH 18/57] add simulink model of pll --- .../encoder-fb/resources/pll_encoder.slx | Bin 0 -> 39681 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/pll_encoder.slx diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/pll_encoder.slx b/source/getting-started/control-with-amdc/encoder-fb/resources/pll_encoder.slx new file mode 100644 index 0000000000000000000000000000000000000000..1503595dcb773bbd2c0c3deab70b92841c174193 GIT binary patch literal 39681 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

rHI~>S+@AI(&Gx}fyYlO~3!#Qj-wO*> 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%0DlJHM#I{u6%Apg8YMbm6%Wktpl71) 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

  • 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 CPK_G? literal 0 HcmV?d00001 From 1086a1366699e1d1e38d4b227d390c165fb1b65f Mon Sep 17 00:00:00 2001 From: Takahiro Noguchi Date: Wed, 11 Dec 2024 22:44:27 -0600 Subject: [PATCH 19/57] Add pll test files --- .../encoder-fb/resources/pll_test.m | 113 ++++++++++++++++++ .../encoder-fb/resources/pll_test_sim.slx | Bin 0 -> 44200 bytes 2 files changed, 113 insertions(+) create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/pll_test.m create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/pll_test_sim.slx diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/pll_test.m b/source/getting-started/control-with-amdc/encoder-fb/resources/pll_test.m new file mode 100644 index 00000000..35c59524 --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/pll_test.m @@ -0,0 +1,113 @@ +clear +close all + +pole_1_Hz = -10; +pole_2_Hz = -100; + +w1 = 2*pi*pole_1_Hz; +w2 = 2*pi*pole_2_Hz; + +Tend = 10; +Tsim = 1e-5; + +finit = 0.1; % initial frequency of chirp [Hz] +ftarget = 1000; % chirp frequency at target time [Hz] + +%% Run simulation +out = sim('pll_test_sim.slx'); + +%% System ID +sim_data = [out.theta_in.Time,out.theta_in.Data,out.theta_out.Data]; + +den = sim_data(:,2); % input signal +num = sim_data(:,3); % output signal + +[freq,mag,phase,coh] = generateFRF(num,den,Tsim,100000,'hann'); + +% Curve Fit Current Command Tracking FRF +% Find where frequency goes positive +idx_f_pos = find(freq >= 0,1); + +G_CL = tf([-w1-w2, w1*w2], [1, -w1-w2, w1*w2]); % CL transfer function +disp('---') +disp('System Poles (Hz):') +my_poles_Hz = pole(G_CL) ./ (2*pi); +disp(my_poles_Hz(1)) +disp(my_poles_Hz(2)) + +mag_pos = mag(idx_f_pos:end); +phase_pos = phase(idx_f_pos:end); +freq_pos = freq(idx_f_pos:end); +[~,index] = min(abs(mag_pos - 1/sqrt(2))); +freq_check = freq_pos(index); +mag_check = mag_pos(index); +phase_check = phase_pos(index); + +%% Plot +markersize = 3; +linewidth = 1; + +% Bode diagram +figure + +f1 = 0.1; +f2 = 1000; + +tiledlayout(3,1); +ax1 = nexttile; +ax2 = nexttile; +ax3 = nexttile; + +% Bode plot by System ID +plot(ax1,freq,20*log10(mag),'oc','markersize',markersize); +plot(ax2,freq,phase,'oc','markersize',markersize); +plot(ax3,freq,coh,'.','markersize',6); +hold (ax1,'on'); +hold (ax2,'on'); + +% Bode plot with ideal Closed-loop transfer function +freq_bode = transpose(linspace(0.1,1/(4*Tsim),10/(4*Tsim))); +[mag_G_CL,phase_G_CL] = bode(G_CL,freq_bode*2*pi); +plot(ax1,freq_bode,squeeze(20*log10(mag_G_CL)),'r','linewidth',linewidth); +plot(ax2,freq_bode,wrapTo180(squeeze(phase_G_CL)),'r','linewidth',linewidth); + +% Set figure limit, label, etc. +xlim(ax1,[f1 f2]); +xlim(ax2,[f1 f2]); +xlim(ax3,[f1 f2]); +ylim(ax3,[0 1]); + +xlabel(ax3,"Frequency (Hz)"); +ylabel(ax1,"Magnitude (dB)"); +ylabel(ax2,"Phase (deg)"); +ylabel(ax3,"Coherence"); + +grid(ax1,'on'); +grid(ax2,'on'); +grid(ax3,'on'); + +set(ax1,'xscale','log'); +set(ax2,'xscale','log'); +set(ax3,'xscale','log'); + +legend(ax1,'System ID','PLL','Location','southwest'); +legend(ax2,'System ID','PLL','Location','southwest'); + +%% +function [freq,mag,phase,coh] = generateFRF(num,den,T,lines,win) + + fs = 1/T; + overlap = lines/2; + averages = floor(length(num)/(lines-overlap)); + windowType = window(win,lines); + + [FRF,freq] = tfestimate(den,num,windowType,overlap,lines,fs); + [coh,freq] = mscohere(den,num,windowType,overlap,lines,fs); + + FRF = fftshift(FRF); + coh = fftshift(coh); + freq = freq - max(freq)/2; + mag = abs(FRF); + phase = angle(FRF) * 180/pi; + +end \ No newline at end of file diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/pll_test_sim.slx b/source/getting-started/control-with-amdc/encoder-fb/resources/pll_test_sim.slx new file mode 100644 index 0000000000000000000000000000000000000000..d84461dfc8f0c1bfff181a6ebd432563d08df1f5 GIT binary patch literal 44200 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

    ~t?DqDywa46Mv#ScyO4zoL_Rr-{u=LP?WA<8g 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

    dOIrS!Z^lU%&l)dSTvhz5VFMEg+ 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 N3rvYr1%Lhbe*k8KIcop_ literal 0 HcmV?d00001 From 93cf84eb06b5fae031806821129092337218ef1a Mon Sep 17 00:00:00 2001 From: "AD\\sung0058" Date: Fri, 6 Jun 2025 17:53:48 -0500 Subject: [PATCH 20/57] Change indexme file --- .../control-with-amdc/encoder-fb/index.md | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 3bf68649..cc0b6039 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -89,18 +89,28 @@ double task_get_theta_m(void) ### Finding the offset -The example code shown above makes uses of an encoder offset value, `enc_theta_m_offset`. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. +The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. It is necessary for the user to find this offset experimentally for their machine, and this section provides a procedure for finding the offset in two steps. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. -To determine the appropriate offset value, eliminate any source of load torque on the shaft and apply a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). This should cause the rotor to align with the phase U winding axis. The offset value can now be obtained as `enc_theta_m_offset = encoder_get_position()`. Alternately, the user may also inject a current along the d-axis using the AMDC [Signal Injection](https://docs.amdc.dev/getting-started/user-guide/injection/index.html) module. While injecting d-axis current, the user must ensure that the rotor position is set to zero in the control code. +#### Step 1 +1. Set the `enc_theta_m_offset` to 0 in the control code. +2. Eliminate any source of load torque on the shaft +3. Align the rotor with the phase U winding axis by: + 1) Method 1: Apply a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). + 2) Method 2: Inject a current along the d-axis using the AMDC [Signal Injection](https://docs.amdc.dev/getting-started/user-guide/injection/index.html) module. +4. Record the offset value which can be obtained as `enc_theta_m_offset = encoder_get_position()`. +5. Set the variable `enc_theta_m_offset` to the appropriate value in the `task_get_theta_m()` function. -After obtaining the offset, the user needs to set the variable `enc_theta_m_offset` to the appropriate value in the `task_get_theta_m()` function. - -To ensure that the obtained encoder offset is correct, the user may perform additional validation. For a permanent magnet synchronous motor, this can be done as follows: +#### Step 2 +To account for dynamic factors like friction and rotor misalignment, the user needs to find the new offset under spinning operation based on the fine-tuned offset from the static test from step 1. For a permanent magnet synchronous motor, this can be done as follows: 1. Spin the motor up-to a steady speed under no load conditions 1. Measure the d-axis voltage commanded by the current regulator -1. Repeat the experiment for a few different rotor speeds -1. Plot the d-axis voltage against the rotor speed +1. Based on the estimated encoder offset values from Step 1, adjust `enc_theta_m_offset` gradually with relatively large values (3-5) until the sign of d-axis voltage flips, to figure out a zero crossing point. +1. Adjust enc_theta_m_offset in smaller increments at the zero crossing and record the d-axis voltage. +1. Repeat procedure 3-4 with various speeds. +1. For each offset value, average the d-axis voltage values for different speeds. +1. Select the offset that shows the lowest average d-axis voltage for different speeds. +1. Plot the d-axis voltage with the selected offset against the different rotor speeds. 1. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly 1. In-case there is an error in the offset value, a significant speed dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. From eab8281493ca3539500aea94871c789c1b957588 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Fri, 6 Jun 2025 18:12:01 -0500 Subject: [PATCH 21/57] Update index.md --- .../control-with-amdc/encoder-fb/index.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index cc0b6039..498d531e 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -92,6 +92,8 @@ double task_get_theta_m(void) The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. It is necessary for the user to find this offset experimentally for their machine, and this section provides a procedure for finding the offset in two steps. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. #### Step 1 +To estimate the initial encoder offset, the user needs to follow the procedure below in no-spinning mode. + 1. Set the `enc_theta_m_offset` to 0 in the control code. 2. Eliminate any source of load torque on the shaft 3. Align the rotor with the phase U winding axis by: @@ -105,14 +107,11 @@ To account for dynamic factors like friction and rotor misalignment, the user ne 1. Spin the motor up-to a steady speed under no load conditions 1. Measure the d-axis voltage commanded by the current regulator -1. Based on the estimated encoder offset values from Step 1, adjust `enc_theta_m_offset` gradually with relatively large values (3-5) until the sign of d-axis voltage flips, to figure out a zero crossing point. -1. Adjust enc_theta_m_offset in smaller increments at the zero crossing and record the d-axis voltage. -1. Repeat procedure 3-4 with various speeds. -1. For each offset value, average the d-axis voltage values for different speeds. -1. Select the offset that shows the lowest average d-axis voltage for different speeds. -1. Plot the d-axis voltage with the selected offset against the different rotor speeds. -1. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly -1. In-case there is an error in the offset value, a significant speed dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. +1. Starting from the estimated encoder offset values from Step 1, record the d-axis voltage while adjusting `enc_theta_m_offset` gradually until the sign of the d-axis voltage flips to figure out a zero crossing point. +1. Repeat procedure 3 with various speeds. +1. For each offset value, average the d-axis voltage values for different speeds and select the offset with the lowest average d-axis voltage. +1. Plot the d-axis voltage with the selected offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly. +1. In-case there is an error in the offset value, a significant speed-dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. From 196297ad36180d502d68e9f60af38d3976fe64b4 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sat, 7 Jun 2025 09:04:49 -0500 Subject: [PATCH 22/57] Edit finding offset and step 1 --- .../control-with-amdc/encoder-fb/index.md | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 498d531e..e6d2d203 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -89,20 +89,22 @@ double task_get_theta_m(void) ### Finding the offset -The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. It is necessary for the user to find this offset experimentally for their machine, and this section provides a procedure for finding the offset in two steps. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. +The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. This value typically needs to be found experimentally for each motor/encoder pair because it depends on how the encoder was aligned when it was coupled to the motors shaft. This section provides a procedure to determine `enc_theta_m_offset`. -#### Step 1 -To estimate the initial encoder offset, the user needs to follow the procedure below in no-spinning mode. +#### Step 1: Determine approximate offset -1. Set the `enc_theta_m_offset` to 0 in the control code. -2. Eliminate any source of load torque on the shaft -3. Align the rotor with the phase U winding axis by: +To estimate the initial encoder offset, use the following procedure with a stationary shaft. + +1. Set the `enc_theta_m_offset` to 0 in the control code `task_get_theta_m()`. +2. Eliminate any source of load torque on the shaft. +3. Align the rotor with the phase U winding axis by either: 1) Method 1: Apply a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). 2) Method 2: Inject a current along the d-axis using the AMDC [Signal Injection](https://docs.amdc.dev/getting-started/user-guide/injection/index.html) module. -4. Record the offset value which can be obtained as `enc_theta_m_offset = encoder_get_position()`. -5. Set the variable `enc_theta_m_offset` to the appropriate value in the `task_get_theta_m()` function. +4. Record the current encoder position and use this as the offset value: `enc_theta_m_offset = encoder_get_position();`. +5. Update the variable `enc_theta_m_offset` to the appropriate value in the `task_get_theta_m()` function. + +#### Step 2: Determine precise offset -#### Step 2 To account for dynamic factors like friction and rotor misalignment, the user needs to find the new offset under spinning operation based on the fine-tuned offset from the static test from step 1. For a permanent magnet synchronous motor, this can be done as follows: 1. Spin the motor up-to a steady speed under no load conditions From b27375c7f434a8ac29fa5eb51bd6abc1885812c9 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sat, 7 Jun 2025 10:24:34 -0500 Subject: [PATCH 23/57] Edit finding the offset section --- .../control-with-amdc/encoder-fb/index.md | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index e6d2d203..6166fb75 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -89,33 +89,31 @@ double task_get_theta_m(void) ### Finding the offset -The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. This value typically needs to be found experimentally for each motor/encoder pair because it depends on how the encoder was aligned when it was coupled to the motors shaft. This section provides a procedure to determine `enc_theta_m_offset`. +The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. This value typically needs to be found experimentally for each motor/encoder pair because it depends on how the encoder was aligned when it was coupled to the motor's shaft. This section provides a procedure to determine `enc_theta_m_offset`. #### Step 1: Determine approximate offset -To estimate the initial encoder offset, use the following procedure with a stationary shaft. +The approximate encoder offset can be found using the following simple procedure without feedback control: 1. Set the `enc_theta_m_offset` to 0 in the control code `task_get_theta_m()`. 2. Eliminate any source of load torque on the shaft. -3. Align the rotor with the phase U winding axis by either: - 1) Method 1: Apply a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). - 2) Method 2: Inject a current along the d-axis using the AMDC [Signal Injection](https://docs.amdc.dev/getting-started/user-guide/injection/index.html) module. -4. Record the current encoder position and use this as the offset value: `enc_theta_m_offset = encoder_get_position();`. -5. Update the variable `enc_theta_m_offset` to the appropriate value in the `task_get_theta_m()` function. +3. Align the rotor with the phase U winding axis by applying a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). This could be accomplished by: + 1) Using a DC power supply, or + 2) Injecting a current command on the d-axis using the AMDC [Signal Injection](/getting-started/user-guide/injection/index.rst) module with `theta_m_enc` fixed to 0. +4. Record the current encoder position and use this as the offset value: `enc_theta_m_offset = encoder_get_position();`. +5. Update the variable `enc_theta_m_offset` to the appropriate value in `task_get_theta_m()`. #### Step 2: Determine precise offset -To account for dynamic factors like friction and rotor misalignment, the user needs to find the new offset under spinning operation based on the fine-tuned offset from the static test from step 1. For a permanent magnet synchronous motor, this can be done as follows: - -1. Spin the motor up-to a steady speed under no load conditions -1. Measure the d-axis voltage commanded by the current regulator -1. Starting from the estimated encoder offset values from Step 1, record the d-axis voltage while adjusting `enc_theta_m_offset` gradually until the sign of the d-axis voltage flips to figure out a zero crossing point. -1. Repeat procedure 3 with various speeds. -1. For each offset value, average the d-axis voltage values for different speeds and select the offset with the lowest average d-axis voltage. -1. Plot the d-axis voltage with the selected offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly. -1. In-case there is an error in the offset value, a significant speed-dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. - +Friction and cogging torque in the motor decrease the accuracy of the estimate in Step 1. The precise offset can be found by fine-tuning the `enc_theta_m_offset` from Step 1 while using closed-loop control to rotate the shaft at significant speed. This can be done as follows: +1. Configure the AMDC for closed-loop speed and DQ current control and configure the operating environment to allow for quick edits to `enc_theta_m_offset` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. +2. Command the motor to rotate in steady speed under no load conditions. Use the estimated `enc_theta_m_offset` obtained in Step 1. +3. Determine the value of `enc_theta_m_offset` that results in the d-axis voltage being closest to 0V. Do this by monitoring the d-axis voltage while adjusting `enc_theta_m_offset` gradually until the sign of the d-axis voltage flips. +4. Repeat step 3 at several speeds. +5. For each offset value, average the d-axis voltage values for different speeds and select the offset with the lowest average d-axis voltage. +6. Plot the d-axis voltage with the selected offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly. +7. In-case there is an error in the offset value, a significant speed-dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. ## Computing Speed from Position From 36971f0e9b4c342cc007d03428d0cf3042a2e86c Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sat, 7 Jun 2025 17:04:06 -0500 Subject: [PATCH 24/57] Add encoder angle diagram, first edits of text to use it. --- .../control-with-amdc/encoder-fb/index.md | 18 +- .../resources/MotorCrossSection.svg | 165 ++++++++++++++++++ 2 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.svg diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 3bf68649..4142868f 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -13,35 +13,39 @@ For more information: The AMDC supports [incremental encoders with quadrature ABZ outputs](https://en.wikipedia.org/wiki/Incremental_encoder#Quadrature_outputs) and a fixed number of counts per revolution `CPR` (for example, `CPR = 1024`). The user needs to provide code that interfaces to the AMDC's drivers to read the encoder count and convert it into usable angular information that is suitable for use within the control code. +This document assumes the configuration shown in the figure below, where the control code expects a measurement of the angle of the rotor's north pole relative to the phase $u$ magnetic axis, labeled as $\theta_{\rm m}$. The encoder provides $\theta_{\rm enc}$, which is the number of counts since the last z-pulse. The user's code needs to convert $\theta_{\rm enc}$ (in units of counts) into $\theta_{\rm m}$ (likely in units of radians) and handle an offset angle $\theta_{\rm off}$ between the encoder's 0 position and the phase $u$ axis. + +![Motor Cross-Section with Encoder Angles](resources/MotorCrossSection.svg) + ### Configuring the encoder -Upon powerup, the AMDC configures the encoder to a default number of pulses per revolution. This is handled in `encoder.c` as part of the standard firmware package. When using an encoder that has a different number pulses per revolution, the user must inform the driver by calling `encoder_set_pulses_per_rev()`. +Upon powerup, the AMDC configures the encoder to a default number of counts per revolution. This is handled in `encoder.c` as part of the standard firmware package. When using an encoder that has a different number counts per revolution, the user must inform the driver by calling `encoder_set_counts_per_rev()`. Example code for a 10 bit encoder: ``` C -#define USER_ENCODER_PULSES_PER_REV_BITS (10) -#define USER_ENCODER_PULSES_PER_REV (1 << USER_ENCODER_PULSES_PER_REV_BITS) +#define USER_ENCODER_COUNTS_PER_REV_BITS (10) +#define USER_ENCODER_COUNTS_PER_REV (1 << USER_ENCODER_COUNTS_PER_REV_BITS) int task_user_app_init(void) { - encoder_set_pulses_per_rev(USER_ENCODER_PULSES_PER_REV); + encoder_set_counts_per_rev(USER_ENCODER_COUNTS_PER_REV); // other user app one-time initialization code // ... ``` ```{tip} -The AMDC provides a convenience function that can be used as an alternate to `encoder_set_pulses_per_rev()` when the encoder is specified as a number of bits: `encoder_set_pulses_per_rev_bits(USER_ENCODER_PULSES_PER_REV_BITS).` +The AMDC provides a convenience function that can be used as an alternate to `encoder_set_counts_per_rev()` when the encoder is specified as a number of bits: `encoder_set_counts_per_rev_bits(USER_ENCODER_COUNTS_PER_REV_BITS).` ``` ### Converting the encoder count into rotor position The recommended approach to reading the shaft position from the encoder is illustrated in the figure below: - + -As a first step, the user may use the AMDC `drv/encoder` driver module function `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted to rotor position in radians. +First, the user uses the AMDC [`drv/encoder`](/firmware/arch/drivers/encoder.md) driver module function `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted to rotor position in radians. Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done manually by rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. In the figure, the signal `CCW` indicates this directionality. It must be set to `1` if the encoder count increases with counter clockwise rotation of shaft and `0` otherwise. Additionally, the user needs to provide the encoder offset, `offset`, and the encoder counts per revolution, `ENCODER_COUNT_PER_REV`. A method to get the value of offset is described in the next [subsection](#finding-the-offset). Using all of these quantities, the obtained count can be translated into angular position using a simple linear equation. Note that this document follows the convention of a positive rotor angle in the counter clockwise direction of shaft rotation while calculating rotor position from the count signal. diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.svg new file mode 100644 index 00000000..fc53967c --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8dc006bbdd70ff845ccdf183321f155223906ea2 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sat, 7 Jun 2025 17:54:40 -0500 Subject: [PATCH 25/57] Add latex source for motor cross section --- .../resources/MotorCrossSection.tex | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.tex diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.tex b/source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.tex new file mode 100644 index 00000000..e5822bae --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.tex @@ -0,0 +1,259 @@ +\documentclass[class=IEEEtran]{standalone}%[conference,10pt]{IEEEtran} +\usepackage{amsmath} +\ifCLASSINFOpdf +\usepackage[pdftex]{graphicx} +\else +\usepackage[dvips]{graphicx} +\fi + +\usepackage{color} % required for `\textcolor' (yatex added) + +%ERIC ADDED: +\usepackage{xcolor, tikz, pgfplots, ifthen} +\usepackage[american,cuteinductors,smartlabels]{circuitikz} +\pgfplotsset{compat=newest} +\usetikzlibrary{intersections,calc,backgrounds} + +\begin{document} + + + \centering + \newcommand{\thisScale}{.09} + + +%THE CALLING TEX FILE MUST DEFINE TWO COMMANDS: +%\thisScale and \setupFig +%for example: +% \newcommand{\setupFig} +%{ +% \def\TopLayerLabels{{"v", "v", "-u", "w", "w", "-v", "u", "u", "-w", "v", "v", "-u", "w", "w", "-v", "u", "u", "-w","v", "v", "-u", "w", "w", "-v", "u", "u", "-w","v", "v", "-u", "w", "w", "-v", "u", "u", "-w"}} +% \def\BotLayerLabels{{"v", "-u", "-u", "w", "-v", "-v", "u", "-w", "-w", "v", "-u", "-u", "w", "-v", "-v", "u", "-w", "-w","v", "-u", "-u", "w", "-v", "-v", "u", "-w", "-w","v", "-u", "-u", "w", "-v", "-v", "u", "-w", "-w"}} +%} +\definecolor{motoGreen}{RGB}{34, 139, 34} +\definecolor{motoRed}{RGB}{196,0,100} % HEX #fedeed +\definecolor{motoBlue}{RGB}{0,110,191} + +%PREAMBLE: +% \usetikzlibrary{intersections} +% \usetikzlibrary{calc} +% +% \newcommand{\setupFig}{\def\showX{0}} +% \newcommand{\thisScale}{1/10} +% +\scalebox{3}{ +\begin{tikzpicture}[font=\tiny,scale=\thisScale,remember picture, >=stealth] + + +%default values: +\def\StatorOR{25} +\def\StatorIR{17.5} + +\def\IntraSlotDepth{.3} +\def\IntraSlotWidth{2} +\def\SlotWidth{2} +\def\SlotDepth{4.1} +\def\NumSlots{36} +\def\CondRadius{1.25} +\def\UseCond{0} +\def\NumberSlots{0} +\def\StatorRotOffset{5} %angle that slot 1 should be at with respect to the x axis +\def\TopLayerLabels{{"v", "v", "-u", "w", "w", "-v", "u", "u", "-w", "v", "v", "-u", "w", "w", "-v", "u", "u", "-w","v", "v", "-u", "w", "w", "-v", "u", "u", "-w","v", "v", "-u", "w", "w", "-v", "u", "u", "-w"}} +\def\BotLayerLabels{{"v", "-u", "-u", "w", "-v", "-v", "u", "-w", "-w", "v", "-u", "-u", "w", "-v", "-v", "u", "-w", "-w","v", "-u", "-u", "w", "-v", "-v", "u", "-w", "-w","v", "-u", "-u", "w", "-v", "-v", "u", "-w", "-w"}} +\def\phaseUcolor{"motoBlue!30!white"} +\def\phaseVcolor{"motoRed!30!white"} +\def\phaseWcolor{"motoGreen!30!white"} +\def\TopCondColors{{\phaseUcolor,\phaseUcolor,\phaseVcolor,\phaseVcolor,\phaseWcolor,\phaseWcolor,\phaseUcolor,\phaseUcolor,\phaseVcolor,\phaseVcolor,\phaseWcolor,\phaseWcolor}} +\def\BotCondColors{{\phaseVcolor,\phaseVcolor,\phaseWcolor,\phaseWcolor,\phaseWcolor,\phaseWcolor,\phaseUcolor,\phaseUcolor,\phaseWcolor,\phaseVcolor,\phaseWcolor,\phaseWcolor}} +\def\nodeFontSize{\tiny} + +%draw rotor: +\def\drawPMrotor{0} +\def\PMrotorPoles{4} +\def\PMrotorRotation{15} +\def\PMrotorPMthick{2} +\def\PMrotorAG{1} +\def\PMnorthColor{red!20!white} +\def\PMsouthColor{green!20!white} +\def\showRotorAngle{1} +\def\rotorAngleLabel{$\theta$} +\def\showRotorAngleTwo{0} +\def\rotorAngleTwoStart{15} +\def\rotorAngleTwoLabel{$\theta_b$} +\def\showAlpha{1} + + +% Overrides for this image +\newcommand{\setupFig} +{ + \def\TopLayerLabels{{ + " ", " ", " "," "," "," ", + " "," "," "," "," "," "}} %back of the slot + \def\BotLayerLabels{{ + " ", " "," "," "," "," ", + " "," "," "," "," "," "}} %front of the slot + \def\TopCondColors{{\phaseUcolor,\phaseVcolor,\phaseWcolor,\phaseUcolor,\phaseVcolor, + \phaseWcolor,\phaseUcolor,\phaseVcolor,\phaseWcolor,\phaseUcolor,\phaseVcolor,\phaseWcolor}} %back of slot + \def\BotCondColors{{\phaseWcolor,\phaseUcolor,\phaseVcolor,\phaseWcolor,\phaseUcolor,\phaseVcolor, + \phaseWcolor,\phaseUcolor,\phaseVcolor,\phaseWcolor,\phaseUcolor,\phaseVcolor}} %front of slot + \def\NumSlots{0} + \def\SlotWidth{3.6} + \def\SlotDepth{6.0} + \def\IntraSlotDepth{.75} + \def\StatorRotOffset{-60} %angle that slot 1 should be at with respect to the x axis + \def\StatorOR{16} + \def\StatorIR{12} + \def\drawPMrotor{1} + \def\PMrotorPoles{8} + \def\PMnorthColor{red!20!white} + \def\PMsouthColor{green!20!white} + \def\showRotorAngle{1} + \def\showRotorAngleTwo{1} + \def\rotorAngleTwoStart{-30} + \def\PMrotorRotation{15} + \def\showAlpha{0} + \def\NumberSlots{0} + \def\UseCond{0} + \def\rotorAngleTwoLabel{$\theta_{\rm enc}$} + \def\rotorAngleLabel{$\theta_{\rm m}$} +} + +\setupFig %must be defined, this can overwrite the above definitions + + + +%Stator laminations +\filldraw[fill=gray!40!white] circle(\StatorOR); +\filldraw[fill=white,very thin] circle (\StatorIR); + + +%make slots: +%find x y coordinate for slot + \pgfmathparse{(\SlotWidth/4.5)/\StatorOR}; %was: \SlotWidth/6 + \edef\slope{\pgfmathresult}; + \pgfmathparse{sqrt((\StatorIR)^2/(1+\slope^2))}; + \edef\xcord{\pgfmathresult}; + \pgfmathparse{sqrt((\StatorIR)^2/(1+(1/\slope)^2))}; + \edef\ycord{\pgfmathresult}; + + \pgfmathparse{sqrt((\StatorIR-1)^2/(1+\slope^2))}; + \edef\xcordsnip{\pgfmathresult}; + \pgfmathparse{sqrt((\StatorIR-1)^2/(1+(1/\slope)^2))}; + \edef\ycordsnip{\pgfmathresult}; + + \ifthenelse{\NumSlots = 0}{}{ + \begin{scope} + \foreach \x in {1,2,...,\NumSlots} + { + \path[fill= white,draw=black,rotate=360/\NumSlots*(\x-1)+\StatorRotOffset, name path =slotPath] (\xcord, \ycord) -- ++(\IntraSlotDepth,0) -- ++(0, \SlotWidth/3) -- ++(\SlotDepth, 0) -- ++(0,-\SlotWidth) -- ++(-\SlotDepth,0) -- ++(0, \SlotWidth/3) -- ++(-\IntraSlotDepth,0) -- cycle; + } + \end{scope} + + \begin{scope} + \foreach \x in {1,2,...,\NumSlots} + { + \pgfmathparse{\TopLayerLabels[\x-1]}; + \edef\TopLayerLabel{\pgfmathresult}; + \pgfmathparse{\BotLayerLabels[\x-1]}; + \edef\BotLayerLabel{\pgfmathresult}; + \path[fill= white,rotate=360/\NumSlots*(\x-1)+\StatorRotOffset, name path =slotPath](\xcordsnip, \ycordsnip) -- ++(\IntraSlotDepth+1,0) -- ++(0, \SlotWidth/3) -- ++(\SlotDepth, 0) -- ++(0,-\SlotWidth) -- ++(-\SlotDepth,0) -- ++(0, \SlotWidth/3) -- ++(-\IntraSlotDepth-1,0) -- cycle; + \ifthenelse{\UseCond = 1} + { + \pgfmathparse{\TopCondColors[\x-1]} + \edef\TopCondColor{\pgfmathresult} + \pgfmathparse{\BotCondColors[\x-1]} + \edef\BotCondColor{\pgfmathresult} + + \filldraw[fill = \BotCondColor, very thin,rotate=360/\NumSlots*(\x-1)+\StatorRotOffset-.5] (\StatorIR+\IntraSlotDepth+\CondRadius+.25,0) circle(\CondRadius) node[font=\tiny]{\BotLayerLabel}; + \filldraw[fill = \TopCondColor, very thin, rotate=360/\NumSlots*(\x-1)+\StatorRotOffset-.5] (\StatorIR+\IntraSlotDepth+\SlotDepth-\CondRadius-.25,0) circle(\CondRadius) node[font=\nodeFontSize]{\TopLayerLabel}; + } + { + \draw[rotate=360/\NumSlots*(\x-1)+\StatorRotOffset] (\StatorIR + \IntraSlotDepth+\SlotDepth/2, -\SlotWidth/2) -- +(0, \SlotWidth); + \draw [rotate=360/\NumSlots*(\x-1)+\StatorRotOffset](\StatorIR + \IntraSlotDepth + \SlotDepth/4, 0) node{\BotLayerLabel}; + \draw [rotate=360/\NumSlots*(\x-1)+\StatorRotOffset](\StatorIR + \IntraSlotDepth + 3*\SlotDepth/4, 0) node{\TopLayerLabel}; + } + \ifthenelse{\NumberSlots = 1} + { + \draw [rotate=360/\NumSlots*(\x-1)+\StatorRotOffset](\StatorIR + \IntraSlotDepth + 5*\SlotDepth/4, 0) node{\x}; + }{} + } + \end{scope}} + + \begin{scope}[rotate = \PMrotorRotation] + + \ifthenelse{\drawPMrotor = 1} + { + \edef\rotorPMOR{\StatorIR-\PMrotorAG} + \edef\rotorShaftOR{\rotorPMOR - \PMrotorPMthick} + \edef\PMangle{180/\PMrotorPoles} + + \fill[gray!40!white] (0,0) circle(\rotorShaftOR); + \foreach \x in {1,2,...,\PMrotorPoles} + { + \begin{scope}[rotate=(\x-1)*\PMangle*2] + \pgfmathparse{int(mod(\x,2))} + \edef\NorthPole{\pgfmathresult} + + \def\pmColor{\PMnorthColor} + \def\pmColor{\PMsouthColor} + \ifthenelse{\NorthPole = 1} + { + \def\pmColor{\PMnorthColor} + } + { + \def\pmColor{\PMsouthColor} + } + %\filldraw[gray!40!white,draw=black] ([shift=(-180\PMrotorPoles:\rotorShaftOR)]0,0) arc (-180/\PMrotorPoles:180/\PMrotorPole:\rotorShaftOR); + \fill[\pmColor] ([shift=(-\PMangle:\rotorShaftOR)]0,0) arc (-\PMangle:\PMangle:\rotorShaftOR) + -- (\PMangle:\rotorPMOR) + -- ([shift=(\PMangle:\rotorPMOR)]0,0) arc (\PMangle:-\PMangle:\rotorPMOR) + -- (-\PMangle:\rotorShaftOR); + \draw([shift=(-\PMangle:\rotorShaftOR)]0,0) arc (-\PMangle:\PMangle:\rotorShaftOR) + -- (\PMangle:\rotorPMOR) + -- ([shift=(\PMangle:\rotorPMOR)]0,0) arc (\PMangle:-\PMangle:\rotorPMOR); + + + \ifthenelse{\NorthPole = 1} + { + \draw[->] (\rotorShaftOR+.25, 0) -- (\rotorPMOR-.25, 0); + } + { + \draw[<-] (\rotorShaftOR+.25, 0) -- (\rotorPMOR-.25, 0); + } + \end{scope} + } + + } + {} +\end{scope} + +\ifthenelse{\showRotorAngle = 1} + { + \draw(\PMrotorRotation:\StatorIR-\PMrotorAG/2) -- (\PMrotorRotation:\StatorOR+5); + \draw(0:\StatorOR+.5) -- (0:\StatorOR+9) node[right]{phase $u$ axis}; + %\draw[dashed](0:\StatorOR+5) -- (0:\StatorOR+10) node[right]{phase $u$ axis}; + \draw[->]([shift=(0:\StatorOR+2.5)]0,0) arc (0:\PMrotorRotation:\StatorOR+2.5) node[xshift=-3,pos=0.5, right]{\rotorAngleLabel}; + } + {} + +\ifthenelse{\showRotorAngleTwo = 1} +{ + \draw(\PMrotorRotation:\StatorIR-\PMrotorAG/2) -- (\PMrotorRotation:\StatorOR+10); + \draw(\rotorAngleTwoStart:\StatorOR+.5) -- (\rotorAngleTwoStart:\StatorOR+10) node[pos=1, right,rotate=\rotorAngleTwoStart] {z-pulse}; + \draw[->]([shift=(\rotorAngleTwoStart:\StatorOR+7.5)]0,0) arc (\rotorAngleTwoStart:\PMrotorRotation:\StatorOR+7.5) node[xshift=-2,pos=0.85, right]{\rotorAngleTwoLabel}; + \draw[->]([shift=(\rotorAngleTwoStart:\StatorOR+2.5)]0,0) arc (\rotorAngleTwoStart:0:\StatorOR+2.5) node[xshift=-3,pos=0.5, right]{$\theta_{\rm off}$}; +} +{} + +\ifthenelse{\showAlpha = 1} +{ + \draw(0,0) -- (60:\StatorIR-\PMrotorAG-\PMrotorPMthick*1.5); + \draw(0,0) -- (0:\StatorIR-\PMrotorAG-\PMrotorPMthick*1.5); + %\draw(0:\StatorOR+.5) -- (0:\StatorOR+5); + \draw[->]([shift=(0:\StatorIR-\PMrotorAG-\PMrotorPMthick*1.5-2.5)]0,0) arc (0:60:\StatorIR-\PMrotorAG-\PMrotorPMthick*1.5-2.5) node[pos=0.95, right]{$\alpha$}; +} +{} +\showAlpha{1} +\end{tikzpicture} +} +\end{document} + From cb1c257d687b4eb4d1978a4e6d05edcc60b52a52 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sat, 7 Jun 2025 17:55:03 -0500 Subject: [PATCH 26/57] add test code --- .../control-with-amdc/encoder-fb/index.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 4142868f..690863f9 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -1,5 +1,17 @@ # Encoder Feedback +[the encoder](/firmware/arch/drivers/encoder.md) + +[firmware](/firmware/index.rst) + +[arch](/firmware/arch/index.md) + +[timing manager](/firmware/arch/timing-manager.md) + +[drivers](/firmware/arch/drivers/index.md) + +[logging](/getting-started/user-guide/logging/index.md) + ## Background Encoders are used to determine the rotor position and speed, and are the typical method of feedback to the control system in a motor drive. This document explains how to use the AMDC's encoder interface to extract high quality rotor position and speed data. From 41096e6bee2468ed3602ba1cd3944db4daa6e35e Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sat, 7 Jun 2025 22:28:48 -0500 Subject: [PATCH 27/57] - Edit everything before finding the offset - Fix links --- .../control-with-amdc/encoder-fb/index.md | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 690863f9..ed3130dc 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -1,25 +1,13 @@ # Encoder Feedback -[the encoder](/firmware/arch/drivers/encoder.md) - -[firmware](/firmware/index.rst) - -[arch](/firmware/arch/index.md) - -[timing manager](/firmware/arch/timing-manager.md) - -[drivers](/firmware/arch/drivers/index.md) - -[logging](/getting-started/user-guide/logging/index.md) - ## Background Encoders are used to determine the rotor position and speed, and are the typical method of feedback to the control system in a motor drive. This document explains how to use the AMDC's encoder interface to extract high quality rotor position and speed data. For more information: -- on how encoders work and are interfaced with the AMDC, see the [encoder hardware subsystem page](https://docs.amdc.dev/hardware/subsystems/encoder.html#) -- on the driver functionality included with the AMDC firmware, see the [encoder driver architecture page](https://docs.amdc.dev/firmware/arch/drivers/encoder.html). +- on how encoders work and are interfaced with the AMDC, see the [encoder hardware subsystem page](/hardware/subsystems/encoder.md); +- on the driver functionality included with the AMDC firmware, see the [encoder driver architecture page](/firmware/arch/drivers/encoder.md). ## Rotor Position @@ -57,52 +45,63 @@ The recommended approach to reading the shaft position from the encoder is illus -First, the user uses the AMDC [`drv/encoder`](/firmware/arch/drivers/encoder.md) driver module function `encoder_get_position()` to get the count of the encoder reading. The`drv/encoder` driver module also has a function called `encoder_get_steps()` which gives the incremental change in the encoder position. Whereas, `encoder_get_position()` gives the actual position of the shaft and this can be converted to rotor position in radians. +First, the AMDC [`drv/encoder`](/firmware/arch/drivers/encoder.md) driver module function `encoder_get_position()` is used to obtain the the encoder's count $\theta_{\rm enc}$ since the last z-pulse. + +```{tip} +The [`drv/encoder`](/firmware/arch/drivers/encoder.md) driver module also has a function called `encoder_get_steps()` which returns the encoder's count since power-on. One rotation direction increments, the other decrements. This value does not wrap around (it ignores `encoder_set_counts_per_rev()` and the z-pulse). Users are advised to use `encoder_get_position()`, which does wrap around and tracks the z-pulse. +``` + +Next, the user should calculate $\theta_{\rm m}$ from $\theta_{\rm enc}$. This is done by 1) removing the offset and 2) converting counts into radians. For the the angles defined as shown in the image above, this is simply calculated as + +$$ +\theta_{\rm m} = \tfrac{2\pi}{\rm COUNTS\_PER\_REV} \left( \theta_{\rm enc} - \theta_{\rm off} \right) +$$ (eq:convCCW) + +In this case, a counter-clockwise rotation of the rotor causes the $\theta_{\rm enc}$ to increase. However, in some teststands a clockwise rotation causes $\theta_{\rm enc}$ to increment. For these encoders, $\theta_{\rm m}$ is calculated as + +$$ +\theta_{\rm m} &= \tfrac{2\pi}{\rm COUNTS\_PER\_REV} \left({\scriptstyle \rm COUNTS\_PER\_REV} - \theta_{\rm enc} + \theta_{\rm off} \right) \\ &= 2\pi - \theta_{\rm m, CCW} +$$ (eq:convCW) + +```{tip} +The user can experimentally determine whether the encoder count increases with counter-clockwise rotation of the shaft by rotating the shaft and using [logging](/getting-started/user-guide/logging/index.md) to observe the trend of $\theta_{\rm enc}$. +``` - Next, the user needs to verify if the encoder count is increasing or decreasing with counter-clock wise rotation of shaft. This may be done manually by rotating the shaft and observing the trend of the reported position with respect to the direction of rotation. In the figure, the signal `CCW` indicates this directionality. It must be set to `1` if the encoder count increases with counter clockwise rotation of shaft and `0` otherwise. Additionally, the user needs to provide the encoder offset, `offset`, and the encoder counts per revolution, `ENCODER_COUNT_PER_REV`. A method to get the value of offset is described in the next [subsection](#finding-the-offset). Using all of these quantities, the obtained count can be translated into angular position using a simple linear equation. Note that this document follows the convention of a positive rotor angle in the counter clockwise direction of shaft rotation while calculating rotor position from the count signal. - - Finally, the user must ensure that angle is within the bounds of $0$ and $2\pi$ by appropriately wrapping the `rotor position` signal using the `mod` function. This is shown in the final block in the diagram. +Finally, the user must ensure that angle is within the bounds of $0$ and $2\pi$ by appropriately wrapping the $\theta_{\rm m}$. This can be accomplished in C by using the `mod` function. This is shown in the final block in the diagram. -Example code to convert encoder to angular position in radians: +Here is example code to convert the encoder to angular position in radians (note that this assumes the encoder offset $\theta_{\rm off}$ is already know; a procedure to determine this is described in the next [subsection](#finding-the-offset)): ```C double task_get_theta_m(void) { - // Get raw encoder position - uint32_t position; - encoder_get_position(&position); - - int ENCODER_COUNT_PER_REV, CCW; - double enc_theta_m_offset; + // User to set encoder offset + double theta_off = 100; // User to set encoder count per revolution - ENCODER_COUNT_PER_REV = 1024; + double ENCODER_COUNT_PER_REV = 1024; - // Set 1 if encoder count increases with CCW rotation of shaft, Set 0 if encoder count increases with CW rotation of shaft - CCW = 1; + // User to set 1 if encoder count increases with CCW rotation of shaft, set 0 if encoder count increases with CW rotation of shaft + int CCW_ROTATION_FLAG = 1; // Angular position to be computed - double theta_m_enc; - - // User to set encoder offset - enc_theta_m_offset = 100; + double theta_m; + // Get raw encoder position + uint32_t theta_enc; + encoder_get_position(&theta_enc); // Convert to radians - if (CCW){ - theta_m_enc = (double) PI2 * ( ( (double)position - enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); - } - else{ - theta_m_enc = (double) PI2 * ( ( (double)ENCODER_COUNT_PER_REV - (double)1 -(double)position + enc_theta_m_offset )/ (double) ENCODER_COUNT_PER_REV); + theta_m = (double) PI2 * ( ((double)theta_enc - theta_off) / (double) ENCODER_COUNT_PER_REV); + + if (!CCW_ROTATION_FLAG){ + theta_m = PI2 - theta_m; } - + // Mod by 2 pi - theta_m_enc = fmod(theta_m_enc,PI2); - return theta_m_enc; + theta_m = fmod(theta_m,PI2); + return theta_m; } ``` - - ### Finding the offset The example code shown above makes uses of an encoder offset value, `enc_theta_m_offset`. It is necessary for the user to find this offset experimentally for their machine. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. From 9e7a82b686c06c158b99001652bf46b2af7d2356 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sat, 7 Jun 2025 22:40:42 -0500 Subject: [PATCH 28/57] change image naming to dash-case --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- .../{MotorCrossSection.svg => motor-cross-section.svg} | 0 .../{MotorCrossSection.tex => motor-cross-section.tex} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename source/getting-started/control-with-amdc/encoder-fb/resources/{MotorCrossSection.svg => motor-cross-section.svg} (100%) rename source/getting-started/control-with-amdc/encoder-fb/resources/{MotorCrossSection.tex => motor-cross-section.tex} (100%) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index ed3130dc..b475c5c3 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -15,7 +15,7 @@ The AMDC supports [incremental encoders with quadrature ABZ outputs](https://en. This document assumes the configuration shown in the figure below, where the control code expects a measurement of the angle of the rotor's north pole relative to the phase $u$ magnetic axis, labeled as $\theta_{\rm m}$. The encoder provides $\theta_{\rm enc}$, which is the number of counts since the last z-pulse. The user's code needs to convert $\theta_{\rm enc}$ (in units of counts) into $\theta_{\rm m}$ (likely in units of radians) and handle an offset angle $\theta_{\rm off}$ between the encoder's 0 position and the phase $u$ axis. -![Motor Cross-Section with Encoder Angles](resources/MotorCrossSection.svg) +![Motor Cross-Section with Encoder Angles](resources/motor-cross-section.svg) ### Configuring the encoder diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.svg similarity index 100% rename from source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.svg rename to source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.svg diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.tex b/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.tex similarity index 100% rename from source/getting-started/control-with-amdc/encoder-fb/resources/MotorCrossSection.tex rename to source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.tex From 600fc24b1cd17d63223e7ed61319af501d64f339 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sun, 8 Jun 2025 09:01:43 -0500 Subject: [PATCH 29/57] Improve typesetting around the image --- .../getting-started/control-with-amdc/encoder-fb/index.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index b475c5c3..40ae502d 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -13,9 +13,13 @@ For more information: The AMDC supports [incremental encoders with quadrature ABZ outputs](https://en.wikipedia.org/wiki/Incremental_encoder#Quadrature_outputs) and a fixed number of counts per revolution `CPR` (for example, `CPR = 1024`). The user needs to provide code that interfaces to the AMDC's drivers to read the encoder count and convert it into usable angular information that is suitable for use within the control code. -This document assumes the configuration shown in the figure below, where the control code expects a measurement of the angle of the rotor's north pole relative to the phase $u$ magnetic axis, labeled as $\theta_{\rm m}$. The encoder provides $\theta_{\rm enc}$, which is the number of counts since the last z-pulse. The user's code needs to convert $\theta_{\rm enc}$ (in units of counts) into $\theta_{\rm m}$ (likely in units of radians) and handle an offset angle $\theta_{\rm off}$ between the encoder's 0 position and the phase $u$ axis. +```{image} resources/motor-cross-section.svg +:alt: Motor Cross-Section with Encoder Angles +:width: 350px +:align: right +``` -![Motor Cross-Section with Encoder Angles](resources/motor-cross-section.svg) +This document assumes the configuration shown to the right, where the control code expects a measurement of the angle of the rotor's north pole relative to the phase $u$ magnetic axis, labeled as $\theta_{\rm m}$. The encoder provides $\theta_{\rm enc}$, which is the number of counts since the last z-pulse. The user's code needs to convert $\theta_{\rm enc}$ (in units of counts) into $\theta_{\rm m}$ (likely in units of radians) and handle an offset angle $\theta_{\rm off}$ between the encoder's 0 position and the phase $u$ axis. ### Configuring the encoder From cc7f0380323c5d2826a5baa308c1e713bd67404a Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sun, 8 Jun 2025 11:04:30 -0500 Subject: [PATCH 30/57] Add torque characteristic and update step 1 instructions. --- .../control-with-amdc/encoder-fb/index.md | 19 +- .../encoder-fb/resources/torque-plot.svg | 203 ++++++++++++++++++ .../encoder-fb/resources/torque-plot.tex | 38 ++++ 3 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/torque-plot.svg create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/torque-plot.tex diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index e00686d8..e84bf56f 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -104,19 +104,28 @@ double task_get_theta_m(void) ### Finding the offset -The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. This value typically needs to be found experimentally for each motor/encoder pair because it depends on how the encoder was aligned when it was coupled to the motor's shaft. This section provides a procedure to determine `enc_theta_m_offset`. +The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. This value typically needs to be found experimentally for each motor/encoder pair because it depends on how the encoder was aligned when it was coupled to the motor's shaft. This section provides a procedure to determine `enc_theta_m_offset`. #### Step 1: Determine approximate offset -The approximate encoder offset can be found using the following simple procedure without feedback control: +```{image} resources/torque-plot.svg +:alt: Torque Variation with Rotor Angle +:width: 250px +:align: right +``` + +The approximate encoder offset can be found by taking advantage of the motor having the torque characteristic shown on the right. This corresponds to [the image at the start of the section](#rotor-position) and positive torque is in the counter-clockwise direction. + +The following simple procedure can be used without any feedback control: 1. Set the `enc_theta_m_offset` to 0 in the control code `task_get_theta_m()`. 2. Eliminate any source of load torque on the shaft. -3. Align the rotor with the phase U winding axis by applying a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). This could be accomplished by: +3. Power on the AMDC and rotate the rotor manually by one revolution (so that the encoder z-pulse is detected). +4. Align the rotor with the phase U winding axis by applying a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). This could be accomplished by: 1) Using a DC power supply, or 2) Injecting a current command on the d-axis using the AMDC [Signal Injection](/getting-started/user-guide/injection/index.rst) module with `theta_m_enc` fixed to 0. -4. Record the current encoder position and use this as the offset value: `enc_theta_m_offset = encoder_get_position();`. -5. Update the variable `enc_theta_m_offset` to the appropriate value in `task_get_theta_m()`. +5. Record the current encoder position and use this as the offset value: `enc_theta_m_offset = encoder_get_position();`. +6. Update the variable `enc_theta_m_offset` to the appropriate value in `task_get_theta_m()`. #### Step 2: Determine precise offset diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/torque-plot.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/torque-plot.svg new file mode 100644 index 00000000..b88ee231 --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/torque-plot.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/torque-plot.tex b/source/getting-started/control-with-amdc/encoder-fb/resources/torque-plot.tex new file mode 100644 index 00000000..d0f3e21e --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/torque-plot.tex @@ -0,0 +1,38 @@ +\documentclass[class=IEEEtran]{standalone}%[conference,10pt]{IEEEtran} +\usepackage{amsmath} +\ifCLASSINFOpdf +\usepackage[pdftex]{graphicx} +\else +\usepackage[dvips]{graphicx} +\fi + +\usepackage{color} % required for `\textcolor' (yatex added) + +%ERIC ADDED: +\usepackage{xcolor, tikz, pgfplots, ifthen} +\usepackage[american,cuteinductors,smartlabels]{circuitikz} +\pgfplotsset{compat=newest} +\usetikzlibrary{intersections,calc,backgrounds} + +\begin{document} + + + \centering + + \begin{tikzpicture}[>=stealth] + % Axes + \draw[thick, ->] (0,0) -- (5,0) node[anchor=west, below, pos=0.9] {$\theta_{\rm enc}$}; + \draw[thick] (0,-1.75) -- (0,1.75) node[anchor=south, rotate = 90, pos = 0.5] {Shaft Torque}; + + % Blue sinusoidal curve + \draw[blue, ultra thick, domain=0:5, samples=100] plot (\x, {1.5*sin(360*\x/5 - 10)}); + + % Red arrow and label + \draw[red, thick, ->] (0,-.25) -- (2.64,-.25) node[pos = 0.8, anchor=north] {$\theta_{\rm off}$}; + \draw[red, thick] (2.64, -.4) -- (2.64, 0); + + % Text at the top + \node at (2.5,1.75) {for $I_u = I_o$, $I_v = I_w = -\frac{1}{2} I_o$}; + \end{tikzpicture} +\end{document} + From 5e845e77a3c2d70c957482848c99b630f2afe2be Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sun, 8 Jun 2025 11:12:54 -0500 Subject: [PATCH 31/57] Make motor cross-section plot 2 poles, add phave v and w axes --- .../resources/motor-cross-section.svg | 266 +++++++++++------- .../resources/motor-cross-section.tex | 4 +- 2 files changed, 165 insertions(+), 105 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.svg index fc53967c..6c6b7b65 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.svg +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.svg @@ -1,5 +1,5 @@ - + @@ -30,136 +30,194 @@ - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.tex b/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.tex index e5822bae..d5878df3 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.tex +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/motor-cross-section.tex @@ -103,7 +103,7 @@ \def\StatorOR{16} \def\StatorIR{12} \def\drawPMrotor{1} - \def\PMrotorPoles{8} + \def\PMrotorPoles{2} \def\PMnorthColor{red!20!white} \def\PMsouthColor{green!20!white} \def\showRotorAngle{1} @@ -230,6 +230,8 @@ { \draw(\PMrotorRotation:\StatorIR-\PMrotorAG/2) -- (\PMrotorRotation:\StatorOR+5); \draw(0:\StatorOR+.5) -- (0:\StatorOR+9) node[right]{phase $u$ axis}; + \draw[rotate=120](0:\StatorOR+.5) -- (0:\StatorOR+9) node[right, xshift=-2, yshift=9, rotate=120+180]{phase $v$ axis}; + \draw[rotate=240](0:\StatorOR+.5) -- (0:\StatorOR+9) node[right, xshift=-7, yshift=-8, rotate=240+180]{phase $w$ axis}; %\draw[dashed](0:\StatorOR+5) -- (0:\StatorOR+10) node[right]{phase $u$ axis}; \draw[->]([shift=(0:\StatorOR+2.5)]0,0) arc (0:\PMrotorRotation:\StatorOR+2.5) node[xshift=-3,pos=0.5, right]{\rotorAngleLabel}; } From d0d86d67031b7405c996a2cce9d3790540c2ef94 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Sun, 8 Jun 2025 11:17:22 -0500 Subject: [PATCH 32/57] Add clarifying comment to torque characteristic --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 06871d9d..abf62c1a 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -118,7 +118,7 @@ The example code shown above makes use of an encoder offset value, `enc_theta_m_ :align: right ``` -The approximate encoder offset can be found by taking advantage of the motor having the torque characteristic shown on the right. This corresponds to [the image at the start of the section](#rotor-position) and positive torque is in the counter-clockwise direction. +The approximate encoder offset can be found by taking advantage of the motor having the torque characteristic shown on the right. This depicts shaft torque as the shaft is rotated counter-clockwise and corresponds to [the image at the start of the section](#rotor-position); positive torque is in the counter-clockwise direction. The following simple procedure can be used without any feedback control: From 9341c81925cff94790a123b9fd11c68c74469e39 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Sun, 8 Jun 2025 16:04:11 -0500 Subject: [PATCH 33/57] Update index.md Apply feedback from Professor's comment (option 2) to revise the steps to find offset --- .../getting-started/control-with-amdc/encoder-fb/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index abf62c1a..6e81667c 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -137,10 +137,10 @@ Friction and cogging torque in the motor decrease the accuracy of the estimate i 1. Configure the AMDC for closed-loop speed and DQ current control and configure the operating environment to allow for quick edits to `enc_theta_m_offset` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate in steady speed under no load conditions. Use the estimated `enc_theta_m_offset` obtained in Step 1. -3. Determine the value of `enc_theta_m_offset` that results in the d-axis voltage being closest to 0V. Do this by monitoring the d-axis voltage while adjusting `enc_theta_m_offset` gradually until the sign of the d-axis voltage flips. -4. Repeat step 3 at several speeds. -5. For each offset value, average the d-axis voltage values for different speeds and select the offset with the lowest average d-axis voltage. -6. Plot the d-axis voltage with the selected offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly. +3. Sweep `enc_theta_m_offset` over a small range around the initial estimate (e.g., ±5 counts). For each value, monitor the d-axis voltage and find the `enc_theta_m_offset` that makes the d-axis voltage closest to 0 V. Idnetify this by observing when the sign of the d-axis voltage changes. +4. Repeat step 3 at multiple rotor speeds. At each speed, record the `enc_theta_m_offset` value that minimizes the d-axis voltage. +5. Take the average of the colleceted `enc_theta_m_offset` values from part 4 to determine the final encoder offset value. +6. Plot the d-axis voltage with the final offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly. 7. In-case there is an error in the offset value, a significant speed-dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. ## Computing Speed from Position From fc88ad46755d1fba074cfaceadc1b660c8af0ef7 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Sun, 8 Jun 2025 16:08:23 -0500 Subject: [PATCH 34/57] Update the encoder offset plot --- .../encoder-fb/resources/encoder-offset.svg | 1623 +++++++++++++++++ 1 file changed, 1623 insertions(+) create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/encoder-offset.svg diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/encoder-offset.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/encoder-offset.svg new file mode 100644 index 00000000..c974104e --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/encoder-offset.svg @@ -0,0 +1,1623 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + 500 + + + + + 1000 + + + + + 1500 + + + + + 2000 + + + + + 2500 + + + + + 3000 + + + + + Rotational speed (RPM) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -0.5 + + + + + -0.4 + + + + + -0.3 + + + + + -0.2 + + + + + -0.1 + + + + + 0 + + + + + 0.1 + + + + + 0.2 + + + + + 0.3 + + + + + 0.4 + + + + + 0.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2747e11b587d79361d6e0e1023c7e99dfe474a54 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Sun, 8 Jun 2025 16:17:23 -0500 Subject: [PATCH 35/57] Update index.md Update the image --- .../getting-started/control-with-amdc/encoder-fb/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 6e81667c..7ce81cad 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -143,6 +143,12 @@ Friction and cogging torque in the motor decrease the accuracy of the estimate i 6. Plot the d-axis voltage with the final offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly. 7. In-case there is an error in the offset value, a significant speed-dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. +An example of the results are shown in the plot below. After the calibration process, the updated encoder offset results in the d-axis voltage being closer to 0 across different speeds compared to the previous value. + +

    + +
    + ## Computing Speed from Position The user needs to compute a rotor speed signal from the obtained position signal to be used in the control algorithm. There are several ways to do this. From 24ff6a8d787c11a74961085efc51cc9a6387c263 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Sun, 8 Jun 2025 17:05:42 -0500 Subject: [PATCH 36/57] Update index.md Add the formula --- .../control-with-amdc/encoder-fb/index.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 7ce81cad..1086a347 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -133,7 +133,17 @@ The following simple procedure can be used without any feedback control: #### Step 2: Determine precise offset -Friction and cogging torque in the motor decrease the accuracy of the estimate in Step 1. The precise offset can be found by fine-tuning the `enc_theta_m_offset` from Step 1 while using closed-loop control to rotate the shaft at significant speed. This can be done as follows: +Friction and cogging torque in the motor decrease the accuracy of the estimate in Step 1. The precise offset can be found by fine-tuning the `enc_theta_m_offset` from Step 1 while using closed-loop control to rotate the shaft at significant speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. + +$$ +v_d = (R + pL) i_d - \hat{\omega}\_e L i_q - \hat{\omega}\_e \lambda\_{\mathrm{pm}} \sin(\tilde{\theta}\_e) +$$ + +$$ +\tilde{\theta}\_e = \theta\_e - \hat{\theta}\_e +$$ + +When the current commands are set to $i_d = i_q = 0$, the $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate. Step 2 describes how to determine the encoder offset by finding the condition where $v_d = 0$. 1. Configure the AMDC for closed-loop speed and DQ current control and configure the operating environment to allow for quick edits to `enc_theta_m_offset` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate in steady speed under no load conditions. Use the estimated `enc_theta_m_offset` obtained in Step 1. From 4a8698a9fc66f0e061b6a31e750807d4f54292aa Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Sun, 8 Jun 2025 20:33:00 -0500 Subject: [PATCH 37/57] Update source/getting-started/control-with-amdc/encoder-fb/index.md Co-authored-by: Takahiro <114006024+noguchi-takahiro@users.noreply.github.com> --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 1086a347..71769d46 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -133,7 +133,7 @@ The following simple procedure can be used without any feedback control: #### Step 2: Determine precise offset -Friction and cogging torque in the motor decrease the accuracy of the estimate in Step 1. The precise offset can be found by fine-tuning the `enc_theta_m_offset` from Step 1 while using closed-loop control to rotate the shaft at significant speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. +Friction and cogging torque in the motor decrease the accuracy of the estimate in Step 1. The precise offset can be found by fine-tuning the `enc_theta_m_offset` from Step 1 while using closed-loop control to rotate the shaft at at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. $$ v_d = (R + pL) i_d - \hat{\omega}\_e L i_q - \hat{\omega}\_e \lambda\_{\mathrm{pm}} \sin(\tilde{\theta}\_e) From ae4a93f999e03d13bded695b2e0edd2a0f51baae Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Sun, 8 Jun 2025 20:37:49 -0500 Subject: [PATCH 38/57] Update index.md --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 71769d46..632b3465 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -136,7 +136,7 @@ The following simple procedure can be used without any feedback control: Friction and cogging torque in the motor decrease the accuracy of the estimate in Step 1. The precise offset can be found by fine-tuning the `enc_theta_m_offset` from Step 1 while using closed-loop control to rotate the shaft at at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. $$ -v_d = (R + pL) i_d - \hat{\omega}\_e L i_q - \hat{\omega}\_e \lambda\_{\mathrm{pm}} \sin(\tilde{\theta}\_e) +v_d = (R_d + pL_d) i_d - \hat{\omega}\_e L_q i_q - \hat{\omega}\_e \lambda\_{\mathrm{pm}} \sin(\tilde{\theta}\_e) $$ $$ From 99a620f7f50a2941b001a527c2d3f8699d6f7203 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 9 Jun 2025 16:41:37 -0500 Subject: [PATCH 39/57] Update index.md --- .../control-with-amdc/encoder-fb/index.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 632b3465..4dd12dc4 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -110,7 +110,7 @@ double task_get_theta_m(void) The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. This value typically needs to be found experimentally for each motor/encoder pair because it depends on how the encoder was aligned when it was coupled to the motor's shaft. This section provides a procedure to determine `enc_theta_m_offset`. -#### Step 1: Determine approximate offset +#### Determine the approximate offset ```{image} resources/torque-plot.svg :alt: Torque Variation with Rotor Angle @@ -131,7 +131,7 @@ The following simple procedure can be used without any feedback control: 5. Record the current encoder position and use this as the offset value: `enc_theta_m_offset = encoder_get_position();`. 6. Update the variable `enc_theta_m_offset` to the appropriate value in `task_get_theta_m()`. -#### Step 2: Determine precise offset +#### Determine precise offset Friction and cogging torque in the motor decrease the accuracy of the estimate in Step 1. The precise offset can be found by fine-tuning the `enc_theta_m_offset` from Step 1 while using closed-loop control to rotate the shaft at at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. @@ -145,15 +145,15 @@ $$ When the current commands are set to $i_d = i_q = 0$, the $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate. Step 2 describes how to determine the encoder offset by finding the condition where $v_d = 0$. -1. Configure the AMDC for closed-loop speed and DQ current control and configure the operating environment to allow for quick edits to `enc_theta_m_offset` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. -2. Command the motor to rotate in steady speed under no load conditions. Use the estimated `enc_theta_m_offset` obtained in Step 1. -3. Sweep `enc_theta_m_offset` over a small range around the initial estimate (e.g., ±5 counts). For each value, monitor the d-axis voltage and find the `enc_theta_m_offset` that makes the d-axis voltage closest to 0 V. Idnetify this by observing when the sign of the d-axis voltage changes. +1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `enc_theta_m_offset` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. +2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `enc_theta_m_offset` obtained in [Finding the offset](#finding-the-offset). +3. Sweep `enc_theta_m_offset` over a small range around the initial estimate (e.g., ±5 counts). For each value, monitor the d-axis voltage and find the `enc_theta_m_offset` that makes the d-axis voltage closest to 0 V. Identify this by observing when the sign of the d-axis voltage changes. 4. Repeat step 3 at multiple rotor speeds. At each speed, record the `enc_theta_m_offset` value that minimizes the d-axis voltage. -5. Take the average of the colleceted `enc_theta_m_offset` values from part 4 to determine the final encoder offset value. -6. Plot the d-axis voltage with the final offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds, if the offset is tuned correctly. -7. In-case there is an error in the offset value, a significant speed-dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. +5. Take the average of the collected `enc_theta_m_offset` values from step 4 to determine the final encoder offset value. +6. Plot the d-axis voltage with the final offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds if the offset is tuned correctly. +7. In case there is an error in the offset value, a significant speed-dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. -An example of the results are shown in the plot below. After the calibration process, the updated encoder offset results in the d-axis voltage being closer to 0 across different speeds compared to the previous value. +An example of the results is shown in the plot below. After the calibration process, the updated encoder offset results in the d-axis voltage being closer to 0 across different speeds compared to the previous value.
    From 783a3a2d9f6b65dbd68021efb2ea41771e8ecf5d Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 9 Jun 2025 16:44:17 -0500 Subject: [PATCH 40/57] Update index.md --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 4dd12dc4..6747cf58 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -133,7 +133,7 @@ The following simple procedure can be used without any feedback control: #### Determine precise offset -Friction and cogging torque in the motor decrease the accuracy of the estimate in Step 1. The precise offset can be found by fine-tuning the `enc_theta_m_offset` from Step 1 while using closed-loop control to rotate the shaft at at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. +Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `enc_theta_m_offset` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. $$ v_d = (R_d + pL_d) i_d - \hat{\omega}\_e L_q i_q - \hat{\omega}\_e \lambda\_{\mathrm{pm}} \sin(\tilde{\theta}\_e) @@ -143,7 +143,7 @@ $$ \tilde{\theta}\_e = \theta\_e - \hat{\theta}\_e $$ -When the current commands are set to $i_d = i_q = 0$, the $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate. Step 2 describes how to determine the encoder offset by finding the condition where $v_d = 0$. +When the current commands are set to $i_d = i_q = 0$, the $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate. The following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. 1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `enc_theta_m_offset` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `enc_theta_m_offset` obtained in [Finding the offset](#finding-the-offset). From 281f09d3213fac71050b952700d18dfa954bdc39 Mon Sep 17 00:00:00 2001 From: Takahiro Noguchi Date: Mon, 9 Jun 2025 20:47:28 -0500 Subject: [PATCH 41/57] Fix rendering of equation --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 6747cf58..f36d828c 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -136,11 +136,11 @@ The following simple procedure can be used without any feedback control: Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `enc_theta_m_offset` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. $$ -v_d = (R_d + pL_d) i_d - \hat{\omega}\_e L_q i_q - \hat{\omega}\_e \lambda\_{\mathrm{pm}} \sin(\tilde{\theta}\_e) +v_d = (R_d + pL_d) i_d - \hat{\omega}_e L_q i_q - \hat{\omega}_e \lambda_{\mathrm{pm}} \sin(\tilde{\theta}_e) $$ $$ -\tilde{\theta}\_e = \theta\_e - \hat{\theta}\_e +\tilde{\theta}_e = \theta_e - \hat{\theta}_e $$ When the current commands are set to $i_d = i_q = 0$, the $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate. The following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. From dc92a10fb50caec8ee5dd0fbbe2afd76d7546f45 Mon Sep 17 00:00:00 2001 From: Takahiro Noguchi Date: Mon, 9 Jun 2025 20:47:45 -0500 Subject: [PATCH 42/57] Fix rendering issue of image --- .../getting-started/control-with-amdc/encoder-fb/index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index f36d828c..f91e4041 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -155,9 +155,11 @@ When the current commands are set to $i_d = i_q = 0$, the $v_d$ value should be An example of the results is shown in the plot below. After the calibration process, the updated encoder offset results in the d-axis voltage being closer to 0 across different speeds compared to the previous value. -
    - -
    +```{image} resources/encoder-offset.svg +:alt: Torque Variation with Rotor Angle +:width: 300px +:align: center +``` ## Computing Speed from Position From 655ec8244b3238060325ec7c96da2893f752d85f Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Fri, 13 Jun 2025 02:04:04 -0500 Subject: [PATCH 43/57] Update the encoder image --- .../encoder-fb/resources/encoder-offset.svg | 1873 +++-------------- 1 file changed, 250 insertions(+), 1623 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/encoder-offset.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/encoder-offset.svg index c974104e..f05fd80d 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/resources/encoder-offset.svg +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/encoder-offset.svg @@ -1,1623 +1,250 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - - - - 500 - - - - - 1000 - - - - - 1500 - - - - - 2000 - - - - - 2500 - - - - - 3000 - - - - - Rotational speed (RPM) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -0.5 - - - - - -0.4 - - - - - -0.3 - - - - - -0.2 - - - - - -0.1 - - - - - 0 - - - - - 0.1 - - - - - 0.2 - - - - - 0.3 - - - - - 0.4 - - - - - 0.5 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + From b5fe0f8d589ece7473c6cdbd211a94c047274223 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Fri, 13 Jun 2025 02:25:25 -0500 Subject: [PATCH 44/57] Update index.md Update the variable names --- .../control-with-amdc/encoder-fb/index.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index f91e4041..79f03853 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -108,7 +108,7 @@ double task_get_theta_m(void) ### Finding the offset -The example code shown above makes use of an encoder offset value, `enc_theta_m_offset`. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. This value typically needs to be found experimentally for each motor/encoder pair because it depends on how the encoder was aligned when it was coupled to the motor's shaft. This section provides a procedure to determine `enc_theta_m_offset`. +The example code shown above makes use of an encoder offset value, `theta_off`. For synchronous machines, this offset is the count value measured by the encoder when the d-axis of the rotor is aligned with the phase U winding axis of the stator. This value typically needs to be found experimentally for each motor/encoder pair because it depends on how the encoder was aligned when it was coupled to the motor's shaft. This section provides a procedure to determine `theta_off`. #### Determine the approximate offset @@ -122,18 +122,18 @@ The approximate encoder offset can be found by taking advantage of the motor hav The following simple procedure can be used without any feedback control: -1. Set the `enc_theta_m_offset` to 0 in the control code `task_get_theta_m()`. +1. Set the `theta_off` to 0 in the control code `task_get_theta_m()`. 2. Eliminate any source of load torque on the shaft. 3. Power on the AMDC and rotate the rotor manually by one revolution (so that the encoder z-pulse is detected). 4. Align the rotor with the phase U winding axis by applying a large current vector at 0 degrees ($I_u = I_0$, $I_v = I_w = -\frac{1}{2} I_0$). This could be accomplished by: 1) Using a DC power supply, or - 2) Injecting a current command on the d-axis using the AMDC [Signal Injection](/getting-started/user-guide/injection/index.rst) module with `theta_m_enc` fixed to 0. -5. Record the current encoder position and use this as the offset value: `enc_theta_m_offset = encoder_get_position();`. -6. Update the variable `enc_theta_m_offset` to the appropriate value in `task_get_theta_m()`. + 2) Injecting a current command on the d-axis using the AMDC [Signal Injection](/getting-started/user-guide/injection/index.rst) module with `theta_m` fixed to 0. +5. Record the current encoder position and use this as the offset value: `theta_off = encoder_get_position();`. +6. Update the variable `theta_off` to the appropriate value in `task_get_theta_m()`. #### Determine precise offset -Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `enc_theta_m_offset` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. +Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `theta_off` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. $$ v_d = (R_d + pL_d) i_d - \hat{\omega}_e L_q i_q - \hat{\omega}_e \lambda_{\mathrm{pm}} \sin(\tilde{\theta}_e) @@ -145,11 +145,11 @@ $$ When the current commands are set to $i_d = i_q = 0$, the $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate. The following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. -1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `enc_theta_m_offset` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. -2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `enc_theta_m_offset` obtained in [Finding the offset](#finding-the-offset). -3. Sweep `enc_theta_m_offset` over a small range around the initial estimate (e.g., ±5 counts). For each value, monitor the d-axis voltage and find the `enc_theta_m_offset` that makes the d-axis voltage closest to 0 V. Identify this by observing when the sign of the d-axis voltage changes. -4. Repeat step 3 at multiple rotor speeds. At each speed, record the `enc_theta_m_offset` value that minimizes the d-axis voltage. -5. Take the average of the collected `enc_theta_m_offset` values from step 4 to determine the final encoder offset value. +1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `theta_off` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. +2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `theta_off` obtained in [Finding the offset](#finding-the-offset). +3. Sweep `theta_off` over a small range around the initial estimate (e.g., ±5 counts). For each value, monitor the d-axis voltage and find the `theta_off` that makes the d-axis voltage closest to 0 V. Identify this by observing when the sign of the d-axis voltage changes. +4. Repeat step 3 at multiple rotor speeds. At each speed, record the `theta_off` value that minimizes the d-axis voltage. +5. Take the average of the collected `theta_off` values from step 4 to determine the final encoder offset value. 6. Plot the d-axis voltage with the final offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds if the offset is tuned correctly. 7. In case there is an error in the offset value, a significant speed-dependent voltage will appear on the d-axis voltage. In this case, the user may have to re-measure the encoder offset. From bc7db65c95b0a95181166b7c2aacd0ba96e71d47 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 22:53:45 -0600 Subject: [PATCH 45/57] Update the figure --- .../control-with-amdc/encoder-fb/resources/reference-frame.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 source/getting-started/control-with-amdc/encoder-fb/resources/reference-frame.svg diff --git a/source/getting-started/control-with-amdc/encoder-fb/resources/reference-frame.svg b/source/getting-started/control-with-amdc/encoder-fb/resources/reference-frame.svg new file mode 100644 index 00000000..9d7b2ea9 --- /dev/null +++ b/source/getting-started/control-with-amdc/encoder-fb/resources/reference-frame.svg @@ -0,0 +1 @@ +𝜹-axisq-axisd-axis𝜸-axis𝑽𝜽𝒆𝜽𝒆phase u-axis \ No newline at end of file From 59b88636e9addd273f729e7bbb3b840ca857c795 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:04:39 -0600 Subject: [PATCH 46/57] Clarify terminology for mechanical angle in documentation --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 79f03853..7ea0b4f5 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -19,7 +19,7 @@ The AMDC supports [incremental encoders with quadrature ABZ outputs](https://en. :align: right ``` -This document assumes the configuration shown to the right, where the control code expects a measurement of the angle of the rotor's north pole relative to the phase $u$ magnetic axis, labeled as $\theta_{\rm m}$. The encoder provides $\theta_{\rm enc}$, which is the number of counts since the last z-pulse. The user's code needs to convert $\theta_{\rm enc}$ (in units of counts) into $\theta_{\rm m}$ (likely in units of radians) and handle an offset angle $\theta_{\rm off}$ between the encoder's 0 position and the phase $u$ axis. +This document assumes the configuration shown to the right, where the control code expects a measurement of the angle of the rotor's north pole relative to the phase $u$ magnetic axis, labeled as a mechanical angle $\theta_{\rm m}$. The encoder provides $\theta_{\rm enc}$, which is the number of counts since the last z-pulse. The user's code needs to convert $\theta_{\rm enc}$ (in units of counts) into $\theta_{\rm m}$ (likely in units of radians) and handle an offset angle $\theta_{\rm off}$ between the encoder's 0 position and the phase $u$ axis. ### Configuring the encoder From e0e32232ac38c0511b434706d2e8fdd343164461 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:37:29 -0600 Subject: [PATCH 47/57] Enhance encoder offset determination section Added detailed explanation and equations for determining encoder offset using closed-loop control and voltage measurements. --- .../control-with-amdc/encoder-fb/index.md | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 7ea0b4f5..d7cf823a 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -133,17 +133,32 @@ The following simple procedure can be used without any feedback control: #### Determine precise offset -Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `theta_off` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing $\hat{\theta}_e$ using the following $v_d$ equation. +Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `theta_off` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing the estimated electrical angle $\hat{\theta}_e$ using the voltage in the $\gamma-\delta$ reference frame that is constructed based on estimated rotor states as shown in the figure below. + +```{image} resources/reference-frame.svg +:alt: Torque Variation with Rotor Angle +:width: 250px +:align: right +``` +The voltage can be expressed in complex vector form as follows. $$ -v_d = (R_d + pL_d) i_d - \hat{\omega}_e L_q i_q - \hat{\omega}_e \lambda_{\mathrm{pm}} \sin(\tilde{\theta}_e) +\vec{v} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \dot{\theta}_e \lambda_{\mathrm{pm}} e^{j{\theta}_e} $$ +This voltage vector can be converted into the $\gamma-\delta$ reference frame as follows. + +$$ +v_{\gamma} + j\ v_{\delta} = \vec{V} e^{-j\hat{\theta}_e} +$$ + +When the current commands are set to $i_d = i_q = 0$, $v_{\gamma}$ can be expressed as follows. + $$ -\tilde{\theta}_e = \theta_e - \hat{\theta}_e +\left. v_{\gamma} \right|_{i=0} = -\omega_e \lambda_{\mathrm{pm}} \sin(\theta_e - \hat{\theta}_e) $$ -When the current commands are set to $i_d = i_q = 0$, the $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate. The following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. +The $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate and there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$). Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. 1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `theta_off` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `theta_off` obtained in [Finding the offset](#finding-the-offset). From 64578a3ec54e90a36d5c5a8832de637e51f5b3d2 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:41:53 -0600 Subject: [PATCH 48/57] Clarify voltage equation with angular velocity note Added clarification about electrical angular velocity in the voltage equation. --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index d7cf823a..23a4c008 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -140,10 +140,10 @@ Friction and cogging torque in the motor decrease the accuracy of the estimate i :width: 250px :align: right ``` -The voltage can be expressed in complex vector form as follows. +The voltage can be expressed in complex vector form as follows. Note that $\omega_e$ is the electrical angular velocity with units of radians per second. $$ -\vec{v} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \dot{\theta}_e \lambda_{\mathrm{pm}} e^{j{\theta}_e} +\vec{v} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \omega_e \lambda_{\mathrm{pm}} e^{j{\theta}_e} $$ This voltage vector can be converted into the $\gamma-\delta$ reference frame as follows. From 5ac71e2855e6a70b054bbf11c344cb66f9b3d326 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:44:10 -0600 Subject: [PATCH 49/57] Clarify voltage vector description in index.md --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 23a4c008..c403b7ff 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -140,7 +140,7 @@ Friction and cogging torque in the motor decrease the accuracy of the estimate i :width: 250px :align: right ``` -The voltage can be expressed in complex vector form as follows. Note that $\omega_e$ is the electrical angular velocity with units of radians per second. +The voltage vector in the figure above can be expressed in complex vector form as follows. Note that $\omega_e$ is the electrical angular velocity with units of radians per second. $$ \vec{v} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \omega_e \lambda_{\mathrm{pm}} e^{j{\theta}_e} From 39163c8175b1454901afc0a5dcba905240b85b15 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:45:18 -0600 Subject: [PATCH 50/57] Fix voltage vector equation formatting Corrected the formatting of the voltage vector equation in complex vector form. --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index c403b7ff..77aa602f 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -143,7 +143,7 @@ Friction and cogging torque in the motor decrease the accuracy of the estimate i The voltage vector in the figure above can be expressed in complex vector form as follows. Note that $\omega_e$ is the electrical angular velocity with units of radians per second. $$ -\vec{v} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \omega_e \lambda_{\mathrm{pm}} e^{j{\theta}_e} +\vec{V} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \omega_e \lambda_{\mathrm{pm}} e^{j{\theta}_e} $$ This voltage vector can be converted into the $\gamma-\delta$ reference frame as follows. From b56bbdffd4fa7c53ea0c040cbff6c19b00604932 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:49:48 -0600 Subject: [PATCH 51/57] Refine description of encoder offset estimation process Clarified the conditions for estimating encoder offset by refining language regarding the alignment of the gamma-delta and d-q frames. --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 77aa602f..508671c3 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -133,7 +133,7 @@ The following simple procedure can be used without any feedback control: #### Determine precise offset -Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `theta_off` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing the estimated electrical angle $\hat{\theta}_e$ using the voltage in the $\gamma-\delta$ reference frame that is constructed based on estimated rotor states as shown in the figure below. +Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `theta_off` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing the estimated electrical angle $\hat{\theta}_e$ using the voltage in the $\gamma-\delta$ frame that is constructed based on estimated rotor states as shown in the figure below. ```{image} resources/reference-frame.svg :alt: Torque Variation with Rotor Angle @@ -158,7 +158,7 @@ $$ \left. v_{\gamma} \right|_{i=0} = -\omega_e \lambda_{\mathrm{pm}} \sin(\theta_e - \hat{\theta}_e) $$ -The $v_d$ value should be zero if the estimated angle $\hat{\theta}_e$ is accurate and there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$). Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. +If there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$), the $\gamma-\delta$ frame aligns with $d-q$ frame and $v_d = v_{\gamma}$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. 1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `theta_off` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `theta_off` obtained in [Finding the offset](#finding-the-offset). From 02d299bb6f60e07a5c4c674beba44208c10783cc Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:51:23 -0600 Subject: [PATCH 52/57] Fix minor grammatical error in encoder offset section --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 508671c3..afca2820 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -158,7 +158,7 @@ $$ \left. v_{\gamma} \right|_{i=0} = -\omega_e \lambda_{\mathrm{pm}} \sin(\theta_e - \hat{\theta}_e) $$ -If there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$), the $\gamma-\delta$ frame aligns with $d-q$ frame and $v_d = v_{\gamma}$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. +If there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$), the $\gamma-\delta$ frame aligns with $d-q$ frame and $v_d$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. 1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `theta_off` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `theta_off` obtained in [Finding the offset](#finding-the-offset). From ed8f8ad3d921ffb12ad189f058bd37014b37ec97 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:52:06 -0600 Subject: [PATCH 53/57] Fix formatting in encoder offset determination section --- source/getting-started/control-with-amdc/encoder-fb/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index afca2820..bc9e8693 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -158,7 +158,7 @@ $$ \left. v_{\gamma} \right|_{i=0} = -\omega_e \lambda_{\mathrm{pm}} \sin(\theta_e - \hat{\theta}_e) $$ -If there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$), the $\gamma-\delta$ frame aligns with $d-q$ frame and $v_d$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. +If there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$), the $\gamma-\delta$ frame aligns with the $d-q$ frame and $v_d$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. 1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `theta_off` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `theta_off` obtained in [Finding the offset](#finding-the-offset). From 2d0d0b15882686b15527aad77a016f703ed4f972 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Mon, 1 Dec 2025 23:56:21 -0600 Subject: [PATCH 54/57] Revise current command notation in encoder equations Updated notation for current commands in the equation for v_gamma. --- source/getting-started/control-with-amdc/encoder-fb/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index bc9e8693..744b8804 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -152,10 +152,10 @@ $$ v_{\gamma} + j\ v_{\delta} = \vec{V} e^{-j\hat{\theta}_e} $$ -When the current commands are set to $i_d = i_q = 0$, $v_{\gamma}$ can be expressed as follows. +When the current commands are set to $\vec{i} = 0$, $v_{\gamma}$ can be expressed as follows. $$ -\left. v_{\gamma} \right|_{i=0} = -\omega_e \lambda_{\mathrm{pm}} \sin(\theta_e - \hat{\theta}_e) +\left. v_{\gamma} \right|_{\vec{i}=0} = -\omega_e \lambda_{\mathrm{pm}} \sin(\theta_e - \hat{\theta}_e) $$ If there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$), the $\gamma-\delta$ frame aligns with the $d-q$ frame and $v_d$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. From 78f961171e0d7271cb90cea7468993c94e0e969b Mon Sep 17 00:00:00 2001 From: Takahiro Noguchi Date: Tue, 2 Dec 2025 21:06:32 -0600 Subject: [PATCH 55/57] Fix rendering issues for images --- .../control-with-amdc/encoder-fb/index.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 744b8804..30b4b1c6 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -47,7 +47,11 @@ The AMDC provides a convenience function that can be used as an alternate to `en The recommended approach to reading the shaft position from the encoder is illustrated in the figure below: - +```{image} resources/EncoderCodeBlockDiagram.svg +:alt: Encoder Code Block Diagram.svg +:width: 700px +:align: center +``` First, the AMDC [`drv/encoder`](/firmware/arch/drivers/encoder.md) driver module function `encoder_get_position()` is used to obtain the the encoder's count $\theta_{\rm enc}$ since the last z-pulse. @@ -207,8 +211,11 @@ Note that this low pass filter approach will always produce a lagging speed esti To obtain a no-lag estimate of the rotor speed, users may create an observer [[1]](#1), which implements a mechanical model of the rotor as shown below. - - +```{image} resources/ObserverFigure.svg +:alt: Observer Figure +:width: 600px +:align: center +``` The estimate of rotor speed is denoted by $\Omega_\text{sf}$. To implement this observer, the user needs to know the system parameters: - `J`: the inertia of the rotor From a826e60b69ee990b4c8546414991134503c985d1 Mon Sep 17 00:00:00 2001 From: Daehoon Sung Date: Fri, 5 Dec 2025 14:57:31 -0600 Subject: [PATCH 56/57] Clarify angle calculation and encoder offset procedure Updated explanations regarding the calculation of the motor's angle and the determination of the encoder offset. Improved clarity on the relationship between electrical angle and voltage vector. --- .../control-with-amdc/encoder-fb/index.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 30b4b1c6..9766c7bd 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -59,7 +59,7 @@ First, the AMDC [`drv/encoder`](/firmware/arch/drivers/encoder.md) driver module The [`drv/encoder`](/firmware/arch/drivers/encoder.md) driver module also has a function called `encoder_get_steps()` which returns the encoder's count since power-on. One rotation direction increments, the other decrements. This value does not wrap around (it ignores `encoder_set_counts_per_rev()` and the z-pulse). Users are advised to use `encoder_get_position()`, which does wrap around and tracks the z-pulse. ``` -Next, the user should calculate $\theta_{\rm m}$ from $\theta_{\rm enc}$. This is done by 1) removing the offset and 2) converting counts into radians. For the the angles defined as shown in the image above, this is simply calculated as +Next, the user should calculate $\theta_{\rm m}$ from $\theta_{\rm enc}$. This is done by 1) removing the offset and 2) converting counts into radians. For the angles defined as shown in the image above, this is simply calculated as $$ \theta_{\rm m} = \tfrac{2\pi}{\rm COUNTS\_PER\_REV} \left( \theta_{\rm enc} - \theta_{\rm off} \right) @@ -137,32 +137,33 @@ The following simple procedure can be used without any feedback control: #### Determine precise offset -Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `theta_off` while using closed-loop control to rotate the shaft at the highest possible speed. The correct offset is determined by observing the estimated electrical angle $\hat{\theta}_e$ using the voltage in the $\gamma-\delta$ frame that is constructed based on estimated rotor states as shown in the figure below. +Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `theta_off` while using closed-loop control to rotate the shaft at the highest possible speed. ```{image} resources/reference-frame.svg :alt: Torque Variation with Rotor Angle :width: 250px :align: right ``` -The voltage vector in the figure above can be expressed in complex vector form as follows. Note that $\omega_e$ is the electrical angular velocity with units of radians per second. + +The correct offset is determined by observing the estimated electrical angle $\hat{\theta}_\mathrm{e}$ using the voltage in the $\gamma-\delta$ frame that is constructed based on estimated rotor states, as shown in the figure on the right. The voltage vector in the figure can be expressed in complex vector form as follows. Note that ${\theta}_{\mathrm{e}} = p \times {\theta}_{\mathrm{m}}$ is the electrical angle where $p$ is the number of pole-pairs and $\omega_\mathrm{e} = \dot{\theta}_{\mathrm{e}}$ is the electrical angular velocity with units of radians per second. $$ -\vec{V} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \omega_e \lambda_{\mathrm{pm}} e^{j{\theta}_e} +\vec{V} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \omega_\mathrm{e} \lambda_{\mathrm{pm}} e^{j{\theta}_\mathrm{e}} $$ This voltage vector can be converted into the $\gamma-\delta$ reference frame as follows. $$ -v_{\gamma} + j\ v_{\delta} = \vec{V} e^{-j\hat{\theta}_e} +v_{\gamma} + j\ v_{\delta} = \vec{V} e^{-j\hat{\theta}_\mathrm{e}} $$ When the current commands are set to $\vec{i} = 0$, $v_{\gamma}$ can be expressed as follows. $$ -\left. v_{\gamma} \right|_{\vec{i}=0} = -\omega_e \lambda_{\mathrm{pm}} \sin(\theta_e - \hat{\theta}_e) +\left. v_{\gamma} \right|_{\vec{i}=0} = -\omega_\mathrm{e} \lambda_{\mathrm{pm}} \sin(\theta_\mathrm{e} - \hat{\theta}_\mathrm{e}) $$ -If there is no estimation error (i.e., $\theta_e - \hat{\theta}_e = 0$), the $\gamma-\delta$ frame aligns with the $d-q$ frame and $v_d$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_d = 0$. +If there is no estimation error (i.e., $\theta_\mathrm{e} - \hat{\theta}_\mathrm{e} = 0$), the $\gamma-\delta$ frame aligns with the $\mathrm{d}-\mathrm{q}$ and $v_\mathrm{d}$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_\mathrm{d} = 0$. 1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `theta_off` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `theta_off` obtained in [Finding the offset](#finding-the-offset). From f28d03d0749a8b5a3caf12f49cdb0655ad968853 Mon Sep 17 00:00:00 2001 From: Eric Severson Date: Tue, 16 Dec 2025 15:03:45 -0600 Subject: [PATCH 57/57] Revise encoder offset section contents (#146) * Edit encoder offset precise section * Improve typesetting and clarify vd --- .../control-with-amdc/encoder-fb/index.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/source/getting-started/control-with-amdc/encoder-fb/index.md b/source/getting-started/control-with-amdc/encoder-fb/index.md index 9766c7bd..f637f4e7 100644 --- a/source/getting-started/control-with-amdc/encoder-fb/index.md +++ b/source/getting-started/control-with-amdc/encoder-fb/index.md @@ -137,7 +137,7 @@ The following simple procedure can be used without any feedback control: #### Determine precise offset -Friction and cogging torque in the motor decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). The precise offset can be found by fine-tuning the `theta_off` while using closed-loop control to rotate the shaft at the highest possible speed. +Friction and cogging torque in the motor can decrease the accuracy of the estimate in [Finding the offset](#finding-the-offset). A more precise offset can be found by fine-tuning the `theta_off` value while using closed-loop control to rotate the shaft at different speeds and monitoring the observed d-axis voltage. ```{image} resources/reference-frame.svg :alt: Torque Variation with Rotor Angle @@ -145,13 +145,21 @@ Friction and cogging torque in the motor decrease the accuracy of the estimate i :align: right ``` -The correct offset is determined by observing the estimated electrical angle $\hat{\theta}_\mathrm{e}$ using the voltage in the $\gamma-\delta$ frame that is constructed based on estimated rotor states, as shown in the figure on the right. The voltage vector in the figure can be expressed in complex vector form as follows. Note that ${\theta}_{\mathrm{e}} = p \times {\theta}_{\mathrm{m}}$ is the electrical angle where $p$ is the number of pole-pairs and $\omega_\mathrm{e} = \dot{\theta}_{\mathrm{e}}$ is the electrical angular velocity with units of radians per second. +The correct offset is determined by considering how errors in the measured rotor angle impact the current controller's understanding of the $\mathrm{d}-\mathrm{q}$ reference frame. This is depicted in the figure on the right, where: + +- $\hat{\theta}_\mathrm{e}$ is the incorrect eletrical angle (due to error in offset $\theta_\mathrm{off}$) that the controller is using +- the $\gamma$-$\delta$ vectors indicate where the controller mistakenly understands the $\mathrm{d}$-$\mathrm{q}$ frame to be located based on $\hat{\theta}_\mathrm{e}$ +- the $\mathrm{d}$-$\mathrm{q}$ vectors and $\theta_\mathrm{e}$ angle depict the actual $\mathrm{d}$-$\mathrm{q}$ frame of the motor. + +Note that ${\theta}_{\mathrm{e}} = p {\theta}_{\mathrm{m}}$ is the electrical angle where $p$ is the number of pole-pairs of the motor and $\omega_\mathrm{e} = \dot{\theta}_{\mathrm{e}}$ is the electrical angular velocity with units of radians per second. + +The voltage vector of the motor terminals $\vec{V}$ is shown in red and can be expressed in complex vector form as follows: $$ \vec{V} = R \vec{i} + L \frac{d\vec{i}}{dt} + j \omega_\mathrm{e} \lambda_{\mathrm{pm}} e^{j{\theta}_\mathrm{e}} $$ -This voltage vector can be converted into the $\gamma-\delta$ reference frame as follows. +This voltage vector can be converted into the $\gamma$-$\delta$ reference frame as follows. $$ v_{\gamma} + j\ v_{\delta} = \vec{V} e^{-j\hat{\theta}_\mathrm{e}} @@ -163,11 +171,11 @@ $$ \left. v_{\gamma} \right|_{\vec{i}=0} = -\omega_\mathrm{e} \lambda_{\mathrm{pm}} \sin(\theta_\mathrm{e} - \hat{\theta}_\mathrm{e}) $$ -If there is no estimation error (i.e., $\theta_\mathrm{e} - \hat{\theta}_\mathrm{e} = 0$), the $\gamma-\delta$ frame aligns with the $\mathrm{d}-\mathrm{q}$ and $v_\mathrm{d}$ value should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_\mathrm{d} = 0$. +If there is no estimation error (i.e., $\theta_\mathrm{e} - \hat{\theta}_\mathrm{e} = 0$), the $\gamma$-$\delta$ frame aligns with the $\mathrm{d}$-$\mathrm{q}$ and the $v_\mathrm{d}$ value seen by the controller should be zero. Based on this fact, the following procedure describes how to determine the encoder offset by finding the condition where $v_\gamma=v_\mathrm{d} = 0$. 1. Configure the AMDC for closed-loop speed and DQ current control, and configure the operating environment to allow for quick edits to `theta_off` and for measuring the d-axis voltage commanded by the current regulator. Consider [adding a custom command](/getting-started/tutorials/vsi/index.md#command-template-c-code) and using [logging](/getting-started/user-guide/logging/index.md) to accomplish this. 2. Command the motor to rotate at a steady speed under no-load conditions. Use the estimated `theta_off` obtained in [Finding the offset](#finding-the-offset). -3. Sweep `theta_off` over a small range around the initial estimate (e.g., ±5 counts). For each value, monitor the d-axis voltage and find the `theta_off` that makes the d-axis voltage closest to 0 V. Identify this by observing when the sign of the d-axis voltage changes. +3. Sweep `theta_off` over a small range around the initial estimate (e.g., ±5 counts). For each value, monitor the d-axis voltage and find the `theta_off` value that makes the d-axis voltage closest to 0 V. Identify this by observing when the sign of the d-axis voltage changes. 4. Repeat step 3 at multiple rotor speeds. At each speed, record the `theta_off` value that minimizes the d-axis voltage. 5. Take the average of the collected `theta_off` values from step 4 to determine the final encoder offset value. 6. Plot the d-axis voltage with the final offset against the different rotor speeds. The d-axis voltage should be close to zero for all speeds if the offset is tuned correctly.