kopia lustrzana https://github.com/kamocat/uSDX
769 wiersze
30 KiB
C++
769 wiersze
30 KiB
C++
// QCX-SSB.ino - https://github.com/threeme3/QCX-SSB
|
|
//
|
|
// Copyright 2019, 2020 Guido PE1NNZ <pe1nnz@amsat.org>
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
#define VERSION "1.02g"
|
|
|
|
#define QCX 1 // If you DO NOT have a QCX then comment-out (add two-slashes // in the beginning of this line)
|
|
//#define F_SAMP_TX 4402
|
|
#define F_SAMP_TX 4810 //4810 // ADC sample-rate; is best a multiple of _UA and fits exactly in OCR0A = ((F_CPU / 64) / F_SAMP_TX) - 1 , should not exceed CPU utilization
|
|
#define _UA (F_SAMP_TX) //360 // unit angle; integer representation of one full circle turn or 2pi radials or 360 degrees, should be a integer divider of F_SAMP_TX and maximized to have higest precision
|
|
//#define MAX_DP (_UA/1) //(_UA/2) // the occupied SSB bandwidth can be further reduced by restricting the maximum phase change (set MAX_DP to _UA/2).
|
|
//#define CONSTANT_AMP 1 // enable this in case there is no circuitry for controlling envelope (key shaping circuit)
|
|
//#define CARRIER_COMPLETELY_OFF_ON_LOW 1 // disable oscillator on no-envelope transitions, to prevent potential unwanted biasing/leakage through PA circuit
|
|
#define MULTI_ADC 1 // multiple ADC conversions for more sensitive (+12dB) microphone input
|
|
#define DEBUG 1 // enable testing and diagnostics features
|
|
#ifdef DEBUG
|
|
static uint32_t sr = 0;
|
|
static uint32_t cpu_load = 0;
|
|
volatile uint16_t param_a = 0; // registers for debugging, testing and experimental purposes
|
|
volatile int16_t param_b = 0;
|
|
volatile int16_t param_c = 0;
|
|
#endif
|
|
|
|
#undef F_CPU
|
|
#define F_CPU 20007000 // myqcx1:20008440, myqcx2:20006000 // Actual crystal frequency of 20MHz XTAL1, note that this declaration is just informative and does not correct the timing in Arduino functions like delay(); hence a 1.25 factor needs to be added for correction.
|
|
//#define F_CPU F_XTAL // in case ATMEGA328P clock is the same as SI5351 clock (ATMEGA clock tapped from SI crystal)
|
|
|
|
|
|
|
|
#include <avr/sleep.h>
|
|
#include <avr/wdt.h>
|
|
#include "math.h"
|
|
|
|
// Hardware Details
|
|
#include "hal/pinout.h"
|
|
#include "hal/lcd.h"
|
|
#include "hal/i2c.h"
|
|
#include "hal/oled.h"
|
|
#include "hal/si5351.h"
|
|
#include "hal/encoder.h"
|
|
#include "hal/adc.h"
|
|
#include "hal/timer.h"
|
|
|
|
// Radio Protocols
|
|
#include "radio/common.h"
|
|
#include "filter.h"
|
|
#include "radio/ssb.h"
|
|
#include "radio/tx.h"
|
|
#include "radio/cw.h"
|
|
#include "radio/am.h"
|
|
#include "radio/dsb.h"
|
|
#include "radio/fm.h"
|
|
#include "radio/cat.h"
|
|
#include "radio/rx.h"
|
|
#include "radio/common2.h"
|
|
#include "radio/calibration.h"
|
|
|
|
#include "ui.h"
|
|
|
|
//FUSES = { .low = 0xFF, .high = 0xD6, .extended = 0xFD }; // Fuse settings should be these at programming.
|
|
|
|
ISR(TIMER2_COMPA_vect) // Timer2 COMPA interrupt
|
|
{
|
|
func_ptr();
|
|
#ifdef DEBUG
|
|
numSamples++;
|
|
#endif
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
digitalWrite(KEY_OUT, LOW); // for safety: to prevent exploding PA MOSFETs, in case there was something still biasing them.
|
|
|
|
uint8_t mcusr = MCUSR;
|
|
MCUSR = 0;
|
|
//wdt_disable();
|
|
wdt_enable(WDTO_4S); // Enable watchdog
|
|
#ifdef DEBUG
|
|
// Benchmark dsp_tx() ISR (this needs to be done in beginning of setup() otherwise when VERSION containts 5 chars, mis-alignment impact performance by a few percent)
|
|
rx_state = 0;
|
|
uint32_t t0, t1;
|
|
func_ptr = dsp_tx;
|
|
t0 = micros();
|
|
TIMER2_COMPA_vect();
|
|
//func_ptr();
|
|
t1 = micros();
|
|
float load_tx = (t1 - t0) * F_SAMP_TX * 100.0 / 1000000.0;
|
|
// benchmark sdr_rx() ISR
|
|
func_ptr = sdr_rx;
|
|
rx_state = 8;
|
|
float load_rx[8];
|
|
float load_rx_avg = 0;
|
|
uint16_t i;
|
|
for(i = 0; i != 8; i++){
|
|
t0 = micros();
|
|
TIMER2_COMPA_vect();
|
|
//func_ptr();
|
|
t1 = micros();
|
|
load_rx[i] = (t1 - t0) * F_SAMP_RX * 100.0 / 1000000.0;
|
|
load_rx_avg += load_rx[i];
|
|
}
|
|
load_rx_avg /= 8;
|
|
|
|
//adc_stop(); // recover general ADC settings so that analogRead is working again
|
|
#endif
|
|
ADMUX = (1 << REFS0); // restore reference voltage AREF (5V)
|
|
|
|
// disable external interrupts
|
|
PCICR = 0;
|
|
PCMSK0 = 0;
|
|
PCMSK1 = 0;
|
|
PCMSK2 = 0;
|
|
|
|
encoder_setup();
|
|
|
|
initPins();
|
|
|
|
delay(100); // at least 40ms after power rises above 2.7V before sending commands
|
|
lcd.begin(16, 2); // Init LCD
|
|
for(i = 0; i != N_FONTS; i++){ // Init fonts
|
|
pgm_cache_item(fonts[i], 8);
|
|
lcd.createChar(0x01 + i, /*fonts[i]*/_item);
|
|
}
|
|
|
|
//Init si5351
|
|
si5351.powerDown(); // Disable all (used) outputs
|
|
|
|
#ifdef QCX
|
|
// Test if QCX has DSP/SDR capability: SIDETONE output disconnected from AUDIO2
|
|
ssb_cap = 0; dsp_cap = 0;
|
|
si5351.SendRegister(SI_CLK_OE, 0b11111111); // Mute QSD: CLK2_EN=CLK1_EN,CLK0_EN=0
|
|
digitalWrite(RX, HIGH); // generate pulse on SIDETONE and test if it can be seen on AUDIO2
|
|
delay(1); // settle
|
|
digitalWrite(SIDETONE, LOW);
|
|
int16_t v1 = analogRead(AUDIO2);
|
|
digitalWrite(SIDETONE, HIGH);
|
|
int16_t v2 = analogRead(AUDIO2);
|
|
digitalWrite(SIDETONE, LOW);
|
|
dsp_cap = !(abs(v2 - v1) > (0.05 * 1024.0 / 5.0)); // DSP capability?
|
|
if(dsp_cap){ // Test if QCX has SDR capability: AUDIO2 is disconnected from AUDIO1 (only in case of DSP capability)
|
|
delay(400); wdt_reset(); // settle: the following test only works well 400ms after startup
|
|
v1 = analogRead(AUDIO1);
|
|
digitalWrite(AUDIO2, HIGH); // generate pulse on AUDIO2 and test if it can be seen on AUDIO1
|
|
pinMode(AUDIO2, OUTPUT);
|
|
delay(1);
|
|
digitalWrite(AUDIO2, LOW);
|
|
delay(1);
|
|
digitalWrite(AUDIO2, HIGH);
|
|
v2 = analogRead(AUDIO1);
|
|
pinMode(AUDIO2, INPUT);
|
|
if(!(abs(v2 - v1) > (0.125 * 1024.0 / 5.0))) dsp_cap = SDR; // SDR capacility?
|
|
}
|
|
// Test if QCX has SSB capability: DAH is connected to DVM
|
|
delay(1); // settle
|
|
pinMode(DAH, OUTPUT);
|
|
digitalWrite(DAH, LOW);
|
|
v1 = analogRead(DVM);
|
|
digitalWrite(DAH, HIGH);
|
|
v2 = analogRead(DVM);
|
|
digitalWrite(DAH, LOW);
|
|
pinMode(DAH, INPUT_PULLUP);
|
|
ssb_cap = (abs(v2 - v1) > (0.05 * 1024.0 / 5.0)); // SSB capability?
|
|
|
|
//ssb_cap = 0; dsp_cap = 0; // force standard QCX capability
|
|
//ssb_cap = 1; dsp_cap = 0; // force SSB and standard QCX-RX capability
|
|
//ssb_cap = 1; dsp_cap = 1; // force SSB and DSP capability
|
|
//ssb_cap = 1; dsp_cap = 2; // force SSB and SDR capability
|
|
#else
|
|
ssb_cap = 1; dsp_cap = 2; // force SSB and SDR capability
|
|
#endif
|
|
|
|
show_banner();
|
|
lcd.setCursor(7, 0); lcd.print(F(" R")); lcd.print(F(VERSION)); lcd_blanks();
|
|
|
|
#ifdef DEBUG
|
|
/*if((mcusr & WDRF) && (!(mcusr & EXTRF)) && (!(mcusr & BORF))){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!Watchdog RESET")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
if((mcusr & BORF) && (!(mcusr & WDRF))){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!Brownout RESET")); lcd_blanks(); // Brow-out reset happened, CPU voltage not stable or make sure Brown-Out threshold is set OK (make sure E fuse is set to FD)
|
|
delay(1500); wdt_reset();
|
|
}
|
|
if(mcusr & PORF){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!Power-On RESET")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}*/
|
|
/*if(mcusr & EXTRF){
|
|
lcd.setCursor(0, 1); lcd.print(F("Power-On")); lcd_blanks();
|
|
delay(1); wdt_reset();
|
|
}*/
|
|
|
|
// Measure CPU loads
|
|
if(!(load_tx <= 100.0)){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!CPU_tx=")); lcd.print(load_tx); lcd.print(F("%")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
|
|
if(!(load_rx_avg <= 100.0)){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!CPU_rx")); lcd.print(F("=")); lcd.print(load_rx_avg); lcd.print(F("%")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
// and specify individual timings for each of the eight alternating processing functions:
|
|
for(i = 1; i != 8; i++){
|
|
if(!(load_rx[i] <= 100.0))
|
|
{
|
|
lcd.setCursor(0, 1); lcd.print(F("!!CPU_rx")); lcd.print(i); lcd.print(F("=")); lcd.print(load_rx[i]); lcd.print(F("%")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Measure VDD (+5V); should be ~5V
|
|
si5351.SendRegister(SI_CLK_OE, 0b11111111); // Mute QSD: CLK2_EN=CLK1_EN,CLK0_EN=0
|
|
digitalWrite(KEY_OUT, LOW);
|
|
digitalWrite(RX, LOW); // mute RX
|
|
delay(100); // settle
|
|
float vdd = 2.0 * (float)analogRead(AUDIO2) * 5.0 / 1024.0;
|
|
digitalWrite(RX, HIGH);
|
|
if(!(vdd > 4.8 && vdd < 5.2)){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!V5.0=")); lcd.print(vdd); lcd.print(F("V")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
|
|
// Measure VEE (+3.3V); should be ~3.3V
|
|
float vee = (float)analogRead(SCL) * 5.0 / 1024.0;
|
|
if(!(vee > 3.2 && vee < 3.8)){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!V3.3=")); lcd.print(vee); lcd.print(F("V")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
|
|
// Measure AVCC via AREF and using internal 1.1V reference fed to ADC; should be ~5V
|
|
analogRead(6); // setup almost proper ADC readout
|
|
bitSet(ADMUX, 3); // Switch to channel 14 (Vbg=1.1V)
|
|
delay(1); // delay improves accuracy
|
|
bitSet(ADCSRA, ADSC);
|
|
for(; bit_is_set(ADCSRA, ADSC););
|
|
float avcc = 1.1 * 1023.0 / ADC;
|
|
if(!(avcc > 4.6 && avcc < 5.2)){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!Vavcc=")); lcd.print(avcc); lcd.print(F("V")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
|
|
// Report no SSB capability
|
|
if(!ssb_cap){
|
|
lcd.setCursor(0, 1); lcd.print(F("No MIC input...")); lcd_blanks();
|
|
delay(300); wdt_reset();
|
|
}
|
|
|
|
// Test microphone polarity
|
|
/*if((ssb_cap) && (!digitalRead(DAH))){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!MIC in rev.pol")); lcd_blanks();
|
|
delay(300); wdt_reset();
|
|
}*/
|
|
|
|
// Measure DVM bias; should be ~VAREF/2
|
|
float dvm = (float)analogRead(DVM) * 5.0 / 1024.0;
|
|
if((ssb_cap) && !(dvm > 1.8 && dvm < 3.2)){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!Vadc2=")); lcd.print(dvm); lcd.print(F("V")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
|
|
// Measure AUDIO1, AUDIO2 bias; should be ~VAREF/2
|
|
if(dsp_cap == SDR){
|
|
float audio1 = (float)analogRead(AUDIO1) * 5.0 / 1024.0;
|
|
if(!(audio1 > 1.8 && audio1 < 3.2)){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!Vadc0=")); lcd.print(dvm); lcd.print(F("V")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
float audio2 = (float)analogRead(AUDIO2) * 5.0 / 1024.0;
|
|
if(!(audio2 > 1.8 && audio2 < 3.2)){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!Vadc1=")); lcd.print(dvm); lcd.print(F("V")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
}
|
|
|
|
// Measure I2C Bus speed for Bulk Transfers
|
|
si5351.freq(freq, 0, 90);
|
|
wdt_reset();
|
|
t0 = micros();
|
|
for(i = 0; i != 1000; i++) si5351.SendPLLBRegisterBulk();
|
|
t1 = micros();
|
|
uint32_t speed = (1000000 * 8 * 7) / (t1 - t0); // speed in kbit/s
|
|
if(false)
|
|
{
|
|
lcd.setCursor(0, 1); lcd.print(F("i2cspeed=")); lcd.print(speed); lcd.print(F("kbps")); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
|
|
// Measure I2C Bit-Error Rate (BER); should be error free for a thousand random bulk PLLB writes
|
|
si5351.freq(freq, 0, 90);
|
|
wdt_reset();
|
|
uint16_t i2c_error = 0; // number of I2C byte transfer errors
|
|
for(i = 0; i != 1000; i++){
|
|
si5351.freq_calc_fast(i);
|
|
//for(int j = 3; j != 8; j++) si5351.pll_regs[j] = rand();
|
|
si5351.SendPLLBRegisterBulk();
|
|
#define SI_SYNTH_PLL_B 34
|
|
for(int j = 3; j != 8; j++) if(si5351.RecvRegister(SI_SYNTH_PLL_B + j) != si5351.pll_regs[j]) i2c_error++;
|
|
}
|
|
if(i2c_error){
|
|
lcd.setCursor(0, 1); lcd.print(F("!!BER_i2c=")); lcd.print(i2c_error); lcd_blanks();
|
|
delay(1500); wdt_reset();
|
|
}
|
|
#endif
|
|
|
|
drive = 4; // Init settings
|
|
if(!ssb_cap){ mode = CW; filt = 4; stepsize = STEP_500; }
|
|
if(dsp_cap != SDR) pwm_max = 255; // implies that key-shaping circuit is probably present, so use full-scale
|
|
cw_offset = tones[1];
|
|
if(dsp_cap == DSP) volume = 10;
|
|
|
|
// Load parameters from EEPROM, reset to factory defaults when stored values are from a different version
|
|
paramAction(LOAD, VERS);
|
|
if((eeprom_version != get_version_id()) || !digitalRead(DIT) ){ // EEPROM clean: if PTT/onboard-key pressed or version signature in EEPROM does NOT corresponds with this firmware
|
|
eeprom_version = get_version_id();
|
|
//for(int n = 0; n != 1024; n++){ eeprom_write_byte((uint8_t *) n, 0); wdt_reset(); } //clean EEPROM
|
|
//eeprom_write_dword((uint32_t *)EEPROM_OFFSET/3, 0x000000);
|
|
paramAction(SAVE); // save default parfameter values
|
|
lcd.setCursor(0, 1); lcd.print(F("Reset settings.."));
|
|
delay(500); wdt_reset();
|
|
} else {
|
|
paramAction(LOAD); // load all parameters
|
|
}
|
|
si5351.iqmsa = 0; // enforce PLL reset
|
|
change = true;
|
|
prev_bandval = bandval;
|
|
|
|
if(!dsp_cap) volume = 0; // mute volume for unmodified QCX receiver
|
|
|
|
for(uint16_t i = 0; i != 256; i++) // refresh LUT based on pwm_min, pwm_max
|
|
lut[i] = (float)i / ((float)255 / ((float)pwm_max - (float)pwm_min)) + pwm_min;
|
|
|
|
show_banner(); // remove release number
|
|
|
|
start_rx();
|
|
|
|
// #define CAT 1
|
|
#ifdef CAT
|
|
Serial.begin(7680); // 9600 baud corrected for F_CPU=20M
|
|
#endif
|
|
}
|
|
|
|
void print_char(uint8_t in){ // Print char in second line of display and scroll right.
|
|
for(int i = 0; i!= 15; i++) out[i] = out[i+1];
|
|
out[15] = in;
|
|
out[16] = '\0';
|
|
cw_event = true;
|
|
}
|
|
|
|
|
|
|
|
void loop()
|
|
{
|
|
#ifdef CAT
|
|
if(Serial.available() > 0){
|
|
uint8_t in = Serial.read();
|
|
print_char(in);
|
|
parse_cat(in);
|
|
}
|
|
#endif
|
|
|
|
#ifndef SIMPLE_RX
|
|
delay(1);
|
|
#endif
|
|
|
|
if(millis() > sec_event_time){
|
|
sec_event_time = millis() + 1000; // schedule time next second
|
|
|
|
//#define LCD_REINIT
|
|
#ifdef LCD_REINIT
|
|
#ifndef OLED
|
|
lcd.begin(); // fast LCD re-init (in case LCD has been taken out and placed back when power-on)
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
if(menumode == 0){
|
|
smeter();
|
|
if(!((mode == CW) && cw_event) && (smode)) stepsize_showcursor(); // restore cursor (when there is no CW text and smeter is enabled)
|
|
}
|
|
|
|
if(cw_event){
|
|
cw_event = false;
|
|
lcd.setCursor(0, 1); lcd.print(out);
|
|
}
|
|
|
|
#ifdef ONEBUTTON
|
|
uint8_t inv = 1;
|
|
#else
|
|
uint8_t inv = 0;
|
|
#endif
|
|
|
|
#define DAH_AS_KEY 1
|
|
#ifdef DAH_AS_KEY
|
|
if(!digitalRead(DIT) || ((mode == CW) && (!digitalRead(DAH))) ){ // PTT/DIT keys transmitter, for CW also DAH
|
|
#else
|
|
if(!digitalRead(DIT) ){ // PTT/DIT keys transmitter
|
|
#endif
|
|
switch_rxtx(1);
|
|
#ifdef DAH_AS_KEY
|
|
for(; !digitalRead(DIT) || ((mode == CW) && (!digitalRead(DAH)));){ // until released
|
|
#else
|
|
for(; !digitalRead(DIT) ;){ // until released
|
|
#endif
|
|
wdt_reset();
|
|
if(inv ^ digitalRead(BUTTONS)) break; // break if button is pressed (to prevent potential lock-up)
|
|
}
|
|
switch_rxtx(0);
|
|
}
|
|
|
|
enum event_t { BL=0x10, BR=0x20, BE=0x30, SC=0x01, DC=0x02, PL=0x04, PT=0x0C }; // button-left, button-right and button-encoder; single-click, double-click, push-long, push-and-turn
|
|
if(inv ^ digitalRead(BUTTONS)){ // Left-/Right-/Rotary-button (while not already pressed)
|
|
if(!(event & PL)){ // hack: if there was long-push before, then fast forward
|
|
uint16_t v = analogSafeRead(BUTTONS);
|
|
event = SC;
|
|
int32_t t0 = millis();
|
|
for(; inv ^ digitalRead(BUTTONS);){ // until released or long-press
|
|
if((millis() - t0) > 300){ event = PL; break; }
|
|
wdt_reset();
|
|
}
|
|
delay(10); //debounce
|
|
for(; (event != PL) && ((millis() - t0) < 500);){ // until 2nd press or timeout
|
|
if(inv ^ digitalRead(BUTTONS)){ event = DC; break; }
|
|
wdt_reset();
|
|
}
|
|
for(; inv ^ digitalRead(BUTTONS);){ // until released, or encoder is turned while longpress
|
|
if(encoder_val && event == PL){ event = PT; break; }
|
|
wdt_reset();
|
|
} // Max. voltages at ADC3 for buttons L,R,E: 3.76V;4.55V;5V, thresholds are in center
|
|
event |= (v < (4.2 * 1024.0 / 5.0)) ? BL : (v < (4.8 * 1024.0 / 5.0)) ? BR : BE; // determine which button pressed based on threshold levels
|
|
} else { // hack: fast forward handling
|
|
event = (event&0xf0) | ((encoder_val) ? PT : PL); // only alternate bewteen push-long/turn when applicable
|
|
}
|
|
switch(event){
|
|
case BL|PL: // Called when menu button released
|
|
menumode = 2;
|
|
//calibrate_predistortion();
|
|
//powermeter();
|
|
//test_tx_amp();
|
|
break;
|
|
case BL|PT:
|
|
menumode = 1;
|
|
//if(menu == 0) menu = 1;
|
|
break;
|
|
case BL|SC:
|
|
//calibrate_iq();
|
|
int8_t _menumode;
|
|
if(menumode == 0){ _menumode = 1; if(menu == 0) menu = 1; } // short left-click while in default screen: enter menu mode
|
|
if(menumode == 1){ _menumode = 2; } // short left-click while in menu: enter value selection screen
|
|
if(menumode == 2){ _menumode = 0; show_banner(); change = true; paramAction(SAVE, menu); } // short left-click while in value selection screen: save, and return to default screen
|
|
menumode = _menumode;
|
|
break;
|
|
case BL|DC:
|
|
//powerDown();
|
|
/*lcd.setCursor(0, 1); lcd.print(F("Pause")); lcd_blanks();
|
|
for(; !digitalRead(BUTTONS);){ // while in VOX mode
|
|
wdt_reset(); // until 2nd press
|
|
delay(300);
|
|
}*/
|
|
break;
|
|
case BR|SC:
|
|
if(!menumode){
|
|
int8_t prev_mode = mode;
|
|
encoder_val = 1;
|
|
paramAction(UPDATE, MODE); // Mode param //paramAction(UPDATE, mode, NULL, F("Mode"), mode_label, 0, _N(mode_label), true);
|
|
#define MODE_CHANGE_RESETS 1
|
|
#ifdef MODE_CHANGE_RESETS
|
|
if(mode != CW) stepsize = STEP_1k; else stepsize = STEP_500; // sets suitable stepsize
|
|
#endif
|
|
if(mode > CW) mode = LSB; // skip all other modes (only LSB, USB, CW)
|
|
#ifdef MODE_CHANGE_RESETS
|
|
if(mode == CW) filt = 4; else filt = 0; // resets filter (to most BW) on mode change
|
|
#else
|
|
prev_stepsize[prev_mode == CW] = stepsize; stepsize = prev_stepsize[mode == CW]; // backup stepsize setting for previous mode, restore previous stepsize setting for current selected mode; filter settings captured for either CQ or other modes.
|
|
prev_filt[prev_mode == CW] = filt; filt = prev_filt[mode == CW]; // backup filter setting for previous mode, restore previous filter setting for current selected mode; filter settings captured for either CQ or other modes.
|
|
#endif
|
|
paramAction(SAVE, MODE);
|
|
paramAction(SAVE, FILTER);
|
|
si5351.iqmsa = 0; // enforce PLL reset
|
|
change = true;
|
|
} else {
|
|
if(menumode == 1){ menumode = 0; show_banner(); change = true; } // short right-click while in menu: enter value selection screen
|
|
if(menumode == 2){ menumode = 1; change = true; paramAction(SAVE, menu); } // short right-click while in value selection screen: save, and return to menu screen
|
|
}
|
|
break;
|
|
case BR|DC:
|
|
//encoder_val = 1; paramAction(UPDATE, drive, NULL, F("Drive"), NULL, 0, 8, true);
|
|
filt++;
|
|
_init = true;
|
|
if(mode == CW && filt > N_FILT) filt = 4;
|
|
if(mode == CW && filt == 4) stepsize = STEP_500; // reset stepsize for 500Hz filter
|
|
if(mode == CW && (filt == 5 || filt == 6) && stepsize < STEP_100) stepsize = STEP_100; // for CW BW 200, 100 -> step = 100 Hz
|
|
if(mode == CW && filt == 7 && stepsize < STEP_10) stepsize = STEP_10; // for CW BW 50 -> step = 10 Hz
|
|
if(mode != CW && filt > 3) filt = 0;
|
|
encoder_val = 0;
|
|
paramAction(UPDATE, FILTER);
|
|
paramAction(SAVE, FILTER);
|
|
wdt_reset(); delay(1500); wdt_reset();
|
|
change = true; // refresh display
|
|
break;
|
|
case BR|PL:
|
|
{
|
|
#ifdef SIMPLE_RX
|
|
// Experiment: ISR-less sdr_rx():
|
|
smode = 0;
|
|
TIMSK2 &= ~(1 << OCIE2A); // disable timer compare interrupt
|
|
delay(100);
|
|
lcd.setCursor(15, 1); lcd.print("X");
|
|
static uint8_t x = 0;
|
|
uint32_t next = 0;
|
|
for(;;){
|
|
func_ptr();
|
|
#ifdef DEBUG
|
|
numSamples++;
|
|
#endif
|
|
if(!rx_state){
|
|
x++;
|
|
if(x > 16){
|
|
loop();
|
|
//lcd.setCursor(9, 0); lcd.print((int16_t)100); lcd.print(F("dBm ")); // delays are taking too long!
|
|
x= 0;
|
|
}
|
|
}
|
|
//for(;micros() < next;); next = micros() + 16; // sync every 1000000/62500=16ms (or later if missed)
|
|
} //
|
|
#endif //SIMPLE_RX
|
|
|
|
//int16_t x = 0;
|
|
lcd.setCursor(15, 1); lcd.print("V");
|
|
for(; !digitalRead(BUTTONS);){ // while in VOX mode
|
|
|
|
int16_t in = analogSampleMic() - 512;
|
|
static int16_t dc;
|
|
int16_t i, q;
|
|
uint8_t j;
|
|
static int16_t v[16];
|
|
for(j = 0; j != 15; j++) v[j] = v[j + 1];
|
|
dc += (in - dc) / 2;
|
|
v[15] = in - dc; // DC decoupling
|
|
//dc = in; // this is actually creating a low-pass filter
|
|
i = v[7];
|
|
q = ((v[0] - v[14]) * 2 + (v[2] - v[12]) * 8 + (v[4] - v[10]) * 21 + (v[6] - v[8]) * 15) / 128 + (v[6] - v[8]) / 2; // Hilbert transform, 40dB side-band rejection in 400..1900Hz (@4kSPS) when used in image-rejection scenario; (Hilbert transform require 5 additional bits)
|
|
|
|
uint16_t _amp = magn(i, q);
|
|
//x = max(x, abs(v[15]) );
|
|
//lcd.setCursor(0, 1); lcd.print(x); lcd_blanks();
|
|
//lcd.setCursor(0, 1); lcd.print(_amp); lcd_blanks();
|
|
if(_amp > vox_thresh){ // workaround for RX noise leakage to AREF
|
|
for(j = 0; j != 16; j++) v[j] = 0; // clean-up
|
|
switch_rxtx(1);
|
|
vox = 1; tx = 255; //kick
|
|
delay(1);
|
|
for(; tx && !digitalRead(BUTTONS); ) wdt_reset(); // while in tx triggered by vox
|
|
switch_rxtx(0);
|
|
delay(1);
|
|
vox = 0;
|
|
continue; // skip the rest for the moment
|
|
}
|
|
wdt_reset();
|
|
}
|
|
}
|
|
lcd.setCursor(15, 1); lcd.print("R");
|
|
break;
|
|
case BR|PT: break;
|
|
case BE|SC:
|
|
if(!menumode)
|
|
stepsize_change(+1);
|
|
else {
|
|
int8_t _menumode;
|
|
if(menumode == 1){ _menumode = 2; } // short encoder-click while in menu: enter value selection screen
|
|
if(menumode == 2){ _menumode = 1; change = true; paramAction(SAVE, menu); } // short encoder-click while in value selection screen: save, and return to menu screen
|
|
menumode = _menumode;
|
|
}
|
|
break;
|
|
case BE|DC:
|
|
delay(100);
|
|
bandval++;
|
|
if(bandval >= N_BANDS) bandval = 0;
|
|
stepsize = STEP_1k;
|
|
change = true;
|
|
break;
|
|
case BE|PL: stepsize_change(-1); break;
|
|
case BE|PT:
|
|
for(; digitalRead(BUTTONS);){ // process encoder changes until released
|
|
wdt_reset();
|
|
if(dsp_cap && encoder_val){
|
|
paramAction(UPDATE, VOLUME);
|
|
paramAction(SAVE, VOLUME);
|
|
if(volume < 0) powerDown(); // powerDown when volume < 0
|
|
}
|
|
}
|
|
change = true; // refresh display
|
|
break;
|
|
}
|
|
} else event = 0; // no button pressed: reset event
|
|
|
|
if(menumode == 1){
|
|
menu += encoder_val; // Navigate through menu of parameters and values
|
|
encoder_val = 0;
|
|
menu = max(1 /* 0 */, min(menu, N_PARAMS));
|
|
}
|
|
|
|
bool param_change = (encoder_val != 0);
|
|
if(menumode != 0){ // Show parameter and value
|
|
if(menu != 0){
|
|
paramAction(UPDATE_MENU, menu); // update param with encoder change and display
|
|
} else {
|
|
menumode = 0; show_banner(); // while scrolling through menu: menu item 0 goes back to main console
|
|
change = true; // refresh freq display (when menu = 0)
|
|
}
|
|
if(menumode == 2){
|
|
if(param_change){
|
|
lcd.setCursor(0, 1); lcd.cursor(); delay(10); // edits menu item value; make cursor visible
|
|
if(menu == MODE){ // post-handling Mode parameter
|
|
delay(100);
|
|
change = true;
|
|
si5351.iqmsa = 0; // enforce PLL reset
|
|
// make more generic:
|
|
if(mode != CW) stepsize = STEP_1k; else stepsize = STEP_500;
|
|
if(mode == CW) filt = 4; else filt = 0;
|
|
}
|
|
if(menu == BAND){
|
|
change = true;
|
|
}
|
|
if(menu == ATT){ // post-handling ATT parameter
|
|
if(dsp_cap == SDR){
|
|
adc_start(0, !(att & 0x01), F_ADC_CONV); admux[0] = ADMUX; // att bit 0 ON: attenuate -13dB by changing ADC AREF (full-scale range) from 1V1 to 5V
|
|
adc_start(1, !(att & 0x01), F_ADC_CONV); admux[1] = ADMUX;
|
|
}
|
|
digitalWrite(RX, !(att & 0x02)); // att bit 1 ON: attenuate -20dB by disabling RX line, switching Q5 (antenna input switch) into 100k resistence
|
|
pinMode(AUDIO1, (att & 0x04) ? OUTPUT : INPUT); // att bit 2 ON: attenuate -40dB by terminating ADC inputs with 10R
|
|
pinMode(AUDIO2, (att & 0x04) ? OUTPUT : INPUT);
|
|
}
|
|
if(menu == SIFXTAL){
|
|
change = true;
|
|
}
|
|
if((menu == PWM_MIN) || (menu == PWM_MAX)){
|
|
for(uint16_t i = 0; i != 256; i++) // refresh LUT based on pwm_min, pwm_max
|
|
lut[i] = (float)i / ((float)255 / ((float)pwm_max - (float)pwm_min)) + pwm_min;
|
|
//lut[i] = min(pwm_max, (float)106*log(i) + pwm_min); // compressed microphone output: drive=0, pwm_min=115, pwm_max=220
|
|
}
|
|
if(menu == CWTONE){
|
|
if(dsp_cap){ cw_offset = (cw_tone == 0) ? tones[0] : tones[1]; paramAction(SAVE, CWOFF); }
|
|
}
|
|
#ifdef CAL_IQ
|
|
if(menu == CALIB){
|
|
if(dsp_cap != SDR) calibrate_iq(); menu = 0;
|
|
}
|
|
#endif
|
|
}
|
|
#ifdef DEBUG
|
|
if(menu == SR){ // measure sample-rate
|
|
numSamples = 0;
|
|
delay(500 * 1.25); // delay 0.5s (in reality because F_CPU=20M instead of 16M, delay() is running 1.25x faster therefore we need to multiply wqith 1.25)
|
|
sr = numSamples * 2; // samples per second */
|
|
}
|
|
if(menu == CPULOAD){ // measure CPU-load
|
|
uint32_t i = 0;
|
|
uint32_t prev_time = millis();
|
|
for(i = 0; i != 300000; i++) wdt_reset(); // fixed CPU-load 132052*1.25us delay under 0% load condition; is 132052*1.25 * 20M = 3301300 CPU cycles fixed load
|
|
cpu_load = 100 - 132 * 100 / (millis() - prev_time);
|
|
}
|
|
if((menu == PARAM_A) || (menu == PARAM_B) || (menu == PARAM_C)){
|
|
delay(300);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
if(menumode == 0){
|
|
if(encoder_val){ // process encoder tuning steps
|
|
process_encoder_tuning_step(encoder_val);
|
|
encoder_val = 0;
|
|
}
|
|
}
|
|
|
|
if(change){
|
|
change = false;
|
|
if(prev_bandval != bandval){ freq = band[bandval]; prev_bandval = bandval; }
|
|
save_event_time = millis() + 1000; // schedule time to save freq (no save while tuning, hence no EEPROM wear out)
|
|
|
|
if(menumode == 0){
|
|
display_vfo(freq);
|
|
stepsize_showcursor();
|
|
|
|
// The following is a hack for SWR measurement:
|
|
//si5351.alt_clk2(freq + 2400);
|
|
//si5351.SendRegister(SI_CLK_OE, 0b11111000); // CLK2_EN=1, CLK1_EN,CLK0_EN=1
|
|
//digitalWrite(SIG_OUT, HIGH); // inject CLK2 on antenna input via 120K
|
|
}
|
|
|
|
noInterrupts();
|
|
if(mode == CW){
|
|
si5351.freq(freq + cw_offset, 90, 0); // RX in CW-R (=LSB), correct for CW-tone offset
|
|
si5351.freq_calc_fast(-cw_offset); si5351.SendPLLBRegisterBulk(); // TX at freq
|
|
} else
|
|
if(mode == LSB)
|
|
si5351.freq(freq, 90, 0); // RX in LSB
|
|
else
|
|
si5351.freq(freq, 0, 90); // RX in USB, ...
|
|
interrupts();
|
|
}
|
|
|
|
if((save_event_time) && (millis() > save_event_time)){ // save freq when time has reached schedule
|
|
paramAction(SAVE, FREQ); // save freq changes
|
|
save_event_time = 0;
|
|
//lcd.setCursor(15, 1); lcd.print("S"); delay(100); lcd.setCursor(15, 1); lcd.print("R");
|
|
}
|
|
|
|
wdt_reset();
|
|
}
|
|
|
|
/* BACKLOG:
|
|
code definitions and re-use for comb, integrator, dc decoupling, arctan
|
|
in func_ptr for different mode types
|
|
refactor main()
|
|
agc based on rms256, agc/smeter after filter
|
|
noisefree integrator (rx audio out) in lower range
|
|
raised cosine tx amp for cw, 4ms tau seems enough: http://fermi.la.asu.edu/w9cf/articles/click/index.html
|
|
auto paddle
|
|
cw tx message/cw encoder
|
|
32 bin fft
|
|
dynamic range cw
|
|
att extended agc
|
|
configurable F_CPU
|
|
CW-L mode
|
|
VFO-A/B+split+RIT
|
|
VOX integration in main loop
|
|
K2/TS480 CAT control
|
|
faster RX-TX switch to support CW
|
|
clock
|
|
qcx API demo code
|
|
scan
|
|
unwanted VOX feedback in DSP mode
|
|
move last bit of arrays into flash? https://www.microchip.com/webdoc/AVRLibcReferenceManual/FAQ_1faq_rom_array.html
|
|
remove floats
|
|
u-law in RX path?: http://dystopiancode.blogspot.com/2012/02/pcm-law-and-u-law-companding-algorithms.html
|
|
Arduino library?
|
|
1. RX bias offset correction by measurement avg, 2. charge decoupling cap. by resetting to 0V and setting 5V for a certain amount of (charge) time
|
|
AGC amplitude sense behind filter, controlling gain before filter
|
|
add 1K (500R?) to GND at TTL RF output to keep zero-level below BS170 threshold
|
|
additional PWM output for potential BOOST conversion
|
|
SWR measurement?
|
|
CW decoder amp thrshld restriction and noise reduction (use of certain pause amounts)
|
|
squelch gating
|
|
|
|
Analyse assembly:
|
|
/home/guido/Downloads/arduino-1.8.10/hardware/tools/avr/bin/avr-g++ -S -g -Os -w -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -Wno-error=narrowing -MMD -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10810 -DARDUINO_AVR_UNO -DARDUINO_ARCH_AVR -I/home/guido/Downloads/arduino-1.8.10/hardware/arduino/avr/cores/arduino -I/home/guido/Downloads/arduino-1.8.10/hardware/arduino/avr/variants/standard /tmp/arduino_build_483134/sketch/QCX-SSB.ino.cpp -o /tmp/arduino_build_483134/sketch/QCX-SSB.ino.cpp.txt
|
|
|
|
Rewire/code I/Q clk pins so that a Div/1 and Div/2 scheme is used instead of 0 and 90 degrees phase shift
|
|
10,11,13,12 10,11,12,13 (pin)
|
|
Q- I+ Q+ I- Q- I+ Q+ I-
|
|
90 deg.shift div/2@S1(pin2)
|
|
|
|
50MHz LSB OK, USB NOK
|
|
s-meter offset vs DC bal.
|
|
|
|
AGC DR
|
|
auto ATT
|
|
class-E
|
|
PCB
|
|
RIT, VFO-B, undersampling, IF-offset
|
|
keyer dash-dot
|
|
tiny-click removal, DC offset correction
|
|
|
|
*/
|