Files
hexapod-robot/2026-current/sim/SimHardware.cpp
T
mangavalkandClaude Opus 5.5 5ff85c4eb8 Robot: Teensy 3.6 with the old robot's servo wiring and calibration
- LEGACY/Hexapod_Teensy/: old robot sketch (reference only).
- platformio.ini: robot env targets teensy36 (owner-confirmed board).
- Config.h: per-servo SERVOS table (pin, pulse at 0 deg, signed
  us/deg, pulse limits) built from the old sketch's pins, ranges, trims
  and the old Unity servo directions. Rear-left/front-left coxa offsets
  set to 0: the owner re-horns those two servos (the old +-90 left them
  at the end of travel; the sim showed them clamping).
- ServoDriver: 18 servos on hardware PWM (50 Hz, 16-bit); the Teensy 3.6
  Servo library stops at 12.
- Radio on CE 39 / CSN 15 with SPI SCK on pin 14.
- sim: analogWrite/PWM shims, pulses decoded through SERVOS (API v6).
- Docs: CLAUDE.md, READMEs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:53:55 +02:00

255 lines
7.9 KiB
C++

// Implementations of the Arduino/Teensy stand-ins declared in shim/.
#include "SimHardware.h"
#include <Arduino.h>
#include <Servo.h>
#include <Wire.h>
#include <RF24.h>
#include <SPI.h>
#include <EEPROM.h>
#include <HexapodProtocol.h>
#include <stdarg.h>
#include <deque>
#include <string>
#include <vector>
SimSerial Serial;
TwoWire Wire;
SPIClass SPI;
SimEEPROM EEPROM;
// =====================================================================
// Clock
// =====================================================================
static uint64_t g_now_us = 0;
namespace simhw {
void advanceMicros(uint32_t us) { g_now_us += us; }
uint64_t nowMicros() { return g_now_us; }
} // namespace simhw
uint32_t micros() { return (uint32_t)g_now_us; }
uint32_t millis() { return (uint32_t)(g_now_us / 1000); }
// The robot loop never blocks; if it ever does, time just moves on.
void delay(uint32_t ms) { g_now_us += (uint64_t)ms * 1000; }
void delayMicroseconds(uint32_t us) { g_now_us += us; }
// =====================================================================
// Serial
// =====================================================================
static std::deque<char> g_serial_in;
static std::string g_serial_out;
namespace simhw {
void serialFeed(const char* bytes, int len) {
for (int i = 0; i < len; i++) g_serial_in.push_back(bytes[i]);
}
int serialDrain(char* out, int cap) {
int n = (int)g_serial_out.size() < cap ? (int)g_serial_out.size() : cap;
memcpy(out, g_serial_out.data(), n);
g_serial_out.erase(0, n);
return n;
}
} // namespace simhw
int SimSerial::available() { return (int)g_serial_in.size(); }
int SimSerial::read() {
if (g_serial_in.empty()) return -1;
char c = g_serial_in.front();
g_serial_in.pop_front();
return (uint8_t)c;
}
size_t SimSerial::write(uint8_t c) {
if (g_serial_out.size() < 65536) g_serial_out.push_back((char)c); // cap if nobody drains
return 1;
}
size_t SimSerial::print(const char* s) {
size_t n = 0;
while (*s) n += write((uint8_t)*s++);
return n;
}
size_t SimSerial::printf(const char* fmt, ...) {
char buf[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
return print(buf);
}
// =====================================================================
// Servos
// =====================================================================
static simhw::ServoPin g_pins[simhw::MAX_PINS];
namespace simhw {
const ServoPin& servoPin(int pin) {
static const ServoPin none;
return (pin >= 0 && pin < MAX_PINS) ? g_pins[pin] : none;
}
} // namespace simhw
uint8_t Servo::attach(int pin) {
if (pin < 0 || pin >= simhw::MAX_PINS) return 0;
pin_ = pin;
if (!g_pins[pin].attached) g_pins[pin].us = 1500;
g_pins[pin].attached = true;
return 1;
}
void Servo::detach() {
if (pin_ >= 0) g_pins[pin_].attached = false;
pin_ = -1;
}
void Servo::writeMicroseconds(int us) {
if (pin_ < 0) return;
g_pins[pin_].us = us;
g_pins[pin_].writes++;
}
// ---- Hardware PWM (how ServoDriver drives the servos on the Teensy 3.6) ----
static uint32_t g_pwm_bits = 8; // Teensyduino default
static float g_pwm_hz[simhw::MAX_PINS]; // 0 = not set (Teensy default ~488 Hz)
void pinMode(uint8_t pin, uint8_t mode) {
if (pin < simhw::MAX_PINS && mode == INPUT) g_pins[pin].attached = false;
}
void digitalWrite(uint8_t, uint8_t) {}
uint32_t analogWriteResolution(uint32_t bits) {
uint32_t old = g_pwm_bits;
g_pwm_bits = bits;
return old;
}
void analogWriteFrequency(uint8_t pin, float hz) {
if (pin < simhw::MAX_PINS) g_pwm_hz[pin] = hz;
}
// A duty of 0 means no pulses (a relaxed servo); otherwise the pulse width
// follows from duty, resolution and frequency, exactly as on the hardware.
void analogWrite(uint8_t pin, int value) {
if (pin >= simhw::MAX_PINS) return;
if (value <= 0) {
g_pins[pin].attached = false;
return;
}
float hz = g_pwm_hz[pin] > 0 ? g_pwm_hz[pin] : 488.28f;
g_pins[pin].us = (int)lroundf((float)value / (1u << g_pwm_bits) * (1e6f / hz));
g_pins[pin].attached = true;
g_pins[pin].writes++;
}
// =====================================================================
// I2C + MPU6050 (register-level, address 0x68)
// =====================================================================
static constexpr int MPU_ADDR = 0x68;
static bool g_imu_present = true;
static uint8_t g_mpu_regs[128];
static uint8_t g_mpu_ptr = 0;
static int g_tx_addr = -1;
static std::vector<uint8_t> g_tx_bytes;
static std::deque<uint8_t> g_rx;
namespace simhw {
void imuSetPresent(bool present) { g_imu_present = present; }
static void putBE(uint8_t reg, float value, float scale) {
float raw = value * scale;
if (raw > 32767) raw = 32767;
if (raw < -32768) raw = -32768;
int16_t v = (int16_t)lroundf(raw);
g_mpu_regs[reg] = (uint8_t)((uint16_t)v >> 8);
g_mpu_regs[reg + 1] = (uint8_t)(v & 0xFF);
}
void imuSet(float ax, float ay, float az, float gx, float gy, float gz) {
// Default +/-2 g and +/-250 deg/s ranges, same scale factors RobotIMU uses.
putBE(0x3B, ax, 16384.0f);
putBE(0x3D, ay, 16384.0f);
putBE(0x3F, az, 16384.0f);
putBE(0x41, 0, 1); // temperature, unused
putBE(0x43, gx, 131.0f);
putBE(0x45, gy, 131.0f);
putBE(0x47, gz, 131.0f);
}
} // namespace simhw
void TwoWire::beginTransmission(int address) {
g_tx_addr = address;
g_tx_bytes.clear();
}
size_t TwoWire::write(uint8_t b) {
g_tx_bytes.push_back(b);
return 1;
}
uint8_t TwoWire::endTransmission(bool) {
if (g_tx_addr != MPU_ADDR || !g_imu_present) return 2; // address NACK
if (!g_tx_bytes.empty()) {
g_mpu_ptr = g_tx_bytes[0] & 0x7F; // first byte = register pointer
for (size_t i = 1; i < g_tx_bytes.size(); i++) g_mpu_regs[(g_mpu_ptr++) & 0x7F] = g_tx_bytes[i];
if (g_tx_bytes.size() > 1) g_mpu_ptr = g_tx_bytes[0] & 0x7F;
}
return 0;
}
uint8_t TwoWire::requestFrom(int address, int quantity) {
g_rx.clear();
if (address != MPU_ADDR || !g_imu_present) return 0;
for (int i = 0; i < quantity; i++) g_rx.push_back(g_mpu_regs[(g_mpu_ptr + i) & 0x7F]);
return (uint8_t)quantity;
}
int TwoWire::available() { return (int)g_rx.size(); }
int TwoWire::read() {
if (g_rx.empty()) return -1;
uint8_t b = g_rx.front();
g_rx.pop_front();
return b;
}
// =====================================================================
// nRF24L01 "air"
// =====================================================================
struct AirPacket {
uint8_t channel;
rf24_datarate_e rate;
uint8_t address[5];
std::vector<uint8_t> payload;
};
static bool g_radio_present = true;
static std::deque<AirPacket> g_air; // packets waiting at the receiver
static constexpr size_t RX_FIFO_DEPTH = 3;
namespace simhw {
void radioSetPresent(bool present) { g_radio_present = present; }
void radioSend(const uint8_t* bytes, int len) {
if (len < 0 || len > 32) return;
if (g_air.size() >= RX_FIFO_DEPTH) return; // FIFO full: the real chip drops it too
AirPacket p;
p.channel = HXP_RADIO_CHANNEL;
p.rate = (rf24_datarate_e)HXP_RADIO_DATA_RATE;
memcpy(p.address, HXP_RADIO_ADDRESS, 5);
p.payload.assign(bytes, bytes + len);
g_air.push_back(p);
}
} // namespace simhw
bool RF24::begin() { return g_radio_present; }
bool RF24::isChipConnected() { return g_radio_present; }
void RF24::openReadingPipe(uint8_t, const uint8_t* address) {
memcpy(address_, address, 5);
pipe_open_ = true;
}
bool RF24::available() {
if (!g_radio_present || !listening_ || !pipe_open_) return false;
// Drop anything this radio's configuration wouldn't have decoded.
while (!g_air.empty()) {
const AirPacket& p = g_air.front();
bool match = p.channel == channel_ && p.rate == data_rate_ &&
memcmp(p.address, address_, 5) == 0 && p.payload.size() == payload_size_;
if (match) return true;
g_air.pop_front();
}
return false;
}
void RF24::read(void* buf, uint8_t len) {
if (g_air.empty()) return;
const AirPacket& p = g_air.front();
memcpy(buf, p.payload.data(), len < p.payload.size() ? len : p.payload.size());
g_air.pop_front();
}