I'm building a firmware to capture very short burst signals through the ADC on a STM32F401CC (Black Pill board).

I have a 100 Hz PWM output running on a different GPIO group

The ADC readout gets offset for about 1 ms every time the PWM timer channel fires. Since this is extremely hard to debug, I've added a signal graph directly on device - here's an example:

enter image description here

There's 17ms of ADC data between the two Xs, and the voltage range is around 0.3-2V. The distance between the notches changes as I change the PWM timer frequency.

Here's a shorter signal, "zoomed in":

enter image description here

Things I've already tried:

  • A different power supply
  • Disabling any non strictly necessary interrupt handlers
  • Lowering ADC sample rate
  • Sampling ADC for more cycles
  • Changing ADC clock
  • Scaling input level
  • Errata workarounds for ADC precision

The input is a pulled-down SFH309FA phototransistor on the A0 pin.

I'm using Rust/RTIC with the following ADC configuration:

let adc_pin = gpioa.pa0.into_analog();

let adc_config = AdcConfig::default()

let mut adc = Adc::adc1(dp.ADC1, true, adc_config);
adc.configure_channel(&adc_pin, Sequence::One, SampleTime::Cycles_3);

DMA Configuration:

let dma = StreamsTuple::new(dp.DMA2);
let dma_config = DmaConfig::default()

let transfer = Transfer::init_peripheral_to_memory(


let mut timer = dp.TIM2.counter_hz(&clocks);

Timer handler:

#[task(binds = TIM2, shared = [transfer], local = [timer])]
fn adcstart(mut cx: adcstart::Context) {
    cx.shared.transfer.lock(|transfer| {
        transfer.start(|adc| {

DMA completion interrupt handler:

#[task(binds = DMA2_STREAM0, shared = [transfer, adc_value, sample_counter, calibration_state, measurement], local = [adc_dma_buffer], priority = 3)]
fn dma(ctx: dma::Context) {
    let mut shared = ctx.shared;
    let local = ctx.local;

    let last_adc_dma_buffer = shared.transfer.lock(|transfer| {
        let (last_adc_dma_buffer, _) = transfer

    let value = *last_adc_dma_buffer;
    *local.adc_dma_buffer = Some(last_adc_dma_buffer);

    //....store value

PWM setup:

        let mut pwm = dp
            .pwm_hz(hal::timer::Channel4::new(gpiob.pb9), 200.Hz(), &clocks);
        pwm.set_duty(hal::timer::Channel::C4, 0);

The entire project is available at https://github.com/eugeny/shutterspeed2

I'm just starting out with embedded and would appreciate any pointers.

  • 1
    \$\begingroup\$ Read the errata sheet of that MCU. \$\endgroup\$ Dec 23, 2023 at 22:46
  • 1
    \$\begingroup\$ @EnricoMigliore the errata workarounds didn't have any effects, but I've narrowed it down to a PWM timer. I've temporarily disabled it and it solves the issue, but I'm still wondering if there's some obvious workaround I'm missing. \$\endgroup\$ Dec 24, 2023 at 9:30
  • 1
    \$\begingroup\$ So what if that is how it is? Your PWM signal edges coupling to ADC input through wiring? Or supplies sagging or ground bouncing on PWM edges? Show schematics and PCB layout design? \$\endgroup\$
    – Justme
    Dec 24, 2023 at 10:01
  • \$\begingroup\$ Errata sheets usually suggest, if present, a work around. You might have found a new bug. Ask you ST's support forum. \$\endgroup\$ Dec 24, 2023 at 12:52

1 Answer 1


You have stuff on your ADC that shouldn't be there. There seem to be different versions of STM32 black pill online, I don't see any schematic in your datasheet so I'll just use this one.

enter image description here

There are several ways noise can get into your ADC, for example:

  • Noise is added to the signal being sampled.

  • Noise on ADC voltage reference.

  • Contamination of ADC power supply or clock

  • User error (different reference voltage used for ADC and a ratiometric output sensor)

The point is that these all behave differently, so you can get information about the issue by observing ADC results.

ADC output is (Input Voltage)/(Reference Voltage).

So the first thing you should do is make a setup where you can reproduce the problem (looks like you did). Then locate VDDA/VREF+ and VSSA/VREF- on your board. Unfortunately they are not on the connector pins, but there is a convenient cap C12 which decouples VDDA so you can solder two bits of resistor legs on it, now you have access.

Now you can put a trimpot like 1kOhm, or just solder two resistors to make a voltage divider. Ratio should be about 1:10.

  • Short the ADC input to VSSA

  • Short the ADC input to VREF

  • Use the voltage divider to input VREF*0.1 into the ADC

  • Flip the voltage divider around to input VREF*0.9 into the ADC

Meanwhile, keep the source of the problem running (ie, the PWM timer). What we're interested in is the difference in ADC reading in the four above cases between PWM ON and PWM OFF. Normally the difference should be zero.

How to interpret the results:

All these tests feed the ADC with an input voltage that is proportional to VREF, so they ignore noise on VREF. If you still get noise on your readings, then the issue is not noise on VREF, but injection into your ADC input or internal crosstalk in the MCU. You can try other ADC inputs and compare. For example, after removing the phototransistor and pullup, if noise occurs on that ADC input but not on other inputs, suspect coupling into the trace.

Most likely your readings will be clean.

Next test: use ADC to acquire a stable voltage, like from a 1.5V AA cell. If your ADC readings are noisy while the input voltage is stable, it means the ADC reference voltage is noisy.

Because ADC output is (Input Voltage)/(Reference Voltage), noise on VREF manifests in ADC readings as roughly proportional to input voltage. Whereas noise added to the input is always the same no matter what the input voltage is. A good test is to vary the ADC input voltage (for example add another AA cell in series so you get 3V) and check if the noise is the same, or doubled.

Since the ADC reference is the supply voltage on that board, I'd expect it to be noisy: in this case you need to remove ferrite bead L1 and put a separate LDO or reference chip for your VREF.

Other stuff to consider:

Maybe the PWM controls something that influences your sensor reading. For example the PWM turns on something that uses power, which drops supply voltage, and your sensor has very low PSRR (ie, phototransistor and pullup). Or the PWM blinks a LED and the sensor is a phototransistor. This will be apparent by checking the sensor output with a scope.

Note a phototransistor with pull resistor has a much higher PSRR if the phototransistor is on top with pulldown resistor. This is because the phototransistor behaves as a current source, and the resistor turns the output current into a voltage referenced to either GND (pulldown) or VCC (pullup).

Also if your phototransistor uses a pullup from VCC and not VREF, which is probably the case since the board has no VREF pin... or you use any ratiometric sensor whose output is proportional to VCC... then what you're doing is measuring VCC using the ADC's VREF. If these voltages are not equal, and they are not because there's a filter on VREF, then your ADC readings will be contaminated by (VCC-VREF) noise. Basically all sensors whose output depends on supply voltage need to be powered from ADC VREF. It is possible to use another clean supply from another LDO, but due to tolerances and drift this introduces a measurement error. Whereas if your sensor is a resistive divider between VREF and GND, the ADC will get an accurate resistor ratio no matter what VREF actually is.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.