diff --git a/CalFM.cpp b/CalFM.cpp new file mode 100644 index 0000000..a654b48 --- /dev/null +++ b/CalFM.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2009-2015 by Jonathan Naylor G4KLX + * Copyright (C) 2016 by Colin Durbridge G4EML + * Copyright (C) 2020 by Phil Taylor M0VSE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "Globals.h" +#include "CalFM.h" + + +const struct TONE_TABLE { + uint16_t frequency; + uint16_t length; + q31_t increment; +} TONE_TABLE_DATA[] = { + {2495U, 10U, 223248821}, + {2079U, 12U, 186025772}, + {1633U, 15U, 146118367}, + {1247U, 19U, 111579672}, + {1039U, 23U, 93012886}, + {956U, 25U, 85541432}}; + +const uint8_t TONE_TABLE_DATA_LEN = 6U; + +CCalFM::CCalFM() : +m_tone(NULL), +m_frequency(0), +m_length(0), +m_level(128*32), +m_transmit(false), +m_lastState(STATE_IDLE), +m_audioSeq(0) +{ +} + +void CCalFM::process() +{ + const TONE_TABLE* entry = NULL; + + if (m_modemState!=m_lastState) + { + switch (m_modemState) { + case STATE_FMCAL10K: + m_frequency=956U; + break; + case STATE_FMCAL12K: + m_frequency=1039U; + break; + case STATE_FMCAL15K: + m_frequency=1247U; + break; + case STATE_FMCAL20K: + m_frequency=1633U; + break; + case STATE_FMCAL25K: + m_frequency=2079U; + break; + case STATE_FMCAL30K: + m_frequency=2495U; + break; + default: + m_frequency=0; + break; + } + + for (uint8_t i = 0U; i < TONE_TABLE_DATA_LEN; i++) { + if (TONE_TABLE_DATA[i].frequency == m_frequency) { + entry = TONE_TABLE_DATA + i; + break; + } + } + + if (entry == NULL) + return; + + m_length = entry->length; + + delete[] m_tone; + m_tone = new q15_t[m_length]; + + q31_t arg = 0; + for (uint16_t i = 0U; i < m_length; i++) { + q63_t value = ::arm_sin_q31(arg) * q63_t(m_level); + m_tone[i] = q15_t(__SSAT((value >> 31), 16)); + + arg += entry->increment; + } + + m_lastState=m_modemState; + } + + if (m_transmit) + { + uint16_t space = io.getSpace(); + while (space > m_length) + { + io.write(m_modemState,m_tone,m_length); + space -= m_length; + } + } +} + + +uint8_t CCalFM::write(const uint8_t* data, uint8_t length) +{ + if (length != 1U) + return 4U; + + m_transmit = data[0U] == 1U; + + return 0U; +} diff --git a/CalFM.h b/CalFM.h new file mode 100644 index 0000000..0dd8887 --- /dev/null +++ b/CalFM.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2009-2015 by Jonathan Naylor G4KLX + * Copyright (C) 2016 by Colin Durbridge G4EML + * Copyright (C) 2020 by Phil Taylor M0VSE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(CALFM_H) +#define CALFM_H + +#include "Config.h" + +class CCalFM { +public: + CCalFM(); + + void process(); + void fm10kcal(); + void fm12k5cal(); + void fm15kcal(); + void fm20kcal(); + void fm25kcal(); + void fm30kcal(); + + uint8_t write(const uint8_t* data, uint8_t length); + +private: + uint16_t m_frequency; + uint16_t m_length; + q15_t* m_tone; + q15_t m_level; + bool m_transmit; + uint8_t m_audioSeq; + uint8_t m_lastState; +}; + +#endif + diff --git a/Config.h b/Config.h index 9d62604..a0e249f 100644 --- a/Config.h +++ b/Config.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -84,5 +84,7 @@ // Use the D-Star and DMR LEDs for POCSAG // #define USE_ALTERNATE_POCSAG_LEDS -#endif +// Use the D-Star and YSF LEDs for FM +// #define USE_ALTERNATE_FM_LEDS +#endif diff --git a/FM.cpp b/FM.cpp new file mode 100644 index 0000000..fd18f11 --- /dev/null +++ b/FM.cpp @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "Globals.h" +#include "FM.h" + +CFM::CFM() : +m_callsign(), +m_rfAck(), +m_ctcssRX(), +m_ctcssTX(), +m_timeoutTone(), +m_state(FS_LISTENING), +m_callsignAtStart(false), +m_callsignAtEnd(false), +m_callsignTimer(), +m_timeoutTimer(), +m_holdoffTimer(), +m_kerchunkTimer(), +m_ackMinTimer(), +m_ackDelayTimer(), +m_hangTimer(), +m_filterStage1( 724, 1448, 724, 32768, -37895, 21352),//3rd order Cheby Filter 300 to 2700Hz, 0.2dB passband ripple, sampling rate 24kHz +m_filterStage2(32768, 0,-32768, 32768, -50339, 19052), +m_filterStage3(32768, -65536, 32768, 32768, -64075, 31460), +m_preemphasis(32768, 13967, 0, 32768, -18801, 0),//75µS 24kHz sampling rate +m_deemphasis (32768, -18801, 0, 32768, 13967, 0),//75µS 24kHz sampling rate +m_blanking(), +m_useCOS(true), +m_rfAudioBoost(1U), +m_downsampler(1024)//Size might need adjustement +{ +} + +void CFM::samples(bool cos, q15_t* samples, uint8_t length) +{ + if (!m_useCOS) + cos = true; + + uint8_t i = 0U; + for (; i < length; i++) { + q15_t currentSample = samples[i];//save to a local variable to avoid indirection on every access + + CTCSSState ctcssState = m_ctcssRX.process(currentSample); + + if (CTCSS_NOT_READY(ctcssState) && m_modemState != STATE_FM) { + //Not enough samples to determine if you have CTCSS, just carry on + continue; + } else if (CTCSS_READY(ctcssState) && m_modemState != STATE_FM) { + //we had enough samples for CTCSS and we are in some other mode than FM + bool validCTCSS = CTCSS_VALID(ctcssState); + stateMachine(validCTCSS && cos, i + 1U); + if (m_modemState != STATE_FM) + continue; + } else if (CTCSS_READY(ctcssState) && m_modemState == STATE_FM) { + //We had enough samples for CTCSS and we are in FM mode, trigger the state machine + bool validCTCSS = CTCSS_VALID(ctcssState); + stateMachine(validCTCSS && cos, i + 1U); + if (m_modemState != STATE_FM) + break; + } else if (CTCSS_NOT_READY(ctcssState) && m_modemState == STATE_FM && i == length - 1) { + //Not enough samples for CTCSS but we already are in FM, trigger the state machine + //but do not trigger the state machine on every single sample, save CPU! + bool validCTCSS = CTCSS_VALID(ctcssState); + stateMachine(validCTCSS && cos, i + 1U); + } + + // Only let audio through when relaying audio + if (m_state == FS_RELAYING || m_state == FS_KERCHUNK) { + currentSample = m_deemphasis.filter(currentSample); + m_downsampler.addSample(currentSample); + currentSample = m_blanking.process(currentSample); + currentSample *= m_rfAudioBoost; + } else { + currentSample = 0U; + } + + if (!m_callsign.isRunning()) + currentSample += m_rfAck.getHighAudio(); + + if (!m_rfAck.isRunning()) { + if (m_state == FS_LISTENING) + currentSample += m_callsign.getHighAudio(); + else + currentSample += m_callsign.getLowAudio(); + } + + if (!m_callsign.isRunning() && !m_rfAck.isRunning()) + currentSample += m_timeoutTone.getAudio(); + + currentSample = m_filterStage3.filter(m_filterStage2.filter(m_filterStage1.filter(currentSample))); + + currentSample = m_preemphasis.filter(currentSample); + + currentSample += m_ctcssTX.getAudio(); + + samples[i] = currentSample; + } + + if (m_modemState == STATE_FM) + io.write(STATE_FM, samples, i);//only write the actual number of processed samples to IO +} + +void CFM::process() +{ +} + +void CFM::reset() +{ + m_ctcssRX.reset(); +} + +uint8_t CFM::setCallsign(const char* callsign, uint8_t speed, uint16_t frequency, uint8_t time, uint8_t holdoff, uint8_t highLevel, uint8_t lowLevel, bool callsignAtStart, bool callsignAtEnd) +{ + m_callsignAtStart = callsignAtStart; + m_callsignAtEnd = callsignAtEnd; + + uint16_t holdoffTime = holdoff * 60U; + uint16_t callsignTime = time * 60U; + + m_holdoffTimer.setTimeout(holdoffTime, 0U); + m_callsignTimer.setTimeout(callsignTime, 0U); + + if (holdoffTime > 0U) + m_holdoffTimer.start(); + + return m_callsign.setParams(callsign, speed, frequency, highLevel, lowLevel); +} + +uint8_t CFM::setAck(const char* rfAck, uint8_t speed, uint16_t frequency, uint8_t minTime, uint16_t delay, uint8_t level) +{ + m_ackDelayTimer.setTimeout(0U, delay); + m_ackMinTimer.setTimeout(minTime, 0U); + + return m_rfAck.setParams(rfAck, speed, frequency, level, level); +} + +uint8_t CFM::setMisc(uint16_t timeout, uint8_t timeoutLevel, uint8_t ctcssFrequency, uint8_t ctcssThreshold, uint8_t ctcssLevel, uint8_t kerchunkTime, uint8_t hangTime, bool useCOS, uint8_t rfAudioBoost, uint8_t maxDev, uint8_t rxLevel) +{ + m_useCOS = useCOS; + m_rfAudioBoost = q15_t(rfAudioBoost); + + m_timeoutTimer.setTimeout(timeout, 0U); + m_kerchunkTimer.setTimeout(kerchunkTime, 0U); + m_hangTimer.setTimeout(hangTime, 0U); + + m_timeoutTone.setParams(timeoutLevel); + m_blanking.setParams(maxDev, timeoutLevel); + + uint8_t ret = m_ctcssRX.setParams(ctcssFrequency, ctcssThreshold, rxLevel); + if (ret != 0U) + return ret; + + return m_ctcssTX.setParams(ctcssFrequency, ctcssLevel); +} + +void CFM::stateMachine(bool validSignal, uint8_t length) +{ + m_callsignTimer.clock(length); + m_timeoutTimer.clock(length); + m_holdoffTimer.clock(length); + m_kerchunkTimer.clock(length); + m_ackMinTimer.clock(length); + m_ackDelayTimer.clock(length); + m_hangTimer.clock(length); + + switch (m_state) { + case FS_LISTENING: + listeningState(validSignal); + break; + case FS_KERCHUNK: + kerchunkState(validSignal); + break; + case FS_RELAYING: + relayingState(validSignal); + break; + case FS_RELAYING_WAIT: + relayingWaitState(validSignal); + break; + case FS_TIMEOUT: + timeoutState(validSignal); + break; + case FS_TIMEOUT_WAIT: + timeoutWaitState(validSignal); + break; + case FS_HANG: + hangState(validSignal); + break; + default: + break; + } + + if (m_state == FS_LISTENING && m_modemState == STATE_FM) { + if (!m_callsign.isRunning() && !m_rfAck.isRunning()) { + DEBUG1("Change to STATE_IDLE"); + m_modemState = STATE_IDLE; + m_callsignTimer.stop(); + m_timeoutTimer.stop(); + m_kerchunkTimer.stop(); + m_ackMinTimer.stop(); + m_ackDelayTimer.stop(); + m_hangTimer.stop(); + } + } +} + +void CFM::listeningState(bool validSignal) +{ + if (validSignal) { + if (m_kerchunkTimer.getTimeout() > 0U) { + DEBUG1("State to KERCHUNK"); + m_state = FS_KERCHUNK; + m_kerchunkTimer.start(); + } else { + DEBUG1("State to RELAYING"); + m_state = FS_RELAYING; + if (m_callsignAtStart) + sendCallsign(); + } + + beginRelaying(); + + m_callsignTimer.start(); + + DEBUG1("Change to STATE_FM"); + m_modemState = STATE_FM; + } +} + +void CFM::kerchunkState(bool validSignal) +{ + if (validSignal) { + if (m_kerchunkTimer.hasExpired()) { + DEBUG1("State to RELAYING"); + m_state = FS_RELAYING; + m_kerchunkTimer.stop(); + } + } else { + DEBUG1("State to LISTENING"); + m_state = FS_LISTENING; + m_kerchunkTimer.stop(); + m_timeoutTimer.stop(); + m_ackMinTimer.stop(); + m_callsignTimer.stop(); + } +} + +void CFM::relayingState(bool validSignal) +{ + if (validSignal) { + if (m_timeoutTimer.isRunning() && m_timeoutTimer.hasExpired()) { + DEBUG1("State to TIMEOUT"); + m_state = FS_TIMEOUT; + m_ackMinTimer.stop(); + m_timeoutTimer.stop(); + m_timeoutTone.start(); + } + } else { + DEBUG1("State to RELAYING_WAIT"); + m_state = FS_RELAYING_WAIT; + m_ackDelayTimer.start(); + } + + if (m_callsignTimer.isRunning() && m_callsignTimer.hasExpired()) { + sendCallsign(); + m_callsignTimer.start(); + } +} + +void CFM::relayingWaitState(bool validSignal) +{ + if (validSignal) { + DEBUG1("State to RELAYING"); + m_state = FS_RELAYING; + m_ackDelayTimer.stop(); + } else { + if (m_ackDelayTimer.isRunning() && m_ackDelayTimer.hasExpired()) { + DEBUG1("State to HANG"); + m_state = FS_HANG; + + if (m_ackMinTimer.isRunning()) { + if (m_ackMinTimer.hasExpired()) { + DEBUG1("Send ack"); + m_rfAck.start(); + m_ackMinTimer.stop(); + } + } else { + DEBUG1("Send ack"); + m_rfAck.start(); + m_ackMinTimer.stop(); + } + + m_ackDelayTimer.stop(); + m_timeoutTimer.stop(); + m_hangTimer.start(); + } + } + + if (m_callsignTimer.isRunning() && m_callsignTimer.hasExpired()) { + sendCallsign(); + m_callsignTimer.start(); + } +} + +void CFM::hangState(bool validSignal) +{ + if (validSignal) { + DEBUG1("State to RELAYING"); + m_state = FS_RELAYING; + DEBUG1("Stop ack"); + m_rfAck.stop(); + beginRelaying(); + } else { + if (m_hangTimer.isRunning() && m_hangTimer.hasExpired()) { + DEBUG1("State to LISTENING"); + m_state = FS_LISTENING; + m_hangTimer.stop(); + + if (m_callsignAtEnd) + sendCallsign(); + + m_callsignTimer.stop(); + } + } + + if (m_callsignTimer.isRunning() && m_callsignTimer.hasExpired()) { + sendCallsign(); + m_callsignTimer.start(); + } +} + +void CFM::timeoutState(bool validSignal) +{ + if (!validSignal) { + DEBUG1("State to TIMEOUT_WAIT"); + m_state = FS_TIMEOUT_WAIT; + m_ackDelayTimer.start(); + } + + if (m_callsignTimer.isRunning() && m_callsignTimer.hasExpired()) { + sendCallsign(); + m_callsignTimer.start(); + } +} + +void CFM::timeoutWaitState(bool validSignal) +{ + if (validSignal) { + DEBUG1("State to TIMEOUT"); + m_state = FS_TIMEOUT; + m_ackDelayTimer.stop(); + } else { + if (m_ackDelayTimer.isRunning() && m_ackDelayTimer.hasExpired()) { + DEBUG1("State to HANG"); + m_state = FS_HANG; + m_timeoutTone.stop(); + DEBUG1("Send ack"); + m_rfAck.start(); + m_ackDelayTimer.stop(); + m_ackMinTimer.stop(); + m_timeoutTimer.stop(); + m_hangTimer.start(); + } + } + + if (m_callsignTimer.isRunning() && m_callsignTimer.hasExpired()) { + sendCallsign(); + m_callsignTimer.start(); + } +} + +void CFM::sendCallsign() +{ + if (m_holdoffTimer.isRunning()) { + if (m_holdoffTimer.hasExpired()) { + DEBUG1("Send callsign"); + m_callsign.start(); + m_holdoffTimer.start(); + } + } else { + DEBUG1("Send callsign"); + m_callsign.start(); + } +} + +void CFM::beginRelaying() +{ + m_timeoutTimer.start(); + m_ackMinTimer.start(); +} diff --git a/FM.h b/FM.h new file mode 100644 index 0000000..a852a80 --- /dev/null +++ b/FM.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FM_H) +#define FM_H + +#include "Config.h" + +#include "FMBlanking.h" +#include "FMCTCSSRX.h" +#include "FMCTCSSTX.h" +#include "FMTimeout.h" +#include "FMKeyer.h" +#include "FMTimer.h" +#include "FMDirectForm1.h" +#include "FMDownsampler.h" + +enum FM_STATE { + FS_LISTENING, + FS_KERCHUNK, + FS_RELAYING, + FS_RELAYING_WAIT, + FS_TIMEOUT, + FS_TIMEOUT_WAIT, + FS_HANG +}; + + + + +class CFM { +public: + CFM(); + + void samples(bool cos, q15_t* samples, uint8_t length); + + void process(); + + void reset(); + + uint8_t setCallsign(const char* callsign, uint8_t speed, uint16_t frequency, uint8_t time, uint8_t holdoff, uint8_t highLevel, uint8_t lowLevel, bool callsignAtStart, bool callsignAtEnd); + uint8_t setAck(const char* rfAck, uint8_t speed, uint16_t frequency, uint8_t minTime, uint16_t delay, uint8_t level); + uint8_t setMisc(uint16_t timeout, uint8_t timeoutLevel, uint8_t ctcssFrequency, uint8_t ctcssThreshold, uint8_t ctcssLevel, uint8_t kerchunkTime, uint8_t hangTime, bool useCOS, uint8_t rfAudioBoost, uint8_t maxDev, uint8_t rxLevel); + +private: + CFMKeyer m_callsign; + CFMKeyer m_rfAck; + CFMCTCSSRX m_ctcssRX; + CFMCTCSSTX m_ctcssTX; + CFMTimeout m_timeoutTone; + FM_STATE m_state; + bool m_callsignAtStart; + bool m_callsignAtEnd; + CFMTimer m_callsignTimer; + CFMTimer m_timeoutTimer; + CFMTimer m_holdoffTimer; + CFMTimer m_kerchunkTimer; + CFMTimer m_ackMinTimer; + CFMTimer m_ackDelayTimer; + CFMTimer m_hangTimer; + CFMDirectFormI m_filterStage1; + CFMDirectFormI m_filterStage2; + CFMDirectFormI m_filterStage3; + CFMDirectFormI m_preemphasis; + CFMDirectFormI m_deemphasis; + CFMBlanking m_blanking; + bool m_useCOS; + q15_t m_rfAudioBoost; + CFMDownsampler m_downsampler; + + void stateMachine(bool validSignal, uint8_t length); + void listeningState(bool validSignal); + void kerchunkState(bool validSignal); + void relayingState(bool validSignal); + void relayingWaitState(bool validSignal); + void timeoutState(bool validSignal); + void timeoutWaitState(bool validSignal); + void hangState(bool validSignal); + + void sendCallsign(); + void beginRelaying(); +}; + +#endif diff --git a/FMBlanking.cpp b/FMBlanking.cpp new file mode 100644 index 0000000..4d8af80 --- /dev/null +++ b/FMBlanking.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "Globals.h" +#include "FMBlanking.h" + +// 2000 Hz sine wave at 24000 Hz sample rate +const q31_t TONE[] = {0, 16384, 28378, 32767, 28378, 16384, 0, -16383, -28377, -32767, -28377, -16383}; + +const uint8_t TONE_LEN = 12U; + +const uint32_t BLEEP_LEN = 2400U; // 100ms + +CFMBlanking::CFMBlanking() : +m_posValue(0), +m_negValue(0), +m_level(128 * 128), +m_running(false), +m_pos(0U), +m_n(0U) +{ +} + +void CFMBlanking::setParams(uint8_t value, uint8_t level) +{ + m_posValue = q15_t(value * 128); + m_negValue = -m_posValue; + + m_level = q15_t(level * 128); +} + +q15_t CFMBlanking::process(q15_t sample) +{ + if (m_posValue == 0) + return sample; + + if (!m_running) { + if (sample >= m_posValue) { + m_running = true; + m_pos = 0U; + m_n = 0U; + } else if (sample <= m_negValue) { + m_running = true; + m_pos = 0U; + m_n = 0U; + } + } + + if (!m_running) + return sample; + + if (m_pos <= BLEEP_LEN) { + q31_t value = TONE[m_n++] * m_level; + sample = q15_t(__SSAT((value >> 15), 16)); + if (m_n >= TONE_LEN) + m_n = 0U; + } else { + sample = 0; + } + + m_pos++; + if (m_pos >= 12000U) + m_running = false; + + return sample; +} diff --git a/FMBlanking.h b/FMBlanking.h new file mode 100644 index 0000000..2aacd4c --- /dev/null +++ b/FMBlanking.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMBlanking_H) +#define FMBlanking_H + +#include "Config.h" + +class CFMBlanking { +public: + CFMBlanking(); + + void setParams(uint8_t value, uint8_t level); + + q15_t process(q15_t sample); + +private: + q15_t m_posValue; + q15_t m_negValue; + q15_t m_level; + bool m_running; + uint32_t m_pos; + uint8_t m_n; +}; + +#endif diff --git a/FMCTCSSRX.cpp b/FMCTCSSRX.cpp new file mode 100644 index 0000000..9e81d3c --- /dev/null +++ b/FMCTCSSRX.cpp @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "Globals.h" +#include "FMCTCSSRX.h" + +const struct RX_CTCSS_TABLE { + uint8_t frequency; + q63_t coeffDivTwo; +} RX_CTCSS_TABLE_DATA[] = { + { 67U, 2147153298}, + { 69U, 2147130228}, + { 71U, 2147103212}, + { 74U, 2147076297}, + { 77U, 2147047330}, + { 79U, 2147016195}, + { 82U, 2146982775}, + { 85U, 2146946945}, + { 88U, 2146907275}, + { 91U, 2146867538}, + { 94U, 2146822298}, + { 97U, 2146785526}, + {100U, 2146747759}, + {103U, 2146695349}, + {107U, 2146637984}, + {110U, 2146578604}, + {114U, 2146513835}, + {118U, 2146445080}, + {123U, 2146370355}, + {127U, 2146291161}, + {131U, 2146205372}, + {136U, 2146112589}, + {141U, 2146014479}, + {146U, 2145910829}, + {151U, 2145796971}, + {156U, 2145676831}, + {159U, 2145604646}, + {162U, 2145547790}, + {165U, 2145468230}, + {167U, 2145409363}, + {171U, 2145324517}, + {173U, 2145261046}, + {177U, 2145170643}, + {179U, 2145102321}, + {183U, 2145006080}, + {186U, 2144932648}, + {189U, 2144830280}, + {192U, 2144748638}, + {196U, 2144639788}, + {199U, 2144555290}, + {203U, 2144436713}, + {206U, 2144346237}, + {210U, 2144217348}, + {218U, 2143983951}, + {225U, 2143735870}, + {229U, 2143622139}, + {233U, 2143469001}, + {241U, 2143182299}, + {250U, 2142874683}, + {254U, 2142733729}}; + +const uint8_t CTCSS_TABLE_DATA_LEN = 50U; + +// 4Hz bandwidth +const uint16_t N = 24000U / 4U; + +CFMCTCSSRX::CFMCTCSSRX() : +m_coeffDivTwo(0), +m_threshold(0), +m_count(0U), +m_q0(0), +m_q1(0), +m_result(CTS_NONE), +m_rxLevelInverse(1) +{ +} + +uint8_t CFMCTCSSRX::setParams(uint8_t frequency, uint8_t threshold, uint8_t level) +{ + m_rxLevelInverse = 511 / q15_t(level); + + m_coeffDivTwo = 0; + + for (uint8_t i = 0U; i < CTCSS_TABLE_DATA_LEN; i++) { + if (RX_CTCSS_TABLE_DATA[i].frequency == frequency) { + m_coeffDivTwo = RX_CTCSS_TABLE_DATA[i].coeffDivTwo; + break; + } + } + + if (m_coeffDivTwo == 0) + return 4U; + + m_threshold = q31_t(threshold); + + return 0U; +} + +CTCSSState CFMCTCSSRX::process(q15_t sample) +{ + q31_t sample31 = q31_t(sample) * m_rxLevelInverse; + + m_result = m_result & (~CTS_READY); + + q31_t q2 = m_q1; + m_q1 = m_q0; + + // Q31 multiplication, t3 = m_coeffDivTwo * 2 * m_q1 + q63_t t1 = m_coeffDivTwo * m_q1; + q31_t t2 = __SSAT((t1 >> 31), 31); + q31_t t3 = t2 * 2; + + // m_q0 = m_coeffDivTwo * m_q1 * 2 - q2 + sample + m_q0 = t3 - q2 + sample31; + + m_count++; + if (m_count == N) { + // Q31 multiplication, t2 = m_q0 * m_q0 + q63_t t1 = q63_t(m_q0) * q63_t(m_q0); + q31_t t2 = __SSAT((t1 >> 31), 31); + + // Q31 multiplication, t4 = m_q0 * m_q0 + q63_t t3 = q63_t(m_q1) * q63_t(m_q1); + q31_t t4 = __SSAT((t3 >> 31), 31); + + // Q31 multiplication, t9 = m_q0 * m_q1 * m_coeffDivTwo * 2 + q63_t t5 = q63_t(m_q0) * q63_t(m_q1); + q31_t t6 = __SSAT((t5 >> 31), 31); + q63_t t7 = t6 * m_coeffDivTwo; + q31_t t8 = __SSAT((t7 >> 31), 31); + q31_t t9 = t8 * 2; + + // value = m_q0 * m_q0 + m_q1 * m_q1 - m_q0 * m_q1 * m_coeffDivTwo * 2 + q31_t value = t2 + t4 - t9; + + bool previousCTCSSValid = CTCSS_VALID(m_result); + m_result = m_result | CTS_READY; + if (value >= m_threshold) + m_result = m_result | CTS_VALID; + else + m_result = m_result & ~CTS_VALID; + + if(previousCTCSSValid != CTCSS_VALID(m_result)) + DEBUG4("CTCSS Value / Threshold / Valid", value, m_threshold, CTCSS_VALID(m_result)); + + m_count = 0U; + m_q0 = 0; + m_q1 = 0; + } + + return m_result; +} + +CTCSSState CFMCTCSSRX::getState() +{ + return m_result; +} + +void CFMCTCSSRX::reset() +{ + m_q0 = 0; + m_q1 = 0; + m_result = CTS_NONE; + m_count = 0U; +} diff --git a/FMCTCSSRX.h b/FMCTCSSRX.h new file mode 100644 index 0000000..4c1dfc3 --- /dev/null +++ b/FMCTCSSRX.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMCTCSSRX_H) +#define FMCTCSSRX_H + +#include "Config.h" + +enum CTCSSState +{ + CTS_NONE = 0, + CTS_READY = 1, + CTS_VALID = 2, + CTS_READY_VALID = CTS_READY | CTS_VALID +}; + +inline CTCSSState operator|(CTCSSState a, CTCSSState b) +{ + return static_cast(static_cast(a) | static_cast(b)); +} + +inline CTCSSState operator&(CTCSSState a, CTCSSState b) +{ + return static_cast(static_cast(a) & static_cast(b)); +} + +inline CTCSSState operator~(CTCSSState a) +{ + return static_cast(~(static_cast(a))); +} + +#define CTCSS_READY(a) ((a & CTS_READY) != 0) +#define CTCSS_NOT_READY(a) ((a & CTS_READY) == 0) +#define CTCSS_VALID(a) ((a & CTS_VALID) != 0) +#define CTCSS_NOT_VALID(a) ((a & CTS_VALID) == 0) + +class CFMCTCSSRX { +public: + CFMCTCSSRX(); + + uint8_t setParams(uint8_t frequency, uint8_t threshold, uint8_t level); + + CTCSSState process(q15_t sample); + + CTCSSState getState(); + + void reset(); + +private: + q63_t m_coeffDivTwo; + q31_t m_threshold; + uint16_t m_count; + q31_t m_q0; + q31_t m_q1; + CTCSSState m_result; + q15_t m_rxLevelInverse; +}; + +#endif diff --git a/FMCTCSSTX.cpp b/FMCTCSSTX.cpp new file mode 100644 index 0000000..fcffbb5 --- /dev/null +++ b/FMCTCSSTX.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "Globals.h" +#include "FMCTCSSTX.h" + +const struct TX_CTCSS_TABLE { + uint8_t frequency; + uint16_t length; + q31_t increment; +} TX_CTCSS_TABLE_DATA[] = { + { 67U, 358U, 5995059}, + { 69U, 346U, 6200860}, + { 71U, 334U, 6433504}, + { 74U, 323U, 6657200}, + { 77U, 312U, 6889844}, + { 79U, 301U, 7131436}, + { 82U, 291U, 7381976}, + { 85U, 281U, 7641463}, + { 88U, 271U, 7918846}, + { 91U, 262U, 8187282}, + { 94U, 253U, 8482561}, + { 97U, 246U, 8715205}, + {100U, 240U, 8947849}, + {103U, 232U, 9261024}, + {107U, 224U, 9592094}, + {110U, 216U, 9923165}, + {114U, 209U, 10272131}, + {118U, 202U, 10630045}, + {123U, 195U, 11005854}, + {127U, 189U, 11390612}, + {131U, 182U, 11793265}, + {136U, 176U, 12213814}, + {141U, 170U, 12643310}, + {146U, 164U, 13081755}, + {151U, 159U, 13547043}, + {156U, 153U, 14021279}, + {159U, 150U, 14298662}, + {162U, 148U, 14513411}, + {165U, 145U, 14808690}, + {167U, 143U, 15023438}, + {171U, 140U, 15327665}, + {173U, 138U, 15551361}, + {177U, 135U, 15864536}, + {179U, 133U, 16097180}, + {183U, 131U, 16419303}, + {186U, 129U, 16660894}, + {189U, 126U, 16991965}, + {192U, 124U, 17251452}, + {196U, 122U, 17591471}, + {199U, 120U, 17850958}, + {203U, 118U, 18208872}, + {206U, 116U, 18477308}, + {210U, 114U, 18853117}, + {218U, 110U, 19515258}, + {225U, 106U, 20195295}, + {229U, 105U, 20499521}, + {233U, 103U, 20902175}, + {241U, 99U, 21635898}, + {250U, 96U, 22396465}, + {254U, 94U, 22736484}}; + +const uint8_t CTCSS_TABLE_DATA_LEN = 50U; + +CFMCTCSSTX::CFMCTCSSTX() : +m_values(NULL), +m_length(0U), +m_n(0U) +{ +} + +uint8_t CFMCTCSSTX::setParams(uint8_t frequency, uint8_t level) +{ + const TX_CTCSS_TABLE* entry = NULL; + + for (uint8_t i = 0U; i < CTCSS_TABLE_DATA_LEN; i++) { + if (TX_CTCSS_TABLE_DATA[i].frequency == frequency) { + entry = TX_CTCSS_TABLE_DATA + i; + break; + } + } + + if (entry == NULL) + return 4U; + + m_length = entry->length; + + delete[] m_values; + m_values = new q15_t[m_length]; + + q31_t arg = 0; + for (uint16_t i = 0U; i < m_length; i++) { + q63_t value = ::arm_sin_q31(arg) * q63_t(level * 13); + m_values[i] = q15_t(__SSAT((value >> 31), 16)); + + arg += entry->increment; + } + + return 0U; +} + +q15_t CFMCTCSSTX::getAudio() +{ + q15_t sample = m_values[m_n++]; + if(m_n >= m_length) + m_n = 0U; + + return sample; +} diff --git a/FMCTCSSTX.h b/FMCTCSSTX.h new file mode 100644 index 0000000..0c3e6e5 --- /dev/null +++ b/FMCTCSSTX.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMCTCSSTX_H) +#define FMCTCSSTX_H + +#include "Config.h" + +class CFMCTCSSTX { +public: + CFMCTCSSTX(); + + uint8_t setParams(uint8_t frequency, uint8_t level); + + q15_t getAudio(); + +private: + q15_t* m_values; + uint16_t m_length; + uint16_t m_n; +}; + +#endif diff --git a/FMDirectForm1.h b/FMDirectForm1.h new file mode 100644 index 0000000..450274d --- /dev/null +++ b/FMDirectForm1.h @@ -0,0 +1,111 @@ + +/******************************************************************************* +This header file has been taken from: +"A Collection of Useful C++ Classes for Digital Signal Processing" +By Vinnie Falco +Bernd Porr adapted it for Linux and turned it into a filter using +fixed point arithmetic. +-------------------------------------------------------------------------------- +License: MIT License (http://www.opensource.org/licenses/mit-license.php) +Copyright (c) 2009 by Vinnie Falco +Copyright (C) 2013-2017, Bernd Porr, mail@berndporr.me.uk +Copyright (C) 2020 , Mario Molitor , mario_molitor@web.de +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. +*******************************************************************************/ + +// based on https://raw.githubusercontent.com/berndporr/iir_fixed_point/master/DirectFormI.h + +#include "Globals.h" + +#ifndef DIRECTFORMI_H_ +#define DIRECTFORMI_H_ +class CFMDirectFormI +{ +public: + + // convenience function which takes the a0 argument but ignores it! + CFMDirectFormI(const q31_t b0, const q31_t b1, const q31_t b2, + const q31_t, const q31_t a1, const q31_t a2) + { + // FIR coefficients + c_b0 = b0; + c_b1 = b1; + c_b2 = b2; + // IIR coefficients + c_a1 = a1; + c_a2 = a2; + reset(); + } + + CFMDirectFormI(const CFMDirectFormI &my) + { + // delay line + m_x2 = my.m_x2; // x[n-2] + m_y2 = my.m_y2; // y[n-2] + m_x1 = my.m_x1; // x[n-1] + m_y1 = my.m_y1; // y[n-1] + + // coefficients + c_b0 = my.c_b0; + c_b1 = my.c_b1; + c_b2 = my.c_b2; // FIR + c_a1 = my.c_a1; + c_a2 = my.c_a2; // IIR + } + + void reset () + { + m_x1 = 0; + m_x2 = 0; + m_y1 = 0; + m_y2 = 0; + } + + // filtering operation: one sample in and one out + inline q15_t filter(const q15_t in) + { + // calculate the output + register q31_t out_upscaled = c_b0 * in + + c_b1 * m_x1 + + c_b2 * m_x2 + - c_a1 * m_y1 + - c_a2 * m_y2; + + q15_t out = __SSAT(out_upscaled >> 15, 15); + + // update the delay lines + m_x2 = m_x1; + m_y2 = m_y1; + m_x1 = in; + m_y1 = out; + + return out; + } + +private: + // delay line + q31_t m_x2; // x[n-2] + q31_t m_y2; // y[n-2] + q31_t m_x1; // x[n-1] + q31_t m_y1; // y[n-1] + + // coefficients + q31_t c_b0,c_b1,c_b2; // FIR + q31_t c_a1,c_a2; // IIR +}; + +#endif /* DIRECTFORMI_H */ \ No newline at end of file diff --git a/FMDownsampleRB.cpp b/FMDownsampleRB.cpp new file mode 100644 index 0000000..7d2cc2a --- /dev/null +++ b/FMDownsampleRB.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + + +#include "FMDownsampleRB.h" + +CFMDownsampleRB::CFMDownsampleRB(uint16_t length) : +m_length(length), +m_head(0U), +m_tail(0U), +m_full(false), +m_overflow(false) +{ + m_samples = new uint8_t[length]; +} + +uint16_t CFMDownsampleRB::getSpace() const +{ + uint16_t n = 0U; + + if (m_tail == m_head) + n = m_full ? 0U : m_length; + else if (m_tail < m_head) + n = m_length - m_head + m_tail; + else + n = m_tail - m_head; + + if (n > m_length) + n = 0U; + + return n; +} + +uint16_t CFMDownsampleRB::getData() const +{ + if (m_tail == m_head) + return m_full ? m_length : 0U; + else if (m_tail < m_head) + return m_head - m_tail; + else + return m_length - m_tail + m_head; +} + +bool CFMDownsampleRB::put(uint8_t sample) +{ + if (m_full) { + m_overflow = true; + return false; + } + + m_samples[m_head] = sample; + + m_head++; + if (m_head >= m_length) + m_head = 0U; + + if (m_head == m_tail) + m_full = true; + + return true; +} + +bool CFMDownsampleRB::get(uint8_t& sample) +{ + if (m_head == m_tail && !m_full) + return false; + + sample = m_samples[m_tail]; + + m_full = false; + + m_tail++; + if (m_tail >= m_length) + m_tail = 0U; + + return true; +} + +bool CFMDownsampleRB::hasOverflowed() +{ + bool overflow = m_overflow; + + m_overflow = false; + + return overflow; +} diff --git a/FMDownsampleRB.h b/FMDownsampleRB.h new file mode 100644 index 0000000..6e8e466 --- /dev/null +++ b/FMDownsampleRB.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMRB_H) +#define FMRB_H + +#if defined(STM32F4XX) +#include "stm32f4xx.h" +#elif defined(STM32F7XX) +#include "stm32f7xx.h" +#elif defined(STM32F105xC) +#include "stm32f1xx.h" +#include +#else +#include +#endif + +#if defined(__SAM3X8E__) || defined(STM32F105xC) +#define ARM_MATH_CM3 +#elif defined(STM32F7XX) +#define ARM_MATH_CM7 +#elif defined(STM32F4XX) || defined(__MK20DX256__) || defined(__MK64FX512__) || defined(__MK66FX1M0__) +#define ARM_MATH_CM4 +#else +#error "Unknown processor type" +#endif + +#include + +class CFMDownsampleRB { +public: + CFMDownsampleRB(uint16_t length); + + uint16_t getSpace() const; + + uint16_t getData() const; + + bool put(uint8_t sample); + + bool get(uint8_t& sample); + + bool hasOverflowed(); + +private: + uint16_t m_length; + volatile uint8_t* m_samples; + volatile uint16_t m_head; + volatile uint16_t m_tail; + volatile bool m_full; + bool m_overflow; +}; + +#endif diff --git a/FMDownsampler.cpp b/FMDownsampler.cpp new file mode 100644 index 0000000..51e9580 --- /dev/null +++ b/FMDownsampler.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * Copyright (C) 2020 by Geoffrey Merck F4FXL - KC3FRA + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "FMDownsampler.h" + + +CFMDownsampler::CFMDownsampler(uint16_t length) : +m_ringBuffer(length),//length might need tweaking +m_packIndex(0), +m_downSampleIndex(0) +{ + m_samplePack = 0; +} + +void CFMDownsampler::addSample(q15_t sample) +{ + //only take one of three samples + if(m_downSampleIndex == 0) { + switch(m_packIndex){ + case 0: + m_samplePack = int32_t(sample) << 12; + break; + case 1:{ + m_samplePack |= int32_t(sample); + + //we did not use MSB; skip it + m_ringBuffer.put(m_samplePackBytes[1]); + m_ringBuffer.put(m_samplePackBytes[2]); + m_ringBuffer.put(m_samplePackBytes[3]); + + m_samplePack = 0; + } + break; + default: + //should never happen + break; + } + m_packIndex++; + if(m_packIndex >= 2) + m_packIndex = 0; + } + + m_downSampleIndex++; + if(m_downSampleIndex >= 3) + m_downSampleIndex = 0; +} \ No newline at end of file diff --git a/FMDownsampler.h b/FMDownsampler.h new file mode 100644 index 0000000..fd17e0a --- /dev/null +++ b/FMDownsampler.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * Copyright (C) 2020 by Geoffrey Merck F4FXL - KC3FRA + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMDOWNSAMPLER_H) +#define FMDOWNSAMPLER_H + +#include "Config.h" +#include "FMDownsampleRB.h" + +class CFMDownsampler { +public: + CFMDownsampler(uint16_t length); + void addSample(q15_t sample); + inline bool getPackedData(uint8_t& data){ return m_ringBuffer.get(data); }; + inline bool hasOverflowed() { return m_ringBuffer.hasOverflowed(); }; + +private: + CFMDownsampleRB m_ringBuffer; + union { + int32_t m_samplePack; + int8_t m_samplePackBytes[4]; + }; + uint8_t m_packIndex; + uint8_t m_downSampleIndex; +}; + +#endif \ No newline at end of file diff --git a/FMKeyer.cpp b/FMKeyer.cpp new file mode 100644 index 0000000..36a2815 --- /dev/null +++ b/FMKeyer.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "Globals.h" +#include "FMKeyer.h" + +const struct { + char c; + uint32_t pattern; + uint8_t length; +} SYMBOL_LIST[] = { + {'A', 0xB8000000U, 8U}, + {'B', 0xEA800000U, 12U}, + {'C', 0xEBA00000U, 14U}, + {'D', 0xEA000000U, 10U}, + {'E', 0x80000000U, 4U}, + {'F', 0xAE800000U, 12U}, + {'G', 0xEE800000U, 12U}, + {'H', 0xAA000000U, 10U}, + {'I', 0xA0000000U, 6U}, + {'J', 0xBBB80000U, 16U}, + {'K', 0xEB800000U, 12U}, + {'L', 0xBA800000U, 12U}, + {'M', 0xEE000000U, 10U}, + {'N', 0xE8000000U, 8U}, + {'O', 0xEEE00000U, 14U}, + {'P', 0xBBA00000U, 14U}, + {'Q', 0xEEB80000U, 16U}, + {'R', 0xBA000000U, 10U}, + {'S', 0xA8000000U, 8U}, + {'T', 0xE0000000U, 6U}, + {'U', 0xAE000000U, 10U}, + {'V', 0xAB800000U, 12U}, + {'W', 0xBB800000U, 12U}, + {'X', 0xEAE00000U, 14U}, + {'Y', 0xEBB80000U, 16U}, + {'Z', 0xEEA00000U, 14U}, + {'1', 0xBBBB8000U, 20U}, + {'2', 0xAEEE0000U, 18U}, + {'3', 0xABB80000U, 16U}, + {'4', 0xAAE00000U, 14U}, + {'5', 0xAA800000U, 12U}, + {'6', 0xEAA00000U, 14U}, + {'7', 0xEEA80000U, 16U}, + {'8', 0xEEEA0000U, 18U}, + {'9', 0xEEEE8000U, 20U}, + {'0', 0xEEEEE000U, 22U}, + {'/', 0xEAE80000U, 16U}, + {'?', 0xAEEA0000U, 18U}, + {',', 0xEEAEE000U, 22U}, + {'-', 0xEAAE0000U, 18U}, + {'=', 0xEAB80000U, 16U}, + {' ', 0x00000000U, 4U}, + {0U, 0x00000000U, 0U} +}; + +const uint8_t BIT_MASK_TABLE[] = {0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U}; + +#define WRITE_BIT_FM(p,i,b) p[(i)>>3] = (b) ? (p[(i)>>3] | BIT_MASK_TABLE[(i)&7]) : (p[(i)>>3] & ~BIT_MASK_TABLE[(i)&7]) +#define READ_BIT_FM(p,i) (p[(i)>>3] & BIT_MASK_TABLE[(i)&7]) + +CFMKeyer::CFMKeyer() : +m_wanted(false), +m_poBuffer(), +m_poLen(0U), +m_poPos(0U), +m_dotLen(0U), +m_dotPos(0U), +m_audio(NULL), +m_audioLen(0U), +m_audioPos(0U), +m_highLevel(0U), +m_lowLevel(0) +{ +} + +uint8_t CFMKeyer::setParams(const char* text, uint8_t speed, uint16_t frequency, uint8_t highLevel, uint8_t lowLevel) +{ + m_poLen = 0U; + + for (uint8_t i = 0U; text[i] != '\0'; i++) { + for (uint8_t j = 0U; SYMBOL_LIST[j].c != 0U; j++) { + if (SYMBOL_LIST[j].c == text[i]) { + uint32_t MASK = 0x80000000U; + for (uint8_t k = 0U; k < SYMBOL_LIST[j].length; k++, m_poLen++, MASK >>= 1) { + bool b = (SYMBOL_LIST[j].pattern & MASK) == MASK; + WRITE_BIT_FM(m_poBuffer, m_poLen, b); + + if (m_poLen >= 995U) { + m_poLen = 0U; + return 4U; + } + } + + break; + } + } + } + + m_highLevel = q15_t(highLevel); + m_lowLevel = q15_t(lowLevel); + + m_dotLen = 24000U / speed; // In samples + + m_audioLen = 24000U / frequency; // In samples + + delete[] m_audio; + m_audio = new bool[m_audioLen]; + + for (uint16_t i = 0U; i < m_audioLen; i++) { + if (i < (m_audioLen / 2U)) + m_audio[i] = true; + else + m_audio[i] = false; + } + + return 0U; +} + +q15_t CFMKeyer::getHighAudio() +{ + q15_t output = 0U; + if (!m_wanted) + return 0U; + + bool b = READ_BIT_FM(m_poBuffer, m_poPos); + if (b) + output = m_audio[m_audioPos] ? m_highLevel : -m_highLevel; + + m_audioPos++; + if (m_audioPos >= m_audioLen) + m_audioPos = 0U; + m_dotPos++; + if (m_dotPos >= m_dotLen) { + m_dotPos = 0U; + m_poPos++; + if (m_poPos >= m_poLen) { + stop(); + return output; + } + } + + return output; +} + +q15_t CFMKeyer::getLowAudio() +{ + q15_t output = 0U; + if (!m_wanted) + return 0U; + + bool b = READ_BIT_FM(m_poBuffer, m_poPos); + if (b) + output = m_audio[m_audioPos] ? m_lowLevel : -m_lowLevel; + + m_audioPos++; + if (m_audioPos >= m_audioLen) + m_audioPos = 0U; + m_dotPos++; + if (m_dotPos >= m_dotLen) { + m_dotPos = 0U; + m_poPos++; + if (m_poPos >= m_poLen) { + stop(); + return output; + } + } + + return output; +} + +void CFMKeyer::start() +{ + if (isRunning()) + return; + + m_wanted = true; + m_poPos = 0U; + m_dotPos = 0U; + m_audioPos = 0U; +} + +void CFMKeyer::stop() +{ + m_wanted = false; + m_poPos = 0U; + m_dotPos = 0U; + m_audioPos = 0U; +} + +bool CFMKeyer::isRunning() const +{ + return m_poPos > 0U || m_dotPos > 0U || m_audioPos > 0U; +} diff --git a/FMKeyer.h b/FMKeyer.h new file mode 100644 index 0000000..799e85c --- /dev/null +++ b/FMKeyer.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMKeyer_H) +#define FMKeyer_H + +#include "Config.h" + +class CFMKeyer { +public: + CFMKeyer(); + + uint8_t setParams(const char* text, uint8_t speed, uint16_t frequency, uint8_t highLevel, uint8_t lowLevel); + + q15_t getHighAudio(); + q15_t getLowAudio(); + + void start(); + void stop(); + + bool isRunning() const; + +private: + bool m_wanted; + uint8_t m_poBuffer[1000U]; + uint16_t m_poLen; + uint16_t m_poPos; + uint16_t m_dotLen; + uint16_t m_dotPos; + bool* m_audio; + uint16_t m_audioLen; + uint16_t m_audioPos; + q15_t m_highLevel; + q15_t m_lowLevel; +}; + +#endif diff --git a/FMTimeout.cpp b/FMTimeout.cpp new file mode 100644 index 0000000..893e53b --- /dev/null +++ b/FMTimeout.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "Globals.h" +#include "FMTimeout.h" + +// 400 Hz sine wave at 24000 Hz sample rate +const q15_t BUSY_AUDIO[] = {0, 3426, 6813, 10126, 13328, 16384, 19261, 21926, 24351, 26510, 28378, 29935, 31164, 32052, 32588, 32767, 32588, 32052, 31164, 29935, 28378, 26510, 24351, + 21926, 19261, 16384, 13328, 10126, 6813, 3425, 0, -3425, -6813, -10126, -13328, -16384, -19261, -21926, -24351, -26510, -28378, -29935, -31164, -32052, + -32588, -32768, -32588, -32052, -31164, -29935, -28378, -26510, -24351, -21926, -19261, -16384, -13328, -10126, -6813, -3425}; +const uint8_t BUSY_AUDIO_LEN = 60U; + +CFMTimeout::CFMTimeout() : +m_level(128 * 128), +m_running(false), +m_pos(0U), +m_n(0U) +{ +} + +void CFMTimeout::setParams(uint8_t level) +{ + m_level = q15_t(level * 128); +} + +q15_t CFMTimeout::getAudio() +{ + q15_t sample = 0U; + if (!m_running) + return sample; + + if (m_pos > 12000U) { + q31_t sample = BUSY_AUDIO[m_n] * m_level; + sample = q15_t(__SSAT((sample >> 15), 16)); + + m_n++; + if (m_n >= BUSY_AUDIO_LEN) + m_n = 0U; + } else { + sample = 0U; + } + + m_pos++; + if (m_pos >= 24000U) + m_pos = 0U; + + return sample; +} + +void CFMTimeout::start() +{ + m_running = true; + m_pos = 0U; + m_n = 0U; +} + +void CFMTimeout::stop() +{ + m_running = false; +} diff --git a/FMTimeout.h b/FMTimeout.h new file mode 100644 index 0000000..b2f747c --- /dev/null +++ b/FMTimeout.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMTimeout_H) +#define FMTimeout_H + +#include "Config.h" + +class CFMTimeout { +public: + CFMTimeout(); + + void setParams(uint8_t level); + + void start(); + void stop(); + + q15_t getAudio(); + +private: + q15_t m_level; + bool m_running; + uint32_t m_pos; + uint8_t m_n; +}; + +#endif diff --git a/FMTimer.cpp b/FMTimer.cpp new file mode 100644 index 0000000..826bc13 --- /dev/null +++ b/FMTimer.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2009,2010,2015,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include "Config.h" +#include "Globals.h" +#include "FMTimer.h" + +CFMTimer::CFMTimer() : +m_timeout(0U), +m_timer(0U) +{ +} + +void CFMTimer::setTimeout(uint16_t secs, uint32_t msecs) +{ + m_timeout = (secs * 24000U) + (msecs * 24U); +} + +uint32_t CFMTimer::getTimeout() const +{ + return m_timeout / 24U; +} + +void CFMTimer::start() +{ + if (m_timeout > 0U) + m_timer = 1U; +} + +void CFMTimer::stop() +{ + m_timer = 0U; +} + +void CFMTimer::clock(uint8_t length) +{ + if (m_timer > 0U && m_timeout > 0U) + m_timer += length; +} + +bool CFMTimer::isRunning() const +{ + return m_timer > 0U; +} + +bool CFMTimer::hasExpired() const +{ + if (m_timeout == 0U || m_timer == 0U) + return false; + + if (m_timer > m_timeout) + return true; + + return false; +} diff --git a/FMTimer.h b/FMTimer.h new file mode 100644 index 0000000..00dc343 --- /dev/null +++ b/FMTimer.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2009,2010,2015,2020 by Jonathan Naylor G4KLX + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#if !defined(FMTimer_H) +#define FMTimer_H + +#include "Config.h" + +class CFMTimer { +public: + CFMTimer(); + + void setTimeout(uint16_t secs, uint32_t msecs); + + uint32_t getTimeout() const; + + void start(); + + void stop(); + + void clock(uint8_t length); + + bool isRunning() const; + + bool hasExpired() const; + +private: + uint32_t m_timeout; + uint32_t m_timer; +}; + +#endif diff --git a/Globals.h b/Globals.h index 149cb25..5cb147a 100644 --- a/Globals.h +++ b/Globals.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -50,6 +50,7 @@ enum MMDVM_STATE { STATE_P25 = 4, STATE_NXDN = 5, STATE_POCSAG = 6, + STATE_FM = 10, // Dummy states start at 90 STATE_NXDNCAL1K = 91, @@ -62,7 +63,13 @@ enum MMDVM_STATE { STATE_DMRCAL = 98, STATE_DSTARCAL = 99, STATE_INTCAL = 100, - STATE_POCSAGCAL = 101 + STATE_POCSAGCAL = 101, + STATE_FMCAL10K = 102, + STATE_FMCAL12K = 103, + STATE_FMCAL15K = 104, + STATE_FMCAL20K = 105, + STATE_FMCAL25K = 106, + STATE_FMCAL30K = 107 }; #include "SerialPort.h" @@ -82,6 +89,7 @@ enum MMDVM_STATE { #include "POCSAGTX.h" #include "CalDStarRX.h" #include "CalDStarTX.h" +#include "CalFM.h" #include "CalDMR.h" #include "CalP25.h" #include "CalNXDN.h" @@ -90,6 +98,7 @@ enum MMDVM_STATE { #include "CWIdTX.h" #include "Debug.h" #include "IO.h" +#include "FM.h" const uint8_t MARK_SLOT1 = 0x08U; const uint8_t MARK_SLOT2 = 0x04U; @@ -114,6 +123,7 @@ extern bool m_ysfEnable; extern bool m_p25Enable; extern bool m_nxdnEnable; extern bool m_pocsagEnable; +extern bool m_fmEnable; extern bool m_duplex; @@ -144,9 +154,12 @@ extern CNXDNTX nxdnTX; extern CPOCSAGTX pocsagTX; +extern CFM fm; + extern CCalDStarRX calDStarRX; extern CCalDStarTX calDStarTX; extern CCalDMR calDMR; +extern CCalFM calFM; extern CCalP25 calP25; extern CCalNXDN calNXDN; extern CCalPOCSAG calPOCSAG; @@ -155,4 +168,3 @@ extern CCalRSSI calRSSI; extern CCWIdTX cwIdTX; #endif - diff --git a/IO.cpp b/IO.cpp index 9d868d0..9db8b5f 100644 --- a/IO.cpp +++ b/IO.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX * Copyright (C) 2015 by Jim Mclaughlin KI6ZUM * Copyright (C) 2016 by Colin Durbridge G4EML * @@ -55,6 +55,7 @@ m_ysfTXLevel(128 * 128), m_p25TXLevel(128 * 128), m_nxdnTXLevel(128 * 128), m_pocsagTXLevel(128 * 128), +m_fmTXLevel(128 * 128), m_rxDCOffset(DC_OFFSET), m_txDCOffset(DC_OFFSET), m_ledCount(0U), @@ -104,6 +105,7 @@ void CIO::selfTest() setP25Int(ledValue); setNXDNInt(ledValue); setPOCSAGInt(ledValue); + setFMInt(ledValue); #endif delayInt(250); } @@ -115,105 +117,56 @@ void CIO::selfTest() setP25Int(false); setNXDNInt(false); setPOCSAGInt(false); + setFMInt(false); delayInt(250); - - setDStarInt(true); setDMRInt(true); - setYSFInt(false); - setP25Int(false); - setNXDNInt(false); - setPOCSAGInt(false); delayInt(250); - - setDStarInt(true); - setDMRInt(true); setYSFInt(true); - setP25Int(false); - setNXDNInt(false); - setPOCSAGInt(false); - - delayInt(250); - setDStarInt(true); - setDMRInt(true); - setYSFInt(true); + delayInt(250); setP25Int(true); - setNXDNInt(false); - setPOCSAGInt(false); - - delayInt(250); - setDStarInt(true); - setDMRInt(true); - setYSFInt(true); - setP25Int(true); +#if !defined(USE_ALTERNATE_NXDN_LEDS) + delayInt(250); setNXDNInt(true); - setPOCSAGInt(false); - - delayInt(250); +#endif - setDStarInt(true); - setDMRInt(true); - setYSFInt(true); - setP25Int(true); - setNXDNInt(true); +#if !defined(USE_ALTERNATE_POCSAG_LEDS) + delayInt(250); setPOCSAGInt(true); - - delayInt(250); +#endif - setDStarInt(true); - setDMRInt(true); - setYSFInt(true); - setP25Int(true); - setNXDNInt(true); +#if !defined(USE_ALTERNATE_FM_LEDS) + delayInt(250); + setFMInt(true); + + delayInt(250); + setFMInt(false); +#endif + +#if !defined(USE_ALTERNATE_POCSAG_LEDS) + delayInt(250); setPOCSAGInt(false); - - delayInt(250); +#endif - setDStarInt(true); - setDMRInt(true); - setYSFInt(true); - setP25Int(true); +#if !defined(USE_ALTERNATE_NXDN_LEDS) + delayInt(250); setNXDNInt(false); - setPOCSAGInt(false); +#endif delayInt(250); - - setDStarInt(true); - setDMRInt(true); - setYSFInt(true); setP25Int(false); - setNXDNInt(false); - setPOCSAGInt(false); delayInt(250); - - setDStarInt(true); - setDMRInt(true); setYSFInt(false); - setP25Int(false); - setNXDNInt(false); - setPOCSAGInt(false); delayInt(250); - - setDStarInt(true); setDMRInt(false); - setYSFInt(false); - setP25Int(false); - setNXDNInt(false); - setPOCSAGInt(false); delayInt(250); - setDStarInt(false); - setDMRInt(false); - setYSFInt(false); - setP25Int(false); - setNXDNInt(false); - setPOCSAGInt(false); #endif } @@ -343,9 +296,17 @@ void CIO::process() #else ::arm_fir_fast_q15(&m_boxcar10Filter, samples, vals, RX_BLOCK_SIZE); #endif - nxdnRX.samples(vals, rssi, RX_BLOCK_SIZE); } + + if (m_fmEnable) { + bool cos = getCOSInt(); +#if defined(USE_DCBLOCKER) + fm.samples(cos, dcSamples, RX_BLOCK_SIZE); +#else + fm.samples(cos, samples, RX_BLOCK_SIZE); +#endif + } } else if (m_modemState == STATE_DSTAR) { if (m_dstarEnable) { q15_t vals[RX_BLOCK_SIZE]; @@ -402,6 +363,13 @@ void CIO::process() #endif nxdnRX.samples(vals, rssi, RX_BLOCK_SIZE); } + } else if (m_modemState == STATE_FM) { + bool cos = getCOSInt(); +#if defined(USE_DCBLOCKER) + fm.samples(cos, dcSamples, RX_BLOCK_SIZE); +#else + fm.samples(cos, samples, RX_BLOCK_SIZE); +#endif } else if (m_modemState == STATE_DSTARCAL) { q15_t vals[RX_BLOCK_SIZE]; #if defined(USE_DCBLOCKER) @@ -450,6 +418,9 @@ void CIO::write(MMDVM_STATE mode, q15_t* samples, uint16_t length, const uint8_t case STATE_POCSAG: txLevel = m_pocsagTXLevel; break; + case STATE_FM: + txLevel = m_fmTXLevel; + break; default: txLevel = m_cwIdTXLevel; break; @@ -498,10 +469,11 @@ void CIO::setMode() setP25Int(m_modemState == STATE_P25); setNXDNInt(m_modemState == STATE_NXDN); setPOCSAGInt(m_modemState == STATE_POCSAG); + setFMInt(m_modemState == STATE_FM); #endif } -void CIO::setParameters(bool rxInvert, bool txInvert, bool pttInvert, uint8_t rxLevel, uint8_t cwIdTXLevel, uint8_t dstarTXLevel, uint8_t dmrTXLevel, uint8_t ysfTXLevel, uint8_t p25TXLevel, uint8_t nxdnTXLevel, uint8_t pocsagTXLevel, int16_t txDCOffset, int16_t rxDCOffset) +void CIO::setParameters(bool rxInvert, bool txInvert, bool pttInvert, uint8_t rxLevel, uint8_t cwIdTXLevel, uint8_t dstarTXLevel, uint8_t dmrTXLevel, uint8_t ysfTXLevel, uint8_t p25TXLevel, uint8_t nxdnTXLevel, uint8_t pocsagTXLevel, uint8_t fmTXLevel, int16_t txDCOffset, int16_t rxDCOffset) { m_pttInvert = pttInvert; @@ -513,6 +485,7 @@ void CIO::setParameters(bool rxInvert, bool txInvert, bool pttInvert, uint8_t rx m_p25TXLevel = q15_t(p25TXLevel * 128); m_nxdnTXLevel = q15_t(nxdnTXLevel * 128); m_pocsagTXLevel = q15_t(pocsagTXLevel * 128); + m_fmTXLevel = q15_t(fmTXLevel * 128); m_rxDCOffset = DC_OFFSET + rxDCOffset; m_txDCOffset = DC_OFFSET + txDCOffset; @@ -563,4 +536,3 @@ bool CIO::hasLockout() const { return m_lockout; } - diff --git a/IO.h b/IO.h index 1fb89b9..3ccbfb0 100644 --- a/IO.h +++ b/IO.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,7 +42,7 @@ public: void interrupt(); - void setParameters(bool rxInvert, bool txInvert, bool pttInvert, uint8_t rxLevel, uint8_t cwIdTXLevel, uint8_t dstarTXLevel, uint8_t dmrTXLevel, uint8_t ysfTXLevel, uint8_t p25TXLevel, uint8_t nxdnTXLevel, uint8_t pocsagTXLevel, int16_t txDCOffset, int16_t rxDCOffset); + void setParameters(bool rxInvert, bool txInvert, bool pttInvert, uint8_t rxLevel, uint8_t cwIdTXLevel, uint8_t dstarTXLevel, uint8_t dmrTXLevel, uint8_t ysfTXLevel, uint8_t p25TXLevel, uint8_t nxdnTXLevel, uint8_t pocsagTXLevel, uint8_t fmTXLevel, int16_t txDCOffset, int16_t rxDCOffset); void getOverflow(bool& adcOverflow, bool& dacOverflow); @@ -80,6 +80,7 @@ private: q15_t m_p25TXLevel; q15_t m_nxdnTXLevel; q15_t m_pocsagTXLevel; + q15_t m_fmTXLevel; uint16_t m_rxDCOffset; uint16_t m_txDCOffset; @@ -112,9 +113,9 @@ private: void setP25Int(bool on); void setNXDNInt(bool on); void setPOCSAGInt(bool on); + void setFMInt(bool on); void delayInt(unsigned int dly); }; #endif - diff --git a/IODue.cpp b/IODue.cpp index 569c6e4..b2aec0a 100644 --- a/IODue.cpp +++ b/IODue.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX * Copyright (C) 2015 by Jim Mclaughlin KI6ZUM * Copyright (C) 2016 by Colin Durbridge G4EML * @@ -35,6 +35,7 @@ #define PIN_P25 19 #define PIN_NXDN 20 #define PIN_POCSAG 4 +#define PIN_FM 5 #define ADC_CHER_Chan (1<<7) // ADC on Due pin A0 - Due AD7 - (1 << 7) #define ADC_ISR_EOC_Chan ADC_ISR_EOC7 #define ADC_CDR_Chan 7 @@ -50,6 +51,7 @@ #define PIN_P25 6 #define PIN_NXDN 5 #define PIN_POCSAG 4 +#define PIN_FM 3 #define ADC_CHER_Chan (1<<13) // ADC on Due pin A11 - Due AD13 - (1 << 13) #define ADC_ISR_EOC_Chan ADC_ISR_EOC13 #define ADC_CDR_Chan 13 @@ -67,6 +69,7 @@ #define PIN_P25 6 #define PIN_NXDN 5 #define PIN_POCSAG 4 +#define PIN_FM 3 #define ADC_CHER_Chan (1<<7) // ADC on Due pin A0 - Due AD7 - (1 << 7) #define ADC_ISR_EOC_Chan ADC_ISR_EOC7 #define ADC_CDR_Chan 7 @@ -107,6 +110,9 @@ void CIO::initInt() #if !defined(USE_ALTERNATE_POCSAG_LEDS) pinMode(PIN_POCSAG, OUTPUT); #endif +#if !defined(USE_ALTERNATE_POCSAG_LEDS) + pinMode(PIN_FM, OUTPUT); +#endif #endif } @@ -258,10 +264,19 @@ void CIO::setPOCSAGInt(bool on) #endif } +void CIO::setFMInt(bool on) +{ +#if defined(USE_ALTERNATE_FM_LEDS) + digitalWrite(PIN_DSTAR, on ? HIGH : LOW); + digitalWrite(PIN_YSF, on ? HIGH : LOW); +#else + digitalWrite(PIN_FM, on ? HIGH : LOW); +#endif +} + void CIO::delayInt(unsigned int dly) { delay(dly); } #endif - diff --git a/IOSTM.cpp b/IOSTM.cpp index 9139953..1d565fe 100644 --- a/IOSTM.cpp +++ b/IOSTM.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2016 by Jim McLaughlin KI6ZUM * Copyright (C) 2016,2017,2018 by Andy Uribe CA6JAU - * Copyright (C) 2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2017,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,8 +42,8 @@ POCSAG PB12 output CN10 Pin16 MDSTAR PC4 output CN10 Pin34 MDMR PC5 output CN10 Pin6 -MYSF PC2 output CN7 Pin35 -MP25 PC3 output CN7 Pin37 +MYSF PC2 output CN7 Pin35 +MP25 PC3 output CN7 Pin37 MNXDN PC6 output CN10 Pin4 MPOCSAG PC8 output CN10 Pin2 @@ -1438,6 +1438,23 @@ void CIO::setPOCSAGInt(bool on) #endif } +void CIO::setFMInt(bool on) +{ +#if defined(USE_ALTERNATE_FM_LEDS) + GPIO_WriteBit(PORT_DSTAR, PIN_DSTAR, on ? Bit_SET : Bit_RESET); + GPIO_WriteBit(PORT_YSF, PIN_YSF, on ? Bit_SET : Bit_RESET); +#if defined(MODE_PINS) && defined(STM32F4_NUCLEO_MORPHO_HEADER) && (defined(STM32F4_NUCLEO) || defined(STM32F722_RPT_HAT)) + GPIO_WriteBit(PORT_MDSTAR, PIN_MDSTAR, on ? Bit_SET : Bit_RESET); + GPIO_WriteBit(PORT_MYSF, PIN_MYSF, on ? Bit_SET : Bit_RESET); +#endif +#else + GPIO_WriteBit(PORT_FM, PIN_FM, on ? Bit_SET : Bit_RESET); +#if defined(MODE_PINS) && defined(STM32F4_NUCLEO_MORPHO_HEADER) && (defined(STM32F4_NUCLEO) || defined(STM32F722_RPT_HAT)) + GPIO_WriteBit(PORT_MFM, PIN_MFM, on ? Bit_SET : Bit_RESET); +#endif +#endif +} + // Simple delay function for STM32 // Example from: http://thehackerworkshop.com/?p=1209 void CIO::delayInt(unsigned int dly) diff --git a/IOSTM_CMSIS.cpp b/IOSTM_CMSIS.cpp index ce498d4..e3763b4 100644 --- a/IOSTM_CMSIS.cpp +++ b/IOSTM_CMSIS.cpp @@ -1,7 +1,7 @@ /* * Copyright (C) 2016 by Jim McLaughlin KI6ZUM * Copyright (C) 2016, 2017 by Andy Uribe CA6JAU - * Copyright (C) 2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2017,2018,2020 by Jonathan Naylor G4KLX * Copyright (C) 2017 by Wojciech Krutnik N0CALL * * This program is free software; you can redistribute it and/or modify @@ -41,6 +41,7 @@ YSF PB8 output P25 PB9 output NXDN PB10 output POCSAG PB11 output +FM PB12 output RX PB0 analog input (ADC1_8) RSSI PB1 analog input (ADC2_9) @@ -84,6 +85,9 @@ USART1_RXD PA10 input (AF) #define PIN_POCSAG 11 #define PORT_POCSAG GPIOB #define BB_POCSAG *((bitband_t)BITBAND_PERIPH(&PORT_POCSAG->ODR, PIN_POCSAG)) +#define PIN_FM 12 +#define PORT_FM GPIOB +#define BB_FM *((bitband_t)BITBAND_PERIPH(&PORT_FM->ODR, PIN_FM)) #define PIN_RX 0 #define PIN_RX_ADC_CH 8 @@ -154,9 +158,9 @@ void GPIOConfigPin(GPIO_TypeDef *port_ptr, uint32_t pin, uint32_t mode_cnf_value #if defined(STM32F1_POG) void FancyLEDEffect() { - bitband_t foo[] = {&BB_LED, &BB_COSLED, &BB_PTT, &BB_DMR, &BB_DSTAR, &BB_YSF, &BB_P25}; + bitband_t foo[] = {&BB_LED, &BB_COSLED, &BB_PTT, &BB_DMR, &BB_DSTAR, &BB_YSF, &BB_P25, &BB_NXDN, &BB_POCSAG, &BB_FM}; - for(int i=0; i<7; i++){ + for(int i=0; i<10; i++){ *foo[i] = 0x01; } GPIOConfigPin(PORT_USART1_TXD, PIN_USART1_TXD, GPIO_CRL_MODE0_1); @@ -172,12 +176,12 @@ void FancyLEDEffect() *((bitband_t)BITBAND_PERIPH(&PORT_USART1_TXD->ODR, PIN_USART1_TXD)) = 0x01; *foo[0] = 0x01; - for(int i=1; i<7; i++){ + for(int i=1; i<10; i++){ delay(SystemCoreClock/1000*10); *foo[i-1] = 0x00; *foo[i] = 0x01; } - for(int i=5; i>=0; i--){ + for(int i=10; i>=0; i--){ delay(SystemCoreClock/1000*10); *foo[i+1] = 0x00; *foo[i] = 0x01; @@ -228,6 +232,9 @@ static inline void GPIOInit() #if !defined(USE_ALTERNATE_POCSAG_LEDS) GPIOConfigPin(PORT_POCSAG, PIN_POCSAG, GPIO_CRL_MODE0_1); #endif +#if !defined(USE_ALTERNATE_FM_LEDS) + GPIOConfigPin(PORT_FM, PIN_FM, GPIO_CRL_MODE0_1); +#endif GPIOConfigPin(PORT_RX, PIN_RX, 0); #if defined(SEND_RSSI_DATA) @@ -460,6 +467,16 @@ void CIO::setPOCSAGInt(bool on) #endif } +void CIO::setFMInt(bool on) +{ +#if defined(USE_ALTERNATE_FM_LEDS) + BB_DSTAR = !!on; + BB_YSF = !!on; +#else + BB_FM = !!on; +#endif +} + void CIO::delayInt(unsigned int dly) { delay(dly); diff --git a/IOTeensy.cpp b/IOTeensy.cpp index 1d9aa79..aaece08 100644 --- a/IOTeensy.cpp +++ b/IOTeensy.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2016,2017,2018,2020 by Jonathan Naylor G4KLX * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,9 +37,11 @@ #if defined(__MK20DX256__) #define PIN_NXDN 2 #define PIN_POCSAG 3 +#define PIN_FM 4 #else #define PIN_NXDN 24 #define PIN_POCSAG 25 +#define PIN_FM 26 #endif #define PIN_ADC 5 // A0, Pin 14 #define PIN_RSSI 4 // Teensy 3.5/3.6, A16, Pin 35. Teensy 3.1/3.2, A17, Pin 28 @@ -76,6 +78,9 @@ void CIO::initInt() #if !defined(USE_ALTERNATE_POCSAG_LEDS) pinMode(PIN_POCSAG, OUTPUT); #endif +#if !defined(USE_ALTERNATE_FM_LEDS) + pinMode(PIN_FM, OUTPUT); +#endif #endif } @@ -243,6 +248,16 @@ void CIO::setPOCSAGInt(bool on) #endif } +void CIO::setFMInt(bool on) +{ +#if defined(USE_ALTERNATE_FM_LEDS) + digitalWrite(PIN_DSTAR, on ? HIGH : LOW); + digitalWrite(PIN_YSF, on ? HIGH : LOW); +#else + digitalWrite(PIN_FM, on ? HIGH : LOW); +#endif +} + void CIO::delayInt(unsigned int dly) { delay(dly); diff --git a/MMDVM.cpp b/MMDVM.cpp index 306cdcd..a6d4577 100644 --- a/MMDVM.cpp +++ b/MMDVM.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX * Copyright (C) 2016 by Mathis Schmieder DB9MAT * Copyright (C) 2016 by Colin Durbridge G4EML * @@ -32,6 +32,7 @@ bool m_ysfEnable = true; bool m_p25Enable = true; bool m_nxdnEnable = true; bool m_pocsagEnable = true; +bool m_fmEnable = true; bool m_duplex = true; @@ -59,11 +60,14 @@ CNXDNTX nxdnTX; CPOCSAGTX pocsagTX; +CFM fm; + CCalDStarRX calDStarRX; CCalDStarTX calDStarTX; CCalDMR calDMR; CCalP25 calP25; CCalNXDN calNXDN; +CCalFM calFM; CCalPOCSAG calPOCSAG; CCalRSSI calRSSI; @@ -106,6 +110,9 @@ void loop() if (m_pocsagEnable && (m_modemState == STATE_POCSAG || pocsagTX.busy())) pocsagTX.process(); + if (m_fmEnable && m_modemState == STATE_FM) + fm.process(); + if (m_modemState == STATE_DSTARCAL) calDStarTX.process(); @@ -121,6 +128,9 @@ void loop() if (m_modemState == STATE_POCSAGCAL) calPOCSAG.process(); + if (m_modemState == STATE_FMCAL10K || m_modemState == STATE_FMCAL12K || m_modemState == STATE_FMCAL15K || m_modemState == STATE_FMCAL20K || m_modemState == STATE_FMCAL25K ||m_modemState == STATE_FMCAL30K) + calFM.process(); + if (m_modemState == STATE_IDLE) cwIdTX.process(); } diff --git a/MMDVM.ino b/MMDVM.ino index dcb1fcf..17c4ad8 100644 --- a/MMDVM.ino +++ b/MMDVM.ino @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015,2016,2017,2018 by Jonathan Naylor G4KLX + * Copyright (C) 2015,2016,2017,2018,2020 by Jonathan Naylor G4KLX * Copyright (C) 2016 by Colin Durbridge G4EML * * This program is free software; you can redistribute it and/or modify @@ -29,6 +29,7 @@ bool m_ysfEnable = true; bool m_p25Enable = true; bool m_nxdnEnable = true; bool m_pocsagEnable = true; +bool m_fmEnable = true; bool m_duplex = true; @@ -56,9 +57,12 @@ CNXDNTX nxdnTX; CPOCSAGTX pocsagTX; +CFM fm; + CCalDStarRX calDStarRX; CCalDStarTX calDStarTX; CCalDMR calDMR; +CCalFM calFM; CCalP25 calP25; CCalNXDN calNXDN; CCalPOCSAG calPOCSAG; @@ -103,12 +107,18 @@ void loop() if (m_pocsagEnable && (m_modemState == STATE_POCSAG || pocsagTX.busy())) pocsagTX.process(); + if (m_fmEnable && m_modemState == STATE_FM) + fm.process(); + if (m_modemState == STATE_DSTARCAL) calDStarTX.process(); if (m_modemState == STATE_DMRCAL || m_modemState == STATE_LFCAL || m_modemState == STATE_DMRCAL1K || m_modemState == STATE_DMRDMO1K) calDMR.process(); + if (m_modemState == STATE_FMCAL10K || m_modemState == STATE_FMCAL12K || m_modemState == STATE_FMCAL15K || m_modemState == STATE_FMCAL20K || m_modemState == STATE_FMCAL25K || m_modemState == STATE_FMCAL30K) + calFM.process(); + if (m_modemState == STATE_P25CAL1K) calP25.process(); @@ -121,4 +131,3 @@ void loop() if (m_modemState == STATE_IDLE) cwIdTX.process(); } - diff --git a/MMDVM_STM32F4xx.coproj b/MMDVM_STM32F4xx.coproj index 2990342..4f1592a 100644 --- a/MMDVM_STM32F4xx.coproj +++ b/MMDVM_STM32F4xx.coproj @@ -273,6 +273,7 @@ + @@ -290,6 +291,7 @@ + @@ -335,4 +337,4 @@ - \ No newline at end of file + diff --git a/SerialPort.cpp b/SerialPort.cpp index 766a2bb..5de7796 100644 --- a/SerialPort.cpp +++ b/SerialPort.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013,2015-2019 by Jonathan Naylor G4KLX + * Copyright (C) 2013,2015-2020 by Jonathan Naylor G4KLX * Copyright (C) 2016 by Colin Durbridge G4EML * * This program is free software; you can redistribute it and/or modify @@ -65,6 +65,10 @@ const uint8_t MMDVM_NXDN_LOST = 0x41U; const uint8_t MMDVM_POCSAG_DATA = 0x50U; +const uint8_t MMDVM_FM_PARAMS1 = 0x60U; +const uint8_t MMDVM_FM_PARAMS2 = 0x61U; +const uint8_t MMDVM_FM_PARAMS3 = 0x62U; + const uint8_t MMDVM_ACK = 0x70U; const uint8_t MMDVM_NAK = 0x7FU; @@ -97,7 +101,7 @@ const uint8_t MMDVM_DEBUG5 = 0xF5U; #define HW_TYPE "MMDVM" #endif -#define DESCRIPTION "20190130 (D-Star/DMR/System Fusion/P25/NXDN/POCSAG)" +#define DESCRIPTION "20200428 (D-Star/DMR/System Fusion/P25/NXDN/POCSAG/FM)" #if defined(GITVERSION) #define concat(h, a, b, c) h " " a " " b " GitID #" c "" @@ -168,6 +172,8 @@ void CSerialPort::getStatus() reply[3U] |= 0x10U; if (m_pocsagEnable) reply[3U] |= 0x20U; + if (m_fmEnable) + reply[3U] |= 0x40U; reply[4U] = uint8_t(m_modemState); @@ -256,7 +262,7 @@ void CSerialPort::getVersion() uint8_t CSerialPort::setConfig(const uint8_t* data, uint8_t length) { - if (length < 18U) + if (length < 19U) return 4U; bool rxInvert = (data[0U] & 0x01U) == 0x01U; @@ -273,6 +279,7 @@ uint8_t CSerialPort::setConfig(const uint8_t* data, uint8_t length) bool p25Enable = (data[1U] & 0x08U) == 0x08U; bool nxdnEnable = (data[1U] & 0x10U) == 0x10U; bool pocsagEnable = (data[1U] & 0x20U) == 0x20U; + bool fmEnable = (data[1U] & 0x40U) == 0x40U; uint8_t txDelay = data[2U]; if (txDelay > 50U) @@ -280,7 +287,7 @@ uint8_t CSerialPort::setConfig(const uint8_t* data, uint8_t length) MMDVM_STATE modemState = MMDVM_STATE(data[3U]); - if (modemState != STATE_IDLE && modemState != STATE_DSTAR && modemState != STATE_DMR && modemState != STATE_YSF && modemState != STATE_P25 && modemState != STATE_NXDN && modemState != STATE_POCSAG && modemState != STATE_DSTARCAL && modemState != STATE_DMRCAL && modemState != STATE_RSSICAL && modemState != STATE_LFCAL && modemState != STATE_DMRCAL1K && modemState != STATE_P25CAL1K && modemState != STATE_DMRDMO1K && modemState != STATE_NXDNCAL1K && modemState != STATE_POCSAGCAL) + if (modemState != STATE_IDLE && modemState != STATE_DSTAR && modemState != STATE_DMR && modemState != STATE_YSF && modemState != STATE_P25 && modemState != STATE_NXDN && modemState != STATE_POCSAG && modemState != STATE_FM && modemState != STATE_DSTARCAL && modemState != STATE_DMRCAL && modemState != STATE_RSSICAL && modemState != STATE_LFCAL && modemState != STATE_DMRCAL1K && modemState != STATE_P25CAL1K && modemState != STATE_DMRDMO1K && modemState != STATE_NXDNCAL1K && modemState != STATE_POCSAGCAL && modemState != STATE_FMCAL10K && modemState != STATE_FMCAL12K && modemState != STATE_FMCAL15K && modemState != STATE_FMCAL20K && modemState != STATE_FMCAL25K && modemState != STATE_FMCAL30K) return 4U; if (modemState == STATE_DSTAR && !dstarEnable) return 4U; @@ -294,6 +301,8 @@ uint8_t CSerialPort::setConfig(const uint8_t* data, uint8_t length) return 4U; if (modemState == STATE_POCSAG && !pocsagEnable) return 4U; + if (modemState == STATE_FM && !fmEnable) + return 4U; uint8_t rxLevel = data[4U]; @@ -318,6 +327,8 @@ uint8_t CSerialPort::setConfig(const uint8_t* data, uint8_t length) uint8_t pocsagTXLevel = data[17U]; + uint8_t fmTXLevel = data[18U]; + m_modemState = modemState; m_dstarEnable = dstarEnable; @@ -326,6 +337,7 @@ uint8_t CSerialPort::setConfig(const uint8_t* data, uint8_t length) m_p25Enable = p25Enable; m_nxdnEnable = nxdnEnable; m_pocsagEnable = pocsagEnable; + m_fmEnable = fmEnable; m_duplex = !simplex; dstarTX.setTXDelay(txDelay); @@ -343,13 +355,81 @@ uint8_t CSerialPort::setConfig(const uint8_t* data, uint8_t length) ysfTX.setParams(ysfLoDev, ysfTXHang); - io.setParameters(rxInvert, txInvert, pttInvert, rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, pocsagTXLevel, txDCOffset, rxDCOffset); + io.setParameters(rxInvert, txInvert, pttInvert, rxLevel, cwIdTXLevel, dstarTXLevel, dmrTXLevel, ysfTXLevel, p25TXLevel, nxdnTXLevel, pocsagTXLevel, fmTXLevel, txDCOffset, rxDCOffset); io.start(); return 0U; } +uint8_t CSerialPort::setFMParams1(const uint8_t* data, uint8_t length) +{ + if (length < 8U) + return 4U; + + uint8_t speed = data[0U];; + uint16_t frequency = data[1U] * 10U; + uint8_t time = data[2U]; + uint8_t holdoff = data[3U]; + uint8_t highLevel = data[4U]; + uint8_t lowLevel = data[5U]; + + bool callAtStart = (data[6U] & 0x01U) == 0x01U; + bool callAtEnd = (data[6U] & 0x02U) == 0x02U; + + char callsign[50U]; + uint8_t n = 0U; + for (uint8_t i = 7U; i < length; i++, n++) + callsign[n] = data[i]; + callsign[n] = '\0'; + + return fm.setCallsign(callsign, speed, frequency, time, holdoff, highLevel, lowLevel, callAtStart, callAtEnd); +} + +uint8_t CSerialPort::setFMParams2(const uint8_t* data, uint8_t length) +{ + if (length < 6U) + return 4U; + + uint8_t speed = data[0U]; + uint16_t frequency = data[1U] * 10U; + uint8_t minTime = data[2U]; + uint16_t delay = data[3U] * 10U; + uint8_t level = data[4U]; + + char ack[50U]; + uint8_t n = 0U; + for (uint8_t i = 5U; i < length; i++, n++) + ack[n] = data[i]; + ack[n] = '\0'; + + return fm.setAck(ack, speed, frequency, minTime, delay, level); +} + +uint8_t CSerialPort::setFMParams3(const uint8_t* data, uint8_t length) +{ + if (length < 11U) + return 4U; + + uint16_t timeout = data[0U] * 5U; + uint8_t timeoutLevel = data[1U]; + + uint8_t ctcssFrequency = data[2U]; + uint8_t ctcssThreshold = data[3U]; + uint8_t ctcssLevel = data[4U]; + + uint8_t kerchunkTime = data[5U]; + uint8_t hangTime = data[6U]; + + bool useCOS = (data[7U] & 0x01U) == 0x01U; + + uint8_t rfAudioBoost = data[8U]; + uint8_t maxDev = data[9U]; + uint8_t rxLevel = data[10U]; + + return fm.setMisc(timeout, timeoutLevel, ctcssFrequency, ctcssThreshold, ctcssLevel, kerchunkTime, hangTime, useCOS, rfAudioBoost, maxDev, rxLevel); +} + uint8_t CSerialPort::setMode(const uint8_t* data, uint8_t length) { if (length < 1U) @@ -360,7 +440,7 @@ uint8_t CSerialPort::setMode(const uint8_t* data, uint8_t length) if (modemState == m_modemState) return 0U; - if (modemState != STATE_IDLE && modemState != STATE_DSTAR && modemState != STATE_DMR && modemState != STATE_YSF && modemState != STATE_P25 && modemState != STATE_NXDN && modemState != STATE_POCSAG && modemState != STATE_DSTARCAL && modemState != STATE_DMRCAL && modemState != STATE_RSSICAL && modemState != STATE_LFCAL && modemState != STATE_DMRCAL1K && modemState != STATE_P25CAL1K && modemState != STATE_DMRDMO1K && modemState != STATE_NXDNCAL1K && modemState != STATE_POCSAGCAL) + if (modemState != STATE_IDLE && modemState != STATE_DSTAR && modemState != STATE_DMR && modemState != STATE_YSF && modemState != STATE_P25 && modemState != STATE_NXDN && modemState != STATE_POCSAG && modemState != STATE_FM && modemState != STATE_DSTARCAL && modemState != STATE_DMRCAL && modemState != STATE_RSSICAL && modemState != STATE_LFCAL && modemState != STATE_DMRCAL1K && modemState != STATE_P25CAL1K && modemState != STATE_DMRDMO1K && modemState != STATE_NXDNCAL1K && modemState != STATE_POCSAGCAL && modemState != STATE_FMCAL10K && modemState != STATE_FMCAL12K && modemState != STATE_FMCAL15K && modemState != STATE_FMCAL20K && modemState != STATE_FMCAL25K && modemState != STATE_FMCAL30K) return 4U; if (modemState == STATE_DSTAR && !m_dstarEnable) return 4U; @@ -374,6 +454,8 @@ uint8_t CSerialPort::setMode(const uint8_t* data, uint8_t length) return 4U; if (modemState == STATE_POCSAG && !m_pocsagEnable) return 4U; + if (modemState == STATE_FM && !m_fmEnable) + return 4U; setMode(modemState); @@ -401,6 +483,9 @@ void CSerialPort::setMode(MMDVM_STATE modemState) case STATE_POCSAG: DEBUG1("Mode set to POCSAG"); break; + case STATE_FM: + DEBUG1("Mode set to FM"); + break; case STATE_DSTARCAL: DEBUG1("Mode set to D-Star Calibrate"); break; @@ -413,8 +498,23 @@ void CSerialPort::setMode(MMDVM_STATE modemState) case STATE_LFCAL: DEBUG1("Mode set to 80 Hz Calibrate"); break; - case STATE_DMRCAL1K: - DEBUG1("Mode set to DMR BS 1031 Hz Calibrate"); + case STATE_FMCAL10K: + DEBUG1("Mode set to FM 10Khz Calibrate"); + break; + case STATE_FMCAL12K: + DEBUG1("Mode set to FM 12.5Khz Calibrate"); + break; + case STATE_FMCAL15K: + DEBUG1("Mode set to FM 15Khz Calibrate"); + break; + case STATE_FMCAL20K: + DEBUG1("Mode set to FM 20Khz Calibrate"); + break; + case STATE_FMCAL25K: + DEBUG1("Mode set to FM 10Khz Calibrate"); + break; + case STATE_FMCAL30K: + DEBUG1("Mode set to FM 30Khz Calibrate"); break; case STATE_P25CAL1K: DEBUG1("Mode set to P25 1011 Hz Calibrate"); @@ -451,6 +551,9 @@ void CSerialPort::setMode(MMDVM_STATE modemState) if (modemState != STATE_NXDN) nxdnRX.reset(); + if (modemState != STATE_FM) + fm.reset(); + cwIdTX.reset(); m_modemState = modemState; @@ -524,11 +627,43 @@ void CSerialPort::process() sendACK(); break; + case MMDVM_FM_PARAMS1: + err = setFMParams1(m_buffer + 3U, m_len - 3U); + if (err == 0U) { + sendACK(); + } else { + DEBUG2("Received invalid FM params 1", err); + sendNAK(err); + } + break; + + case MMDVM_FM_PARAMS2: + err = setFMParams2(m_buffer + 3U, m_len - 3U); + if (err == 0U) { + sendACK(); + } else { + DEBUG2("Received invalid FM params 2", err); + sendNAK(err); + } + break; + + case MMDVM_FM_PARAMS3: + err = setFMParams3(m_buffer + 3U, m_len - 3U); + if (err == 0U) { + sendACK(); + } else { + DEBUG2("Received invalid FM params 3", err); + sendNAK(err); + } + break; + case MMDVM_CAL_DATA: if (m_modemState == STATE_DSTARCAL) err = calDStarTX.write(m_buffer + 3U, m_len - 3U); if (m_modemState == STATE_DMRCAL || m_modemState == STATE_LFCAL || m_modemState == STATE_DMRCAL1K || m_modemState == STATE_DMRDMO1K) err = calDMR.write(m_buffer + 3U, m_len - 3U); + if (m_modemState == STATE_FMCAL10K || m_modemState == STATE_FMCAL12K || m_modemState == STATE_FMCAL15K || m_modemState == STATE_FMCAL20K || m_modemState == STATE_FMCAL25K || m_modemState == STATE_FMCAL30K) + err = calFM.write(m_buffer + 3U, m_len - 3U); if (m_modemState == STATE_P25CAL1K) err = calP25.write(m_buffer + 3U, m_len - 3U); if (m_modemState == STATE_NXDNCAL1K) diff --git a/SerialPort.h b/SerialPort.h index 728b6b3..cd09bbe 100644 --- a/SerialPort.h +++ b/SerialPort.h @@ -73,6 +73,9 @@ private: uint8_t setConfig(const uint8_t* data, uint8_t length); uint8_t setMode(const uint8_t* data, uint8_t length); void setMode(MMDVM_STATE modemState); + uint8_t setFMParams1(const uint8_t* data, uint8_t length); + uint8_t setFMParams2(const uint8_t* data, uint8_t length); + uint8_t setFMParams3(const uint8_t* data, uint8_t length); // Hardware versions void beginInt(uint8_t n, int speed); @@ -83,4 +86,3 @@ private: }; #endif - diff --git a/Tools/FMGenerateFilterCoefficients.py b/Tools/FMGenerateFilterCoefficients.py new file mode 100644 index 0000000..7d8a3db --- /dev/null +++ b/Tools/FMGenerateFilterCoefficients.py @@ -0,0 +1,46 @@ +# based on https://github.com/berndporr/iir_fixed_point/blob/master/gen_coeff.py + +import numpy as np +import scipy.signal as signal +import pylab as pl + +# Calculate the coefficients for a pure fixed point +# integer filter + +# sampling rate +fs = 24000 + +# cutoffs +f1 = 300 +f2 = 2700 +# ripple +rp = 0.2 + +# scaling factor in bits, do not change ! +q = 0 +# scaling factor as facor... +scaling_factor = 2**q + +# let's generate a sequence of 2nd order IIR filters +sos = signal.cheby1(3,rp,[f1, f2],'bandpass', output='sos', fs=fs) +#sos = signal.cheby1(1, rp, 2122, 'lowpass', output='sos', fs=fs) #deemphasis filter +#sos = signal.cheby1(1, rp, 2122, 'highpass', output='sos', fs=fs) #deemphasis filter + +#sos = np.round((sos) * scaling_factor) + +# print coefficients +for biquad in sos: + for coeff in biquad: + #print(int(coeff),",",sep="",end="") + print((coeff),",",sep="",end="") + print("") + +# plot the frequency response +b,a = signal.sos2tf(sos) +w,h = signal.freqz(b,a) +pl.plot(w/np.pi/2*fs,20*np.log(np.abs(h))) +pl.xlabel('frequency/Hz'); +pl.ylabel('gain/dB'); +pl.ylim(top=1,bottom=-20); +pl.xlim(left=250, right=12000); +pl.show() \ No newline at end of file diff --git a/Tools/emphasis.txt b/Tools/emphasis.txt new file mode 100644 index 0000000..39660d1 --- /dev/null +++ b/Tools/emphasis.txt @@ -0,0 +1,58 @@ +% GNU Octave script to generate pre and deemphasis filters +% https://dsp.stackexchange.com/questions/34605/biquad-cookbook-formula-for-broadcast-fm-de-emphasis +% PACKAGES + +pkg load control +pkg load signal + +clear all; +clc; + +fs = 24000; +samplingtime = 1/fs; + +% analog prototype +A2 = [1]; +B2 = [0.000075 1]; + +% Pre +Ds = tf(B2, A2); +% De +% Ds = tf(A2, B2); + +Ds = Ds/dcgain(Ds); + +% MZT +T1 = 0.000075; % 75us +z1 = -exp(-1.0/(fs*T1)); +p1 = 1+z1; + +a0 = 1.0; +a1 = p1; +a2 = 0; + +b0 = 1.0; +b1 = z1; +b2 = 0; + +% swap between a1, b1 to select pre- or de-emphasis + +# Pre +Bmzt = [b0 a1 b2] +Amzt = [a0 b1 a2] +% De +% Bmzt = [b0 b1 b2] +% Amzt = [a0 a1 a2] + +DzMZT = tf(Amzt, Bmzt, samplingtime); +DzMZT = DzMZT/dcgain(DzMZT); + +%% Plot +wmin = 2 * pi * 20.0; % 20Hz +wmax = 2 * pi * ((fs/2.0) - (fs/2 - 20000)); %20kHz + +figure(1); +bode(Ds, 'b', DzMZT, 'c', {wmin, wmax}); +legend('Analog prototype', 'MZT 2nd order','location', 'northwest'); +grid on; +pause();