-
Notifications
You must be signed in to change notification settings - Fork 57
/
bitx40.ino
632 lines (553 loc) · 21.4 KB
/
bitx40.ino
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
/**
* This source file is under General Public License version 3.
*
* Most source code are meant to be understood by the compilers and the computers.
* Code that has to be hackable needs to be well understood and properly documented.
* Donald Knuth coined the term Literate Programming to indicate code that is written be
* easily read and understood.
*
* The Raduino is a small board that includes the Arduin Nano, a 16x2 LCD display and
* an Si5351a frequency synthesizer. This board is manufactured by Paradigm Ecomm Pvt Ltd
*
* To learn more about Arduino you may visit www.arduino.cc.
*
* The Arduino works by firt executing the code in a function called setup() and then it
* repeatedly keeps calling loop() forever. All the initialization code is kept in setup()
* and code to continuously sense the tuning knob, the function button, transmit/receive,
* etc is all in the loop() function. If you wish to study the code top down, then scroll
* to the bottom of this file and read your way up.
*
* Below are the libraries to be included for building the Raduino
*
* The EEPROM library is used to store settings like the frequency memory, caliberation data,
* callsign etc .
*/
#include <EEPROM.h>
/**
* The main chip which generates upto three oscillators of various frequencies in the
* Raduino is the Si5351a. To learn more about Si5351a you can download the datasheet
* from www.silabs.com although, strictly speaking it is not a requirment to understand this code.
* Instead, you can look up the Si5351 library written by Jason Mildrum, NT7S. You can download and
* install it from https://github.com/etherkit/Si5351Arduino to complile this file.
* The Wire.h library is used to talk to the Si5351 and we also declare an instance of
* Si5351 object to control the clocks.
*/
#include <Wire.h>
#include <si5351.h>
Si5351 si5351;
/**
* The Raduino board is the size of a standard 16x2 LCD panel. It has three connectors:
*
* First, is an 8 pin connector that provides +5v, GND and six analog input pins that can also be
* configured to be used as digital input or output pins. These are referred to as A0,A1,A2,
* A3,A6 and A7 pins. The A4 and A5 pins are missing from this connector as they are used to
* talk to the Si5351 over I2C protocol.
*
* A0 A1 A2 A3 +5v GND A6 A7
* BLACK BROWN RED ORANGE YELLOW GREEN BLUE VIOLET
*
* Second is a 16 pin LCD connector. This connector is meant specifically for the standard 16x2
* LCD display in 4 bit mode. The 4 bit mode requires 4 data lines and two control lines to work:
* Lines used are : RESET, ENABLE, D4, D5, D6, D7
* We include the library and declare the configuration of the LCD panel too
*/
#include <LiquidCrystal.h>
LiquidCrystal lcd(8,9,10,11,12,13);
/**
* The Arduino, unlike C/C++ on a regular computer with gigabytes of RAM, has very little memory.
* We have to be very careful with variables that are declared inside the functions as they are
* created in a memory region called the stack. The stack has just a few bytes of space on the Arduino
* if you declare large strings inside functions, they can easily exceed the capacity of the stack
* and mess up your programs.
* We circumvent this by declaring a few global buffers as kitchen counters where we can
* slice and dice our strings. These strings are mostly used to control the display or handle
* the input and output from the USB port. We must keep a count of the bytes used while reading
* the serial port as we can easily run out of buffer space. This is done in the serial_in_count variable.
*/
char serial_in[32], c[30], b[30], printBuff[32];
int count = 0;
unsigned char serial_in_count = 0;
/**
* We need to carefully pick assignment of pin for various purposes.
* There are two sets of completely programmable pins on the Raduino.
* First, on the top of the board, in line with the LCD connector is an 8-pin connector
* that is largely meant for analog inputs and front-panel control. It has a regulated 5v output,
* ground and six pins. Each of these six pins can be individually programmed
* either as an analog input, a digital input or a digital output.
* The pins are assigned as follows:
* A0, A1, A2, A3, +5v, GND, A6, A7
* BLACK BROWN RED ORANGE YELLW GREEN BLUEVIOLET
* (while holding the board up so that back of the board faces you)
*
* Though, this can be assigned anyway, for this application of the Arduino, we will make the following
* assignment
* A2
is connected to a push button that can momentarily ground this line. This will be used to switch between different modes, etc.
* A6 is to implement a keyer, it is reserved and not yet implemented
* A7 is connected to a center pin of good quality 100K or 10K linear potentiometer with the two other ends connected to
* ground and +5v lines available on the connector. This implments the tuning mechanism
*/
#define ANALOG_KEYER (A1)
#define CAL_BUTTON (A2)
#define FBUTTON (A3)
#define PTT (A6)
#define ANALOG_TUNING (A7)
/**
* The second set of 16 pins on the bottom connector are have the three clock outputs and the digital lines to control the rig.
* This assignment is as follows :
* Pin 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
* +5V +5V CLK0 GND GND CLK1 GND GND CLK2 GND D2 D3 D4 D5 D6 D7
* These too are flexible with what you may do with them, for the Raduino, we use them to :
* - TX_RX line : Switches between Transmit and Receive after sensing the PTT or the morse keyer
* - CW_KEY line : turns on the carrier for CW
* These are not used at the moment.
*/
#define TX_RX (7)
#define CW_TONE (6)
#define CW_KEY (5)
#define TX_LPF_SEL (4)
/**
* The raduino has a number of timing parameters, all specified in milliseconds
* CW_TIMEOUT : how many milliseconds between consecutive keyup and keydowns before switching back to receive?
* The next set of three parameters determine what is a tap, a double tap and a hold time for the funciton button
* TAP_DOWN_MILLIS : upper limit of how long a tap can be to be considered as a button_tap
* TAP_UP_MILLIS : upper limit of how long a gap can be between two taps of a button_double_tap
* TAP_HOLD_MILIS : many milliseconds of the buttonb being down before considering it to be a button_hold
*/
#define TAP_UP_MILLIS (500)
#define TAP_DOWN_MILLIS (600)
#define TAP_HOLD_MILLIS (2000)
#define CW_TIMEOUT (600l) // in milliseconds, this is the parameter that determines how long the tx will hold between cw key downs
/**
* The Raduino supports two VFOs : A and B and receiver incremental tuning (RIT).
* we define a variables to hold the frequency of the two VFOs, RITs
* the rit offset as well as status of the RIT
*
* To use this facility, wire up push button on A3 line of the control connector
*/
#define VFO_A 0
#define VFO_B 1
char ritOn = 0;
char vfoActive = VFO_A;
unsigned long vfoA=7100000L, vfoB=14200000L, ritA, ritB, sideTone=800;
/**
* Raduino needs to keep track of current state of the transceiver. These are a few variables that do it
*/
char inTx = 0;
char keyDown = 0;
char isUSB = 0;
unsigned long cwTimeout = 0;
unsigned char txFilter = 0;
/** Tuning Mechanism of the Raduino
* We use a linear pot that has two ends connected to +5 and the ground. the middle wiper
* is connected to ANALOG_TUNNING pin. Depending upon the position of the wiper, the
* reading can be anywhere from 0 to 1024.
* The tuning control works in steps of 50Hz each for every increment between 50 and 950.
* Hence the turning the pot fully from one end to the other will cover 50 x 900 = 45 KHz.
* At the two ends, that is, the tuning starts slowly stepping up or down in 10 KHz steps
* To stop the scanning the pot is moved back from the edge.
* To rapidly change from one band to another, you press the function button and then
* move the tuning pot. Now, instead of 50 Hz, the tuning is in steps of 50 KHz allowing you
* rapidly use it like a 'bandset' control.
* To implement this, we fix a 'base frequency' to which we add the offset that the pot
* points to. We also store the previous position to know if we need to wake up and change
* the frequency.
*/
#define INIT_BFO_FREQ (1199800L)
unsigned long baseTune = 7100000L;
unsigned long bfo_freq = 11998000L;
int old_knob = 0;
#define CW_OFFSET (800l)
#define LOWEST_FREQ (6995000l)
#define HIGHEST_FREQ (7500000l)
long frequency, stepSize=100000;
/**
* The raduino can be booted into multiple modes:
* MODE_NORMAL : works the radio normally
* MODE_CALIBRATION : used to calibrate Raduino.
* To enter this mode, hold the function button down and power up. Tune to exactly 10 MHz on clock0 and release the function button
*/
#define MODE_NORMAL (0)
#define MODE_CALIBRATE (1)
char mode = MODE_NORMAL;
/**
* Display Routines
* These two display routines print a line of characters to the upper and lower lines of the 16x2 display
*/
void printLine1(char *c){
if (strcmp(c, printBuff)){
lcd.setCursor(0, 0);
lcd.print(c);
strcpy(printBuff, c);
count++;
}
}
void printLine2(char *c){
lcd.setCursor(0, 1);
lcd.print(c);
}
/**
* Building upon the previous two functions,
* update Display paints the first line as per current state of the radio
*
* At present, we are not using the second line. YOu could add a CW decoder or SWR/Signal strength
* indicator
*/
void updateDisplay(){
sprintf(b, "%08ld", frequency);
sprintf(c, "%s:%.2s.%.4s", vfoActive == VFO_A ? "A" : "B" , b, b+2);
if (isUSB)
strcat(c, " USB");
else
strcat(c, " LSB");
if (inTx)
strcat(c, " TX");
else if (ritOn)
strcat(c, " +R");
else
strcat(c, " ");
printLine1(c);
}
/**
* To use calibration sets the accurate readout of the tuned frequency
* To calibrate, follow these steps:
* 1. Tune in a signal that is at a known frequency.
* 2. Now, set the display to show the correct frequency,
* the signal will no longer be tuned up properly
* 3. Press the CAL_BUTTON line to the ground
* 4. tune in the signal until it sounds proper.
* 5. Release CAL_BUTTON
* In step 4, when we say 'sounds proper' then, for a CW signal/carrier it means zero-beat
* and for SSB it is the most natural sounding setting.
*
* Calibration is an offset that is added to the VFO frequency by the Si5351 library.
* We store it in the EEPROM's first four bytes and read it in setup() when the Radiuno is powered up
*/
void calibrate(){
int32_t cal;
// The tuning knob gives readings from 0 to 1000
// Each step is taken as 10 Hz and the mid setting of the knob is taken as zero
cal = (analogRead(ANALOG_TUNING) - 500) * 500ULL;
// if the button is released, we save the setting
// and delay anything else by 5 seconds to debounce the CAL_BUTTON
// Debounce : it is the rapid on/off signals that happen on a mechanical switch
// when you change it's state
if (digitalRead(CAL_BUTTON) == HIGH){
mode = MODE_NORMAL;
EEPROM.put(0,cal);
printLine2("Calibrated ");
delay(5000);
}
else {
// while the calibration is in progress (CAL_BUTTON is held down), keep tweaking the
// frequency as read out by the knob, display the chnage in the second line
si5351.set_correction(cal);
si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLA);
si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLB);
setFrequency(frequency);
updateDisplay();
sprintf(c, "%08ld ", cal);
printLine2(c);
}
}
/**
* The setFrequency is a little tricky routine, it works differently for USB and LSB
*
* The BITX BFO is permanently set to lower sideband, (that is, the crystal frequency
* is on the higher side slope of the crystal filter).
*
* LSB: The VFO frequency is subtracted from the BFO. Suppose the BFO is set to exactly 12 MHz
* and the VFO is at 5 MHz. The output will be at 12.000 - 5.000 = 7.000 MHz
*
* USB: The BFO is subtracted from the VFO. Makes the LSB signal of the BITX come out as USB!!
* Here is how it will work:
* Consider that you want to transmit on 14.000 MHz and you have the BFO at 12.000 MHz. We set
* the VFO to 26.000 MHz. Hence, 26.000 - 12.000 = 14.000 MHz. Now, consider you are whistling a tone
* of 1 KHz. As the BITX BFO is set to produce LSB, the output from the crystal filter will be 11.999 MHz.
* With the VFO still at 26.000, the 14 Mhz output will now be 26.000 - 11.999 = 14.001, hence, as the
* frequencies of your voice go down at the IF, the RF frequencies will go up!
*
* Thus, setting the VFO on either side of the BFO will flip between the USB and LSB signals.
*/
void setFrequency(unsigned long f){
uint64_t osc_f;
if (isUSB){
si5351.set_freq((bfo_freq + f) * 100ULL, SI5351_CLK2);
}
else{
si5351.set_freq((bfo_freq - f) * 100ULL, SI5351_CLK2);
}
frequency = f;
}
/**
* The Checkt TX toggles the T/R line. The current BITX wiring up doesn't use this
* but if you would like to make use of RIT, etc, You must wireup an NPN transistor to to the PTT line
* as follows :
* emitter to ground,
* base to TX_RX line through a 4.7K resistr(as defined at the top of this source file)
* collecter to the PTT line
* Now, connect the PTT to the control connector's PTT line (see the definitions on the top)
*
* Yeah, surprise! we have CW supported on the Raduino
*/
void checkTX(){
// We don't check for ptt when transmitting cw
// as long as the cwTimeout is non-zero, we will continue to hold the
// radio in transmit mode
if (cwTimeout > 0)
return;
if (digitalRead(PTT) == 0 && inTx == 0){
inTx = 1;
digitalWrite(TX_RX, 1);
updateDisplay();
}
if (digitalRead(PTT) == 1 && inTx == 1){
inTx = 0;
digitalWrite(TX_RX, 0);
updateDisplay();
}
}
/* CW is generated by injecting the side-tone into the mic line.
* Watch http://bitxhacks.blogspot.com for the CW hack
* nonzero cwTimeout denotes that we are in cw transmit mode.
*
* This function is called repeatedly from the main loop() hence, it branches
* each time to do a different thing
*
* There are three variables that track the CW mode
* inTX : true when the radio is in transmit mode
* keyDown : true when the CW is keyed down, you maybe in transmit mode (inTX true)
* and yet between dots and dashes and hence keyDown could be true or false
* cwTimeout: Figures out how long to wait between dots and dashes before putting
* the radio back in receive mode
*/
void checkCW(){
if (keyDown == 0 && analogRead(ANALOG_KEYER) < 50){
//switch to transmit mode if we are not already in it
if (inTx == 0){
digitalWrite(TX_RX, 1);
//give the relays a few ms to settle the T/R relays
delay(50);
}
inTx = 1;
keyDown = 1;
tone(CW_TONE, sideTone);
updateDisplay();
}
//reset the timer as long as the key is down
if (keyDown == 1){
cwTimeout = CW_TIMEOUT + millis();
}
//if we have a keyup
if (keyDown == 1 && analogRead(ANALOG_KEYER) > 150){
keyDown = 0;
noTone(CW_TONE);
cwTimeout = millis() + CW_TIMEOUT;
}
//if we are in cw-mode and have a keyuup for a longish time
if (cwTimeout > 0 && inTx == 1 && cwTimeout < millis()){
//move the radio back to receive
digitalWrite(TX_RX, 0);
inTx = 0;
cwTimeout = 0;
updateDisplay();
}
}
/**
* A trivial function to wrap around the function button
*/
int btnDown(){
if (digitalRead(FBUTTON) == HIGH)
return 0;
else
return 1;
}
/**
* A click on the function button toggles the RIT
* A double click switches between A and B vfos
* A long press copies both the VFOs to the same frequency
*/
void checkButton(){
int i, t1, t2, knob, new_knob, duration;
//rest of this function is interesting only if the button is pressed
if (!btnDown())
return;
// we are at this point because we detected that the button was indeed pressed!
// we wait for a while and confirm it again so we can be sure it is not some noise
delay(50);
if (!btnDown())
return;
// note the time of the button going down and where the tuning knob was
t1 = millis();
knob = analogRead(ANALOG_TUNING);
duration = 0;
// if you move the tuning knob within 3 seconds (3000 milliseconds) of pushing the button down
// then consider it to be a coarse tuning where you you can move by 100 Khz in each step
// This is useful only for multiband operation.
while (btnDown() && duration < 3000){
new_knob = analogRead(ANALOG_TUNING);
//has the tuninng knob moved while the button was down from its initial position?
if (abs(new_knob - knob) > 10){
int count = 0;
/* track the tuning and return */
while (btnDown()){
frequency = baseTune = ((analogRead(ANALOG_TUNING) * 30000l) + 1000000l);
setFrequency(frequency);
updateDisplay();
count++;
delay(200);
}
delay(1000);
return;
} /* end of handling the bandset */
delay(100);
duration += 100;
}
//we reach here only upon the button being released
// if the button has been down for more thn TAP_HOLD_MILLIS, we consider it a long press
// set both VFOs to the same frequency, update the display and be done
if (duration > TAP_HOLD_MILLIS){
printLine2("VFOs reset!");
vfoA= vfoB = frequency;
delay(300);
updateDisplay();
return;
}
// t1 holds the duration for the first press
t1 = duration;
//now wait for another click
delay(100);
// if there a second button press, toggle the VFOs
if (btnDown()){
//swap the VFOs on double tap
if (vfoActive == VFO_B){
vfoActive = VFO_A;
vfoB = frequency;
frequency = vfoA;
}
else{
vfoActive = VFO_B;
vfoA = frequency;
frequency = vfoB;
}
//printLine2("VFO swap! ");
delay(600);
updateDisplay();
}
// No, there was not more taps
else {
//on a single tap, toggle the RIT
if (ritOn)
ritOn = 0;
else
ritOn = 1;
updateDisplay();
}
}
/**
* The Tuning mechansim of the Raduino works in a very innovative way. It uses a tuning potentiometer.
* The tuning potentiometer that a voltage between 0 and 5 volts at ANALOG_TUNING pin of the control connector.
* This is read as a value between 0 and 1000. Hence, the tuning pot gives you 1000 steps from one end to
* the other end of its rotation. Each step is 50 Hz, thus giving approximately 50 Khz of tuning range.
* When the potentiometer is moved to either end of the range, the frequency starts automatically moving
* up or down in 10 Khz increments
*/
void doTuning(){
unsigned long newFreq;
int knob = analogRead(ANALOG_TUNING)-10;
unsigned long old_freq = frequency;
// the knob is fully on the low end, move down by 10 Khz and wait for 200 msec
if (knob < 10 && frequency > LOWEST_FREQ) {
baseTune = baseTune - 10000l;
frequency = baseTune;
updateDisplay();
setFrequency(frequency);
delay(200);
}
// the knob is full on the high end, move up by 10 Khz and wait for 200 msec
else if (knob > 1010 && frequency < HIGHEST_FREQ) {
baseTune = baseTune + 10000l;
frequency = baseTune + 50000l;
setFrequency(frequency);
updateDisplay();
delay(200);
}
// the tuning knob is at neither extremities, tune the signals as usual
else if (knob != old_knob){
frequency = baseTune + (50l * knob);
old_knob = knob;
setFrequency(frequency);
updateDisplay();
}
}
/**
* setup is called on boot up
* It setups up the modes for various pins as inputs or outputs
* initiliaizes the Si5351 and sets various variables to initial state
*
* Just in case the LCD display doesn't work well, the debug log is dumped on the serial monitor
* Choose Serial Monitor from Arduino IDE's Tools menu to see the Serial.print messages
*/
void setup()
{
int32_t cal;
lcd.begin(16, 2);
printBuff[0] = 0;
printLine1("Raduino v1.01");
printLine2(" ");
// Start serial and initialize the Si5351
Serial.begin(9600);
analogReference(DEFAULT);
Serial.println("*Raduino booting up\nv0.01\n");
//configure the function button to use the external pull-up
pinMode(FBUTTON, INPUT);
digitalWrite(FBUTTON, HIGH);
pinMode(PTT, INPUT);
digitalWrite(PTT, HIGH);
pinMode(CAL_BUTTON, INPUT);
digitalWrite(CAL_BUTTON, HIGH);
pinMode(CW_KEY, OUTPUT);
pinMode(CW_TONE, OUTPUT);
digitalWrite(CW_KEY, 0);
digitalWrite(CW_TONE, 0);
digitalWrite(TX_RX, 0);
delay(500);
EEPROM.get(0,cal);
si5351.init(SI5351_CRYSTAL_LOAD_8PF,25000000l, cal);
sprintf(b, "cal: %ld\n", cal);
Serial.println(b);
si5351.set_correction(cal);
Serial.println("*Initiliazed Si5351\n");
si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLA);
si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLB);
Serial.println("*Fixed PLL\n");
//si5351.drive_strength(SI5351_CLK2, SI5351_DRIVE_2MA);
si5351.output_enable(SI5351_CLK0, 0);
si5351.output_enable(SI5351_CLK1, 0);
si5351.output_enable(SI5351_CLK2, 1);
Serial.println("*Output enabled PLL\n");
si5351.set_freq(500000000l, SI5351_CLK2);
Serial.println("*Si5350 ON\n");
mode = MODE_NORMAL;
delay(10);
}
void loop(){
if (digitalRead(CAL_BUTTON) == LOW && mode == MODE_NORMAL){
mode = MODE_CALIBRATE;
si5351.set_correction(0);
printLine2("Calibration... ");
delay(5000);
return;
}
else if (mode == MODE_CALIBRATE){
calibrate();
delay(50);
return;
}
/*
checkCW();
checkTX();
checkButton(); */
doTuning();
delay(50);
}