stm32/machine_i2s: Improve accuracy of SCK frequency.

Configures the I2S PLL to produce a frequency that the I2S clock generator
can use to create an optimal SCK frequency.  The I2S PLL configuration
table is automatically generated at build time.

Fixes issue #10280.

Signed-off-by: Mike Teachman <mike.teachman@gmail.com>
This commit is contained in:
MikeTeachman 2023-01-28 19:16:39 -08:00 committed by Damien George
parent f3889db265
commit 58112fc49a
3 changed files with 265 additions and 3 deletions

View File

@ -65,6 +65,7 @@ OPENOCD_CONFIG ?= boards/openocd_stm32f4.cfg
include stm32.mk
PLLVALUES = boards/pllvalues.py
PLLI2SVALUES = boards/plli2svalues.py
MAKE_PINS = boards/make-pins.py
BOARD_PINS = $(BOARD_DIR)/pins.csv
PREFIX_FILE = boards/stm32f4xx_prefix.c
@ -82,6 +83,7 @@ GEN_CDCINF_FILE = $(HEADER_BUILD)/pybcdc.inf
GEN_CDCINF_HEADER = $(HEADER_BUILD)/pybcdc_inf.h
GEN_PLLFREQTABLE_HDR = $(HEADER_BUILD)/pllfreqtable.h
GEN_PLLI2STABLE_HDR = $(HEADER_BUILD)/plli2stable.h
GEN_STMCONST_HDR = $(HEADER_BUILD)/modstm_const.h
GEN_STMCONST_MPZ = $(HEADER_BUILD)/modstm_mpz.h
CMSIS_MCU_HDR = $(STM32LIB_CMSIS_ABS)/Include/$(CMSIS_MCU_LOWER).h
@ -681,6 +683,11 @@ $(GEN_PLLFREQTABLE_HDR): $(PLLVALUES) | $(HEADER_BUILD)
$(ECHO) "GEN $@"
$(Q)$(PYTHON) $(PLLVALUES) -c -m $(CMSIS_MCU_LOWER) file:$(BOARD_DIR)/stm32$(MCU_SERIES)xx_hal_conf.h > $@
$(TOP)/extmod/machine_i2s.c: $(GEN_PLLI2STABLE_HDR)
$(GEN_PLLI2STABLE_HDR): $(PLLI2SVALUES) | $(HEADER_BUILD)
$(ECHO) "GEN $@"
$(Q)$(PYTHON) $(PLLI2SVALUES) -c -m $(CMSIS_MCU_LOWER) hse:$(BOARD_DIR)/stm32$(MCU_SERIES)xx_hal_conf.h pllm:$(BOARD_DIR)/mpconfigboard.h > $@
$(BUILD)/modstm.o: $(GEN_STMCONST_HDR)
$(HEADER_BUILD)/modstm_const.h: $(CMSIS_MCU_HDR) make-stmconst.py | $(HEADER_BUILD)
$(ECHO) "GEN stmconst $@"

View File

