Code for the motor controller
This is basically what we used in Detroit.
Change-Id: If2820d7ec5fcbc5f33b4082025027a6e969ad0e1
diff --git a/motors/BUILD b/motors/BUILD
index e652069..e75f74b 100644
--- a/motors/BUILD
+++ b/motors/BUILD
@@ -8,7 +8,32 @@
],
deps = [
':util',
+ ':motor',
+ ':motor_controls',
'//motors/core',
+ '//motors/peripheral:can',
+ '//motors/peripheral:adc',
+ '//motors/usb:legacy',
+ ],
+ restricted_to = mcu_cpus,
+)
+
+cc_library(
+ name = 'motor',
+ hdrs = [
+ 'motor.h',
+ ],
+ srcs = [
+ 'motor.cc',
+ ],
+ deps = [
+ ':algorithms',
+ ':util',
+ '//motors/core',
+ '//motors/peripheral:configuration',
+
+ '//motors/peripheral:adc',
+ '//motors/peripheral:can',
'//motors/usb:legacy',
],
restricted_to = mcu_cpus,
@@ -25,5 +50,86 @@
hdrs = [
'util.h',
],
+ deps = [
+ '//motors/core',
+ ],
+ restricted_to = mcu_cpus,
+)
+
+genrule(
+ name = 'doc',
+ srcs = [
+ 'NOTES.md',
+ ],
+ outs = [
+ 'NOTES.html',
+ ],
+ cmd = ' '.join([
+ 'pandoc',
+ '-f', 'markdown_github-hard_line_breaks',
+ '-t', 'html5',
+ '-o', '$@', '$<',
+ ]),
+)
+
+cc_library(
+ name = 'algorithms',
+ hdrs = [
+ 'algorithms.h',
+ ],
+ srcs = [
+ 'algorithms.cc',
+ ],
+ compatible_with = mcu_cpus,
+)
+
+cc_test(
+ name = 'algorithms_test',
+ srcs = [
+ 'algorithms_test.cc',
+ ],
+ deps = [
+ ':algorithms',
+ '//aos/testing:googletest',
+ ],
+)
+
+cc_library(
+ name = 'math',
+ hdrs = [
+ 'math.h',
+ ],
+ srcs = [
+ 'math.cc',
+ ],
+ compatible_with = mcu_cpus,
+)
+
+cc_test(
+ name = 'math_test',
+ srcs = [
+ 'math_test.cc',
+ ],
+ deps = [
+ ':math',
+ '//aos/testing:googletest',
+ '//third_party/googletest:googlemock',
+ ],
+)
+
+cc_library(
+ name = 'motor_controls',
+ hdrs = [
+ 'motor_controls.h',
+ ],
+ srcs = [
+ 'motor_controls.cc',
+ ],
+ deps = [
+ ':math',
+ ':motor',
+ '//motors/peripheral:configuration',
+ '//third_party/eigen',
+ ],
restricted_to = mcu_cpus,
)
diff --git a/motors/NOTES.md b/motors/NOTES.md
new file mode 100644
index 0000000..7185bed
--- /dev/null
+++ b/motors/NOTES.md
@@ -0,0 +1,79 @@
+* Teensy 3.5 has a MK64FX512VMD12 (Freescale Kinetis K64 sub-family)
+ * It's a Cortex-M4F, which means it has an FPU and DSP instructions.
+ * 512 KB of flash
+ * 192 kB SRAM (64 kB SRAM\_L, 128 kB SRAM\_U)
+ * Up to 120 MHz
+* [datasheet](http://cache.freescale.com/files/microcontrollers/doc/data_sheet/K64P144M120SF5.pdf)
+* [reference manual](http://cache.nxp.com/assets/documents/data/en/reference-manuals/K64P144M120SF5RM.pdf)
+* [schematic](https://www.pjrc.com/teensy/schematic.html).
+* [actual docs on the bit banding](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0439b/Behcjiic.html)
+* [ARM Cortex-M Programming Guide to Memory Barrier Instructions](https://static.docs.arm.com/dai0321/a/DAI0321A_programming_guide_memory_barriers_for_m_profile.pdf)
+* [Cortex-M4 instruction timings](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0439b/CHDDIGAC.html)
+* RM08, 5v, 2048 edges per revolution, single ended incremental, with index pulse
+* [motor](https://hobbyking.com/en_us/turnigy-aquastar-t20-3t-730kv-1280kv-water-cooled-brushless-motor.html)
+ * 2 pole pairs per revolution
+
+* Change MOSFET gate resistors to 1 ohm (or something else?)
+* Reshuffle input capacitor connections
+ * Thermals?
+ * Farther apart
+ * Other side?
+* More capacitors on battery power
+ * Like across individual half bridges
+* No paste on wire pads
+
+### Clocking
+* Running the core clock at its maximum of 120 MHz
+* Bus clocks (both of them) are their maximum of 60 MHz
+
+### Timing (what triggers what)
+Coordinating the timing of everything is pretty important. The general idea is
+to keep everything in sync based on the FTM module(s) which drive the motor
+outputs. They trigger the ADCs too.
+
+FTM0 and FTM3 are synced using the global time base.
+
+The timing is divided into "cycles". Each cycle is a fixed length of time.
+The start of the cycle is when the FTM module(s) have a count of 0.
+
+TODO(Brian): Update the rest of this.
+
+Both PDBs are used to trigger ADC samples, in conjunction with DMA to
+automatically setup the correct sequence and record the results. This gives
+tight control over the timing (DMA can take a bit to get there, and interrupts
+would be worse) and doesn't require CPU time besides reading the result at the
+end.
+The PDB is triggered by the initialization external trigger of an FTM.
+From there, DMA triggered by the ADC conversion complete event finishes and
+prepares for the next round.
+PDB triggers of ADCs alternate between channels so DMA can read+update the other
+one in the mean time.
+One DMA channel copies the result out, and then links to another channel which
+reconfigures that ADC conversion setup register to prepare for the next time.
+This doesn't allow skipping ADC conversions, but conversion results can always
+be ignored later.
+This also doesn't allow scheduling ADC conversions at any points besides at
+fixed multiples from thes start of the cycle, but that should be fine.
+The ADC setup should stop converting by the end of the cycle so the new PDB
+trigger will restart everything, and both DMA channels should reset themselves
+after two cycles (so that the code can read ADC results in one area while
+they're being written to the other).
+
+Both PDBs are used to trigger ADC samples, in conjunction with DMA to
+automatically setup the correct sequence and record the results. This gives
+tight control over the timing (DMA can take a bit to get there, and interrupts
+would be worse) and doesn't require CPU time besides reading the result at the
+end.
+The PDB is triggered by the initialization external trigger of an FTM.
+From there, DMA triggered by two unused channels of the FTMs alternates between
+copying results and reconfiguring each of the sets of ADC registers.
+The DMA uses the SMOD functionality to get the corresponding registers of both
+ADCs in one go, which means using a total of 4 DMA channels (1 to configure the
+A registers, 1 to configure the B registers, 1 to copy the A results, and 1 to
+copy the B results).
+
+Currently, ADC sampling all happens at the beginning of a cycle, then the
+computation of new output values happens, then these values need to be
+written to the hardware before the next cycle starts.
+ADC sampling may move to DMA-based at some point to increase flexibility and
+reduce CPU time spent on it.
diff --git a/motors/algorithms.cc b/motors/algorithms.cc
new file mode 100644
index 0000000..1d655ef
--- /dev/null
+++ b/motors/algorithms.cc
@@ -0,0 +1,50 @@
+#include "motors/algorithms.h"
+
+namespace frc971 {
+namespace salsa {
+
+BalancedReadings BalanceReadings(const ReadingsToBalance to_balance) {
+ // TODO(Brian): Get rid of the floating point divides.
+ BalancedReadings result;
+ if (to_balance.weights[0] == 0) {
+ const float average1 = static_cast<float>(to_balance.sums[1]) /
+ static_cast<float>(to_balance.weights[1]);
+ const float average2 = static_cast<float>(to_balance.sums[2]) /
+ static_cast<float>(to_balance.weights[2]);
+ result.readings[0] = -(average1 + average2);
+ result.readings[1] = average1;
+ result.readings[2] = average2;
+ } else if (to_balance.weights[1] == 0) {
+ const float average0 = static_cast<float>(to_balance.sums[0]) /
+ static_cast<float>(to_balance.weights[0]);
+ const float average2 = static_cast<float>(to_balance.sums[2]) /
+ static_cast<float>(to_balance.weights[2]);
+ result.readings[0] = average0;
+ result.readings[1] = -(average0 + average2);
+ result.readings[2] = average2;
+ } else if (to_balance.weights[2] == 0) {
+ const float average0 = static_cast<float>(to_balance.sums[0]) /
+ static_cast<float>(to_balance.weights[0]);
+ const float average1 = static_cast<float>(to_balance.sums[1]) /
+ static_cast<float>(to_balance.weights[1]);
+ result.readings[0] = average0;
+ result.readings[1] = average1;
+ result.readings[2] = -(average0 + average1);
+ } else {
+ const float average0 = static_cast<float>(to_balance.sums[0]) /
+ static_cast<float>(to_balance.weights[0]);
+ const float average1 = static_cast<float>(to_balance.sums[1]) /
+ static_cast<float>(to_balance.weights[1]);
+ const float average2 = static_cast<float>(to_balance.sums[2]) /
+ static_cast<float>(to_balance.weights[2]);
+ const float offset = (average0 + average1 + average2) / -3;
+ result.readings[0] = average0 + offset;
+ result.readings[1] = average1 + offset;
+ result.readings[2] = average2 + offset;
+ }
+
+ return result;
+}
+
+} // namespace salsa
+} // namespace frc971
diff --git a/motors/algorithms.h b/motors/algorithms.h
new file mode 100644
index 0000000..d1fce7a
--- /dev/null
+++ b/motors/algorithms.h
@@ -0,0 +1,32 @@
+#ifndef MOTORS_ALGORITHMS_H_
+#define MOTORS_ALGORITHMS_H_
+
+#include <stdint.h>
+
+namespace frc971 {
+namespace salsa {
+
+struct ReadingsToBalance {
+ // Adds a single reading at index.
+ void Add(int index, int32_t value) {
+ sums[index] += value;
+ ++weights[index];
+ }
+
+ int32_t sums[3];
+ int weights[3];
+};
+
+struct BalancedReadings {
+ float readings[3];
+};
+
+// Returns three readings which add up to 0 and are the same distances apart as
+// the input ones (by weight). The distances between the averages of the inputs
+// and the corresponding outputs will be inversely proportional to the weights.
+BalancedReadings BalanceReadings(ReadingsToBalance to_balance);
+
+} // namespace salsa
+} // namespace frc971
+
+#endif // MOTORS_ALGORITHMS_H_
diff --git a/motors/algorithms_test.cc b/motors/algorithms_test.cc
new file mode 100644
index 0000000..87cb5b5
--- /dev/null
+++ b/motors/algorithms_test.cc
@@ -0,0 +1,100 @@
+#include "motors/algorithms.h"
+
+#include <inttypes.h>
+
+#include "gtest/gtest.h"
+
+namespace frc971 {
+namespace salsa {
+namespace testing {
+
+class BalanceReadingsTest : public ::testing::Test {
+ protected:
+ void CheckReadingsResult(const ReadingsToBalance &to_balance) {
+ ASSERT_GE((to_balance.weights[0] > 0) + (to_balance.weights[1] > 0) +
+ (to_balance.weights[2] > 0),
+ 2)
+ << "Need at least 2 readings";
+ ASSERT_GE(to_balance.weights[0], 0);
+ ASSERT_GE(to_balance.weights[1], 0);
+ ASSERT_GE(to_balance.weights[2], 0);
+
+ const BalancedReadings result = BalanceReadings(to_balance);
+
+ {
+ const auto sum =
+ result.readings[0] + result.readings[1] + result.readings[2];
+ EXPECT_GE(sum, -2);
+ EXPECT_LE(sum, 2);
+ }
+
+ if (to_balance.weights[0] == 0) {
+ const double averages[3] = {
+ 0, static_cast<double>(to_balance.sums[1]) / to_balance.weights[1],
+ static_cast<double>(to_balance.sums[2]) / to_balance.weights[2]};
+ EXPECT_LE(::std::abs((averages[1] - averages[2]) -
+ (result.readings[1] - result.readings[2])),
+ 0.5);
+ } else if (to_balance.weights[1] == 0) {
+ const double averages[3] = {
+ static_cast<double>(to_balance.sums[0]) / to_balance.weights[0], 0,
+ static_cast<double>(to_balance.sums[2]) / to_balance.weights[2]};
+ EXPECT_LE(::std::abs((averages[0] - averages[2]) -
+ (result.readings[0] - result.readings[2])),
+ 0.5);
+ } else if (to_balance.weights[2] == 0) {
+ const double averages[3] = {
+ static_cast<double>(to_balance.sums[0]) / to_balance.weights[0],
+ static_cast<double>(to_balance.sums[1]) / to_balance.weights[1], 0};
+ EXPECT_LE(::std::abs((averages[0] - averages[1]) -
+ (result.readings[0] - result.readings[1])),
+ 0.5);
+ } else {
+ const double averages[3] = {
+ static_cast<double>(to_balance.sums[0]) / to_balance.weights[0],
+ static_cast<double>(to_balance.sums[1]) / to_balance.weights[1],
+ static_cast<double>(to_balance.sums[2]) / to_balance.weights[2]};
+
+ const double middle = (averages[0] + averages[1] + averages[2]) / 3;
+ const double average_distances[3] = {
+ ::std::abs(averages[0] - middle - result.readings[0]),
+ ::std::abs(averages[1] - middle - result.readings[1]),
+ ::std::abs(averages[2] - middle - result.readings[2])};
+ // distances[0]/distances[1] = weights[1]/weights[0]
+ // distances[0]*weights[0]/weights[1] = distances[1]
+ EXPECT_LE(::std::abs(average_distances[0] *
+ static_cast<double>(to_balance.weights[0]) /
+ static_cast<double>(to_balance.weights[1]) -
+ average_distances[1]),
+ 0.01);
+ EXPECT_LE(::std::abs(average_distances[2] *
+ static_cast<double>(to_balance.weights[2]) /
+ static_cast<double>(to_balance.weights[1]) -
+ average_distances[1]),
+ 0.01);
+ EXPECT_LE(::std::abs(average_distances[0] *
+ static_cast<double>(to_balance.weights[0]) /
+ static_cast<double>(to_balance.weights[2]) -
+ average_distances[2]),
+ 0.01);
+ }
+ }
+};
+
+TEST_F(BalanceReadingsTest, Basic) {
+ CheckReadingsResult({{50, 50, 50}, {1, 1, 1}});
+ CheckReadingsResult({{50, 50, 0}, {1, 1, 0}});
+ CheckReadingsResult({{50, 0, 50}, {1, 0, 1}});
+ CheckReadingsResult({{0, 50, 50}, {0, 1, 1}});
+ CheckReadingsResult({{0, 50, 100}, {0, 1, 2}});
+ CheckReadingsResult({{100, 50, 50}, {2, 1, 1}});
+ CheckReadingsResult({{100, 100, 50}, {2, 2, 1}});
+
+ CheckReadingsResult({{150, 50, 50}, {1, 1, 1}});
+ CheckReadingsResult({{150, 50, 50}, {2, 2, 2}});
+ CheckReadingsResult({{3424, 5013, 3424}, {2, 2, 2}});
+}
+
+} // namespace testing
+} // namespace salsa
+} // namespace frc971
diff --git a/motors/core/mk20dx128.c b/motors/core/mk20dx128.c
index f7dbd09..eef0488 100644
--- a/motors/core/mk20dx128.c
+++ b/motors/core/mk20dx128.c
@@ -31,6 +31,7 @@
#include "motors/core/kinetis.h"
#include <errno.h>
+#include <stdio.h>
// Flash Security Setting. On Teensy 3.2, you can lock the MK20 chip to prevent
// anyone from reading your code. You CAN still reprogram your Teensy while
@@ -63,6 +64,8 @@
void __libc_init_array(void);
void fault_isr(void) {
+ FTM0_C0V = FTM0_C1V = FTM0_C2V = FTM0_C3V = FTM0_C4V = FTM0_C5V = 0;
+ printf("fault_isr\n");
while (1) {
// keep polling some communication while in fault
// mode, so we don't completely die.
diff --git a/motors/decode_dump.py b/motors/decode_dump.py
new file mode 100755
index 0000000..1010e88
--- /dev/null
+++ b/motors/decode_dump.py
@@ -0,0 +1,60 @@
+#!/usr/bin/python3
+
+# Pipe the binary data in and give the CSV output filename as an argument.
+
+import struct
+import sys
+import select
+
+DatapointStruct = struct.Struct('<11h')
+DATAPOINTS = 5000
+TOTAL_SIZE = DatapointStruct.size * DATAPOINTS
+
+data = bytes()
+while len(data) < TOTAL_SIZE:
+ read_now = sys.stdin.buffer.read(TOTAL_SIZE - len(data))
+ if not read_now:
+ print('EOF before data finished', file=sys.stderr)
+ sys.exit(1)
+ data += read_now
+print('%s' % len(data))
+
+readable, _, _ = select.select([sys.stdin.buffer], [], [], 1)
+if readable:
+ print('Extra bytes', file=sys.stderr)
+ sys.exit(1)
+
+decoded = []
+for i in range(DATAPOINTS):
+ datapoint = DatapointStruct.unpack_from(data, i * DatapointStruct.size)
+ decoded.append(datapoint)
+
+def current(reading, ref):
+ reading_voltage = reading / 4096 * 3.3 / 1.47 * (0.768 + 1.47)
+ #reading_ref = ref / 4096 * 3.3
+ reading_ref = 2.5
+ reading_ref = 0
+ return (reading_voltage - reading_ref) / 50 / 0.0003
+
+with open(sys.argv[1], 'w') as out:
+ out.write('current0.0,current1.0,current2.0,current0.1,current1.1,current2.1,count\n')
+ #for point in decoded[2000:7200]:
+ for point in decoded:
+ out.write(','.join(str(d) for d in (
+ current(point[0], point[6]),
+ current(point[1], point[6]),
+ current(point[2], point[6]),
+ #current(point[3], point[6]),
+ #current(point[4], point[6]),
+ #current(point[5], point[6]),
+ point[3] / 100.0,
+ point[4] / 100.0,
+ point[5] / 100.0,
+ point[6] / 100.0,
+ point[7] / 100.0,
+ point[8] / 100.0,
+ point[9] / 100.0,
+ point[10] / 100.0,
+ )) + '\n')
+
+print('all done')
diff --git a/motors/math.cc b/motors/math.cc
new file mode 100644
index 0000000..291ca43
--- /dev/null
+++ b/motors/math.cc
@@ -0,0 +1,37 @@
+#include "motors/math.h"
+
+#include <math.h>
+
+namespace frc971 {
+namespace salsa {
+namespace math_internal {
+
+float sin_int_table[SinCosTableSize()];
+float cos_int_table[SinCosTableSize()];
+float sin_float_table[SinCosTableSize() + 1];
+float cos_float_table[SinCosTableSize() + 1];
+
+} // namespace math_internal
+
+using math_internal::SinCosTableSize;
+
+__attribute__((cold)) void MathInit() {
+ for (uint32_t i = 0; i < SinCosTableSize(); ++i) {
+ const double int_theta =
+ ((static_cast<double>(i) + 0.5) / SinCosTableSize()) * 2.0 * M_PI;
+ math_internal::sin_int_table[i] = sin(int_theta);
+ math_internal::cos_int_table[i] = cos(int_theta);
+ }
+ for (uint32_t i = 0; i < SinCosTableSize() + 1; ++i) {
+ const double float_theta =
+ (static_cast<int32_t>(i) -
+ static_cast<int32_t>(SinCosTableSize() / 2)) *
+ static_cast<double>(math_internal::FloatMaxMagnitude()) /
+ static_cast<double>(SinCosTableSize() / 2);
+ math_internal::sin_float_table[i] = sin(float_theta);
+ math_internal::cos_float_table[i] = cos(float_theta);
+ }
+}
+
+} // namespace salsa
+} // namespace frc971
diff --git a/motors/math.h b/motors/math.h
new file mode 100644
index 0000000..851a90a
--- /dev/null
+++ b/motors/math.h
@@ -0,0 +1,131 @@
+#ifndef MOTORS_MATH_H_
+#define MOTORS_MATH_H_
+
+#include <limits.h>
+
+#include <complex>
+#include <ratio>
+
+// This file has some specialized math functions useful for implementing our
+// controls in a minimal number of cycles.
+
+namespace frc971 {
+namespace salsa {
+
+inline constexpr unsigned int Log2RoundUp(unsigned int x) {
+ return (x < 2) ? x : (1 + Log2RoundUp(x / 2));
+}
+
+template <typename T>
+inline constexpr const T &ConstexprMax(const T &a, const T &b) {
+ return (a < b) ? b : a;
+}
+
+namespace math_internal {
+
+constexpr uint32_t SinCosTableSize() { return 1024; }
+
+constexpr float FloatMaxMagnitude() { return 1.0f; }
+
+constexpr bool IsPowerOf2(uint32_t value) {
+ return value == (1u << (Log2RoundUp(value) - 1));
+}
+
+static_assert(IsPowerOf2(SinCosTableSize()), "Tables need to be a power of 2");
+
+extern float sin_int_table[SinCosTableSize()];
+extern float cos_int_table[SinCosTableSize()];
+extern float sin_float_table[SinCosTableSize() + 1];
+extern float cos_float_table[SinCosTableSize() + 1];
+
+template <class Rotation>
+float FastTableLookupInt(uint32_t theta, const float *table) {
+ static_assert(IsPowerOf2(Rotation::den),
+ "Denominator needs to be a power of 2");
+
+ // Don't need to worry about the sizes of intermediates given this constraint.
+ static_assert(
+ ConstexprMax<uint32_t>(Rotation::den, SinCosTableSize()) * Rotation::num <
+ UINT32_MAX,
+ "Numerator and denominator are too big");
+
+ // Rounding/truncating here isn't supported.
+ static_assert(Rotation::den <= SinCosTableSize(),
+ "Tables need to be bigger");
+
+ // Don't feel like thinking through the consequences of this not being true.
+ static_assert(Rotation::num > 0 && Rotation::den > 0,
+ "Need a positive ratio");
+
+ constexpr uint32_t kDenominatorRatio = SinCosTableSize() / Rotation::den;
+
+ // These should always be true given the other constraints.
+ static_assert(kDenominatorRatio * Rotation::den == SinCosTableSize(),
+ "Math is broken");
+ static_assert(IsPowerOf2(kDenominatorRatio), "Math is broken");
+
+ return table[(theta * kDenominatorRatio * Rotation::num +
+ kDenominatorRatio * Rotation::num / 2) %
+ SinCosTableSize()];
+}
+
+inline float FastTableLookupFloat(float theta, const float *table) {
+ const int index = (SinCosTableSize() / 2) +
+ static_cast<int32_t>(theta * ((SinCosTableSize() / 2) /
+ FloatMaxMagnitude()));
+ return table[index];
+}
+
+} // namespace math_internal
+
+// All theta arguments to the float-based functions must be in [-0.2, 0.2].
+
+inline float FastSinFloat(float theta) {
+ return math_internal::FastTableLookupFloat(theta,
+ math_internal::sin_float_table);
+}
+
+inline float FastCosFloat(float theta) {
+ return math_internal::FastTableLookupFloat(theta,
+ math_internal::cos_float_table);
+}
+
+inline ::std::complex<float> ImaginaryExpFloat(float theta) {
+ return ::std::complex<float>(FastCosFloat(theta), FastSinFloat(theta));
+}
+
+// The integer-based sin/cos functions all have a Rotation template argument,
+// which should be a ::std::ratio. The real argument to the trigonometric
+// function is this ratio multiplied by the theta argument.
+//
+// Specifically, they return the function evaluated at
+// (Rotation * (theta + 0.5)).
+//
+// All theta arguments must be in [0, Rotation::den).
+//
+// All denominators must be powers of 2.
+
+template<class Rotation>
+float FastSinInt(uint32_t theta) {
+ return math_internal::FastTableLookupInt<Rotation>(
+ theta, math_internal::sin_int_table);
+}
+
+template<class Rotation>
+float FastCosInt(uint32_t theta) {
+ return math_internal::FastTableLookupInt<Rotation>(
+ theta, math_internal::cos_int_table);
+}
+
+template<class Rotation>
+::std::complex<float> ImaginaryExpInt(uint32_t theta) {
+ return ::std::complex<float>(FastCosInt<Rotation>(theta),
+ FastSinInt<Rotation>(theta));
+}
+
+void MathInit();
+
+} // namespace salsa
+} // namespace frc971
+
+#endif // MOTORS_MATH_H_
diff --git a/motors/math_test.cc b/motors/math_test.cc
new file mode 100644
index 0000000..672ee69
--- /dev/null
+++ b/motors/math_test.cc
@@ -0,0 +1,78 @@
+#include "motors/math.h"
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+
+namespace frc971 {
+namespace salsa {
+namespace testing {
+
+class SinCosIntTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ MathInit();
+ }
+
+ template <class Rotation>
+ void CheckSinCos() {
+ static constexpr float kTolerance = 0.004;
+ for (uint32_t theta = 0; theta < Rotation::den; ++theta) {
+ EXPECT_THAT(
+ FastSinInt<Rotation>(theta),
+ ::testing::FloatNear(sin(ThetaToFloat<Rotation>(theta)), kTolerance));
+ EXPECT_THAT(
+ FastCosInt<Rotation>(theta),
+ ::testing::FloatNear(cos(ThetaToFloat<Rotation>(theta)), kTolerance));
+ }
+ }
+
+ private:
+ template <class Rotation>
+ double ThetaToFloat(uint32_t theta) {
+ const double rotation_double =
+ static_cast<double>(Rotation::num) / static_cast<double>(Rotation::den);
+ return (static_cast<double>(theta) + 0.5) * rotation_double * 2.0 * M_PI;
+ }
+};
+
+TEST_F(SinCosIntTest, Numerator1) {
+ CheckSinCos<::std::ratio<1, 2>>();
+ CheckSinCos<::std::ratio<1, 4>>();
+ CheckSinCos<::std::ratio<1, 8>>();
+ CheckSinCos<::std::ratio<1, 128>>();
+ CheckSinCos<::std::ratio<1, 1024>>();
+}
+
+TEST_F(SinCosIntTest, Numerator5) {
+ CheckSinCos<::std::ratio<5, 8>>();
+ CheckSinCos<::std::ratio<5, 128>>();
+ CheckSinCos<::std::ratio<5, 1024>>();
+}
+
+class SinCosFloatTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ MathInit();
+ }
+
+ void CheckSinCos(float theta) {
+ ASSERT_GE(theta, -0.2f);
+ ASSERT_LE(theta, 0.2f);
+
+ static constexpr float kTolerance = 0.002;
+ EXPECT_THAT(FastSinFloat(theta),
+ ::testing::FloatNear(sin(theta), kTolerance));
+ EXPECT_THAT(FastCosFloat(theta),
+ ::testing::FloatNear(cos(theta), kTolerance));
+ }
+};
+
+TEST_F(SinCosFloatTest, Endpoints) {
+ CheckSinCos(0);
+ CheckSinCos(-0.2);
+ CheckSinCos(0.2);
+}
+
+} // namespace testing
+} // namespace salsa
+} // namespace frc971
diff --git a/motors/medium_salsa.cc b/motors/medium_salsa.cc
index 1fbe9a6..144fc1e 100644
--- a/motors/medium_salsa.cc
+++ b/motors/medium_salsa.cc
@@ -2,15 +2,35 @@
#include <stdio.h>
+#include <atomic>
+
#include "motors/core/time.h"
+#include "motors/motor.h"
+#include "motors/motor_controls.h"
+#include "motors/peripheral/adc.h"
+#include "motors/peripheral/can.h"
#include "motors/usb/usb_serial.h"
#include "motors/util.h"
namespace frc971 {
namespace salsa {
+namespace {
+
+::std::atomic<Motor *> global_motor{nullptr};
extern "C" {
+
void *__stack_chk_guard = (void *)0x67111971;
+void __stack_chk_fail(void) {
+ while (true) {
+ GPIOC_PSOR = (1 << 5);
+ printf("Stack corruption detected\n");
+ delay(1000);
+ GPIOC_PCOR = (1 << 5);
+ delay(1000);
+ }
+}
+
extern void usb_init();
int _write(int file, char *ptr, int len) {
(void)file;
@@ -24,7 +44,12 @@
extern uint32_t __heap_start__[];
extern uint32_t __stack_end__[];
+void ftm0_isr(void) {
+ global_motor.load(::std::memory_order_relaxed)->HandleInterrupt();
+}
+
} // extern "C"
+} // namespace
extern "C" int main(void) {
// for background about this startup delay, please see these conversations
@@ -39,38 +64,63 @@
// relative to each other, which means centralizing them here makes it a lot
// more manageable.
NVIC_SET_SANE_PRIORITY(IRQ_USBOTG, 0x7);
+ NVIC_SET_SANE_PRIORITY(IRQ_FTM0, 0x3);
// Set the LED's pin to output mode.
GPIO_BITBAND(GPIOC_PDDR, 5) = 1;
PORTC_PCR5 = PORT_PCR_DSE | PORT_PCR_MUX(1);
+ GPIO_BITBAND(GPIOA_PDDR, 15) = 1;
+ PORTA_PCR15 = PORT_PCR_DSE | PORT_PCR_MUX(1);
+
+ DMA_CR = DMA_CR_EMLM;
usb_serial_init();
usb_descriptor_set_product_id(0x0490);
usb_init();
+ AdcInit();
+ MathInit();
+ delay(1000);
+ can_init();
+
+ MotorControlsImplementation controls;
+
+ delay(1000);
+ Motor motor(FTM0, FTM1, &controls);
+ motor.Init();
+ global_motor.store(&motor, ::std::memory_order_relaxed);
+ // Output triggers to things like the PDBs on initialization.
+ FTM0_EXTTRIG = FTM_EXTTRIG_INITTRIGEN;
+ // Don't let any memory accesses sneak past here, because we actually
+ // need everything to be starting up.
+ __asm__("" :: : "memory");
// Give everything a chance to get going.
delay(100);
+#if 0
printf("Ram start: %p\n", __bss_ram_start__);
printf("Heap start: %p\n", __heap_start__);
printf("Heap end: %p\n", __brkval);
printf("Stack start: %p\n", __stack_end__);
+#endif
- GPIOC_PSOR = 1 << 5;
+ printf("Going silent to zero motors...\n");
+ // Give the print a chance to make it out.
+ delay(1000);
+ motor.Zero();
+
+ printf("Zeroed motor!\n");
+ // Give stuff a chance to recover from interrupts-disabled.
+ delay(100);
+ motor.Start();
+
+ GPIOA_PCOR = 1 << 15;
+
+ // TODO(Brian): Use SLEEPONEXIT to reduce interrupt latency?
while (true) {}
return 0;
}
-void __stack_chk_fail(void) {
- while (true) {
- GPIOC_PSOR = (1 << 5);
- printf("Stack corruption detected\n");
- delay(1000);
- GPIOC_PCOR = (1 << 5);
- delay(1000);
- }
-}
-
} // namespace salsa
} // namespace frc971
diff --git a/motors/motor.cc b/motors/motor.cc
new file mode 100644
index 0000000..06f9f08
--- /dev/null
+++ b/motors/motor.cc
@@ -0,0 +1,556 @@
+#include "motors/motor.h"
+
+#include <limits.h>
+
+#include <array>
+
+#include "motors/peripheral/configuration.h"
+
+#include <stdio.h>
+#include <inttypes.h>
+#include "motors/core/time.h"
+#include "motors/usb/usb_serial.h"
+#include "motors/peripheral/can.h"
+
+namespace frc971 {
+namespace salsa {
+namespace {
+
+constexpr int kDeadtimeCompensation = 9;
+
+uint32_t CalculateOnTime(uint32_t width, bool flip) {
+ if (width > 0) {
+ width += kDeadtimeCompensation;
+ if (flip) {
+ width += 1;
+ }
+ }
+ return 1500 - width / 2;
+}
+
+uint32_t CalculateOffTime(uint32_t width, bool flip) {
+ if (width > 0) {
+ width += kDeadtimeCompensation;
+ if (!flip) {
+ width += 1;
+ }
+ }
+ return 1500 + width / 2;
+}
+
+} // namespace
+
+Motor::Motor(BigFTM *pwm_ftm, LittleFTM *encoder_ftm, MotorControls *controls)
+ : pwm_ftm_(pwm_ftm), encoder_ftm_(encoder_ftm), controls_(controls) {}
+
+static_assert((BUS_CLOCK_FREQUENCY % SWITCHING_FREQUENCY) == 0,
+ "Switching frequency needs to divide the bus clock frequency");
+
+static_assert(BUS_CLOCK_FREQUENCY / SWITCHING_FREQUENCY < UINT16_MAX,
+ "Need to prescale");
+
+void Motor::Init() {
+ PORTE_PCR24 = PORT_PCR_MUX(1);
+ PORTB_PCR0 = PORT_PCR_MUX(6);
+ PORTB_PCR1 = PORT_PCR_MUX(6);
+
+ PORTC_PCR1 = PORT_PCR_MUX(4);
+ PORTC_PCR2 = PORT_PCR_MUX(4);
+ PORTC_PCR3 = PORT_PCR_MUX(4);
+ PORTC_PCR4 = PORT_PCR_MUX(4);
+ PORTD_PCR4 = PORT_PCR_MUX(4);
+ PORTD_PCR5 = PORT_PCR_MUX(4);
+
+ // PWMSYNC doesn't matter because we set SYNCMODE down below.
+ pwm_ftm_->MODE = FTM_MODE_WPDIS;
+ pwm_ftm_->MODE = FTM_MODE_WPDIS | FTM_MODE_FTMEN;
+ encoder_ftm_->MODE = FTM_MODE_WPDIS;
+ encoder_ftm_->MODE = FTM_MODE_WPDIS | FTM_MODE_FTMEN;
+
+ pwm_ftm_->SC = FTM_SC_CLKS(0) /* Disable counting for now */;
+ encoder_ftm_->SC =
+ FTM_SC_CLKS(1) /* Use the system clock (not sure it matters) */ |
+ FTM_SC_PS(0) /* Don't prescale the clock (not sure it matters) */;
+
+ pwm_ftm_->CNTIN = encoder_ftm_->CNTIN = 0;
+ pwm_ftm_->CNT = encoder_ftm_->CNT = 0;
+
+ pwm_ftm_->MOD = (BUS_CLOCK_FREQUENCY / SWITCHING_FREQUENCY) - 1;
+ encoder_ftm_->MOD = controls_->counts_per_revolution() - 1;
+
+ // Put them all into combine active-high mode, and all the low ones staying on
+ // all the time by default.
+ // TODO: update comment
+ pwm_ftm_->C0SC = FTM_CSC_ELSA;
+ pwm_ftm_->C0V = 0;
+ pwm_ftm_->C1SC = FTM_CSC_ELSA;
+ pwm_ftm_->C1V = 0;
+ pwm_ftm_->C2SC = FTM_CSC_ELSA;
+ pwm_ftm_->C2V = 0;
+ pwm_ftm_->C3SC = FTM_CSC_ELSA;
+ pwm_ftm_->C3V = 0;
+ pwm_ftm_->C4SC = FTM_CSC_ELSA;
+ pwm_ftm_->C4V = 0;
+ pwm_ftm_->C5SC = FTM_CSC_ELSA;
+ pwm_ftm_->C5V = 0;
+
+ // I think you have to set this to something other than 0 for the quadrature
+ // encoder mode to actually work? This is "input capture on rising edge only",
+ // which should be fine.
+ encoder_ftm_->C0SC = FTM_CSC_ELSA;
+ encoder_ftm_->C1SC = FTM_CSC_ELSA;
+
+ // Initialize all the channels to 0.
+ // TODO(Brian): Is this really what we want?
+ pwm_ftm_->OUTINIT = 0;
+
+ pwm_ftm_->COMBINE = FTM_COMBINE_SYNCEN3 /* Synchronize updates usefully */ |
+ FTM_COMBINE_DTEN3 /* Enable deadtime */ |
+ FTM_COMBINE_COMP3 /* Make them complementary */ |
+ FTM_COMBINE_COMBINE3 /* Combine the channels */ |
+ FTM_COMBINE_SYNCEN2 /* Synchronize updates usefully */ |
+ FTM_COMBINE_DTEN2 /* Enable deadtime */ |
+ FTM_COMBINE_COMP2 /* Make them complementary */ |
+ FTM_COMBINE_COMBINE2 /* Combine the channels */ |
+ FTM_COMBINE_SYNCEN1 /* Synchronize updates usefully */ |
+ FTM_COMBINE_DTEN1 /* Enable deadtime */ |
+ FTM_COMBINE_COMP1 /* Make them complementary */ |
+ FTM_COMBINE_COMBINE1 /* Combine the channels */ |
+ FTM_COMBINE_SYNCEN0 /* Synchronize updates usefully */ |
+ FTM_COMBINE_DTEN0 /* Enable deadtime */ |
+ FTM_COMBINE_COMP0 /* Make them complementary */ |
+ FTM_COMBINE_COMBINE0 /* Combine the channels */;
+
+ // Set a deadtime of 500ns.
+ // TODO: update comment
+ pwm_ftm_->DEADTIME =
+ FTM_DEADTIME_DTPS(0) /* Prescaler of 1 */ | FTM_DEADTIME_DTVAL(9);
+
+ // All of the channels are active high.
+ pwm_ftm_->POL = 0;
+
+ encoder_ftm_->FILTER = FTM_FILTER_CH0FVAL(0) /* No filter */ |
+ FTM_FILTER_CH1FVAL(0) /* No filter */;
+
+ // Could set PHAFLTREN and PHBFLTREN here to enable the filters.
+ encoder_ftm_->QDCTRL = FTM_QDCTRL_QUADEN;
+
+ pwm_ftm_->SYNCONF =
+ FTM_SYNCONF_HWWRBUF /* Hardware trigger flushes switching points */ |
+ FTM_SYNCONF_SWWRBUF /* Software trigger flushes switching points */ |
+ FTM_SYNCONF_SWRSTCNT /* Software trigger resets the count */ |
+ FTM_SYNCONF_SYNCMODE /* Use the new synchronization mode */;
+ encoder_ftm_->SYNCONF =
+ FTM_SYNCONF_SWWRBUF /* Software trigger flushes MOD */ |
+ FTM_SYNCONF_SWRSTCNT /* Software trigger resets the count */ |
+ FTM_SYNCONF_SYNCMODE /* Use the new synchronization mode */;
+
+ // Don't want any intermediate loading points.
+ pwm_ftm_->PWMLOAD = 0;
+
+ // This has to happen after messing with SYNCONF, and should happen after
+ // messing with various other things so the values can get flushed out of the
+ // buffers.
+ pwm_ftm_->SYNC =
+ FTM_SYNC_SWSYNC /* Flush everything out right now */ |
+ FTM_SYNC_CNTMAX /* Load new values at the end of the cycle */;
+ encoder_ftm_->SYNC = FTM_SYNC_SWSYNC /* Flush everything out right now */;
+
+ // Wait for the software synchronization to finish.
+ while (pwm_ftm_->SYNC & FTM_SYNC_SWSYNC) {
+ }
+ while (encoder_ftm_->SYNC & FTM_SYNC_SWSYNC) {
+ }
+}
+
+void Motor::Zero() {
+#if 0
+ while (true) {
+ if (GPIO_BITBAND(GPIOE_PDIR, 24)) {
+ encoder_ftm_->CNT = 0;
+ break;
+ }
+ }
+#else
+ uint32_t scratch;
+ __disable_irq();
+ // Stuff all of this in an inline assembly statement so we can make sure the
+ // compiler doesn't decide sticking constant loads etc in the middle of
+ // the loop is a good idea, because that increases the latency of recognizing
+ // the index pulse edge which makes velocity affect the zeroing accuracy.
+ __asm__ __volatile__(
+ // A label to restart the loop.
+ "0:\n"
+ // Load the current PDIR value for the pin we care about.
+ "ldr %[scratch], [%[pdir_word]]\n"
+ // Terminate the loop if it's non-0.
+ "cbnz %[scratch], 1f\n"
+ // Go back around again.
+ "b 0b\n"
+ // A label to finish the loop.
+ "1:\n"
+ // Reset the count once we're down here. It doesn't actually matter what
+ // value we store because writing anything resets it to CNTIN (ie 0).
+ "str %[scratch], [%[cnt]]\n"
+ : [scratch] "=&l"(scratch)
+ : [pdir_word] "l"(&GPIO_BITBAND(GPIOE_PDIR, 24)),
+ [cnt] "l"(&encoder_ftm_->CNT));
+ __enable_irq();
+#endif
+}
+
+void Motor::Start() {
+ pwm_ftm_->SC = FTM_SC_TOIE /* Interrupt on overflow */ |
+ FTM_SC_CLKS(1) /* Use the system clock */ |
+ FTM_SC_PS(0) /* Don't prescale the clock */;
+ pwm_ftm_->MODE &= ~FTM_MODE_WPDIS;
+ encoder_ftm_->MODE &= ~FTM_MODE_WPDIS;
+
+ NVIC_ENABLE_IRQ(IRQ_FTM0);
+}
+
+void Motor::HandleInterrupt() {
+ const uint32_t starting_sc = pwm_ftm_->SC;
+ pwm_ftm_->SC = starting_sc & ~FTM_SC_TOF;
+
+ const uint32_t start_count = pwm_ftm_->CNT;
+ __asm__("":::"memory");
+ const MediumAdcReadings adc_readings = AdcReadMedium();
+
+ // Turn the light on if we're starting too late (this generally means a
+ // previous iteration took too long).
+ if (start_count > 100) {
+ GPIOC_PSOR = 1 << 5;
+ }
+
+ ReadingsToBalance to_balance{{0, 0, 0}, {0, 0, 0}};
+ {
+ for (int reading = 0; reading < 2; ++reading) {
+ for (int phase = 0; phase < 3; ++phase) {
+ to_balance.Add(phase, adc_readings.motor_currents[phase][reading]);
+ }
+ }
+ }
+ const BalancedReadings balanced = BalanceReadings(to_balance);
+
+ static float current_command = 0;
+ static uint32_t last_command_receive_time = 0;
+ {
+ unsigned char command_data[8];
+ int command_length;
+ can_receive_command(command_data, &command_length);
+ if (command_length == 4) {
+ last_command_receive_time = micros();
+ uint32_t result = command_data[0] << 24 | command_data[1] << 16 |
+ command_data[2] << 8 | command_data[3];
+ current_command = static_cast<float>(result) / 1000.0f;
+ }
+ }
+ if (!time_after(time_add(last_command_receive_time, 100000), micros())) {
+ current_command = 0;
+ }
+
+ static bool high_gear = false;
+ if (controls_->estimated_velocity() < -2015) {
+ high_gear = true;
+ }
+ if (current_command < 1) {
+ high_gear = false;
+ }
+ float current_now = current_command;
+ if (!high_gear) {
+ current_now = current_now * -120.0f / 120.0f;
+ } else {
+ current_now = current_now * 115.0f / 120.0f;
+ }
+
+#if 0
+ static int status_send_counter = 0;
+ if (++status_send_counter == 1000) {
+ // can_send(uint32_t can_id, const unsigned char *data, unsigned int length)
+ unsigned char send_data[8];
+ can_send(9 << 8, send_data, 0);
+ status_send_counter = 0;
+ printf("sent\n");
+ }
+#endif
+
+#define DO_CONTROLS 1
+#if DO_CONTROLS
+ static constexpr int kEncoderOffset = 810;
+ const uint32_t adjusted_count = (encoder_ftm_->CNT + kEncoderOffset) % 1024;
+ const ::std::array<uint32_t, 3> switching_points =
+ controls_->DoIteration(balanced.readings, adjusted_count, current_now);
+ constexpr uint32_t kMax = 2945;
+ static bool done = false;
+ bool done_now = false;
+ if (switching_points[0] > kMax || switching_points[1] > kMax ||
+ switching_points[2] > kMax) {
+ done_now = true;
+ }
+#define USE_ABSOLUTE_CUTOFF 1
+#if USE_ABSOLUTE_CUTOFF
+ static unsigned int current_done_count = 0;
+ bool current_done = false;
+ for (int phase = 0; phase < 3; ++phase) {
+ const float scaled_reading =
+ MotorControls::scale_current_reading(balanced.readings[0]);
+ static constexpr float kMaxBalancedCurrent = 190.0f;
+ if (scaled_reading > kMaxBalancedCurrent ||
+ scaled_reading < -kMaxBalancedCurrent) {
+ current_done = true;
+ }
+ }
+ if (current_done) {
+ if (current_done_count > 5) {
+ done_now = true;
+ }
+ ++current_done_count;
+ } else {
+ current_done_count = 0;
+ }
+#endif
+ if (done_now && !done) {
+ printf("done now\n");
+ printf("switching_points %" PRIu32 " %" PRIu32 " %" PRIu32 "\n",
+ switching_points[0], switching_points[1], switching_points[2]);
+ printf("raw %" PRIu16 " %" PRIu16 " %" PRIu16 " %" PRIu16 " %" PRIu16
+ " %" PRIu16 "\n",
+ adc_readings.motor_currents[0][0], adc_readings.motor_currents[0][1],
+ adc_readings.motor_currents[1][0], adc_readings.motor_currents[1][1],
+ adc_readings.motor_currents[2][0],
+ adc_readings.motor_currents[2][1]);
+ printf("balanced %" PRIu16 " %" PRIu16 " %" PRIu16 "\n",
+ static_cast<uint16_t>(balanced.readings[0]),
+ static_cast<uint16_t>(balanced.readings[1]),
+ static_cast<uint16_t>(balanced.readings[2]));
+ done = true;
+ }
+ if (!done) {
+ pwm_ftm_->C0V = CalculateOnTime(switching_points[0], flip_time_offset_);
+ pwm_ftm_->C1V = CalculateOffTime(switching_points[0], flip_time_offset_);
+ pwm_ftm_->C2V = CalculateOnTime(switching_points[1], flip_time_offset_);
+ pwm_ftm_->C3V = CalculateOffTime(switching_points[1], flip_time_offset_);
+ pwm_ftm_->C4V = CalculateOnTime(switching_points[2], flip_time_offset_);
+ pwm_ftm_->C5V = CalculateOffTime(switching_points[2], flip_time_offset_);
+ flip_time_offset_ = !flip_time_offset_;
+ } else {
+ pwm_ftm_->C0V = 0;
+ pwm_ftm_->C1V = 0;
+ pwm_ftm_->C2V = 0;
+ pwm_ftm_->C3V = 0;
+ pwm_ftm_->C4V = 0;
+ pwm_ftm_->C5V = 0;
+ }
+#define DO_FIXED_PULSE 0
+#elif DO_FIXED_PULSE
+ // An on-width of 60 with 30V in means about 50A through the motor and about
+ // 30W total power dumped by the motor.
+#if 0
+ static int i = 0;
+ ++i;
+ if (i == 2) {
+ pwm_ftm_->C3V = 111;
+ i = 0;
+ } else {
+ pwm_ftm_->C3V = 0;
+ }
+#endif
+ pwm_ftm_->C0V = 0;
+ pwm_ftm_->C1V = 0;
+ pwm_ftm_->C2V = 0;
+ //pwm_ftm_->C3V = 100;
+ pwm_ftm_->C4V = 0;
+ pwm_ftm_->C5V = 0;
+#endif
+
+#define PRINT_READINGS 0
+#if PRINT_READINGS
+ static int i = 0;
+ if (i == 100) {
+ i = 0;
+ printf("i=%" PRIu16 "\n", adc_readings.input_voltage);
+ } else if (i == 20) {
+ printf("0=%" PRIu16 " r=%" PRIu16 "\n",
+ adc_readings.motor_currents[0][0], adc_readings.motor_current_ref);
+ } else if (i == 40) {
+ printf("1=%" PRIu16 " r=%" PRIu16 "\n",
+ adc_readings.motor_currents[1][0], adc_readings.motor_current_ref);
+ } else if (i == 60) {
+ printf("2=%" PRIu16 " r=%" PRIu16 "\n",
+ adc_readings.motor_currents[2][0], adc_readings.motor_current_ref);
+ } else {
+ //printf("%" PRIu32 " to %" PRIu32 "\n", start_count, end_count);
+ }
+ ++i;
+#define PRINT_ALL_READINGS 0
+#elif PRINT_ALL_READINGS
+ printf("ref=%" PRIu16 " 0.0=%" PRIu16 " 1.0=%" PRIu16 " 2.0=%" PRIu16
+ " in=%" PRIu16 " 0.1=%" PRIu16 " 1.1=%" PRIu16 " 2.1=%" PRIu16 "\n",
+ adc_readings.motor_current_ref, adc_readings.motor_currents[0][0],
+ adc_readings.motor_currents[1][0], adc_readings.motor_currents[2][0],
+ adc_readings.input_voltage, adc_readings.motor_currents[0][1],
+ adc_readings.motor_currents[1][1], adc_readings.motor_currents[2][1]);
+#define TAKE_SAMPLE 1
+#elif TAKE_SAMPLE
+#if 0
+ constexpr int kStartupWait = 50000;
+#elif 0
+ constexpr int kStartupWait = 0;
+#elif 0
+ constexpr int kStartupWait = 30000;
+#elif 1
+ constexpr int kStartupWait = 2 * 20000;
+#endif
+ constexpr int kSubsampling = 1;
+ constexpr int kPoints = 5000;
+ constexpr int kSamplingEnd = kStartupWait + kPoints * kSubsampling;
+ (void)kSamplingEnd;
+ static int j = 0;
+ static int16_t data[kPoints][11];
+ static int written = 0;
+ static_assert((kStartupWait % kSubsampling) == 0, "foo");
+ static_assert((kPoints % kSubsampling) == 0, "foo");
+ if (j < kStartupWait) {
+ // Wait to be started up.
+ ++j;
+#define SAMPLE_UNTIL_DONE 0
+#if !SAMPLE_UNTIL_DONE
+ } else if (j < kSamplingEnd && (j % kSubsampling) == 0) {
+#else
+ } else if (!done) {
+#endif
+ {
+ const int index = ((j - kStartupWait) / kSubsampling) % kPoints;
+ auto point = data[index];
+#if 0
+ point[0] = adc_readings.motor_currents[0][0];
+ point[1] = adc_readings.motor_currents[1][0];
+ point[2] = adc_readings.motor_currents[2][0];
+ point[3] = adc_readings.motor_currents[0][1];
+ point[4] = adc_readings.motor_currents[1][1];
+ point[5] = adc_readings.motor_currents[2][1];
+#else
+ point[0] = balanced.readings[0] / 2;
+ point[1] = balanced.readings[1] / 2;
+ point[2] = balanced.readings[2] / 2;
+#if 1
+ point[3] = controls_->Debug(0);
+ point[4] = controls_->Debug(1);
+ point[5] = controls_->Debug(2);
+ point[6] = controls_->Debug(3);
+ point[7] = controls_->Debug(4);
+ point[8] = controls_->Debug(5);
+ point[9] = controls_->Debug(6);
+ point[10] = controls_->Debug(7);
+#else
+ point[3] = adc_readings.motor_currents[0][0];
+ point[4] = adc_readings.motor_currents[1][0];
+ point[5] = adc_readings.motor_currents[2][0];
+ point[6] = adc_readings.motor_currents[0][1];
+ point[7] = adc_readings.motor_currents[1][1];
+ point[8] = adc_readings.motor_currents[2][1];
+ point[9] = temp1;
+ point[10] = temp2;
+#endif
+ (void)temp1;
+ (void)temp2;
+#if 0
+ point[3] = pwm_ftm_->C1V - pwm_ftm_->C0V;
+ point[4] = pwm_ftm_->C3V - pwm_ftm_->C2V;
+ point[5] = pwm_ftm_->C5V - pwm_ftm_->C4V;
+#endif
+#endif
+#if 0
+ point[6] = adc_readings.motor_current_ref;
+ point[7] = adc_readings.input_voltage;
+#else
+#endif
+ }
+
+#define DO_STEP_RESPONSE 0
+#if DO_STEP_RESPONSE
+ // Step response
+ if (j > 25000) {
+ pwm_ftm_->C1V = 20;
+ }
+#define DO_PULSE_SWEEP 0
+#elif DO_PULSE_SWEEP
+ // Sweep the pulse through the ADC sampling points.
+ static constexpr int kMax = 2500;
+ static constexpr int kExtraWait = 1500;
+ if (j > kStartupWait && j < kStartupWait + kExtraWait) {
+ pwm_ftm_->C4V = 0;
+ pwm_ftm_->C5V = 60;
+ } else if (j < kStartupWait + kMax + kExtraWait) {
+ uint32_t start = j - kStartupWait - kExtraWait;
+ pwm_ftm_->C4V = start;
+ pwm_ftm_->C5V = start + 60;
+ } else {
+ pwm_ftm_->C4V = 0;
+ pwm_ftm_->C5V = 0;
+ }
+#endif
+
+ ++j;
+#if !SAMPLE_UNTIL_DONE
+ } else if (j < kSamplingEnd) {
+ ++j;
+ } else if (j == kSamplingEnd) {
+#else
+ } else if (false) {
+#endif
+ printf("finished\n");
+ ++j;
+#if !SAMPLE_UNTIL_DONE
+ } else {
+#else
+ } else if (done) {
+#endif
+ // Time to write the data out.
+ if (written < (int)sizeof(data)) {
+ int to_write = sizeof(data) - written;
+#if 1
+ if (to_write > 20) {
+ to_write = 20;
+ }
+ // TODO(Brian): Fix usb_serial_write so we can write more than 1 byte at a
+ // time.
+ if (to_write > 1) {
+ to_write = 1;
+ }
+ int result =
+ usb_serial_write(1, ((const char *)data) + written, to_write);
+#else
+ if (to_write > 8) {
+ to_write = 8;
+ }
+ int result =
+ can_send(97, ((const unsigned char *)data) + written, to_write);
+#endif
+ if (result >= 0) {
+ written += to_write;
+ } else {
+ //printf("error\n");
+ }
+ if (written == (int)sizeof(data)) {
+ printf("done writing %d\n", written);
+ }
+ }
+ }
+#endif
+ (void)adc_readings;
+ (void)start_count;
+ (void)balanced;
+
+ // Tell the hardware to use the new switching points.
+ pwm_ftm_->PWMLOAD = FTM_PWMLOAD_LDOK;
+
+ // If another cycle has already started, turn the light on right now.
+ if (pwm_ftm_->SC & FTM_SC_TOF) {
+ GPIOC_PSOR = 1 << 5;
+ }
+}
+
+} // namespace salsa
+} // namespace frc971
diff --git a/motors/motor.h b/motors/motor.h
new file mode 100644
index 0000000..f5b731f
--- /dev/null
+++ b/motors/motor.h
@@ -0,0 +1,111 @@
+#ifndef MOTORS_MOTOR_H_
+#define MOTORS_MOTOR_H_
+
+#include <limits.h>
+
+#include <array>
+
+#include "motors/algorithms.h"
+#include "motors/core/kinetis.h"
+#include "motors/peripheral/adc.h"
+#include "motors/util.h"
+
+namespace frc971 {
+namespace salsa {
+
+class MotorControls {
+ public:
+ MotorControls() = default;
+ virtual ~MotorControls() = default;
+
+ MotorControls(const MotorControls &) = delete;
+ void operator=(const MotorControls &) = delete;
+
+ static constexpr float scale_current_reading(float reading) {
+ return reading *
+ static_cast<float>(1.0 / 4096.0 /* Full-scale ADC reading */ *
+ 3.3 /* ADC reference voltage */ /
+ (1.47 / (0.768 + 1.47)) /* 5V -> 3.3V divider */ /
+ 50.0 /* Current sense amplification */ /
+ 0.0003 /* Sense resistor */);
+ }
+
+ static constexpr int counts_per_revolution() { return 2048 / 2; }
+
+ // raw_currents are in amps for each phase.
+ // theta is in electrical counts, which will be less than
+ // counts_per_revolution().
+ virtual ::std::array<uint32_t, 3> DoIteration(
+ const float raw_currents[3], uint32_t theta,
+ const float command_current) = 0;
+
+ virtual int16_t Debug(uint32_t theta) = 0;
+
+ virtual float estimated_velocity() const = 0;
+};
+
+// Controls a single motor.
+class Motor {
+ public:
+ // pwm_ftm is used to drive the PWM outputs.
+ // encoder_ftm is used for reading the encoder.
+ Motor(BigFTM *pwm_ftm, LittleFTM *encoder_ftm, MotorControls *controls);
+
+ Motor(const Motor &) = delete;
+ void operator=(const Motor &) = delete;
+
+ // Sets up everything but doesn't actually start the timers.
+ //
+ // This assumes the global time base configuration happens outside so the
+ // timers for both motors (if applicable) are synced up.
+ // TODO(Brian): "40.4.28.1 Enabling the global time base (GTB)" says something
+ // about needing to do stuff in a specific order, so this API might need
+ // revising.
+ void Init();
+
+ // Zeros the encoder. This involves blocking for an arbitrary length of time
+ // with interrupts disabled.
+ void Zero();
+
+ // Starts the timers.
+ void Start();
+
+ void HandleInterrupt();
+
+ private:
+ // Represents the ADC reading which is closest to an edge.
+ struct CloseAdcReading {
+ // Adds a new reading to the readings to balance or pushes the previous
+ // closest one there and saves off this one.
+ //
+ // Returns true if it saves off the new reading.
+ bool MaybeUse(int new_distance, const MediumAdcReadings &adc_readings,
+ int phase, int sample, ReadingsToBalance *to_balance) {
+ if (new_distance < distance) {
+ if (distance != INT_MAX) {
+ to_balance->Add(index, value);
+ }
+ distance = new_distance;
+ value = adc_readings.motor_currents[phase][sample];
+ index = phase;
+ return true;
+ }
+ return false;
+ }
+
+ int distance = INT_MAX;
+ int32_t value = 0;
+ int index = 0;
+ };
+
+ bool flip_time_offset_ = false;
+
+ BigFTM *const pwm_ftm_;
+ LittleFTM *const encoder_ftm_;
+ MotorControls *const controls_;
+};
+
+} // namespace salsa
+} // namespace frc971
+
+#endif // MOTORS_MOTOR_H_
diff --git a/motors/motor_controls.cc b/motors/motor_controls.cc
new file mode 100644
index 0000000..187a781
--- /dev/null
+++ b/motors/motor_controls.cc
@@ -0,0 +1,240 @@
+#include "motors/motor_controls.h"
+
+#include "motors/peripheral/configuration.h"
+
+namespace frc971 {
+namespace salsa {
+namespace {
+
+template <int kRows, int kCols>
+using ComplexMatrix = MotorControlsImplementation::ComplexMatrix<kRows, kCols>;
+
+using Complex = ::std::complex<float>;
+
+constexpr int kCountsPerRevolution = MotorControls::counts_per_revolution();
+
+#if 1
+constexpr double kMaxDutyCycle = 0.98;
+#elif 1
+constexpr double kMaxDutyCycle = 0.6;
+#elif 0
+constexpr double kMaxDutyCycle = 0.2;
+#endif
+
+constexpr int kPhaseBOffset = kCountsPerRevolution / 3;
+constexpr int kPhaseCOffset = 2 * kCountsPerRevolution / 3;
+
+constexpr double K1_unscaled = 1.81e04;
+constexpr double K2_unscaled = -2.65e03;
+
+// Make the amplitude of the fundamental 1 for ease of math.
+constexpr double K2 = K2_unscaled / K1_unscaled;
+constexpr double K1 = 1;
+
+// volts
+constexpr double vcc = 30.0;
+
+constexpr double Kv = 22000.0 * 2.0 * M_PI / 60.0 / 30.0 * 2.0;
+
+// Henries
+constexpr double L = 1e-05;
+
+constexpr double M = 1e-06;
+
+// ohms for system
+constexpr double R = 0.008;
+
+::Eigen::Matrix<float, 3, 3> A_discrete() {
+ ::Eigen::Matrix<float, 3, 3> r;
+ static constexpr float kDiagonal = 0.960091f;
+ static constexpr float kOffDiagonal = 0.00356245f;
+ r << kDiagonal, kOffDiagonal, kOffDiagonal, kOffDiagonal, kDiagonal,
+ kOffDiagonal, kOffDiagonal, kOffDiagonal, kDiagonal;
+ return r;
+}
+
+::Eigen::Matrix<float, 3, 3> B_discrete_inverse() {
+ return ::Eigen::Matrix<float, 1, 3>::Constant(0.18403f).asDiagonal();
+}
+
+// The number to divide the product of the unit BEMF and the per phase current
+// by to get motor current.
+constexpr double kOneAmpScalar = 1.46785;
+
+constexpr double kMaxOneAmpDrivingVoltage = 0.0361525;
+
+// Use FluxLinkageTable() to access this with a const so you don't accidentally
+// modify it.
+float flux_linkage_table[kCountsPerRevolution];
+
+void MakeFluxLinkageTable() {
+ for (int i = 0; i < kCountsPerRevolution; ++i) {
+ const double theta = static_cast<double>(i) /
+ static_cast<double>(kCountsPerRevolution) * 2.0 * M_PI;
+ flux_linkage_table[i] = K1 * sin(theta) - K2 * sin(theta * 5);
+ }
+}
+
+// theta doesn't have to be less than kCountsPerRevolution.
+::Eigen::Matrix<float, 3, 1> FluxLinkageAt(uint32_t theta) {
+ ::Eigen::Matrix<float, 3, 1> r;
+ r(0) = flux_linkage_table[theta % kCountsPerRevolution];
+ r(1) = flux_linkage_table[(theta + kPhaseBOffset) % kCountsPerRevolution];
+ r(2) = flux_linkage_table[(theta + kPhaseCOffset) % kCountsPerRevolution];
+ return r;
+}
+
+::Eigen::Matrix<float, 3, 3> MakeK() {
+ ::Eigen::Matrix<float, 3, 3> Vconv;
+ Vconv << 2.0f, -1.0f, -1.0f, -1.0f, 2.0f, -1.0f, -1.0f, -1.0f, 2.0f;
+ static constexpr float kControllerGain = 0.07f;
+ return kControllerGain * (Vconv / 3.0f);
+}
+
+ComplexMatrix<3, 1> MakeE1Unrotated() {
+ ComplexMatrix<3, 1> rotation;
+ rotation << Complex(0, -1), Complex(::std::sqrt(3) / 2, 0.5),
+ Complex(-::std::sqrt(3) / 2, 0.5);
+ return K1 * rotation;
+}
+
+ComplexMatrix<3, 1> MakeE2Unrotated() {
+ ComplexMatrix<3, 1> rotation;
+ rotation << Complex(0, -1), Complex(-::std::sqrt(3) / 2, 0.5),
+ Complex(::std::sqrt(3) / 2, 0.5);
+ return K2 * rotation;
+}
+
+ComplexMatrix<3, 3> Hn(float omega, int scalar) {
+ const Complex a(static_cast<float>(R),
+ omega * static_cast<float>(scalar * L));
+ const Complex b(0, omega * static_cast<float>(scalar * M));
+ const Complex temp1 = a + b;
+ const Complex temp2 = -b;
+ ComplexMatrix<3, 3> matrix;
+ matrix << temp1, temp2, temp2, temp2, temp1, temp2, temp2, temp2, temp1;
+ return matrix *
+ -(omega / static_cast<float>(Kv) / (a * a + a * b - 2.0f * b * b));
+}
+
+} // namespace
+
+MotorControlsImplementation::MotorControlsImplementation()
+ : E1Unrotated_(MakeE1Unrotated()), E2Unrotated_(MakeE2Unrotated()) {
+ MakeFluxLinkageTable();
+}
+
+::std::array<uint32_t, 3> MotorControlsImplementation::DoIteration(
+ const float raw_currents[3], const uint32_t theta_in,
+ const float command_current) {
+ static constexpr float kCurrentSlewRate = 0.05f;
+ if (command_current > filtered_current_ + kCurrentSlewRate) {
+ filtered_current_ += kCurrentSlewRate;
+ } else if (command_current < filtered_current_ - kCurrentSlewRate) {
+ filtered_current_ -= kCurrentSlewRate;
+ } else {
+ filtered_current_ = command_current;
+ }
+ const float goal_current_in = filtered_current_;
+ const float max_current =
+ (static_cast<float>(vcc * kMaxDutyCycle) -
+ estimated_velocity_ / static_cast<float>(Kv / 2.0)) /
+ static_cast<float>(kMaxOneAmpDrivingVoltage);
+ const float min_current =
+ (-static_cast<float>(vcc * kMaxDutyCycle) -
+ estimated_velocity_ / static_cast<float>(Kv / 2.0)) /
+ static_cast<float>(kMaxOneAmpDrivingVoltage);
+ const float goal_current =
+ ::std::max(min_current, ::std::min(max_current, goal_current_in));
+
+#if 0
+ const uint32_t theta =
+ (theta_in + static_cast<uint32_t>(estimated_velocity_ * 1.0f)) % 1024;
+#elif 0
+ const uint32_t theta =
+ (theta_in + kCountsPerRevolution - 160u) % kCountsPerRevolution;
+#elif 1
+ const uint32_t theta =
+ (theta_in + kCountsPerRevolution +
+ ((estimated_velocity_ > 0) ? (kCountsPerRevolution - 10u) : 60u)) %
+ kCountsPerRevolution;
+#elif 0
+ const uint32_t theta = theta_in;
+#endif
+
+ const ::Eigen::Matrix<float, 3, 1> measured_current =
+ (::Eigen::Matrix<float, 3, 1>() << scale_current_reading(raw_currents[0]),
+ scale_current_reading(raw_currents[1]),
+ scale_current_reading(raw_currents[2])).finished();
+
+ const ComplexMatrix<3, 1> E1 =
+ E1Unrotated_ *
+ ImaginaryExpInt<::std::ratio<1, counts_per_revolution()>>(theta);
+ const ComplexMatrix<3, 1> E2 =
+ E2Unrotated_ *
+ ImaginaryExpInt<::std::ratio<5, counts_per_revolution()>>(theta);
+
+ const float overall_measured_current =
+ ((E1 + E2).real().transpose() * measured_current /
+ static_cast<float>(kOneAmpScalar))(0);
+ const float current_error = goal_current - overall_measured_current;
+ estimated_velocity_ += current_error * 0.1f;
+ debug_[3] = theta;
+ const float omega = estimated_velocity_;
+
+ debug_[4] = max_current * 10;
+
+ const ::Eigen::Matrix<float, 3, 1> I_now = I_last_;
+ const ::Eigen::Matrix<float, 3, 1> I_next =
+ FluxLinkageAt(theta) * goal_current;
+
+ const ComplexMatrix<3, 3> H1 = Hn(omega, 1);
+ const ComplexMatrix<3, 3> H2 = Hn(omega, 5);
+
+ const ComplexMatrix<3, 1> p_imaginary = H1 * E1 + H2 * E2;
+ const ComplexMatrix<3, 1> p_next_imaginary =
+ ImaginaryExpFloat(omega / SWITCHING_FREQUENCY) * H1 * E1 +
+ ImaginaryExpFloat(omega * 5 / SWITCHING_FREQUENCY) * H2 * E2;
+ const ::Eigen::Matrix<float, 3, 1> p = p_imaginary.real();
+ const ::Eigen::Matrix<float, 3, 1> p_next = p_next_imaginary.real();
+
+ const ::Eigen::Matrix<float, 3, 1> Vn_ff =
+ B_discrete_inverse() * (I_next - A_discrete() * (I_now - p) - p_next);
+ const ::Eigen::Matrix<float, 3, 1> Vn =
+ Vn_ff + MakeK() * (I_now - measured_current);
+
+ debug_[0] = (I_next)(0) * 100;
+ debug_[1] = (I_next)(1) * 100;
+ debug_[2] = (I_next)(2) * 100;
+
+ debug_[5] = Vn(0) * 100;
+ debug_[6] = Vn(1) * 100;
+ debug_[7] = Vn(2) * 100;
+
+ ::Eigen::Matrix<float, 3, 1> times = Vn / vcc;
+ {
+ const float min_time = times.minCoeff();
+ times -= ::Eigen::Matrix<float, 3, 1>::Constant(min_time);
+ }
+ {
+ const float max_time = times.maxCoeff();
+ const float scalar =
+ static_cast<float>(kMaxDutyCycle) /
+ ::std::max(static_cast<float>(kMaxDutyCycle), max_time);
+ times *= scalar;
+ }
+
+ I_last_ = I_next;
+
+ // TODO(Austin): Figure out why we need the min here.
+ return {static_cast<uint32_t>(::std::max(0.0f, times(0)) * 3000.0f),
+ static_cast<uint32_t>(::std::max(0.0f, times(1)) * 3000.0f),
+ static_cast<uint32_t>(::std::max(0.0f, times(2)) * 3000.0f)};
+}
+
+int16_t MotorControlsImplementation::Debug(uint32_t theta) {
+ return debug_[theta];
+}
+
+} // namespace salsa
+} // namespace frc971
diff --git a/motors/motor_controls.h b/motors/motor_controls.h
new file mode 100644
index 0000000..e0fa28c
--- /dev/null
+++ b/motors/motor_controls.h
@@ -0,0 +1,45 @@
+#ifndef MOTORS_MOTOR_CONTROLS_H_
+#define MOTORS_MOTOR_CONTROLS_H_
+
+#include <array>
+#include <complex>
+
+#include "motors/math.h"
+#include "motors/motor.h"
+
+#include "Eigen/Dense"
+
+namespace frc971 {
+namespace salsa {
+
+class MotorControlsImplementation : public MotorControls {
+ public:
+ template <int kRows, int kCols>
+ using ComplexMatrix = ::Eigen::Matrix<::std::complex<float>, kRows, kCols>;
+
+ MotorControlsImplementation();
+ ~MotorControlsImplementation() override = default;
+
+ ::std::array<uint32_t, 3> DoIteration(const float raw_currents[3],
+ uint32_t theta,
+ const float command_current) override;
+
+ int16_t Debug(uint32_t theta) override;
+
+ float estimated_velocity() const override { return estimated_velocity_; }
+
+ private:
+ const ComplexMatrix<3, 1> E1Unrotated_, E2Unrotated_;
+
+ float estimated_velocity_ = 0;
+ float filtered_current_ = 0;
+
+ ::Eigen::Matrix<float, 3, 1> I_last_ = ::Eigen::Matrix<float, 3, 1>::Zero();
+
+ int16_t debug_[9];
+};
+
+} // namespace salsa
+} // namespace frc971
+
+#endif // MOTORS_MOTOR_CONTROLS_H_
diff --git a/motors/peripheral/BUILD b/motors/peripheral/BUILD
new file mode 100644
index 0000000..10fef28
--- /dev/null
+++ b/motors/peripheral/BUILD
@@ -0,0 +1,43 @@
+load("//tools:environments.bzl", "mcu_cpus")
+
+cc_library(
+ name = 'adc',
+ visibility = ['//visibility:public'],
+ hdrs = [
+ 'adc.h',
+ ],
+ srcs = [
+ 'adc.cc',
+ ],
+ deps = [
+ ':configuration',
+ '//motors:util',
+ '//motors/core',
+ ],
+ restricted_to = mcu_cpus,
+)
+
+cc_library(
+ name = 'configuration',
+ visibility = ['//visibility:public'],
+ hdrs = [
+ 'configuration.h',
+ ],
+ restricted_to = mcu_cpus,
+)
+
+cc_library(
+ name = 'can',
+ visibility = ['//visibility:public'],
+ hdrs = [
+ 'can.h',
+ ],
+ srcs = [
+ 'can.c',
+ ],
+ deps = [
+ '//motors/core',
+ '//motors:util',
+ ],
+ restricted_to = mcu_cpus,
+)
diff --git a/motors/peripheral/adc.cc b/motors/peripheral/adc.cc
new file mode 100644
index 0000000..ba2b247
--- /dev/null
+++ b/motors/peripheral/adc.cc
@@ -0,0 +1,118 @@
+#include "motors/peripheral/adc.h"
+
+#include "motors/core/kinetis.h"
+
+namespace frc971 {
+namespace salsa {
+
+#define ADC_SC2_BASE (ADC_SC2_REFSEL(0) /* Use the external reference pins. */)
+
+#define ADC_FINISH_CALIBRATION(n, PM) \
+ do { \
+ uint16_t variable = 0; \
+ variable += ADC##n##_CL##PM##0; \
+ variable += ADC##n##_CL##PM##1; \
+ variable += ADC##n##_CL##PM##2; \
+ variable += ADC##n##_CL##PM##3; \
+ variable += ADC##n##_CL##PM##4; \
+ variable += ADC##n##_CL##PM##S; \
+ variable /= 2; \
+ variable |= 0x8000; \
+ ADC##n##_##PM##G = variable; \
+ } while (0);
+
+#define ADC_INIT_SINGLE(n) \
+ do { \
+ ADC##n##_CFG1 = ADC_CFG1_ADIV(2) /* Divide clock by 4 to get 15MHz. */ | \
+ ADC_CFG1_MODE(1) /* 12 bit mode. */ | \
+ ADC_CFG1_ADICLK(0) /* Use the bus clock (60MHz). */; \
+ ADC##n##_CFG2 = ADC_CFG2_MUXSEL /* Use the b channels. */ | \
+ ADC_CFG2_ADHSC /* Support higher ADC clock speeds. */; \
+ ADC##n##_SC1A = 0; /* Clear SC1A's COCO flag. */ \
+ ADC##n##_SC2 = ADC_SC2_BASE; \
+ do { \
+ ADC##n##_SC3 = ADC_SC3_CAL | ADC_SC3_AVGE | \
+ ADC_SC3_AVGS(3) /* Average 32 samples (max). */; \
+ /* Wait for calibration to complete. */ \
+ while (!(ADC##n##_SC1A & ADC_SC1_COCO)) { \
+ } \
+ } while (ADC##n##_SC3 & ADC_SC3_CALF); \
+ ADC_FINISH_CALIBRATION(n, P); \
+ ADC_FINISH_CALIBRATION(n, M); \
+ \
+ ADC##n##_SC3 = 0 /* Disable hardware averaging. */; \
+ } while (0)
+
+void AdcInit() {
+ SIM_SCGC3 |= SIM_SCGC3_ADC1;
+ SIM_SCGC6 |= SIM_SCGC6_ADC0;
+ // TODO(Brian): Mess with SIM_SOPT7 to reconfigure ADC trigger input source?
+ ADC_INIT_SINGLE(0);
+ ADC_INIT_SINGLE(1);
+
+ // M_CH2V/M1_CH2F ADC0_SE14
+ PORTC_PCR0 = PORT_PCR_MUX(0);
+
+ // M_CH0V/M1_CH0F ADC0_SE13
+ PORTB_PCR3 = PORT_PCR_MUX(0);
+
+ // M_CH1V/M1_CH1F ADC0_SE12
+ PORTB_PCR2 = PORT_PCR_MUX(0);
+
+ // M0_CH0F/M_CH0F ADC1_SE14
+ PORTB_PCR10 = PORT_PCR_MUX(0);
+
+ // M0_CH1F/M_CH1F ADC1_SE15
+ PORTB_PCR11 = PORT_PCR_MUX(0);
+
+ // WHEEL_ABS/M_VREF ADC0_SE18
+ PORTE_PCR25 = PORT_PCR_MUX(0);
+
+ // VIN/VIN ADC1_SE5B
+ PORTC_PCR9 = PORT_PCR_MUX(0);
+
+ // M0_CH2F/M_CH2F ADC1_SE17
+ PORTA_PCR17 = PORT_PCR_MUX(0);
+}
+
+MediumAdcReadings AdcReadMedium() {
+ MediumAdcReadings r;
+
+ ADC1_SC1A = 14;
+ while (!(ADC1_SC1A & ADC_SC1_COCO)) {
+ }
+ ADC1_SC1A = 15;
+ r.motor_currents[0][0] = ADC1_RA;
+ while (!(ADC1_SC1A & ADC_SC1_COCO)) {
+ }
+ ADC1_SC1A = 17;
+ ADC0_SC1A = 18;
+ r.motor_currents[1][0] = ADC1_RA;
+ while (!(ADC1_SC1A & ADC_SC1_COCO)) {
+ }
+ ADC1_SC1A = 5;
+ r.motor_currents[2][0] = ADC1_RA;
+ while (!(ADC0_SC1A & ADC_SC1_COCO)) {
+ }
+ r.motor_current_ref = ADC0_RA;
+ while (!(ADC1_SC1A & ADC_SC1_COCO)) {
+ }
+ ADC1_SC1A = 14;
+ r.input_voltage = ADC1_RA;
+ while (!(ADC1_SC1A & ADC_SC1_COCO)) {
+ }
+ ADC1_SC1A = 15;
+ r.motor_currents[0][1] = ADC1_RA;
+ while (!(ADC1_SC1A & ADC_SC1_COCO)) {
+ }
+ ADC1_SC1A = 17;
+ r.motor_currents[1][1] = ADC1_RA;
+ while (!(ADC1_SC1A & ADC_SC1_COCO)) {
+ }
+ r.motor_currents[2][1] = ADC1_RA;
+
+ return r;
+}
+
+} // namespace salsa
+} // namespace frc971
diff --git a/motors/peripheral/adc.h b/motors/peripheral/adc.h
new file mode 100644
index 0000000..4aee769
--- /dev/null
+++ b/motors/peripheral/adc.h
@@ -0,0 +1,22 @@
+#ifndef MOTORS_PERIPHERAL_ADC_H_
+#define MOTORS_PERIPHERAL_ADC_H_
+
+#include <stdint.h>
+
+namespace frc971 {
+namespace salsa {
+
+struct MediumAdcReadings {
+ uint16_t motor_currents[3][2];
+ uint16_t motor_current_ref;
+ uint16_t input_voltage;
+};
+
+void AdcInit();
+
+MediumAdcReadings AdcReadMedium();
+
+} // namespace salsa
+} // namespace frc971
+
+#endif // MOTORS_PERIPHERAL_ADC_H_
diff --git a/motors/peripheral/can.c b/motors/peripheral/can.c
new file mode 100644
index 0000000..7187ac9
--- /dev/null
+++ b/motors/peripheral/can.c
@@ -0,0 +1,189 @@
+#include "motors/peripheral/can.h"
+
+#include <stddef.h>
+#include <string.h>
+
+#include "motors/core/kinetis.h"
+#include "motors/util.h"
+
+#include <stdio.h>
+#include <inttypes.h>
+
+// General note: this peripheral is really weird about accessing its memory. It
+// goes much farther than normal memory-mapped device semantics. In particular,
+// it "locks" various regions of memory under complicated conditions. Because of
+// this, all the code in here touching the device memory is fairly paranoid
+// about how it does that.
+
+// The number of message buffers we're actually going to use. The chip only has
+// 16. Using fewer means less for the CAN module (and CPU) to go through looking
+// for actual data.
+// 0 is for sending and 1 is for receiving commands.
+#define NUMBER_MESSAGE_BUFFERS 2
+
+#if NUMBER_MESSAGE_BUFFERS > 16
+#error Only have 16 message buffers on this part.
+#endif
+
+// TODO(Brian): Do something about CAN errors and warnings (enable interrupts?).
+
+// Flags for the interrupt to process which don't actually come from the
+// hardware. Currently, only used for tx buffers.
+static volatile uint32_t can_manual_flags = 0;
+
+void can_init(void) {
+ printf("can_init\n");
+ PORTB_PCR18 = PORT_PCR_DSE | PORT_PCR_MUX(2);
+ PORTB_PCR19 = PORT_PCR_DSE | PORT_PCR_MUX(2);
+
+ SIM_SCGC6 |= SIM_SCGC6_FLEXCAN0;
+
+ // Put it into freeze mode and wait for it to actually do that.
+ // Don't OR these bits in because it starts in module-disable mode, which
+ // isn't what we want. It will ignore the attempt to change some of the bits
+ // because it's not in freeze mode, but whatever.
+ CAN0_MCR = CAN_MCR_FRZ | CAN_MCR_HALT;
+ while (!(CAN0_MCR & CAN_MCR_FRZACK)) {}
+
+ // Initializing this before touching the mailboxes because the reference
+ // manual slightly implies you have to, and the registers and RAM on this
+ // thing are weird (get locked sometimes) so it actually might matter.
+ CAN0_MCR =
+ CAN_MCR_FRZ | CAN_MCR_HALT /* Stay in freeze mode. */ |
+ CAN_MCR_SRXDIS /* Don't want to see our own frames at all. */ |
+ CAN_MCR_IRMQ /* Use individual masks for each filter. */ |
+ CAN_MCR_LPRIOEN /* Let us prioritize TX mailboxes. */ |
+ (0 << 8) /* No need to pack IDs tightly, so it's easier not to. */ |
+ (NUMBER_MESSAGE_BUFFERS - 1);
+
+ // Initialize all the buffers and RX filters we're enabling.
+
+ // Just in case this does anything...
+ CAN0_RXIMRS[0] = 0;
+ CAN0_MESSAGES[0].prio_id = 0;
+ CAN0_MESSAGES[0].control_timestamp =
+ CAN_MB_CONTROL_INSERT_CODE(CAN_MB_CODE_TX_INACTIVE) | CAN_MB_CONTROL_IDE;
+
+ CAN0_RXIMRS[1] = (1 << 31) /* Want to filter out RTRs. */ |
+ (1 << 30) /* Want to only get extended frames. */ |
+ 0xFF /* Filter on the 1-byte VESC id. */;
+ CAN0_MESSAGES[1].prio_id = 0;
+ CAN0_MESSAGES[1].control_timestamp =
+ CAN_MB_CONTROL_INSERT_CODE(CAN_MB_CODE_RX_EMPTY) | CAN_MB_CONTROL_IDE;
+
+ // Using the oscillator clock directly because it's a reasonable frequency and
+ // more stable than the PLL-based peripheral clock, which matters.
+ // We're going with a sample point fraction of 0.875 because that's what
+ // SocketCAN defaults to.
+ CAN0_CTRL1 = CAN_CTRL1_PRESDIV(
+ 1) /* Divide the crystal frequency by 2 to get 8 MHz. */ |
+ CAN_CTRL1_RJW(0) /* RJW/SJW of 1, which is most common. */ |
+ CAN_CTRL1_PSEG1(7) /* 8 time quanta before sampling. */ |
+ CAN_CTRL1_PSEG2(1) /* 2 time quanta after sampling. */ |
+ CAN_CTRL1_SMP /* Use triple sampling. */ |
+ CAN_CTRL1_PROPSEG(4) /* 5 time quanta before sampling. */;
+ CAN0_CTRL2 = CAN_CTRL2_TASD(25) /* We have so few mailboxes and */
+ /* such a fast peripheral clock, this has lots of margin. */ |
+ CAN_CTRL2_EACEN /* Match on IDE and RTR. */;
+
+ // Enable interrupts for the RX mailbox.
+ CAN0_IMASK1 = 1 << 1;
+
+ // Now take it out of freeze mode.
+ CAN0_MCR &= ~CAN_MCR_HALT;
+
+ //NVIC_ENABLE_IRQ(IRQ_CAN_MESSAGE);
+}
+
+static void can_vesc_process_rx(volatile CanMessageBuffer *buffer,
+ unsigned char *data_out, int *length_out) {
+ // Wait until the buffer is marked as not being busy. The reference manual
+ // says to do this, although it's unclear how we could get an interrupt
+ // asserted while it's still busy. Maybe if the interrupt was slow and now
+ // it's being overwritten?
+ uint32_t control_timestamp;
+ do {
+ control_timestamp = buffer->control_timestamp;
+ } while (control_timestamp & CAN_MB_CONTROL_CODE_BUSY_MASK);
+ // The message buffer is now locked, so it won't be modified by the hardware.
+
+ const uint32_t prio_id = buffer->prio_id;
+ // Making sure to access the data 32 bits at a time, copy it out. It's
+ // ambiguous whether you're allowed to access the individual bytes, and this
+ // memory is weird enough to not make sense risking it. Also, it's only 2
+ // cycles, which is pretty hard to beat by doing anything with the length...
+ // Also, surprise!: the hardware stores the data big-endian.
+ uint32_t data[2];
+ data[0] = __builtin_bswap32(buffer->data[0]);
+ data[1] = __builtin_bswap32(buffer->data[1]);
+
+ // Yes, it might actually matter that we clear the interrupt flag before
+ // unlocking it...
+ CAN0_IFLAG1 = 1 << (buffer - CAN0_MESSAGES);
+
+ // Now read the timer to unlock the message buffer. Want to do this ASAP
+ // rather than waiting until we get to processing the next buffer, plus we
+ // might want to write to the next one, which results in weird, bad things.
+ {
+ uint16_t dummy = CAN0_TIMER;
+ (void)dummy;
+ }
+
+ // The message buffer is now unlocked and "serviced", but its control word
+ // code is still CAN_MB_CODE_RX_FULL. However, said code will stay
+ // CAN_MB_CODE_RX_FULL the next time a message is received into it (the code
+ // won't change to CAN_MB_CODE_RX_OVERRUN because it has been "serviced").
+ // Yes, really...
+
+ memcpy(data_out, data, 8);
+ *length_out = CAN_MB_CONTROL_EXTRACT_DLC(control_timestamp);
+ (void)prio_id;
+}
+
+int can_send(uint32_t can_id, const unsigned char *data, unsigned int length) {
+ volatile CanMessageBuffer *const message_buffer = &CAN0_MESSAGES[0];
+
+ if (CAN_MB_CONTROL_EXTRACT_CODE(message_buffer->control_timestamp) ==
+ CAN_MB_CODE_TX_DATA) {
+ return -1;
+ }
+
+ // Yes, it might actually matter that we clear the interrupt flag before
+ // doing stuff...
+ CAN0_IFLAG1 = 1 << (message_buffer - CAN0_MESSAGES);
+ message_buffer->prio_id = can_id;
+ // Copy only the bytes from data that we're supposed to onto the stack, and
+ // then move it into the message buffer 32 bits at a time (because it might
+ // get unhappy about writing individual bytes). Plus, we have to byte-swap
+ // each 32-bit word because this hardware is weird...
+ {
+ uint32_t data_words[2] = {0, 0};
+ for (uint8_t *dest = (uint8_t *)&data_words[0];
+ dest - (uint8_t *)&data_words[0] < (ptrdiff_t)length; ++dest) {
+ *dest = *data;
+ ++data;
+ }
+ message_buffer->data[0] = __builtin_bswap32(data_words[0]);
+ message_buffer->data[1] = __builtin_bswap32(data_words[1]);
+ }
+ message_buffer->control_timestamp =
+ CAN_MB_CONTROL_INSERT_DLC(length) | CAN_MB_CONTROL_SRR |
+ CAN_MB_CONTROL_IDE | CAN_MB_CONTROL_INSERT_CODE(CAN_MB_CODE_TX_DATA);
+ return 0;
+}
+
+void can_receive_command(unsigned char *data, int *length) {
+ if (0) {
+ static int i = 0;
+ if (i++ == 13) {
+ printf("IFLAG1=%" PRIx32 " ESR=%" PRIx32 " ESR1=%" PRIx32 "\n",
+ CAN0_IFLAG1, CAN0_ECR, CAN0_ESR1);
+ i = 0;
+ }
+ }
+ if ((CAN0_IFLAG1 & (1 << 1)) == 0) {
+ *length = -1;
+ return;
+ }
+ can_vesc_process_rx(&CAN0_MESSAGES[1], data, length);
+}
diff --git a/motors/peripheral/can.h b/motors/peripheral/can.h
new file mode 100644
index 0000000..acd6cce
--- /dev/null
+++ b/motors/peripheral/can.h
@@ -0,0 +1,24 @@
+#ifndef PERIPHERAL_CAN_H_
+#define PERIPHERAL_CAN_H_
+
+#include <stdint.h>
+
+// The code defined here calls functions in vesc/vesc_can.h from various
+// interrupts and expects them to call back into this file to do something.
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void can_init(void);
+
+int can_send(uint32_t can_id, const unsigned char *data, unsigned int length);
+
+// Sets *length to -1 if there isn't a new piece of data to receive.
+void can_receive_command(unsigned char *data, int *length);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // PERIPHERAL_CAN_H_
diff --git a/motors/peripheral/configuration.h b/motors/peripheral/configuration.h
new file mode 100644
index 0000000..3fa686a
--- /dev/null
+++ b/motors/peripheral/configuration.h
@@ -0,0 +1,24 @@
+#ifndef MOTORS_PERIPHERAL_CONFIGURATION_H_
+#define MOTORS_PERIPHERAL_CONFIGURATION_H_
+
+// We're just going to leave the default DMA priorities which correspond to the
+// channel numbers and fixed priority mode, so channel 0 is the lowest priority
+// and 15 is the highest.
+// We're also going to leave DMA_CR alone except for setting EMLM.
+
+// The frequency of the peripheral bus(es) in hz.
+#define BUS_CLOCK_FREQUENCY (F_CPU / 2)
+
+// The frequency we switch the motor FETs at in hz.
+#define SWITCHING_FREQUENCY 20000
+
+#if 0
+// Which PDB the ADC triggering uses.
+#define ADC_TRIGGER_PDB 0
+// The DMA channel which copies ADC results.
+#define ADC_RESULT_DMA_CHANNEL 7
+// The DMA channel which reconfigures the ADCs to take the next samples.
+#define ADC_RECONFIGURE_DMA_CHANNEL 8
+#endif
+
+#endif // MOTORS_PERIPHERAL_CONFIGURATION_H_
diff --git a/motors/plot.py b/motors/plot.py
new file mode 100755
index 0000000..013f308
--- /dev/null
+++ b/motors/plot.py
@@ -0,0 +1,25 @@
+#!/usr/bin/python3
+
+import numpy
+from matplotlib import pylab
+
+data = numpy.loadtxt('/tmp/dump3.csv',
+ delimiter=',',
+ skiprows=1)
+x = range(len(data))
+
+pylab.subplot(1, 1, 1)
+pylab.plot(x, [d[0] for d in data], 'ro', label='ia')
+pylab.plot(x, [d[1] for d in data], 'go', label='ib')
+pylab.plot(x, [d[2] for d in data], 'bo', label='ic')
+pylab.plot(x, [d[3] for d in data], 'r--', label='ia_goal')
+pylab.plot(x, [d[4] for d in data], 'g--', label='ib_goal')
+pylab.plot(x, [d[5] for d in data], 'b--', label='ic_goal')
+pylab.plot(x, [d[6] for d in data], 'rx', label='i_overall')
+pylab.plot(x, [d[7] for d in data], 'gx', label='omega')
+pylab.plot(x, [d[8] for d in data], 'r', label='van')
+pylab.plot(x, [d[9] for d in data], 'g', label='vbn')
+pylab.plot(x, [d[10] for d in data], 'b', label='vcn')
+pylab.legend()
+
+pylab.show()
diff --git a/motors/usb/usb_serial.h b/motors/usb/usb_serial.h
index 2512adc..40e7c36 100644
--- a/motors/usb/usb_serial.h
+++ b/motors/usb/usb_serial.h
@@ -56,7 +56,7 @@
// Drops any unread input until the most recent packet sent.
void usb_serial_flush_input(int port);
-// Writes data. Returns -1 if it times out or size if it succeeds.
+// Writes data. Returns -1 if it times out or 0 if it succeeds.
//
// NOTE: This does not send immediately. The data is buffered
int usb_serial_write(int port, const void *buffer, uint32_t size);
diff --git a/motors/util.h b/motors/util.h
index e4a0271..d1eaf53 100644
--- a/motors/util.h
+++ b/motors/util.h
@@ -1,6 +1,11 @@
#ifndef MOTORS_UTIL_H_
#define MOTORS_UTIL_H_
+#include <stdint.h>
+#include <stddef.h>
+
+#include "motors/core/kinetis.h"
+
#ifdef __cplusplus
extern "C"
{
@@ -97,6 +102,7 @@
#define CAN_MB_CONTROL_IDE ((uint32_t)(1 << 21))
#define CAN_MB_CONTROL_SRR ((uint32_t)(1 << 22))
#define CAN_MB_CONTROL_INSERT_CODE(n) ((uint32_t)(((n) & 0xF) << 24))
+#define CAN_MB_CONTROL_EXTRACT_CODE(n) ((uint32_t)(((n) >> 24) & 0xF))
#define CAN_MB_CONTROL_CODE_BUSY_MASK CAN_MB_CONTROL_INSERT_CODE(1)
#define CAN_MB_PRIO_ID_PRIORITY_MASK ((uint32_t)((1 << 29) - 1))
#define CAN_MB_CODE_RX_INACTIVE 0
@@ -161,6 +167,116 @@
};
#endif // __cplusplus
+typedef struct {
+ uint32_t saddr;
+ uint16_t soff;
+ uint16_t attr;
+ uint32_t nbytes_mlno;
+ uint32_t slast;
+ uint32_t daddr;
+ uint16_t doff;
+ uint16_t citer;
+ uint32_t dlastsga;
+ uint32_t _reserved;
+} DmaTcd __attribute__((aligned(0x20)));
+#ifdef __cplusplus
+static_assert(sizeof(DmaTcd) == 0x20, "DMA TCD is the wrong size");
+#endif
+
+#define ALL_FTM_REGISTERS \
+ FOR_BOTH_FTM_REGISTER(SC) \
+ FOR_BOTH_FTM_REGISTER(CNT) \
+ FOR_BOTH_FTM_REGISTER(MOD) \
+ FOR_BOTH_FTM_REGISTER(C0SC) \
+ FOR_BOTH_FTM_REGISTER(C0V) \
+ FOR_BOTH_FTM_REGISTER(C1SC) \
+ FOR_BOTH_FTM_REGISTER(C1V) \
+ FOR_BIG_FTM_REGISTER(C2SC) \
+ FOR_BIG_FTM_REGISTER(C2V) \
+ FOR_BIG_FTM_REGISTER(C3SC) \
+ FOR_BIG_FTM_REGISTER(C3V) \
+ FOR_BIG_FTM_REGISTER(C4SC) \
+ FOR_BIG_FTM_REGISTER(C4V) \
+ FOR_BIG_FTM_REGISTER(C5SC) \
+ FOR_BIG_FTM_REGISTER(C5V) \
+ FOR_BIG_FTM_REGISTER(C6SC) \
+ FOR_BIG_FTM_REGISTER(C6V) \
+ FOR_BIG_FTM_REGISTER(C7SC) \
+ FOR_BIG_FTM_REGISTER(C7V) \
+ FOR_BOTH_FTM_REGISTER(CNTIN) \
+ FOR_BOTH_FTM_REGISTER(STATUS) \
+ FOR_BOTH_FTM_REGISTER(MODE) \
+ FOR_BOTH_FTM_REGISTER(SYNC) \
+ FOR_BOTH_FTM_REGISTER(OUTINIT) \
+ FOR_BOTH_FTM_REGISTER(OUTMASK) \
+ FOR_BOTH_FTM_REGISTER(COMBINE) \
+ FOR_BOTH_FTM_REGISTER(DEADTIME) \
+ FOR_BOTH_FTM_REGISTER(EXTTRIG) \
+ FOR_BOTH_FTM_REGISTER(POL) \
+ FOR_BOTH_FTM_REGISTER(FMS) \
+ FOR_BOTH_FTM_REGISTER(FILTER) \
+ FOR_BOTH_FTM_REGISTER(FLTCTRL) \
+ FOR_LITTLE_FTM_REGISTER(QDCTRL) \
+ FOR_BOTH_FTM_REGISTER(CONF) \
+ FOR_BOTH_FTM_REGISTER(FLTPOL) \
+ FOR_BOTH_FTM_REGISTER(SYNCONF) \
+ FOR_BOTH_FTM_REGISTER(INVCTRL) \
+ FOR_BOTH_FTM_REGISTER(SWOCTRL) \
+ FOR_BOTH_FTM_REGISTER(PWMLOAD)
+
+typedef struct {
+#define FOR_BIG_FTM_REGISTER(name) volatile uint32_t name;
+#define FOR_BOTH_FTM_REGISTER(name) volatile uint32_t name;
+#define FOR_LITTLE_FTM_REGISTER(name) const uint32_t _reserved_##name;
+ ALL_FTM_REGISTERS
+#undef FOR_BIG_FTM_REGISTER
+#undef FOR_LITTLE_FTM_REGISTER
+} BigFTM;
+
+typedef struct {
+#define FOR_BIG_FTM_REGISTER(name) const uint32_t _reserved_##name;
+#define FOR_LITTLE_FTM_REGISTER(name) volatile uint32_t name;
+ ALL_FTM_REGISTERS
+#undef FOR_BIG_FTM_REGISTER
+#undef FOR_LITTLE_FTM_REGISTER
+#undef FOR_BOTH_FTM_REGISTER
+} LittleFTM;
+
+#define FTM0 ((BigFTM *)0x40038000)
+#define FTM1 ((LittleFTM *)0x40039000)
+#define FTM2 ((LittleFTM *)0x400B8000)
+#define FTM3 ((BigFTM *)0x400B9000)
+
+#ifdef __cplusplus
+#define FOR_BIG_FTM_REGISTER(name) \
+ static_assert(offsetof(BigFTM, name) == \
+ (reinterpret_cast<const volatile char *>(&FTM0_##name) - \
+ reinterpret_cast<volatile char *>(FTM0)), \
+ #name " is at the wrong place"); \
+ static_assert(offsetof(BigFTM, name) == \
+ (reinterpret_cast<const volatile char *>(&FTM3_##name) - \
+ reinterpret_cast<volatile char *>(FTM3)), \
+ #name " is at the wrong place");
+#define FOR_LITTLE_FTM_REGISTER(name) \
+ static_assert(offsetof(LittleFTM, name) == \
+ (reinterpret_cast<const volatile char *>(&FTM1_##name) - \
+ reinterpret_cast<volatile char *>(FTM1)), \
+ #name " is at the wrong place"); \
+ static_assert(offsetof(LittleFTM, name) == \
+ (reinterpret_cast<const volatile char *>(&FTM2_##name) - \
+ reinterpret_cast<volatile char *>(FTM2)), \
+ #name " is at the wrong place");
+#define FOR_BOTH_FTM_REGISTER(name) \
+ FOR_BIG_FTM_REGISTER(name) \
+ FOR_LITTLE_FTM_REGISTER(name)
+ALL_FTM_REGISTERS
+#undef FOR_BIG_FTM_REGISTER
+#undef FOR_LITTLE_FTM_REGISTER
+#undef FOR_BOTH_FTM_REGISTER
+#endif
+
+#undef ALL_FTM_REGISTERS
+
#ifdef __cplusplus
}
#endif