Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ jobs:
target: esp32s3
- path: 'components/aw9523/example'
target: esp32
- path: 'components/basicmicro/example'
target: esp32
- path: 'components/bdc_driver/example'
target: esp32s3
- path: 'components/binary-log/example'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/upload_components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
components/aw9523
components/base_component
components/base_peripheral
components/basicmicro
components/bdc_driver
components/binary-log
components/bldc_current_sense
Expand Down
7 changes: 7 additions & 0 deletions components/basicmicro/CMakeLists.txt
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
)
78 changes: 78 additions & 0 deletions components/basicmicro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Basicmicro (MCP / RoboClaw) Motor Controller Component

[![Badge](https://components.espressif.com/components/espp/basicmicro/badge.svg)](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
```
19 changes: 19 additions & 0 deletions components/basicmicro/example/CMakeLists.txt
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)
55 changes: 55 additions & 0 deletions components/basicmicro/example/README.md
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
```
4 changes: 4 additions & 0 deletions components/basicmicro/example/main/CMakeLists.txt
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 components/basicmicro/example/main/basicmicro_example.cpp
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);
},
Comment thread
finger563 marked this conversation as resolved.
// 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);
}
}
4 changes: 4 additions & 0 deletions components/basicmicro/example/sdkconfig.defaults
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
25 changes: 25 additions & 0 deletions components/basicmicro/idf_component.yml
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'
Loading
Loading