MT6701 Magnetic Encoder

The MT6701 magnetic encoder component provides the user a convenient way to measure

  • Raw count

  • Raw radians

  • Raw degrees

  • Accumulated count (since the component was created)

  • Accumulated radians (since the component was created)

  • Accumulated degrees (since the component was created)

  • Speed (rotations per minute / RPM)

It does so by spawning a task which periodically reads the magnetic encoder, updates the accumulator, and computes the velocity. The component can be configured to optionally filter the velocity.

The periodicity / update rate of the encoder can be configured at time of creation.

The encoder is designed to fulfill the needs of the BldcMotor API, to provide closed-loop motor control.

API Reference

Header File

Classes

template<Mt6701Interface Interface = Mt6701Interface::I2C>
class Mt6701 : public espp::BasePeripheral<uint8_t, Mt6701Interface::I2C == Mt6701Interface::I2C>

Class for position and velocity measurement using a MT6701 magnetic encoder. This class starts its own measurement task at the specified frequency which reads the current angle, updates the accumulator, and filters / updates the velocity measurement. The Mt6701 supports I2C, SSI, ABZ, UVW, Analog/PWM, and Push-Button interfaces.

Mt6701 I2C Example

    std::atomic<bool> quit_test = false;

    // make the I2C that we'll use to communicate
    espp::I2c i2c({
        .port = I2C_NUM_0,
        .sda_io_num = (gpio_num_t)CONFIG_EXAMPLE_I2C_SDA_GPIO,
        .scl_io_num = (gpio_num_t)CONFIG_EXAMPLE_I2C_SCL_GPIO,
        .clk_speed = 1 * 1000 * 1000, // MT6701 supports 1 MHz I2C
    });

    // make the velocity filter
    static constexpr float filter_cutoff_hz = 10.0f;
    static constexpr float encoder_update_period = 0.001f; // seconds
    espp::ButterworthFilter<2, espp::BiquadFilterDf2> filter(
        {.normalized_cutoff_frequency = 2.0f * filter_cutoff_hz * encoder_update_period});
    auto filter_fn = [&filter](float raw) -> float { return filter.update(raw); };

    // now make the mt6701 which decodes the data
    using Mt6701 = espp::Mt6701<espp::Mt6701Interface::I2C>;
    Mt6701 mt6701(
        Mt6701::Config{.write = std::bind(&espp::I2c::write, &i2c, std::placeholders::_1,
                                          std::placeholders::_2, std::placeholders::_3),
                       .read = std::bind(&espp::I2c::read, &i2c, std::placeholders::_1,
                                         std::placeholders::_2, std::placeholders::_3),
                       .velocity_filter = filter_fn,
                       .update_period = std::chrono::duration<float>(encoder_update_period),
                       .run_task = true, // run a task which calls the update function at the update
                                         // period
                       .log_level = espp::Logger::Verbosity::WARN});

    // NOTE: since this is I2C, we cannot get the magnetic field strength,
    // tracking status, or push button state

    // and finally, make the task to periodically poll the mt6701 and print the
    // state. NOTE: the Mt6701 runs its own task to maintain state, so we're
    // just polling the current state.
    auto task_fn = [&quit_test, &mt6701](std::mutex &m, std::condition_variable &cv) {
      static auto start = std::chrono::high_resolution_clock::now();
      auto now = std::chrono::high_resolution_clock::now();
      auto seconds = std::chrono::duration<float>(now - start).count();
      auto count = mt6701.get_count();
      auto radians = mt6701.get_radians();
      auto degrees = mt6701.get_degrees();
      auto rpm = mt6701.get_rpm();
      fmt::print("{:.3f}, {}, {:.3f}, {:.3f}, {:.3f}\n", seconds, count, radians, degrees, rpm);
      quit_test = degrees <= -720.0f;
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 10ms);
      }
      // don't want to stop the task
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config =
                                {
                                    .name = "Mt6701 Task",
                                    .stack_size_bytes = 5 * 1024,
                                },
                            .log_level = espp::Logger::Verbosity::WARN});
    fmt::print("% time(s), count, radians, degrees, rpm\n");
    task.start();

