Basicmicro (MCP / RoboClaw) Motor Controller Component
Overview
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
espp::detail inside include/detail/basicmicro_core.hpp, a
host-buildable core that depends only on the C++20 standard library and is
unit-tested off-target.
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 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), battery voltages (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_codeThread-safe: each transaction (request write + ACK/reply read) is serialized by an internal mutex
Protocol
Per the MCP Series User Manual (section 2.2), write commands send
[Address, Command, Data..., CRC16] and are acknowledged with a single
0xFF byte, while read commands send [Address, Command] (no CRC) and
reply with the data followed by a CRC16 seeded with the sent address and
command bytes. All multi-byte values are big-endian and the CRC is
CRC-16/XMODEM (poly 0x1021, init 0). A 10 ms inter-byte gap clears the
controller’s packet buffer, so the configured receive timeout (>= 10 ms)
doubles as the error-recovery mechanism.
Basic Usage
espp::Basicmicro mcp({
.address = 0x80,
.write = [](std::span<const uint8_t> data) { /* UART write */ return true; },
.read = [](std::span<uint8_t> data, std::chrono::milliseconds timeout) -> size_t {
/* UART read with timeout, return bytes read */ return 0;
},
});
std::error_code ec;
std::string version;
mcp.read_firmware_version(version, ec);
mcp.drive_m1_duty(4096, ec); // ~12.5% duty
uint32_t count; uint8_t status;
mcp.read_encoder_m1(count, status, ec);
mcp.drive_m1_duty(0, ec);
API Reference
Header File
Classes
-
class Basicmicro : public espp::BaseComponent
Driver for Basicmicro (MCP236 / MCP266 and RoboClaw-family) brushed DC motor controllers speaking the PACKET SERIAL protocol, typically over UART.
The component is transport-agnostic: it performs no I/O itself and instead calls the user-provided write / read functions for each transaction, so it works over a UART driver, USB CDC, RS-232 adapter, etc. 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.
Protocol summary (MCP Series User Manual section 2.2):
Write commands send [Address, Command, Data…, CRC16] and the controller replies with a single 0xFF ACK byte only when the packet was valid.
Read commands send [Address, Command] (no CRC) and the controller replies with the data followed by a CRC16 seeded with the sent address + command.
Error recovery: a >=10 ms gap between bytes makes the controller discard any partial packet, so the configured receive timeout (>=10 ms, default 20 ms) doubles as the recovery mechanism — after a timed-out transaction the controller’s packet buffer has already cleared itself and the next packet starts fresh.
All methods report errors via a std::error_code out-parameter (no exceptions) and return true on success.
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");
Note
Thread safety: every public method runs one complete transaction (write the request, then read the ACK / reply) while holding an internal mutex, so concurrent calls from multiple tasks serialize cleanly and replies cannot interleave. The user-provided read / write functions ARE called with that mutex held — this is intentional, since the transaction is precisely the I/O — so they must not call back into this component.
Public Types
-
using Command = detail::BasicmicroCommand
Command bytes (verified against the MCP Series User Manual).
-
using Status = detail::BasicmicroStatus
Status bit masks returned by read_status() (manual command 90).
-
typedef std::function<bool(std::span<const uint8_t> data)> write_fn
Function used to transmit a complete packet to the controller. Should return true when all bytes were written.
-
typedef std::function<size_t(std::span<uint8_t> data, std::chrono::milliseconds timeout)> read_fn
Function used to receive reply bytes from the controller. Should block until at least one byte is available or the timeout expires, and return the number of bytes actually read into the span (0 on timeout).
Public Functions
-
inline explicit Basicmicro(const Config &config)
Create a Basicmicro driver.
Note
The documented contracts are enforced here: a timeout below the protocol’s 10 ms packet-clear window is clamped up to 10 ms, and an address outside 0x80-0x87 is clamped into the valid range (both with a warning) rather than silently violating the wire protocol.
- Parameters:
config – Configuration parameters.
-
inline bool drive_m1_duty(int16_t duty, std::error_code &ec)
Drive motor 1 with a signed duty cycle (command 32).
- Parameters:
duty – Signed duty, -32767 to +32767 (= -100% to +100%).
ec – Set on failure.
- Returns:
True on success.
-
inline bool drive_m2_duty(int16_t duty, std::error_code &ec)
Drive motor 2 with a signed duty cycle (command 33).
- Parameters:
duty – Signed duty, -32767 to +32767 (= -100% to +100%).
ec – Set on failure.
- Returns:
True on success.
-
inline bool drive_duty(int16_t duty_m1, int16_t duty_m2, std::error_code &ec)
Drive both motors with signed duty cycles (command 34).
- Parameters:
duty_m1 – Signed duty for motor 1, -32767 to +32767.
duty_m2 – Signed duty for motor 2, -32767 to +32767.
ec – Set on failure.
- Returns:
True on success.
-
inline bool drive_m1_speed(int32_t qpps, std::error_code &ec)
Drive motor 1 at a signed speed in quadrature pulses per second (command 35). Requires an encoder and tuned velocity PID.
- Parameters:
qpps – Signed speed in quad pulses per second.
ec – Set on failure.
- Returns:
True on success.
-
inline bool drive_m2_speed(int32_t qpps, std::error_code &ec)
Drive motor 2 at a signed speed in quadrature pulses per second (command 36). Requires an encoder and tuned velocity PID.
- Parameters:
qpps – Signed speed in quad pulses per second.
ec – Set on failure.
- Returns:
True on success.
-
inline bool drive_speed(int32_t qpps_m1, int32_t qpps_m2, std::error_code &ec)
Drive both motors at signed speeds in quadrature pulses per second (command 37).
- Parameters:
qpps_m1 – Signed speed for motor 1 in quad pulses per second.
qpps_m2 – Signed speed for motor 2 in quad pulses per second.
ec – Set on failure.
- Returns:
True on success.
-
inline bool drive_m1_speed_accel(uint32_t accel, int32_t qpps, std::error_code &ec)
Drive motor 1 at a signed speed with an acceleration ramp (command 38).
- Parameters:
accel – Acceleration in qpps per second (unsigned).
qpps – Signed target speed in quad pulses per second.
ec – Set on failure.
- Returns:
True on success.
-
inline bool drive_m2_speed_accel(uint32_t accel, int32_t qpps, std::error_code &ec)
Drive motor 2 at a signed speed with an acceleration ramp (command 39).
- Parameters:
accel – Acceleration in qpps per second (unsigned).
qpps – Signed target speed in quad pulses per second.
ec – Set on failure.
- Returns:
True on success.
-
inline bool drive_speed_accel(uint32_t accel, int32_t qpps_m1, int32_t qpps_m2, std::error_code &ec)
Drive both motors at signed speeds with a shared acceleration ramp (command 40).
- Parameters:
accel – Acceleration in qpps per second (unsigned, applies to both).
qpps_m1 – Signed target speed for motor 1.
qpps_m2 – Signed target speed for motor 2.
ec – Set on failure.
- Returns:
True on success.
-
inline bool buffered_drive_m1_speed_distance(int32_t qpps, uint32_t distance, bool immediate, std::error_code &ec)
Buffered drive of motor 1 with signed speed and distance (command 41).
- Parameters:
qpps – Signed speed in quad pulses per second.
distance – Distance in quad pulses (unsigned).
immediate – If true, stop the currently-executing command, flush the buffer and run this command now; if false, queue it (up to 64 commands per motor buffer).
ec – Set on failure.
- Returns:
True on success.
-
inline bool buffered_drive_m2_speed_distance(int32_t qpps, uint32_t distance, bool immediate, std::error_code &ec)
Buffered drive of motor 2 with signed speed and distance (command 42).
- Parameters:
qpps – Signed speed in quad pulses per second.
distance – Distance in quad pulses (unsigned).
immediate – If true, stop the currently-executing command, flush the buffer and run this command now; if false, queue it.
ec – Set on failure.
- Returns:
True on success.
-
inline bool buffered_drive_speed_distance(int32_t qpps_m1, uint32_t distance_m1, int32_t qpps_m2, uint32_t distance_m2, bool immediate, std::error_code &ec)
Buffered drive of both motors with signed speeds and distances (command 43).
- Parameters:
qpps_m1 – Signed speed for motor 1 in quad pulses per second.
distance_m1 – Distance for motor 1 in quad pulses (unsigned).
qpps_m2 – Signed speed for motor 2 in quad pulses per second.
distance_m2 – Distance for motor 2 in quad pulses (unsigned).
immediate – If true, stop the currently-executing command, flush the buffer and run this command now; if false, queue it.
ec – Set on failure.
- Returns:
True on success.
-
inline bool buffered_drive_m1_speed_accel_distance(uint32_t accel, int32_t qpps, uint32_t distance, bool immediate, std::error_code &ec)
Buffered drive of motor 1 with acceleration, signed speed and distance (command 44).
- Parameters:
accel – Acceleration in qpps per second (unsigned).
qpps – Signed speed in quad pulses per second.
distance – Distance in quad pulses (unsigned).
immediate – If true, stop the currently-executing command, flush the buffer and run this command now; if false, queue it.
ec – Set on failure.
- Returns:
True on success.
-
inline bool buffered_drive_m2_speed_accel_distance(uint32_t accel, int32_t qpps, uint32_t distance, bool immediate, std::error_code &ec)
Buffered drive of motor 2 with acceleration, signed speed and distance (command 45).
- Parameters:
accel – Acceleration in qpps per second (unsigned).
qpps – Signed speed in quad pulses per second.
distance – Distance in quad pulses (unsigned).
immediate – If true, stop the currently-executing command, flush the buffer and run this command now; if false, queue it.
ec – Set on failure.
- Returns:
True on success.
-
inline bool buffered_drive_speed_accel_distance(uint32_t accel, int32_t qpps_m1, uint32_t distance_m1, int32_t qpps_m2, uint32_t distance_m2, bool immediate, std::error_code &ec)
Buffered drive of both motors with a shared acceleration, signed speeds and distances (command 46).
- Parameters:
accel – Acceleration in qpps per second (unsigned, applies to both).
qpps_m1 – Signed speed for motor 1 in quad pulses per second.
distance_m1 – Distance for motor 1 in quad pulses (unsigned).
qpps_m2 – Signed speed for motor 2 in quad pulses per second.
distance_m2 – Distance for motor 2 in quad pulses (unsigned).
immediate – If true, stop the currently-executing command, flush the buffer and run this command now; if false, queue it.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_buffer_lengths(uint8_t &buffer_m1, uint8_t &buffer_m2, std::error_code &ec)
Read how many buffered commands are waiting per motor (command 47).
- Parameters:
buffer_m1 – Motor 1 buffer state: 0x80 = buffer empty / last command finished, 0 = last command is executing, 1-0x3F = commands waiting.
buffer_m2 – Motor 2 buffer state (same encoding).
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_encoder_m1(uint32_t &count, uint8_t &status, std::error_code &ec)
Read the motor 1 encoder count / position (command 16).
- Parameters:
count – Encoder count (quadrature: full 32-bit range; absolute: 0-4095).
status – Status bits: bit0 = underflow occurred (cleared on read), bit1 = direction (0 forward, 1 backward), bit2 = overflow occurred (cleared on read).
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_encoder_m2(uint32_t &count, uint8_t &status, std::error_code &ec)
Read the motor 2 encoder count / position (command 17).
- Parameters:
count – Encoder count (quadrature: full 32-bit range; absolute: 0-4095).
status – Status bits: bit0 = underflow occurred (cleared on read), bit1 = direction (0 forward, 1 backward), bit2 = overflow occurred (cleared on read).
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_encoders(uint32_t &count_m1, uint32_t &count_m2, std::error_code &ec)
Read both encoder counters in one transaction (command 78).
- Parameters:
count_m1 – Motor 1 encoder count.
count_m2 – Motor 2 encoder count.
ec – Set on failure.
- Returns:
True on success.
-
inline bool reset_encoders(std::error_code &ec)
Reset both quadrature encoder counters to zero (command 20).
- Parameters:
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_encoder_speed_m1(int32_t &qpps, uint8_t &direction, std::error_code &ec)
Read the motor 1 encoder speed in pulses per second (command 18).
- Parameters:
qpps – Speed in pulses per second (as reported by the controller).
direction – 0 = forward, 1 = backward.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_encoder_speed_m2(int32_t &qpps, uint8_t &direction, std::error_code &ec)
Read the motor 2 encoder speed in pulses per second (command 19).
- Parameters:
qpps – Speed in pulses per second (as reported by the controller).
direction – 0 = forward, 1 = backward.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_ispeeds(int32_t &qpps_m1, int32_t &qpps_m2, std::error_code &ec)
Read both instantaneous speeds (counts per second over the last 1/300th of a second) in one transaction (command 79).
- Parameters:
qpps_m1 – Motor 1 instantaneous speed.
qpps_m2 – Motor 2 instantaneous speed.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_encoder_modes(uint8_t &mode_m1, uint8_t &mode_m2, std::error_code &ec)
Read the encoder modes / pin assignments for both motors (command 91).
- Parameters:
mode_m1 – Motor 1 encoder mode.
mode_m2 – Motor 2 encoder mode.
ec – Set on failure.
- Returns:
True on success.
-
inline bool set_velocity_pid_m1(float p, float i, float d, uint32_t qpps, std::error_code &ec)
Set the motor 1 velocity PID constants and QPPS (command 28).
Gains are converted to the controller’s 16.16 fixed-point representation (value * 65536); the controller defaults correspond to P=1.0, I=0.5, D=0.25, QPPS=44000.
- Parameters:
p – Proportional gain.
i – Integral gain.
d – Derivative gain.
qpps – Encoder speed (quad pulses per second) at 100% motor power.
ec – Set on failure.
- Returns:
True on success.
-
inline bool set_velocity_pid_m2(float p, float i, float d, uint32_t qpps, std::error_code &ec)
Set the motor 2 velocity PID constants and QPPS (command 29). See set_velocity_pid_m1() for the fixed-point conversion.
- Parameters:
p – Proportional gain.
i – Integral gain.
d – Derivative gain.
qpps – Encoder speed (quad pulses per second) at 100% motor power.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_velocity_pid_m1(float &p, float &i, float &d, uint32_t &qpps, std::error_code &ec)
Read the motor 1 velocity PID constants and QPPS (command 55). Fixed-point values are converted back to floats (divide by 65536).
- Parameters:
p – Proportional gain.
i – Integral gain.
d – Derivative gain.
qpps – Encoder speed (quad pulses per second) at 100% motor power.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_velocity_pid_m2(float &p, float &i, float &d, uint32_t &qpps, std::error_code &ec)
Read the motor 2 velocity PID constants and QPPS (command 56). Fixed-point values are converted back to floats (divide by 65536).
- Parameters:
p – Proportional gain.
i – Integral gain.
d – Derivative gain.
qpps – Encoder speed (quad pulses per second) at 100% motor power.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_firmware_version(std::string &version, std::error_code &ec)
Read the firmware version string (command 21).
The controller returns up to 48 bytes terminated by a line feed and a NUL character (e.g. “MCP266 2x60A v1.0.0”); the returned string has the terminators stripped.
- Parameters:
version – The firmware / product version string.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_main_battery_voltage(float &volts, std::error_code &ec)
Read the main battery (B+/B-) voltage (command 24).
- Parameters:
volts – Voltage in volts (the controller reports tenths of a volt).
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_logic_battery_voltage(float &volts, std::error_code &ec)
Read the logic battery (LB+/LB-) voltage (command 25).
- Parameters:
volts – Voltage in volts (the controller reports tenths of a volt).
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_currents(float &s_m1, float &s_m2, std::error_code &ec)
Read the motor currents (command 49).
- Parameters:
amps_m1 – Motor 1 current in amps (the controller reports 10 mA units, i.e. value / 100).
amps_m2 – Motor 2 current in amps.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_motor_pwms(float &percent_m1, float &percent_m2, std::error_code &ec)
Read the motor PWM output values (command 48).
- Parameters:
percent_m1 – Motor 1 duty cycle in percent (-100 to +100; the controller reports +/-32767, i.e. value / 327.67).
percent_m2 – Motor 2 duty cycle in percent.
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_temperature(float °rees, std::error_code &ec)
Read the board temperature (command 82).
- Parameters:
degrees – Temperature in degrees (the controller reports tenths of a degree).
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_temperature2(float °rees, std::error_code &ec)
Read the second board temperature (command 83, only on supported units).
- Parameters:
degrees – Temperature in degrees (the controller reports tenths of a degree).
ec – Set on failure.
- Returns:
True on success.
-
inline bool read_status(uint32_t &status, std::error_code &ec)
Read the unit status bit mask (command 90). See Basicmicro::Status for the bit definitions (the manual documents the low 16 bits).
Note
The manual leaves the field width unstated, but current MCP firmware returns a 32-bit status (Basicmicro’s official Arduino library reads it with Read4). This reads 4 bytes first; if that transaction fails (older firmware replying 16-bit makes the reply end mid-read), the device’s 10 ms packet-clear gap has already elapsed via the timeout, so a single 16-bit retry is performed for legacy-firmware units.
- Parameters:
status – The status bit mask (0 = normal).
ec – Set on failure.
- Returns:
True on success.
-
inline bool write_settings_to_eeprom(std::error_code &ec)
Write all settings to non-volatile memory (command 94) so they are reloaded on power-up.
Note
Per the manual this request is sent WITHOUT a CRC ([Address, 94]) but is still acknowledged with 0xFF.
- Parameters:
ec – Set on failure.
- Returns:
True on success.
-
inline bool e_stop_reset(std::error_code &ec)
Reset an E-Stop condition (command 200). Does nothing unless the E-Stop reset has been unlocked (manual command 201).
- Parameters:
ec – Set on failure.
- Returns:
True on success.
-
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
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- 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
See also
See also
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
See also
See also
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
See also
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
-
struct Config
Configuration for the Basicmicro driver.
Public Members
-
uint8_t address = {0x80}
Packet serial address of the controller (0x80 - 0x87).
-
std::chrono::milliseconds timeout{20}
Total receive timeout per transaction. Must be >= 10 ms: the controller discards a partial packet after a 10 ms inter-byte gap (manual section 2.2.4), so waiting at least that long guarantees its packet buffer has cleared before the next transaction.
-
uint8_t address = {0x80}