From 58112fc49a23de08e40dab578e23188f65089763 Mon Sep 17 00:00:00 2001 From: MikeTeachman Date: Sat, 28 Jan 2023 19:16:39 -0800 Subject: [PATCH] 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 --- ports/stm32/Makefile | 7 + ports/stm32/boards/plli2svalues.py | 213 +++++++++++++++++++++++++++++ ports/stm32/machine_i2s.c | 48 ++++++- 3 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 ports/stm32/boards/plli2svalues.py diff --git a/ports/stm32/Makefile b/ports/stm32/Makefile index 0bae26f2f1..a1449e7181 100644 --- a/ports/stm32/Makefile +++ b/ports/stm32/Makefile @@ -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 $@" diff --git a/ports/stm32/boards/plli2svalues.py b/ports/stm32/boards/plli2svalues.py new file mode 100644 index 0000000000..8ff7f0ff8f --- /dev/null +++ b/ports/stm32/boards/plli2svalues.py @@ -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 ] ") + 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() diff --git a/ports/stm32/machine_i2s.c b/ports/stm32/machine_i2s.c index b3e1cee34f..dbee81bb39 100644 --- a/ports/stm32/machine_i2s.c +++ b/ports/stm32/machine_i2s.c @@ -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) {