Mt6701 SSI / SPI Example

    std::atomic<bool> quit_test = false;

    // make the SSI (SPI) that we'll use to communicate

    // create the spi host
    spi_device_handle_t encoder_spi_handle;
    spi_bus_config_t buscfg;
    memset(&buscfg, 0, sizeof(buscfg));
    buscfg.mosi_io_num = -1;
    buscfg.miso_io_num = CONFIG_EXAMPLE_SPI_MISO_GPIO;
    buscfg.sclk_io_num = CONFIG_EXAMPLE_SPI_SCLK_GPIO;
    buscfg.quadwp_io_num = -1;
    buscfg.quadhd_io_num = -1;
    buscfg.max_transfer_sz = 32;

    // create the spi device
    spi_device_interface_config_t devcfg;
    memset(&devcfg, 0, sizeof(devcfg));
    devcfg.mode = 0;
    devcfg.clock_speed_hz = CONFIG_EXAMPLE_SPI_CLOCK_SPEED; // Supports 64ns clock period, 15.625MHz
    devcfg.input_delay_ns = 0;
    devcfg.spics_io_num = CONFIG_EXAMPLE_SPI_CS_GPIO;
    devcfg.queue_size = 1;

    esp_err_t ret;
    // Initialize the SPI bus
    auto spi_num = SPI2_HOST;
    ret = spi_bus_initialize(spi_num, &buscfg, SPI_DMA_CH_AUTO);
    ESP_ERROR_CHECK(ret);
    // Attach the LCD to the SPI bus
    ret = spi_bus_add_device(spi_num, &devcfg, &encoder_spi_handle);
    ESP_ERROR_CHECK(ret);

    // make the velocity filter
    static constexpr float filter_cutoff_hz = 10.0f;
    static constexpr float encoder_update_period = 0.001f; // seconds
    espp::ButterworthFilter<2, espp::BiquadFilterDf2> filter(
        {.normalized_cutoff_frequency = 2.0f * filter_cutoff_hz * encoder_update_period});
    auto filter_fn = [&filter](float raw) -> float { return filter.update(raw); };

    // now make the mt6701 which decodes the data
    using Mt6701 = espp::Mt6701<espp::Mt6701Interface::SSI>;
    Mt6701 mt6701({.read = [&](uint8_t *data, size_t len) -> bool {
                     // we can use the SPI_TRANS_USE_RXDATA since our length is <= 4 bytes (32
                     // bits), this means we can directly use the tarnsaction's rx_data field
                     static constexpr uint8_t SPIBUS_READ = 0x80;
                     spi_transaction_t t = {
                         .flags = 0,
                         .cmd = 0,
                         .addr = SPIBUS_READ,
                         .length = len * 8,
                         .rxlength = len * 8,
                         .user = nullptr,
                         .tx_buffer = nullptr,
                         .rx_buffer = data,
                     };
                     if (len <= 4) {
                       t.flags = SPI_TRANS_USE_RXDATA;
                       t.rx_buffer = nullptr;
                     }
                     esp_err_t err = spi_device_transmit(encoder_spi_handle, &t);
                     if (err != ESP_OK) {
                       return false;
                     }
                     if (len <= 4) {
                       // copy the data from the rx_data field
                       for (size_t i = 0; i < len; i++) {
                         data[i] = t.rx_data[i];
                       }
                     }
                     return true;
                   },
                   .velocity_filter = filter_fn,
                   .update_period = std::chrono::duration<float>(encoder_update_period),
                   .run_task = true, // run a task which calls the update function at the update
                                     // period
                   .log_level = espp::Logger::Verbosity::WARN});

    // get the initial state
    auto field_strength = mt6701.get_magnetic_field_strength();
    auto tracking_status = mt6701.get_tracking_status();
    bool push_button = mt6701.get_push_button();
    fmt::print("Initial state: field_strength: {}, tracking_status: {}, push_button: {}\n",
               field_strength, tracking_status, push_button);

    // and finally, make the task to periodically poll the mt6701 and print the
    // state. NOTE: the Mt6701 runs its own task to maintain state, so we're
    // just polling the current state.
    auto task_fn = [&quit_test, &mt6701](std::mutex &m, std::condition_variable &cv) {
      static auto start = std::chrono::high_resolution_clock::now();
      auto now = std::chrono::high_resolution_clock::now();
      auto seconds = std::chrono::duration<float>(now - start).count();
      auto count = mt6701.get_count();
      auto radians = mt6701.get_radians();
      auto degrees = mt6701.get_degrees();
      auto rpm = mt6701.get_rpm();
      fmt::print("{:.3f}, {}, {:.3f}, {:.3f}, {:.3f}\n", seconds, count, radians, degrees, rpm);
      quit_test = degrees <= -720.0f;
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 10ms);
      }
      // don't want to stop the task
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config =
                                {
                                    .name = "Mt6701 Task",
                                    .stack_size_bytes = 5 * 1024,
                                },
                            .log_level = espp::Logger::Verbosity::WARN});
    fmt::print("% time(s), count, radians, degrees, rpm\n");
    task.start();

