Grounded grid VHF front-end

Nov 09, 2024 Last reply: 1 year ago 48 Replies

We're designing some products around the RP2040, the Pi Pico processor.

Turns out that in some cases, it's easier to bit-bang an SPI interface than program an SPI engine. To fine-tune timings in 7 ns increments, we can use no-op instructions.

I wonder what's a safe c-language NOP single-clock operation that no compiler is smart enough to optimize out and doesn't add a bunch of loads and stores.

We're experimenting with that sort of timing on an oscilloscope. The GCC or whatever code timing tools don't work in this case.

Something like

gpio_put(FIRST_GPIO, 1); gpio_put(FIRST_GPIO, 0); gpio_put(FIRST_GPIO, 1); gpio_put(FIRST_GPIO, 0);

Makes the port pin change every 7 ns. That's astounding. So maybe a dummy port bang is my no-op. Just repeat what we just set it to.

Code in assembly. It's trivial to use assembly code with c in gcc.

Well, I heard that PICs are hard to program in C. I am not sure how small you mean. Smallest micros I have are MSP430 with 256 bytes of RAM and 8kB flash (but AFAICS C would work fine also on smaller ones, say 64 bytes of RAM and 1kB flash). Cheapest one is STM8 with 1kB RAM and 8kB flash. Smallest STM32 I have has 4kB RAM and 16kB flash, that is plenty for many programs (actually I run most test programs entiriely in RAM, so 4kB code+data). Of course one needs to work with hardware registers and understand hardware. Below is my UART receive routine (called from an interrupt handler) Actual data reception is very easy, first line gets the data from UART. Rest of routine deals with receive buffer (cyclic one):

void do_usart1_rx(void) { uint8_t c = USART1_DR; uint8_t head = i_buff.head; uint8_t cnt = (head - i_buff.tail)&BUFF_SIZE_MASK; /* Drop characters in case of buffer overflow */ if (cnt != BUFF_SIZE_MASK) { i_buff.buff[head] = c; head++; head &= BUFF_SIZE_MASK; i_buff.head = head; } }

Compiler generated assembly for STM32F103 (Cortex M3) is below:

