====== Yaesu FC-40 Standalone Control ======
[{{:yaesu:fc40:fc40_exposed.jpeg?300 |The Yaesu FC-40 - those weird grey strips of plastics on the relays weren't placed by me, I don't know why they are there.}}]
The FC-40 is Yaesu's remote automatic antenna tuner, designed to mount at the antenna feedpoint and pair with a handful of Yaesu HF transceivers (FT-857, FT-897, FT-891, FT-450, and similar) that carry the matching TUN/LIN connector. Unlike the generic four-wire ATU interface common on other remote tuners, the FC-40 speaks a proprietary serial protocol to the radio over an 8-pin mini-DIN cable.
A carefully-chosen combination of solid-state switching components and high-speed relays allows the FC-40 to match a wide variety of antennas to within a 2:1 SWR on any amateur band frequency (160 through 5 meters), typically in less than ten seconds. Transmitter power required for matching may be as little as 4 - 60 Watts, and matching settings are automatically stored in memory for instant recall when the same frequency range is selected later.
FC-40 Operating Manual, Introduction
I got mine at the Hermon Hamfest on July 11, 2026 for $40. I was hoping that I could just transmit into it and it would default to an automatic mode and start tuning. I was very wrong. I like good deals, and I like remote tuners, so I wanted to make it work. Hopefully, if I am successful, other people can do the same.
The goal of this project is to figure out how to drive the FC-40 **without a Yaesu radio** - to use it as a standalone remote tuner triggered by a microcontroller, alongside any transceiver capable of producing a keyed carrier. Ultimately I would like to be able to use this tuner (and understand this protocol well enough to potentially emulate it) with radios that have no CAT interface at all, or that I simply don't wish to tie to a computer. Doing so with minimal additional hardware and interaction with the tuner is my goal, and I want it to be easy enough that other folks can reproduce it. Thankfully, some folks before me did a significant amount of legwork that I was able to use to get started. The opposite side of this project is trying to figure out how to get Yaesu HF radios like the FT-891 and FT-857D to interact with "generic" tuners. LDG and MFJ figured it out, but to my knowledge it has never been made public.
This page documents the reverse-engineering effort from first principles through to a working standalone tune. It is very much a living document and will be updated as testing continues. All of my initial testing has been done with a multimeter, arduino, and non-yaesu HF radio. I plan to get my hands on a real Yaesu HF rig soon, and then I'm going to put a logic analyzer on this interface and expose all of its secrets.
This is an active research project. The protocol interpretation here is built entirely from observation and inference, not from any official documentation. Several of my early conclusions turned out to be wrong (and are corrected further down). Treat everything as provisional.
===== Prior Work =====
This project stands on the shoulders of others who chipped away at the same puzzle. Credit where it is due:
* **Albin Stigo (SM6WJM)** wrote [[https://gist.github.com/ast/cc62dff65d35c857c3a2bd8dfb40051f|reverse engineering the Yaesu FC-30]] (a companion tuner using the same interface and radios), connected to an FT-891. This was the first documentation I found of the actual byte protocol, the ''0xFF'' wakeup, the ''F0''/''F1''/''F2'' command structure, and the BCD frequency encoding.
* **John Price (WA2FZW)** and his excellent "FT-891 to MFJ-939Y Tuner Interface Test Box" project. The MFJ-939Y is a third-party tuner that clones the Yaesu protocol, and John built an Arduino Mega test box to sit between radio and tuner, capturing the traffic in both directions. His documented captures of the commanded-tune sequence, along with the timing relationships to the transmit line, were invaluable. Thank you for publishing your work openly - it is exactly the kind of thing that makes projects like this possible. His work can be found on the [[https://groups.io/g/FT-891|FT-891 groups.io page]], which is free to join.
Where those efforts were about understanding the tuner **in conversation with a Yaesu radio**, this project asks a different question: can the tuner be driven entirely on its own?
===== The Interface =====
==== Pinout ====
The tuner connects via an 8-pin mini-DIN cable. From the FC-40 service manual, the pins (as numbered there) are:
^ Pin ^ Signal ^ Direction ^ Notes ^
| 1 | 13.5V in | → tuner | Main power. Common with the MONITOR connector's 13V. |
| 2 | TX GND | → tuner | Pulled low by the radio while transmitting. |
| 3 | GND | — | Signal/logic ground. |
| 4 | DATA IN | → tuner | Serial RX (into tuner). 5V TTL. |
| 5 | DATA OUT | ← tuner | Serial TX (out of tuner). 5V TTL. |
| 6 | SENSE | — | Grounded in the tuner; used by the radio for presence detection. |
| 7 | RESET | → tuner | Held high by the radio the entire time it is powered on. |
| 8 | TX_INH | ← tuner | Transmit inhibit, back to the radio. |
Beware of pin numbering. Some documentation numbers this same connector in the **reverse** order (e.g. pin 1 is TX_INH, pin 8 is +13V). Always confirm physically with a meter and scope before trusting anyone's numbering, including mine. Don't blow up your stuff!
For standalone operation, only four pins actually matter: **13.5V**, **GND**, **DATA IN**, and **DATA OUT**. The MFJ clone's schematic reinforces this - it uses only power, ground, and the two data lines, and simply grounds TX_INH and SENSE. This is the single most encouraging fact for the whole project: the tuner appears to need nothing more than power and a UART.
==== The MONITOR Connector ====
[{{:yaesu:fc40:FC-40_monitor_schematic.jpeg?300 |The 4-pin MONITOR header on the FC-40 schematic}}]
The FC-40 board carries a second, undocumented-on-the-outside 4-pin 0.1" header labelled **MONITOR**, along with two single-color status LEDs. The schematic lists its pins as **GND, 13V, PC_RX, PC_TX**. The 13V is common with the main connector's supply rail - unswitched - which is exactly the arrangement you'd use to power a smart external accessory (a remote display or head) that needs both power and a serial link from a single plug - though to my knowledge Yaesu never made such a device. The TX/RX pins are 5v UART, not RS232 12v levels.
The "PC" naming strongly implies a host/computer-facing interface, too. I haven't captured anything on it yet. This port remains unexplored as of this writing; it is a candidate for a future rabbit hole, particularly if it exposes richer telemetry than the terse control bus.
===== The Protocol =====
The tuner control link is **4800 baud, 8N1, 5V TTL**, on two independent unidirectional lines (DATA IN and DATA OUT are not a shared bus). The 4800 baud figure is corroborated three independent ways - SM6WJM's FC-30 notes, WA2FZW's code, and my own captures. This choice makes sense for a slow link over a long feedline cable carrying only a handful of bytes. The FC-40 shipped with a 5m control cable. This cable also uses the miniDIN 8 connector, the same as vintage Macintosh serial cables, and, like those cables, is straight through - so you can use one to connect to a Yaesu radio if you need to.
==== Command Structure ====
Commands from the radio (or, in the case of my findings so far, the tuner's microcontroller) take the form:
0xFF
The leading ''0xFF'' is a wakeup byte, sent roughly **50 ms before** the three-byte body. This gap matters - when I sent commands as one back-to-back blob, the tuner responded sluggishly with a canned handshake (~125 ms latency), but when the wake gap was present it processed and replied almost immediately (~8 ms). The tuner clearly wants that pause.
Frequency is encoded as two BCD bytes at 10 kHz resolution. ''3.57 MHz'' becomes ''0x03 0x57''; ''14.25 MHz'' becomes ''0x14 0x25''.
^ Command ^ Bytes ^ Meaning (inferred) ^
| Init | ''FF E0 01 00'' / ''FF E0 00 01'' | Radio power-on handshake. Purpose still unclear. |
| Enable / Recall | ''FF F0 '' | Engage tuner, recall stored match for frequency. |
| Disable | ''FF F1 00 00'' | Bypass; drop the network to straight-through. Idempotent. |
| Tune | ''FF F2 '' | Start an actual tuning cycle at the given frequency. |
An important architectural note: the CAT command ''AC002;'' (the "TNR" button, or a software tune request) arrives on the radio's **normal CAT port**, and the radio internally translates it into this proprietary bus protocol. **The tuner bus is not CAT.** Do not expect ASCII CAT verbs on the DIN serial pins.
==== Responses and the Ack/Ready Envelope ====
This is where my understanding evolved the most, and where I was wrong more than once. The current working model:
Every transaction is bracketed by a handshake:
* **''0xA0''** - acknowledge / received / working
* **''0xA1''** - ready / transaction complete
The ''A0''→''A1'' gap is a fixed ~800 ms for a trivial transaction (a bare ''FF'', or the unprompted pair the tuner emits at power-on), and stretches to several seconds during a real tuning hunt, because the tuner is genuinely busy between "ack" and "ready".
The **result** of a tune is carried not by A0/A1 themselves but by what appears **inside or after** that envelope:
^ Pattern ^ Meaning ^
| ''A0 ...... A1'' (nothing trailing) | **Success** - matched and latched |
| ''A0 ...... A1 A2'' | **Late failure** - hunted, could not match the antenna |
| ''A0 B0 .... A1'' | **Early abort** - no or insufficient carrier |
| ''A8 ...... A1'' | Ack **variant** - possibly an extended search; success observed |
Probably the single most counterintuitive finding of this whole project: **success is signalled by silence.** A clean ''A0 ... A1'' with nothing trailing it means the match succeeded, but you see that pair for other things, too. A trailing ''A2'' means it failed (at least, I'm pretty sure). I spent multiple test sessions convinced ''A2'' meant "success" because it appeared on my first celebrated tune - but that tune never actually latched. Once I stopped assuming and watched which bands held a match across key cycles, the truth was unmistakable: ''A2'' is a failure code.
Because I was testing with a random length of wire, I didn't really know what to expect. 40 and 20M struggled a lot, where 80, 12, and 10 did great. The wire I threw out was not something I would ever use "in production" as it were, but rather it was just there so I could properly test how the tuner behaved with real RF. All of my test transmissions were 5w, CW, into dead bandwidth.
Anyway - the ''B0''-before-''A1'' versus ''A2''-after-''A1'' distinction is a genuinely useful diagnostic: ''B0'' seems to mean the carrier was bad (an early bail at ~900 ms), while ''A2'' suggests the carrier was fine but the antenna simply could not be matched (a full hunt that gave up). I ran each of these scenarios a few times on a few bands to be consistent. The ''A8 .... A1'' cycle I saw was one time when the wire presented a 1.3:1 match, but I forced the tuner to try a cycle anyway. It successfully matched 1:1, and gave me that A8 code. That one is harder to reproduce.
===== The RF Front End =====
[{{:yaesu:fc40:fc40_rf_input.png?400 |The FC-40's resistive bridge and attenuator front end, from the service manual schematic}}]
While studying the schematic I found two signals coming from the CPU which distracted me, going to relays near the RF input, labelled **ATT_RL** and **CAL_RL**, feeding a network that made no sense to me at first - a cluster of enormous 20W resistors with heatsinks (a 180Ω across the top of a Pi, two 33Ω in the legs) dumping into a small resistive bridge and a sampling transformer, then a Schottky detector into another CPU pin labelled ''RET''.
This is not the directional coupler / SWR bridge I'm used to seeing in tuners. It looks like the sort of thing I'd see in a test insturment - a resistive return-loss bridge sitting behind a ~12 dB protective pad. The pieces:
* The three ~47Ω resistors plus the antenna network form a Wheatstone / return-loss bridge. When the tuning matrix presents 50Ω, the bridge balances and the detector nulls. The CPU's job during a tune is simply to step L and C until the ''RET'' voltage hits minimum.
* The Schottky rectifies sampled RF to a DC level for the CPU's ADC; a clamp diode protects the pin.
* The 12 dB pad means the radio sees any antenna mismatch through the pad **twice** (~24 dB round trip). Even a dead short or open at the bridge port looks like ~1.1:1 at the transmitter. The tuner can safely measure and hunt into a completely unknown load without ever stressing the PA. If you've ever watched your rig's SWR when a "normal" ATU hunts for a match, it usually is flapping around like crazy. With the FC-40, the radio always sees a perfect 1:1 until the match is found. It's a really slick design.
The ATT/CAL relays right by the RF input give a three-state front end: straight-through (operate), through-pad-only (calibrate against a known reference), and pad-into-bridge (measure/tune). The ''CAL'' naming likely suggests the tuner takes a reference reading to compensate for the Schottky's nonlinearity before or during a hunt, or some other variability in the hardware or environment.
===== Getting a Standalone Tune =====
The path to a working tune was not a straight line. Here are a the key things I discovered while banging my head on the bench:
- **The FC-40 is not autonomous.** Simply transmitting into a reset tuner does nothing - no relay activity, no messages. In bypass, the bridge detector is disconnected from the line, so the tuner is physically blind to SWR until something commands it into measurement mode. What lead me astray on this topic was a comment WA2FZW made regarding his setup's "auto-tune on high SWR" behavior. This almost certainly is something that originates from his Yaesu radio (which has its own SWR metering and issues the tune command), or to the MFJ clone's own added sensing. Now, this said, there are a bunch of DIP switches in on the board. All but one (hard reset) are undocumented. Maybe a combination of these puts the tuner into an autonomous mode....
- **Sequencing is everything.** The carrier must be present **during** the ''F2'' measurement window. Sending ''F2'' and then keying is too late - the tuner has already aborted with ''B0''. The correct sequence is: key the carrier first, hold it, send ''F2'', and wait for the cycle to complete.
- **Success latches natively.** When the tuner actually finds a match, it holds it across key cycles all on its own - no ''F0'' recall, no ''E0'' init handshake required - this is something you would expect from an ATU, once it's tuned on a frequency it stays tuned until either the radio changes frequency, or the environment changes the antenna system. Early in my testing I keppt thinking that matches wouldn't persist after they were found, and that an additional command was needed to save them. This was an artifact of testing on bands my testing antenna system simply could not match AND the fact that since the radio sees the attenuator in the tuner as a perfect load, the radio reports 1:1 during the whole process, and it took me took long to make sense of this.
The working procedure, with nothing but an Arduino, power, and a keyed carrier:
- Key the radio into a steady low-power CW carrier. (Make sure the frequency is clear, first!)
- While transmitting, send ''FF F2 ''.
- Watch the bus: ''A0'' (ack), relays hunt, ''A1'' (ready). If **no** ''A2''/''B0'' trails the ''A1'', the match succeeded and will hold.
- Unkey.
I have confirmed successful matches on multiple bands this way, verified both by the radio's own 1:1 SWR reading, including at higher powers, and from attaching my NanoVNA to the tuner to sweep its match. The FC-40 works as a standalone device! (Mostly, you just need a brainslug microcontroller and a button or computer.)
==== Open Questions ====
* **F0 recall.** Whether ''F0 '' correctly recalls a previously stored match (without RF) is not yet confirmed - largely because my earlier recall tests were run against bands that had never produced a stored solution to begin with. This is now testable properly and is high on the list.
* **The E0 init handshake.** I don't really know what this is doing.
* **The variant ack codes.** I have observed ''A4'', ''A8'', and ''E4'' in the opening (A0) slot on successful tunes, seemingly correlated with the starting SWR or search strategy. Several were captured while experiencing RFI and may be untrustworthy, however (see below), and ''A4''/''A8'' are only a single bit away from ''A0'', so some may be corrupted ''A0'' bytes rather than real sub-codes. Reproducing them on a clean line is the only way to know. I need to do more testing with much better isolation.
===== RFI - A Cautionary Note =====
Testing at the bench with the Arduino sitting within a foot of the tuner and a live wire sometimes produced spectacular garbage on the receive line the moment a match completed and full power hit the antenna. The line would spew hundreds of bytes - ''FF'', ''F7'', ''77'', ''DF'' - in continuous ~3 ms bursts.
These are not real data. Written in binary they are almost all 1-bits, the classic signature of RF glitching an idle-high UART line into framing errors. Tellingly, the spew only ever appears //after// the match completes - i.e. exactly when the protective pad drops out and full power reaches the wire.
The mitigation plan for a future test session: put real distance between the tuner and the microcontroller (8-pin mini-DIN on a run of Cat6 probably, using its twisted pairs, with the Arduino perhaps ten feet away), more ferrite chokes on both the data cable and the USB cable, and maybe grounded metal shielding around the Arduino? At 4800 baud there is enormous timing margin, so a longer cable shouldn't impose any issues, and with 5v logic levels voltage drop shouldn't be a big concern either.
===== The Test Sketch =====
Below is the current state of the testing firmware. It targets an ATmega32U4 board with normal Arduino firmware (Leonardo/Micro/Pro Micro), chosen because its native USB gives us a hardware UART (''Serial1'') for the tuner that is fully independent of the USB console (''Serial''). It's also 5v tolerant, cheap, and I had one close at hand when I got started. Realistically, because the communuication is all happening over a UART, a FTDI or Prolific USB interface could be used, along with a python script or whatever. I've been sticking with Arduino because eventually I want my interface to be a box with a button and an LED or two, I don't want it to be entirely reliant on a computer.. unless I do.. (more on that later.)
==== What it does ====
* Listens on the tuner UART at 4800 8N1 and prints every received byte with a millisecond timestamp, decoding the known response codes.
* Lets you send commands from the Arduino IDE Serial Monitor: wakeup, the full init sequence, individual ''E0''/''F0''/''F1''/''F2'' commands (with frequency in MHz, auto-converted to BCD), and arbitrary raw hex bytes.
* Frames commands correctly, with the ~50 ms wake gap after ''0xFF''.
* Detects "success by silence" - if an ''A1'' arrives and ~250 ms pass with no trailing ''A2''/''B0'', it declares the tune matched, and drives status LEDs (D13 success, D12 fail, though realistically these are kind of guesses).
* Collapses rapid RFI bursts into a single suppressed-count line so real, well-spaced bytes remain visible - though this is kind of tricky because bad enough RFI will either crash the Arduino, or reset the USB interface.
* Loudly flags any unrecognized byte, so new response variants can't hide. I'll update this page as I find more.
==== What it does NOT do (yet) ====
* **It is not a controller.** It cannot key your radio - you supply the carrier by hand (or with a separate PTT arrangement). The commanded sequence is issued by you, timed against the carrier you're holding. I've just got a morse key next to my keyboard.
* **It does not trust its own success/fail output under RFI.** The noise filter improves readability but does not make the line reliable; a stray byte in a quiet gap can still mis-decode, and RFI can crash it out easily.
* **The response-code interpretation is provisional.** ''A2''=fail and ''B0''=no-carrier are well-supported and have been consistent; ''A4''/''A8''/''E4'' are guesses and have been a lot harder to get consistently.
* **No memory / recall logic, no E0 handshake automation** beyond a bare ''init'' command for experimentation. I plan eventually test this, but I may just end up running this tuner as a "dumb" remote ATU where it regularly has to do a full cycle anyway. We'll see how things end up.
* It is a **bench diagnostic tool**, full stop. It is not fit for unattended or production use.
==== Honesty Moment ====
I did use generative AI to help me with this sketch. I'm not super great at putting this kind of stuff together in a clean fashion. Between the work of those before me, and banging my head on the keyboard, I was able to get sequences that worked, and got the tuner doing what it needed to do. Once I got that far, I used Anthropic's opus to clean it up.
If you don't like that, I understand, and you should be able to use the above descriptions and resources to write your own methods.
==== Code ====
/*
* FC-40 tuner probe & listener (ATmega32U4, 5V logic)
* ----------------------------------------------------
* Serial (USB CDC) -> IDE console @ 115200
* Serial1 (HW UART, pins 0/1) -> tuner @ 4800 8N1
*
* Wiring (FC-40 service-manual pin numbers):
* pin5 DATA OUT -> board RX (D0)
* pin4 DATA IN <- board TX (D1)
* pin3 GND -- board GND
* pin1 +13.5V -> board Vin (optional)
*
* Acknowledgement: builds on the reverse-engineering of the Yaesu tuner
* control protocol published by John Price (WA2FZW), and Albin Stigo's (SM6WJM)
* FC-30 reverse-engineering notes. Thanks for sharing openly.
*
* ---- Protocol model (empirical, still being refined) --------------------
* Every transaction is bracketed by an ack/ready handshake:
* A0 ... A1 A0 = ack (received/working) ; A1 = ready (done)
* The A0->A1 gap is ~800ms for a trivial handshake (boot / bare FF),
* and stretches to several seconds during a real F2 tuning hunt.
* The RESULT of a tune is carried by what appears INSIDE/AFTER that bracket:
* A0 ...... A1 -> SUCCESS (matched & latched, holds on re-key)
* A0 ...... A1 A2 -> LATE FAIL (hunted, could not match antenna)
* A0 B0 . A1 -> EARLY ABORT (no / insufficient carrier)
* A8 ...... A1 -> ack VARIANT (extended search?) - provisional
* Success is signalled by the ABSENCE of a trailing A2/B0, detected by a
* short quiet window after A1. A4/A8/E4 variants are provisional pending
* clean (RFI-free) captures.
* ------------------------------------------------------------------------
*
* Console commands (newline to send):
* h help
* w wakeup 0xFF
* init full bring-up: E0 01 00 / E0 00 01 / F0 00 00
* e0 send FF E0 (hex args, e.g. e0 01 00)
* f0 enable+recall (FF F0 )
* f1 disable/bypass (FF F1 00 00)
* f2 start tune (FF F2 )
* probe f1 then f2
* raw raw bytes, NO wake gap (e.g. raw FF E0 01 00)
* x reset timestamp clock
*/
// ---------- optional indicator LEDs ----------
#define USE_LED 1
const uint8_t PIN_LED_OK = 13; // onboard user LED = success
const uint8_t PIN_LED_FAIL = 12; // external LED = fail
// ---------- protocol bytes ----------
const uint8_t WAKEUP = 0xFF;
const uint8_t OP_INIT = 0xE0;
const uint8_t OP_ENABLE = 0xF0;
const uint8_t OP_DISABLE = 0xF1;
const uint8_t OP_TUNE = 0xF2;
const uint16_t WAKE_GAP_MS = 50; // FF ... gap ... body (the tuner wants this)
// ---------- timestamp clock ----------
uint32_t t0 = 0, lastActivity = 0;
const uint32_t IDLE_RESET_MS = 14000;
// ---------- transaction/result tracking ----------
uint32_t a1SeenAt = 0;
bool failFlag = false;
const uint32_t SUCCESS_WINDOW_MS = 250;
// ---------- RFI burst filter ----------
uint32_t lastRxMs = 0;
uint16_t noiseCount = 0;
const uint16_t BURST_GAP_MS = 15;
// ---------- LED one-shots ----------
uint32_t okOffAt = 0, failOffAt = 0;
char lineBuf[48]; uint8_t lineLen = 0;
// =========================================================================
void setup() {
Serial.begin(115200);
while (!Serial && millis() < 3000) {}
Serial1.begin(4800, SERIAL_8N1);
#if USE_LED
pinMode(PIN_LED_OK, OUTPUT); digitalWrite(PIN_LED_OK, LOW);
pinMode(PIN_LED_FAIL, OUTPUT); digitalWrite(PIN_LED_FAIL, LOW);
#endif
Serial.println(F("\n=== FC-40 probe (32U4) === type 'h' for help"));
resetClock();
}
void loop() {
drainTuner();
checkSuccess();
handleConsole();
serviceLeds();
idleReset();
}
// =========================================================================
uint32_t stamp() {
uint32_t now = millis();
if (t0 == 0) t0 = now;
lastActivity = now;
return now - t0;
}
void resetClock() { t0 = 0; Serial.println(F("\n-- t0 reset --")); }
void idleReset() {
if (t0 && (millis() - lastActivity) > IDLE_RESET_MS) {
t0 = 0; Serial.println(F("\n-- t0 reset (idle) --"));
}
}
// ------------------------------------------------------------------------
void flushNoise() {
if (noiseCount) {
char b[52];
snprintf(b, sizeof(b), "\t ...(%u noise bytes suppressed - RFI)", noiseCount);
Serial.println(b);
noiseCount = 0;
}
}
void drainTuner() {
while (Serial1.available()) {
uint8_t b = Serial1.read();
uint32_t now = millis();
uint32_t gap = now - lastRxMs;
lastRxMs = now;
// Rapid back-to-back bytes = RFI on the idle-high line. Count, don't spam.
if (gap < BURST_GAP_MS) {
noiseCount++;
if (noiseCount > 250) flushNoise();
continue;
}
flushNoise();
char buf[26];
snprintf(buf, sizeof(buf), "%05lu\t[0x%02X]", (unsigned long)stamp(), b);
Serial.print(buf);
switch (b) {
case 0xA0: Serial.print(F(" A0 ack (received/working)")); failFlag=false; a1SeenAt=0; break;
case 0xA4: Serial.print(F(" A4 ack variant (provisional)")); failFlag=false; a1SeenAt=0; break;
case 0xA8: Serial.print(F(" A8 ack variant (provisional)")); failFlag=false; a1SeenAt=0; break;
case 0xE4: Serial.print(F(" E4 ack variant (provisional)")); failFlag=false; a1SeenAt=0; break;
case 0xA1: Serial.print(F(" A1 ready (transaction done)")); a1SeenAt=millis(); break;
case 0xA2: Serial.print(F(" A2 *** TUNE FAILED (no match) ***")); failFlag=true; a1SeenAt=0; ledFail(); break;
case 0xB0: Serial.print(F(" B0 *** NO CARRIER (early abort) ***")); failFlag=true; a1SeenAt=0; ledFail(); break;
default: Serial.print(F(" <<< UNKNOWN BYTE - INVESTIGATE >>>")); break;
}
Serial.println();
}
}
// A plain A1 with nothing trailing it => the tune matched and latched.
void checkSuccess() {
if (a1SeenAt && (millis() - a1SeenAt > SUCCESS_WINDOW_MS)) {
if (!failFlag) {
Serial.println(F("\t\t=> SUCCESS (matched & latched)"));
ledOk();
}
a1SeenAt = 0;
}
}
// ------------------------------------------------------------------------
void ledOk() {
#if USE_LED
digitalWrite(PIN_LED_OK, HIGH); okOffAt = millis() + 1500;
digitalWrite(PIN_LED_FAIL, LOW); failOffAt = 0;
#endif
}
void ledFail() {
#if USE_LED
digitalWrite(PIN_LED_FAIL, HIGH); failOffAt = millis() + 1500;
digitalWrite(PIN_LED_OK, LOW); okOffAt = 0;
#endif
}
void serviceLeds() {
#if USE_LED
if (okOffAt && millis() >= okOffAt) { digitalWrite(PIN_LED_OK, LOW); okOffAt = 0; }
if (failOffAt && millis() >= failOffAt) { digitalWrite(PIN_LED_FAIL, LOW); failOffAt = 0; }
#endif
}
// ------------------------------------------------------------------------
void txByte(uint8_t b) {
Serial1.write(b);
char buf[26];
snprintf(buf, sizeof(buf), "%05lu\t0x%02X (tx)", (unsigned long)stamp(), b);
Serial.println(buf);
}
void sendCommand(uint8_t op, uint8_t a, uint8_t b) {
txByte(WAKEUP); Serial1.flush();
delay(WAKE_GAP_MS);
txByte(op); txByte(a); txByte(b); Serial1.flush();
}
void doInit() {
Serial.println(F("[init] E0 01 00")); sendCommand(OP_INIT, 0x01, 0x00); delay(120);
Serial.println(F("[init] E0 00 01")); sendCommand(OP_INIT, 0x00, 0x01); delay(120);
Serial.println(F("[init] F0 00 00")); sendCommand(OP_ENABLE, 0x00, 0x00);
}
bool mhzToBcd(float mhz, uint8_t &hi, uint8_t &lo) {
long u = lround(mhz * 100.0);
if (u < 0 || u > 9999) return false;
hi = (((u/1000)%10)<<4) | ((u/100)%10);
lo = (((u/10)%10)<<4) | (u%10);
return true;
}
// ------------------------------------------------------------------------
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) return;
if (!strcmp(s,"h")) { printHelp(); return; }
if (!strcmp(s,"x")) { resetClock(); return; }
if (!strcmp(s,"w")) { txByte(WAKEUP); return; }
if (!strcmp(s,"init")) { doInit(); return; }
if (!strcmp(s,"f1")) { sendCommand(OP_DISABLE,0,0); return; }
if (!strncmp(s,"e0",2)) {
char *p = s+2; uint8_t a = strtol(p,&p,16); uint8_t b = strtol(p,&p,16);
sendCommand(OP_INIT, a, b); return;
}
if (!strncmp(s,"f0",2) || !strncmp(s,"f2",2)) {
uint8_t op = (s[1]=='0') ? OP_ENABLE : OP_TUNE;
uint8_t hi, lo; float mhz = atof(s+2);
if (mhz > 0 && mhzToBcd(mhz, hi, lo)) sendCommand(op, hi, lo);
else Serial.println(F("usage: f0/f2 "));
return;
}
if (!strncmp(s,"probe",5)) {
uint8_t hi, lo; float mhz = atof(s+5);
if (mhz > 0 && mhzToBcd(mhz, hi, lo)) {
sendCommand(OP_DISABLE,0,0); delay(400); sendCommand(OP_TUNE, hi, lo);
} else Serial.println(F("usage: probe "));
return;
}
if (!strncmp(s,"raw",3)) {
char *p = s+3;
while (*p) { while(*p==' ')p++; if(!*p)break; txByte((uint8_t)strtol(p,&p,16)); }
return;
}
Serial.println(F("? type 'h'"));
}
void printHelp() {
Serial.println(F("\n commands:"));
Serial.println(F(" h help"));
Serial.println(F(" w wakeup 0xFF"));
Serial.println(F(" init full bring-up: E0 01 00 / E0 00 01 / F0 00 00"));
Serial.println(F(" e0 send FF E0 (hex, e.g. e0 01 00)"));
Serial.println(F(" f0 enable+recall (FF F0 )"));
Serial.println(F(" f1 disable/bypass (FF F1 00 00)"));
Serial.println(F(" f2 start tune (FF F2 )"));
Serial.println(F(" probe f1 then f2"));
Serial.println(F(" raw raw bytes, no wake gap (raw FF E0 01 00)"));
Serial.println(F(" x reset timestamp clock"));
}
===== Next Steps =====
* Move the Arduino away from the tuner on a Cat6 run, add ferrites, and re-run the band sweep on a clean line to settle the ''A4''/''A8''/''E4'' variant question.
* Test ''F0'' recall properly, from a band that has actually stored a successful match.
* Send the ''E0'' init sequence with correct wake-gap framing and see whether it unlocks any new behavior.
* Characterize the front-end insertion loss quantitatively with a calibrated through-line attenuator into the spectrum analyzer.
* Investigate the MONITOR / PC port.
===== Photos & Media =====
* {{ :yaesu:fc40:fc_40_antenna_tuner_manual_en_35bf.pdf}}
* {{ :yaesu:fc40:fc-40_brochure.pdf}}
{{gallery>:yaesu:fc40}}