bring DAC sample rate closer to 48kHz#500
Conversation
The DAC is driven by the I2S peripheral. The STM32F72 reference manual describes the formula for the sample rate of the I2S in PCM mode with MCLK enabled on pg. 991. To have 4 channels outputting at the desired 48kHz, our I2S sample frequency must be 48kHz*4=192kHz. To achieve this, we need to set PLLI2SN and PLLI2SR such that a divider can be found that gets the sample rate as close to 192kHz as possible. The best values are N=344 and R=2, which results in an actual sample rate of 191.964kHz, so a per-output sample rate of 47.992kHz
|
thank you for digging into this! paging @trentgill for review |
|
looks good! there is a kind of cursed beauty in seeing the amount of work / investigation / research you did to change a single constant — such is the embedded engineer’s life! this would be a great case study to show to a manager of software engineers to explain why lines of code is a ridiculous metric for performance. i don’t have time to test it myself, but your testing methodology looks sound. regarding release & breaking changes, this feels like the only challenge. we do have a few other small changes/fixes (including clock details from @evnoj ) but little that feels like a compelling reason to upgrade. i’m hesitant to add this fix, knowing that it will cause friction for folks who do rely on nb_drumcrow for perhaps no benefit. what are you thoughts on this @tehn ? @evnoj thanks for digging in here! |
|
my impulse is to merge it. for anyone using nb_drumcrow, only a small fix to that script is needed. but to me there is likely a much greater number of people impacted by the timing issues who are just navigating their own scripts, for example i'd really like this script to be accurate: i understand it's a hassle to build another release, however. in any case, i feel like it's worth merging. |
This PR brings the output sample rate as close to 48kHz as I believe is possible. Currently, the sample rate of the outputs is not actually 48kHz, it it is 46.875kHz. This is caused by the clock configuration for the I2S peripheral that drives the DAC. The effect is that the rate of change of output voltage is slower than expected (a.k.a. it takes longer for a voltage to reach its target than expected). This is because
lib/slopes.h:8uses a sample rate of 48kHz to calculate the number of samples per ms, andlib/slopes.c:112uses this value to calculate the per-sample voltage delta.LLM disclosure
This issue was initially found by an LLM, when I was hacking on the firmware. I couldn't figure out why I was unable to accurately change voltage over time at a specified speed by calculating the delta that should be applied per sample. This calculation relied on the sample rate being 48kHz, but the voltage's rate of change was always slightly slower than expected.
This issue went significantly deeper into the workings of a microcontroller than I was familiar with, so I have spent a lot of time learning about clocking on microcontrollers, reading the STM32F72 manual, and reading the crow code and HAL library code. I have asked an LLM questions about the microcontroller and about specifics of how crow uses the I2S peripheral. I've tried very hard to ensure that I understand the issue and factors at play, and come up with a good solution, but this is new territory for me and I apologize if I've missed something or not made sense.
This report and all code is entirely human written. Sorry if it is too verbose, I wasn't sure what parts I could gloss over and didn't want to make assumptions.
I understand if you are uninterested in engaging with this due to the LLM assistance, if that is the case feel free to close this.
explanation
To achieve 48kHz on each of the 4 outputs, we need to be sending I2S frames (where a frame is a sample for one output) at$4\ channels*48kHz=192kHz$ .
The STM32F72 reference manual describes the sample frequency (frequency at which a frame is generated) on pg. 991. This is the formula for PCM mode with
MCKOEenabled (which is configured inll/dac8565.cviaI2S_STANDARD_PCM_SHORTandI2S_MCLKOUTPUT_ENABLE):We can consider$(2 \times \text{I2SDIV}) + \text{ODD}$ as a single term, an integer divider in the range 4-511, simplifying the sample rate formula:
The PLL multiplier ($N$ ) and divider ($R$ ) for the PLLI2S clock is set in
HAL_I2S_MspInit()withinll/dac8565.c:And the input to the PLLI2S clock is the either the HSE or HSI, divided by PLLM (see the clock tree on pg. 131 of the manual). This is set in
ll/system.c:With an HSE of 8MHz and a PLLM of 8, PLLI2S receives a 1MHz clock. Using PLLI2SN = 384 and PLLI2SR = 2, the PLLI2S output clock is$1\text{MHz} \times 384 \div 2 = 192\text{MHz}$
So, to achieve our desired sample rate of 192kHz, we must find a valid divider that plugs into our formula given$F_{\text{I2SxCLK}} = 192\text{MHz}$ . The HAL allows us to specify our desired $Fs$ , and it will set the divider as close as it can. This is done in
ll/dac8565.c:This calculation is at
STM32F7xx_HAL_Driver/Src/stm32f7xx_hal_i2s.c:354(since we have MCLK enabled, and ourDataFormatis 24B):i2sclkis 192,000,000packetlengthis 32AudioFreqis 192,000The multiplication by 10, adding 5, and dividing by 10 rounds to the nearest integer. Without the rounding, the simplified formula is:
Plugging in our values:
Spread across four outputs, the sample rate of each output is 187.5kHz / 4 = 46.875kHz
fix
The HAL is already finding the divider that gets the closest possible sample rate to 192kHz, given the$F_{\text{I2SxCLK}}$ of 192MHz. We want to find the value of $F_{\text{I2SxCLK}}$ for which an integer divider in the range 4-511 can be found that gets the result of this calculation as close as possible to 192000:
Where$F_{\text{I2SxCLK}} = \frac{N}{R} \times 10^6$ , $N$ is an integer 50-432 and $R$ is an integer 2-7.
This lua script simply tests every possible value to find the best combination of N and R:
output:
So we cannot land on 192kHz exactly, but we can get very close. The actual output sample rate we can achieve is$191.964 \text{kHz} / 4 = 47.992 \text{kHz}$
This is the same sample rate that Mutable Instruments Rings is able to hit.
testing
I made two lua scripts to verify the issue and test its resolution. One tests error with long envelopes by measuring elapsed time, and the other measures audio-rate frequencies and requires using a tuner/scope. Given the expected sample rate of 48000, and the actual sample rate of 46875, we'd expect error in the tests of$(48000-46875)/48000 \times 100 = 2.34$ %
This is a script that measures the actual time it takes for the voltage to reach its target with several different durations:
This is the output of that script on my crow w/ the current fw:
The errors are near the expected error of 2.34%
This is the output with the updated clock configuration:
The errors are now close to 0%
This is a script that sets each of the 4 outputs to an audio-rate cycle, they should be plugged into a tuner/scope to measure their actual frequency:
My measurements:
final notes
Fixing this issue may impact scripts which have compensated for the sample rate error. The only one I know of is nb_drumcrow, which has this line that multiplies the cycle time by
1.023996(close to the 2.34% sample rate error) to achieve the expected frequency:Let me know if you have any questions, thoughts, or concerns. And I get it if you're not interested in changing the MCU clock configuration for a module that people haven't had a problem with for years and is no longer in production.