Note

This implementation currently only supports I2C and SSI interfaces.

Note

There is an implicit assumption in this class regarding the maximum velocity it can measure (above which there will be aliasing). The fastest velocity it can measure will be (0.5f / update_period * 60.0f) RPM which is half a rotation in one update period.

Note

The assumption above also affects the reliability of the accumulator, since it is based on accumulating position differences every update period.

Public Types

enum class MagneticFieldStrength : uint8_t

Enum class for the magnetic field strength of the MT6701.

Values:

enumerator NORMAL

The magnetic field is normal.

enumerator TOO_STRONG

The magnetic field is too strong to measure the angle.

enumerator TOO_WEAK

The magnetic field is too weak to measure the angle.

enum class TrackingStatus : uint8_t

Enum class for the tracking status of the MT6701.

Values:

enumerator NORMAL

Normal tracking status.

enumerator LOST

Tracking has been lost.

typedef std::function<float(float raw)> velocity_filter_fn

Filter the input raw velocity and return it.

Param raw

Most recent raw velocity measured.

Return

Filtered velocity.

typedef std::function<bool(uint8_t)> probe_fn

Function to probe the peripheral

Param address

The address to probe

Return

True if the peripheral is found at the given address

Public Functions

inline explicit Mt6701(const Config &config)

Construct the Mt6701 and start the update task.

inline void initialize(bool run_task, std::error_code &ec)

Initialize the accumulator to the current position and start the update task.

Note

If you do not start the task, you must call update() manually.

Parameters
  • run_task – Whether to start the update task.

  • ec – Error code to set if there is an error.

inline bool needs_zero_search() const

Return whether the sensor has found absolute 0 yet.

Note

The MT6701 (using I2C/SPI) does not need to search for absolute 0 and will always know it on startup. Therefore this function always returns false.

Returns

True because the magnetic sensor (using I2C/SPI) does not need to sarch for 0.

inline int get_count() const

Get the most recently updated raw count value from the encoder.

Note

This value always represents the angle of the encoder modulo one rotation, meaning it only represents the range 0 to 360 degrees. It is not recommended to use this function, but is provided for edge use cases.

Returns

Raw count value in the range [0, 16384] (0 to 360 degrees).

inline int get_accumulator() const

Return the accumulated count that the encoder has generated since it was initialized.

Note

This value is a raw counter value that can be +/-, meaning COUNTS_PER_REVOLUTION can be used to convert it to revolutions.

Returns

Raw accumulator value.

inline float get_mechanical_radians() const

Return the mechanical / shaft angle of the encoder, in radians, within the range [0, 2pi].

Returns

Angle in radians of the encoder within the range [0, 2pi].

inline float get_mechanical_degrees() const

Return the mechanical / shaft angle of the encoder, in degrees, within the range [0, 360].

Returns

Angle in degrees of the encoder within the range [0, 360].

inline float get_radians() const

Return the accumulated position of the encoder, in radians.

Note

This can be any value, it is not restricted to [0, 2pi].

Returns

Position in radians of the encoder.

inline float get_degrees() const

Return the accumulated position of the encoder, in degrees.

Note

This can be any value, it is not restricted to [0, 360].

Returns

Position in degrees of the encoder.

inline float get_rpm() const

Return the filtered velocity of the encoder, in RPM.

Returns

Filtered velocity (revolutions / minute, RPM).

