-
Notifications
You must be signed in to change notification settings - Fork 29
feat(basicmicro): MCP/RoboClaw packet-serial motor controller component #729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
fc14122
feat(basicmicro): MCP/RoboClaw packet-serial motor controller component
finger563 5b9837e
fix(basicmicro): address PR #729 review comments
finger563 eb15928
fix(basicmicro): transport validation + cppcheck uninitvar (PR #729 r…
finger563 387abbf
fix(basicmicro): read command 90 status as 32-bit with a legacy 16-bi…
finger563 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # NOTE: like odrive_native, this component's detail/ lives INSIDE include/ | ||
| # (include/detail/*.hpp) so that the host-buildable wire core can be included | ||
| # as `#include "detail/basicmicro_core.hpp"` by consumers and by the host test. | ||
| idf_component_register( | ||
| INCLUDE_DIRS "include" | ||
| REQUIRES base_component | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # Basicmicro (MCP / RoboClaw) Motor Controller Component | ||
|
|
||
| [](https://components.espressif.com/components/espp/basicmicro) | ||
|
|
||
| `espp::Basicmicro` is a driver for Basicmicro **MCP236 / MCP266** (and other | ||
| RoboClaw-family) brushed DC motor controllers speaking their **PACKET SERIAL** | ||
| protocol, typically over UART. | ||
|
|
||
| The component is transport-agnostic: it performs no I/O itself and instead | ||
| calls user-provided `write` / `read` functions for each transaction, so it | ||
| works over a UART driver, USB CDC, an RS-232 adapter, etc. All wire-format | ||
| logic (CRC16, packet building, reply validation, big-endian codecs) lives in | ||
| `include/detail/basicmicro_core.hpp`, a host-buildable core with zero ESP | ||
| dependencies that is unit-tested off-target. | ||
|
|
||
| <!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc --> | ||
| **Table of Contents** | ||
|
|
||
| - [Basicmicro (MCP / RoboClaw) Motor Controller Component](#basicmicro-mcp--roboclaw-motor-controller-component) | ||
| - [Features](#features) | ||
| - [Protocol](#protocol) | ||
| - [API](#api) | ||
| - [Example](#example) | ||
| - [Testing](#testing) | ||
|
|
||
| <!-- markdown-toc end --> | ||
|
|
||
| ## Features | ||
|
|
||
| - Duty-cycle drive (commands 32/33/34) and closed-loop speed drive in | ||
| quadrature pulses per second (35/36/37), with optional acceleration ramps | ||
| (38/39/40) | ||
| - Buffered speed / accel / distance motion commands (41-46) plus buffer-state | ||
| readback (47) | ||
| - Encoder support: counts (16/17/78), speeds (18/19/79), reset (20) and | ||
| encoder-mode readback (91) | ||
| - Velocity PID get/set with automatic 16.16 fixed-point conversion (28/29, | ||
| 55/56) | ||
| - Telemetry: firmware version (21), main/logic battery voltage (24/25), motor | ||
| currents (49), motor PWMs (48), board temperatures (82/83) and unit status | ||
| (90) | ||
| - Management: write settings to EEPROM (94), E-Stop reset (200) | ||
| - No exceptions; all methods report errors via `std::error_code` | ||
| - Thread-safe: each transaction (request + ACK/reply) is serialized by an | ||
| internal mutex | ||
|
|
||
| ## Protocol | ||
|
|
||
| The packet serial protocol (MCP Series User Manual, section 2.2): | ||
|
|
||
| - Write commands send `[Address, Command, Data..., CRC16]`; the controller | ||
| replies with a single `0xFF` ACK byte only when the packet was valid. | ||
| - Read commands send `[Address, Command]` (no CRC); the reply is the data | ||
| followed by a CRC16 computed over the *sent* address + command bytes plus the | ||
| reply data. | ||
| - All multi-byte values (including the CRC) are big-endian ("high byte first"). | ||
| - CRC16 is CRC-16/XMODEM (poly `0x1021`, init `0`, non-reflected). | ||
| - Error recovery: the controller discards a partial packet after a 10 ms | ||
| inter-byte gap, so the configured receive timeout (>= 10 ms, default 20 ms) | ||
| doubles as the recovery mechanism. | ||
|
|
||
| ## API | ||
|
|
||
| See the [documentation](https://esp-cpp.github.io/espp/motor_control/basicmicro.html). | ||
|
|
||
| ## Example | ||
|
|
||
| The [example](./example) shows how to wire the driver to the ESP-IDF UART | ||
| driver, read the firmware version / battery voltage / status, run a gentle | ||
| duty-cycle ramp on motor 1 with encoder readback, and stop. | ||
|
|
||
| ## Testing | ||
|
|
||
| The wire core is host-buildable and unit-tested without ESP-IDF: | ||
|
|
||
| ```sh | ||
| c++ -std=c++20 -I include -o /tmp/bm_test test/basicmicro_host_test.cpp && /tmp/bm_test | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| cmake_minimum_required(VERSION 3.20) | ||
|
|
||
| set(ENV{IDF_COMPONENT_MANAGER} "0") | ||
| include($ENV{IDF_PATH}/tools/cmake/project.cmake) | ||
|
|
||
| set(EXTRA_COMPONENT_DIRS | ||
| "${CMAKE_CURRENT_LIST_DIR}/../.." | ||
| ) | ||
|
|
||
| set( | ||
| COMPONENTS | ||
| "main esptool_py esp_driver_uart basicmicro" | ||
| CACHE STRING | ||
| "List of components to include" | ||
| ) | ||
|
|
||
| project(basicmicro_example) | ||
|
|
||
| set(CMAKE_CXX_STANDARD 20) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Basicmicro (MCP / RoboClaw) Example | ||
|
|
||
| This example demonstrates how to use the `espp::Basicmicro` component to talk | ||
| to a Basicmicro MCP236 / MCP266 (or RoboClaw-family) motor controller over | ||
| UART using the packet serial protocol. It: | ||
|
|
||
| 1. reads the firmware version, main battery voltage and unit status, | ||
| 2. resets the encoders, | ||
| 3. runs a gentle duty-cycle ramp on motor 1 while reading back the encoder | ||
| count and speed, then ramps back down and stops, and | ||
| 4. periodically logs the motor currents and board temperature. | ||
|
|
||
| <!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc --> | ||
| **Table of Contents** | ||
|
|
||
| - [Basicmicro (MCP / RoboClaw) Example](#basicmicro-mcp--roboclaw-example) | ||
| - [Requirements](#requirements) | ||
| - [Hardware](#hardware) | ||
| - [Build](#build) | ||
| - [Flash and Monitor](#flash-and-monitor) | ||
|
|
||
| <!-- markdown-toc end --> | ||
|
|
||
| ## Requirements | ||
|
|
||
| - ESP-IDF installed and `get_idf` available in your shell | ||
| - A Basicmicro MCP / RoboClaw controller configured for **packet serial** mode | ||
| at 38400 baud, address `0x80` (the defaults used by this example) | ||
|
|
||
| ## Hardware | ||
|
|
||
| | ESP32 (this example) | MCP / RoboClaw | | ||
| |----------------------|----------------| | ||
| | GPIO 17 (UART1 TX) | S1 (RX) | | ||
| | GPIO 16 (UART1 RX) | S2 (TX) | | ||
| | GND | GND | | ||
|
|
||
| Adjust the pins / port / baud rate at the top of | ||
| [basicmicro_example.cpp](./main/basicmicro_example.cpp) to match your wiring. | ||
|
|
||
| ## Build | ||
|
|
||
| ```sh | ||
| # From repo root | ||
| cd components/basicmicro/example | ||
| get_idf | ||
| idf.py set-target esp32 | ||
| idf.py build | ||
| ``` | ||
|
|
||
| ## Flash and Monitor | ||
|
|
||
| ```sh | ||
| idf.py flash monitor | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| idf_component_register( | ||
| SRC_DIRS "." | ||
| INCLUDE_DIRS "." | ||
| ) |
122 changes: 122 additions & 0 deletions
122
components/basicmicro/example/main/basicmicro_example.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| #include <chrono> | ||
| #include <thread> | ||
|
|
||
| #include "driver/uart.h" | ||
|
|
||
| #include "basicmicro.hpp" | ||
| #include "logger.hpp" | ||
|
|
||
| using namespace std::chrono_literals; | ||
|
|
||
| // UART wiring for the MCP: it defaults to packet serial mode at 38400 baud; S1 | ||
| // (controller RX) goes to our TX pin and S2 (controller TX) goes to our RX pin. | ||
| // File-scope so the captureless transport lambdas below can reference them | ||
| // without any capture-semantics ambiguity. | ||
| static constexpr uart_port_t uart_port = UART_NUM_1; | ||
| static constexpr int uart_tx_pin = 17; // -> MCP S1 | ||
| static constexpr int uart_rx_pin = 16; // <- MCP S2 | ||
| static constexpr int uart_baud = 38400; | ||
|
|
||
| extern "C" void app_main(void) { | ||
| espp::Logger logger({.tag = "Basicmicro Example", .level = espp::Logger::Verbosity::INFO}); | ||
| logger.info("Starting basicmicro example"); | ||
|
|
||
| //! [basicmicro example] | ||
|
|
||
| uart_config_t uart_config = {}; | ||
| uart_config.baud_rate = uart_baud; | ||
| uart_config.data_bits = UART_DATA_8_BITS; | ||
| uart_config.parity = UART_PARITY_DISABLE; | ||
| uart_config.stop_bits = UART_STOP_BITS_1; | ||
| uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; | ||
| uart_config.source_clk = UART_SCLK_DEFAULT; | ||
| ESP_ERROR_CHECK(uart_driver_install(uart_port, 256, 0, 0, nullptr, 0)); | ||
| ESP_ERROR_CHECK(uart_param_config(uart_port, &uart_config)); | ||
| ESP_ERROR_CHECK( | ||
| uart_set_pin(uart_port, uart_tx_pin, uart_rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); | ||
|
|
||
| espp::Basicmicro mcp({ | ||
| .address = 0x80, // default packet serial address (0x80 - 0x87) | ||
| .write = | ||
| [](std::span<const uint8_t> data) { | ||
| const int written = uart_write_bytes( | ||
| uart_port, reinterpret_cast<const char *>(data.data()), data.size()); | ||
| return written == static_cast<int>(data.size()); | ||
| }, | ||
| .read = [](std::span<uint8_t> data, std::chrono::milliseconds timeout) -> size_t { | ||
| const int read = | ||
| uart_read_bytes(uart_port, data.data(), data.size(), pdMS_TO_TICKS(timeout.count())); | ||
| return read < 0 ? 0 : static_cast<size_t>(read); | ||
| }, | ||
| // must be >= 10 ms: a 10 ms quiet gap is also what clears the | ||
| // controller's packet buffer after a communication error | ||
| .timeout = 20ms, | ||
| .log_level = espp::Logger::Verbosity::INFO, | ||
| }); | ||
|
|
||
| std::error_code ec; | ||
|
|
||
| // identify the controller | ||
| std::string version; | ||
| if (mcp.read_firmware_version(version, ec)) { | ||
| logger.info("Firmware version: '{}'", version); | ||
| } else { | ||
| logger.error("Could not read firmware version: {}", ec.message()); | ||
| logger.error("Is the controller connected, powered, and in packet serial mode?"); | ||
| } | ||
|
|
||
| float volts{0}; | ||
| if (mcp.read_main_battery_voltage(volts, ec)) | ||
| logger.info("Main battery: {:.1f} V", volts); | ||
|
|
||
| uint32_t status{0}; | ||
| if (mcp.read_status(status, ec)) | ||
| logger.info("Status: 0x{:08X}{}", status, status == 0 ? " (normal)" : ""); | ||
|
|
||
| // start from a known encoder state | ||
| if (mcp.reset_encoders(ec)) | ||
| logger.info("Encoders reset"); | ||
|
|
||
| // gentle speed ramp on M1 (up to ~12.5% duty) with encoder readback, then | ||
| // back down to a stop. Duty-cycle drive works without a tuned velocity PID; | ||
| // if your encoders + PID are configured, try drive_m1_speed() instead. | ||
| static constexpr int16_t max_duty = 4096; // of 32767 | ||
| static constexpr int16_t step = 512; | ||
| for (int16_t duty = 0; duty <= max_duty; duty = static_cast<int16_t>(duty + step)) { | ||
| if (!mcp.drive_m1_duty(duty, ec)) { | ||
| logger.error("drive_m1_duty({}) failed: {}", duty, ec.message()); | ||
| break; | ||
| } | ||
| std::this_thread::sleep_for(250ms); | ||
| uint32_t count{0}; | ||
| uint8_t enc_status{0}; | ||
| int32_t speed{0}; | ||
| uint8_t direction{0}; | ||
| if (mcp.read_encoder_m1(count, enc_status, ec) && | ||
| mcp.read_encoder_speed_m1(speed, direction, ec)) { | ||
| logger.info("duty {:5d}: encoder count = {:10d}, speed = {} pulses/s ({})", duty, count, | ||
| speed, direction ? "backward" : "forward"); | ||
| } | ||
| } | ||
| for (int16_t duty = max_duty; duty >= 0; duty = static_cast<int16_t>(duty - step)) { | ||
| if (!mcp.drive_m1_duty(duty, ec)) | ||
| break; | ||
| std::this_thread::sleep_for(100ms); | ||
| } | ||
|
|
||
| // make sure the motor is stopped | ||
| if (mcp.drive_m1_duty(0, ec)) | ||
| logger.info("Motor stopped"); | ||
|
|
||
| //! [basicmicro example] | ||
|
|
||
| // periodically log some telemetry | ||
| while (true) { | ||
| float amps_m1{0}, amps_m2{0}, temperature{0}; | ||
| if (mcp.read_currents(amps_m1, amps_m2, ec)) | ||
| logger.info("Currents: M1 = {:.2f} A, M2 = {:.2f} A", amps_m1, amps_m2); | ||
| if (mcp.read_temperature(temperature, ec)) | ||
| logger.info("Temperature: {:.1f} C", temperature); | ||
| std::this_thread::sleep_for(5s); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Common ESP-related | ||
| # | ||
| CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 | ||
| CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| ## IDF Component Manager Manifest File | ||
| license: "MIT" | ||
| description: "Driver for Basicmicro MCP236 / MCP266 (and RoboClaw-family) brushed DC motor controllers using the packet serial protocol over UART" | ||
| url: "https://github.com/esp-cpp/espp/tree/main/components/basicmicro" | ||
| repository: "https://github.com/esp-cpp/espp.git" | ||
| maintainers: | ||
| - William Emfinger <waemfinger@gmail.com> | ||
| documentation: "https://esp-cpp.github.io/espp/motor_control/basicmicro.html" | ||
| examples: | ||
| - path: example | ||
| tags: | ||
| - cpp | ||
| - Component | ||
| - Basicmicro | ||
| - RoboClaw | ||
| - MCP236 | ||
| - MCP266 | ||
| - Motor | ||
| - Encoder | ||
| - UART | ||
| - Serial | ||
| dependencies: | ||
| idf: | ||
| version: '>=5.0' | ||
| espp/base_component: '>=1.0' |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.