Can anyone give me a "for dummies" explanation of how to speed up the ADC a bit on a generic STM32F103 board?
I'm building a generator monitoring system and want to sample the 60 Hz output... I can probably make it work okay with the default sample rate, but if I could get a little improvement like 2x speed increase it would be great. I don't need to go nuts with DMA and multi-mHz sampling, but it seems like there might be some room for improvement by changing the ADC clock prescaler, I just can't figure out how to do it.
I've spent hours Googling and a lot of what I'm finding seems to revolve around a "adc_set_sample_rate" function, but when I add that to my code the compiler says it's undefined and I can't find what library it's supposed to be in! There also seem to be constants that go with it like "ADC_SMPR_1_5" but I can probably fake those if necessary.
I'm using the STMicroelectronics board defs with board = "Generic STM32F1 series" and board part number = "BluePill F103CB (or C8 with 128K)". Libraries I'm currently using are Adafruit_GFX, MCUFRIEND_kbv, TimerInterrupt_Generic, and ISR_Timer_Generic.
Just how much do you want to oversample. Nyquist's theorem states that a periodic signal must be sampled at more than twice the highest frequency component of the signal. In practice, because of the finite time available, a sample rate somewhat higher than this is necessary. A sample rate of 4 per cycle at oscilloscope bandwidth would be typical.
ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 1, ADC_SampleTime_71Cycles5); Changing the sampling will possibly cause accuracy problems in the results. I believe a 40Mhz sampling clock is max.
Using "analogRead() and the STM Arduino STM32duino core, the bluepill ADC conversion time is about 7 microseconds or nominally about 140k samples/second. It's hard to imagine how that isn't fast enough for power line monitoring.
If you have a ADC sampling rate problem it's almost certainly somewhere else in your code thus speeding up the ADC per se isn't going to help. Given your use of the Adafruit_GFX library, my guess is that you're refreshing the display in such a way that it is blocking ADC sampling.
Edit to add: As per the post #5 below, this speed was measured with the STM32duino core. The STM official core is indeed much slower at 63.30 microseconds using compiler default options.
Leading on from that comment by MrMark, if you are simply doing an analogRead() in the loop(), you may be better attempting to move it to a timer driven ISR. This may help if no other of your functions suspend interrupts for protracted periods.
Well I've been fooling around and gotten some very interesting results. I wrote a super-basic test sketch which is attached... all it does is 100 a/d conversions and reports how long it took.
Using the STMicroelectronics board definitions and the fastest compiler option (settings below and attached as "STBoardSettings.png", it takes about 5645 microseconds to do the 100 conversions, so 56.45 uSec per (including the loop instructions but those only take about 1 uSec, I checked).
Using the "stm32duino" board definitions, it's... faster. A lot faster: it does the 100 conversions in 698 microseconds, which works out to about 7 uSec per. Almost 10 times faster! If anyone's interested, the settings I used with that board def are below and attached as "DuinoBoardSettings.png".
The crummy thing about this is I can't use the stm32duino board defs because for the LCD I'm using the "mcufriend_kbv" library and that lib doesn't play nice with the non-ST board definitions (the compiler throws an error).
So guess the answer is to try and dig and figure out why it's so slow with the ST board def but I don't even know where to start on that.
6v6gt:
Leading on from that comment by MrMark, if you are simply doing an analogRead() in the loop(), you may be better attempting to move it to a timer driven ISR. This may help if no other of your functions suspend interrupts for protracted periods.
Yeah, that's exactly what I'm doing! Full (and by no means done yet) sketch attached if anyone's interested.
gilshultz:
Just how much do you want to oversample. Nyquist's theorem states that a periodic signal must be sampled at more than twice the highest frequency component of the signal. In practice, because of the finite time available, a sample rate somewhat higher than this is necessary. A sample rate of 4 per cycle at oscilloscope bandwidth would be typical.
ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 1, ADC_SampleTime_71Cycles5); Changing the sampling will possibly cause accuracy problems in the results. I believe a 40Mhz sampling clock is max.
The problem is I think the Nyqust thing only applies if you're using a lowpass filter to reconstruct the signal. If you think about sampling a sine wave with only two samples, if you plot the unprocessed sampled data you'll get a square wave... nothing like the original input waveform.
To accurately get the nuances of a waveform's shape (even just min and max on a sine wave) you need more samples... how many depends on what you're trying to achieve. I've actually struggled with what is the right number for what I'm doing, but things I'm reading online indicate that 20-25 samples per cycle is enough to be able to measure RMS voltage pretty accurately. I can get about 70 with my current code so it may be okay the way it is... maybe I should check.
elaw:
Yeah, that's exactly what I'm doing! Full (and by no means done yet) sketch attached if anyone's interested.
I see. It is ALL interrupt driven.
The normal rule is that ISRs should small and quick to execute. All that screen handling code will surely block and would probably be better moved to the loop.
It looks like the code to read the ADC is not currently active but I guess that you will be reading the ADC far more often that you will be writing to the display so the ISR should simply prepare the data to be written to the screen and the actual formatting, writing etc. is done in the loop.
Well... I've never been one to follow convention when coding!
But the program does work as I intend it to. Every 1 second the interrupt fires, it reads all the various parameters (some are faked right now because I don't have any of the analog circuitry built but if you look at line 224 you'll see the code that samples the generator output values is active), and displays them. It does nothing for the remainder of that second, then the interrupt fires again and it repeats.
Later I might make things work more asynchronously... I've been thinking of trying to move this to an ESP32 or something so it could have network connectivity and you could view the measured parameters in a browser. But I'd still want the LCD to work - it took a lot of fiddling to get this LCD (which has a parallel interface) working on the STM32, and I'm not sure I want to repeat that effort.
If anyone's interested, I did take a short video of the thing running: Youtube video - it's not fancy but it's good enough for its intended purpose.
OK. But if you are reading the data and refreshing the screen with new data in a cycle once every second then you don't need interrupts at all. I got that impression from your first post that you were working close to the limits of the ADC (which is many kHz).
Anyway, I had a quick look at the video and it is nice clean display so clearly it does work.
You're right! Really the only reason I'm using interrupts is so I can get an accurate "run time" value. Although as I type this I'm thinking the STM32 has an RTC in it (right?) so it probably would make more sense to use that.
On the ADC topic, I honestly figured maybe it was one of the libraries I'm using (like the timer-interrupt one) that was making the A/D run slow. I was pretty shocked when I wrote the test sketch with no libraries at all and it was still slow!
elaw, I've confirmed that the "analogRead()" conversion speed is about 7 microseconds with the STM32duino core and around 63 microseconds with the STM official core.
I seem to recall some discussion about this on the https://www.stm32duino.com/ once upon a time, but can't immediately locate it. My understanding is the official STM core uses STM libraries behind the Arduino core libraries for portability across their family of processors and that results in some inefficiencies. It's kind of mind-boggling that it would be that much worse. In any case that forum is probably a better place to bring up this sort of issue as there is a deeper pool of experienced STM32 users there.
The Bluepill has a built in RTC. I'm using one with a satellite clock and have the RTC in the low power domain and powered with a supercap so it quickly recovers from any mains power glitches etc.
Instead of using interrupts, this construct or similar would, in this case, be better. It has the advantage that debugging could be easier because Serial.print() etc., which itself uses interrupts, cannot be used reliably in the context of an ISR.
void loop() {
static uint32_t lastCycleAtMs = millis() ;
if ( millis() - lastCycleAtMs > 1000UL ) { // 1 second
// do stuff - in your case, call TimerHandler0()
lastCycleAtMs = millis() ;
}
}
MrMark:
elaw, I've confirmed that the "analogRead()" conversion speed is about 7 microseconds with the STM32duino core and around 63 microseconds with the STM official core.
I seem to recall some discussion about this on the https://www.stm32duino.com/ once upon a time, but can't immediately locate it. My understanding is the official STM core uses STM libraries behind the Arduino core libraries for portability across their family of processors and that results in some inefficiencies. It's kind of mind-boggling that it would be that much worse. In any case that forum is probably a better place to bring up this sort of issue as there is a deeper pool of experienced STM32 users there.