Gotchas of the serial interface of the FY6600 and FY6800
Three weeks ago I began to write a Python driver for the FY6600/6800. I tried a few existing ones but they did not seem to work very well, so I started my own. This turned out to become a really strange journey, due to many oddities how the firmware of the devices processes parameters sent from a host computer and of the data returned by the them.
I have an FY6600 with firmware version 3.2 and an FY6800 with firmware version 1.7.1. Both have identical quirks.
I would not feel offended if anybody would question my mental sanity while reading this post. So I invite any owner of an FY6600/FY6800 to check my observations. This is not difficult: You can easily type the commands in a termial program like minicom (Linux) or Putty (Windows). Just keep one detail in mind: Commands must end with a newline character ('\n'). Hitting the RETURN key sends a carrigae return ('\ŗ') – and that confuses the FY6x00. Type ctrl-J isnstead to send the '\n'.
Now the details:
Reading frequency values:
The FY6x00 allows to set frequencies in Hz with six decimal digits, i.e., with a resolution of Microhertz. The last five digits cannot be reliably read via the serial interface. Example: Set the frequency of channel 1 to 1234.567890 Hz.
The command "RMF\n" returns this value: 00001234.502354
Let's treat the last five digits as an integer: 67890 - 2354 -> 65536. So it seems that the last five digits are calculated modulo 2**16. Why?
This oddity is present for all frequency parameters: Output frequency of channel 1 and 2, "secondary frequency" for FSK modulation, "Bias" of FM modulation
Offset voltage part 1: Reading the offset voltage.Positive values are properly returned as integers, giving the value in mV. Negative values are returned slightly different: For example, set an offset voltage of -3.816V, then issue the command to retrieve the offset voltage of channel 1, "RMO\n". The devices will respond with this number: 4294963480.
As you may guess, the returned value seems to come from an unsigned 32 bit integer: 2**32 - 4294963480 == 3816. Seems that the firmware is either missing a type cast somewhere or it has one type cast too many.
Offset voltage part 2: Setting the offset voltage.This works overall fine, with some exceptions: Certain values slightly below 8.192V, 4.096V, 2.048V, 1.024V, 0.512V and 0.256V cannot be set exactly via the serial interface, only via the front panel. Some example values: 8.19, 8.176, 4.095, 2.041, 1.022, 0.506, 0.251. To reproduce, issue a command like "WMO4.095\b". The front panel display will show the value 4.094, and the command "RMO" will return the same value.
Interestingly, not all values slightly below 2**N/1000 are affected. For example, the command "WMO8.191\n" works as expected.
Negative values are affected too, BTW.
For the real usage this quirk does not matter that much: The offset voltage is generated by an MCP4822E (U113 and U114 in the schematics
https://github.com/DerKammi/FY6600-15-30-50-60M/blob/master/Hardware/FY6600_Main_A0.pdf), which is a 12 bit DAC. So, setting the offset voltage in mV steps over the range -10V to +10V is anyway a bit silly, at least for larger offset and amplitude values, when the amplifiers U5/U21/U22 are activated.
Other parameters where the set value may be off by 1 in the last digit: Duty cycle, phase, phase shift in modulation mode.
Length of the "adjust pulse".This is special, even compared with the oddities described so far.
According to Feeltech's documentation, the value returned for the command "RSS\n":
"is a positive integer number {0…?} that represents the pulse period in ns. A return value of 10000 means the CH1 pulse period is 10000 nS."
(Source:
https://codeberg.org/jschwender/FY6600-15-30-50-60M/raw/branch/master/Software/FY6600%20Serial%20communication%20protocol%20v3.pdf )
If the value selected and displayed on the front panel is for example 10,000ns, the command "RSS\n" returns the string "100000", i.e. one digit too many. So what, I thought at first: Just throw that last digit away and keep the rest.
Let's try larger values, for example 100,000,000ns. The result: 1000000000. OK, again just one trailing zero too many. Same for 400,000,000ns and somewhat larger values. Now let's try 500,000,000ns. "RSS\n" returns:
705032704
Whut? 70,503,270.4 nanoseconds? If we simply ignore the last digit, we read 70503270ns, much less than what is selected and shown on the front panel. And, BTW, the signal that is generated, has indeed the timing as set on the front panel.
If an even larger value, 1,000,000,000ns, is set, "RSS\n" returns:
1410065408
If we simply throw the last digit away, we get 0.141006540 seconds. But the value shown on the display is one billion nanoseconds, or 1 second...
Now let's see what happens when we subtract the number 141006540 from the set time and select that number on the front panel:
1,000,000,000 - 141006540 == 858993460
Now the FY6x00 returns for "RSS\n" just one digit:
8
If we increment the value by 10, to 1,000,000,010 (smaller increments are not possible on the front panel), "RSS\n" returns:
108
So, the difference between the two values makes somehow sense. If we select 858993450 on the front panel, "RSS\n" returns:
4294967204
Notice already a pattern?
Let's go back to the first odd result: The selected value was 500,000,000, the returned value was 705032704. So, subtract the latter number, without the last digit, from the former:
500,000,000 - 70503270 == 429496730
That's half of the difference 1,000,000,000 - 141006540 (or 858993460).
After some more tests with other numbers I figured out what the algorithm must be that generates the data returned for an "RSS\n" query:
- Let the selected pulse width be N
- calculate the remainder R and the quotient Q of the integer division N / 429496730.
- Calculate the remainder of 4 * Q / 10. Let this be Q2. (In other words: Take the last digit of the decimal representation of (4 * Q), and call it Q2)
- if R is zero, return the one-digit number Q2 as an ASCII digit.
- if R is non-zero, return the decimal ASCII representation of R, followed by the ASCII representation of Q2. No separator symbol between the two numbers.
This is quite convoluted but it allows, after all, to read pulse length up to (429496730 * 5 - 1)ns, or 2,147,483,650ns (or 2**31 + 2). Problem is that the front panel allows to set values up to 4,000,000,000ns. But for pulse lengths above 429496730 * 5ns the value Q in the algorithm described above becomes 5 or larger, and the last digit of Q2 is again 0, 4, 8, 2, so the values returned for these pulse lengths are the same as for shorter pulse length.
So, while the caluculation of the quotient and remainder of the division N / 429496730 is crazy enough: If Q itself would be used in the response instead of Q2, an unambiguous "reconstruction" of the pulse length would be possible on the host side. So: Why the insane additional calculation of Q2? To me, this looks like a rase case of a weird WTF inside an big WTF.
And finally: All these crazy operations happen in an STM32, if my memory is right. I haven't worked with this type of microcontroller, but I think it is a safe bet to claim that the available C development tools provide reliably working functions like printf() or sprintf(). The pulse length can be easily represented as an unsigned 32 bit integer, so using a call like
printf("%d\n", pulse_length);
would be much easier than to develop a strange in-house implementation to convert an integer into an ASCII string.
What might have been the reason not to use printf() or sprintf()?
The front panel also shows a decimal representation of the pulse length. And the conversion between this decimal representation and, I assume, an unsigned 32 bit integer works just fine. So: Why is there another, convoluted and buggy, implemenration that generates the ASCII digits that are sent to the serial interface?
Finally a cautionary tale about cargo cult programming,Before I started to write my driver (
https://gitlab.com/adeuring/fy6x00), I looked a bit around in other libraries. One detail caught my attention: After reading this function (
https://github.com/mattwach/fygen/blob/e4822ec77939e1efd1117b722d3cfffc9b9f3e84/fygen.py#L305):
def send(self, command, retry_count=5):
"""Sends command, then waits for a response. Returns the response."""
data = command + '\n'
if self.is_serial:
data = data.encode()
self.port.reset_output_buffer()
self.port.reset_input_buffer()
self.port.write(data)
self.port.flush()
response = self._recv(command)
if self.is_serial and not response and retry_count > 0:
# sometime the siggen answers queries with nothing. Wait a bit and try
# again
time.sleep(0.1)
return self.send(command, retry_count - 1)
return response.strip()
and reading these lines from a VB file published by Feeltech:
Function RenYiBo_send(WData() As Double)
Dim WAVE_buf(16390) As Byte
[...]
Dim n As Integer
Dim m As Integer
Dim delay As Long
Dim strSend As String
[...]
MSComm1.OutBufferCount = 0
MSComm1.InBufferCount = 0
strSend = "DDS_WAVE" + Format(SAVE_SEL, "00") + Chr(&HA)
For n = 0 To 100
MSComm1.Output = strSend
delay = timeGetTime
While timeGetTime <= delay + 100
DoEvents
Wend
buf = MSComm1.Input
If Left(buf, 1) = "W" Then
Exit For
End If
Next n
I concluded that the FY6x00 obviously sometimes does not properly listens to commands send via the serial interface and that it makes sense to repeat a command when the FY6x00 does not respond within a certain time (0.1 seconds in the case of the VB script; 5 seconds in the case of the Python driver)
(Note about the source of the VB code: I found a zip file containing the code cited above sometime ago on Feeltech's fownload page:
http://en.feeltech.net/index.php?case=archive&act=list&catid=6 . It seems to be no longer be available.)
Repeating commands to the FY6x00 turned out to be a really bad idea, at least with a relatively short timeout. I chose 0.25 seconds after reading another function in Feeltech's VB sourcecode, which looked like it used this value. I did not notice at first that the entire repetition loop in that function was commented out... An unfortunate combination of sloppiness, bad eyesight and thus a hardly visible symbol that declares in Basic that the rest of a line is a comment (').
At first this seemed to work, though I noticed an oddity: When two different parameters were queried, the first response to second parameter query seemed to contain the value for the first query. I blamed - wrongly - the firmware and concluded that it is best to query each parameter twice and to keep only what looked like the response to the second query.
This worked more or less fine to read and change parameters like amplitude or frequency.
But I got stuck when I tried to upload a custom waveform. Whatever I tried, the first samples of the uploaded waveform were not what I intended to write. Instead I always saw an obscure glitch in the first five or six samples of the waveform.
It took me some time to remember that commands were repeated under certain circumstances – and this was indeed the cause of the problem: When the FY6x00 receives the command to upload the waveform ("DDS_WAVE07\n", for example) it responds with a "handshake" character (the symbol "W") – but only after a delay that was often longer than 0.25 seconds. So my driver repeated the command – and the FY6x00 treated this second command as the first bytes of the waveform data...
This means that Feeltech's VB code I quoted above is plain and simply wrong with its repetition of the "DDS_WAWE.." command after 0.1 seconds.
Since I finally removed the automatic repetition of issuing a command completely and set the timeout for responses to 1 second, just one oddity remained on this "low communication level": When a command to query a parameter is issued, the FY6x00 is supposed to respond with the parameter value, i.e., one or more ASCII digits, followed by an '\ņ'. But the FY6x00 sends quite often one or two '\n' symbols before the ASCII digits, so a driver should just ignore these empty lines and wait a bit for a line with "real data". A timeout of 1 second seems reasonable.
Conclusion: Don't trust source code from even from the manufacturer of a device blindly...
To be fair to the author of the Python example cited above: He uses his code with an FY2300. This device may very well need the repetition of commands. I don't own one, so I cannot tell one way or another.
Oh, if anbody is interested: My Python driver for the FY6600/FY6800 is here:
https://gitlab.com/adeuring/fy6x00