Skip to content

Commit 163a043

Browse files
ApehaengerClemensElflein
authored andcommitted
Add YFR4 ESC driver
1 parent 200df92 commit 163a043

File tree

10 files changed

+598
-1
lines changed

10 files changed

+598
-1
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE
7676
src/drivers/motor/vesc/buffer.cpp
7777
src/drivers/motor/vesc/crc.cpp
7878
src/drivers/motor/vesc/VescDriver.cpp
79+
# YFR4-ESC driver
80+
src/drivers/motor/yfr4esc/YFR4escDriver.cpp
7981
# PWM motor driver
8082
src/drivers/motor/pwm/pwm_motor_driver.cpp
8183
# GPS driver

robots/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ endif ()
88

99
# find the platform sources and add them
1010
file(GLOB_RECURSE PLATFORM_SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/*")
11+
12+
# Expose a preprocessor guard like ROBOT_PLATFORM_YardForce_V4=1 for #ifdef use
13+
add_compile_definitions(ROBOT_PLATFORM_${ROBOT_PLATFORM}=1)

robots/include/robot.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define ROBOT_HPP
33

44
#include <drivers/motor/vesc/VescDriver.h>
5+
#include <drivers/motor/yfr4esc/YFR4escDriver.h>
56
#include <hal.h>
67

78
#include <debug/debug_tcp_interface.hpp>
@@ -48,7 +49,14 @@ class MowerRobot : public Robot {
4849

4950
xbot::driver::motor::VescDriver left_motor_driver_{};
5051
xbot::driver::motor::VescDriver right_motor_driver_{};
51-
xbot::driver::motor::VescDriver mower_motor_driver_{};
52+
53+
// Select mower motor ESC by platform: YFR4 on YardForce_V4, VESC otherwise
54+
#if defined(ROBOT_PLATFORM_YardForce_V4)
55+
using MowerEscDriver = xbot::driver::motor::YFR4escDriver;
56+
#else
57+
using MowerEscDriver = xbot::driver::motor::VescDriver;
58+
#endif
59+
MowerEscDriver mower_motor_driver_{};
5260

5361
DebugTCPInterface left_esc_driver_interface_{65102, &left_motor_driver_};
5462
DebugTCPInterface mower_esc_driver_interface_{65103, &mower_motor_driver_};

robots/src/yardforce_v4_robot.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <services.hpp>
44

55
void YardForce_V4Robot::InitPlatform() {
6+
// Initialize default motors (VESC for left/right + mower)
67
InitMotors();
78
charger_.setI2C(&I2CD1);
89
power_service.SetDriver(&charger_);
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
//
2+
// Created by GitHub Copilot on 16.08.25.
3+
//
4+
5+
#include "YFR4escDriver.h"
6+
7+
#include <cstdio>
8+
#include <cstring>
9+
10+
#include "cobs.h"
11+
#include "crc16_hw.hpp"
12+
#define LOG_TAG_STR "YFR4esc"
13+
#include "ulog_rate_limit.hpp"
14+
15+
namespace xbot::driver::motor {
16+
17+
using namespace yfr4esc;
18+
19+
// Event when the RX DMA buffer wrapped (full) and was re-armed.
20+
static constexpr uint32_t EVT_RX_DMA_WRAP = 1;
21+
22+
bool YFR4escDriver::SetUART(UARTDriver *uart, uint32_t baudrate) {
23+
chDbgAssert(!IsStarted(), "Only set UART when the driver is stopped");
24+
chDbgAssert(uart != nullptr, "need to provide a driver");
25+
if (IsStarted()) return false;
26+
27+
uart_ = uart;
28+
uart_config_.speed = baudrate;
29+
uart_config_.context = this;
30+
31+
return true;
32+
}
33+
34+
void YFR4escDriver::ProcessDecodedPacket(uint8_t *packet, size_t len) {
35+
// Packages have to be at least 1 byte of type + 1 byte of data + 2 bytes of CRC
36+
if (len < 4) {
37+
ULOGT_EVERY_MS(WARNING, 500, "Decoded packet too short (%zu bytes). Dropping!", len);
38+
return;
39+
}
40+
41+
// Check CRC (on-wire is little-endian; STM32 is little-endian too)
42+
uint16_t crc_rx;
43+
memcpy(&crc_rx, &packet[len - 2], sizeof(crc_rx)); // memcpy avoids potential unaligned access
44+
uint16_t crc_calc = yfr4esc_crc::crc16_ccitt_false(packet, len - 2);
45+
if (crc_rx != crc_calc) {
46+
ULOGT_EVERY_MS(WARNING, 500, "CRC mismatch (rx=0x%04X calc=0x%04X len=%u). Dropping!", crc_rx, crc_calc,
47+
(unsigned)len);
48+
return;
49+
}
50+
51+
uint8_t msg_type = packet[0];
52+
switch (msg_type) {
53+
case MessageType::STATUS: {
54+
if (len != sizeof(StatusPacket)) {
55+
ULOGT_EVERY_MS(WARNING, 500, "Status packet length mismatch (%u != %zu). Dropping!", (unsigned)len,
56+
sizeof(StatusPacket));
57+
break;
58+
}
59+
StatusPacket sp{};
60+
memcpy(&sp, packet, sizeof(StatusPacket));
61+
62+
latest_state_.fw_major = sp.fw_version_major;
63+
latest_state_.fw_minor = sp.fw_version_minor;
64+
latest_state_.temperature_pcb = static_cast<float>(sp.temperature_pcb);
65+
latest_state_.current_input = static_cast<float>(sp.current_input);
66+
latest_state_.duty_cycle = static_cast<float>(sp.duty_cycle);
67+
latest_state_.direction = sp.direction ? 1.0f : 0.0f;
68+
latest_state_.tacho = sp.tacho;
69+
latest_state_.tacho_absolute = sp.tacho_absolute;
70+
latest_state_.rpm = static_cast<float>(sp.rpm);
71+
latest_state_.status =
72+
sp.fault_code == 0 ? ESCState::ESCStatus::ESC_STATUS_OK : ESCState::ESCStatus::ESC_STATUS_ERROR;
73+
74+
if (sp.fault_code & (0b1 << FaultBit::UNINITIALIZED)) {
75+
SendSettings();
76+
}
77+
78+
/* Debugging
79+
ULOGT_EVERY_MS(
80+
INFO, 1000, "Status: fw=%u.%u temp=%.2fC I_in=%.3fA duty=%.3f dir=%u tacho=%u tacho_abs=%u rpm=%u fault=%d",
81+
latest_state_.fw_major, latest_state_.fw_minor, latest_state_.temperature_pcb, latest_state_.current_input,
82+
latest_state_.duty_cycle, (unsigned)latest_state_.direction, latest_state_.tacho,
83+
latest_state_.tacho_absolute, (unsigned)latest_state_.rpm, (int)sp.fault_code); */
84+
85+
NotifyCallback();
86+
break;
87+
}
88+
default: ULOGT_EVERY_MS(WARNING, 500, "Unknown msg type: 0x%02X len=%u. Dropping!", msg_type, (unsigned)len); break;
89+
}
90+
}
91+
92+
void YFR4escDriver::ProcessRxBytes(const volatile uint8_t *data, size_t len) {
93+
if (len == 0) return;
94+
if (IsRawMode()) {
95+
RawDataOutput(const_cast<uint8_t *>(data), len);
96+
return;
97+
}
98+
for (size_t i = 0; i < len; ++i) {
99+
uint8_t b = data[i];
100+
if (b == 0) { // COBS end marker
101+
if (cobs_rx_len_ > 0) {
102+
size_t dec = cobs_decode(cobs_rx_buffer_, cobs_rx_len_, cobs_decoded_);
103+
if (dec >= 3) {
104+
ProcessDecodedPacket(cobs_decoded_, dec);
105+
}
106+
cobs_rx_len_ = 0;
107+
}
108+
} else {
109+
if (cobs_rx_len_ < COBS_BUFFER_SIZE) {
110+
cobs_rx_buffer_[cobs_rx_len_++] = b;
111+
} else {
112+
cobs_rx_len_ = 0; // overflow, reset
113+
}
114+
}
115+
}
116+
}
117+
118+
void YFR4escDriver::SetDuty(float duty) {
119+
if (!IsStarted() || IsRawMode()) return;
120+
last_duty_ = duty;
121+
SendControl(duty);
122+
}
123+
124+
void YFR4escDriver::SendControl(float duty) {
125+
ControlPacket cp{.message_type = MessageType::CONTROL, .duty_cycle = static_cast<double>(duty), .crc = 0};
126+
const uint8_t *payload = reinterpret_cast<const uint8_t *>(&cp);
127+
cp.crc = yfr4esc_crc::crc16_ccitt_false(payload, sizeof(ControlPacket) - sizeof(cp.crc));
128+
129+
chMtxLock(&mutex_); // protect shared tx_buffer_ and send
130+
size_t len = cobs_encode(reinterpret_cast<const uint8_t *>(&cp), sizeof(cp), tx_buffer_);
131+
if (len + 1 > TX_BUFFER_SIZE) {
132+
ULOGT_EVERY_MS(WARNING, 1000, "TX control frame too large: len=%u", (unsigned)len);
133+
chMtxUnlock(&mutex_);
134+
return;
135+
}
136+
tx_buffer_[len++] = 0; // COBS end marker
137+
uartSendFullTimeout(uart_, &len, tx_buffer_, TIME_INFINITE);
138+
chMtxUnlock(&mutex_);
139+
}
140+
141+
void YFR4escDriver::SendSettings() {
142+
// Defaults. How get those set?
143+
SettingsPacket sp{.message_type = MessageType::SETTINGS,
144+
.motor_current_limit = 2.5f,
145+
.min_pcb_temp = 0.0f,
146+
.max_pcb_temp = 70.0f,
147+
.crc = 0};
148+
149+
// Compute CRC over message_type + data only (exclude crc), store as-is (LE on wire)
150+
const uint8_t *payload = reinterpret_cast<const uint8_t *>(&sp);
151+
sp.crc = yfr4esc_crc::crc16_ccitt_false(payload, sizeof(SettingsPacket) - sizeof(sp.crc));
152+
153+
chMtxLock(&mutex_); // protect shared tx_buffer_ and send
154+
size_t len = cobs_encode(reinterpret_cast<const uint8_t *>(&sp), sizeof(SettingsPacket), tx_buffer_);
155+
if (len + 1 > TX_BUFFER_SIZE) {
156+
ULOGT_EVERY_MS(WARNING, 1000, "TX settings frame too large: len=%u", (unsigned)len);
157+
chMtxUnlock(&mutex_);
158+
return;
159+
}
160+
tx_buffer_[len++] = 0; // COBS end marker
161+
uartSendFullTimeout(uart_, &len, tx_buffer_, TIME_INFINITE);
162+
chMtxUnlock(&mutex_);
163+
}
164+
165+
void YFR4escDriver::RawDataInput(uint8_t *data, size_t size) {
166+
if (!IsRawMode() || !IsStarted()) return;
167+
chMtxLock(&mutex_);
168+
size_t len = size > TX_BUFFER_SIZE ? TX_BUFFER_SIZE : size;
169+
memcpy(tx_buffer_, data, len);
170+
uartSendFullTimeout(uart_, &len, tx_buffer_, TIME_INFINITE);
171+
chMtxUnlock(&mutex_);
172+
}
173+
174+
bool YFR4escDriver::Start() {
175+
chDbgAssert(!IsStarted(), "don't start the driver twice");
176+
if (IsStarted()) return false;
177+
178+
// Configure RX callback
179+
uart_config_.rxend_cb = [](UARTDriver *uartp) {
180+
chSysLockFromISR();
181+
YFR4escDriver *instance = reinterpret_cast<const UARTConfigEx *>(uartp->config)->context;
182+
chDbgAssert(instance != nullptr, "instance cannot be null!");
183+
// Buffer reached full length. Re-arm DMA immediately to minimize RX gap, then signal the thread.
184+
uartStartReceiveI(uartp, DMA_RX_BUFFER_SIZE, const_cast<uint8_t *>(instance->dma_rx_buffer_));
185+
// Tell the thread a wrap occurred; it will handle the final tail and reset rx_seen_len_.
186+
if (instance->processing_thread_) {
187+
chEvtSignalI(instance->processing_thread_, EVT_RX_DMA_WRAP);
188+
}
189+
chSysUnlockFromISR();
190+
};
191+
192+
if (!(uartStart(uart_, &uart_config_) == MSG_OK)) return false;
193+
194+
// Set the started flag before launching the thread to avoid race where IsStarted() is false in threadFunc
195+
if (!MotorDriver::Start()) return false;
196+
197+
// Now start the processing thread; IsStarted() should be true now and the thread will spin
198+
processing_thread_ = chThdCreateStatic(&thd_wa_, sizeof(thd_wa_), NORMALPRIO, threadHelper, this);
199+
#ifdef USE_SEGGER_SYSTEMVIEW
200+
processing_thread_->name = "YFR4escDriver";
201+
#endif
202+
203+
// Arm RX
204+
rx_seen_len_ = 0;
205+
uartStartReceive(uart_, DMA_RX_BUFFER_SIZE, const_cast<uint8_t *>(dma_rx_buffer_));
206+
207+
// Send an initial settings packet
208+
SendSettings();
209+
210+
return MotorDriver::IsStarted();
211+
}
212+
213+
void YFR4escDriver::threadFunc() {
214+
systime_t last_heartbeat = 0;
215+
216+
while (IsStarted()) {
217+
uint32_t events;
218+
219+
// YFRev4-ESC always send a status packet every 20ms.
220+
// So, read with timeout, instead of waiting for a buffer full ISR call.
221+
events = chEvtWaitOneTimeout(EVT_RX_DMA_WRAP, TIME_MS2I(10));
222+
(void)events;
223+
224+
// Process newly arrived bytes immediately using NDTR deltas; handle wrap if the buffer was refilled.
225+
uint32_t ndtr_now = uart_->dmarx->stream->NDTR;
226+
if (ndtr_now <= DMA_RX_BUFFER_SIZE) {
227+
size_t received_so_far = DMA_RX_BUFFER_SIZE - ndtr_now;
228+
if (received_so_far < rx_seen_len_) {
229+
// Buffer wrapped (rxend_cb re-armed). Process tail [rx_seen_len_ .. end), then reset.
230+
ProcessRxBytes(dma_rx_buffer_ + rx_seen_len_, DMA_RX_BUFFER_SIZE - rx_seen_len_);
231+
rx_seen_len_ = 0;
232+
}
233+
if (received_so_far > rx_seen_len_) {
234+
size_t delta = received_so_far - rx_seen_len_;
235+
ProcessRxBytes(dma_rx_buffer_ + rx_seen_len_, delta);
236+
rx_seen_len_ = received_so_far;
237+
}
238+
}
239+
240+
// Heartbeat: send control periodically even if unchanged, to satisfy ESC watchdog
241+
systime_t now = chVTGetSystemTimeX();
242+
if ((systime_t)(now - last_heartbeat) >= HEARTBEAT_INTERVAL) {
243+
last_heartbeat = now;
244+
SendControl(last_duty_);
245+
}
246+
}
247+
}
248+
249+
} // namespace xbot::driver::motor
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// Created by Apehaenger <joerg@ebeling.ws> on 16.08.25.
3+
//
4+
5+
#ifndef YFR4ESCDRIVER_H
6+
#define YFR4ESCDRIVER_H
7+
8+
#include <cstdint>
9+
#include <debug/debuggable_driver.hpp>
10+
#include <drivers/motor/motor_driver.hpp>
11+
12+
#include "ch.h"
13+
#include "datatypes.h"
14+
#include "hal.h"
15+
16+
namespace xbot::driver::motor {
17+
class YFR4escDriver : public DebuggableDriver, public MotorDriver {
18+
public:
19+
YFR4escDriver() {
20+
latest_state_.status = ESCState::ESCStatus::ESC_STATUS_DISCONNECTED;
21+
};
22+
~YFR4escDriver() override = default;
23+
24+
bool SetUART(UARTDriver *uart, uint32_t baudrate);
25+
26+
void RequestStatus() override{}; // No-op, ESC streams status periodically
27+
void SetDuty(float duty) override;
28+
bool Start() override;
29+
30+
void RawDataInput(uint8_t *data, size_t size) override;
31+
32+
private:
33+
// Heartbeat: resend control regularly to satisfy ESC watchdog
34+
static constexpr systime_t HEARTBEAT_INTERVAL = TIME_MS2I(100); // 10 Hz
35+
36+
float last_duty_ = 0.0f;
37+
38+
struct UARTConfigEx : UARTConfig {
39+
YFR4escDriver *context;
40+
};
41+
42+
// RX buffer size calc: We only receive Status packets
43+
static constexpr size_t RX_MAX_PKT_DECODED = sizeof(yfr4esc::StatusPacket);
44+
static constexpr size_t RX_MAX_PKT_CODED = RX_MAX_PKT_DECODED + (RX_MAX_PKT_DECODED / 254) + 1; // COBS worst-case
45+
46+
// RX DMA buffer holds two full encoded frames + delimiter for wrap/delta comfort
47+
static constexpr size_t DMA_RX_BUFFER_SIZE = 2 * (RX_MAX_PKT_CODED + 1);
48+
volatile uint8_t dma_rx_buffer_[DMA_RX_BUFFER_SIZE]{};
49+
50+
// COBS RX buffer (enough for one full encoded Status frame (without trailing 0))
51+
static constexpr size_t COBS_BUFFER_SIZE = RX_MAX_PKT_CODED;
52+
uint8_t cobs_rx_buffer_[COBS_BUFFER_SIZE]{};
53+
// Decoded buffer holds one Status frame
54+
uint8_t cobs_decoded_[RX_MAX_PKT_DECODED]{};
55+
56+
// DMA-safe TX buffer (avoid sending from stack/DTCM)
57+
static constexpr size_t TX_MAX_PKT_DECODED = (sizeof(yfr4esc::ControlPacket) > sizeof(yfr4esc::SettingsPacket))
58+
? sizeof(yfr4esc::ControlPacket)
59+
: sizeof(yfr4esc::SettingsPacket);
60+
static constexpr size_t TX_MAX_PKT_CODED = TX_MAX_PKT_DECODED + (TX_MAX_PKT_DECODED / 254) + 1; // COBS worst-case
61+
// One full encoded TX frame including trailing 0
62+
static constexpr size_t TX_BUFFER_SIZE = TX_MAX_PKT_CODED + 1;
63+
uint8_t tx_buffer_[TX_BUFFER_SIZE]{};
64+
65+
size_t cobs_rx_len_ = 0;
66+
size_t rx_seen_len_ = 0; // Track how many bytes we already processed in the receiving DMA buffer
67+
68+
THD_WORKING_AREA(thd_wa_, 512){}; // AH20250819: Measured stack usage of 144 bytes (without ULOG errors)
69+
thread_t *processing_thread_ = nullptr;
70+
71+
UARTDriver *uart_{};
72+
UARTConfigEx uart_config_{};
73+
74+
void threadFunc();
75+
static void threadHelper(void *instance) {
76+
static_cast<YFR4escDriver *>(instance)->threadFunc();
77+
};
78+
79+
void ProcessRxBytes(const volatile uint8_t *data, size_t len);
80+
void ProcessDecodedPacket(uint8_t *packet, size_t len);
81+
82+
void SendControl(float duty);
83+
void SendSettings();
84+
};
85+
} // namespace xbot::driver::motor
86+
87+
#endif // YFR4ESCDRIVER_H

0 commit comments

Comments
 (0)