ESP multidecoder RTTY - PSK31 - CW auto - ASCII - SSTV - SSTV auto color TRUE COLOR

MODO SSTV TRUE COLOR ROBOT 72

RTTY 45.45 MODO

/*Novo arquivo da biblioteca video_color.h para funcionar*/

/* DECODIFICADOR MULTIMODO RTTY / PSK31 / CW / ASCII / SSTV - ESP32 (Arduino IDE) Saida de video composto com a biblioteca ESP32CompositeColorVideo (marciot, API bitluni) Compativel com ESP32 Arduino Core 2.x e 3.x LIGACOES: GPIO34 : entrada de audio (ADC1_CH6), polarizada em ~1,65 V (divisor + capacitor de acoplamento) GPIO0 : botao para GND toque curto (< 0,8 s) = proximo modo toque longo (>= 0,8 s) = liga/desliga o SINAL DE TESTE interno (nao segure o botao durante o reset: GPIO0 em nivel baixo entra no modo de gravacao) GPIO25 : video composto (saida para o RCA) MODOS (indice para o comando m): 0..3 RTTY 45.45 / 50 / 75 / 100 baud (mark 2125 Hz, space 2295 Hz) 4 PSK31 5 CW 6,7 ASCII 110 / 300 baud (mark 2125 Hz, space 2295 Hz, 7 ou 8 bits automatico) [NOVO] 8..16 SSTV: Martin M1, Martin M2, Scottie 1, Scottie 2, Robot 36, Robot 72, PD120, Scottie DX, AUTO (identifica pelo codigo VIS) [NOVO] SINTONIA AUTOMATICA: PSK31 e CW procuram sozinhos o tom mais forte entre 300 e 2700 Hz. COMANDOS PELO MONITOR SERIAL (115200 baud, terminar com Enter): ? ajuda n proximo modo m 0..16 escolhe o modo t liga/desliga teste r inverte mark/space (RTTY/ASCII) ou fase (PSK) f 1000 fixa a frequencia (PSK31 ou CW) e desliga a sintonia automatica u liga/desliga a sintonia automatica w 15 CW com velocidade fixa em 15 WPM w 0 CW com velocidade automatica b troca a largura de banda do CW (60 / 120 / 250 Hz) g 11.5 ganho de entrada s 0.2 squelch (0 = aberto, 1 = muito fechado) BIBLIOTECA: ESP32CompositeColorVideo (marciot) Usar o mesmo video_out.h que voce ja substituiu na biblioteca (funciona nas versoes 2.x e 3.x do core da Espressif). Ele nao muda nesta versao. ALTERACOES DESTA VERSAO: - RTTY, PSK31 e CW: codigo de decodificacao NAO foi alterado. - Novos: ASCII 110/300 e SSTV (imagem colorida 160x128 ampliada para a tela cheia). - REMOVIDOS nesta versao: SSTV AVT 90 e NBTV (e o clock de 25 kHz do NBTV). - SSTV: a imagem e guardada em 160x128 (1 byte de paleta por pixel) e mostrada em tela cheia. - SSTV COR: nova TABELA DE CORES. A paleta da biblioteca (Atari: 16 matizes x 16 brilhos) e modelada em RGB (ssPalRGB). TABELA VIRTUAL DE 1024 CORES (par A,B calculado): cada cor e um par de indices reais da paleta mostrados em pixels vizinhos (x par/impar); a tabela de busca tem 4096 entradas (RGB 4-4-4 -> melhor das 1024 cores, ~5 KB) e montada na partida. Alem disso ha difusao de erro (Floyd-Steinberg) para simular tons intermediarios (saturacao parcial, tons de pele, ceu, etc.). - Ajuste fino pelo serial (vale para a PROXIMA imagem recebida): c X = saturacao (0.32), h X = fase do matiz 1 em graus (-24), j X = passo entre matizes em graus (24; negativo inverte o sentido), y X = gama de brilho (1.0), d = liga/desliga a difusao de erro. - Teste SSTV (t ou botao longo): agora gera barras de cor, tons naturais, varredura de matiz e rampa de cinza, para voce comparar a TV com o esperado e calibrar com c / h / j / y. - SSTV TRUE COLOR (SS_TRUECOLOR 1): a imagem 160x128 nao passa mais pela paleta de 256 cores. Cada pixel guarda Y (6 bits) + croma a,b (5 bits cada) e o ISR de video (video_out.h modificado) gera direto as 4 amostras do DAC de cada ciclo da subportadora. A resolucao de cor passa a ser a do proprio DAC de 8 bits (~49 niveis de luminancia x ~31 x 31 de croma), nao mais 256 cores fixas. Serial (SSTV true color): c X = ganho de croma (1.0 = NTSC padrao), h X = giro de matiz em graus, y X = gama de luminancia, d = liga/desliga difusao de erro. - NITIDEZ SSTV: instante do sincronismo em fracao de amostra, integracao por area em cada pixel, media das linhas de origem que caem na mesma linha 160x128 (antes eram descartadas), mascara de nitidez na luminancia (k X) e cores mistas A/B so nas areas suaves (nas bordas, cor pura). - SSTV: ao entrar no modo o nome aparece no canto superior esquerdo por 1 s. A imagem recebida PERMANECE na tela depois do fim da recepcao; so e apagada quando a 1a linha de uma nova recepcao chega (ou ao trocar de modo), nao mais ao detectar o cabecalho/ruido. - Robot 36, Robot 72 e PD120 agora COLORIDOS (Y + R-Y + B-Y). - RDS (57 kHz), Olivia, FT8/FT4 e JS8Call ainda NAO foram portados. - PADRAO SSTV: x0 (margem lateral), z14 (margem topo/base), v0 (deslocamento vertical). */ // SS_TRUECOLOR 1: SSTV em TRUE COLOR (exige o video_out.h com o caminho "TRUECOLOR", so NTSC). // SS_TRUECOLOR 0: volta a paleta de 256 cores + tabela de 1024 cores (funciona com o video_out.h original). #define SS_TRUECOLOR 1 #include #include #include #include #include #include #define USE_ATARI_COLORS #include "CompositeGraphics.h" #include "CompositeColorOutput.h" #include "font6x8.h" // ---- chaves de ajuste/teste ---- #define TIMER_NUCLEO0 1 // 1 = timer de 8 kHz no nucleo 0 (recomendado); 0 = como antes #define FRAME_MS 25 // pausa entre quadros (~28 quadros/s); aumente para 40 se ainda deformar #define VIDEO_MONO 0 // 1 = texto e graficos em cinza (teste de artefato de cor) #define PIN_AUDIO 34 #define PIN_MODE 0 void outChar(char c); void outStr(const char* s); void rxStatus(uint8_t st); static void setSampleClock(uint32_t us); static uint32_t timerPeriodUs = 125; // 125 us = 8 kHz // ==== DSP BEGIN ==================================================================== #define SR 8000.0f #define TWO_PI_F 6.28318530718f #define PI_F 3.14159265359f static const char ITA2_LETTERS[32] = { 0, 'E', '\n', 'A', ' ', 'S', 'I', 'U', '\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K', 'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q', 'O', 'B', 'G', 0, 'M', 'X', 'V', 0 }; static const char ITA2_FIGURES[32] = { 0, '3', '\n', '-', ' ', '\'', '8', '7', '\r', '$', '4', '\'', ',', '!', ':', '(', '5', '"', ')', '2', '#', '6', '0', '1', '9', '?', '&', 0, '.', '/', ';', 0 }; static const char* const VARICODE_CODES[128] = { "1010101011","1011011011","1011101101","1101110111","1011101011","1101011111","1011101111","1011111101","1011111111","11101111","11101","1101101111","1011011101","11111","1101110101","1110101011","1011110111","1011110101","1110101101","1110101111","1101011011","1101101011","1101101101","1101010111","1101111011","1101111101","1110110111","1101010101","1101011101","1110111011","1011111011","1101111111","1","111111111","101011111","111110101","111011011","1011010101","1010111011","101111111","11111011","11110111","101101111","111011111","1110101","110101","1010111","110101111","10110111","10111101","11101101","11111111","101110111","101011011","101101011","110101101","110101011","110110111","11110101","110111101","111101101","1010101","111010111","1010101111","1010111101","1111101","11101011","10101101","10110101","1110111","11011011","11111101","101010101","1111111","111111101","101111101","11010111","10111011","11011101","10101011","11010101","111011101","10101111","1101111","1101101","101010111","110110101","101011101","101110101","101111011","1010101101","111110111","111101111","111111011","1010111111","101101101","1011011111","1011","1011111","101111","101101","11","111101","1011011","101011","1101","111101011","10111111","11011","111011","1111","111","111111","110111111","10101","10111","101","110111","1111011","1101011","11011111","1011101","111010101","1010110111","110111011","1010110101","1011010111","1110110101" }; static int varicodeLookup(const char* code) { for (int i = 0; i < 128; i++) if (strcmp(VARICODE_CODES[i], code) == 0) return i; return -1; } enum { T_RTTY = 0, T_PSK = 1, T_CW = 2, T_ASCII = 3, T_SSTV = 4 }; // bits: RTTY/ASCII = bits de dados; SSTV = indice na tabela SSD (8 = AUTO) struct ModeDef { const char* name; uint8_t type; float baud; uint8_t bits; }; #define N_MODES 17 static const ModeDef MODES[N_MODES] = { { "RTTY 45.45 BAUD", T_RTTY, 45.45f, 5 }, { "RTTY 50.00 BAUD", T_RTTY, 50.00f, 5 }, { "RTTY 75.00 BAUD", T_RTTY, 75.00f, 5 }, { "RTTY 100.00 BAUD", T_RTTY, 100.00f, 5 }, { "PSK31 (31.25 BAUD)", T_PSK, 31.25f, 0 }, { "CW (MORSE)", T_CW, 0.0f, 0 }, { "ASCII 110 BAUD", T_ASCII, 110.0f, 7 }, { "ASCII 300 BAUD", T_ASCII, 300.0f, 7 }, { "SSTV MARTIN M1", T_SSTV, 0.0f, 0 }, { "SSTV MARTIN M2", T_SSTV, 0.0f, 1 }, { "SSTV SCOTTIE 1", T_SSTV, 0.0f, 2 }, { "SSTV SCOTTIE 2", T_SSTV, 0.0f, 3 }, { "SSTV ROBOT 36", T_SSTV, 0.0f, 4 }, { "SSTV ROBOT 72", T_SSTV, 0.0f, 5 }, { "SSTV PD120", T_SSTV, 0.0f, 6 }, { "SSTV SCOTTIE DX", T_SSTV, 0.0f, 7 }, { "SSTV AUTO (VIS)", T_SSTV, 0.0f, 8 } }; // comandos aceitos por dspCommand() enum { CMD_MODE = 1, CMD_NEXTMODE, CMD_TEST, CMD_FREQ, CMD_WPM, CMD_AUTOTUNE, CMD_BW }; static const float MARK_FREQ = 2125.0f; static const float SPACE_FREQ = 2295.0f; static const bool USOS = true; static float pskCenterFreq = 1000.0f; static uint8_t currentModeIdx = 0; static volatile uint8_t modeEpoch = 0; static bool reverseMS = false; static float squelchPower = 0.200f; static float inputGain = 11.5f; static bool figsMode = false; static bool prevMark = true; static volatile bool uiDirty = true; static bool testActive = false; static bool cwAutoTune = true, pskAutoTune = true; static volatile uint32_t overruns = 0; static volatile float adcPk = 0.0f; // pico do ADC em contagens (0..2048) static inline float clampf(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); } static uint8_t ledState = 255; static void ledStatus(bool active, bool squelched) { uint8_t st = active ? 1 : (squelched ? 2 : 0); if (st != ledState) { ledState = st; rxStatus(st); } } struct Tone { float c = 1.0f, s = 0.0f, cd = 1.0f, sd = 0.0f, i = 0.0f, q = 0.0f, i2 = 0.0f, q2 = 0.0f; void setFreq(float f) { float w = TWO_PI_F * f / SR; cd = cosf(w); sd = sinf(w); c = 1.0f; s = 0.0f; i = 0.0f; q = 0.0f; i2 = 0.0f; q2 = 0.0f; } void retune(float f) { float w = TWO_PI_F * f / SR; cd = cosf(w); sd = sinf(w); } float run(float x, float alpha) { i += alpha * (x * c - i); q += alpha * (x * s - q); float nc = c * cd - s * sd; s = s * cd + c * sd; c = nc; return sqrtf(i * i + q * q); } // dois polos em cascata (melhor rejeicao do tom vizinho) float run2(float x, float alpha) { i += alpha * (x * c - i); q += alpha * (x * s - q); i2 += alpha * (i - i2); q2 += alpha * (q - q2); float nc = c * cd - s * sd; s = s * cd + c * sd; c = nc; return sqrtf(i2 * i2 + q2 * q2); } void renorm() { float m = 1.0f / sqrtf(c * c + s * s); c *= m; s *= m; } }; static Tone toneMark, toneSpace, toneNoiseA, toneNoiseB; // ================================ RTTY ================================ enum { RX_IDLE = 0, RX_START, RX_DATA, RX_STOP_CENTER, RX_STOP_END }; static uint8_t rxState = RX_IDLE; static float rxCnt = 0.0f; static uint8_t rxIdx = 0; static uint8_t rxCode = 0; static bool stopCenterOk = false; static float levelMark = 0.0f, levelSpace = 0.0f; static float markSm = 0.0f, spaceSm = 0.0f; static float dispMark = 0.0f, dispSpace = 0.0f, dispNoise = 0.0f; static float rttyAlpha = 0.05f, rttySig = 0.0f, rttyNoise = 0.0f; static bool rttyOpen = false, rttyBit = true; static void deliverBaudot(uint8_t code) { code &= 0x1F; if (code == 0x1F) { figsMode = false; uiDirty = true; return; } if (code == 0x1B) { figsMode = true; uiDirty = true; return; } char c = figsMode ? ITA2_FIGURES[code] : ITA2_LETTERS[code]; if (code == 4 && USOS) { figsMode = false; uiDirty = true; } if (c) outChar(c); } static void processUART(bool bit, float baud, uint8_t nbits) { const float bp = SR / baud; switch (rxState) { case RX_IDLE: if (prevMark && !bit) { rxState = RX_START; rxCnt = bp * 0.5f; } break; case RX_START: rxCnt -= 1.0f; if (rxCnt <= 0.0f) { if (bit) rxState = RX_IDLE; else { rxState = RX_DATA; rxIdx = 0; rxCode = 0; rxCnt += bp; } } break; case RX_DATA: rxCnt -= 1.0f; if (rxCnt <= 0.0f) { if (bit) rxCode |= (1 << rxIdx); rxIdx++; rxCnt += bp; if (rxIdx >= nbits) { rxState = RX_STOP_CENTER; stopCenterOk = false; } } break; case RX_STOP_CENTER: rxCnt -= 1.0f; if (rxCnt <= 0.0f) { stopCenterOk = bit; rxState = RX_STOP_END; rxCnt += bp * 0.5f; } break; case RX_STOP_END: rxCnt -= 1.0f; if (rxCnt <= 0.0f) { if (stopCenterOk && bit) deliverBaudot(rxCode); rxState = RX_IDLE; } break; } prevMark = bit; } static void rttyConfigure() { float baud = MODES[currentModeIdx].baud; float fc = clampf(1.25f * baud, 50.0f, 110.0f); // corner de cada polo rttyAlpha = TWO_PI_F * fc / SR; toneMark.setFreq(MARK_FREQ); toneSpace.setFreq(SPACE_FREQ); toneNoiseA.setFreq(1900.0f); toneNoiseB.setFreq(2520.0f); // referencias de ruido fora do sinal levelMark = levelSpace = markSm = spaceSm = 0.0f; rttySig = rttyNoise = 0.0f; dispMark = dispSpace = dispNoise = 0.0f; rttyOpen = false; rttyBit = true; } static void processRTTYSample(float s) { const ModeDef& md = MODES[currentModeIdx]; float m = toneMark.run2(s, rttyAlpha); float sp = toneSpace.run2(s, rttyAlpha); float na = toneNoiseA.run2(s, rttyAlpha); float nb = toneNoiseB.run2(s, rttyAlpha); levelMark = fmaxf(m, levelMark * 0.9998f); levelSpace = fmaxf(sp, levelSpace * 0.9998f); markSm += 0.01f * (m - markSm); spaceSm += 0.01f * (sp - spaceSm); dispMark = markSm; dispSpace = spaceSm; rttySig += 0.002f * (fmaxf(m, sp) - rttySig); rttyNoise += 0.002f * (0.5f * (na + nb) - rttyNoise); dispNoise = rttyNoise; float pr = (rttySig * rttySig) / (rttyNoise * rttyNoise + 1e-12f); float thr = 1.0f + squelchPower * 20.0f; if (!rttyOpen) { if (pr >= thr && rttySig > 0.002f) rttyOpen = true; } else if (pr < 0.6f * thr || rttySig < 0.001f) rttyOpen = false; ledStatus(rttyOpen, !rttyOpen); bool bit = true; // sem sinal: linha em repouso (mark) if (rttyOpen) { float lm = fmaxf(levelMark, 0.35f * levelSpace); float ls = fmaxf(levelSpace, 0.35f * levelMark); bool mk = (m / (lm + 1e-9f)) > (sp / (ls + 1e-9f)); bit = reverseMS ? !mk : mk; } rttyBit = bit; processUART(bit, md.baud, md.bits); } // ================================ PSK31 ================================ #define PSK_DEC 16 #define PSK_FIR_N 129 #define PSK_FIR_FC 36.0f static float PSK_FIR[PSK_FIR_N]; static void buildPskFir() { const float fs = SR / 8.0f; // 1000 Hz apos a decimacao por 8 const float x = 2.0f * PSK_FIR_FC / fs; float sum = 0.0f; for (int k = 0; k < PSK_FIR_N; k++) { float n = (float)k - (PSK_FIR_N - 1) * 0.5f; float h = (n == 0.0f) ? x : sinf(PI_F * x * n) / (PI_F * n); float w = 0.42f - 0.5f * cosf(2.0f * PI_F * k / (PSK_FIR_N - 1)) + 0.08f * cosf(4.0f * PI_F * k / (PSK_FIR_N - 1)); // Blackman PSK_FIR[k] = h * w; sum += h * w; } for (int k = 0; k < PSK_FIR_N; k++) PSK_FIR[k] /= sum; } struct Box { float b[3][8]; float s[3]; uint8_t p; void reset() { memset(this, 0, sizeof(*this)); } float push(float x) { float v = x; for (int st = 0; st < 3; st++) { float old = b[st][p]; b[st][p] = v; s[st] += v - old; v = s[st] * 0.125f; } p = (p + 1) & 7; if (p == 0) { for (int st = 0; st < 3; st++) { float t = 0; for (int k = 0; k < 8; k++) t += b[st][k]; s[st] = t; } } return v; } }; struct Branch { Tone t; Box bi, bq; uint8_t cnt, odd; int w; float hi[2 * PSK_FIR_N]; float hq[2 * PSK_FIR_N]; float zi, zq; void reset() { bi.reset(); bq.reset(); cnt = 0; odd = 0; w = 0; memset(hi, 0, sizeof(hi)); memset(hq, 0, sizeof(hq)); zi = 0; zq = 0; } bool run(float x) { float vi = bi.push(x * t.c), vq = bq.push(x * t.s); float nc = t.c * t.cd - t.s * t.sd; t.s = t.s * t.cd + t.c * t.sd; t.c = nc; if ((++cnt & 7) != 0) return false; hi[w] = hi[w + PSK_FIR_N] = vi; hq[w] = hq[w + PSK_FIR_N] = vq; w = (w + 1) % PSK_FIR_N; odd ^= 1; if (!odd) return false; float si = 0, sq = 0; const float* a = hi + w; const float* b = hq + w; for (int k = 0; k < PSK_FIR_N; k++) { si += PSK_FIR[k] * a[k]; sq += PSK_FIR[k] * b[k]; } zi = si; zq = sq; return true; } }; static Branch pskMain, pskNoiseA, pskNoiseB; static float pskFreq = 1000.0f; static bool pskAfc = true; static int pskDips = 0, pskLock = 0, pskTick = 0, pskSinceDip = 99; static float pskPhase = 0, pskRate = 0; static bool pskDecided = false; static float ringI[5], ringQ[5]; static float pskM0 = 0, pskM1 = 0, pskM2 = 0, pskPk = 0; static float pskPrevI = 1, pskPrevQ = 0; static float afcR = 0, afcI = 0, afcW = 0, pskPrevZ2R = 0, pskPrevZ2I = 0; static char pskAcc[24]; static uint8_t pskAccLen = 0; static float pskSigPow = 0, pskNoisePow = 0; static bool pskOpen = false; static bool pskLastWasCR = false; static float rotR = 0, rotI = 0; #define SCOPE_N 1024 static volatile int8_t scX[SCOPE_N], scY[SCOPE_N]; static volatile uint8_t scF[SCOPE_N]; static volatile uint16_t scW = 0; static void applyPskFreq() { pskMain.t.retune(pskFreq); pskNoiseA.t.retune(pskFreq + 250.0f); pskNoiseB.t.retune(pskFreq - 250.0f); } static void resetPskDemod() { pskMain.reset(); pskNoiseA.reset(); pskNoiseB.reset(); pskDips = 0; pskLock = 0; pskTick = 0; pskPhase = 0; pskRate = 0; pskDecided = false; pskSinceDip = 99; memset(ringI, 0, sizeof(ringI)); memset(ringQ, 0, sizeof(ringQ)); pskM0 = pskM1 = pskM2 = 0; pskPk = 0; pskPrevI = 1; pskPrevQ = 0; afcR = afcI = afcW = pskPrevZ2R = pskPrevZ2I = 0; pskAccLen = 0; pskAcc[0] = 0; pskSigPow = 0; pskNoisePow = 0; pskOpen = false; pskLastWasCR = false; rotR = rotI = 0; } static void setPskFreq(float f) { pskCenterFreq = clampf(f, 400.0f, 2600.0f); pskFreq = pskCenterFreq; applyPskFreq(); resetPskDemod(); } static void emitPskChar(int c) { if (c == 13) { outChar('\n'); pskLastWasCR = true; return; } if (c == 10) { if (!pskLastWasCR) outChar('\n'); pskLastWasCR = false; return; } pskLastWasCR = false; if (c == 8) { outChar('\b'); return; } if (c >= 32 && c < 127) outChar((char)c); } static void pskDecideBit() { static const float w[5] = { 0.3f, 0.7f, 1.0f, 0.7f, 0.3f }; float si = 0, sq = 0; for (int j = 0; j < 5; j++) { si += w[j] * ringI[j]; sq += w[j] * ringQ[j]; } float re = si * pskPrevI + sq * pskPrevQ; pskPrevI = si; pskPrevQ = sq; if (!pskOpen) { pskAccLen = 0; pskAcc[0] = 0; return; } char bit = re < 0 ? '0' : '1'; if (reverseMS) bit = (bit == '0') ? '1' : '0'; pskAcc[pskAccLen++] = bit; pskAcc[pskAccLen] = 0; for (;;) { char* p = strstr(pskAcc, "00"); if (!p) break; int k = (int)(p - pskAcc); int s = 0; while (s < k && pskAcc[s] == '0') s++; char code[20]; int len = k - s; memcpy(code, pskAcc + s, len); code[len] = 0; int rest = pskAccLen - (k + 2); memmove(pskAcc, pskAcc + k + 2, rest + 1); pskAccLen = rest; if (len > 0) { int ch = varicodeLookup(code); if (ch >= 0) emitPskChar(ch); } } if (pskAccLen > 14) { pskAccLen = 0; pskAcc[0] = 0; } } static void pskSymbolTick(float zi, float zq, float noiseP) { float p = zi * zi + zq * zq; float mag = sqrtf(p); pskSigPow += (p - pskSigPow) * 0.02f; pskNoisePow += (noiseP - pskNoisePow) * 0.02f; float snr = pskSigPow / (pskNoisePow + 1e-12f); float thr = 1.0f + squelchPower * 10.0f; if (!pskOpen) { if (snr >= thr) pskOpen = true; } else if (snr < fmaxf(1.3f, thr * 0.6f)) pskOpen = false; ledStatus(pskOpen, !pskOpen); pskPk += (mag > pskPk ? 0.1f : 0.003f) * (mag - pskPk); for (int j = 0; j < 4; j++) { ringI[j] = ringI[j + 1]; ringQ[j] = ringQ[j + 1]; } ringI[4] = zi; ringQ[4] = zq; pskM2 = pskM1; pskM1 = pskM0; pskM0 = mag; float z2r = zi * zi - zq * zq, z2i = 2 * zi * zq; afcR += z2r * pskPrevZ2R + z2i * pskPrevZ2I; afcI += z2i * pskPrevZ2R - z2r * pskPrevZ2I; afcW += sqrtf((z2r * z2r + z2i * z2i) * (pskPrevZ2R * pskPrevZ2R + pskPrevZ2I * pskPrevZ2I)); pskPrevZ2R = z2r; pskPrevZ2I = z2i; rotR += 0.02f * (z2r - rotR); rotI += 0.02f * (z2i - rotI); if ((pskTick & 15) == 15) { if (pskAfc && snr >= 1.5f && afcW > 0 && sqrtf(afcR * afcR + afcI * afcI) / afcW > 0.35f) { float df = -atan2f(afcI, afcR) * 500.0f / (4.0f * PI_F); pskFreq += 0.03f * df; if (pskFreq > pskCenterFreq + 50) pskFreq = pskCenterFreq + 50; if (pskFreq < pskCenterFreq - 50) pskFreq = pskCenterFreq - 50; applyPskFreq(); } afcR = afcI = afcW = 0; } pskTick++; float ang = 0.5f * atan2f(rotI, rotR); float ca = cosf(ang), sa = sinf(ang); float kk = 0.8f / (pskPk + 1e-9f); uint16_t scopeIdx = scW & (SCOPE_N - 1); scX[scopeIdx] = (int8_t)clampf((zi * ca + zq * sa) * kk * 100.0f, -127, 127); scY[scopeIdx] = (int8_t)clampf((-zi * sa + zq * ca) * kk * 100.0f, -127, 127); scF[scopeIdx] = pskOpen ? 1 : 0; scW = scW + 1; pskPhase += 1.0f + pskRate; if (pskPhase >= 16) { pskPhase -= 16; pskDecided = false; } pskSinceDip++; if (pskOpen && pskSinceDip >= 10 && pskPk > 0 && pskM1 < pskM2 && pskM1 <= pskM0 && pskM1 < 0.6f * pskPk) { float den = pskM2 - 2 * pskM1 + pskM0; float d = den > 1e-12f ? 0.5f * (pskM2 - pskM0) / den : 0; if (d > 0.5f) d = 0.5f; else if (d < -0.5f) d = -0.5f; float e = pskPhase - (1 - d) * (1 + pskRate); e = fmodf(fmodf(e, 16.0f) + 16.0f, 16.0f); if (e > 8) e -= 16; float kp = 0.02f + 0.12f * expf(-(float)pskDips / 8.0f); float ki = 0.0002f + 0.0006f * expf(-(float)pskDips / 12.0f); pskDips++; if (fabsf(e) < 2.0f) pskLock++; else pskLock = 0; pskPhase -= kp * e; pskRate -= ki * e; if (pskRate > 0.015f) pskRate = 0.015f; else if (pskRate < -0.015f) pskRate = -0.015f; while (pskPhase < 0) pskPhase += 16; while (pskPhase >= 16) pskPhase -= 16; pskSinceDip = 0; } if (!pskDecided && pskPhase >= 10 && pskPhase < 14) { pskDecided = true; if (pskOpen) scF[scopeIdx] = 2; pskDecideBit(); } } static void processPSK31Sample(float s) { bool ready = pskMain.run(s); pskNoiseA.run(s); pskNoiseB.run(s); if (!ready) return; float na = pskNoiseA.zi * pskNoiseA.zi + pskNoiseA.zq * pskNoiseA.zq; float nb = pskNoiseB.zi * pskNoiseB.zi + pskNoiseB.zq * pskNoiseB.zq; pskSymbolTick(pskMain.zi, pskMain.zq, 0.5f * (na + nb)); } // ================================ CW (MORSE) ================================ #define CW_MIN_HZ 200.0f #define CW_MAX_HZ 3400.0f #define CW_DEB 3 #define CW_MERGE_GAP 8 #define CW_MIN_MARK 10 #define CW_DIT_MIN 20.0f #define CW_DIT_MAX 400.0f static const float CW_BWS[3] = { 60.0f, 120.0f, 250.0f }; struct MorseDef { char ch; const char* pat; }; static const MorseDef MORSE[] = { {'A', ".-"}, {'B', "-..."}, {'C', "-.-."}, {'D', "-.."}, {'E', "."}, {'F', "..-."}, {'G', "--."}, {'H', "...."}, {'I', ".."}, {'J', ".---"}, {'K', "-.-"}, {'L', ".-.."}, {'M', "--"}, {'N', "-."}, {'O', "---"}, {'P', ".--."}, {'Q', "--.-"}, {'R', ".-."}, {'S', "..."}, {'T', "-"}, {'U', "..-"}, {'V', "...-"}, {'W', ".--"}, {'X', "-..-"}, {'Y', "-.--"}, {'Z', "--.."}, {'0', "-----"}, {'1', ".----"}, {'2', "..---"}, {'3', "...--"}, {'4', "....-"}, {'5', "....."}, {'6', "-...."}, {'7', "--..."}, {'8', "---.."}, {'9', "----."}, {'.', ".-.-.-"}, {',', "--..--"}, {'?', "..--.."}, {'/', "-..-."}, {'=', "-...-"}, {'+', ".-.-."}, {'-', "-....-"}, {'(', "-.--."}, {')', "-.--.-"}, {'\'', ".----."}, {':', "---..."}, {';', "-.-.-."}, {'@', ".--.-."}, {'&', ".-..."}, {'"', ".-..-."}, {'$', "...-..-"}, {'!', "-.-.--"} }; #define N_MORSE ((int)(sizeof(MORSE) / sizeof(MORSE[0]))) static char morseDecode(const char* pat) { for (int i = 0; i < N_MORSE; i++) if (strcmp(MORSE[i].pat, pat) == 0) return MORSE[i].ch; return 0; } static const char* morseEncode(char ch) { for (int i = 0; i < N_MORSE; i++) if (MORSE[i].ch == ch) return MORSE[i].pat; return NULL; } static float cwFreq = 700.0f; static uint8_t cwBwIdx = 1; static bool cwAuto = true; static int cwWpm = 10; // usado quando a velocidade e fixa static int cwWpmDisp = 0; // velocidade mostrada na tela static float cwAlpha = 0.15f; static Tone cwTone, cwRefA, cwRefB; // tom principal e duas referencias de ruido static bool cwRefAOn = true, cwRefBOn = true; static float cwAcc = 0, cwRefAcc = 0; static int cwDecCnt = 0; static float cwNoise = 0, cwPeak = 0, cwEnvNow = 0, cwThrNow = 0; static bool cwPresent = false, cwRaw = false, cwKey = false, cwRunVal = false, cwDahValid = false, cwWordPending = false; static int cwRun = 0, cwOnMs = 0, cwOffMs = 0, cwPendingOn = 0, cwGapAtRise = 0, cwWarm = 0, cwTicks = 0; static float cwSym[16]; static int cwSymN = 0; static float cwHist[32]; static int cwHistN = 0; static float cwGapHist[24]; static int cwGapN = 0; static float cwDit = 100.0f, cwDah = 300.0f, cwWordThr = 500.0f; #define CW_TRACE_N 160 static uint8_t cwTrace[CW_TRACE_N], cwTraceThr[CW_TRACE_N]; static volatile uint8_t cwTraceW = 0; static void cwSetBw(uint8_t i) { cwBwIdx = i; cwAlpha = fminf(0.6f, TWO_PI_F * (CW_BWS[i] / 0.644f) / SR); } static void cwConfigRefs() { float off = 4.0f * CW_BWS[cwBwIdx]; cwRefAOn = (cwFreq + off) < 3800.0f; cwRefBOn = (cwFreq - off) > 100.0f; if (cwRefAOn) cwRefA.retune(cwFreq + off); if (cwRefBOn) cwRefB.retune(cwFreq - off); } static void setCwFreq(float f) { cwFreq = clampf(f, CW_MIN_HZ, CW_MAX_HZ); cwTone.retune(cwFreq); cwConfigRefs(); } static void cwResetTiming() { cwHistN = 0; cwGapN = 0; if (cwAuto) { cwDit = 100.0f; cwDah = 300.0f; cwDahValid = false; } else { cwDit = 1200.0f / cwWpm; cwDah = 3 * cwDit; cwDahValid = true; } cwWordThr = 5.0f * cwDit; cwWpmDisp = (int)(1200.0f / cwDit + 0.5f); } static void resetCwDecoder() { cwTone.setFreq(cwFreq); cwRefA.setFreq(cwFreq); cwRefB.setFreq(cwFreq); cwConfigRefs(); cwAcc = cwRefAcc = 0; cwDecCnt = 0; cwNoise = cwPeak = cwEnvNow = cwThrNow = 0; cwPresent = false; cwRaw = false; cwRun = 0; cwKey = false; cwOnMs = 0; cwOffMs = 0; cwPendingOn = 0; cwGapAtRise = 0; cwSymN = 0; cwWordPending = false; cwWarm = 400; cwRunVal = false; cwTicks = 0; memset(cwTrace, 0, sizeof(cwTrace)); memset(cwTraceThr, 0, sizeof(cwTraceThr)); cwResetTiming(); } static void setCwAuto(bool b) { cwAuto = b; cwResetTiming(); } static void sortF(float* a, int n) { for (int i = 1; i < n; i++) { float v = a[i]; int j = i - 1; while (j >= 0 && a[j] > v) { a[j + 1] = a[j]; j--; } a[j + 1] = v; } } static float medSorted(const float* a, int n) { return (n & 1) ? a[(n - 1) / 2] : 0.5f * (a[n / 2 - 1] + a[n / 2]); } static void cwUpdateWordThr() { float def = 5.0f * cwDit; int n = cwGapN; if (n < 3) { cwWordThr = def; return; } float s[24]; memcpy(s, cwGapHist, n * sizeof(float)); sortF(s, n); float lo = s[(int)floorf(0.1f * (n - 1))], hi = s[(int)ceilf(0.9f * (n - 1))]; if (n >= 4 && hi / lo >= 1.7f) { float t = sqrtf(lo * hi), g1[24], g2[24]; int n1 = 0, n2 = 0; for (int it = 0; it < 8; it++) { n1 = n2 = 0; for (int k = 0; k < n; k++) { if (s[k] < t) g1[n1++] = s[k]; else g2[n2++] = s[k]; } if (!n1 || !n2) break; t = 0.5f * (medSorted(g1, n1) + medSorted(g2, n2)); } cwWordThr = fmaxf(t, 3.5f * cwDit); return; } float m = medSorted(s, n); cwWordThr = m < 4.5f * cwDit ? def : 1.7f * m; } static void cwUpdateTiming(float d) { if (!cwAuto) return; if (cwHistN >= 32) { memmove(cwHist, cwHist + 1, 31 * sizeof(float)); cwHistN = 31; } cwHist[cwHistN++] = d; int n = cwHistN; float s[32]; memcpy(s, cwHist, n * sizeof(float)); sortF(s, n); if (n < 3) { cwDit = clampf(s[0], CW_DIT_MIN, CW_DIT_MAX); cwDah = 3 * cwDit; cwDahValid = false; cwWpmDisp = (int)(1200.0f / cwDit + 0.5f); cwUpdateWordThr(); return; } float lo = s[(int)floorf(0.1f * (n - 1))], hi = s[(int)ceilf(0.9f * (n - 1))]; float ndit = 0, ndah = 0; bool valid = false; if (hi / lo >= 1.9f) { float t = sqrtf(lo * hi), g1[32], g2[32]; int n1 = 0, n2 = 0; for (int it = 0; it < 8; it++) { n1 = n2 = 0; for (int k = 0; k < n; k++) { if (s[k] < t) g1[n1++] = s[k]; else g2[n2++] = s[k]; } if (!n1 || !n2) break; t = 0.5f * (medSorted(g1, n1) + medSorted(g2, n2)); } if (n1 && n2) { ndit = medSorted(g1, n1); ndah = medSorted(g2, n2); float r = ndah / ndit; valid = (r >= 2.0f && r <= 4.5f); } } if (!valid) { // so um grupo de duracoes: ou so pontos, ou so tracos float m = medSorted(s, n); if (cwDahValid && m > 0.5f * (cwDit + cwDah)) { ndit = cwDit; ndah = clampf(m, 2.0f * cwDit, 4.5f * cwDit); valid = true; } else { ndit = m; ndah = 3.0f * m; } } cwDit = clampf(ndit, CW_DIT_MIN, CW_DIT_MAX); cwDah = valid ? ndah : 3.0f * cwDit; cwDahValid = valid; cwWpmDisp = (int)(1200.0f / cwDit + 0.5f); cwUpdateWordThr(); } static void cwRecordGap(float g) { if (!cwAuto) return; if (cwGapN >= 24) { memmove(cwGapHist, cwGapHist + 1, 23 * sizeof(float)); cwGapN = 23; } cwGapHist[cwGapN++] = g; cwUpdateWordThr(); } static void cwEmitLetter() { if (cwSymN <= 0) return; float thr = cwDahValid ? 0.5f * (cwDit + cwDah) : 2.0f * cwDit; char pat[18]; int n = 0; for (int k = 0; k < cwSymN && n < 16; k++) pat[n++] = (cwSym[k] < thr) ? '.' : '-'; pat[n] = 0; cwSymN = 0; char c = morseDecode(pat); outChar(c ? c : '*'); } static void cwCommitPending() { if (cwPendingOn <= 0) return; if (cwSymN >= 16) cwEmitLetter(); cwSym[cwSymN++] = (float)cwPendingOn; cwUpdateTiming((float)cwPendingOn); cwPendingOn = 0; } static void cwTick(float env, float nref) { cwEnvNow = env; cwTicks++; if (cwNoise <= 0.0f) cwNoise = nref; else cwNoise += 0.004f * (nref - cwNoise); if (cwPeak < cwNoise) cwPeak = cwNoise; if (env > cwPeak) cwPeak += 0.25f * (env - cwPeak); else cwPeak += 0.0003f * (cwNoise - cwPeak); float nz = fmaxf(cwNoise, 1e-4f); float ratio = 1.0f + 12.0f * squelchPower; if (!cwPresent) cwPresent = (cwPeak > 0.003f && cwPeak > ratio * nz); else cwPresent = (cwPeak > 0.002f && cwPeak > 0.75f * ratio * nz); float span = cwPeak - cwNoise; if (span < 0) span = 0; cwThrNow = cwNoise + (cwRaw ? 0.35f : 0.5f) * span; if ((cwTicks % 20) == 0) { // um ponto do grafico a cada 20 ms float scale = fmaxf(fmaxf(cwPeak * 1.2f, cwNoise * 2.0f), 1e-4f); uint8_t w = cwTraceW; cwTrace[w] = (uint8_t)clampf(env / scale * 255.0f, 0, 255); cwTraceThr[w] = (uint8_t)clampf(cwThrNow / scale * 255.0f, 0, 255); cwTraceW = (uint8_t)((w + 1) % CW_TRACE_N); } if (cwWarm > 0) { cwWarm--; ledStatus(false, false); return; } if (!cwPresent) { if (cwKey || cwPendingOn > 0 || cwSymN > 0) { if (cwKey && cwOnMs >= CW_MIN_MARK) cwPendingOn = cwOnMs; cwCommitPending(); cwEmitLetter(); } cwKey = false; cwRaw = false; cwRun = 0; cwRunVal = false; cwOnMs = 0; cwOffMs = 0; cwWordPending = false; cwSymN = 0; cwPendingOn = 0; ledStatus(false, true); return; } ledStatus(true, false); bool raw = env > cwThrNow; cwRaw = raw; if (raw == cwRunVal) { if (cwRun < 10000) cwRun++; } else { cwRunVal = raw; cwRun = 1; } if (cwRunVal != cwKey && cwRun >= CW_DEB) { if (cwKey) { // marca -> espaco int d = cwOnMs - (CW_DEB - 1); if (d < 1) d = 1; cwKey = false; if (d < CW_MIN_MARK) { // ruido curto: o espaco continua cwOffMs = cwGapAtRise + d + CW_DEB; } else { cwPendingOn = d; cwOffMs = CW_DEB; } } else { // espaco -> marca int gap = cwOffMs - (CW_DEB - 1); if (gap < 1) gap = 1; cwKey = true; if (cwPendingOn > 0 && gap < CW_MERGE_GAP) { // buraco curto: junta com a marca anterior cwOnMs = cwPendingOn + gap + CW_DEB; cwPendingOn = 0; } else { cwCommitPending(); if (gap > 2.0f * cwDit && gap < 15.0f * cwDit) cwRecordGap((float)gap); cwGapAtRise = gap; cwOnMs = CW_DEB; cwWordPending = false; } } } else { if (cwKey) cwOnMs++; else cwOffMs++; } if (!cwKey && !cwRunVal) { // dentro de um espaco int off = cwOffMs - CW_DEB; if (cwPendingOn > 0 && off >= CW_MERGE_GAP) cwCommitPending(); if (cwSymN > 0 && cwPendingOn == 0 && (float)off >= 2.0f * cwDit) { cwEmitLetter(); cwWordPending = true; } if (cwWordPending && (float)off >= cwWordThr) { outChar(' '); cwWordPending = false; } } } static void processCWSample(float s) { float e = cwTone.run2(s, cwAlpha); float r = 0; bool have = false; if (cwRefAOn) { r = cwRefA.run2(s, cwAlpha); have = true; } if (cwRefBOn) { float b = cwRefB.run2(s, cwAlpha); r = have ? fminf(r, b) : b; have = true; } cwAcc += e; cwRefAcc += r; if (++cwDecCnt >= 8) { // 8 kHz / 8 = 1 ms float en = cwAcc * 0.125f, rn = cwRefAcc * 0.125f * 1.2f; cwAcc = 0; cwRefAcc = 0; cwDecCnt = 0; cwTick(en, rn); } } // ================== PROCURA AUTOMATICA DO TOM (Goertzel, 300..2700 Hz) ================== #define SCAN_F0 300.0f #define SCAN_DF 10.0f #define SCAN_NB 241 #define SCAN_N 800 static float scanCoef[SCAN_NB], scanS1[SCAN_NB], scanS2[SCAN_NB]; static int scanCnt = 0, scanStable = 0, scanLastIdx = -100, scanWeak = 0; static float scanHz = 0, scanRatio = 0; static void scanInit() { for (int k = 0; k < SCAN_NB; k++) scanCoef[k] = 2.0f * cosf(TWO_PI_F * (SCAN_F0 + SCAN_DF * k) / SR); } static void scanReset() { memset(scanS1, 0, sizeof(scanS1)); memset(scanS2, 0, sizeof(scanS2)); scanCnt = 0; scanStable = 0; scanLastIdx = -100; scanWeak = 0; scanHz = 0; scanRatio = 0; } static inline float scanPow(int k) { return scanS1[k] * scanS1[k] + scanS2[k] * scanS2[k] - scanCoef[k] * scanS1[k] * scanS2[k]; } static void scanEval() { float sum = 0, pmax = 0; int im = 0; for (int k = 0; k < SCAN_NB; k++) { float p = scanPow(k); if (p < 0) p = 0; sum += p; if (p > pmax) { pmax = p; im = k; } } float mean = sum / SCAN_NB; scanRatio = pmax / (mean + 1e-12f); float aEst = 2.0f * sqrtf(pmax) / SCAN_N; bool strong = (scanRatio > 25.0f && aEst > 0.004f); if (strong) { float y1 = sqrtf(pmax); float y0 = (im > 0) ? sqrtf(fmaxf(scanPow(im - 1), 0)) : y1; float y2 = (im < SCAN_NB - 1) ? sqrtf(fmaxf(scanPow(im + 1), 0)) : y1; float den = y0 - 2.0f * y1 + y2; float dlt = (fabsf(den) > 1e-12f) ? 0.5f * (y0 - y2) / den : 0.0f; dlt = clampf(dlt, -0.5f, 0.5f); scanHz = SCAN_F0 + SCAN_DF * (im + dlt); if (abs(im - scanLastIdx) <= 2) scanStable++; else scanStable = 1; scanLastIdx = im; scanWeak = 0; } else if (++scanWeak > 30) { scanStable = 0; scanLastIdx = -100; } for (int k = 0; k < SCAN_NB; k++) { scanS1[k] = 0; scanS2[k] = 0; } if (!strong) return; const uint8_t type = MODES[currentModeIdx].type; if (type == T_CW && cwAutoTune && scanStable >= 3 && fabsf(scanHz - cwFreq) > 15.0f) { setCwFreq(scanHz); } else if (type == T_PSK && pskAutoTune && !pskOpen && scanStable >= 5 && fabsf(scanHz - pskFreq) > 25.0f) { setPskFreq(scanHz); } } static void scanFeed(float x) { for (int k = 0; k < SCAN_NB; k++) { float s0 = x + scanCoef[k] * scanS1[k] - scanS2[k]; scanS2[k] = scanS1[k]; scanS1[k] = s0; } if (++scanCnt >= SCAN_N) { scanCnt = 0; scanEval(); } } // ================================ ASCII (110 / 300 baud, FSK) ================================ // Mesmo mark/space do RTTY (2125 / 2295 Hz). Receptor assincrono: start + 7 ou 8 bits (LSB primeiro) // + stop conferido no centro do bit de stop. O quadro (7 ou 8 bits) e escolhido automaticamente // pelo menor numero de erros de enquadramento. struct UartRx { uint8_t bits, st, idx; float cnt; uint16_t code; bool prev; void reset() { st = 0; idx = 0; cnt = 0.0f; code = 0; prev = true; } // devolve -1 (nada), -2 (erro de enquadramento) ou o codigo recebido int push(bool bit, float bp) { int res = -1; switch (st) { case 0: if (prev && !bit) { st = 1; cnt = bp * 0.5f; } break; case 1: cnt -= 1.0f; if (cnt <= 0.0f) { if (bit) st = 0; else { st = 2; idx = 0; code = 0; cnt += bp; } } break; case 2: cnt -= 1.0f; if (cnt <= 0.0f) { if (bit) code |= (1 << idx); idx++; cnt += bp; if (idx >= bits) st = 3; } break; case 3: cnt -= 1.0f; if (cnt <= 0.0f) { res = bit ? (int)code : -2; st = 0; } break; } prev = bit; return res; } }; static UartRx uartA7, uartA8; static float asciiAlpha = 0.05f, asciiS7 = 0.0f, asciiS8 = 0.0f; static uint8_t asciiPick = 7; static bool asciiLastCR = false; static void asciiConfigure() { const float bp = SR / MODES[currentModeIdx].baud; asciiAlpha = fminf(0.5f, 1.0f / (0.35f * bp)); toneNoiseA.setFreq(1500.0f); toneNoiseB.setFreq(2900.0f); // referencias de ruido mais afastadas (filtro largo) uartA7.bits = 7; uartA8.bits = 8; uartA7.reset(); uartA8.reset(); asciiS7 = asciiS8 = 0.0f; asciiPick = 7; asciiLastCR = false; } static void deliverAscii(int code) { int c = code & 0x7F; if (c == 13) { outChar('\n'); asciiLastCR = true; return; } if (c == 10) { if (!asciiLastCR) outChar('\n'); asciiLastCR = false; return; } asciiLastCR = false; if (c == 8) { outChar('\b'); return; } if (c == 9) { outChar(' '); return; } if (c >= 32 && c < 127) outChar((char)c); } static void processASCIISample(float s) { const float bp = SR / MODES[currentModeIdx].baud; float m = toneMark.run(s, asciiAlpha); float sp = toneSpace.run(s, asciiAlpha); float na = toneNoiseA.run(s, asciiAlpha); float nb = toneNoiseB.run(s, asciiAlpha); levelMark = fmaxf(m, levelMark * 0.9998f); levelSpace = fmaxf(sp, levelSpace * 0.9998f); markSm += 0.01f * (m - markSm); spaceSm += 0.01f * (sp - spaceSm); dispMark = markSm; dispSpace = spaceSm; rttySig += 0.002f * (fmaxf(m, sp) - rttySig); rttyNoise += 0.002f * (0.5f * (na + nb) - rttyNoise); dispNoise = rttyNoise; float pr = (rttySig * rttySig) / (rttyNoise * rttyNoise + 1e-12f); float thr = 1.0f + squelchPower * 20.0f; if (!rttyOpen) { if (pr >= thr && rttySig > 0.002f) rttyOpen = true; } else if (pr < 0.6f * thr || rttySig < 0.001f) rttyOpen = false; ledStatus(rttyOpen, !rttyOpen); bool bit = true; if (rttyOpen) { float lm = fmaxf(levelMark, 0.35f * levelSpace); float ls = fmaxf(levelSpace, 0.35f * levelMark); bool mk = (m / (lm + 1e-9f)) > (sp / (ls + 1e-9f)); bit = reverseMS ? !mk : mk; } rttyBit = bit; int r7 = uartA7.push(bit, bp), r8 = uartA8.push(bit, bp); if (r7 != -1) asciiS7 = asciiS7 * 0.95f + (r7 >= 0 ? 1.0f : -2.0f); if (r8 != -1) asciiS8 = asciiS8 * 0.95f + (r8 >= 0 ? 1.0f : -2.0f); if (asciiS8 > asciiS7 + 0.5f) asciiPick = 8; else if (asciiS7 > asciiS8 + 0.5f) asciiPick = 7; int r = (asciiPick == 7) ? r7 : r8; if (r >= 0) deliverAscii(r); } // ================================ SSTV ================================ // Demodulacao FM por sinal analitico (Hilbert FIR de 31 taps) a 8 kHz; imagem 160x128 RGB, // ampliada para tela cheia. Sincronismo por linha (pulso de 1200 Hz) e cabecalho VIS. #define SS_W 160 #define SS_H 128 #define SS_AMP_MIN 0.002f enum { SK_MARTIN = 0, SK_SCOTTIE, SK_ROBOT, SK_PD }; struct SsDef { const char* nm; uint8_t vis; uint8_t kind; uint16_t lines; float sync, porch, scan, sep, lt; }; #define N_SS 8 static const SsDef SSD[N_SS] = { { "MARTIN M1", 44, SK_MARTIN, 256, 0.004862f, 0.000572f, 0.146432f, 0.000572f, 0.446446f }, { "MARTIN M2", 40, SK_MARTIN, 256, 0.004862f, 0.000572f, 0.073216f, 0.000572f, 0.226798f }, { "SCOTTIE 1", 60, SK_SCOTTIE, 256, 0.009f, 0.0015f, 0.138240f, 0.0015f, 0.428220f }, { "SCOTTIE 2", 56, SK_SCOTTIE, 256, 0.009f, 0.0015f, 0.088064f, 0.0015f, 0.277692f }, { "ROBOT 36", 8, SK_ROBOT, 240, 0.009f, 0.003f, 0.088f, 0.0045f, 0.150f }, { "ROBOT 72", 12, SK_ROBOT, 240, 0.009f, 0.003f, 0.138f, 0.0045f, 0.300f }, { "PD120", 95, SK_PD, 496, 0.020f, 0.00208f, 0.1216f, 0.0f, 0.50848f }, { "SCOTTIE DX", 76, SK_SCOTTIE, 256, 0.009f, 0.0015f, 0.3456f, 0.0015f, 1.0503f } }; struct SsSeg { int32_t st, ln; uint8_t ch, act; }; // act: 1 = compoe linha RGB, 2/3 = grava luma direto // Cor virtual (0..1023) = indice real A (8 bits, 1 byte/pixel) + variante V (2 bits, 4 pixels por byte). // Total 25 KB (a versao de 256 cores usava 20 KB). A variante define o parceiro B de A (ver ssMixPartner). #if SS_TRUECOLOR static uint16_t ssTc[SS_H * SS_W]; // true color: pixel = TC_PIX(Y, a, b), lido direto pelo ISR de video (40 KB) #else static uint8_t ssPalette[SS_H * SS_W]; static uint8_t ssPalVar[(SS_H * SS_W) / 4]; #endif // ---- NITIDEZ: pipeline de linhas (soma/media de linhas -> mascara de nitidez -> cor) ---- // 3 linhas RGB de 8 bits: anterior, do meio (a que e emitida) e nova; a mascara de nitidez precisa das vizinhas. #define SS_SHARP_MAX 60 // limite do realce de luminancia (evita halos fortes) #define SS_EDGE_T 48 // |dR|+|dG|+|dB| entre vizinhos acima disso = borda: pixel usa cor PURA static float ssSharp = 0.4f; // forca da mascara de nitidez (0 = desligada); serial: k X static volatile bool ssMix = true; // true: pares A/B (1024 cores) nas areas suaves; false: so cores puras; serial: p static uint8_t ssRb[3][3][SS_W]; // [linha][canal][x] static uint16_t ssAcc[3][SS_W]; // soma das linhas de origem que caem na mesma linha 160x128 static uint8_t ssSh[3][SS_W]; // linha ja com nitidez static int ssIp = 0, ssIm = 1, ssIc = 2; // quais buffers sao anterior / meio / nova static int ssYp = -1, ssYm = -1, ssYc = -1; // linha de destino (y) de cada um (-1 = vazio) static int ssAccY = -1, ssAccN = 0, ssLastY = -1; static void ssPipeReset() { ssIp = 0; ssIm = 1; ssIc = 2; ssYp = ssYm = ssYc = -1; ssAccY = -1; ssAccN = 0; ssLastY = -1; memset(ssAcc, 0, sizeof(ssAcc)); } static volatile bool ssClearPending = false; // true: a imagem antiga so e apagada quando chega a 1a linha da nova #if SS_TRUECOLOR static inline void ssImgClear() { for (int i = 0; i < SS_H * SS_W; ++i) ssTc[i] = TC_BLACK; ssPipeReset(); } #else static inline void ssImgClear() { memset(ssPalette, 0, sizeof(ssPalette)); memset(ssPalVar, 0, sizeof(ssPalVar)); ssPipeReset(); } static inline void ssImgSet(int i, uint16_t id) { ssPalette[i] = (uint8_t)(id & 255); const int sh = (i & 3) * 2; ssPalVar[i >> 2] = (uint8_t)((ssPalVar[i >> 2] & ~(3 << sh)) | (((id >> 8) & 3) << sh)); } static inline uint8_t ssImgVar(int i) { return (ssPalVar[i >> 2] >> ((i & 3) * 2)) & 3; } #endif #if !SS_TRUECOLOR static uint8_t ssRgbToPalette(uint8_t r, uint8_t g, uint8_t b); #endif static float ssSum[SS_W]; static float ssWt[SS_W]; // peso (comprimento em amostras) acumulado em cada pixel static uint8_t ssCh[4][SS_W]; // 0 = R (ou Cr no Robot), 1 = G (ou Cb), 2 = B, 3 = Y static inline void ssChromaNeutral() { memset(ssCh[0], 128, SS_W); memset(ssCh[1], 128, SS_W); } // Robot: croma neutra ate chegar a primeira static SsSeg ssSeg[4]; static int ssNseg = 0, ssK = 0; static float ssHx[32], ssHc[8]; static int ssHp = 0; static float ssRePrev = 1.0f, ssImPrev = 0.0f, ssAmp = 0.0f, ssSf = 1500.0f; static float ssSfPrev = 1500.0f; // ssSf da amostra anterior (interpola o instante do sincronismo) static float ssFr = 0.0f; // fracao de amostra do inicio da linha: inicio real = ssT0 + ssFr static volatile float ssFiShow = 1500.0f; static uint32_t ssN = 0, ssT0 = 0, ssLastSync = 0, ssVisN0 = 0; static uint8_t ssState = 0; // 0 lider1, 1 espera quebra, 2 quebra, 3 lider2, 4 bits VIS, 5 espera sync, 6 varredura static int ssA = 0, ssB = 0, ssVc = 0, ssVn = 0, ssRow = 0, ssNoSig = 0, ssLowRun = 0; static float ssVsum = 0.0f; static uint8_t ssVbits = 0; // Detector de cabecalho (lider 1900 Hz + quebra + lider + VIS): roda SEMPRE, tambem durante a recepcao de uma // imagem (ou de estatica tratada como imagem), para que um sinal valido reinicie a imagem do comeco. static uint8_t ssHs = 0; // 0 lider 1, 1 espera a quebra, 2 quebra, 3 lider 2, 4 bits do VIS static int ssHA = 0, ssHB = 0, ssHVc = 0, ssHVn = 0; static float ssHVsum = 0.0f; static uint8_t ssHVbits = 0; static uint32_t ssHN0 = 0; static bool ssLow = false, ssHaveLast = false, ssFirst = false, ssFree = false, ssAuto = false, ssGotVis = false; static uint8_t ssSel = 0, ssCur = 0; static volatile uint8_t ssStat = 0; // 0 procurando, 1 VIS invalido, 2 aguardando sincronismo, 3 recebendo, 4 completa, 5 sinal perdido static volatile int ssRowShow = 0; static void ssInitHilbert() { for (int j = 0; j < 8; j++) { int m = 2 * j + 1; float h = 2.0f / (PI_F * (float)m); float w = 0.54f + 0.46f * cosf(PI_F * (float)m / 16.0f); ssHc[j] = h * w; } } static inline int32_t ssS(float t) { return (int32_t)(t * SR + 0.5f); } static void ssPipeFlushAll(); static void ssLose() { ssPipeFlushAll(); ssState = 0; ssA = 0; ssB = 0; ssStat = 5; ssHaveLast = false; } static void ssFinish() { ssState = 0; ssA = 0; ssB = 0; ssStat = 4; ssHaveLast = false; } static void ssSetup() { uint8_t sel = MODES[currentModeIdx].bits; ssAuto = (sel >= N_SS); ssSel = ssAuto ? 0 : sel; ssCur = ssSel; ssState = 0; ssA = 0; ssB = 0; ssLow = false; ssLowRun = 0; ssHaveLast = false; ssStat = 0; ssGotVis = false; ssRowShow = 0; ssNoSig = 0; ssFree = false; ssFirst = false; ssAmp = 0.0f; ssSf = 1500.0f; ssRePrev = 1.0f; ssImPrev = 0.0f; ssN = 0; memset(ssHx, 0, sizeof(ssHx)); ssHp = 0; ssChromaNeutral(); ssHs = 0; ssHA = 0; ssHB = 0; ssClearPending = false; ssImgClear(); memset(ssSum, 0, sizeof(ssSum)); memset(ssWt, 0, sizeof(ssWt)); } static void ssBuildSegs() { const SsDef& d = SSD[ssCur]; const int32_t L = ssS(d.scan); ssNseg = 0; #define SSEG(t, c, a) { ssSeg[ssNseg].st = ssS(t); ssSeg[ssNseg].ln = L; ssSeg[ssNseg].ch = (c); ssSeg[ssNseg].act = (a); ssNseg++; } #define SSEGL(t, len, c, a) { ssSeg[ssNseg].st = ssS(t); ssSeg[ssNseg].ln = (len); ssSeg[ssNseg].ch = (c); ssSeg[ssNseg].act = (a); ssNseg++; } switch (d.kind) { case SK_MARTIN: // sync, porch, G, B, R SSEG(d.porch, 1, 0); SSEG(d.porch + d.scan + d.sep, 2, 0); SSEG(d.porch + 2.0f * (d.scan + d.sep), 0, 1); break; case SK_SCOTTIE: // ... G, B, sync, porch, R (o sync fica entre B e R) if (ssRow < 0) { // primeiro periodo: sep, G, sep, B SSEG(d.sep, 1, 0); SSEG(2.0f * d.sep + d.scan, 2, 0); } else { SSEG(d.porch, 0, 1); // R fecha a linha (G e B vieram antes do sync) SSEG(d.porch + d.scan + d.sep, 1, 0); SSEG(d.porch + 2.0f * (d.scan + d.sep), 2, 0); } break; case SK_ROBOT: { // COLORIDO: Y + croma (R-Y = Cr, B-Y = Cb), croma com metade da largura const int32_t LC = ssS(d.scan * 0.5f); const float tc1 = d.porch + d.scan + d.sep + 0.0015f; // depois de Y, separador e porch de 1,5 ms SSEGL(d.porch, L, 3, 0); // Y if (d.lt < 0.2f) { // Robot 36: uma croma por linha SSEGL(tc1, LC, (ssRow & 1) ? 1 : 0, 4); // linhas pares: R-Y; impares: B-Y (a outra vem da linha vizinha) } else { // Robot 72: R-Y e depois B-Y em cada linha SSEGL(tc1, LC, 0, 0); SSEGL(tc1 + d.scan * 0.5f + d.sep + 0.0015f, LC, 1, 4); } break; } case SK_PD: // COLORIDO: Y (linha par), R-Y, B-Y, Y (linha impar); croma comum ao par SSEG(d.porch, 2, 0); // Y da linha par -> ssCh[2] SSEG(d.porch + d.scan, 0, 0); // R-Y (Cr) -> ssCh[0] SSEG(d.porch + 2.0f * d.scan, 1, 0); // B-Y (Cb) -> ssCh[1] SSEG(d.porch + 3.0f * d.scan, 3, 5); // Y da linha impar -> ssCh[3]; monta as duas linhas break; default: break; } #undef SSEG } // ---- TABELA DE CORES SSTV ---------------------------------------------------------------- // A paleta de video (USE_ATARI_COLORS) tem 256 cores: indice = (matiz << 4) | brilho. // Matiz 0 = escala de cinza; matizes 1..15 = cores com saturacao fixa e fase crescente. // Aqui cada indice e convertido para o RGB que a TV mostra (modelo YIQ) e a imagem recebida e // casada com o indice MAIS PROXIMO em cor (nao so por matiz + brilho, como antes). #define SS_DITHER_ERRMAX 48 // limita o erro difundido (cores fora do alcance da paleta) static float ssPalSat = 0.32f; // amplitude da croma (YIQ). Sobe = cores mais fortes static float ssPalPhase = -24.0f; // fase do matiz 1 em graus (YIQ: 0 = laranja, 90 = magenta) static float ssPalStep = 24.0f; // passo de fase entre matizes (360/15); negativo inverte static float ssPalGamma = 1.0f; // curva de brilho (1.0 = linear com o nibble de brilho) static volatile bool ssDither = true; // TABELA VIRTUAL DE 1024 CORES: o video so tem 256 indices reais (1 byte por pixel na biblioteca), // entao cada cor virtual = indice real A + variante V (0..3) = PAR (A, B) mostrado em pixels vizinhos // (x par = A, x impar = B). A TV mistura os dois e o olho ve a cor intermediaria. id = (V << 8) | A. // V0: B = A (cor pura, as 256 originais) // V1: B = mesmo matiz, brilho + 1 (meio-passo de brilho) // V2: B = matiz vizinho, mesmo brilho (1..15 ciclico; cinza mistura com o matiz 1) // V3: B = cinza do mesmo brilho (dessatura; no cinza, brilho + 2) // Nao ha tabelas de pares na RAM: B e calculado na hora. Memoria extra total ~ 6 KB. #define SS_NMIX 1024 static uint8_t ssPalRGB[256][3]; // RGB esperado de cada indice real da paleta static uint8_t* ssLut = nullptr; // 5 KB: 4096 bytes (indice A) + 1024 bytes (4 variantes por byte) static inline uint8_t ssMixPartner(uint8_t a, uint8_t v) { const int h = a >> 4, l = a & 15; switch (v) { case 1: return (uint8_t)((h << 4) | (l < 15 ? l + 1 : l)); case 2: return (uint8_t)((h == 0 ? 1 : (h == 15 ? 1 : h + 1)) << 4 | l); case 3: return (uint8_t)(h == 0 ? (l < 14 ? l + 2 : 15) : l); default: return a; } } static inline void ssMixRGBOf(uint16_t id, uint8_t* o) { const uint8_t a = (uint8_t)(id & 255), b = ssMixPartner(a, (uint8_t)(id >> 8)); for (int c = 0; c < 3; ++c) o[c] = (uint8_t)(((int)ssPalRGB[a][c] + (int)ssPalRGB[b][c] + 1) >> 1); } static inline uint16_t ssLutGet(int i) { return (uint16_t)(ssLut[i] | (((ssLut[4096 + (i >> 2)] >> ((i & 3) * 2)) & 3) << 8)); } static inline void ssLutSet(int i, uint16_t id) { ssLut[i] = (uint8_t)(id & 255); const int sh = (i & 3) * 2; ssLut[4096 + (i >> 2)] = (uint8_t)((ssLut[4096 + (i >> 2)] & ~(3 << sh)) | (((id >> 8) & 3) << sh)); } static int16_t ssErr[2][SS_W + 2][3];// erro de difusao (x16) da linha atual e da proxima static int ssErrRow = 0, ssDithLast = 0x7fffffff; static bool ssDirRtl = false; static void ssBuildPalette() { const float deg = PI_F / 180.0f; for (int h = 0; h < 16; ++h) { float I = 0.0f, Q = 0.0f; if (h) { const float a = (ssPalPhase + (float)(h - 1) * ssPalStep) * deg; I = ssPalSat * cosf(a); Q = ssPalSat * sinf(a); } for (int l = 0; l < 16; ++l) { const float Y = powf((float)l / 15.0f, ssPalGamma); const float R = Y + 0.956f * I + 0.621f * Q; const float G = Y - 0.272f * I - 0.647f * Q; const float B = Y - 1.106f * I + 1.703f * Q; uint8_t* p = ssPalRGB[(h << 4) | l]; p[0] = (uint8_t)(clampf(R, 0.0f, 1.0f) * 255.0f + 0.5f); p[1] = (uint8_t)(clampf(G, 0.0f, 1.0f) * 255.0f + 0.5f); p[2] = (uint8_t)(clampf(B, 0.0f, 1.0f) * 255.0f + 0.5f); } } } // Distancia de cor "redmean" (aproxima a percepcao; so aritmetica inteira) static inline int ssColDist(int r, int g, int b, const uint8_t* p) { const int rm = (r + p[0]) >> 1; const int dr = r - p[0], dg = g - p[1], db = b - p[2]; return (((512 + rm) * dr * dr) >> 8) + 4 * dg * dg + (((767 - rm) * db * db) >> 8); } static void ssBuildLut() { for (int i = 0; i < 4096; ++i) { const int r = (((i >> 8) & 15) << 4) + 8; // centro da faixa de 16 niveis const int g = (((i >> 4) & 15) << 4) + 8; const int b = ((i & 15) << 4) + 8; int best = 0, bd = 0x7fffffff; uint8_t q[3]; for (int k = 0; k < SS_NMIX; ++k) { ssMixRGBOf((uint16_t)k, q); const int d = ssColDist(r, g, b, q); if (d < bd) { bd = d; best = k; } } ssLutSet(i, (uint16_t)best); } } // Monta (ou remonta) a tabela. Devolve false se faltar memoria (usa-se a conversao simples). static bool ssPalRebuild() { ssBuildPalette(); if (!ssLut) { ssLut = (uint8_t*)malloc(4096 + 1024); if (ssLut) memset(ssLut, 0, 4096 + 1024); } if (!ssLut) return false; ssBuildLut(); return true; } static inline int ssLutIndex(int r, int g, int b) { return ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4); } #if SS_TRUECOLOR // ---------------- TRUE COLOR: RGB -> (Y, a, b) em niveis do DAC ---------------- // Luma: preto = 24, branco = 73 (49 niveis). Croma (padrao NTSC): (a, b) e o fasor da subportadora medido // contra o burst; relacao com YIQ derivada das tabelas da propria biblioteca (atari_4_phase_ntsc): // a = k * ( cos(psi) * I - sin(psi) * Q ) b = -k * ( sin(psi) * I + cos(psi) * Q ) // psi = 57 graus + giro de matiz; k = 48,5 niveis por unidade de croma * ganho static float ssTcGain = 1.0f; // ganho de croma (1.0 = NTSC padrao); serial: c X static float ssTcHue = 0.0f; // giro de matiz em graus; serial: h X static float ssTcGamma = 1.0f; // gama da luminancia; serial: y X static float ssTcM[4] = { 0, 0, 0, 0 }; static void ssTcCalc() { const float psi = (57.0f + ssTcHue) * PI_F / 180.0f; const float k = 48.5f * ssTcGain, c = cosf(psi), sn = sinf(psi); ssTcM[0] = k * c; ssTcM[1] = -k * sn; ssTcM[2] = -k * sn; ssTcM[3] = -k * c; } #define SS_TC_ERRMAX 1.5f // limita o erro de quantizacao difundido (em niveis do DAC) static void ssConvertRow(int off, const uint8_t* r, const uint8_t* g, const uint8_t* b) { const bool dith = ssDither; int16_t (*cur)[3] = ssErr[ssErrRow]; int16_t (*nxt)[3] = ssErr[ssErrRow ^ 1]; if (dith) memset(nxt, 0, sizeof(ssErr[0])); const int dir = ssDirRtl ? -1 : 1; int x = ssDirRtl ? SS_W - 1 : 0; const float k255 = 1.0f / 255.0f; for (int n = 0; n < SS_W; ++n, x += dir) { const float R = r[x] * k255, G = g[x] * k255, B = b[x] * k255; float Y = 0.299f * R + 0.587f * G + 0.114f * B; const float I = 0.596f * R - 0.274f * G - 0.322f * B; const float Q = 0.211f * R - 0.523f * G + 0.312f * B; if (ssTcGamma != 1.0f) Y = powf(Y, ssTcGamma); float yt = 49.0f * Y; float at = ssTcM[0] * I + ssTcM[1] * Q; float bt = ssTcM[2] * I + ssTcM[3] * Q; if (dith) { yt += (float)cur[x + 1][0] * (1.0f / 256.0f); at += (float)cur[x + 1][1] * (1.0f / 256.0f); bt += (float)cur[x + 1][2] * (1.0f / 256.0f); } int yq = (int)lroundf(yt); yq = yq < 0 ? 0 : (yq > 49 ? 49 : yq); // limite de amplitude: nenhuma amostra abaixo de 14 (perto do nivel de sincronismo) e |a|,|b| <= 15 const float lim = (10.0f + (float)yq) < 15.0f ? (10.0f + (float)yq) : 15.0f; const float am = fabsf(at) > fabsf(bt) ? fabsf(at) : fabsf(bt); if (am > lim) { const float sc = lim / am; at *= sc; bt *= sc; } int aq = (int)lroundf(at), bq = (int)lroundf(bt); aq = aq < -15 ? -15 : (aq > 15 ? 15 : aq); bq = bq < -15 ? -15 : (bq > 15 ? 15 : bq); ssTc[off + x] = TC_PIX(yq, aq, bq); if (!dith) continue; const float e3[3] = { yt - (float)yq, at - (float)aq, bt - (float)bq }; for (int c = 0; c < 3; ++c) { float e = e3[c]; e = e > SS_TC_ERRMAX ? SS_TC_ERRMAX : (e < -SS_TC_ERRMAX ? -SS_TC_ERRMAX : e); const int e16 = (int)lroundf(e * 16.0f); // erro x16; os pesos de Floyd-Steinberg somam 16 (x256 no total) cur[x + 1 + dir][c] += (int16_t)(e16 * 7); nxt[x + 1 - dir][c] += (int16_t)(e16 * 3); nxt[x + 1][c] += (int16_t)(e16 * 5); nxt[x + 1 + dir][c] += (int16_t)(e16 * 1); } } if (dith) { ssErrRow ^= 1; ssDirRtl = !ssDirRtl; } } #else // Converte uma linha RGB para indices da paleta (difusao de erro Floyd-Steinberg em serpentina). // Cores MISTAS (par A,B em pixels vizinhos) so onde a imagem e suave: nas bordas o pixel vira cor PURA // (A ou B, a mais proxima), porque misturar com o vizinho borra justamente o detalhe fino. static void ssConvertRow(int off, const uint8_t* r, const uint8_t* g, const uint8_t* b) { if (!ssLut) { // sem memoria: conversao simples for (int x = 0; x < SS_W; ++x) ssImgSet(off + x, ssRgbToPalette(r[x], g[x], b[x])); return; } const bool dith = ssDither; int16_t (*cur)[3] = ssErr[ssErrRow]; int16_t (*nxt)[3] = ssErr[ssErrRow ^ 1]; if (dith) memset(nxt, 0, sizeof(ssErr[0])); const int dir = ssDirRtl ? -1 : 1; int x = ssDirRtl ? SS_W - 1 : 0; for (int n = 0; n < SS_W; ++n, x += dir) { int v[3]; if (dith) { v[0] = r[x] + cur[x + 1][0] / 16; v[1] = g[x] + cur[x + 1][1] / 16; v[2] = b[x] + cur[x + 1][2] / 16; for (int c = 0; c < 3; ++c) v[c] = v[c] < 0 ? 0 : (v[c] > 255 ? 255 : v[c]); } else { v[0] = r[x]; v[1] = g[x]; v[2] = b[x]; } uint16_t idx = ssLutGet(ssLutIndex(v[0], v[1], v[2])); bool edgy = false; if (idx > 255) { bool pure = !ssMix; if (!pure) { const int xl = x > 0 ? x - 1 : x, xr = x < SS_W - 1 ? x + 1 : x; const int el = abs((int)r[x] - (int)r[xl]) + abs((int)g[x] - (int)g[xl]) + abs((int)b[x] - (int)b[xl]); const int er = abs((int)r[x] - (int)r[xr]) + abs((int)g[x] - (int)g[xr]) + abs((int)b[x] - (int)b[xr]); edgy = (el > SS_EDGE_T) || (er > SS_EDGE_T); pure = edgy; } if (pure) { const uint8_t a = (uint8_t)(idx & 255), q = ssMixPartner(a, (uint8_t)(idx >> 8)); idx = (ssColDist(v[0], v[1], v[2], ssPalRGB[q]) < ssColDist(v[0], v[1], v[2], ssPalRGB[a])) ? q : a; } } ssImgSet(off + x, idx); if (!dith) continue; uint8_t p[3]; ssMixRGBOf(idx, p); for (int c = 0; c < 3; ++c) { int e = v[c] - (int)p[c]; if (e > SS_DITHER_ERRMAX) e = SS_DITHER_ERRMAX; if (e < -SS_DITHER_ERRMAX) e = -SS_DITHER_ERRMAX; if (edgy) e /= 2; // nao espalha tanto erro atraves de uma borda cur[x + 1 + dir][c] += (int16_t)(e * 7); nxt[x + 1 - dir][c] += (int16_t)(e * 3); nxt[x + 1][c] += (int16_t)(e * 5); nxt[x + 1 + dir][c] += (int16_t)(e * 1); } } if (dith) { ssErrRow ^= 1; ssDirRtl = !ssDirRtl; } } #endif static inline int ssLum(const uint8_t (*row)[SS_W], int x) { return (77 * (int)row[0][x] + 150 * (int)row[1][x] + 29 * (int)row[2][x]) >> 8; } // Mascara de nitidez (so na luminancia: realca contorno sem colorir ruido) e conversao da linha y. // p / n = linhas vizinhas de cima / de baixo (nullptr = nao existe). static void ssEmitRow(int y, const uint8_t (*p)[SS_W], const uint8_t (*m)[SS_W], const uint8_t (*n)[SS_W]) { const uint8_t (*src)[SS_W] = m; const int k = (int)(ssSharp * 64.0f); if (k > 0) { for (int x = 0; x < SS_W; ++x) { const int xl = x > 0 ? x - 1 : x, xr = x < SS_W - 1 ? x + 1 : x; const int ym = ssLum(m, x); const int lap = 4 * ym - ssLum(m, xl) - ssLum(m, xr) - (p ? ssLum(p, x) : ym) - (n ? ssLum(n, x) : ym); int d = (k * lap) / 256; // = ssSharp * (Y - media dos 4 vizinhos) if (d > SS_SHARP_MAX) d = SS_SHARP_MAX; if (d < -SS_SHARP_MAX) d = -SS_SHARP_MAX; for (int c = 0; c < 3; ++c) { int v = (int)m[c][x] + d; ssSh[c][x] = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v)); } } src = ssSh; } ssConvertRow(y * SS_W, src[0], src[1], src[2]); if (ssLastY >= 0 && y > ssLastY + 1) { // linhas perdidas: repete a anterior em vez de deixar preto for (int yy = ssLastY + 1; yy < y; ++yy) { #if SS_TRUECOLOR memcpy(&ssTc[yy * SS_W], &ssTc[ssLastY * SS_W], SS_W * sizeof(uint16_t)); #else memcpy(&ssPalette[yy * SS_W], &ssPalette[ssLastY * SS_W], SS_W); memcpy(&ssPalVar[(yy * SS_W) / 4], &ssPalVar[(ssLastY * SS_W) / 4], SS_W / 4); #endif } } ssLastY = y; } // Fecha a linha em acumulacao: media das linhas de origem -> linha "nova"; emite a do meio (que agora tem // vizinha de baixo) e desloca anterior <- meio <- nova. static void ssPipeAdvance() { if (ssAccN == 0) return; const uint32_t inv = 65536u / (uint32_t)ssAccN; for (int c = 0; c < 3; ++c) for (int x = 0; x < SS_W; ++x) ssRb[ssIc][c][x] = (uint8_t)(((uint32_t)ssAcc[c][x] * inv + 32768u) >> 16); ssYc = ssAccY; memset(ssAcc, 0, sizeof(ssAcc)); ssAccN = 0; ssAccY = -1; if (ssYm >= 0) ssEmitRow(ssYm, ssYp >= 0 ? ssRb[ssIp] : nullptr, ssRb[ssIm], ssRb[ssIc]); const int t = ssIp; ssIp = ssIm; ssYp = ssYm; ssIm = ssIc; ssYm = ssYc; ssIc = t; ssYc = -1; } static void ssPipeFlush() { // ultima linha: sem vizinha de baixo if (ssYm >= 0) ssEmitRow(ssYm, ssYp >= 0 ? ssRb[ssIp] : nullptr, ssRb[ssIm], nullptr); ssYm = -1; } static void ssPipeFlushAll() { ssPipeAdvance(); ssPipeFlush(); } static void ssResetDither() { memset(ssErr, 0, sizeof(ssErr)); ssErrRow = 0; ssDirRtl = false; } static void ssPutRow(int srcRow, const uint8_t* r, const uint8_t* g, const uint8_t* b) { if (srcRow < 0) return; if (ssClearPending) { ssClearPending = false; ssImgClear(); ssDithLast = 0x7fffffff; } // nova recepcao de verdade if (srcRow <= ssDithLast) { ssResetDither(); ssPipeReset(); } // linha 0 (ou repetida) = imagem nova ssDithLast = srcRow; const SsDef& d = SSD[ssCur]; int y = (int)((int32_t)srcRow * SS_H / d.lines); if (y >= SS_H) y = SS_H - 1; // Varias linhas de origem caem na mesma linha 160x128 (256/240/496 linhas -> 128): faz a MEDIA delas // (antes so a ultima ficava e as outras eram jogadas fora = aliasing e ruido). if (ssAccN > 0 && y != ssAccY) ssPipeAdvance(); ssAccY = y; for (int x = 0; x < SS_W; ++x) { ssAcc[0][x] += r[x]; ssAcc[1][x] += g[x]; ssAcc[2][x] += b[x]; } ssAccN++; ssRowShow = srcRow; if (srcRow >= (int)d.lines - 1) { ssPipeFlushAll(); ssFinish(); } } static void ssCompose() { ssPutRow(ssRow, ssCh[0], ssCh[1], ssCh[2]); } // Robot / PD: Y (ssCh[ych]) + Cr (ssCh[0]) + Cb (ssCh[1]) -> RGB (ITU-R BT.601, faixa de estudio) na linha "row" static void ssComposeYUV(int row, int ych) { static uint8_t tr[SS_W], tg[SS_W], tb[SS_W]; for (int x = 0; x < SS_W; ++x) { const float y = 1.164f * ((float)ssCh[ych][x] - 16.0f); const float cr = (float)ssCh[0][x] - 128.0f, cb = (float)ssCh[1][x] - 128.0f; float R = y + 1.596f * cr, G = y - 0.392f * cb - 0.813f * cr, B = y + 2.017f * cb; tr[x] = (uint8_t)(R < 0 ? 0 : (R > 255 ? 255 : R + 0.5f)); tg[x] = (uint8_t)(G < 0 ? 0 : (G > 255 ? 255 : G + 0.5f)); tb[x] = (uint8_t)(B < 0 ? 0 : (B > 255 ? 255 : B + 0.5f)); } ssPutRow(row, tr, tg, tb); } static void ssPutGrayRow(int srcRow, const uint8_t* lum) { ssPutRow(srcRow, lum, lum, lum); } static void ssEndSeg(int gi) { // recebe o indice (o pre-processador do Arduino poe prototipos antes das structs) const SsSeg& g = ssSeg[gi]; uint8_t last = 0; for (int p = 0; p < SS_W; p++) { if (ssWt[p] > 0.05f) { float v = (ssSum[p] / ssWt[p] - 1500.0f) * (255.0f / 800.0f); last = (uint8_t)clampf(v, 0.0f, 255.0f); } ssCh[g.ch][p] = last; ssSum[p] = 0.0f; ssWt[p] = 0.0f; } const SsDef& d = SSD[ssCur]; if (g.act == 1) ssCompose(); else if (g.act == 2) ssPutGrayRow(d.kind == SK_PD ? 2 * ssRow : ssRow, ssCh[3]); else if (g.act == 4) ssComposeYUV(ssRow, 3); // Robot else if (g.act == 5) { ssComposeYUV(2 * ssRow, 2); ssComposeYUV(2 * ssRow + 1, 3); } // PD: as duas linhas do par else if (g.act == 3) ssPutGrayRow(2 * ssRow + 1, ssCh[3]); if (ssState != 6) return; // imagem completa ssK++; if (ssK >= ssNseg) { if (ssFree) { ssRow++; ssK = 0; const float add = d.lt * SR; ssFr += add; const int32_t whole = (int32_t)floorf(ssFr); ssT0 += (uint32_t)whole; ssFr -= (float)whole; } else ssState = 5; } } static void ssBeginImage(uint32_t vis0) { ssClearPending = true; // NAO apaga agora: a imagem anterior fica na tela ate chegar a 1a linha ssChromaNeutral(); memset(ssSum, 0, sizeof(ssSum)); memset(ssWt, 0, sizeof(ssWt)); const SsDef& d = SSD[ssCur]; ssRow = (d.kind == SK_SCOTTIE) ? -1 : 0; ssFirst = true; ssK = 0; ssRowShow = 0; ssNoSig = 0; (void)vis0; ssFree = false; ssT0 = ssN; ssFr = 0.0f; // referencia para o tempo limite do primeiro sincronismo ssState = 5; ssStat = 2; } static void ssVisDone() { uint8_t v = ssHVbits & 0x7F; int ones = 0; for (int b = 0; b < 7; b++) ones += (v >> b) & 1; ones += (ssHVbits >> 7) & 1; int idx = -1; for (int i = 0; i < N_SS; i++) if (SSD[i].vis == v) idx = i; if ((ones & 1) || idx < 0 || (!ssAuto && idx != ssSel)) { if (ssState == 0) { ssA = 0; ssStat = 1; } // so informa "VIS invalido" se nao havia imagem em andamento return; // uma imagem em andamento NAO e interrompida por um VIS invalido } ssCur = (uint8_t)idx; ssGotVis = true; ssBeginImage(ssHN0); // VIS valido: recomeca do comeco, mesmo no meio de outra "imagem" } // Maquina do cabecalho (estados 0..4 do detector). Durante uma imagem exige lideres mais longos (250 ms) // para que um trecho cinza liso da imagem nao seja confundido com um cabecalho. static void ssHdrStep(float fi, bool sig, bool inL) { const int need1 = (ssState == 0) ? 400 : 2000; const int need2 = (ssState == 0) ? 800 : 2000; switch (ssHs) { case 0: // lider 1 (1900 Hz) if (inL) { if (++ssHA >= need1) { ssHs = 1; ssHB = 0; } } else ssHA = 0; break; case 1: // espera a quebra de 1200 Hz if (!sig) { ssHs = 0; ssHA = 0; } else if (ssSf < 1450.0f) { ssHs = 2; ssHA = 0; } else if (++ssHB > 4800) { ssHs = 0; ssHA = 0; } break; case 2: // quebra (~10 ms) if (ssSf < 1450.0f) { if (++ssHA > 400) { ssHs = 0; ssHA = 0; } } else if (inL) { if (ssHA >= 24) { ssHs = 3; ssHA = 1; } else { ssHs = 0; ssHA = 0; } } break; case 3: // lider 2; a queda marca o bit de partida do VIS if (inL) ssHA++; else if (ssSf < 1450.0f) { if (ssHA >= need2) { ssHs = 4; ssHVc = 0; ssHVsum = 0.0f; ssHVn = 0; ssHVbits = 0; ssHN0 = ssN - 3; } else { ssHs = 0; ssHA = 0; } } break; case 4: { // 8 bits (7 dados + paridade), 30 ms cada ssHVc++; int k = ssHVc / 240 - 1, ph = ssHVc % 240; if (k >= 0 && k < 8) { if (ph >= 40 && ph < 200) { ssHVsum += fi; ssHVn++; } else if (ph == 200 && ssHVn > 0) { if (ssHVsum / (float)ssHVn < 1200.0f) ssHVbits |= (uint8_t)(1 << k); ssHVsum = 0.0f; ssHVn = 0; } } if (ssHVc >= 2121) { ssHs = 0; ssHA = 0; ssVisDone(); } break; } } } // fim do pulso de sincronismo (subida acima de ~1430 Hz depois de pelo menos 3 ms abaixo de 1360 Hz) static void ssOnSync() { uint32_t tEnd = ssN - 3; // Instante exato (sub-amostra) em que ssSf cruzou 1430 Hz entre a amostra anterior e esta. // Mesma calibracao de antes na media (+-0,5 amostra em torno de tEnd), mas sem o serrilhado de 1 amostra. float frc = 0.5f; { const float dsf = ssSf - ssSfPrev; if (dsf > 0.001f) frc = clampf((1430.0f - ssSfPrev) / dsf, 0.0f, 1.0f); } const float fracOff = frc - 0.5f; const SsDef& d = SSD[ssCur]; if (ssState == 5) { if (ssFirst) ssFirst = false; else { float r = (float)(tEnd - ssLastSync) / (d.lt * SR); int kk = (int)(r + 0.5f); if (kk < 1) kk = 1; if (kk > 4) { ssLose(); return; } ssRow += kk; } ssLastSync = tEnd; ssT0 = tEnd; ssFr = fracOff; ssK = 0; ssBuildSegs(); ssState = 6; ssStat = 3; } else if (ssState == 0) { // modo fixo sem VIS: dois sincronismos separados por um tempo de linha iniciam a imagem if (!ssAuto && ssHs == 0) { ssCur = ssSel; const SsDef& e = SSD[ssCur]; float lt = e.lt * SR; if (ssHaveLast && fabsf((float)(tEnd - ssLastSync) - lt) < 0.04f * lt) { ssClearPending = true; ssChromaNeutral(); memset(ssSum, 0, sizeof(ssSum)); memset(ssWt, 0, sizeof(ssWt)); ssRow = 0; ssFirst = false; ssFree = false; ssRowShow = 0; ssNoSig = 0; ssLastSync = tEnd; ssT0 = tEnd; ssFr = fracOff; ssK = 0; ssBuildSegs(); ssState = 6; ssStat = 3; return; } } ssLastSync = tEnd; ssHaveLast = true; } } // Integra a frequencia instantanea por AREA: cada amostra (1 amostra de largura, centrada em ac) reparte // o seu peso entre os dois pixels que ela cobre. Com o instante do sincronismo em fracao de amostra, as // bordas verticais ficam alinhadas de linha a linha (sem o serrilhado de +-1 amostra a 8 kHz). static void ssScan(float fi) { const SsSeg& g = ssSeg[ssK]; const float ln = (float)g.ln; const float ac = (float)(int32_t)(ssN - ssT0) - ssFr - (float)g.st; // posicao do centro da amostra no segmento if (ac >= ln) { ssEndSeg(ssK); return; } const float a1 = ac + 0.5f; if (a1 <= 0.0f) return; const float lo = (ac - 0.5f) < 0.0f ? 0.0f : (ac - 0.5f); const float hi = a1 > ln ? ln : a1; const float kW = (float)SS_W / ln; // pixels por amostra const int p0 = (int)(lo * kW); int p1 = (int)(hi * kW); if (p1 > SS_W - 1) p1 = SS_W - 1; if (p0 >= SS_W) return; if (p0 >= p1) { const float w = hi - lo; ssSum[p0] += fi * w; ssWt[p0] += w; } else { const float edge = (float)p1 / kW; // fronteira entre os dois pixels (em amostras) const float w0 = edge - lo, w1 = hi - edge; ssSum[p0] += fi * w0; ssWt[p0] += w0; ssSum[p1] += fi * w1; ssWt[p1] += w1; } } static void processSSTVSample(float x) { // sinal analitico: parte real = x atrasado 15 amostras; parte imaginaria = Hilbert FIR ssHx[ssHp] = x; ssHp = (ssHp + 1) & 31; const int nw = ssHp - 1; // posicao da amostra mais nova float re = ssHx[(nw - 15) & 31]; float im = 0.0f; for (int j = 0; j < 8; j++) { int m = 2 * j + 1; im += ssHc[j] * (ssHx[(nw - 15 - m) & 31] - ssHx[(nw - 15 + m) & 31]); } float dot = re * ssRePrev + im * ssImPrev; float crs = im * ssRePrev - re * ssImPrev; ssRePrev = re; ssImPrev = im; ssAmp += 0.01f * ((re * re + im * im) - ssAmp); float fi = clampf(atan2f(crs, dot) * (SR / TWO_PI_F), 1000.0f, 2600.0f); ssSfPrev = ssSf; ssSf += 0.3f * (fi - ssSf); ssFiShow = ssSf; ssN++; if (ssN < 48) return; const bool sig = ssAmp > SS_AMP_MIN; ledStatus(sig, !sig); if (!sig) { if (++ssNoSig > 12000 && ssState != 0 && !ssFree) ssLose(); } else ssNoSig = 0; // detector de sincronismo if (!sig) ssLow = false; else if (!ssLow) { if (ssSf < 1360.0f) { ssLow = true; ssLowRun = 0; } } else { ssLowRun++; if (ssSf > 1430.0f) { ssLow = false; if (ssLowRun >= 24) ssOnSync(); } } const bool inL = sig && ssSf > 1800.0f && ssSf < 2000.0f; ssHdrStep(fi, sig, inL); // detector de cabecalho: sempre ativo switch (ssState) { case 0: // ocioso: so o detector de cabecalho (acima) trabalha break; case 5: // espera o sincronismo (ssOnSync inicia a linha) if ((uint32_t)(ssN - ssT0) > (uint32_t)(SSD[ssCur].lt * SR * 3.0f) + 4000u) ssLose(); break; case 6: ssScan(fi); break; } } // ================== GERADOR DE SINAL DE TESTE ================== static uint8_t tUnits[1536]; static int tN = 0, tPos = 0; static float tUnitCnt = 0, tUnitLen = 1, tPh = 0, tGate = 0; static int tSymPos = 0; static float tSign = 1.0f, tPrevSign = 1.0f; static bool tRev = false; static float testAmp = 0.30f, testNoise = 0.03f, tFreq = 1000.0f; static uint32_t rngState = 2463534242u; static float rndf() { // ruido uniforme -1..1 rngState ^= rngState << 13; rngState ^= rngState >> 17; rngState ^= rngState << 5; return (float)(rngState & 0xFFFF) / 32768.0f - 1.0f; } static void tAdd(uint8_t v, int n) { while (n-- > 0 && tN < (int)sizeof(tUnits)) tUnits[tN++] = v; } static void tAddRttyCode(uint8_t code) { // unidade = meio bit tAdd(0, 2); // start for (int b = 0; b < 5; b++) tAdd((code >> b) & 1, 2); tAdd(1, 3); // stop de 1,5 bit } static void tAddAscii(uint8_t c, int stopUnits) { // unidade = meio bit; 7 bits de dados, LSB primeiro tAdd(0, 2); for (int b = 0; b < 7; b++) tAdd((c >> b) & 1, 2); tAdd(1, stopUnits); } // ---- teste SSTV: cabecalho VIS + imagem de teste colorida no modo escolhido (AUTO = Martin M1) ---- static uint32_t ssTn = 0, ssTTotal = 1; static float ssTph = 0.0f; static inline int ssTestIdx() { return ssAuto ? 0 : ssSel; } // Padrao de teste (xf = 0..1 na horizontal, yf = 0..1 na vertical): // 0-40%: barras 100% (branco, amarelo, ciano, verde, magenta, vermelho, azul, preto) // 40-60%: tons naturais (pele, ceu, grama, marrom, rosa, oliva, cinza, azul escuro) // 60-80%: varredura de matiz com saturacao media 80-100%: rampa de cinza static void ssTestRGB(float xf, float yf, uint8_t* o) { static const uint8_t BAR[8][3] = { {255,255,255}, {255,255,0}, {0,255,255}, {0,255,0}, {255,0,255}, {255,0,0}, {0,0,255}, {0,0,0} }; static const uint8_t NAT[8][3] = { {224,172,148}, {120,160,220}, {90,140,60}, {120,80,50}, {240,170,190}, {200,190,90}, {150,150,150}, {60,60,90} }; int k = (int)(xf * 8.0f); if (k > 7) k = 7; if (yf < 0.40f) { o[0] = BAR[k][0]; o[1] = BAR[k][1]; o[2] = BAR[k][2]; } else if (yf < 0.60f) { o[0] = NAT[k][0]; o[1] = NAT[k][1]; o[2] = NAT[k][2]; } else if (yf < 0.80f) { float h = xf * 6.0f; int i = (int)h; float f = h - (float)i; if (i > 5) { i = 5; f = 1.0f; } const float v = 0.85f, s = 0.6f; const float p = v * (1.0f - s), q = v * (1.0f - s * f), t = v * (1.0f - s * (1.0f - f)); float r, g, b; switch (i) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; default: r = v; g = p; b = q; break; } o[0] = (uint8_t)(r * 255.0f); o[1] = (uint8_t)(g * 255.0f); o[2] = (uint8_t)(b * 255.0f); } else { const uint8_t gy = (uint8_t)(xf * 255.0f); o[0] = o[1] = o[2] = gy; } } // ch: 0 = R, 1 = G, 2 = B, 3 = luminancia Y static inline float ssTonePix(float xf, int y, int ch) { uint8_t c[3]; ssTestRGB(xf, (float)y / (float)SSD[ssTestIdx()].lines, c); float v; if (ch == 3) v = 0.299f * c[0] + 0.587f * c[1] + 0.114f * c[2]; else v = (float)c[ch]; return 1500.0f + v * (800.0f / 255.0f); } // Robot: ch 0 = Y, 1 = Cr (R-Y), 2 = Cb (B-Y), codificados como no padrao (BT.601, faixa de estudio) static inline float ssToneYUV(float xf, int y, int ch) { uint8_t c[3]; ssTestRGB(xf, (float)y / (float)SSD[ssTestIdx()].lines, c); const float R = c[0] / 255.0f, G = c[1] / 255.0f, B = c[2] / 255.0f; const float Y = 0.299f * R + 0.587f * G + 0.114f * B; float v = (ch == 0) ? 16.0f + 219.0f * Y : (ch == 1) ? 128.0f + 159.8f * (R - Y) : 128.0f + 126.4f * (B - Y); v = v < 0 ? 0 : (v > 255 ? 255 : v); return 1500.0f + v * (800.0f / 255.0f); } static float ssTestFreq(uint32_t n) { const SsDef& d = SSD[ssTestIdx()]; const uint32_t HDR = 7280; // 0,910 s de cabecalho if (n < HDR) { if (n < 2400) return 1900.0f; if (n < 2480) return 1200.0f; if (n < 4880) return 1900.0f; if (n < 5120) return 1200.0f; // bit de partida uint32_t b = (n - 5120) / 240; if (b < 8) { int ones = 0; for (int k = 0; k < 7; k++) ones += (d.vis >> k) & 1; int bit = (b < 7) ? ((d.vis >> b) & 1) : (ones & 1); return bit ? 1100.0f : 1300.0f; } return 1200.0f; // bit de parada } const float m = (float)(n - HDR); const float LT = d.lt * SR; int l; float tl; switch (d.kind) { case SK_MARTIN: { l = (int)(m / LT); tl = (m - l * LT) / SR; if (tl < d.sync) return 1200.0f; tl -= d.sync; if (tl < d.porch) return 1500.0f; tl -= d.porch; int k = (int)(tl / (d.scan + d.sep)); if (k > 2) return 1500.0f; float t2 = tl - k * (d.scan + d.sep); if (t2 >= d.scan) return 1500.0f; return ssTonePix(t2 / d.scan, l, k == 0 ? 1 : (k == 1 ? 2 : 0)); // G, B, R } case SK_SCOTTIE: { if (m < 72.0f) return 1200.0f; // pulso inicial da imagem (9 ms) float m2 = m - 72.0f; l = (int)(m2 / LT); tl = (m2 - l * LT) / SR; if (tl < d.sep) return 1500.0f; tl -= d.sep; if (tl < d.scan) return ssTonePix(tl / d.scan, l, 1); // G tl -= d.scan; if (tl < d.sep) return 1500.0f; tl -= d.sep; if (tl < d.scan) return ssTonePix(tl / d.scan, l, 2); // B tl -= d.scan; if (tl < d.sync) return 1200.0f; tl -= d.sync; if (tl < d.porch) return 1500.0f; tl -= d.porch; if (tl < d.scan) return ssTonePix(tl / d.scan, l, 0); // R return 1500.0f; } case SK_ROBOT: { l = (int)(m / LT); tl = (m - l * LT) / SR; if (tl < d.sync) return 1200.0f; tl -= d.sync; if (tl < d.porch) return 1500.0f; tl -= d.porch; if (tl < d.scan) return ssToneYUV(tl / d.scan, l, 0); // Y tl -= d.scan; if (d.lt < 0.2f) { // Robot 36 if (tl < d.sep) return (l & 1) ? 2300.0f : 1500.0f; tl -= d.sep; if (tl < 0.0015f) return 1900.0f; tl -= 0.0015f; if (tl < d.scan * 0.5f) return ssToneYUV(tl / (d.scan * 0.5f), l, (l & 1) ? 2 : 1); return 1500.0f; } if (tl < d.sep) return 1500.0f; // Robot 72: Cr e depois Cb tl -= d.sep; if (tl < 0.0015f) return 1900.0f; tl -= 0.0015f; if (tl < d.scan * 0.5f) return ssToneYUV(tl / (d.scan * 0.5f), l, 1); tl -= d.scan * 0.5f; if (tl < d.sep) return 2300.0f; tl -= d.sep; if (tl < 0.0015f) return 1900.0f; tl -= 0.0015f; if (tl < d.scan * 0.5f) return ssToneYUV(tl / (d.scan * 0.5f), l, 2); return 1500.0f; } case SK_PD: { l = (int)(m / LT); tl = (m - l * LT) / SR; if (tl < d.sync) return 1200.0f; tl -= d.sync; if (tl < d.porch) return 1500.0f; tl -= d.porch; int k = (int)(tl / d.scan); if (k > 3) return 1500.0f; float t2 = tl - k * d.scan; if (k == 0) return ssToneYUV(t2 / d.scan, 2 * l, 0); // Y (linha par) if (k == 1) return ssToneYUV(t2 / d.scan, 2 * l, 1); // R-Y if (k == 2) return ssToneYUV(t2 / d.scan, 2 * l, 2); // B-Y return ssToneYUV(t2 / d.scan, 2 * l + 1, 0); // Y (linha impar) } default: return 1500.0f; } } static void ssTestInit() { const SsDef& d = SSD[ssTestIdx()]; float frames = (d.kind == SK_PD) ? d.lines / 2.0f : (float)d.lines; ssTTotal = 7280u + (uint32_t)(frames * d.lt * SR) + 16000u; // repete depois de 2 s de silencio de tom (1900 Hz) ssTn = 0; ssTph = 0.0f; } static float sstvTestSample() { float f = (ssTn < ssTTotal - 16000u) ? ssTestFreq(ssTn) : 1900.0f; if (++ssTn >= ssTTotal) ssTn = 0; ssTph += TWO_PI_F * f / SR; if (ssTph > TWO_PI_F) ssTph -= TWO_PI_F; return testAmp * sinf(ssTph) + testNoise * rndf(); } static void testStart() { tN = 0; tPos = 0; tUnitCnt = 0; tPh = 0; tGate = 0; tSymPos = 0; tSign = tPrevSign = 1.0f; tRev = false; const uint8_t type = MODES[currentModeIdx].type; if (type == T_RTTY) { const char* txt = "RYRYRY THE QUICK BROWN FOX 0123456789\r\n"; bool figs = false; tAdd(1, 40); tAddRttyCode(0x1F); // LTRS: garante o estado inicial em cada volta for (const char* p = txt; *p; p++) { char c = *p; int cl = -1, cf = -1; for (int k = 1; k < 32; k++) { if (ITA2_LETTERS[k] == c && cl < 0) cl = k; if (ITA2_FIGURES[k] == c && cf < 0) cf = k; } uint8_t code; if (cl >= 0 && cf >= 0) code = (uint8_t)cl; else if (cl >= 0) { if (figs) { tAddRttyCode(0x1F); figs = false; } code = (uint8_t)cl; } else if (cf >= 0) { if (!figs) { tAddRttyCode(0x1B); figs = true; } code = (uint8_t)cf; } else continue; tAddRttyCode(code); if (code == 4) figs = false; // USOS } tAdd(1, 120); tUnitLen = SR / (2.0f * MODES[currentModeIdx].baud); } else if (type == T_ASCII) { const char* txt = "\r\nASCII TESTE 0123456789 THE QUICK BROWN FOX\r\n"; const int stopUnits = (MODES[currentModeIdx].baud < 150.0f) ? 4 : 2; // 110 baud: 2 stops; 300 baud: 1 stop tAdd(1, 40); for (const char* p = txt; *p; p++) tAddAscii((uint8_t)*p, stopUnits); tAdd(1, 120); tUnitLen = SR / (2.0f * MODES[currentModeIdx].baud); } else if (type == T_PSK) { const char* txt = "CQ CQ CQ DE TESTE PSK31 "; tFreq = pskCenterFreq + 8.0f; tAdd(0, 64); for (const char* p = txt; *p; p++) { const char* code = VARICODE_CODES[(uint8_t)*p & 127]; for (; *code; code++) tAdd(*code == '1' ? 1 : 0, 1); tAdd(0, 2); } tAdd(0, 24); } else if (type == T_SSTV) { ssTestInit(); tN = 1; // testSample() so roda com tN > 0 } else { const char* txt = "CQ CQ DE TESTE"; tFreq = cwAutoTune ? 800.0f : cwFreq; tAdd(0, 14); for (const char* p = txt; *p; p++) { if (*p == ' ') { tAdd(0, 4); continue; } const char* pat = morseEncode(*p); if (!pat) continue; for (; *pat; pat++) { tAdd(1, *pat == '.' ? 1 : 3); tAdd(0, 1); } tAdd(0, 2); } tAdd(0, 14); tUnitLen = 9600.0f / 15.0f; // 15 WPM } } static float testSample() { const uint8_t type = MODES[currentModeIdx].type; float v = 0.0f; if (tN <= 0) return 0.0f; if (type == T_SSTV) return sstvTestSample(); if (type == T_RTTY || type == T_ASCII) { if (--tUnitCnt <= 0.0f) { tPos = (tPos + 1) % tN; tUnitCnt += tUnitLen; } float f = tUnits[tPos] ? MARK_FREQ : SPACE_FREQ; tPh += TWO_PI_F * f / SR; if (tPh > TWO_PI_F) tPh -= TWO_PI_F; v = testAmp * sinf(tPh); } else if (type == T_PSK) { if (tSymPos == 0) { tPos = (tPos + 1) % tN; tPrevSign = tSign; tRev = (tUnits[tPos] == 0); if (tRev) tSign = -tSign; } float env = tRev ? tPrevSign * cosf(PI_F * (float)tSymPos / 256.0f) : tPrevSign; tSymPos = (tSymPos + 1) & 255; float f = tFreq; tPh += TWO_PI_F * f / SR; if (tPh > TWO_PI_F) tPh -= TWO_PI_F; v = testAmp * env * sinf(tPh); } else { if (--tUnitCnt <= 0.0f) { tPos = (tPos + 1) % tN; tUnitCnt += tUnitLen; } tGate += 0.01f * ((tUnits[tPos] ? 1.0f : 0.0f) - tGate); float f = tFreq; tPh += TWO_PI_F * f / SR; if (tPh > TWO_PI_F) tPh -= TWO_PI_F; v = testAmp * tGate * sinf(tPh); } return v + testNoise * rndf(); } // ================== DESPACHO GERAL ================== static int renormCnt = 0; static void renormAll() { toneMark.renorm(); toneSpace.renorm(); toneNoiseA.renorm(); toneNoiseB.renorm(); cwTone.renorm(); cwRefA.renorm(); cwRefB.renorm(); pskMain.t.renorm(); pskNoiseA.t.renorm(); pskNoiseB.t.renorm(); } static void applyMode(uint8_t idx) { currentModeIdx = idx % N_MODES; const ModeDef& m = MODES[currentModeIdx]; figsMode = false; rxState = RX_IDLE; prevMark = true; ledState = 255; scanReset(); if (m.type == T_RTTY) rttyConfigure(); else if (m.type == T_ASCII) { rttyConfigure(); asciiConfigure(); } else if (m.type == T_PSK) { pskMain.t.setFreq(pskCenterFreq); pskNoiseA.t.setFreq(pskCenterFreq + 250.0f); pskNoiseB.t.setFreq(pskCenterFreq - 250.0f); setPskFreq(pskCenterFreq); } else if (m.type == T_SSTV) ssSetup(); else resetCwDecoder(); setSampleClock(125); // 8 kHz if (testActive) testStart(); modeEpoch++; uiDirty = true; } static void dspSample(float s) { if (++renormCnt >= 1024) { renormCnt = 0; renormAll(); } const uint8_t type = MODES[currentModeIdx].type; if (type == T_RTTY) { processRTTYSample(s); return; } if (type == T_ASCII) { processASCIISample(s); return; } if (type == T_SSTV) { processSSTVSample(s); return; } scanFeed(s); if (type == T_PSK) processPSK31Sample(s); else processCWSample(s); } static float dcEst = 2048.0f; static uint32_t dcCnt = 0; static void dspAdcSample(int raw) { float x; if (testActive) { x = testSample(); } else { float a = (dcCnt < 800) ? 0.02f : 0.001f; dcCnt++; dcEst += a * ((float)raw - dcEst); float d = (float)raw - dcEst; float ad = fabsf(d); adcPk = fmaxf(ad, adcPk * 0.9995f); x = d * (inputGain / 2048.0f); } dspSample(x); } static void dspCommand(uint8_t id, float v) { const uint8_t type = MODES[currentModeIdx].type; switch (id) { case CMD_MODE: applyMode((uint8_t)v); break; case CMD_NEXTMODE: applyMode((uint8_t)(currentModeIdx + 1)); break; case CMD_TEST: testActive = !testActive; if (testActive) testStart(); ledState = 255; uiDirty = true; break; case CMD_FREQ: if (type == T_PSK) { pskAutoTune = false; setPskFreq(v); } else if (type == T_CW) { cwAutoTune = false; setCwFreq(v); } break; case CMD_WPM: if (v <= 0.0f) setCwAuto(true); else { cwAuto = false; cwWpm = (int)clampf(v, 5.0f, 60.0f); cwResetTiming(); } break; case CMD_AUTOTUNE: { bool b = !(cwAutoTune && pskAutoTune); cwAutoTune = pskAutoTune = b; } break; case CMD_BW: cwSetBw((uint8_t)((cwBwIdx + 1) % 3)); cwConfigRefs(); break; } } static void dspInit() { buildPskFir(); scanInit(); ssInitHilbert(); pskFreq = pskCenterFreq; cwSetBw(cwBwIdx); applyMode(0); } // ==== DSP END ====================================================================== // ======================= TEXTO NA TELA (fila entre o DSP e o video) ======================= #define COLS 52 #define ROWS 14 static char tbuf[ROWS][COLS + 1]; static int tcol = 0, trow = 0; static bool crSeen = false; static volatile uint8_t uiLed = 0; #define RB_SIZE 512 static char rbuf[RB_SIZE]; static volatile uint16_t rbHead = 0, rbTail = 0; void outChar(char c) { uint16_t n = (uint16_t)((rbHead + 1) & (RB_SIZE - 1)); if (n == rbTail) return; // fila cheia: descarta rbuf[rbHead] = c; __sync_synchronize(); rbHead = n; } void outStr(const char* s) { while (*s) outChar(*s++); } void rxStatus(uint8_t st) { uiLed = st; } static void uiClear() { for (int r = 0; r < ROWS; r++) { memset(tbuf[r], ' ', COLS); tbuf[r][COLS] = 0; } tcol = 0; trow = 0; crSeen = false; } static void uiNewline() { tcol = 0; if (++trow >= ROWS) { for (int r = 1; r < ROWS; r++) memcpy(tbuf[r - 1], tbuf[r], COLS + 1); memset(tbuf[ROWS - 1], ' ', COLS); tbuf[ROWS - 1][COLS] = 0; trow = ROWS - 1; } } static void uiPut(char c) { if (c == '\r') { if (!crSeen) uiNewline(); crSeen = true; return; } if (c == '\n') { if (!crSeen) uiNewline(); crSeen = false; return; } crSeen = false; if (c == '\b') { if (tcol > 0) { tcol--; tbuf[trow][tcol] = ' '; } return; } if (c < 32 || c > 126) return; if (tcol >= COLS) uiNewline(); tbuf[trow][tcol++] = c; } static void uiDrain() { while (rbTail != rbHead) { char c = rbuf[rbTail]; __sync_synchronize(); rbTail = (uint16_t)((rbTail + 1) & (RB_SIZE - 1)); uiPut(c); } } // ======================= VIDEO ======================= // A classe CompositeGraphics ja tem dois buffers internos (frame e backbuffer): // begin()/desenho/end() acontecem no backbuffer e end() troca os dois. Nao precisa de buffer extra. CompositeGraphics graphics(CompositeColorOutput::XRES, CompositeColorOutput::YRES); CompositeColorOutput composite(CompositeColorOutput::NTSC); // troque por ::PAL se a sua TV for PAL-M/PAL Font font(6, 8, font6x8::pixels); #if VIDEO_MONO static const int COL_TXT = 0x0C, COL_HI = 0x0F, COL_DIM = 0x06, COL_AMB = 0x09, COL_RED = 0x0F; #else static const int COL_TXT = 0xDC, COL_HI = 0xDF, COL_DIM = 0xD6, COL_AMB = 0xFC, COL_RED = 0x4A; #endif static const int X0 = 12, X1 = 324, Y0 = 24, ROWH = 9; static char sbuf[40]; static void txt(int x, int y, const char* s, int col) { graphics.setTextColor(col); graphics.setCursor(x, y); graphics.print(s); } static void drawBar(int x, int y, int w, int h, float frac, int col) { graphics.rect(x, y, w, h, COL_DIM); int fw = (int)(clampf(frac, 0.0f, 1.0f) * (w - 2)); if (fw > 0) graphics.fillRect(x + 1, y + 1, fw, h - 2, col); } static void drawPanelRTTY() { float ref = fmaxf(fmaxf(levelMark, levelSpace), 1e-6f); txt(X0, 158, "M", COL_TXT); drawBar(X0 + 12, 157, 140, 10, dispMark / ref, COL_HI); txt(X0, 172, "S", COL_TXT); drawBar(X0 + 12, 171, 140, 10, dispSpace / ref, COL_HI); txt(X0, 186, "R", COL_TXT); drawBar(X0 + 12, 185, 140, 10, dispNoise / ref, COL_AMB); txt(X0, 202, rttyBit ? "BIT: MARK " : "BIT: SPACE", COL_HI); } static void drawPanelPSK() { const int cx = X0 + 29, cy = 156 + 29; graphics.rect(X0, 156, 58, 58, COL_DIM); graphics.xLine(X0 + 1, X0 + 57, cy, COL_DIM); graphics.line(cx, 157, cx, 213, COL_DIM); uint16_t w = scW; for (int i = 1; i <= 48; i++) { uint16_t k = (uint16_t)((w - i) & (SCOPE_N - 1)); int px = cx + ((int)scX[k] * 26) / 100; int py = cy - ((int)scY[k] * 26) / 100; uint8_t f = scF[k]; if (f == 0) { graphics.dot(px, py, COL_DIM); } else { graphics.dot(px, py, COL_HI); if (f == 2) { graphics.dot(px + 1, py, COL_HI); graphics.dot(px, py + 1, COL_HI); } } } float snrdb = 10.0f * log10f((pskSigPow + 1e-12f) / (pskNoisePow + 1e-12f)); txt(X0 + 66, 158, "SNR", COL_TXT); drawBar(X0 + 92, 157, 68, 10, snrdb / 30.0f, pskOpen ? COL_HI : COL_AMB); txt(X0 + 66, 172, "SYN", COL_TXT); drawBar(X0 + 92, 171, 68, 10, pskLock / 10.0f, COL_HI); txt(X0 + 66, 186, pskOpen ? "DECODIFICANDO" : "SQUELCH FECHADO", pskOpen ? COL_HI : COL_AMB); } static void drawPanelCW() { const int base = 214, hmax = 56; graphics.xLine(X0, X0 + CW_TRACE_N, base, COL_DIM); uint8_t w = cwTraceW; for (int i = 0; i < CW_TRACE_N; i++) { int k = (w + i) % CW_TRACE_N; int h = ((int)cwTrace[k] * hmax) / 255; if (h > 0) graphics.line(X0 + i, base - 1, X0 + i, base - h, COL_HI); int th = ((int)cwTraceThr[k] * hmax) / 255; graphics.dot(X0 + i, base - th, COL_AMB); } } static void drawInfo() { const int x = 184; const uint8_t type = MODES[currentModeIdx].type; const int gp = (int)(adcPk * 100.0f / 2048.0f + 0.5f); const char* l[7]; static char b[7][26]; for (int i = 0; i < 7; i++) { b[i][0] = 0; l[i] = b[i]; } if (type == T_RTTY) { snprintf(b[0], 26, "BAUD %.2f", MODES[currentModeIdx].baud); snprintf(b[1], 26, "MARK %d HZ", (int)MARK_FREQ); snprintf(b[2], 26, "SPACE %d HZ", (int)SPACE_FREQ); snprintf(b[3], 26, "SQL %.2f REV %s", squelchPower, reverseMS ? "SIM" : "NAO"); snprintf(b[4], 26, "GAIN %.1f", inputGain); snprintf(b[5], 26, "CODIGO %s", figsMode ? "FIGS" : "LTRS"); } else if (type == T_ASCII) { snprintf(b[0], 26, "BAUD %d", (int)(MODES[currentModeIdx].baud + 0.5f)); snprintf(b[1], 26, "MARK %d HZ", (int)MARK_FREQ); snprintf(b[2], 26, "SPACE %d HZ", (int)SPACE_FREQ); snprintf(b[3], 26, "SQL %.2f REV %s", squelchPower, reverseMS ? "SIM" : "NAO"); snprintf(b[4], 26, "GAIN %.1f", inputGain); snprintf(b[5], 26, "QUADRO %d BITS AUTO", (int)asciiPick); } else if (type == T_PSK) { snprintf(b[0], 26, "FREQ %d HZ", (int)(pskFreq + 0.5f)); snprintf(b[1], 26, "DESVIO %+d HZ", (int)roundf(pskFreq - pskCenterFreq)); snprintf(b[2], 26, "SINTONIA %s", pskAutoTune ? "AUTO" : "FIXA"); snprintf(b[3], 26, "SQL %.2f REV %s", squelchPower, reverseMS ? "SIM" : "NAO"); snprintf(b[4], 26, "GAIN %.1f", inputGain); snprintf(b[5], 26, "AFC %s", pskAfc ? "LIGADO" : "DESLIG"); } else { snprintf(b[0], 26, "TOM %d HZ", (int)(cwFreq + 0.5f)); snprintf(b[1], 26, "VEL %d WPM %s", cwWpmDisp, cwAuto ? "AUTO" : "FIXA"); snprintf(b[2], 26, "BANDA %d HZ", (int)CW_BWS[cwBwIdx]); snprintf(b[3], 26, "SQL %.2f", squelchPower); snprintf(b[4], 26, "GAIN %.1f", inputGain); snprintf(b[5], 26, "SINTONIA %s", cwAutoTune ? "AUTO" : "FIXA"); } snprintf(b[6], 26, "ADC %d%% OVR %u", gp, (unsigned)overruns); for (int i = 0; i < 7; i++) { int col = COL_TXT; if (i == 6 && gp > 90) col = COL_RED; txt(x, 157 + i * 9, l[i], col); } } #if !SS_TRUECOLOR // ---- Conversao simples (matiz + brilho): so e usada se faltar memoria para a tabela de cores ---- static uint8_t ssRgbToPalette(uint8_t r, uint8_t g, uint8_t b) { const int mx = max((int)r, max((int)g, (int)b)); const int mn = min((int)r, min((int)g, (int)b)); const int lum = (r * 30 + g * 59 + b * 11) / 100; int hue = 0; if (mx != mn && mx - mn >= 18) { float h; if (mx == r) h = 60.0f * ((float)g - b) / (mx - mn); else if (mx == g) h = 120.0f + 60.0f * ((float)b - r) / (mx - mn); else h = 240.0f + 60.0f * ((float)r - g) / (mx - mn); if (h < 0) h += 360.0f; hue = ((int)(h / 22.5f + 0.5f) + 2) & 15; } int l = lum >> 4; if (l > 15) l = 15; return (uint8_t)((hue << 4) | l); } #endif // ---- SSTV em tela cheia: usa lookup pre-calculado para nao pesar o DSP/Serial ---- static volatile uint32_t ssModeT0 = 0; // instante em que o modo atual foi selecionado (loop) static volatile int ssVShift = 0; // padrao SSTV: v0 (sem deslocamento vertical; serial: v N) static volatile int ssVMargin = 14; // padrao SSTV: z14 = linhas pretas no topo e na base (overscan da TV); a imagem e reduzida para caber (serial: z N) // deslocamento vertical circular da imagem SSTV, em linhas (serial: v N) #if SS_TRUECOLOR // Mapa linha da tela -> linha da imagem (mesma logica do desenho por paleta: deslocamento circular v e margem z). // O proprio ISR de video le a imagem ssTc; aqui so se atualiza a tabela de linhas. static void ssTcApply() { const int vh = (int)CompositeColorOutput::YRES; int sh = ssVShift % vh; if (sh < 0) sh += vh; int m = ssVMargin; if (m < 0) m = 0; if (m > 60) m = 60; const int ih = vh - 2 * m; for (int y = 0; y < vh; ++y) { int r = y + sh; if (r >= vh) r -= vh; RawCompositeVideoBlitter::_tc_rowmap[y] = (r < m || r >= vh - m) ? 0xFF : (uint8_t)(((r - m) * SS_H) / ih); } } #else static uint8_t ssXmap[CompositeColorOutput::XRES]; // coluna da tela -> coluna da imagem 160x128 (calculada uma vez) static uint8_t ssLine[CompositeColorOutput::XRES]; // linha de tela ja resolvida (par A/B aplicado) static bool ssXmapOk = false; static void drawSSTV() { const int vw = CompositeColorOutput::XRES; const int vh = CompositeColorOutput::YRES; if (!ssXmapOk) { for (int x = 0; x < vw; ++x) ssXmap[x] = (uint8_t)((x * SS_W) / vw); ssXmapOk = true; } int sh = ssVShift % vh; if (sh < 0) sh += vh; int m = ssVMargin; if (m < 0) m = 0; if (m > 60) m = 60; const int ih = vh - 2 * m; // altura visivel da imagem int lastSy = -2; // Cada linha da imagem 160x128 cobre 1 ou 2 linhas da tela: resolve a linha UMA vez e reaproveita // (menos trabalho por quadro = menos disputa com o video, que e o que deforma/rola o quadro). for (int y = 0; y < vh; ++y) { int r = y + sh; if (r >= vh) r -= vh; // linha de imagem mostrada na linha y (circular) const int sy = (r < m || r >= vh - m) ? -1 : ((r - m) * SS_H) / ih; // -1 = barra preta if (sy != lastSy) { lastSy = sy; const int src = sy * SS_W; if (sy < 0) memset(ssLine, 0, sizeof(ssLine)); else for (int x = 0; x < vw; ++x) { const int i = src + ssXmap[x]; const uint8_t a = ssPalette[i]; ssLine[x] = (x & 1) ? ssMixPartner(a, ssImgVar(i)) : a; } } for (int x = 0; x < vw; ++x) graphics.dot(x, y, ssLine[x]); } } #endif static void draw() { const ModeDef& m = MODES[currentModeIdx]; #if SS_TRUECOLOR if (m.type == T_SSTV) { // A imagem true color e desenhada pelo proprio ISR de video; aqui so o nome do modo (1 s), em fundo preto. if ((uint32_t)(millis() - ssModeT0) < 1000u) { graphics.begin(0); const int w = (int)strlen(m.name) * 6 + 6; graphics.fillRect(2, 2, w, 12, 0); txt(5, 14, m.name, COL_HI);//coluna 5 hi 4 graphics.end(); } return; // depois: nada a desenhar (menos carga de CPU) } #endif graphics.begin(0); txt(X0, 10, m.name, COL_HI); const uint8_t led = uiLed; const char* st = led == 1 ? "SINAL" : (led == 2 ? "SQUELCH" : "---"); int stc = led == 1 ? COL_HI : (led == 2 ? COL_AMB : COL_DIM); txt(X1 - 12 - 7 * 6, 10, st, stc); graphics.fillRect(X1 - 8, 11, 6, 6, stc); graphics.xLine(X0, X1, 20, COL_DIM); if (m.type == T_SSTV) { #if !SS_TRUECOLOR drawSSTV(); if ((uint32_t)(millis() - ssModeT0) < 1000u) { // nome do modo no canto superior esquerdo por 1 s const int w = (int)strlen(m.name) * 6 + 6; graphics.fillRect(2, 2, w, 12, 0); txt(5, 4, m.name, COL_HI); } graphics.end(); return; #endif } for (int r = 0; r < ROWS; r++) txt(X0, Y0 + r * ROWH, tbuf[r], COL_TXT); if ((millis() / 400) & 1) graphics.fillRect(X0 + tcol * 6, Y0 + trow * ROWH, 5, 8, COL_TXT); graphics.xLine(X0, X1, 152, COL_DIM); if (m.type == T_RTTY || m.type == T_ASCII) drawPanelRTTY(); else if (m.type == T_PSK) drawPanelPSK(); else drawPanelCW(); drawInfo(); graphics.xLine(X0, X1, 218, COL_DIM); if (testActive) { if ((millis() / 500) & 1) txt(X0, 222, "*** SINAL DE TESTE INTERNO ***", COL_AMB); } else txt(X0, 222, "CURTO=MODO LONGO=TESTE", COL_DIM); graphics.end(); } // ======================= TAREFAS (DSP no nucleo 0, video no nucleo 1) ======================= struct DspCmd { uint8_t id; float val; }; static QueueHandle_t cmdQ = NULL; static TaskHandle_t dspTaskHandle = NULL; static hw_timer_t* sampleTimer = NULL; static void sendCmd(uint8_t id, float v) { if (!cmdQ) return; DspCmd c; c.id = id; c.val = v; xQueueSend(cmdQ, &c, 0); } static void IRAM_ATTR onSampleTimer() { BaseType_t hp = pdFALSE; vTaskNotifyGiveFromISR(dspTaskHandle, &hp); #if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3 portYIELD_FROM_ISR(hp); #else if (hp) portYIELD_FROM_ISR(); #endif } static void startSampleTimer() { #if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3 sampleTimer = timerBegin(1000000); // 1 MHz timerAttachInterrupt(sampleTimer, &onSampleTimer); timerAlarm(sampleTimer, timerPeriodUs, true, 0); // 125 us = 8 kHz #else sampleTimer = timerBegin(0, 80, true); // 80 MHz / 80 = 1 MHz timerAttachInterrupt(sampleTimer, &onSampleTimer, true); timerAlarmWrite(sampleTimer, timerPeriodUs, true); timerAlarmEnable(sampleTimer); #endif } // troca a taxa de amostragem (125 us = 8 kHz); chamada pela tarefa do DSP static void setSampleClock(uint32_t us) { if (us == timerPeriodUs) return; timerPeriodUs = us; if (!sampleTimer) return; #if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3 timerAlarm(sampleTimer, us, true, 0); #else timerAlarmWrite(sampleTimer, us, true); #endif timerWrite(sampleTimer, 0); } static void dspTask(void*) { dspTaskHandle = xTaskGetCurrentTaskHandle(); #if TIMER_NUCLEO0 startSampleTimer(); // a interrupcao de 8 kHz fica no nucleo 0 #endif uint32_t n = 0; for (;;) { uint32_t cnt = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if (cnt > 1) overruns = overruns + (cnt - 1); int raw = testActive ? 0 : analogRead(PIN_AUDIO); dspAdcSample(raw); if ((++n & 63) == 0) { DspCmd c; while (xQueueReceive(cmdQ, &c, 0) == pdTRUE) dspCommand(c.id, c.val); } } } // Botao (GPIO0): lido pelo loop() (nucleo 1), no mesmo caminho que o serial, que funciona. Antes era uma tarefa de // baixa prioridade no nucleo 0, que fica sem tempo quando a tarefa do DSP (prioridade maior, 8 kHz) o ocupa por inteiro. static void btnPoll() { static bool down = false, longDone = false; static uint32_t t0 = 0; const bool p = (digitalRead(PIN_MODE) == LOW); const uint32_t now = millis(); if (p && !down) { down = true; t0 = now; longDone = false; } else if (p && down && !longDone && now - t0 >= 800) { longDone = true; Serial.println(F("botao: toque longo -> teste")); sendCmd(CMD_TEST, 0); } else if (!p && down) { down = false; if (!longDone) { Serial.println(F("botao: toque curto -> proximo modo")); sendCmd(CMD_NEXTMODE, 0); } } } // ======================= COMANDOS PELO SERIAL ======================= static void serialHelp() { Serial.println(F("Comandos: n=proximo modo, m N=modo 0..16, t=teste, r=inverte,, f HZ=frequencia fixa,")); Serial.println(F(" u=sintonia auto liga/desliga, w N=WPM fixo (0=auto), b=banda CW, g X=ganho, s X=squelch,")); Serial.println(F(" SSTV cor: c X=saturacao, h X=fase do matiz (graus), j X=passo entre matizes, y X=gama, d=difusao de erro,")); Serial.println(F(" SSTV nitidez: k X=forca da mascara (0=off, padrao 0.4), i=diagnostico do ISR de video, p=cores mistas (so SS_TRUECOLOR 0), v N=desloca a imagem na vertical (circular, padrao 0), z N=margem preta topo/base em linhas (padrao 14), x N=margem preta lateral em ciclos por lado (padrao 0; so true color),")); } static void handleLine(char* line) { char c = line[0]; if (c >= 'A' && c <= 'Z') c = (char)(c + 32); float v = atof(line + 1); switch (c) { case '?': serialHelp(); break; case 'n': sendCmd(CMD_NEXTMODE, 0); break; case 'm': sendCmd(CMD_MODE, v); break; case 't': sendCmd(CMD_TEST, 0); break; case 'r': reverseMS = !reverseMS; Serial.println(reverseMS ? F("inversao: SIM") : F("inversao: NAO")); break; case 'f': sendCmd(CMD_FREQ, v); break; case 'u': sendCmd(CMD_AUTOTUNE, 0); break; case 'w': sendCmd(CMD_WPM, v); break; case 'b': sendCmd(CMD_BW, 0); break; case 'g': if (v > 0.05f && v < 200.0f) { inputGain = v; Serial.printf("ganho: %.2f\n", v); } break; case 's': if (v >= 0.0f && v <= 2.0f) { squelchPower = v; Serial.printf("squelch: %.2f\n", v); } break; #if SS_TRUECOLOR case 'c': if (v >= 0.0f && v <= 3.0f) { ssTcGain = v; ssTcCalc(); Serial.printf("SSTV ganho de croma: %.2f (1.00 = NTSC padrao)\n", v); } break; case 'h': if (v >= -360.0f && v <= 360.0f) { ssTcHue = v; ssTcCalc(); Serial.printf("SSTV giro de matiz: %.1f graus\n", v); } break; case 'y': if (v >= 0.3f && v <= 3.0f) { ssTcGamma = v; Serial.printf("SSTV gama de luminancia: %.2f\n", v); } break; #else case 'c': if (v >= 0.05f && v <= 0.90f) { ssPalSat = v; ssPalRebuild(); Serial.printf("SSTV saturacao: %.2f\n", v); } break; case 'h': if (v >= -360.0f && v <= 360.0f) { ssPalPhase = v; ssPalRebuild(); Serial.printf("SSTV fase do matiz: %.1f graus\n", v); } break; case 'j': if (v >= -40.0f && v <= 40.0f && fabsf(v) > 1.0f) { ssPalStep = v; ssPalRebuild(); Serial.printf("SSTV passo de matiz: %.1f graus\n", v); } break; case 'y': if (v >= 0.3f && v <= 3.0f) { ssPalGamma = v; ssPalRebuild(); Serial.printf("SSTV gama: %.2f\n", v); } break; #endif #if SS_TRUECOLOR case 'x': if (v >= 0.0f && v <= 60.0f) { RawCompositeVideoBlitter::tc_set_xmargin((int)v); Serial.printf("SSTV margem preta lateral: %d ciclos por lado (1 ciclo = 2 pixels; padrao 0)\n", (int)v); } break; #endif case 'k': if (v >= 0.0f && v <= 3.0f) { ssSharp = v; Serial.printf("SSTV nitidez: %.2f\n", v); } break; case 'p': ssMix = !ssMix; Serial.println(ssMix ? F("SSTV cores mistas (1024) nas areas suaves: SIM") : F("SSTV cores mistas: NAO (so 256 puras)")); break; case 'v': ssVShift = (int)v; Serial.printf("SSTV deslocamento vertical: %d linhas (positivo sobe a imagem; o que sai por cima volta por baixo)\n", (int)v); break; case 'z': if (v >= 0.0f && v <= 60.0f) { ssVMargin = (int)v; Serial.printf("SSTV margem preta topo/base: %d linhas\n", (int)v); } break; #if SS_TRUECOLOR case 'i': { // diagnostico do ISR de video (pontos esticados / quadro deformado) uint32_t calls, late, longrun; float mrun, mgap; RawCompositeVideoBlitter::isr_stats_take(&calls, &mrun, &mgap, &late, &longrun); Serial.printf("ISR de video: %u linhas | duracao max %.1f us | intervalo max %.1f us (nominal 63.6) | intervalos > 70 us: %u | execucoes > 40 us: %u\n", (unsigned)calls, mrun, mgap, (unsigned)late, (unsigned)longrun); Serial.println(F(" (linhas sem dado velho: intervalo max perto de 64 e execucoes > 40 us = 0; contadores zeram a cada leitura)")); { char lst[96]; float mv, mb; RawCompositeVideoBlitter::isr_stats_lines(lst, sizeof(lst), 40.0f, &mv, &mb); Serial.printf(" linhas > 40 us: %s | pior linha visivel (0-239): %.1f us | pior fora da imagem (240+): %.1f us\n", lst, mv, mb); } } break; #endif case 'd': ssDither = !ssDither; Serial.println(ssDither ? F("SSTV difusao de erro: SIM") : F("SSTV difusao de erro: NAO")); break; default: serialHelp(); break; } } static void serialPoll() { static char line[24]; static uint8_t n = 0; while (Serial.available()) { char ch = (char)Serial.read(); if (ch == '\r' || ch == '\n') { if (n) { line[n] = 0; handleLine(line); n = 0; } } else if (n < sizeof(line) - 1) line[n++] = ch; } } // ======================= ARDUINO ======================= static uint8_t modeEpochSeen = 255; void setup() { Serial.begin(115200); pinMode(PIN_MODE, INPUT_PULLUP); analogReadResolution(12); analogSetPinAttenuation(PIN_AUDIO, ADC_11db); composite.init(); // o video aloca os buffers grandes primeiro graphics.init(); graphics.setFont(font); pinMode(PIN_MODE, INPUT_PULLUP); // de novo, depois de iniciar o video (garante o pull-up do GPIO0) #if SS_TRUECOLOR ssTcCalc(); ssImgClear(); RawCompositeVideoBlitter::tc_setup(ssTc, SS_W); ssTcApply(); RawCompositeVideoBlitter::tc_set_xmargin(0); // padrao SSTV: x0 (sem margem preta lateral) if (RawCompositeVideoBlitter::_pal_) Serial.println(F("AVISO: SSTV true color so funciona em NTSC; use SS_TRUECOLOR 0 em PAL")); #else if (!ssPalRebuild()) Serial.println(F("SSTV: sem memoria para a tabela de cores (usando conversao simples)")); #endif dspInit(); uiClear(); cmdQ = xQueueCreate(8, sizeof(DspCmd)); if (xTaskCreatePinnedToCore(dspTask, "dsp", 8192, NULL, 4, &dspTaskHandle, 0) != pdPASS) Serial.println(F("ERRO: sem memoria para a tarefa do DSP (reduza a RAM usada)")); #if !TIMER_NUCLEO0 startSampleTimer(); #endif Serial.printf("Decodificador pronto. Memoria livre: %u bytes. Digite ? para ajuda.\n", (unsigned)ESP.getFreeHeap()); } void loop() { serialPoll(); if (modeEpochSeen != modeEpoch) { modeEpochSeen = modeEpoch; ssModeT0 = millis(); uiClear(); } uiDrain(); #if SS_TRUECOLOR ssTcApply(); // acompanha os comandos v / z RawCompositeVideoBlitter::tc_enable(MODES[currentModeIdx].type == T_SSTV && (uint32_t)(millis() - ssModeT0) >= 1000u); #endif draw(); composite.sendFrameHalfResolution(&graphics.frame); for (uint32_t t = 0; t < FRAME_MS; t += 5) { // cede tempo (menos disputa com a interrupcao de video) btnPoll(); // e le o botao a cada 5 ms const TickType_t tk = pdMS_TO_TICKS(5); vTaskDelay(tk ? tk : 1); } }

Comentários

Postagens mais visitadas deste blog

JVC Compulink, COMPU LINK, JVC, Tape Deck, Arduino, DCS Codes

JVC Compu Link Final

RTTY, PSK31, FT8, JS8CALL, NBTV, RDS FM, OLIVIA, SSTV, SSTV AUTO,CW, ASCII Encoder / Decoder