@ -0,0 +1,213 @@
"""
This program computes I2S PLL parameters for STM32
processors supporting an I2S PLL in the clock tree.
Those processors are listed below in the mcu_support_plli2s[] list.
"""
import re
from collections import namedtuple
class MCU:
def __init__(self, range_plli2sn, range_plli2sr):
self.range_plli2sn = range_plli2sn
self.range_plli2sr = range_plli2sr
mcu_default = MCU(range_plli2sn=range(50, 432 + 1), range_plli2sr=range(2, 7 + 1))
mcu_table = {"stm32f401xe": MCU(range_plli2sn=range(192, 432 + 1), range_plli2sr=range(2, 7 + 1))}
# list of stm32 processors that support an I2S PLL in the clock tree
mcu_support_plli2s = [
"stm32f405xx",
"stm32f401xe",
"stm32f407xx",
"stm32f411xe",
"stm32f412zx",
"stm32f413xx",
"stm32f427xx",
"stm32f429xx",
"stm32f439xx",
"stm32f446xx",
"stm32f722xx",
"stm32f733xx",
"stm32f746xx",
"stm32f756xx",
"stm32f767xx",
"stm32f769xx",
]
# The following function calculates the multiplier (plli2sn) and divider (plli2sr) parameters
# for the I2S PLL that leads to the best possible accuracy for the I2S sampling clock.
# This is done by creating a set of candidate parameters (plli2sn, plli2sr)
# and then determining which candidate parameters lead to a sampling clock frequency (Fs)
# that most closely matches the desired sampling clock frequency (I2S rate).
#
# A description of the clock tree helps to understand this function:
# The clock tree on a STM32 device is complex. A subset of the clock tree is used for I2S, as follows:
# 1. HSE clock is divided by the PLLM divider and feeds the I2S PLL (called PLLI2S in the STM32 clock tree).
# 2. The output frequency of the I2S PLL is controlled by two parameters, plli2sn and plli2sr.
# 3. The clock output of the I2S PLL is called PLLI2SCLK
# 4. PLLI2SCLK is gated into the I2S peripheral, where the name changes to I2SxCLK
# 5. I2SxCLK is an input to the I2S clock generator.
# 6. The output frequency of the I2S clock generator is controlled by
# two configuration parameters, I2SDIV and ODD.
# 7. The output of the I2S clock generator is the audio sampling frequency (Fs),
# which is used to create the signal at the I2S WS output pin.
#
# Example references:
# RM0090 Reference manual STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439
# - section 6.3.23 RCC PLLI2S configuration register (RCC_PLLI2SCFGR)
# - section 28.4.4 Clock generator
# RM0385 Reference manual STM32F75xxx and STM32F74xxx
# - section 5.3.23 RCC PLLI2S configuration register (RCC_PLLI2SCFGR)
# - section 32.7.5 Clock generator
#
# The calculations below mimic the fixed-point integer calculations in the STM32 I2S driver,
# in the function HAL_I2S_Init().
def compute_plli2s_table(hse, pllm):
plli2s = namedtuple("plli2s", "bits rate plli2sn plli2sr i2sdiv odd error")
plli2s_table = []
for bits in (16, 32):
for rate in (8_000, 11_025, 12_000, 16_000, 22_050, 24_000, 32_000, 44_100, 48_000):
plli2s_candidates = []
for plli2sn in mcu.range_plli2sn:
for plli2sr in mcu.range_plli2sr:
I2SxCLK = hse // pllm * plli2sn // plli2sr
if I2SxCLK < 192_000_000:
# compute I2S clock generator parameters: i2sdiv, odd
tmp = (((I2SxCLK // (bits * 2)) * 10) // rate) + 5
tmp = tmp // 10
odd = tmp & 1
i2sdiv = (tmp - odd) // 2
Fs = I2SxCLK / ((bits * 2) * ((2 * i2sdiv) + odd))
error = (abs(Fs - rate) / rate) * 100
plli2s_candidates.append(
plli2s(
bits=bits,
rate=rate,
plli2sn=plli2sn,
plli2sr=plli2sr,
i2sdiv=i2sdiv,
odd=odd,
error=error,
)
)
# sort based on error
plli2s_candidates_sorted = sorted(plli2s_candidates, key=lambda x: x.error)
# select the best candidate
plli2s_table.append(plli2s_candidates_sorted[0])
return plli2s_table
def generate_c_table(plli2s_table, hse, pllm):
print("// MAKE generated file, created by plli2svalues.py: DO NOT EDIT")
print("// This table is used in machine_i2s.c")
print(f"// HSE_VALUE = {hse}")
print(f"// MICROPY_HW_CLK_PLLM = {pllm} \n")
print("#define PLLI2S_TABLE \\")
print("{ \\")
for plli2s in plli2s_table:
print(
f" {{{plli2s.rate}, "
f"{plli2s.bits}, "
f"{plli2s.plli2sr}, "
f"{plli2s.plli2sn} }}, "
f"/* i2sdiv: {int(plli2s.i2sdiv)}, "
f"odd: {plli2s.odd}, "
f"rate error % (desired vs actual)%: {plli2s.error:.4f} */ \\"
)
print("}")
def search_header(filename, re_include, re_define, lookup, val):
regex_include = re.compile(re_include)
regex_define = re.compile(re_define)
with open(filename) as f:
for line in f:
line = line.strip()
m = regex_include.match(line)
if m:
# Search included file
search_header(m.group(1), re_include, re_define, lookup, val)
continue
m = regex_define.match(line)
if m:
# found lookup value
if m.group(1) == lookup:
val[0] = int(m.group(3))
return val
def main():
global mcu
# parse input args
import sys
argv = sys.argv[1:]
c_table = False
mcu_series = "stm32f4"
hse = None
pllm = None
while True:
if argv[0] == "-c":
c_table = True
argv.pop(0)
elif argv[0] == "-m":
argv.pop(0)
mcu_series = argv.pop(0).lower()
else:
break
if mcu_series in mcu_support_plli2s:
if len(argv) != 2:
print("usage: pllvalues.py [-c] [-m <mcu_series>] <hse in MHz> <pllm in MHz>")
sys.exit(1)
if argv[0].startswith("hse:"):
# extract HSE_VALUE from header file
(hse,) = search_header(
argv[0][len("hse:") :],
r'#include "(boards/[A-Za-z0-9_./]+)"',
r"#define +(HSE_VALUE) +\((\(uint32_t\))?([0-9]+)\)",
"HSE_VALUE",
[None],
)
if hse is None:
raise ValueError("%s does not contain a definition of HSE_VALUE" % argv[0])
argv.pop(0)
if argv[0].startswith("pllm:"):
# extract MICROPY_HW_CLK_PLLM from header file
(pllm,) = search_header(
argv[0][len("pllm:") :],
r'#include "(boards/[A-Za-z0-9_./]+)"',
r"#define +(MICROPY_HW_CLK_PLLM) +\((\(uint32_t\))?([0-9]+)\)",
"MICROPY_HW_CLK_PLLM",
[None],
)
if pllm is None:
raise ValueError(
"%s does not contain a definition of MICROPY_HW_CLK_PLLM" % argv[0]
)
argv.pop(0)
# Select MCU parameters
mcu = mcu_default
for m in mcu_table:
if mcu_series.startswith(m):
mcu = mcu_table[m]
break
plli2s_table = compute_plli2s_table(hse, pllm)
if c_table:
generate_c_table(plli2s_table, hse, pllm)
if __name__ == "__main__":
main()

View File

@ -33,6 +33,7 @@
#include "py/mphal.h"
#include "pin.h"
#include "dma.h"
#include "genhdr/plli2stable.h"
// Notes on this port's specific implementation of I2S:
// - the DMA callbacks (1/2 complete and complete) are used to implement the asynchronous background operations
@ -61,6 +62,13 @@ typedef enum {
BOTTOM_HALF
} ping_pong_t;
typedef struct _plli2s_config_t {
uint32_t rate;
uint8_t bits;
uint8_t plli2sr;
uint16_t plli2sn;
} plli2s_config_t;
typedef struct _machine_i2s_obj_t {
mp_obj_base_t base;
uint8_t i2s_id;
@ -98,12 +106,26 @@ STATIC const int8_t i2s_frame_map[NUM_I2S_USER_FORMATS][I2S_RX_FRAME_SIZE_IN_BYT
{ 2, 3, 0, 1, 6, 7, 4, 5 }, // Stereo, 32-bits
};
STATIC const plli2s_config_t plli2s_config[] = PLLI2S_TABLE;
void machine_i2s_init0() {
for (uint8_t i = 0; i < MICROPY_HW_MAX_I2S; i++) {
MP_STATE_PORT(machine_i2s_obj)[i] = NULL;
}
}
STATIC bool lookup_plli2s_config(int8_t bits, int32_t rate, uint16_t *plli2sn, uint16_t *plli2sr) {
for (uint16_t i = 0; i < MP_ARRAY_SIZE(plli2s_config); i++) {
if ((plli2s_config[i].bits == bits) && (plli2s_config[i].rate == rate)) {
*plli2sn = plli2s_config[i].plli2sn;
*plli2sr = plli2s_config[i].plli2sr;
return true;
}
}
return false;
}
// For 32-bit audio samples, the STM32 HAL API expects each 32-bit sample to be encoded
// in an unusual byte ordering: Byte_2, Byte_3, Byte_0, Byte_1
// where: Byte_0 is the least significant byte of the 32-bit sample
@ -294,6 +316,29 @@ STATIC bool i2s_init(machine_i2s_obj_t *self) {
HAL_GPIO_Init(self->sd->gpio, &GPIO_InitStructure);
}
// configure I2S PLL
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0};
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_I2S;
// lookup optimal PLL multiplier (PLLI2SN) and divisor (PLLI2SR) for a given sample size and sampling frequency
uint16_t plli2sn;
uint16_t plli2sr;
if (lookup_plli2s_config(self->mode == I2S_MODE_MASTER_RX ? 32 : self->bits, self->rate, &plli2sn, &plli2sr)) {
// match found
PeriphClkInitStruct.PLLI2S.PLLI2SN = plli2sn;
PeriphClkInitStruct.PLLI2S.PLLI2SR = plli2sr;
} else {
// no match for sample size and rate
// configure PLL to use power-on default values when a non-standard sampling frequency is used
PeriphClkInitStruct.PLLI2S.PLLI2SN = 192;
PeriphClkInitStruct.PLLI2S.PLLI2SR = 2;
}
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) {
return false;
}
if (HAL_I2S_Init(&self->hi2s) == HAL_OK) {
// Reset and initialize Tx and Rx DMA channels
if (self->mode == I2S_MODE_MASTER_RX) {
@ -306,13 +351,10 @@ STATIC bool i2s_init(machine_i2s_obj_t *self) {
self->hi2s.hdmatx = &self->hdma_tx;
}
__HAL_RCC_PLLI2S_ENABLE(); // start I2S clock
return true;
} else {
return false;
}
}
void HAL_I2S_ErrorCallback(I2S_HandleTypeDef *hi2s) {