Skip to content

Commit a7d730f

Browse files
3djcclaude
andauthored
feat: MEMS microphone software handling for TX15 and TX16SMK3 (#7328)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 13bba22 commit a7d730f

39 files changed

Lines changed: 1385 additions & 4 deletions

radio/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ set(SRC
464464
serial.cpp
465465
audio.cpp
466466
model_audio.cpp
467+
pdm_wav_recorder.cpp
467468
sbus.cpp
468469
input_mapping.cpp
469470
inactivity_timer.cpp

radio/src/boards/rm-h750/board.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ bool isBacklightEnabled();
208208
int audioInit();
209209
void audioConsumeCurrentBuffer();
210210

211+
#if defined(PDM_CLOCK)
212+
#include "pdm_software_driver.h"
213+
#endif
214+
211215
// Telemetry driver
212216
#define INTMODULE_FIFO_SIZE 512
213217
#define TELEMETRY_FIFO_SIZE 512
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/*
2+
* Copyright (C) EdgeTX
3+
*
4+
* Based on code named
5+
* opentx - https://github.com/opentx/opentx
6+
* th9x - http://code.google.com/p/th9x
7+
* er9x - http://code.google.com/p/er9x
8+
* gruvin9x - http://code.google.com/p/gruvin9x
9+
*
10+
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11+
*
12+
* This program is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License version 2 as
14+
* published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU General Public License for more details.
20+
*/
21+
22+
#include "board.h"
23+
24+
#include "hal/gpio.h"
25+
#include "stm32_gpio.h"
26+
#include "stm32_dma.h"
27+
#include "stm32_hal_ll.h"
28+
29+
#if defined(PDM_CAPTURE_DMA)
30+
31+
static constexpr uint32_t PDM_SAI_MCKDIV =
32+
PDM_SAI_KER_FREQ / PDM_CLOCK_FREQ;
33+
34+
static_assert(PDM_SAI_MCKDIV >= 1 && PDM_SAI_MCKDIV <= 63,
35+
"PDM_SAI MCKDIV out of range for SAI_xCR1_MCKDIV (6 bits)");
36+
37+
// Burst size: ~6 ms of PDM at 1.6 MHz (multiple of 32 required for word packing).
38+
static constexpr uint32_t PDM_BURST_BITS = 10080;
39+
static constexpr uint32_t PDM_BURST_WORDS = (PDM_BURST_BITS + 31) / 32;
40+
41+
static uint32_t pdmBurstBuf[PDM_BURST_WORDS];
42+
static uint8_t lastSoundLevel = 0;
43+
44+
// 40000 bytes at 1.6 MHz = ~25 ms ring; gives ~21 ms slack for audio-task stalls.
45+
static constexpr uint32_t PDM_RING_BYTES = 40000;
46+
static uint8_t pdmRingBuf[PDM_RING_BYTES] __DMA_NO_CACHE;
47+
static uint32_t pdmRingReadPos = 0;
48+
49+
// CIC integrator + comb state (used by pdmConvertToPCM, defined further down).
50+
// Declared here so pdmStart() can zero them at session boundaries — without a
51+
// reset the leftover integrator state from the previous session would emit a
52+
// DC transient on the first decoded burst.
53+
static int32_t cic_i1 = 0, cic_i2 = 0, cic_i3 = 0;
54+
static int32_t cic_p1 = 0, cic_p2 = 0, cic_p3 = 0;
55+
static uint32_t cic_bitsSeen = 0;
56+
57+
static bool pdmRunning = false;
58+
59+
void pdmStart()
60+
{
61+
if (pdmRunning) return;
62+
63+
// Per-session state reset. Must happen before the DMA starts producing data
64+
// so the audio-task path never sees a half-reset filter.
65+
cic_i1 = cic_i2 = cic_i3 = 0;
66+
cic_p1 = cic_p2 = cic_p3 = 0;
67+
cic_bitsSeen = 0;
68+
pdmRingReadPos = 0;
69+
lastSoundLevel = 0;
70+
71+
gpio_init_af(PDM_CLOCK, PDM_CLOCK_GPIO_AF, GPIO_PIN_SPEED_VERY_HIGH);
72+
gpio_init(PDM_DATA, GPIO_IN, GPIO_PIN_SPEED_VERY_HIGH);
73+
74+
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_SAI1EN);
75+
(void)READ_BIT(RCC->APB2ENR, RCC_APB2ENR_SAI1EN);
76+
77+
SAI_Block_TypeDef* block = PDM_SAI_BLOCK;
78+
79+
CLEAR_BIT(block->CR1, SAI_xCR1_SAIEN);
80+
while (READ_BIT(block->CR1, SAI_xCR1_SAIEN)) {}
81+
82+
block->CR1 = (0U << SAI_xCR1_MODE_Pos) // Master TX
83+
| (0U << SAI_xCR1_PRTCFG_Pos) // Free protocol
84+
| (4U << SAI_xCR1_DS_Pos) // 16-bit data
85+
| SAI_xCR1_NODIV // BCLK = ker_ck / MCKDIV
86+
| (PDM_SAI_MCKDIV << SAI_xCR1_MCKDIV_Pos);
87+
88+
block->CR2 = (1U << SAI_xCR2_FTH_Pos); // FIFO threshold = 1/4 full
89+
90+
block->FRCR = (15U << 0) // FRL = 15 (16-bit frame)
91+
| (7U << 8); // FSALL = 7
92+
block->SLOTR = (0U << 8) // NBSLOT = 0 -> 1 slot
93+
| (1U << 16); // SLOTEN slot 0
94+
95+
// Prime the FIFO so the block starts clocking immediately (no underrun).
96+
for (int i = 0; i < 4; ++i) block->DR = 0U;
97+
98+
SET_BIT(block->CR1, SAI_xCR1_SAIEN);
99+
100+
// TIM15 is free because FLYSKY_GIMBAL is OFF.
101+
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_TIM15EN);
102+
(void)READ_BIT(RCC->APB2ENR, RCC_APB2ENR_TIM15EN);
103+
104+
stm32_dma_enable_clock(PDM_CAPTURE_DMA);
105+
106+
PDM_CAPTURE_TIMER->CR1 = 0;
107+
PDM_CAPTURE_TIMER->PSC = 0;
108+
PDM_CAPTURE_TIMER->ARR = (PDM_CAPTURE_TIMER_FREQ / PDM_CLOCK_FREQ) - 1U;
109+
PDM_CAPTURE_TIMER->CNT = 0;
110+
PDM_CAPTURE_TIMER->EGR = TIM_EGR_UG; // load PSC/ARR
111+
PDM_CAPTURE_TIMER->SR = 0; // clear update flag
112+
PDM_CAPTURE_TIMER->DIER = TIM_DIER_UDE; // fire DMA request on every UEV
113+
114+
LL_DMA_DeInit(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM);
115+
116+
LL_DMA_InitTypeDef dmaInit;
117+
LL_DMA_StructInit(&dmaInit);
118+
dmaInit.PeriphRequest = PDM_CAPTURE_DMA_REQUEST;
119+
dmaInit.Mode = LL_DMA_MODE_CIRCULAR;
120+
dmaInit.Direction = LL_DMA_DIRECTION_PERIPH_TO_MEMORY;
121+
dmaInit.PeriphOrM2MSrcAddress = (uintptr_t)&PDM_DATA_GPIO_PORT->IDR;
122+
dmaInit.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT;
123+
dmaInit.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE;
124+
dmaInit.MemoryOrM2MDstAddress = (uintptr_t)pdmRingBuf;
125+
dmaInit.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT;
126+
dmaInit.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE;
127+
dmaInit.NbData = PDM_RING_BYTES;
128+
dmaInit.Priority = LL_DMA_PRIORITY_HIGH;
129+
LL_DMA_Init(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM, &dmaInit);
130+
131+
LL_DMA_EnableStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM);
132+
133+
PDM_CAPTURE_TIMER->CR1 |= TIM_CR1_CEN;
134+
135+
pdmRunning = true;
136+
}
137+
138+
void pdmStop()
139+
{
140+
if (!pdmRunning) return;
141+
142+
// Stop DMA pacing first so no further requests fire.
143+
PDM_CAPTURE_TIMER->CR1 &= ~TIM_CR1_CEN;
144+
PDM_CAPTURE_TIMER->DIER = 0;
145+
146+
// Disable the DMA stream and wait for it to actually stop. A subsequent
147+
// pdmStart() calls LL_DMA_DeInit which expects the stream idle.
148+
LL_DMA_DisableStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM);
149+
while (LL_DMA_IsEnabledStream(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM)) {}
150+
151+
// Disable the SAI block — this stops the PDM clock to the mic, putting it
152+
// into low-power mode.
153+
SAI_Block_TypeDef* block = PDM_SAI_BLOCK;
154+
CLEAR_BIT(block->CR1, SAI_xCR1_SAIEN);
155+
while (READ_BIT(block->CR1, SAI_xCR1_SAIEN)) {}
156+
157+
pdmRunning = false;
158+
}
159+
160+
static uint32_t pdmRingAvailable()
161+
{
162+
const uint32_t ndtr =
163+
LL_DMA_GetDataLength(PDM_CAPTURE_DMA, PDM_CAPTURE_DMA_STREAM);
164+
const uint32_t writePos = PDM_RING_BYTES - ndtr;
165+
if (writePos >= pdmRingReadPos) return writePos - pdmRingReadPos;
166+
return PDM_RING_BYTES - pdmRingReadPos + writePos;
167+
}
168+
169+
bool pdmCapture()
170+
{
171+
uint32_t avail = pdmRingAvailable();
172+
if (avail < PDM_BURST_BITS) return false;
173+
174+
// Skip ahead if DMA is about to overwrite unread data.
175+
if (avail > PDM_RING_BYTES - PDM_BURST_BITS) {
176+
const uint32_t target_avail = PDM_BURST_BITS + PDM_BURST_BITS / 2;
177+
const uint32_t skip = avail - target_avail;
178+
pdmRingReadPos = (pdmRingReadPos + skip) % PDM_RING_BYTES;
179+
}
180+
181+
static_assert(PDM_BURST_BITS % 32U == 0U,
182+
"PDM_BURST_BITS must be a multiple of 32");
183+
184+
uint32_t src = pdmRingReadPos;
185+
for (uint32_t w = 0; w < PDM_BURST_WORDS; ++w) {
186+
uint32_t acc = 0;
187+
for (uint32_t b = 0; b < 32; ++b) {
188+
const uint8_t s = pdmRingBuf[src];
189+
if (++src >= PDM_RING_BYTES) src = 0;
190+
acc = (acc << 1) | ((s >> PDM_DATA_GPIO_PIN) & 1U);
191+
}
192+
pdmBurstBuf[w] = acc;
193+
}
194+
195+
pdmRingReadPos = src;
196+
return true;
197+
}
198+
199+
bool pdmUpdateSoundLevel()
200+
{
201+
if (!pdmCapture()) return false;
202+
203+
// Ones-density of the PDM bitstream approximates signal amplitude.
204+
uint32_t ones = 0;
205+
for (uint32_t w = 0; w < PDM_BURST_WORDS; ++w) {
206+
ones += __builtin_popcount(pdmBurstBuf[w]);
207+
}
208+
209+
const uint32_t total = PDM_BURST_WORDS * 32U;
210+
const int32_t mid = (int32_t)(total / 2U);
211+
int32_t dev = (int32_t)ones - mid;
212+
if (dev < 0) dev = -dev;
213+
214+
uint32_t level = (uint32_t)dev * 200U / total;
215+
if (level > 100U) level = 100U;
216+
lastSoundLevel = (uint8_t)level;
217+
return true;
218+
}
219+
220+
uint8_t pdmGetSoundLevel()
221+
{
222+
return lastSoundLevel;
223+
}
224+
225+
// ---------------------------------------------------------------------------
226+
// PDM -> PCM conversion: 3rd-order CIC decimator (R = PDM_PCM_DECIMATION).
227+
//
228+
// Structure (Hogenauer):
229+
// integrator ×3 at 1 MHz -> down-sample by R -> comb ×3 at 1 MHz / R
230+
//
231+
// Integrators run on 1-bit input (0 or 1), so for R=64 and N=3 the worst-case
232+
// magnitude that has to be representable is R^N = 262 144 — fits easily in
233+
// int32_t. 2's-complement wrap-around through the integrators is intentional
234+
// and cancels in the comb stage.
235+
//
236+
// DC gain of the filter is R^N. Silence (50/50 bitstream) therefore sits at
237+
// R^N / 2, and the useful signal swing is ±R^N / 2. We subtract the DC offset
238+
// and scale so that full swing maps to the int16 range.
239+
// ---------------------------------------------------------------------------
240+
241+
// PDM_POST_GAIN_SHIFT is defined in pdm_software_driver.h so trimSilence
242+
// can scale its threshold from the same value.
243+
244+
// CIC integrator + comb state is declared near the top of the file (so
245+
// pdmStart() can reset it on session boundaries).
246+
247+
uint32_t pdmConvertToPCM(int16_t* pcm, uint32_t max)
248+
{
249+
if (!pcm || max == 0) return 0;
250+
251+
constexpr uint32_t R = PDM_PCM_DECIMATION;
252+
constexpr int32_t G = (int32_t)(R * R * R);
253+
constexpr int32_t DC = G / 2;
254+
255+
// floor_log2(DC) - 14: ensures (c3 - DC) >> SCALE_SHIFT fits in int16.
256+
// Works for any R, not just powers of two.
257+
constexpr int SCALE_SHIFT = (31 - __builtin_clz((unsigned)(DC))) - 14;
258+
static_assert(SCALE_SHIFT >= 0, "Decimation too small to fit PCM range");
259+
260+
uint32_t produced = 0;
261+
const uint32_t totalBits = PDM_BURST_WORDS * 32U;
262+
263+
for (uint32_t idx = 0; idx < totalBits && produced < max; ++idx) {
264+
const uint32_t word = pdmBurstBuf[idx >> 5];
265+
const int32_t bit = (int32_t)((word >> (31U - (idx & 31U))) & 1U);
266+
267+
cic_i1 += bit;
268+
cic_i2 += cic_i1;
269+
cic_i3 += cic_i2;
270+
271+
if (++cic_bitsSeen == R) {
272+
cic_bitsSeen = 0;
273+
274+
const int32_t c1 = cic_i3 - cic_p1; cic_p1 = cic_i3;
275+
const int32_t c2 = c1 - cic_p2; cic_p2 = c1;
276+
const int32_t c3 = c2 - cic_p3; cic_p3 = c2;
277+
278+
int32_t s = (c3 - DC) >> SCALE_SHIFT;
279+
s <<= PDM_POST_GAIN_SHIFT;
280+
if (s > 32767) s = 32767;
281+
if (s < -32768) s = -32768;
282+
pcm[produced++] = (int16_t)s;
283+
}
284+
}
285+
286+
return produced;
287+
}
288+
289+
#endif // PDM_CAPTURE_DMA
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (C) EdgeTX
3+
*
4+
* Based on code named
5+
* opentx - https://github.com/opentx/opentx
6+
* th9x - http://code.google.com/p/th9x
7+
* er9x - http://code.google.com/p/er9x
8+
* gruvin9x - http://code.google.com/p/gruvin9x
9+
*
10+
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11+
*
12+
* This program is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License version 2 as
14+
* published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU General Public License for more details.
20+
*/
21+
22+
#pragma once
23+
24+
#include <stdint.h>
25+
26+
// Decimation factor: output rate = PDM_CLOCK_FREQ / PDM_PCM_DECIMATION.
27+
// R=100, PDM_CLOCK=1.6 MHz → SRC_RATE=16000 Hz exactly (no resampler needed).
28+
#define PDM_PCM_DECIMATION 100
29+
30+
// Post-CIC gain (left shift before int16 saturation; +6 dB per step).
31+
// Default calibrated at 4. Override per target in hal.h before this header
32+
// is reached. trimSilence() in pdm_wav_recorder.cpp scales its threshold
33+
// from this same value, so changing it keeps trim in sync automatically.
34+
#ifndef PDM_POST_GAIN_SHIFT
35+
#define PDM_POST_GAIN_SHIFT 5
36+
#endif
37+
38+
// Bring up SAI clock + TIM15 + DMA capture and reset filter state.
39+
// Idempotent. Call when entering a context that needs the mic.
40+
void pdmStart();
41+
// Stop the DMA pacer, disable SAI block (the PDM clock to the mic stops).
42+
void pdmStop();
43+
bool pdmUpdateSoundLevel();
44+
uint8_t pdmGetSoundLevel();
45+
bool pdmCapture();
46+
uint32_t pdmConvertToPCM(int16_t* pcm, uint32_t max);

0 commit comments

Comments
 (0)