.global do_usart1_rx .thumb .thumb_func .type do_usart1_rx, %function do_usart1_rx: @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 @ link register save eliminated. ldr r3, .L7 ldr r0, [r3] ldr r3, .L7+4 uxtb r0, r0 ldrb r2, [r3] @ zero_extendqisi2 ldrb r1, [r3, #1] @ zero_extendqisi2 uxtb r2, r2 subs r1, r2, r1 and r1, r1, #15 cmp r1, #15 beq .L1 adds r1, r3, r2 adds r2, r2, #1 and r2, r2, #15 strb r0, [r1, #2] strb r2, [r3] .L1: bx lr .L8: .align 2 .L7: .word 1073821700 .word .LANCHOR0 .size do_usart1_rx, .-do_usart1_rx

That is 17 executable instructions, 48 bytes of code and AFAICS only two zero-extend instructions could be dropped. So one could save

2 instructions, but the rest is very much forced by how the processor works (and by cyclic buffer logic). Compiled code for Cortex M0 is slightly different. The same C routine should work on STM8 (UART port address is different but data register should behave the same as on STM32) and GD32VF103 (Riscv core but peripherials compatible with STM32F103). Cyclic buffer logic could be copied and used on different processors, like MSP430 or AVR.

Test program for UART routines is 1192 bytes code and uses probably about 100 bytes of RAM (36 for global data, the rest is stack (I made a conservative guess for possible stack use)). That is about

1.8% of available code space and 0.5% of available RAM. Of the code 336 bytes is table of interrupt vectors (essentially its presence is forced by the hardware). The program includes setting clock to desired frequency, configuration of pins, UART and interrupt controller.

BTW, the interrupt handler itself is:

void usart1_isr(void) { uint32_t isr = USART1_SR; if (isr&USART_SR_RXNE) { do_usart1_rx(); } if (isr&USART_SR_TXE) { do_usart1_tx(); } }

which generates 32 bytes of code.

Cortex M have hardware 32-bit mutiplication and C compiler will expand inline most of 64-bit operations. MSP430 and STM8 needs support routines.

A gpio_put to an unused port pin would work, but we don't have any unused port pins.

I suspect that we could clear some internal device register, like in a timer or something, that we don't use. That would kill a clock or so and the compiler can't know that we don't use it.

My guy did

gpio up A = 3+5 gpio down

and of course the add takes zero time.

A = A+5

does take time, looks like four clocks.

Around 1985 I planned to build a Z80 machine, but then I got ZX Spectrum and there was no need to build it.

Well, I used to think "assembler requires comparable effort to C and is more efficient", but then I looked how much time both take and compared efficiency: assembler may be more efficient but efficient assember requires significantly more effort than C. One can write assembler in a way that saves effort, but then it tends to be less efficient than output of a good C compiler, and still takes a bit more effort than C. You may be used to assembler, but if you are used to both, then reading C is easier than reading assembler.

Anyway, I see no reason to use PIC-s, from normal sources I would have to pay more for them than I pay for STM32 and I see no special advantage of PICs.

BTW: It seems that there are few thousends of instructions in your code, AFAICS object code for such a program when compiled for something like STM32 would be of comparable size and C source would be smaller.

"so close to the hardware" does not change much. I have routines handling almost all buitin devices in STM32F030, most devices in STM32F103 and many devices in other models (STM has resonable compatibility between models so a single routine covers a lot of models). They directly access device register, configure them etc. I do not see how you could get "closer to the hardware". Writing them in C was significantly easier than writing them in assembler, using them is easier too. Assembler could give me some marginal efficiency gains which in most cases do no matter. And when extra efficiency really matteres I can rewrite _what is needed_ in assembler. For example, I wanted to know how far I can push nominally 400kHz I2C periferial in MSP430. Bottleneck was in I2C interrupt handler, so I did it in assembler. And the answer is: I2C worked reasonably well up to about 1.4MHz and almost worked at 2.4MHz (almost worked meant that data went back and forth, but was slightly mangled). But for normal use at 400kHz interrupt handler in C is fine.

BTW, the routines I mention are written by me and deliberatly are very close to the hardware. When I needed to use things unhanlded by my routines (mainly USB and ENC28J60) I used C routines that I fetched from the net. ENC28J60 routine actually was for AVR, but most of it was idependent of the CPU, so I just replaced the critical part and used the rest without changes. You can find changes that I made to 'libmaple' on the Github (however orginal authors do not develop it anymore and I also for most things am using different code).

Well, I am using libopencm3 from about 2015 (newer version have significant incompatibilities). Actually I am phasing out use of it, ATM I am using it mainly for definitions of various magic constants. I decided to write may own routines which as I wrote are closer to the hardware and smaller than routines in libopencm3 (which in turn seem to be smaller than routines in STM developement pack).

So one can use old code or enhance it when mainainers make undesirable changes. At some moment I intended to do signioficant developement on libmaple, but then decided that the design had significant problems, so did my routines in quite a different way.

As JM wrote, with gcc (and I think clang too) you can use imline asm like:

__asm__ volatile ("nop");

gcc is supposed to keep it. There is potential trouble with reordering, in general gcc can move statements to a different place when it thinks that the affect is the same. 'volatile' is supposed to prevent such movement (gcc still may move "normal" code around it, but access to GPIO should be volatile too and gcc will not reorder volatile operations).

Oscilloscope is good to see what happens at the pins. For observing internal time one can read systick register (I did not check RP2040, but I think it can be configured to change every clock).

I you want accuracy up to a single clock, then your code is brittle. RP2040 is a complex device and can do many things simultaneously. There is large potential for undesired interference. Basically, if you have code in a single bank of the RAM and your RP2040 is doing nothing else than bit-banging, then you are resonably safe. However, if SPI peripherial can do what you need, then IMO it is better solution, as SPI is dedicated hardware block which can operate without interference in parallel with other blocks.

I have no experience with radio design, but for curiosity looked at several early TV sets from soviet block. _All_ tube circuits that I found were common cathode in first stage, in late sixties apparently they converged on caskode configuration (two early designs had two common cathode stages). I found also few early transistor circunts, majority was common base, one was common emiter. I think reasonable guess is that tube characteristics make caskode preferable, while for transitors (at least those from sixties and seventies) common base seem to be better.

The CPUs are Alice and Bob. Alice is the manager, doing USB and Ethernet and blinking LEDs and stuff. It will run code out of the big serial flash, cached.

Bob is the realtime bare-metal processor. It will run one main loop and get one periodic interrupt at 120 KHz. It will bit-bang the SPI interface of the dreadful AD7699 8-channel ADC. Bob will run out of ram so should be perfectly repeatable.

We figure that the IRQ can read one ADC channel, and run for about 2 usec, which leaves 3/4 of Bob's compute power for the main loop.

Do not read the ADC data sheet unless you have good medical insurance that includes PTSD coverage.

The RP2040 does an instruction in 7 ns. I recall when Cray stunned the world with a big ECL liquid-freon-cooled computer with a 7 ns clock.

Join the Discussion

Have something to add? Share your thoughts — no account required.

Didn't find your answer?

Ask the community — no account required