Operating the Yaesu FC-40 remote ATU without a Yaesu radio connected to it is something I am interested in doing. This Arduino sketch helps determine the feasibility of that.
/* * FC-40 / Yaesu-protocol antenna tuner probe & listener * ------------------------------------------------------ * Target: ATmega32U4 board (Leonardo / Micro / Pro Micro), 5V logic. * Direct-connect to the tuner's 5V TTL UART (verify levels on a scope!). * * Serial (USB CDC) -> IDE Serial Monitor console @ 115200 * Serial1 (HW UART, pins 0=RX / 1=TX) -> tuner UART @ 4800 8N1 * * WIRING (uses FC-40 SERVICE MANUAL pin numbers): * Tuner pin 5 DATA OUT (tuner TX) -> board RX (D0) * Tuner pin 4 DATA IN (tuner RX) <- board TX (D1) * Tuner pin 3 GND -- board GND (MUST be common) * Tuner pin 1 +13.5V -> board Vin/RAW (onboard regulator) [optional] * * *** VERIFY PIN NUMBERING AND LOGIC LEVELS ON A SCOPE FIRST. *** * The mini-DIN can be numbered in either direction depending on whether * you read the plug or the jack; signals are what matter, not the count. * * OPTIONAL: onboard user LED (D13) blips on each received A1. This is a * convenience only -- see note below about what an A1 does and does not mean. * * CONSOLE COMMANDS (type in Serial Monitor, newline to send): * h help * w send wakeup (0xFF) * f1 send tuner-disable (0xFF 0xF1 0x00 0x00) * f0 <MHz> send enable+freq (0xFF 0xF0 <bcd hi> <bcd lo>) * f2 <MHz> send start-tune (0xFF 0xF2 <bcd hi> <bcd lo>) * probe <MHz> f1 ... (gap) ... f2 <MHz> (mimics a radio commanded tune) * raw <hh..> send arbitrary hex bytes, e.g. raw FF E0 01 00 * x reset the timestamp clock * * Received (tuner) bytes print in [brackets] with a ms timestamp. * Bytes we transmit print without brackets. * * ------------------------------------------------------ * Acknowledgement: * This work builds directly on the prior reverse-engineering and * documentation of the Yaesu tuner control protocol by John Price (WA2FZW), * whose published test box, captures, and analysis of the FT-891 / tuner * interface made this possible. Thank you for sharing your research openly. * ------------------------------------------------------ */ // ---- optional indicator ---- #define USE_LED_A1 1 // set 0 to disable the D13 blip entirely const uint8_t PIN_LED = 13; // ---- protocol bytes ---- const uint8_t WAKEUP = 0xFF; const uint8_t OP_ENABLE = 0xF0; // enable + set freq const uint8_t OP_DISABLE = 0xF1; // "set freq to zero" / bypass const uint8_t OP_TUNE = 0xF2; // start tune at freq const uint8_t RESP_A0 = 0xA0; // freq-ack / tuning-motion start const uint8_t RESP_A1 = 0xA1; // TX gate (cmd mode, x2) OR single-shot (auto) // const uint8_t RESP_FAIL = 0x??; // UNKNOWN - fill in once you provoke a failure const uint16_t WAKE_GAP_MS = 50; // observed ~50ms between 0xFF and command body // ---- timestamp clock ---- uint32_t t0 = 0; uint32_t lastActivity = 0; const uint32_t IDLE_RESET_MS = 14000; // ---- A1 tracking (helps distinguish auto vs command mode) ---- uint8_t a1Count = 0; // ---- LED one-shot ---- uint32_t ledOffAt = 0; char lineBuf[48]; uint8_t lineLen = 0; // ------------------------------------------------------------------- void setup() { Serial.begin(115200); // USB CDC console while (!Serial && millis() < 3000) { } // wait briefly for host (32U4) Serial1.begin(4800, SERIAL_8N1); // tuner UART #if USE_LED_A1 pinMode(PIN_LED, OUTPUT); digitalWrite(PIN_LED, LOW); #endif Serial.println(F("\n=== FC-40 tuner probe / listener (ATmega32U4) ===")); Serial.println(F("Listening on Serial1 @ 4800 8N1.")); Serial.println(F("Type 'h' for commands. Power-cycle the tuner now to catch boot chatter.")); resetClock(); } // ------------------------------------------------------------------- void loop() { drainTuner(); handleConsole(); serviceLed(); idleReset(); } // ------------------------------------------------------------------- uint32_t stamp() { uint32_t now = millis(); if (t0 == 0) t0 = now; lastActivity = now; return now - t0; } void resetClock() { t0 = 0; a1Count = 0; Serial.println(F("\n-- t0 reset --")); } void idleReset() { if (t0 != 0 && (millis() - lastActivity) > IDLE_RESET_MS) { t0 = 0; a1Count = 0; Serial.println(F("\n-- t0 reset (idle) --")); } } // ------------------------------------------------------------------- // Everything from the tuner prints in [brackets]. void drainTuner() { while (Serial1.available()) { uint8_t b = Serial1.read(); uint32_t ts = stamp(); char buf[24]; snprintf(buf, sizeof(buf), "%05lu\t[0x%02X]", (unsigned long)ts, b); Serial.print(buf); switch (b) { case RESP_A0: Serial.print(F(" A0 freq-ack / tuning motion start")); break; case RESP_A1: a1Count++; // AUTO mode: a single A1 means the cycle STARTED, not that it // succeeded. COMMAND mode: A1 #1 = key now, A1 #2 = done/unkey. ledBlip(1000); if (a1Count == 1) Serial.print(F(" A1 #1 (auto: cycle running / cmd: key radio now)")); else Serial.print(F(" A1 #2 (cmd mode: tune complete, unkey)")); break; // case RESP_FAIL: // Serial.print(F(" FAIL")); // break; default: break; } Serial.println(); } } // ------------------------------------------------------------------- void ledBlip(uint32_t ms) { #if USE_LED_A1 digitalWrite(PIN_LED, HIGH); ledOffAt = millis() + ms; #else (void)ms; #endif } void serviceLed() { #if USE_LED_A1 if (ledOffAt && millis() >= ledOffAt) { digitalWrite(PIN_LED, LOW); ledOffAt = 0; } #endif } // ------------------------------------------------------------------- // Bytes we send echo to the console WITHOUT brackets. void txByte(uint8_t b) { Serial1.write(b); uint32_t ts = stamp(); char buf[24]; snprintf(buf, sizeof(buf), "%05lu\t0x%02X (tx)", (unsigned long)ts, b); Serial.println(buf); } // 0xFF, wait the observed gap, then a 3-byte command body. void sendCommand(uint8_t op, uint8_t hi, uint8_t lo) { txByte(WAKEUP); Serial1.flush(); delay(WAKE_GAP_MS); txByte(op); txByte(hi); txByte(lo); Serial1.flush(); } // MHz -> two BCD bytes at 10 kHz resolution. // 3.57 -> 0x03 0x57 // 7.22 -> 0x07 0x22 // 14.25 -> 0x14 0x25 bool mhzToBcd(float mhz, uint8_t &hi, uint8_t &lo) { long units = lround(mhz * 100.0); // 10 kHz units if (units < 0 || units > 9999) return false; uint8_t d3 = (units / 1000) % 10; uint8_t d2 = (units / 100) % 10; uint8_t d1 = (units / 10) % 10; uint8_t d0 = units % 10; hi = (d3 << 4) | d2; lo = (d1 << 4) | d0; return true; } // Mimic a radio's commanded-tune sequence (minus actually keying RF). // Watch for: [A1] (key here) ... [A0] ... [A1] (done). void commandedProbe(float mhz) { uint8_t hi, lo; if (!mhzToBcd(mhz, hi, lo)) { Serial.println(F("bad freq")); return; } a1Count = 0; sendCommand(OP_DISABLE, 0x00, 0x00); // FF F1 00 00 delay(400); sendCommand(OP_TUNE, hi, lo); // FF F2 <bcd> } // ------------------------------------------------------------------- void handleConsole() { while (Serial.available()) { char c = Serial.read(); if (c == '\r') continue; if (c == '\n') { lineBuf[lineLen] = 0; parseLine(lineBuf); lineLen = 0; } else if (lineLen < sizeof(lineBuf) - 1) { lineBuf[lineLen++] = c; } } } void parseLine(char *s) { while (*s == ' ') s++; if (*s == 0) return; if (!strcmp(s, "h")) { printHelp(); return; } if (!strcmp(s, "x")) { resetClock(); return; } if (!strcmp(s, "w")) { txByte(WAKEUP); return; } if (!strcmp(s, "f1")) { sendCommand(OP_DISABLE, 0x00, 0x00); return; } if (!strncmp(s, "f0", 2) || !strncmp(s, "f2", 2)) { uint8_t op = (s[1] == '0') ? OP_ENABLE : OP_TUNE; float mhz = atof(s + 2); uint8_t hi, lo; if (mhz > 0 && mhzToBcd(mhz, hi, lo)) sendCommand(op, hi, lo); else Serial.println(F("usage: f0 <MHz> | f2 <MHz>")); return; } if (!strncmp(s, "probe", 5)) { float mhz = atof(s + 5); if (mhz > 0) commandedProbe(mhz); else Serial.println(F("usage: probe <MHz>")); return; } if (!strncmp(s, "raw", 3)) { char *p = s + 3; while (*p) { while (*p == ' ') p++; if (!*p) break; uint8_t b = (uint8_t) strtol(p, &p, 16); txByte(b); } return; } Serial.println(F("? unknown - type 'h'")); } void printHelp() { Serial.println(F("\n commands:")); Serial.println(F(" h help")); Serial.println(F(" w wakeup 0xFF")); Serial.println(F(" f1 disable (FF F1 00 00)")); Serial.println(F(" f0 <MHz> enable+freq (FF F0 <bcd>)")); Serial.println(F(" f2 <MHz> start tune (FF F2 <bcd>)")); Serial.println(F(" probe <MHz> f1 then f2 (commanded sequence)")); Serial.println(F(" raw <hh..> send raw hex, e.g. raw FF E0 01 00")); Serial.println(F(" x reset timestamp clock")); }