-
Notifications
You must be signed in to change notification settings - Fork 29
feat(mcp266): CANopen dual-channel MCP266 motor controller component #758
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
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 canopen, this component's detail/ lives INSIDE include/ | ||
| # (include/detail/mcp266_core.hpp), so registering "include" alone makes | ||
| # `#include "detail/mcp266_core.hpp"` resolve for consumers. | ||
| idf_component_register( | ||
| INCLUDE_DIRS "include" | ||
| REQUIRES base_component canopen | ||
| ) |
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,63 @@ | ||
| # MCP266 CANopen Motor Controller Component | ||
|
|
||
| [](https://components.espressif.com/components/espp/mcp266) | ||
|
|
||
| The `Mcp266` class is a dual-channel controller for a Basicmicro **MCP266** | ||
| (RoboClaw family) brushed-DC motor driver over **CANopen**. It is layered on | ||
| `espp::CanopenClient` (like `espp::Ds402Drive`), so it is transport-agnostic: | ||
| the application owns the CAN transport, feeds received frames to the client's | ||
| `process_frame()`, and the client's node id selects the MCP266. | ||
|
|
||
| Both motor channels (`M1`, `M2`) are driven symmetrically. M2's CiA 402 | ||
| objects mirror M1's at `+0x800`, handled through `Ds402Drive`'s object offset. | ||
|
|
||
| ## What works, and what does not | ||
|
|
||
| **Position control** uses the standard CiA 402 profile position mode | ||
| (`move_to_position`) and is the supported, validated capability. It needs the | ||
| position loop configured first (`configure_position_loop`). | ||
|
|
||
| **Velocity / duty control is not functional** on the MCP266 firmware tested. | ||
| The standard target objects and the manufacturer speed/duty command mirror are | ||
| both accepted by the drive but leave the velocity generator idle even with the | ||
| drive in Operation Enabled. Supported-drive-modes (`0x6502`) advertises only | ||
| the cyclic-sync modes, so velocity likely requires csv mode with cyclic | ||
| SYNC/PDO updates, which is undocumented for this device. `drive_speed` / | ||
| `drive_duty` are implemented but are currently a no-op for motion; use position | ||
| mode. | ||
|
|
||
| ## Device specifics | ||
|
|
||
| The MCP266's control-loop parameters are **not** standard CiA 402 objects. The | ||
| MCP mirrors its packet-serial command set into the manufacturer region of the | ||
| object dictionary at index `0x2000 + command number` (see | ||
| `include/detail/mcp266_core.hpp`, which is host-buildable and unit-tested). | ||
| This component uses that to: | ||
|
|
||
| * configure the position PID (commands 61-64), | ||
| * issue the manufacturer speed/duty commands (32/33, 35/36), and | ||
| * read telemetry: main battery (24) and temperature (82). | ||
|
|
||
| Two device quirks are handled by `configure_position_loop()`, which must be | ||
| called once per boot (the MCP reverts to its EEPROM configuration on power-up): | ||
|
|
||
| * The position PID's `MinPos`/`MaxPos` clamp defaults to `[0, 0]`, which forces | ||
| every position target to zero — it is widened here. | ||
| * The setter (commands 61/62) uses field order `D, P, I` while the readback | ||
| (63/64) uses `P, I, D`, so a naive read-modify-write of the record would move | ||
| `P` into the `D` slot and zero `P`. The correct field shuffle (and seeding a | ||
| non-zero `P` when the record has none) is done here. | ||
|
|
||
| ## Example | ||
|
|
||
| The [example](./example) brings up an `espp::Twai` transport and an | ||
| `espp::CanopenClient`, NMT-starts the node, reads telemetry, configures the M1 | ||
| position loop, and runs a small profile-position sequence. | ||
|
|
||
| ## Related components | ||
|
|
||
| * `espp/canopen` — the CANopen client and the `Ds402Drive` CiA 402 helper this | ||
| component builds on. | ||
| * `espp/basicmicro` — the Basicmicro **packet serial** protocol driver (a | ||
| different transport to the same controller family), including velocity and | ||
| position PID configuration over UART. |
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,22 @@ | ||
| # The following lines of boilerplate have to be in your project's CMakeLists | ||
| # in this exact order for cmake to work correctly | ||
| cmake_minimum_required(VERSION 3.20) | ||
|
|
||
| set(ENV{IDF_COMPONENT_MANAGER} "0") | ||
| include($ENV{IDF_PATH}/tools/cmake/project.cmake) | ||
|
|
||
| # add the component directories that we want to use | ||
| set(EXTRA_COMPONENT_DIRS | ||
| "../../../components/" | ||
| ) | ||
|
|
||
| set( | ||
| COMPONENTS | ||
| "main esptool_py mcp266 canopen twai" | ||
| CACHE STRING | ||
| "List of components to include" | ||
| ) | ||
|
|
||
| project(mcp266_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,41 @@ | ||
| # MCP266 CANopen Example | ||
|
|
||
| This example demonstrates how to use the `espp::Mcp266` component to drive a | ||
| Basicmicro MCP266 (RoboClaw-family) motor controller over **CANopen**. It: | ||
|
|
||
| 1. brings up an `espp::Twai` transport and an `espp::CanopenClient`, wiring the | ||
| received frames into the client, | ||
| 2. NMT-starts the node and clears any latched CiA 402 faults, | ||
| 3. reads the main battery voltage and board temperature, | ||
| 4. configures the M1 position loop (widening the position clamp and seeding a | ||
| non-zero P gain — required once per boot), and | ||
| 5. runs a small profile-position sequence on M1, reporting arrival at each | ||
| target. | ||
|
|
||
| See the [component README](../README.md) for the device specifics (the | ||
| manufacturer command-object mirror, the position-PID field-order quirk, and the | ||
| velocity-control limitation). | ||
|
|
||
| ## Requirements | ||
|
|
||
| - An ESP target with a TWAI (CAN) controller. | ||
| - A 3.3 V CAN transceiver (e.g. SN65HVD230) wired to the configured TX/RX | ||
| GPIOs, on a properly terminated bus. | ||
| - A Basicmicro MCP266 configured for CANopen (CAN pins, bit rate, and node id | ||
| set in Basicmicro Motion Studio), with an encoder and a tuned velocity PID. | ||
|
|
||
| ## Hardware | ||
|
|
||
| Edit the `tx_gpio`, `rx_gpio`, `baudrate`, and `node_id` in | ||
| `main/mcp266_example.cpp` to match your board and MCP266 configuration. | ||
|
|
||
| ## Build | ||
|
|
||
| Build the project and flash it to the board, then run monitor to view the | ||
| serial output: | ||
|
|
||
| ```sh | ||
| idf.py -p PORT flash monitor | ||
| ``` | ||
|
|
||
| Replace PORT with the serial port of your board. |
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,2 @@ | ||
| idf_component_register(SRC_DIRS "." | ||
| INCLUDE_DIRS ".") |
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,136 @@ | ||
| #include <chrono> | ||
| #include <cstdlib> | ||
| #include <thread> | ||
|
|
||
| #include "canopen_client.hpp" | ||
| #include "mcp266.hpp" | ||
| #include "twai.hpp" | ||
|
|
||
| using namespace std::chrono_literals; | ||
|
|
||
| extern "C" void app_main(void) { | ||
| static espp::Logger logger({.tag = "MCP266 Example", .level = espp::Logger::Verbosity::INFO}); | ||
| logger.info("Starting MCP266 CANopen example!"); | ||
|
|
||
| //! [mcp266 example] | ||
| // The CANopen node id configured on the MCP266 (Motion Studio -> CAN | ||
| // settings). Change to match your device. | ||
| static constexpr uint8_t node_id = 10; | ||
|
|
||
| // Forward-declared handle so the Twai on_receive callback (registered at | ||
| // Twai construction) can feed frames to the client constructed below. | ||
| static espp::CanopenClient *client_ptr = nullptr; | ||
|
|
||
| // Bring up the TWAI (CAN 2.0) peripheral. Talking to a real MCP266 requires | ||
| // Mode::NORMAL with a 3.3 V CAN transceiver on the tx/rx GPIOs, a properly | ||
| // terminated bus, and a matching baudrate (set in Motion Studio). | ||
| // NOTE: twai / client / mcp are function-local statics: the Twai receive | ||
| // task and the client's send lambda reference them, and app_main() has | ||
| // early-return paths, so static storage keeps them alive for every callback. | ||
| static espp::Twai twai({ | ||
| .tx_gpio = 17, // change to match your board / transceiver | ||
| .rx_gpio = 16, | ||
| .baudrate = 1000000, | ||
| .mode = espp::Twai::Mode::NORMAL, | ||
| .tx_queue_depth = 10, | ||
| .on_receive = | ||
| [](const espp::Twai::Message &msg) { | ||
| if (client_ptr) { | ||
| client_ptr->process_frame(espp::CanopenClient::CanFrame{ | ||
| .id = msg.id, | ||
| .extended = msg.extended, | ||
| .rtr = msg.rtr, | ||
| .dlc = msg.dlc, | ||
| .data = msg.data, | ||
| }); | ||
| } | ||
| }, | ||
| .log_level = espp::Logger::Verbosity::WARN, | ||
| }); | ||
|
|
||
| static espp::CanopenClient client({ | ||
| .node_id = node_id, | ||
| // captureless: twai has static storage duration and is referenced | ||
| // directly (capturing a static is ill-formed under -Werror) | ||
| .send = | ||
| [](const espp::CanopenClient::CanFrame &frame) { | ||
| espp::Twai::Message msg{ | ||
| .id = frame.id, | ||
| .extended = frame.extended, | ||
| .rtr = frame.rtr, | ||
| .dlc = frame.dlc, | ||
| .data = frame.data, | ||
| }; | ||
| std::error_code tx_ec; | ||
| return twai.transmit(msg, tx_ec); | ||
| }, | ||
| .sdo_timeout = 500ms, | ||
| .log_level = espp::Logger::Verbosity::WARN, | ||
| }); | ||
| client_ptr = &client; | ||
|
|
||
| static espp::Mcp266 mcp(client, {.log_level = espp::Logger::Verbosity::INFO}); | ||
|
|
||
| std::error_code ec; | ||
| if (!twai.initialize(ec)) { | ||
| logger.error("Failed to initialize TWAI: {}", ec.message()); | ||
| return; | ||
| } | ||
| // NMT-start the node and clear any latched faults on both axes. | ||
| if (!mcp.start(ec)) { | ||
| logger.error("Failed to start MCP266: {} -- is the node on the bus?", ec.message()); | ||
| return; | ||
| } | ||
|
|
||
| // Telemetry sanity check. | ||
| float volts = 0.0f, temp_c = 0.0f; | ||
| if (mcp.read_main_battery_voltage(volts, ec)) { | ||
| logger.info("Main battery: {:.1f} V", volts); | ||
| } | ||
| if (mcp.read_temperature(temp_c, ec)) { | ||
| logger.info("Board temperature: {:.1f} C", temp_c); | ||
| } | ||
|
|
||
| using Axis = espp::Mcp266::Axis; | ||
|
|
||
| // One-time per-boot position-loop setup on M1: widen the MinPos/MaxPos clamp | ||
| // (factory [0, 0] forces every target to zero) and ensure a non-zero | ||
| // position P gain. The MCP reverts to EEPROM on power-up, so this must run | ||
| // every boot before commanding moves. Clear any latched e-stop first. | ||
| mcp.reset_estop(ec); | ||
| if (!mcp.configure_position_loop(Axis::M1, -2'000'000'000, 2'000'000'000, ec)) { | ||
| logger.error("Failed to configure M1 position loop: {}", ec.message()); | ||
| return; | ||
| } | ||
| mcp.set_position_limits(Axis::M1, -20'000, 20'000, ec); | ||
|
|
||
| // Run a small profile-position sequence and report arrival. | ||
| static constexpr int32_t targets[] = {10'000, -10'000, 0}; | ||
| static constexpr uint32_t profile_velocity = 500; // counts/s | ||
| static constexpr uint32_t profile_accel = 500; // counts/s^2 | ||
| static constexpr uint32_t profile_decel = 500; // counts/s^2 | ||
| static constexpr int32_t tolerance = 100; // counts | ||
| for (int32_t target : targets) { | ||
| logger.info("Moving M1 to {}", target); | ||
| if (!mcp.move_to_position(Axis::M1, target, profile_velocity, profile_accel, profile_decel, | ||
| ec)) { | ||
| logger.error("Move command rejected: {}", ec.message()); | ||
| continue; | ||
| } | ||
| const auto deadline = std::chrono::steady_clock::now() + 30s; | ||
| while (std::chrono::steady_clock::now() < deadline) { | ||
| std::this_thread::sleep_for(250ms); | ||
| int32_t position = 0; | ||
| if (mcp.read_encoder(Axis::M1, position, ec) && std::abs(position - target) <= tolerance) { | ||
| logger.info(" reached {} (position={})", target, position); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| //! [mcp266 example] | ||
|
|
||
| logger.info("MCP266 example complete!"); | ||
| while (true) { | ||
| std::this_thread::sleep_for(1s); | ||
| } | ||
| } | ||
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: "Dual-channel Basicmicro MCP266 (RoboClaw family) motor controller over CANopen (CiA 402 profile position)" | ||
| url: "https://github.com/esp-cpp/espp/tree/main/components/mcp266" | ||
| repository: "https://github.com/esp-cpp/espp.git" | ||
| maintainers: | ||
| - William Emfinger <waemfinger@gmail.com> | ||
| documentation: "https://esp-cpp.github.io/espp/motorcontrol/mcp266.html" | ||
| examples: | ||
| - path: example | ||
| tags: | ||
| - cpp | ||
| - Component | ||
| - Basicmicro | ||
| - MCP266 | ||
| - RoboClaw | ||
| - CANopen | ||
| - CiA402 | ||
| - DS402 | ||
| - Motor | ||
| dependencies: | ||
| idf: | ||
| version: '>=5.0' | ||
| espp/base_component: '>=1.0' | ||
| espp/canopen: '>=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.