Quantcast
Viewing all articles
Browse latest Browse all 4910

General • PIO hardware bug: Instruction-dependent sub-cycle jitter?

Some background first: I did a Pico project that outputted stereo audio directly to GPIO using a mixture of PWM and delta-sigma modulation, using PIO. During development, I discovered that some PIO programs resulted in audible distortion, whereas others were clean. At the time, I simply avoided the "bad" programs and stuck with the clean ones, but I could not explain what property of the bad programs was causing the distortion.

Later, I made a very short proof-of-concept that reproduced the buggy behavior. I will include it here.

The Pico I discovered the bug on was from early 2021 (RP2-B1), and had experienced but apparently survived a certain "12 volt incident", so at first I thought the bug could have been caused by a bad silicon batch or the incident. A few weeks ago, I bought some new Picos (RP2-B2), and the first UF2 I flashed to one of them was the POC for this bug, and that new Pico exhibited the same behavior. So now I am confident that other people can reproduce this bug and hopefully help figure out what is going on.

PIO source:

Code:

.program berrybug.wrap_targetout   pins,1 [7]out   pins,1 [3]out   pins,1 [1]out   pins,1out   pins,1.wrap% c-sdk {static inline void berrybug_program_init( PIO pio, uint sm, uint offset, uint pin ) {pio_sm_config c = berrybug_program_get_default_config(offset);sm_config_set_out_pins( &c, pin, 1 );sm_config_set_out_shift( &c, false, true, 30 ); // shift_right=false, autopull=true, pull_threshold=30sm_config_set_clkdiv_int_frac( &c, 1, 0 );pio_gpio_init( pio, pin );pio_sm_set_consecutive_pindirs( pio, sm, pin, 1, true );pio_sm_init( pio, sm, offset, &c );pio_sm_set_enabled( pio, sm, true );}%}
C source:

Code:

#include "pico/stdlib.h"#include "hardware/pio.h"#include "main.pio.h"#include "hardware/pll.h"#include "hardware/clocks.h"#include "hardware/structs/pll.h"#include "hardware/structs/clocks.h"#define BERRY_PIO pio1#define BERRY_SM  2#define BERRY_PIN 10int __not_in_flash_func(main)(void) {//set_sys_clock_pll(1764000000uLL,5,2); // 176.4 MHzgpio_init(BERRY_PIN);gpio_set_dir(BERRY_PIN, GPIO_OUT);uint berrybug_offset = pio_add_program( BERRY_PIO, &berrybug_program );berrybug_program_init( BERRY_PIO, BERRY_SM, berrybug_offset, BERRY_PIN );const uint32_t PushVal = 0x84210840uL; // set all 6 bits with duration value 8const uint32_t Vals[3] = {PushVal,  // 1633 mV~PushVal, // 1621 mV0,        //    0 mV};unsigned i = 0;const unsigned long Period = (6 * 125000000uL) / (16*6); // 6-second period at 125 MHzunsigned long Count = Period;for (;;) {pio_sm_put_blocking( BERRY_PIO, BERRY_SM, Vals[i] );Count --;if ( Count == 0 ) {Count = Period;i ++;if ( i == 3 ) {i = 0;}}}return 0;}
PIO program overview:
The PIO SM is driving one GPIO, processing each word from the TXFIFO as 6 groups of 5 bits each, autopulling every 30 bits consumed. Each 5-bit group drives the GPIO directly using 5 OUT PINS,1 ([n]) instructions, where the instruction's delay (or lack of one) makes the 5 bits drive the pin for 8, 4, 2, 1, and 1 clock cycle respectively. The sum of these durations is 16 clock cycles, so each 5-bit group takes 16 cycles to process, and can set the total on time of the GPIO from 0 to 16 cycles inclusive (sort of like PWM, but the pin may transition more than 2 times per "period").

C program overview:
The C program sets up the GPIO pin, PIO, and starts execution of the PIO. Then, in an infinite loop, it pushes into the TXFIFO 30-bit words whose 5-bit groups are: 0b10000 for 6 seconds, then 0b01111 for 6 seconds, then 0b00000 for 6 seconds.

Expected non-buggy behavior:
When the PIO is processing the value 0b10000, it should output 1111111100000000 to the pin across the 16-cycle period. When the PIO processes 0b01111, it should output 0000000011111111. When it processes 0b00000, it obviously should output 0 for all 16 cycles. The first two values should both result in the GPIO being on with the same 50% duty cycle.

Observed behavior:
When configured to use pio1 SM2 to output to GPIO 10, if I use a multimeter and measure the voltage between GPIO 10 and GND, then when the PIO is processing 0b10000, I see a 1633 mV reading, and 0b01111 results in 1621 mV reading. This 12 mV difference is consistent. But if I instead choose to use pio0, SM0, outputting to GPIO 0, then the voltage difference is 0 mV (1 mV being the precision limit of my cheap multimeter at that voltage scale).

What on earth is going on? And why does the behavior depend on the choice of PIO/SM/pin?

If my blind guess is correct and it really is jitter (i.e. the pin output swings from 1 to 0 late in the 0b10000 case, or from 0 to 1 late in the 0b01111 case, or both), then some quick arithmetic ((12 mV/3300 mV) * 16 cycles = 0.05818181... clock cycles @ 125 MHz = 0.46545454... ns) suggests PIO is doing a 0/1 or 1/0 transition ~1/17 cycles earlier or later in one case than the other, and that someone would need a 2 Gsps oscilloscope or better to see it (I don't have any oscilloscope). Does anyone with a scope care to have a look?

In the POC, I included a "set_sys_clock_pll" function call commented out. Uncommenting it out increases the Pico clock frequency from 125 MHz to 176.4 MHz, and results in a greater voltage swing. This suggests the jitter is some fixed duration and doesn't scale down with increased clock frequency.

I've tried replacing the delays with the equivalent number of NOPs following the OUT PINS instructions, and the bug persists. The rule of thumb I followed to hopefully avoid writing "bad" PIO programs during development was "don't use OUT PINS". That project had more things running on the Pico (USB, multicore, 2 PIO SMs), but I'm confident I haven't tested everything relevant to this bug, so please test whatever variants you can think of (e.g. multiple SMs, both PIOs, more pin combinations, different instructions).

Thanks.

Statistics: Posted by BerryA — Tue Feb 20, 2024 1:50 pm



Viewing all articles
Browse latest Browse all 4910

Trending Articles