Tuesday, July 08, 2014

An Updated Memory Module : Part 2a - Hardware Changes, Demo Software

I know, I know … last time I said I'd post some very basic software soon. And, slackly, I haven't. Here's why…

The short version: after the first home-made prototype of the interface board I made a few changes to the circuit, re-designed the board, sent it out to have a small run professionally made, started work on schematic for the main board, ordered the parts for both, and planned to release a quick demo package after building a new interface board (rather than release my mess of spaghetti code and half-arsed functions as a 'demo' that'd need modification to work with the new design).

Most of that went swimmingly. But…

Installing and testing the interface board.
Of course there had to be a hold up. This time, it was parts availability - the TCA9535 chips and some of the connectors (as typical of Japanese radios, 2.5mm pitch rather than the usual 2.54mm / 0.1" pitch*) were on back order, and of course they turned up in the last two shipments. Then real-life got in the way a bit, and… well, that's where the last month or so has gone.

But now the final version of the interface board is built, tested, and working. The picture above shows just how small the board is - at 50x57mm it's the same size as the original cover plate on the back of the FRG-7700, and even uses the same mounting holes & rivets! The pic to the left shows how the radio's existing memory unit wiring plugs into the board (SV1 & SV2, which connect to the main board, are mounted on the component side).

Just for info, the main board is planned to be only slightly larger - the same width (50mm), and under 100mm long - and will piggyback mount on the interface board. The whole thing will be a fraction of the size of the original...

Interface Board v3 schematic

"Main" board demo schematic
The new schematic is shown below, along with a schematic for the Arduino, DDS module, and VFO buffer amp used for testing. The changes in this version of the interface board are fairly minor. For a start there's now proper supply bypassing for the ICs, as well as some RC debouncing for the M and MR pushbuttons.The interrupt pins from the port expanders are routed to the main board (and will be connected to the micro in the final version), so that the software can easily detect band or memory switch operation. Also, the 800Hz clock from the radio - ultimately derived from the radio's TCXO, and used in the original memory unit to gate the frequency counter - is passed through to the main board, where it'll be used in the final version to calibrate the frequency counter routine.

I'm still working on the main board - the real schematic (not the dodgy cobbled-together Arduino-and-DDS-module-on-a-breadboard shown here) is pretty much finalised, the PCB design is nearly done, and I'll probably be sending it off to be made later this week (OK, now that it's Wednesday that may be a bit optimistic - let's say early next week). That means I should have the boards in my hand and ready to start building one in around two two to three weeks time (fingers crossed!).

(edit, 17/07: ordered the boards on Monday. Haven't heard from iTead since they received the gerbers, which is generally a good sign [means the gerbers passed their factory audit OK]. Hopefully I'll hear from them that the boards are in the mail soon...)

That leaves the software. At the moment it's necessarily in a very bare-bones state, since much of it is dependent on the main board. The basic functions needed to prove that the idea can work have been written, and indeed do work - albeit as a very basic memory unit with no fine tune and a single memory position that is cleared when the radio is turned off…

(Truth be told, it wouldn't be hard to tidy the software up a little more so that the Arduino-and-breadboard circuit here recreates all the functions of the original MU-7700 - except for fine tune and the need for batteries. It's just that I've got some extra features in mind, and how well they work out in practice will determine exactly how the storage and retrieval routines are structured/written.)

Anyway, the current Arduino software - and I emphasise again that it's a very rudimentary, bare-bones, proof-of-concept, collection of functions - can be downloaded here, or you can read through it down below. You'll also need to download and install a couple of libraries - my AD9850_Serial library (Google Drive link) and Martin Nawrath's FreqCounter library (website link).

Note: I use OSX's Xcode IDE with embedXcode for programming, which inserts/requires a load of extraneous stuff at the top of the source. If you're using the Arduino's so-called IDE**, don't worry about all the extraneous code-sense & function declarations in the first 40 lines or so - the compiler ignores them and the source code compiles as-is without any changes necessary.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
// FRG-7700 Memory Module Demo
//
// Software for Arduino-based FRG-7700 memory module
// Developed with [embedXcode](http://embedXcode.weebly.com)
// 
// Author       ThinkingBrainDog
//              the7700.blogspot.com
//
// Date         8/07/14 2:00 PM
// Version      <#version#>
// 
// Copyright    © ThinkingBrainDog, 2014
// License      <#license#>
//
// See          ReadMe.txt for references
//

// Core library for code-sense
#if defined(WIRING) // Wiring specific
#include "Wiring.h"
#elif defined(MAPLE_IDE) // Maple specific
#include "WProgram.h"   
#elif defined(MPIDE) // chipKIT specific
#include "WProgram.h"
#elif defined(DIGISPARK) // Digispark specific
#include "Arduino.h"
#elif defined(ENERGIA) // LaunchPad MSP430, Stellaris and Tiva, Experimeter Board FR5739 specific
#include "Energia.h"
#elif defined(CORE_TEENSY) // Teensy specific
#include "WProgram.h"
#elif defined(ARDUINO) && (ARDUINO >= 100) // Arduino 1.0 and 1.5 specific
#include "Arduino.h"
#elif defined(ARDUINO) && (ARDUINO < 100) // Arduino 23 specific
#include "WProgram.h"
#else // error
#error Platform not defined
#endif

// Include application, user and local libraries
#include <Wire.h>
#include <AD9850_Serial.h>
#include <FreqCounter.h>

// Function declarations
// Guess - or go read the functions themselves - for details ;)
void    FRG7700_read();
byte    FRG7700_read_memsw();
void    FRG7700_read_membuttons();
void    FRG7700_write(byte band, byte VCO, byte ant);
void    TCA9535_init(int addr);
byte    TCA9535_read(int addr, byte port);
void    TCA9535_write(int addr, byte port0_write, byte port1_write);
long    Get_VFO_Freq();

// Defines, constants, & variables
//
// DEFINES
// Port expander addresses & I/O config masks
// First port expander
#define TCA9535_1_Addr          0x20        // Address
#define TCA9535_1_Port0_Conf    B00000000   // All pins output
#define TCA9535_1_Port1_Conf    B11100000   // Pins 0-4 output, 5-7 input
// Second port expander
#define TCA9535_2_Addr          0x21        // Address
#define TCA9535_2_Port0_Conf    B11111111   // All pins input
#define TCA9535_2_Port1_Conf    B11111111   // All pins input
// Bitmasks
// Used to extract relevant bits from the byte read
#define AntFilt_bitmask         B11100000   // Top 3 bits of port
#define Band_bitmask            B00111111   // Bottom 6 bits of port
#define VCO_bitmask             B11000000   // Top 2 bits of port
// Anything else
// Memory button pins
#define MEM                     2           // "M" switch connected to pin 2
#define MEM_R                   4           // "MR" switch connected to pin 4
//
// CONSTANTS
// Lookup tables:
// Memory # switch (12 position): [switch position - 1] = {read byte}
const   byte    memory_in_map[] = {1,2,4,8,16,32,64,128,129,65,33,17};
const   byte    memory_in_map_len = 12;
// Bandswitch inbound (0-29MHz): [band] = {read byte}
const   byte    band_in_map[] = {4,5,6,7,8,9,16,17,18,19,
                20,21,22,23,24,25,32,33,34,35,
                36,37,38,39,40,41,48,49,50,51};
const   byte    band_in_map_len = 30;
// Bandswitch outbound (0-29MHz): [band] = {write byte}
// Bits 1 & 6 are 0 to allow interleaving/OR'ing with the outbound VCO data
const   byte    band_out_map[] =
                {B10000000,B10100000,B10000001,B10100001,B00001000,B00101000,
                B00000100,B00100100,B00000101,B00100101,B10000100,B10100100,
                B10000101,B10100101,B00001100,B00101100,B00010000,B00110000,
                B00010001,B00110001,B10010000,B10110000,B10010001,B10110001,
                B00011000,B00111000,B00010100,B00110100,B00010101,B00110101};
const   byte    band_out_map_len = 30;
// VCO outbound (0-3): [vco setting] = {write byte}
// Only bits 1 & 6 are used, OR'd with the bandswitch outbound value
const   byte    VCO_out_map[] = {B00000000,B01000000,B00000010,B01000010};
const   byte    VCO_out_map_len = 4;
// Antenna filter outbound (0-6): [antfilter] = {write bits}
// Note: position "5" is not found in the incoming data; B000 used there...
const   byte    antfilt_out_map[] = {B000,B001,B100,B101,B010,B000,B110};
const   byte    antfilt_out_map_len = 7;
//
// Miscellaneous constants:
// Frequency compensation factor for Get_VFO_Freq (is slightly Arduino/crystal
// dependent - final design will allow calibration off 800Hz signal).
const   byte    freq_comp = 3;
//
// VARIABLES
byte    read_port0 = 0;                     // default value of port0
byte    read_port1 = 0;                     // default value of port1
byte    read_memsw = 0;                     // default value of memory switch
byte    read_bandsw = 0;                    // default value of bandswitch
byte    read_VCO = 0;                       // default value of VCO
byte    read_antfilter = 0;                 // default value of antenna filter
long    read_VFO = 0;                       // default value of VFO frequency
boolean button_m = false;                   // default "M" button state
boolean button_mr = false;                  // default "MR" button state
boolean m_flag = false;                     // previous button state flag
boolean mr_flag = false;                    // previous button state flag
// Temp vars for storing a single setting in memory
byte    s_mem_band = 0;
byte    s_mem_vco = 0;
byte    s_mem_antfilt = 0;
long    s_mem_vfo = 0;
//
// ANYTHING ELSE
// Create a DDS object (pins are CLK, FQ_UD, DATA, RESET)
// Currently, for the demo, we use a cheap AD9850 DDS module.
AD9850_Serial dds(8,9,10,11);


// ***** Program proper starts here

// Setup
void setup(){
    // Start I2C comms
    Wire.begin();
    // Initialise the TCA9535 port expanders
    TCA9535_init(TCA9535_1_Addr);
    TCA9535_init(TCA9535_2_Addr);
    // Initialise the memory button (M & MR) pins (inputs, internal pullups on)
    pinMode(MEM, INPUT_PULLUP);
    pinMode(MEM_R, INPUT_PULLUP);
    // Prime/clear the freq. counter (discard first reading after start)
    Get_VFO_Freq();
    // Initialise/reset the DDS
    dds.reset();
    // And turn it off (it defaults to on at powerup/reset)
    dds.off();
    // DEBUG
    // Set up serial port for development / debugging
    //
    Serial.begin(9600);
    Serial.print("FRG-7700 Memory Unit. Waiting...");
}

// Main loop
void loop(){
    // Read the front panel "M" & "MR" buttons
    FRG7700_read_membuttons();
    // Handle "M" button press
    if (button_m != m_flag){
        if (button_m == true){
            // Do something if button has been PRESSED.
            Serial.println("M button pressed. Reading settings...");
            // Read & display radio settings
            FRG7700_read();
            Serial.print("Band="); Serial.print(read_bandsw);
            s_mem_band = read_bandsw;
            Serial.print(", VCO="); Serial.print(read_VCO);
            s_mem_vco = read_VCO;
            Serial.print(", Filter="); Serial.print(read_antfilter);
            s_mem_antfilt = read_antfilter;
            Serial.print(", VFO="); s_mem_vfo = Get_VFO_Freq();
            Serial.print(s_mem_vfo); Serial.println("Hz");
            Serial.println("Settings stored in memory\n");
        } else {
            // Do something if button has been RELEASED.
        }
    }
    m_flag = button_m;
    // Handle "MR" button press
    if (button_mr != mr_flag){
        if (button_mr == true){
            // Do something if button has been PRESSED.
            Serial.println("MR button pressed. Controlling radio...");
            // Write settings to radio
            FRG7700_write(s_mem_band, s_mem_vco, s_mem_antfilt);
            // Set memory unit VFO (DDS module) to stored frequency
            dds.setfreq(s_mem_vfo);
            // Turn on the DDS output
            dds.on();
        } else {
            // Do something if button has been RELEASED.
            Serial.println("MR button released. Returning control...\n");
            // Turn off the memory unit VFO
            dds.off();
            // Re-initialise TCA9535s (sets all I/O pins to input/hi-z)
            TCA9535_init(TCA9535_1_Addr);
            TCA9535_init(TCA9535_2_Addr);
        }
    }
    mr_flag = button_mr;
}

// ***** Functions-B-Here

void FRG7700_read(){
    // Read radio settings - band, VCO, & antenna settings, &
    // VFO frequency (memory switch is read separately by FRG7700_read_memsw)
    // Modifies:
    //      read_bandsw     : bandswitch position (remapped to 0-29MHz)
    //      read_VCO        : VCO setting (shifted to 0-3)
    //      read_antfilter  : antenna filter setting (shifted to 0-6)
    //      read_VFO        : VFO frequency (approx. 2.5-3.5MHz; 5Hz resolution)
    //
    // Locals
    byte tmp, port;

    // Get bandswitch & VCO data
    // Band = TCA9535 #2, port 1 bits 0-5
    // VCO = TCA9535 #2, port 1 bits 6-7
    port = TCA9535_read(TCA9535_2_Addr,1);  // returns byte

    // Remap bandswitch to 0-29MHz
    for (tmp=0; tmp<band_in_map_len; tmp++){
        if (band_in_map[tmp] == (port & Band_bitmask)){
            read_bandsw = tmp;
        }
    }
    // Mask & shift VCO setting to values 0-3
    read_VCO = (port & VCO_bitmask) >> 6;

    // Get antenna filter data
    // Antenna filter = TCA9535 #1, port1 bits 5-7
    port = TCA9535_read(TCA9535_1_Addr,1);
    // Mask & shift antenna filter setting to values 0-6
    read_antfilter = (port & AntFilt_bitmask) >> 5;

    // Get VFO frequency
    read_VFO = Get_VFO_Freq();

    // Everything's done; go home now...
}

byte FRG7700_read_memsw(){
    // Read memory switch
    // Returns byte containing remapped memory switch position (0-11)
    //
    // Locals
    byte tmp, port, result;

    // Get memory switch data
    port = TCA9535_read(TCA9535_2_Addr,0);
    // Remap memory switch to 0-11
    for (tmp=0; tmp<memory_in_map_len; tmp++){
        if (memory_in_map[tmp] == port){
            result = tmp;
        }
    }
    return result;
}

void FRG7700_read_membuttons(){
    // Check the M & MR buttons; set Booleans button_m & button_mr
    // as appropriate.
    // Note: MR cuts the throat of M - both can't be true at the same time,
    // and MR always wins.
    // Modifies:
    //      button_m        : front panel M button (TRUE or FALSE)
    //      button_mr       : front panel MR button (TRUE or FALSE)
    if (digitalRead(MEM) == LOW){
        button_m = true;
    } else {
        button_m = false;
    }
    if (digitalRead(MEM_R) == LOW) {
        button_mr = true;
    } else {
        button_mr = false;
    }
}

void FRG7700_write(byte band, byte VCO, byte ant){
    // Write the info to the FRG-7700 through TCA9535_1
    // Port 0 (bits 0-7) = band & VCO, Port 1 (bits 2-4) = filter
    //
    // Locals
    byte band_vco_out = (band_out_map[band] | VCO_out_map[VCO]);
    byte filter_out = (antfilt_out_map[ant] << 2);

    // Hmmmm... will have to think about the best way to do this!
    Wire.beginTransmission(TCA9535_1_Addr);
    Wire.write(6);                          // Configure Port0/1
    Wire.write(TCA9535_1_Port0_Conf);
    Wire.write(TCA9535_1_Port1_Conf);
    Wire.endTransmission();

    TCA9535_write(TCA9535_1_Addr, band_vco_out, filter_out);
}

void TCA9535_init(int addr){
    // Initialise a TCA9535 to all pins as input (Hi-Z).
    //
    // Note: Wire.begin needs to have been called already e.g. in setup().
    // The Wire library doesn't actually send anything until
    // Wire.endTransmission() is called...
    Wire.beginTransmission(addr);
    Wire.write(6);                          // Port 0 configuration register
    Wire.write(0xFF);                       // Configure Port 0, all bits INPUT
    Wire.write(0xFF);                       // Configure Port 1, all bits INPUT
    Wire.endTransmission();                 // Write config to wire
}

byte TCA9535_read(int addr, byte port){
    // Read from the specified TCA9535 the specified port (0 or 1)
    // Returns: byte of data
    //
    // A bit of trivia / something to remember: although the I2C address "addr"
    // is a byte (at least as far as we're concerned), Wire.requestFrom()
    // expects an int. Strangely, Wire.beginTransmission etc. don't...
    //
    // Locals
    byte retval;

    port = constrain(port, 0, 1);           // Probably overkill...

    Wire.beginTransmission(addr);           // Start talking to device
    Wire.write(port);                       // Port to read (0 or 1)
    Wire.endTransmission();
    Wire.requestFrom(addr,1);               // Request 1 byte
    if (Wire.available()){
        retval = Wire.read();               // Wait until we have it...
    }
    Wire.endTransmission();                 // Stop talking to device
    return retval;                          // Return byte read
}

void TCA9535_write(int addr, byte port0_write, byte port1_write){
    // Write output pins of the specified TCA9535
    Wire.beginTransmission(addr);           // Start talking to device
    Wire.write(2);                          // Start write @ port 0
    Wire.write(port0_write);                // Write
    Wire.write(port1_write);
    Wire.endTransmission();                 // Stop talking to device
}

long Get_VFO_Freq(){
    // Get VFO frequency (in Hz)
    // Returns: long int containing frequency in Hz (5Hz resolution)
    //
    // Colour me surprised - this is actually quite stable/accurate when
    // calibrated; compared to my Tektronix counter it's within +-7Hz or so!
    //
    // Locals
    long frq;

    FreqCounter::f_comp = freq_comp;        // Compensation = 3 works for me
    FreqCounter::start(200);                // Start count; gatetime of 200ms
    while (FreqCounter::f_ready == 0){      // Wait until counter ready
        frq = (FreqCounter::f_freq*5);
    }
    return frq;                             // Return result in Hz
}

A quick run-through:
  • Lines 58-66 define the addresses of the two TCA9535 I/O chips on the interface board, and bitmasks used for configuring the pins as inputs or outputs. These are only used when actually driving the radio - to avoid the memory unit intefering during normal operation, when the MR button is out and front panel controls are operational all pins (input and output) are configured as high-impedance inputs.
  • Lines 68-71 define bitmasks used for individually extracting the antenna filter, band, and VCO setting data. Only the memory switch occupies a full 8-bit port of one TCA9535 - the band switch (6 bits) and VCO setting (2 bits) are together on one 8-bit port, and the antenna filter occupies 3 bits on another 8-bit port. To separate them, we simply logical-AND the bitmask with the value from the port.
  • Lines 78-103 define various lookup tables for converting memory switch, band switch, and VCO data to and from "human logical" values. When reading each setting the bits are in one (mixed up) order; when writing each setting they need to go out in a different (but still mixed-up!) order. To save my sanity, the lookup tables are used to convert them to & from human readable values - e.g 1-12 for the memory switch, 0-29 for the band switch, etc.
  • Lines 111-126 sets up some global variables used throughout the program with default values.
  • Line 131 is where we create an object, named 'dds', using the AD9850_Serial library. This represents the AD9850 DDS module connected to pins 8-11 that serves as the memory unit's VFO.
  • Setup happens in lines 137-157 - I2C communications are started (line 139), the TCA9535s are initialised (lines 141-142), the Arduino pins used to read the "M" and "MR" buttons are configured (lines 144-145), the frequency counter routine is run once to initialise/reset it (line 147), the DDS module is initialised/reset and turned off (lines 149-151), and the serial port (used for debugging) is started (lines 155-156).
After that, the software simply runs through the main() loop (lines 159-206), continuously reading the front-panel "M" and "MR" buttons (line 163). In that loop:
  • If the "M" button is pressed a message is sent to the serial port (line 167), the 'FRG7700_read' function is called to read the radio settings (line 169), and the 'Get_VFO_Freq' function is called to count the radio's VFO (front panel tuning knob) frequency (line 176). At the same time, the radio settings and VFO frequency are printed to the serial port and stored in variables (s_mem_band, s_mem_vco, etc).
  • When the "MR" button is first pressed (it's a latching button), another message is sent to the serial port (line 188). The stored memory settings are then sent to the radio (line 190), and the DDS is programmed to produce the stored VFO frequency (line 192) and its output turned on (line 194).
  • When the "MR" button is later pressed to un-latch it, a message indicating this is sent to the serial port (line 197). The DDS is then turned off (line 199), and the TCA9535s re-initialised to set all pins to high impedance inputs (lines 201-202) which returns control to the front panel.
And that's it! The various functions lower down (lines 208-366) do all the actual heavy lifting, but you can read them on your own (they're pretty self-explanatory, I hope…)

(I'm also sure you can find many improvements. Me too. Remember again that this is a collection of code fragments meant for testing and proving individual functions, not final polished software!)



Now that I've written all that, it brings up the issue of how I want to release the whole project when it's done. To be honest, I'm … unsure. My feelings boil down to three things:
  1. I want people to learn by doing, not just buy a box and plug it in - though I realise that's not an option for some people.
  2. I would love to see people take my all my investigation & development work, circuit design, and software, and improve it, but
  3. I would hate to see my exact design ripped off by someone else just so they could make money from it.
I'm also conflicted by my personal belief in BSD-style licencing over the GPL, since I feel that giving with attached strings and expectations - as the GPL & similar 'copyleft' licences do by design - is manipulative and not really in the spirit of 'giving'…

The plan at the moment is to release the circuit schematics, parts list, and base software (with all the functionality of the original MU-7700 memory unit) freely under a BSD licence, but hang on to my board designs and any enhanced software. Anyone is free to modify the design, make their own boards, and improve the software, and I'll possibly have boards/kits/finished units to sell.

So … does that seem fair?


(* Oh, yeah, did I mention that? All the memory unit connectors (and most of the others) in the radio are metric, not imperial. "So what?" you might say, "yeah, imperial is big in the electronics world, but metric is pretty common amongst connectors".

To which I would say "go on, find the right connectors". Whatever the original ones were (the connector part numbers listed in the service manual couldn't be found) - and they're a pretty basic pin-header type, just in metric - they don't seem to be made anymore. The closest I could find was some Multicomp (Element14's in-house brand) headers (e.g.). While not exactly right, those are a near-perfect fit - but not available in all sizes, and are being discontinued. I ended up using mostly Molex 22-04-1xx1 (where 'xx' is the number of pins) connectors, which are a slightly less-perfect fit since the pins are ~2mm too long…)


(** Really, guys - if your aim is to get people interested in programming, then maybe something remotely approaching a decent IDE is in order? That … thing … you provide which claims to be an IDE is terrible. Beginners would almost be better off using Notepad or Textedit…)

No comments:

Post a Comment