|
| 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 |
0 commit comments