feat: MEMS microphone software handling for TX15 and TX16SMK3 (#7328)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3djc
2026-06-03 13:31:40 +10:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 13bba22a4d
commit a7d730fded
39 changed files with 1385 additions and 4 deletions
+1
View File
@@ -464,6 +464,7 @@ set(SRC
serial.cpp
audio.cpp
model_audio.cpp
pdm_wav_recorder.cpp
sbus.cpp
input_mapping.cpp
inactivity_timer.cpp
+4
View File
@@ -208,6 +208,10 @@ bool isBacklightEnabled();
int audioInit();
void audioConsumeCurrentBuffer();
#if defined(PDM_CLOCK)
#include "pdm_software_driver.h"
#endif
// Telemetry driver
#define INTMODULE_FIFO_SIZE 512
#define TELEMETRY_FIFO_SIZE 512
@@ -0,0 +1,289 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "board.h"
#include "hal/gpio.h"
#include "stm32_gpio.h"
#include "stm32_dma.h"
#include "stm32_hal_ll.h"
#if defined(PDM_CAPTURE_DMA)
static constexpr uint32_t PDM_SAI_MCKDIV =
PDM_SAI_KER_FREQ / PDM_CLOCK_FREQ;
static_assert(PDM_SAI_MCKDIV >= 1 && PDM_SAI_MCKDIV <= 63,
"PDM_SAI MCKDIV out of range for SAI_xCR1_MCKDIV (6 bits)");
// Burst size: ~6 ms of PDM at 1.6 MHz (multiple of 32 required for word packing).
static constexpr uint32_t PDM_BURST_BITS = 10080;
static constexpr uint32_t PDM_BURST_WORDS = (PDM_BURST_BITS + 31) / 32;
static uint32_t pdmBurstBuf[PDM_BURST_WORDS];
static uint8_t lastSoundLevel = 0;
// 40000 bytes at 1.6 MHz = ~25 ms ring; gives ~21 ms slack for audio-task stalls.
static constexpr uint32_t PDM_RING_BYTES = 40000;
static uint8_t pdmRingBuf[PDM_RING_BYTES] __DMA_NO_CACHE;
static uint32_t pdmRingReadPos = 0;
// CIC integrator + comb state (used by pdmConvertToPCM, defined further down).
// Declared here so pdmStart() can zero them at session boundaries — without a
// reset the leftover integrator state from the previous session would emit a
// DC transient on the first decoded burst.
static int32_t cic_i1 = 0, cic_i2 = 0, cic_i3 = 0;
static int32_t cic_p1 = 0, cic_p2 = 0, cic_p3 = 0;
static uint32_t cic_bitsSeen = 0;
static bool pdmRunning = false;
void pdmStart()
{
if (pdmRunning) return;
// Per-session state reset. Must happen before the DMA starts producing data
// so the audio-task path never sees a half-reset filter.
cic_i1 = cic_i2 = cic_i3 = 0;
cic_p1 = cic_p2 = cic_p3 = 0;
cic_bitsSeen = 0;
pdmRingReadPos = 0;
lastSoundLevel = 0;
gpio_init_af(PDM_CLOCK, PDM_CLOCK_GPIO_AF, GPIO_PIN_SPEED_VERY_HIGH);
gpio_init(PDM_DATA, GPIO_IN, GPIO_PIN_SPEED_VERY_HIGH);
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_SAI1EN);
(void)READ_BIT(RCC->APB2ENR, RCC_APB2ENR_SAI1EN);
SAI_Block_TypeDef* block = PDM_SAI_BLOCK;
CLEAR_BIT(block->CR1, SAI_xCR1_SAIEN);
while (READ_BIT(block->CR1, SAI_xCR1_SAIEN)) {}
block->CR1 = (0U << SAI_xCR1_MODE_Pos) // Master TX
| (0U << SAI_xCR1_PRTCFG_Pos) // Free protocol
| (4U << SAI_xCR1_DS_Pos) // 16-bit data
| SAI_xCR1_NODIV // BCLK = ker_ck / MCKDIV
| (PDM_SAI_MCKDIV << SAI_xCR1_MCKDIV_Pos);
block->CR2 = (1U << SAI_xCR2_FTH_Pos); // FIFO threshold = 1/4 full
block->FRCR = (15U << 0) // FRL = 15 (16-bit frame)
| (7U << 8); // FSALL = 7
block->SLOTR = (0U << 8) // NBSLOT = 0 -> 1 slot
| (1U << 16); // SLOTEN slot 0
// Prime the FIFO so the block starts clocking immediately (no underrun).
for (int i = 0; i < 4; ++i) block->DR = 0U;
SET_BIT(block->CR1, SAI_xCR1_SAIEN);
// TIM15 is free because FLYSKY_GIMBAL is OFF.
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_TIM15EN);
(void)READ_BIT(RCC->APB2ENR, RCC_APB2ENR_TIM15EN);
stm32_dma_enable_clock(PDM_CAPTURE_DMA);
PDM_CAPTURE_TIMER->CR1 = 0;
PDM_CAPTURE_TIMER->PSC = 0;
PDM_CAPTURE_TIMER->ARR = (PDM_CAPTURE_TIMER_FREQ / PDM_CLOCK_FREQ) - 1U;
PDM_CAPTURE_TIMER->CNT = 0;
PDM_CAPTURE_TIMER->EGR = TIM_EGR_UG; // load PSC/ARR
PDM_CAPTURE_TIMER->SR = 0; // clear update flag
PDM_CAPTURE_TIMER->DIER = TIM_DIER_UDE; // fire DMA request on every UEV
LL_DMA_DeInit(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM);
LL_DMA_InitTypeDef dmaInit;
LL_DMA_StructInit(&dmaInit);
dmaInit.PeriphRequest = PDM_CAPTURE_DMA_REQUEST;
dmaInit.Mode = LL_DMA_MODE_CIRCULAR;
dmaInit.Direction = LL_DMA_DIRECTION_PERIPH_TO_MEMORY;
dmaInit.PeriphOrM2MSrcAddress = (uintptr_t)&PDM_DATA_GPIO_PORT->IDR;
dmaInit.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT;
dmaInit.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE;
dmaInit.MemoryOrM2MDstAddress = (uintptr_t)pdmRingBuf;
dmaInit.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT;
dmaInit.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE;
dmaInit.NbData = PDM_RING_BYTES;
dmaInit.Priority = LL_DMA_PRIORITY_HIGH;
LL_DMA_Init(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM, &dmaInit);
LL_DMA_EnableStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM);
PDM_CAPTURE_TIMER->CR1 |= TIM_CR1_CEN;
pdmRunning = true;
}
void pdmStop()
{
if (!pdmRunning) return;
// Stop DMA pacing first so no further requests fire.
PDM_CAPTURE_TIMER->CR1 &= ~TIM_CR1_CEN;
PDM_CAPTURE_TIMER->DIER = 0;
// Disable the DMA stream and wait for it to actually stop. A subsequent
// pdmStart() calls LL_DMA_DeInit which expects the stream idle.
LL_DMA_DisableStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM);
while (LL_DMA_IsEnabledStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM)) {}
// Disable the SAI block — this stops the PDM clock to the mic, putting it
// into low-power mode.
SAI_Block_TypeDef* block = PDM_SAI_BLOCK;
CLEAR_BIT(block->CR1, SAI_xCR1_SAIEN);
while (READ_BIT(block->CR1, SAI_xCR1_SAIEN)) {}
pdmRunning = false;
}
static uint32_t pdmRingAvailable()
{
const uint32_t ndtr =
LL_DMA_GetDataLength(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM);
const uint32_t writePos = PDM_RING_BYTES - ndtr;
if (writePos >= pdmRingReadPos) return writePos - pdmRingReadPos;
return PDM_RING_BYTES - pdmRingReadPos + writePos;
}
bool pdmCapture()
{
uint32_t avail = pdmRingAvailable();
if (avail < PDM_BURST_BITS) return false;
// Skip ahead if DMA is about to overwrite unread data.
if (avail > PDM_RING_BYTES - PDM_BURST_BITS) {
const uint32_t target_avail = PDM_BURST_BITS + PDM_BURST_BITS / 2;
const uint32_t skip = avail - target_avail;
pdmRingReadPos = (pdmRingReadPos + skip) % PDM_RING_BYTES;
}
static_assert(PDM_BURST_BITS % 32U == 0U,
"PDM_BURST_BITS must be a multiple of 32");
uint32_t src = pdmRingReadPos;
for (uint32_t w = 0; w < PDM_BURST_WORDS; ++w) {
uint32_t acc = 0;
for (uint32_t b = 0; b < 32; ++b) {
const uint8_t s = pdmRingBuf[src];
if (++src >= PDM_RING_BYTES) src = 0;
acc = (acc << 1) | ((s >> PDM_DATA_GPIO_PIN) & 1U);
}
pdmBurstBuf[w] = acc;
}
pdmRingReadPos = src;
return true;
}
bool pdmUpdateSoundLevel()
{
if (!pdmCapture()) return false;
// Ones-density of the PDM bitstream approximates signal amplitude.
uint32_t ones = 0;
for (uint32_t w = 0; w < PDM_BURST_WORDS; ++w) {
ones += __builtin_popcount(pdmBurstBuf[w]);
}
const uint32_t total = PDM_BURST_WORDS * 32U;
const int32_t mid = (int32_t)(total / 2U);
int32_t dev = (int32_t)ones - mid;
if (dev < 0) dev = -dev;
uint32_t level = (uint32_t)dev * 200U / total;
if (level > 100U) level = 100U;
lastSoundLevel = (uint8_t)level;
return true;
}
uint8_t pdmGetSoundLevel()
{
return lastSoundLevel;
}
// ---------------------------------------------------------------------------
// PDM -> PCM conversion: 3rd-order CIC decimator (R = PDM_PCM_DECIMATION).
//
// Structure (Hogenauer):
// integrator ×3 at 1 MHz -> down-sample by R -> comb ×3 at 1 MHz / R
//
// Integrators run on 1-bit input (0 or 1), so for R=64 and N=3 the worst-case
// magnitude that has to be representable is R^N = 262 144 — fits easily in
// int32_t. 2's-complement wrap-around through the integrators is intentional
// and cancels in the comb stage.
//
// DC gain of the filter is R^N. Silence (50/50 bitstream) therefore sits at
// R^N / 2, and the useful signal swing is ±R^N / 2. We subtract the DC offset
// and scale so that full swing maps to the int16 range.
// ---------------------------------------------------------------------------
// PDM_POST_GAIN_SHIFT is defined in pdm_software_driver.h so trimSilence
// can scale its threshold from the same value.
// CIC integrator + comb state is declared near the top of the file (so
// pdmStart() can reset it on session boundaries).
uint32_t pdmConvertToPCM(int16_t* pcm, uint32_t max)
{
if (!pcm || max == 0) return 0;
constexpr uint32_t R = PDM_PCM_DECIMATION;
constexpr int32_t G = (int32_t)(R * R * R);
constexpr int32_t DC = G / 2;
// floor_log2(DC) - 14: ensures (c3 - DC) >> SCALE_SHIFT fits in int16.
// Works for any R, not just powers of two.
constexpr int SCALE_SHIFT = (31 - __builtin_clz((unsigned)(DC))) - 14;
static_assert(SCALE_SHIFT >= 0, "Decimation too small to fit PCM range");
uint32_t produced = 0;
const uint32_t totalBits = PDM_BURST_WORDS * 32U;
for (uint32_t idx = 0; idx < totalBits && produced < max; ++idx) {
const uint32_t word = pdmBurstBuf[idx >> 5];
const int32_t bit = (int32_t)((word >> (31U - (idx & 31U))) & 1U);
cic_i1 += bit;
cic_i2 += cic_i1;
cic_i3 += cic_i2;
if (++cic_bitsSeen == R) {
cic_bitsSeen = 0;
const int32_t c1 = cic_i3 - cic_p1; cic_p1 = cic_i3;
const int32_t c2 = c1 - cic_p2; cic_p2 = c1;
const int32_t c3 = c2 - cic_p3; cic_p3 = c2;
int32_t s = (c3 - DC) >> SCALE_SHIFT;
s <<= PDM_POST_GAIN_SHIFT;
if (s > 32767) s = 32767;
if (s < -32768) s = -32768;
pcm[produced++] = (int16_t)s;
}
}
return produced;
}
#endif // PDM_CAPTURE_DMA
@@ -0,0 +1,46 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#pragma once
#include <stdint.h>
// Decimation factor: output rate = PDM_CLOCK_FREQ / PDM_PCM_DECIMATION.
// R=100, PDM_CLOCK=1.6 MHz → SRC_RATE=16000 Hz exactly (no resampler needed).
#define PDM_PCM_DECIMATION 100
// Post-CIC gain (left shift before int16 saturation; +6 dB per step).
// Default calibrated at 4. Override per target in hal.h before this header
// is reached. trimSilence() in pdm_wav_recorder.cpp scales its threshold
// from this same value, so changing it keeps trim in sync automatically.
#ifndef PDM_POST_GAIN_SHIFT
#define PDM_POST_GAIN_SHIFT 5
#endif
// Bring up SAI clock + TIM15 + DMA capture and reset filter state.
// Idempotent. Call when entering a context that needs the mic.
void pdmStart();
// Stop the DMA pacer, disable SAI block (the PDM clock to the mic stops).
void pdmStop();
bool pdmUpdateSoundLevel();
uint8_t pdmGetSoundLevel();
bool pdmCapture();
uint32_t pdmConvertToPCM(int16_t* pcm, uint32_t max);
+68
View File
@@ -50,8 +50,10 @@
#include <malloc.h>
#include <new>
#include <stdarg.h>
#include <string.h>
#include "lua/lua_states.h"
#include "pdm_wav_recorder.h"
#define CLI_COMMAND_MAX_ARGS 8
#define CLI_COMMAND_MAX_LEN 256
@@ -262,6 +264,69 @@ int cliPlay(const char ** argv)
return 0;
}
#if defined(PDM_CLOCK)
// Record from the PDM microphone to a 16-bit mono WAV file. The actual
// capture + resampling + WAV I/O lives in PdmWavRecorder so the same
// pipeline is reused by the GUI recorder tool.
int cliRecord(const char ** argv)
{
if (!argv[1] || !argv[1][0]) {
cliSerialPrint("%s: missing filename", argv[0]);
return 0;
}
int seconds = 5;
if (argv[2] && argv[2][0]) {
if (toInt(argv, 2, &seconds) == 0 || seconds <= 0 || seconds > 600) {
cliSerialPrint("%s: Invalid duration \"%s\"", argv[0], argv[2]);
return 0;
}
}
char pathBuf[64];
const char* path = argv[1];
const size_t nameLen = strlen(argv[1]);
const bool hasExt =
nameLen >= 4 && strcasecmp(argv[1] + nameLen - 4, ".wav") == 0;
if (!hasExt) {
if (nameLen + 5 > sizeof(pathBuf)) {
cliSerialPrint("%s: path too long", argv[0]);
return 0;
}
memcpy(pathBuf, argv[1], nameLen);
memcpy(pathBuf + nameLen, ".wav", 5);
path = pathBuf;
}
const uint32_t totalSamples = PdmWavRecorder::DST_RATE * (uint32_t)seconds;
// Capture only runs while needed — bring the hardware up for this command.
pdmStart();
PdmWavRecorder rec;
FRESULT res = rec.start(path, (uint32_t)seconds);
if (res != FR_OK) {
pdmStop();
cliSerialPrint("%s: cannot open \"%s\" (err %u)", argv[0], path,
(unsigned)res);
return 0;
}
cliSerialPrint("Recording %d s (%u samples @ %u Hz) to %s",
seconds, (unsigned)totalSamples,
(unsigned)PdmWavRecorder::DST_RATE, path);
while (rec.isRecording() && rec.getSamplesWritten() < totalSamples) {
sleep_ms(50);
}
rec.stop();
pdmStop();
cliSerialPrint("Recorded %u samples (%u bytes)",
(unsigned)rec.getSamplesWritten(),
(unsigned)rec.getBytesWritten());
return 0;
}
#endif // PDM_CLOCK
int cliLs(const char ** argv)
{
FILINFO fno;
@@ -1787,6 +1852,9 @@ const CliCommand cliCommands[] = {
{ "readsd", cliReadSD, "<start sector> <sectors count> <read buffer size (sectors)>" },
{ "testsd", cliTestSD, "" },
{ "play", cliPlay, "<filename>" },
#if defined(PDM_CLOCK)
{ "rec", cliRecord, "<filename> [<seconds>]" },
#endif
{ "reboot", cliReboot, "[wdt]" },
{ "set", cliSet, "<what> <value>" },
#if defined(ENABLE_SERIAL_PASSTHROUGH)
+1
View File
@@ -98,6 +98,7 @@ set(GUI_SRC
radio/radio_trainer.cpp
radio/radio_version.cpp
radio/radio_gps_tool.cpp
radio/radio_mic_recorder.cpp
)
macro(add_gui_src src)
@@ -0,0 +1,282 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "radio_mic_recorder.h"
#if defined(PDM_CLOCK)
#include <stdio.h>
#include "button.h"
#include "dialog.h"
#include "edgetx.h"
#include "ff.h"
#include "sdcard.h"
#include "static.h"
#include "timers_driver.h"
static constexpr coord_t MIC_BTN_W = 260;
static constexpr coord_t MIC_BTN_H = EdgeTxStyles::UI_ELEMENT_HEIGHT * 2 + PAD_LARGE;
RadioMicRecorder::RadioMicRecorder() :
Page(ICON_RADIO_TOOLS)
{
// Bring the PDM hardware up only while this tool is open. Capture is off
// outside of this scope so the mic clock and DMA stream are idle.
pdmStart();
buildHeader(header);
buildBody(body);
enterIdle();
}
RadioMicRecorder::~RadioMicRecorder()
{
// An async call scheduled from the LabelDialog confirm path may still be
// pending when the page is closed (e.g. user long-presses EXIT while the
// dialog is being dismissed). Cancelling ensures the callback never fires
// on a destroyed instance.
lv_async_call_cancel(&RadioMicRecorder::asyncProcessPendingRename, this);
if (recorder.isRecording()) recorder.stop();
pdmStop();
}
void RadioMicRecorder::asyncProcessPendingRename(void* ctx)
{
static_cast<RadioMicRecorder*>(ctx)->processPendingRename();
}
void RadioMicRecorder::buildHeader(Window* window)
{
header->setTitle(STR_MENUTOOLS);
header->setTitle2(STR_MIC_RECORDER);
}
void RadioMicRecorder::buildBody(Window* window)
{
window->padAll(PAD_ZERO);
const coord_t w = window->width();
const coord_t h = window->height();
bigLabel = new StaticText(
window,
{0, h / 3 - 20, w, 40},
"", COLOR_THEME_PRIMARY1_INDEX, FONT(XL));
lv_obj_set_style_text_align(bigLabel->getLvObj(), LV_TEXT_ALIGN_CENTER, 0);
infoLabel = new StaticText(
window,
{0, h / 3 + 30, w, 24},
"", COLOR_THEME_SECONDARY1_INDEX, FONT(STD));
lv_obj_set_style_text_align(infoLabel->getLvObj(), LV_TEXT_ALIGN_CENTER, 0);
actionButton = new TextButton(
window,
{(w - MIC_BTN_W) / 2,
h - MIC_BTN_H - PAD_LARGE * 2,
MIC_BTN_W, MIC_BTN_H},
"", [this]() {
onActionPressed();
return 0;
});
actionButton->setFont(FONT_XL_INDEX);
}
void RadioMicRecorder::onEvent(event_t event)
{
if (event == EVT_KEY_LONG(KEY_EXIT)) {
killEvents(event);
if (recorder.isRecording()) recorder.stop();
onCancel();
}
}
void RadioMicRecorder::onActionPressed()
{
switch (state) {
case State::IDLE: enterCountdown(); break;
case State::COUNTDOWN: enterIdle(); break; // cancel
case State::RECORDING: stopRecording(); break;
}
}
void RadioMicRecorder::enterIdle()
{
if (recorder.isRecording()) recorder.stop();
state = State::IDLE;
stateStart = get_tmr10ms();
refreshUI();
}
void RadioMicRecorder::enterCountdown()
{
state = State::COUNTDOWN;
stateStart = get_tmr10ms();
refreshUI();
}
void RadioMicRecorder::enterRecording()
{
pickNextFilename();
FRESULT res = recorder.start(filename, 0);
if (res != FR_OK) {
char msg[40];
snprintf(msg, sizeof(msg), "%s %u", STR_OPEN_ERROR, (unsigned)res);
bigLabel->setText(msg);
state = State::IDLE;
stateStart = get_tmr10ms();
actionButton->setText(STR_RECORD);
return;
}
state = State::RECORDING;
stateStart = get_tmr10ms();
refreshUI();
}
void RadioMicRecorder::stopRecording()
{
recorder.stop();
PdmWavRecorder::trimSilence(filename);
state = State::IDLE;
stateStart = get_tmr10ms();
refreshUI();
const char* base = strrchr(filename, '/');
base = base ? base + 1 : filename;
char baseName[PATH_MAX_LEN] = {};
strncpy(baseName, base, sizeof(baseName) - 1);
char* dot = strrchr(baseName, '.');
if (dot) *dot = '\0';
new LabelDialog(baseName, LEN_FUNCTION_NAME, STR_SAVE_AS, [this](std::string newName) {
if (newName.empty()) return;
char dir[sizeof(SOUNDS_PATH) + 1];
strcpy(dir, SOUNDS_PATH "/");
strncpy(dir + SOUNDS_PATH_LNG_OFS, currentLanguagePack->id, 2);
snprintf(pendingRename, sizeof(pendingRename), "%s%s.wav", dir, newName.c_str());
if (strcmp(pendingRename, filename) == 0) { refreshUI(); return; }
// Defer the overwrite check until LabelDialog has finished closing —
// creating a modal now would leave us stacked on top of LabelDialog,
// which corrupts the lv_group chain when LabelDialog deletes itself.
// The page dtor cancels this async call if the user bails out before
// it fires (see ~RadioMicRecorder).
lv_async_call(&RadioMicRecorder::asyncProcessPendingRename, this);
});
}
void RadioMicRecorder::processPendingRename()
{
FILINFO info;
if (f_stat(pendingRename, &info) == FR_OK) {
new ConfirmDialog(STR_FILE_EXISTS, STR_ASK_OVERWRITE,
[this]() { applyRename(); });
} else {
applyRename();
}
}
void RadioMicRecorder::applyRename()
{
f_unlink(pendingRename);
f_rename(filename, pendingRename);
strncpy(filename, pendingRename, sizeof(filename) - 1);
filename[sizeof(filename) - 1] = '\0';
refreshUI();
}
void RadioMicRecorder::refreshUI()
{
char buf[64];
switch (state) {
case State::IDLE:
bigLabel->setText(STR_PUSH_TO_RECORD);
if (recorder.getSamplesWritten() > 0) {
snprintf(buf, sizeof(buf), "%s %s (%us)", STR_SAVED,
filename, (unsigned)recorder.getElapsedSeconds());
infoLabel->setText(buf);
} else {
infoLabel->setText("");
}
actionButton->setText(STR_RECORD);
break;
case State::COUNTDOWN: {
const uint32_t elapsed10 = (uint32_t)(get_tmr10ms() - stateStart);
int remaining = (int)COUNTDOWN_SECONDS - (int)(elapsed10 / 100U);
if (remaining < 0) remaining = 0;
snprintf(buf, sizeof(buf), "%s %d", STR_STARTING_IN, remaining);
bigLabel->setText(buf);
infoLabel->setText(STR_GET_READY);
actionButton->setText(STR_CANCEL);
break;
}
case State::RECORDING: {
const uint32_t s = recorder.getElapsedSeconds();
snprintf(buf, sizeof(buf), "%s %02u:%02u", STR_REC,
(unsigned)(s / 60U), (unsigned)(s % 60U));
bigLabel->setText(buf);
snprintf(buf, sizeof(buf), "%s %u KB",
filename, (unsigned)(recorder.getBytesWritten() / 1024U));
infoLabel->setText(buf);
actionButton->setText(STR_STOP);
break;
}
}
}
void RadioMicRecorder::checkEvents()
{
Page::checkEvents();
if (state == State::COUNTDOWN) {
const uint32_t elapsed10 = (uint32_t)(get_tmr10ms() - stateStart);
if (elapsed10 >= COUNTDOWN_SECONDS * 100U) {
enterRecording();
} else {
refreshUI();
}
} else if (state == State::RECORDING) {
if (!recorder.isRecording()) {
state = State::IDLE;
stateStart = get_tmr10ms();
}
refreshUI();
}
}
void RadioMicRecorder::pickNextFilename()
{
char dir[sizeof(SOUNDS_PATH) + 1];
strcpy(dir, SOUNDS_PATH "/");
strncpy(dir + SOUNDS_PATH_LNG_OFS, currentLanguagePack->id, 2);
FILINFO info;
for (int i = 0; i < 100; i++) {
snprintf(filename, sizeof(filename), "%srec_%02d.wav", dir, i);
if (f_stat(filename, &info) != FR_OK) return;
}
snprintf(filename, sizeof(filename), "%srec.wav", dir);
}
#endif // PDM_CLOCK
@@ -0,0 +1,75 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#pragma once
#include "page.h"
#include "pdm_wav_recorder.h"
#include "sdcard.h"
#if defined(PDM_CLOCK)
class TextButton;
class StaticText;
class RadioMicRecorder : public Page
{
public:
RadioMicRecorder();
~RadioMicRecorder() override;
protected:
enum class State : uint8_t { IDLE, COUNTDOWN, RECORDING };
static constexpr uint32_t COUNTDOWN_SECONDS = 5;
static constexpr int PATH_MAX_LEN = sizeof(SOUNDS_PATH) + 14; // "/lang/rec_00.wav\0"
State state = State::IDLE;
tmr10ms_t stateStart = 0;
char filename[PATH_MAX_LEN] = {0};
char pendingRename[PATH_MAX_LEN] = {0};
PdmWavRecorder recorder;
StaticText* bigLabel = nullptr;
StaticText* infoLabel = nullptr;
TextButton* actionButton = nullptr;
void buildHeader(Window* window);
void buildBody(Window* window);
void checkEvents() override;
void onEvent(event_t event) override;
void onActionPressed();
void enterIdle();
void enterCountdown();
void enterRecording();
void stopRecording();
void processPendingRename();
void applyRename();
void refreshUI();
void pickNextFilename();
// Stable callback address so the dtor can cancel a pending lv_async call
// scheduled from the LabelDialog confirm path.
static void asyncProcessPendingRename(void* ctx);
};
#endif // PDM_CLOCK
@@ -29,6 +29,7 @@
#include "radio_ghost_module_config.h"
#include "radio_spectrum_analyser.h"
#include "radio_gps_tool.h"
#include "radio_mic_recorder.h"
#include "standalone_lua.h"
#include "etx_lv_theme.h"
#include "lib_file.h"
@@ -211,6 +212,13 @@ static void run_ghost_config(const std::string&)
}
#endif
#if defined(PDM_CLOCK)
static void run_mic_recorder(const std::string&)
{
new RadioMicRecorder();
}
#endif
//-----------------------------------------------------------------------------
struct ToolButton : public TextButton {
@@ -349,6 +357,10 @@ void RadioToolsPage::rebuild(Window* window)
}
#endif
#if defined(PDM_CLOCK)
tools.emplace_back(ToolEntry{STR_MIC_RECORDER, "", run_mic_recorder});
#endif
#if defined(LUA)
scanLuaTools(tools);
#endif
+243
View File
@@ -0,0 +1,243 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "pdm_wav_recorder.h"
#if defined(PDM_CLOCK)
#include <string.h>
#include "os/task.h"
// Guards against concurrent access from the audio task and the owner thread.
static PdmWavRecorder* s_active = nullptr;
static mutex_handle_t s_mutex;
static bool s_mutexInited = false;
static void ensureMutex()
{
if (!s_mutexInited) {
mutex_create(&s_mutex);
s_mutexInited = true;
}
}
static void writeLE16(uint8_t* p, uint16_t v)
{
p[0] = v & 0xFF;
p[1] = (v >> 8) & 0xFF;
}
static void writeLE32(uint8_t* p, uint32_t v)
{
p[0] = v & 0xFF;
p[1] = (v >> 8) & 0xFF;
p[2] = (v >> 16) & 0xFF;
p[3] = (v >> 24) & 0xFF;
}
FRESULT PdmWavRecorder::start(const char* path, uint32_t expectedSeconds)
{
ensureMutex();
MutexLock lock = MutexLock::MakeInstance(&s_mutex);
if (s_active != nullptr) return FR_LOCKED;
samplesWritten = 0;
maxSamples = expectedSeconds * DST_RATE;
recording = false;
FRESULT res = f_open(&file, path, FA_CREATE_ALWAYS | FA_WRITE);
if (res != FR_OK) return res;
const uint32_t dataBytes = maxSamples * 2U;
uint8_t header[44];
memcpy(&header[0], "RIFF", 4);
writeLE32(&header[4], 36U + dataBytes);
memcpy(&header[8], "WAVE", 4);
memcpy(&header[12], "fmt ", 4);
writeLE32(&header[16], 16);
writeLE16(&header[20], 1); // PCM
writeLE16(&header[22], 1); // mono
writeLE32(&header[24], DST_RATE);
writeLE32(&header[28], DST_RATE * 2U); // byte rate
writeLE16(&header[32], 2); // block align
writeLE16(&header[34], 16); // bits per sample
memcpy(&header[36], "data", 4);
writeLE32(&header[40], dataBytes);
UINT written = 0;
res = f_write(&file, header, sizeof(header), &written);
if (res != FR_OK || written != sizeof(header)) {
f_close(&file);
return res != FR_OK ? res : FR_DISK_ERR;
}
recording = true;
s_active = this;
return FR_OK;
}
bool PdmWavRecorder::tickLocked()
{
if (!recording) return false;
if (!pdmCapture()) return false;
uint32_t n = pdmConvertToPCM(pcm, PCM_MAX);
if (n == 0) return false;
if (maxSamples != 0) {
const uint32_t remaining = maxSamples - samplesWritten;
if (n > remaining) n = remaining;
}
UINT w = 0;
FRESULT res = f_write(&file, pcm, n * sizeof(int16_t), &w);
if (res != FR_OK || w != n * sizeof(int16_t)) {
recording = false;
return false;
}
samplesWritten += n;
if (maxSamples != 0 && samplesWritten >= maxSamples) {
recording = false;
}
return true;
}
void PdmWavRecorder::audioTick()
{
if (s_active == nullptr) return;
ensureMutex();
MutexLock lock = MutexLock::MakeInstance(&s_mutex);
if (s_active != nullptr) s_active->tickLocked();
}
FRESULT PdmWavRecorder::stop()
{
ensureMutex();
MutexLock lock = MutexLock::MakeInstance(&s_mutex);
if (s_active != this) return FR_OK;
s_active = nullptr;
recording = false;
const uint32_t dataBytes = samplesWritten * 2U;
uint8_t buf[4];
UINT written = 0;
writeLE32(buf, 36U + dataBytes);
f_lseek(&file, 4);
f_write(&file, buf, 4, &written);
writeLE32(buf, dataBytes);
f_lseek(&file, 40);
f_write(&file, buf, 4, &written);
return f_close(&file);
}
FRESULT PdmWavRecorder::trimSilence(const char* path)
{
// Threshold tracks the post-CIC gain so trim stays calibrated when
// PDM_POST_GAIN_SHIFT is changed. Baseline 4000 was tuned at shift=4.
static constexpr int32_t _silenceRaw =
((int32_t)4000 * (1 << PDM_POST_GAIN_SHIFT)) / 16;
static constexpr int16_t SILENCE_THRESHOLD =
_silenceRaw > 32767 ? (int16_t)32767 : (int16_t)_silenceRaw;
static constexpr uint32_t PAD_SAMPLES = DST_RATE / 100; // 10 ms
static constexpr uint32_t CHUNK = 256;
FIL f;
FRESULT res = f_open(&f, path, FA_READ | FA_WRITE);
if (res != FR_OK) return res;
uint8_t hdr[44];
UINT br = 0;
res = f_read(&f, hdr, sizeof(hdr), &br);
if (res != FR_OK || br != sizeof(hdr)) { f_close(&f); return res; }
const uint32_t dataBytes =
(uint32_t)hdr[40] | ((uint32_t)hdr[41] << 8) |
((uint32_t)hdr[42] << 16) | ((uint32_t)hdr[43] << 24);
const uint32_t total = dataBytes / 2;
if (total == 0) { f_close(&f); return FR_OK; }
int16_t buf[CHUNK];
uint32_t firstActive = UINT32_MAX;
uint32_t lastActive = 0;
for (uint32_t base = 0; base < total; base += CHUNK) {
const uint32_t n = (base + CHUNK <= total) ? CHUNK : total - base;
f_lseek(&f, 44 + base * 2);
res = f_read(&f, buf, n * 2, &br);
if (res != FR_OK) { f_close(&f); return res; }
const uint32_t got = br / 2;
for (uint32_t i = 0; i < got; i++) {
const int16_t abs = buf[i] < 0 ? (int16_t)-buf[i] : buf[i];
if (abs > SILENCE_THRESHOLD) {
if (firstActive == UINT32_MAX) firstActive = base + i;
lastActive = base + i;
}
}
}
if (firstActive == UINT32_MAX) { f_close(&f); return FR_OK; } // all silent
const uint32_t trimStart = firstActive > PAD_SAMPLES ? firstActive - PAD_SAMPLES : 0;
const uint32_t trimEnd = (lastActive + PAD_SAMPLES < total) ? lastActive + PAD_SAMPLES : total - 1;
const uint32_t newTotal = trimEnd - trimStart + 1;
if (trimStart == 0 && trimEnd == total - 1) { f_close(&f); return FR_OK; }
if (trimStart > 0) {
uint32_t src = trimStart, dst = 0;
while (src <= trimEnd) {
const uint32_t n = (src + CHUNK - 1 <= trimEnd) ? CHUNK : trimEnd - src + 1;
f_lseek(&f, 44 + src * 2);
res = f_read(&f, buf, n * 2, &br);
if (res != FR_OK) { f_close(&f); return res; }
UINT bw = 0;
f_lseek(&f, 44 + dst * 2);
res = f_write(&f, buf, br, &bw);
if (res != FR_OK) { f_close(&f); return res; }
src += n;
dst += n;
}
}
f_lseek(&f, 44 + newTotal * 2);
f_truncate(&f);
const uint32_t newDataBytes = newTotal * 2;
uint8_t tmp[4];
UINT bw = 0;
writeLE32(tmp, 36 + newDataBytes);
f_lseek(&f, 4);
f_write(&f, tmp, 4, &bw);
writeLE32(tmp, newDataBytes);
f_lseek(&f, 40);
f_write(&f, tmp, 4, &bw);
return f_close(&f);
}
#endif // PDM_CLOCK
+67
View File
@@ -0,0 +1,67 @@
/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#pragma once
#include "edgetx.h"
#if defined(PDM_CLOCK)
#include <stdint.h>
#include "ff.h"
// 16 kHz: PDM_CLOCK_FREQ(1.6 MHz)/PDM_PCM_DECIMATION(100) = 16000 Hz directly.
// Must also divide AUDIO_SAMPLE_RATE(32 kHz) evenly for the WAV player.
class PdmWavRecorder
{
public:
static constexpr uint32_t DST_RATE = 16000;
// expectedSeconds == 0 means open-ended; header is patched on stop().
// At most one recorder active at a time; concurrent start() returns FR_LOCKED.
FRESULT start(const char* path, uint32_t expectedSeconds);
FRESULT stop();
// Called from the audio task every ~4 ms.
static void audioTick();
// Trim leading/trailing silence in-place and patch the WAV header.
static FRESULT trimSilence(const char* path);
bool isRecording() const { return recording; }
uint32_t getSamplesWritten() const { return samplesWritten; }
uint32_t getBytesWritten() const { return samplesWritten * 2U; }
uint32_t getElapsedSeconds() const { return samplesWritten / DST_RATE; }
private:
static constexpr uint32_t PCM_MAX = 256;
bool tickLocked();
FIL file;
int16_t pcm[PCM_MAX];
// 32-bit aligned reads on Cortex-M7 are atomic — safe to read without mutex.
volatile uint32_t samplesWritten = 0;
uint32_t maxSamples = 0; // 0 = open-ended
volatile bool recording = false;
};
#endif // PDM_CLOCK
+15
View File
@@ -611,6 +611,21 @@ FRESULT f_getcwd(TCHAR *path, UINT sz_path)
return FR_OK;
}
FRESULT f_truncate(FIL* fil)
{
if (fil && fil->obj.fs) {
_simu_FIL* sf = reinterpret_cast<_simu_FIL*>(fil->obj.fs);
if (sf->stream && sf->stream->is_open()) {
sf->stream->flush();
if (!sf->stream->good()) return FR_DISK_ERR;
std::error_code ec;
fs::resize_file(sf->filepath, fil->fptr, ec);
if (ec) return FR_DISK_ERR;
}
}
return FR_OK;
}
FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs)
{
// just fake that we always have some clusters free
+10
View File
@@ -880,3 +880,13 @@ uint32_t simuGetCustomSwitchColor(uint8_t idx)
#endif
return 0;
}
#if defined(PDM_CLOCK)
#include "boards/rm-h750/pdm_software_driver.h"
void pdmStart() {}
void pdmStop() {}
bool pdmUpdateSoundLevel() { return false; }
uint8_t pdmGetSoundLevel() { return 0; }
bool pdmCapture() { return false; }
uint32_t pdmConvertToPCM(int16_t*, uint32_t) { return 0; }
#endif
+1
View File
@@ -183,6 +183,7 @@ add_library(board OBJECT EXCLUDE_FROM_ALL
${BOARD_COMMON_SRC}
${BOARD_SRC_DIR}/tp_gt911.cpp
${BOARD_SRC_DIR}/audio_driver.cpp
${BOARD_SRC_DIR}/pdm_software_driver.cpp
drivers/tas2505.cpp
targets/common/arm/stm32/heartbeat_driver.cpp
targets/common/arm/stm32/mixer_scheduler_driver.cpp
+22 -2
View File
@@ -29,7 +29,7 @@ SDMMC uses own DMA controler
DMA1
Stream0: LED_STRIP_TIMER_DMA_STREAM
Stream1: INTMODULE_DMA_STREAM
Stream2: FLYSKY_HALL_DMA_Stream_RX
Stream2: FLYSKY_HALL_DMA_Stream_RX (FLYSKY_GIMBAL) / PDM_CAPTURE_DMA_STREAM (!FLYSKY_GIMBAL)
Stream3: TELEMETRY_DMA_Stream_RX
Stream4: I2S_DMA_Stream
Stream5: INTMODULE_RX_DMA_STREAM
@@ -55,9 +55,11 @@ TIM7:
TIM8: TRAINER_TIMER
TIM12: MIXER_SCHEDULER_TIMER
TIM14: MS_TIMER
TIM15: PDM
TIM15:
TIM16:
TIM17: ROTARY_ENCODER_TIMER
SAI1_Block_A: PDM_CLOCK (1 MHz bit-clock output on SAI1_CK1 / PE5, !FLYSKY_GIMBAL only)
*/
#ifndef _HAL_H_
@@ -201,6 +203,24 @@ TIM17: ROTARY_ENCODER_TIMER
#define I2S_DMA_Stream_IRQn DMA1_Stream4_IRQn
#define I2S_DMA_Stream_IRQHandler DMA1_Stream4_IRQHandler
// MEMS PDM microphone — not available when FLYSKY_GIMBAL is enabled
#if !defined(FLYSKY_GIMBAL)
#define PDM_CLOCK GPIO_PIN(GPIOE, 5)
#define PDM_CLOCK_GPIO_AF LL_GPIO_AF_6
#define PDM_SAI_BLOCK SAI1_Block_A
#define PDM_SAI_KER_FREQ 48000000 // PLL1Q (see system_clock.c)
#define PDM_CLOCK_FREQ 1600000 // 1.6 MHz → MCKDIV=30, R=100 → 16 kHz PCM direct
#define PDM_DATA GPIO_PIN(GPIOE, 4)
#define PDM_DATA_GPIO_PORT GPIOE
#define PDM_DATA_GPIO_PIN 4
#define PDM_DATA_IDR_MASK (1U << PDM_DATA_GPIO_PIN)
#define PDM_CAPTURE_DMA DMA1
#define PDM_CAPTURE_DMA_STREAM LL_DMA_STREAM_2
#define PDM_CAPTURE_DMA_REQUEST LL_DMAMUX1_REQ_TIM15_UP
#define PDM_CAPTURE_TIMER TIM15
#define PDM_CAPTURE_TIMER_FREQ (PERI2_FREQUENCY * TIMER_MULT_APB2)
#endif
// I2C Bus
#define I2C_B1 I2C4
@@ -179,6 +179,7 @@ add_library(board OBJECT EXCLUDE_FROM_ALL
${BOARD_COMMON_SRC}
${BOARD_SRC_DIR}/tp_gt911.cpp
${BOARD_SRC_DIR}/audio_driver.cpp
${BOARD_SRC_DIR}/pdm_software_driver.cpp
${BOARD_SRC_DIR}/luminosity_sensor.cpp
drivers/tas2505.cpp
drivers/icm42607C.cpp
+25 -2
View File
@@ -29,7 +29,7 @@ SDMMC uses own DMA controler
DMA1
Stream0: LED_STRIP_TIMER_DMA_STREAM
Stream1: INTMODULE_DMA_STREAM
Stream2: FLYSKY_HALL_DMA_Stream_RX
Stream2: PDM_CAPTURE_DMA_STREAM
Stream3: TELEMETRY_DMA_Stream_RX
Stream4: I2S_DMA_Stream
Stream5: INTMODULE_RX_DMA_STREAM
@@ -55,10 +55,12 @@ TIM7:
TIM8: TRAINER_TIMER
TIM12: MIXER_SCHEDULER_TIMER
TIM14: MS_TIMER
TIM15: PDM
TIM15: PDM_CAPTURE_TIMER
TIM16:
TIM17: ROTARY_ENCODER_TIMER
SAI1_Block_A: PDM_CLOCK (1 MHz bit-clock output on SAI1_CK1 / PE5)
USART1: TELEMETRY_USART
USART2: EXTMODULE_USART
USART3: AUX2_SERIAL_USART
@@ -405,6 +407,27 @@ USART6: INTMODULE_USART
#define I2S_DMA_Stream_IRQn DMA1_Stream4_IRQn
#define I2S_DMA_Stream_IRQHandler DMA1_Stream4_IRQHandler
//MEMS — PDM microphone:
// clock output driven by SAI1 block A on SAI1_CK1 / PE5 (AF6, 1 MHz)
// data input captured by DMA from PE4 @ 1 MHz.
#define PDM_CLOCK GPIO_PIN(GPIOE, 5)
#define PDM_CLOCK_GPIO_AF LL_GPIO_AF_6
#define PDM_SAI_BLOCK SAI1_Block_A
#define PDM_SAI_KER_FREQ 48000000 // PLL1Q (see system_clock.c)
#define PDM_CLOCK_FREQ 1600000 // 1.6 MHz → MCKDIV=30, R=100 → 16 kHz PCM direct
#define PDM_DATA GPIO_PIN(GPIOE, 4)
#define PDM_DATA_GPIO_PORT GPIOE
#define PDM_DATA_GPIO_PIN 4
#define PDM_DATA_IDR_MASK (1U << PDM_DATA_GPIO_PIN)
// capture PE4 via GPIO->memory DMA paced by TIM15 updates -> hardware-precise,
// non-blocking PDM sampling.
#define PDM_CAPTURE_DMA DMA1
#define PDM_CAPTURE_DMA_STREAM LL_DMA_STREAM_2
#define PDM_CAPTURE_DMA_REQUEST LL_DMAMUX1_REQ_TIM15_UP
#define PDM_CAPTURE_TIMER TIM15
#define PDM_CAPTURE_TIMER_FREQ (PERI2_FREQUENCY * TIMER_MULT_APB2)
// I2C Bus
#define I2C_B1 I2C4
#define I2C_B1_SDA_GPIO GPIO_PIN(GPIOD, 13) // PD.13
+7
View File
@@ -28,6 +28,7 @@
#include "timers_driver.h"
#include "hal/abnormal_reboot.h"
#include "hal/watchdog_driver.h"
#include "pdm_wav_recorder.h"
#include "tasks.h"
#include "tasks/mixer_task.h"
@@ -115,6 +116,12 @@ static void audioTask()
DEBUG_TIMER_SAMPLE(debugTimerAudioIterval);
DEBUG_TIMER_START(debugTimerAudioDuration);
audioQueue.wakeup();
#if defined(PDM_CLOCK)
// Drive microphone recording (if any) at the audio-task cadence.
// Much shorter than the UI tick interval, so the PDM ring buffer
// never fills enough to trigger skip-ahead / sample drops.
PdmWavRecorder::audioTick();
#endif
DEBUG_TIMER_STOP(debugTimerAudioDuration);
sleep_until(&next_tick, 4);
}
+10
View File
@@ -966,6 +966,16 @@
#define TR_POWERMETER_ATTN "衰减器"
#define TR_POWERMETER_FREQ "频率"
#define TR_MENUTOOLS "扩展工具"
#define TR_MIC_RECORDER "麦克风录音"
#define TR_PUSH_TO_RECORD "按下录音"
#define TR_RECORD "录音"
#define TR_STOP "停止"
#define TR_REC "REC"
#define TR_STARTING_IN "倒计时"
#define TR_GET_READY "准备好..."
#define TR_SAVED "已保存:"
#define TR_SAVE_AS "另存为"
#define TR_OPEN_ERROR "打开失败"
#define TR_TURN_OFF_RECEIVER "请关闭接收机"
#define TR_STOPPING "正在停止..."
#define TR_MENU_SPECTRUM_ANALYSER "频谱仪"
+10
View File
@@ -964,6 +964,16 @@
#define TR_POWERMETER_ATTN "Útlum"
#define TR_POWERMETER_FREQ "Frek."
#define TR_MENUTOOLS "NÁSTROJE"
#define TR_MIC_RECORDER "Záznam mikrofonu"
#define TR_PUSH_TO_RECORD "Stiskni pro záznam"
#define TR_RECORD "Nahrát"
#define TR_STOP "Zastavit"
#define TR_REC "REC"
#define TR_STARTING_IN "Začne za"
#define TR_GET_READY "Připravit se..."
#define TR_SAVED "Uloženo:"
#define TR_SAVE_AS "Uložit jako"
#define TR_OPEN_ERROR "Chyba otevření"
#define TR_TURN_OFF_RECEIVER "Vypněte přijímač"
#define TR_STOPPING "Zastavuji..."
#define TR_MENU_SPECTRUM_ANALYSER "SPEKTRÁLNÍ ANALYZÉR"
+10
View File
@@ -971,6 +971,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS "VÆRKTØJ"
#define TR_MIC_RECORDER "Mikrofon optager"
#define TR_PUSH_TO_RECORD "Tryk for at optage"
#define TR_RECORD "Optag"
#define TR_STOP "Stop"
#define TR_REC "REC"
#define TR_STARTING_IN "Starter om"
#define TR_GET_READY "Gør dig klar..."
#define TR_SAVED "Gemt:"
#define TR_SAVE_AS "Gem som"
#define TR_OPEN_ERROR "Åbn fejl"
#define TR_TURN_OFF_RECEIVER "Sluk modtager"
#define TR_STOPPING "Standser..."
#define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSE"
+10
View File
@@ -965,6 +965,16 @@
#define TR_POWERMETER_ATTN "Dämpf."
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS TR_BW_COL("TOOLS", "Tools")
#define TR_MIC_RECORDER TR_BW_COL("Mikrofon Rek.", "Mikrofon Rekorder")
#define TR_PUSH_TO_RECORD TR_BW_COL("Drücken=Aufnahme", "Drücken zum Aufnehmen")
#define TR_RECORD "Aufnahme"
#define TR_STOP "Stop"
#define TR_REC "REC"
#define TR_STARTING_IN "Start in"
#define TR_GET_READY "Bereit machen..."
#define TR_SAVED "Gespeichert:"
#define TR_SAVE_AS "Speichern als"
#define TR_OPEN_ERROR "Öffnungsfehler"
#define TR_TURN_OFF_RECEIVER "Empf. ausschalten"
#define TR_STOPPING "Stoppe..."
#define TR_MENU_SPECTRUM_ANALYSER TR_BW_COL("SPEKTRUM ANALYSATOR", "Spektrum Analysator")
+10
View File
@@ -967,6 +967,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS "TOOLS"
#define TR_MIC_RECORDER "Mic recorder"
#define TR_PUSH_TO_RECORD "Push to record"
#define TR_RECORD "Record"
#define TR_STOP "Stop"
#define TR_REC "REC"
#define TR_STARTING_IN "Starting in"
#define TR_GET_READY "Get ready..."
#define TR_SAVED "Saved:"
#define TR_SAVE_AS "Save as"
#define TR_OPEN_ERROR "Open error"
#define TR_TURN_OFF_RECEIVER "Turn off receiver"
#define TR_STOPPING "Stopping..."
#define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSER"
+10
View File
@@ -960,6 +960,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Frec."
#define TR_MENUTOOLS "UTILS"
#define TR_MIC_RECORDER "Grabador de mic"
#define TR_PUSH_TO_RECORD "Pulsar para grabar"
#define TR_RECORD "Grabar"
#define TR_STOP "Parar"
#define TR_REC "REC"
#define TR_STARTING_IN "Empieza en"
#define TR_GET_READY "Prepárate..."
#define TR_SAVED "Guardado:"
#define TR_SAVE_AS "Guardar como"
#define TR_OPEN_ERROR "Error al abrir"
#define TR_TURN_OFF_RECEIVER "Apaga el receptor"
#define TR_STOPPING "Parando..."
#define TR_MENU_SPECTRUM_ANALYSER "ANALIZADOR DE ESPECTRO"
+10
View File
@@ -960,6 +960,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Taajuus"
#define TR_MENUTOOLS "TOOLS"
#define TR_MIC_RECORDER "Mikrofoni tallennin"
#define TR_PUSH_TO_RECORD "Paina tallentaaksesi"
#define TR_RECORD "Tallenna"
#define TR_STOP "Lopeta"
#define TR_REC "REC"
#define TR_STARTING_IN "Alkaa"
#define TR_GET_READY "Valmistaudu..."
#define TR_SAVED "Tallennettu:"
#define TR_SAVE_AS "Tallenna nimellä"
#define TR_OPEN_ERROR "Avausvirhe"
#define TR_TURN_OFF_RECEIVER "Turn off receiver"
#define TR_STOPPING "Stopping..."
#define TR_MENU_SPECTRUM_ANALYSER "SPEKTRIANALYSERI"
+10
View File
@@ -964,6 +964,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Fréq."
#define TR_MENUTOOLS "OUTILS"
#define TR_MIC_RECORDER "Enregistreur micro"
#define TR_PUSH_TO_RECORD "Appuyer pour enregistrer"
#define TR_RECORD "Enregistrer"
#define TR_STOP "Arrêter"
#define TR_REC "REC"
#define TR_STARTING_IN "Démarrage dans"
#define TR_GET_READY "Tenez-vous prêt..."
#define TR_SAVED "Enregistré:"
#define TR_SAVE_AS "Enregistrer sous"
#define TR_OPEN_ERROR "Erreur ouverture"
#define TR_TURN_OFF_RECEIVER "Éteindre Récepteur."
#define TR_STOPPING "Arrêt..."
#define TR_MENU_SPECTRUM_ANALYSER "ANALYSEUR SPECTRE"
+10
View File
@@ -969,6 +969,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS "כלים וסקריפטים"
#define TR_MIC_RECORDER "מקליט מיקרופון"
#define TR_PUSH_TO_RECORD "לחץ להקלטה"
#define TR_RECORD "הקלט"
#define TR_STOP "עצור"
#define TR_REC "REC"
#define TR_STARTING_IN "מתחיל בעוד"
#define TR_GET_READY "...היה מוכן"
#define TR_SAVED ":נשמר"
#define TR_SAVE_AS "שמור בשם"
#define TR_OPEN_ERROR "שגיאת פתיחה"
#define TR_TURN_OFF_RECEIVER "כיבוי מקלט"
#define TR_STOPPING "...עוצר"
#define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSER"
+10
View File
@@ -966,6 +966,16 @@
#define TR_POWERMETER_ATTN "Att."
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS "TOOLS"
#define TR_MIC_RECORDER "Registr. microfono"
#define TR_PUSH_TO_RECORD "Premi per registrare"
#define TR_RECORD "Registra"
#define TR_STOP "Stop"
#define TR_REC "REC"
#define TR_STARTING_IN "Inizia tra"
#define TR_GET_READY "Preparati..."
#define TR_SAVED "Salvato:"
#define TR_SAVE_AS "Salva come"
#define TR_OPEN_ERROR "Errore apertura"
#define TR_TURN_OFF_RECEIVER "Spegni la RX"
#define TR_STOPPING "Fermando..."
#define TR_MENU_SPECTRUM_ANALYSER "ANALIZZATORE SPETTRO"
+10
View File
@@ -965,6 +965,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "周波数"
#define TR_MENUTOOLS "ツール"
#define TR_MIC_RECORDER "Mic recorder"
#define TR_PUSH_TO_RECORD "Push to record"
#define TR_RECORD "Record"
#define TR_STOP "Stop"
#define TR_REC "REC"
#define TR_STARTING_IN "Starting in"
#define TR_GET_READY "Get ready..."
#define TR_SAVED "Saved:"
#define TR_SAVE_AS "Save as"
#define TR_OPEN_ERROR "Open error"
#define TR_TURN_OFF_RECEIVER "受信機 電源OFF"
#define TR_STOPPING "停止中..."
#define TR_MENU_SPECTRUM_ANALYSER "スペクトラムアナライザー"
+10
View File
@@ -1008,6 +1008,16 @@
#define TR_POWERMETER_ATTN "감쇠"
#define TR_POWERMETER_FREQ "주파수"
#define TR_MENUTOOLS "도구"
#define TR_MIC_RECORDER "마이크 녹음기"
#define TR_PUSH_TO_RECORD "눌러서 녹음"
#define TR_RECORD "녹음"
#define TR_STOP "중지"
#define TR_REC "REC"
#define TR_STARTING_IN "시작까지"
#define TR_GET_READY "준비하세요..."
#define TR_SAVED "저장됨:"
#define TR_SAVE_AS "다른 이름으로 저장"
#define TR_OPEN_ERROR "열기 오류"
#define TR_TURN_OFF_RECEIVER "수신기 전원 끄기"
#define TR_STOPPING "중지 중..."
+10
View File
@@ -962,6 +962,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS "TOOLS"
#define TR_MIC_RECORDER "Microfoon recorder"
#define TR_PUSH_TO_RECORD "Druk om op te nemen"
#define TR_RECORD "Opnemen"
#define TR_STOP "Stop"
#define TR_REC "REC"
#define TR_STARTING_IN "Start over"
#define TR_GET_READY "Maak je klaar..."
#define TR_SAVED "Opgeslagen:"
#define TR_SAVE_AS "Opslaan als"
#define TR_OPEN_ERROR "Fout bij openen"
#define TR_TURN_OFF_RECEIVER "Turn off receiver"
#define TR_STOPPING "Stopping..."
#define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSER"
+10
View File
@@ -959,6 +959,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS "NARZĘDZIA"
#define TR_MIC_RECORDER "Nagrywarka mikrofonu"
#define TR_PUSH_TO_RECORD "Naciśnij aby nagrać"
#define TR_RECORD "Nagraj"
#define TR_STOP "Stop"
#define TR_REC "REC"
#define TR_STARTING_IN "Start za"
#define TR_GET_READY "Przygotuj się..."
#define TR_SAVED "Zapisano:"
#define TR_SAVE_AS "Zapisz jako"
#define TR_OPEN_ERROR "Błąd otwarcia"
#define TR_TURN_OFF_RECEIVER "Wyłącz odbiornik"
#define TR_STOPPING "Stopping..."
#define TR_MENU_SPECTRUM_ANALYSER "SPECTRUM ANALYSER"
+10
View File
@@ -967,6 +967,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS "FERRAMENTAS"
#define TR_MIC_RECORDER "Gravador de microfone"
#define TR_PUSH_TO_RECORD "Pressionar p/ gravar"
#define TR_RECORD "Gravar"
#define TR_STOP "Parar"
#define TR_REC "REC"
#define TR_STARTING_IN "Começa em"
#define TR_GET_READY "Prepare-se..."
#define TR_SAVED "Guardado:"
#define TR_SAVE_AS "Guardar como"
#define TR_OPEN_ERROR "Erro ao abrir"
#define TR_TURN_OFF_RECEIVER "Turn off receiver"
#define TR_STOPPING "Stopping..."
#define TR_MENU_SPECTRUM_ANALYSER "ANALISADOR ESPECTRO"
+10
View File
@@ -968,6 +968,16 @@
#define TR_POWERMETER_ATTN "Аттен"
#define TR_POWERMETER_FREQ "Частота"
#define TR_MENUTOOLS "ИНСТРУМЕНТЫ"
#define TR_MIC_RECORDER "Запись микрофона"
#define TR_PUSH_TO_RECORD "Нажмите для записи"
#define TR_RECORD "Запись"
#define TR_STOP "Стоп"
#define TR_REC "REC"
#define TR_STARTING_IN "Начало через"
#define TR_GET_READY "Приготовьтесь..."
#define TR_SAVED "Сохранено:"
#define TR_SAVE_AS "Сохранить как"
#define TR_OPEN_ERROR "Ошибка открытия"
#define TR_TURN_OFF_RECEIVER "Выкл приемник"
#define TR_STOPPING "Остановка..."
#define TR_MENU_SPECTRUM_ANALYSER "АНАЛИЗАТОР СПЕКТРА"
+10
View File
@@ -967,6 +967,16 @@
#define TR_POWERMETER_ATTN "Attn"
#define TR_POWERMETER_FREQ "Freq."
#define TR_MENUTOOLS "VERKTYG"
#define TR_MIC_RECORDER "Mikrofoninspelning"
#define TR_PUSH_TO_RECORD "Tryck för att spela in"
#define TR_RECORD "Spela in"
#define TR_STOP "Stoppa"
#define TR_REC "REC"
#define TR_STARTING_IN "Startar om"
#define TR_GET_READY "Gör dig redo..."
#define TR_SAVED "Sparad:"
#define TR_SAVE_AS "Spara som"
#define TR_OPEN_ERROR "Öppningsfel"
#define TR_TURN_OFF_RECEIVER "Stäng av mottagare"
#define TR_STOPPING "Stoppar..."
#define TR_MENU_SPECTRUM_ANALYSER "SPEKTRUMANALYSATOR"
+10
View File
@@ -963,6 +963,16 @@
#define TR_POWERMETER_ATTN "衰減器"
#define TR_POWERMETER_FREQ "頻率"
#define TR_MENUTOOLS "擴展工具"
#define TR_MIC_RECORDER "麥克風錄音"
#define TR_PUSH_TO_RECORD "按下錄音"
#define TR_RECORD "錄音"
#define TR_STOP "停止"
#define TR_REC "REC"
#define TR_STARTING_IN "倒計時"
#define TR_GET_READY "準備好..."
#define TR_SAVED "已保存:"
#define TR_SAVE_AS "另存為"
#define TR_OPEN_ERROR "打開失敗"
#define TR_TURN_OFF_RECEIVER "請關閉接收機"
#define TR_STOPPING "正在停止..."
#define TR_MENU_SPECTRUM_ANALYSER "頻譜儀"
+10
View File
@@ -967,6 +967,16 @@
#define TR_POWERMETER_ATTN "Атт"
#define TR_POWERMETER_FREQ "Част."
#define TR_MENUTOOLS "ІНСТРУМЕНТИ"
#define TR_MIC_RECORDER "Запис мікрофона"
#define TR_PUSH_TO_RECORD "Натисніть для запису"
#define TR_RECORD "Записати"
#define TR_STOP "Зупинити"
#define TR_REC "REC"
#define TR_STARTING_IN "Початок через"
#define TR_GET_READY "Готуйтесь..."
#define TR_SAVED "Збережено:"
#define TR_SAVE_AS "Зберегти як"
#define TR_OPEN_ERROR "Помилка відкриття"
#define TR_TURN_OFF_RECEIVER "Вимкнути приймач"
#define TR_STOPPING "Зупинка..."
#define TR_MENU_SPECTRUM_ANALYSER "АНАЛІЗАТОР СПЕКТРУ"
+13
View File
@@ -243,6 +243,19 @@
#define STR_MAIN_VIEW_9 currentLangStrings->STR_MAIN_VIEW_9
// Main Menu
#if defined(PDM_CLOCK)
#define STR_MIC_RECORDER currentLangStrings->STR_MIC_RECORDER
#define STR_PUSH_TO_RECORD currentLangStrings->STR_PUSH_TO_RECORD
#define STR_RECORD currentLangStrings->STR_RECORD
#define STR_STOP currentLangStrings->STR_STOP
#define STR_REC currentLangStrings->STR_REC
#define STR_STARTING_IN currentLangStrings->STR_STARTING_IN
#define STR_GET_READY currentLangStrings->STR_GET_READY
#define STR_SAVED currentLangStrings->STR_SAVED
#define STR_SAVE_AS currentLangStrings->STR_SAVE_AS
#define STR_OPEN_ERROR currentLangStrings->STR_OPEN_ERROR
#endif
#else // !COLORLCD
#if LCD_W >= 212
+13
View File
@@ -242,6 +242,19 @@ STR(MAIN_VIEW_8)
STR(MAIN_VIEW_9)
// Main Menu
#if defined(PDM_CLOCK)
STR(MIC_RECORDER)
STR(PUSH_TO_RECORD)
STR(RECORD)
STR(STOP)
STR(REC)
STR(STARTING_IN)
STR(GET_READY)
STR(SAVED)
STR(SAVE_AS)
STR(OPEN_ERROR)
#endif
#else // !COLORLCD
#if LCD_W >= 212