MagneticFieldStrength get_magnetic_field_strength () const requires(Interface

Return the magnetic field strength of the encoder.

Note

This function is only available when using SSI communications.

Returns

Magnetic field strength of the encoder.

inline bool probe (std::error_code &ec) requires(UseAddress)

Probe the peripheral

Note

This function is thread safe

Note

If the probe function is not set, this function will return false and set the error code to operation_not_supported

Note

This function is only available if UseAddress is true

Parameters

ec – The error code to set if there is an error

Returns

True if the peripheral is found

inline void set_address (uint8_t address) requires(UseAddress)

Set the address of the peripheral

Note

This function is thread safe

Note

This function is only available if UseAddress is true

Parameters

address – The address of the peripheral

inline void set_probe (const probe_fn &probe) requires(UseAddress)

Set the probe function

Note

This function is thread safe

Note

This should rarely be used, as the probe function is usually set in the constructor. If you need to change the probe function, consider using the set_config function instead.

Note

This function is only available if UseAddress is true

Parameters

probe – The probe function

inline void set_write(const write_fn &write)

Set the write function

Note

This function is thread safe

Note

This should rarely be used, as the write function is usually set in the constructor. If you need to change the write function, consider using the set_config function instead.

Parameters

write – The write function

inline void set_read(const read_fn &read)

Set the read function

Note

This function is thread safe

Note

This should rarely be used, as the read function is usually set in the constructor. If you need to change the read function, consider using the set_config function instead.

Parameters

read – The read function

inline void set_read_register(const read_register_fn &read_register)

Set the read register function

Note

This function is thread safe

Note

This should rarely be used, as the read register function is usually set in the constructor. If you need to change the read register function, consider using the set_config function instead.

Parameters

read_register – The read register function

inline void set_write_then_read(const write_then_read_fn &write_then_read)

Set the write then read function

Note

This function is thread safe

Note

This should rarely be used, as the write then read function is usually set in the constructor. If you need to change the write then

Parameters

write_then_read – The write then read function

inline void set_separate_write_then_read_delay(const std::chrono::milliseconds &delay)

Set the delay between the write and read operations in write_then_read

Note

This function is thread safe

Note

This should rarely be used, as the delay is usually set in the constructor. If you need to change the delay, consider using the set_config function instead.

Note

This delay is only used if the write_then_read function is not set to a custom function and the write and read functions are separate functions.

Parameters

delay – The delay between the write and read operations in write_then_read

inline void set_config(const Config &config)

Set the configuration for the peripheral

Note

This function is thread safe

Note

The configuration should normally be set in the constructor, but this function can be used to change the configuration after the peripheral has been created - for instance if the peripheral could be found on different communications buses.

Parameters

config – The configuration for the peripheral

inline void set_config(Config &&config)

Set the configuration for the peripheral

Note

This function is thread safe

Note

The configuration should normally be set in the constructor, but this function can be used to change the configuration after the peripheral has been created - for instance if the peripheral could be found on different communications buses.

Parameters

config – The configuration for the peripheral

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters

rate_limit – The rate limit to use for the logger

Public Static Attributes

static constexpr uint8_t DEFAULT_ADDRESS = (0b0000110)

I2C address of the MT6701. It can be programmed to be 0b1000110 as well. Only used if Interface == Mt6701Interface::I2C.

static constexpr int COUNTS_PER_REVOLUTION = 16384

Int number of counts per revolution for the magnetic encoder.

static constexpr float COUNTS_PER_REVOLUTION_F = 16384.0f

Float number of counts per revolution for the magnetic encoder.

static constexpr float COUNTS_TO_RADIANS = 2.0f * M_PI / COUNTS_PER_REVOLUTION_F

Conversion factor to convert from count value to radians.

static constexpr float COUNTS_TO_DEGREES = 360.0f / COUNTS_PER_REVOLUTION_F

Conversion factor to convert from count value to degrees.

static constexpr float SECONDS_PER_MINUTE = 60.0f

Conversion factor to convert from seconds to minutes.

struct Config

Configuration information for the Mt6701.

Public Members

uint8_t device_address = DEFAULT_ADDRESS

I2C address of the device. Only used if Interface == Mt6701Interface::I2C.

BasePeripheral<uint8_t, Interface == Mt6701Interface::I2C>::write_fn write{nullptr}

Function to write to the device.

BasePeripheral<uint8_t, Interface == Mt6701Interface::I2C>::read_fn read{nullptr}

Function to read data from the device.

velocity_filter_fn velocity_filter = {nullptr}

Function to filter the veolcity.

Note

Will be called once every update_period seconds.

std::chrono::duration<float> update_period{.01f}

Update period (1/sample rate) in seconds. This determines the periodicity of the task which will read the position, update the accumulator, and update/filter velocity.

bool auto_init = {true}

Whether to automatically initialize the accumulator to the current position on startup.

bool run_task = {true}

Whether to run the task on startup. If false, you must call update() manually.