STM32 - Parallel bit-stream of multiple slaves  - Page 1
Forum section: electronics / general

author: Kneidl
author_rank: Contributor
timestamp: January 13, 2023, 11:46:06 am
content: Hi,my system consists of 1 master-µC (STM32G491) and 12 to 16 slave-µCs.The master just provides a shared CLK signal to the slaves and the slaves just have 1 line back to the master. (all on PortA)I would like to receive a periodic bit-stream (~24bits) from each of them with a update-rate of 100kHz --> CLK is min. 2.4MHz (aiming for around 4MHz)There is also a ST-Application Note for this:https://www.st.com/resource/en/application_note/an4666-parallel-synchronous-transmission-using-gpio-and-dma-stmicroelectronics.pdfwhich uses the GPIO combined with a Timer and the DMA.This is ideal for a 1:1 communication and if I adapt this to my use-case I will have the data in a undesired arrangement.uint16_t sample_at_clock = slave1-bit-0, slave2-bit-0, slave-3-bit-0, ...uint16_t sample_at_clock = slave1-bit-1, slave2-bit-1, slave-3-bit-1, ......In that case I would have to use some matrix-transpose, but bit-wise, for which the CMSIS is not opitmized.Therefore with a software-solution for the bit-rearranging the µC will spend multiple hundered clock cycles, which in case of 100kHz ends up in a big waste of CPU power.Idally I would like to have 16 indepedent SPI channels with a data-width of 24bits, but nor does the STM offer that many SPIs, nor does it support such a long data-width.-Is there some way to get the bit-stream into the correct arrangement without loading the CPU that much?-Is there in general some other Interface oout there, which is more optimized for such things? (Camera-Interface, FSMC, ... = unfortunately both of them are not supported by the G491)

author: Siwastaja
author_rank: Super Contributor
timestamp: January 13, 2023, 12:30:38 pm
content: The question is, what do yo do with the data?For example, if you just write it to a memory card or send through another interface for later processing, then you can rearrange the data during that processing.OTOH, if you plan to do e.g. signal processing on the MCU, then easiest is to do it on the slave MCUs before sending the data.

author: alexanderbrevig
author_rank: Frequent Contributor
timestamp: January 13, 2023, 01:44:01 pm
content: Code:#include <stdio.h>#include <stdint.h>// Hackers Delight 7.3// https://github.com/hcs0/Hackers-Delight/blob/master/transpose32.c.txtvoid transpose32a(int32_t a) {int j, k;int32_t m, t;for (j = 16, m = 0x0000FFFF; j; j >>= 1, m ^= m << j) {for (k = 0; k < 32; k = ((k | j) + 1) & ~j) {t = (a ^ (a[k|j] >> j)) & m;a ^= t;a[k|j] ^= (t << j);}}}int32_t list[] = {1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2};int main(){transpose32a(list);for (int i=0; i<32; i++) {printf("%02x", list);}// as expected it prints 30 0x00000000 and then 0x55555555 0xAAAAAAAA}

author: Kneidl
author_rank: Contributor
timestamp: January 13, 2023, 02:40:10 pm
content: @Siwastaja: the data needs to be processed in real-time on the master-µC, (2-3 simple threshold compares and sum-up function), which means with the 100kHz update rate, the master-µC has 10µs to process the data of of all 12...16 slaves.@alexanderbrevig: thanks for the suggestion. A few hours ago I just tried out exactly this code, but that alone takes around 10µs just for the transpose process, which means there is no time left for the rest.Thanks to both of you for your answers, but do you know some way to get rid of the need of the transposeing?Like setting up the DMA into bit-wise-mode or use some other µC integrated communication-bus to do that?

author: Siwastaja
author_rank: Super Contributor
timestamp: January 13, 2023, 02:47:03 pm
content: I don't think there is such mode. Even with bit-banding, you would need to simultaneously write to 16 different addresses.Did you try to compile the above code with -O3? STM32G491 has ITCM. Did you try to put the procedure there? Still too slow? Manual loop unrolling of thekloop could also improve things.

author: Kneidl
author_rank: Contributor
timestamp: January 13, 2023, 04:11:33 pm
content: Thanks for the hint, but I dont think the G491 has a ITCM (not listed in the datasheet)My test was with -OFast, but -O3 results into roughly the same runtime: around 12µsNext steps will be some manual optimizations, but I don't think they will be that significant.

author: Siwastaja
author_rank: Super Contributor
timestamp: January 13, 2023, 05:19:30 pm
content: Quote from: Kneidl on January 13, 2023, 04:11:33 pmSorry about that, lazy googling where Google combined text randomly out of context making it look like a it was a contiguous excerpt.Anyway, I don't think there is easy way out. Such custom bit manipulation needs to be handled in some way; there is no hardwired peripheral for it so your options are,1) try to preprocess on the slave MCUs2) use a small external CPLD to combine the data streams with the bit swapping3) get a faster CPU4) try to optimize the shit out of the code, on current MCU.(3) could be simplest since you have a nearly working solution. A Cortex-M7 MCU at 480MHz would do it.

author: pcprogrammer
author_rank: Super Contributor
timestamp: January 13, 2023, 07:26:29 pm
content: What are the slave micro's like?Instead of having them provide the data to the main controller in parallel see if it is possible to do in a chain of micro's.I have a project in sleep mode (started on it long back and need to finish it some day) where there is a master device sending data down a chain of slaves with high rate SPI based on DMA. For you it would be the other way round. This way the data can be stored in the master as proper words instead of separate bits.

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 14, 2023, 08:16:19 pm
content: RGB LED strips like WS2811 and similar do chaining, but use different formats to describe logic levels (typically pulse width).Adapted to OP's case, the master would output CLK and RST, and have a single input, passed serially through each slave. (Each slave would therefore have three inputs: CLK, RST, DIN, and one output, DOUT, with slaves chained DOUT-DIN.)Whenever RST is asserted, each slave latches their own 24 bits to their 24-bit shift register. On each CLK pulse, the shift register is shifted by one bit, with the oldest bit shifted to DOUT, and a new bit shifted in from DIN.The master reads theN×24 bits from its single input by pulsing RST, and then clocking theN×24 bits.This also means that the sampling moment is the same for all slaves (except for pulse signal delay differences, if they are not very close to each other).I do believe one could use SPI to implement this, although I am only a hobbyist, and not familiar with STM32G491 in particular.The idea is that in the slaves, RST is both an interrupt trigger, as well as (CS) chip select. The SPI subsystem needs to have a buffer or FIFO of at least 24 bits. Whenever RST is triggered (chip select deselected), each slave copies their 24 bit state to the SPI output buffer. Whenever transfer completes, it is copied from the SPI input buffer to the SPI output buffer. (Some SPI subsystems can do this internally, for others you might be able to use DMA. This copy must occur without any delay, within a single CLK clock cycle between final bit and first bit from new slave.)The master will receive a sequence of 24-bit transfers (being also the SPI master), with the slave closest to the master first, and the slave furthest from the master last. If the slaves have an internal pullup or pulldown by default on the DI line, then after all slaves valid data, the master will receive a continuous stream of 0xFFFFFF = 16777216 (for pullups) or zeros (for pulldowns), respectively. This means you can add new slaves to the chain, and if the all-zeros/all-ones code is reserved for this use, the master can auto-detect the number of slaves connected.A 4 MHz SPI clock should not be a problem for most microcontrollers.

author: Kneidl
author_rank: Contributor
timestamp: January 18, 2023, 04:58:59 pm
content: Hi,@ Nominal Animal: true, but this would only help if the STM32 would have min. 12-16 separate SPI, but unfortunately we are stuck with like 5 or 6. But yes, then we could skip the whole transpose operation. If I misunderstood it, just let me know.@Siwastaja I guess you are right with your points.With some manual optimization I was able to reduce the calculation time by ~10%, which unfortunately is not sufficient for what I need.The only option I just see is that there might be some nice hackers-delight code modification possible,because I need to transpose a 16x32bit matrix (actually 16x24), but my bit-modification skills are deep enough to optimize the code it into that direction :/

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 18, 2023, 06:53:01 pm
content: Quote from: Kneidl on January 18, 2023, 04:58:59 pmYou're thinking of a star network, whereas I was describing a single series configuration:master - slave - slave - slave - slave - slave - ... - slaveThe entire thing acts like one huge shift register.Each slave is a SPI slave, and uses exactly one SPI bus for this. (MOSI/DI connected to the slave further from master in the chain, and MISO/DO to the slave nearer to the master in the chain.)Similarly, only one SPI bus is used on the master.

author: Kneidl
author_rank: Contributor
timestamp: January 18, 2023, 07:56:06 pm
content: @Nominal Animal: alright! got it!This would result in a clock-frequency of ~40MHz (100kHz/Device * 16 slaves * 24bit), which is a little bit too fast for my slaves, but maybe half the update-rate would work.The propagation-delay would be roughly the worse, but that needs to be evaluated.But the issue for at least my target application is that I need an additional signal to each slave (CLK), like each slave requires in that case 3 signals instead of 2, but that also needs to be evaluated if it is worth to integrate a 3rd channel.

author: alexanderbrevig
author_rank: Frequent Contributor
timestamp: January 18, 2023, 08:07:04 pm
content: Could you add an FPGA frontend and have that talk to the 16 slaves? Then expose the bitstream you want for parallell in?

author: DavidAlfa
author_rank: Super Contributor
timestamp: January 18, 2023, 09:03:13 pm
content: How about using a 74595 shift register for each slave? (16x 74hc595), interfacing them in groups of 2 (so 2 slaves are read in a single transfer).1 - Send 8 clock pulses, all 16 slaves send one byte.2 - Send latch pulse to transfer data to the output, start sending the next 8 clocks.3 - Read current data by enabling outputs of each 16-bit group, in 8 reads: 2+2+.2... (Byte0 of slaves 0...15)4 - The next 8 clocks are done, repeat step 2-3 to read second and third byte.5 - All slaves are done, start over.The whole processs could be assisted by a timer-triggered dma.

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 18, 2023, 09:13:10 pm
content: Quote from: Kneidl on January 18, 2023, 07:56:06 pm16 slave devices is a lot. First thing is, you'll never find any MCU with that many individual SPI/I2C or whatever else interface. Unless maybe some very niche MCUs.So you don't have a choice but combine communication using fewer interfaces. Can be daisy-chained SPI as suggested, or RS-485, or something like that.But, regarding your above calculation, you don't need to use a single SPI peripheral either, if the resulting clock speed would be too high. If your MCU has 2, 3 or 4 SPI peripherals (which is relatively common), you can split your 16 slaves in 2 to 4 chained busses and thus lower clock rate and latency.Of course you could also use a FPGA, but that's another story entirely. Implementing several busses as I suggested should do the trick.

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 19, 2023, 12:14:05 am
content: I wonder if bit-banging the inputs would yield sufficient speed.Let's say you use PA0 to PA15 for the inputs from the 16 slaves, and PC9 for the clock output. The loop to shift the 24 bits from 16 different slaves would then be written asCode:#include <stdint.h>void get_data(uint32_t *buffer, uint32_t step, volatile uint32_t *datair, volatile uint32_t *clkbsrr, const uint32_t tick){const uint32_t tock = (tick << 16) | (tick >> 16);uint32_t input, left;uint32_t *data;input = *datair;*clkbsrr = tick;data = buffer;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*clkbsrr = tock;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;*data = input & 1; data += step; input >>= 1;left = 23; while (left-->0) {input = *datair;*clkbsrr = tick;data = buffer;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*clkbsrr = tock;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;*data = (*data << 1) | (input & 1); data += step; input >>= 1;}}by callingget_data(data, 1, GPIOA_IR, GPIOC_BSRR, (1<<9) << 16);. Note this requires that all 16 input pins are in the same bank (Px0..Px15), and the generated clock will have jitter depending on interrupts occurring at the same time. If you use a larger array, for exampledatato store 8 consecutive samples for each source, then useget_data((uint32_t *)data +n, 8, GPIOA_IR, GPIOC_BSRR, (1<<9)<<16);forn=0..7.If you have a STM32G491 dev board, you could run the above code, and measure the frequency on the CLK. If it is at least say 4 MHz, then the above takes at most 6µs per call (24 cycles / 4 MHz = 6 µs) and you'd have about 40% of the CPU time left to do whatever you want with the data, and keep the 100 kHz per-source sample rate.On the STM32G491xC, the GPIOx_BSRR register can set or clear any combination of pins in bank x. If any of the 16 high bits is set, the corresponding pin is set low; if any of the 16 low bits is set, the corresponding pin is set high. So, specifying(1<<9)<<16means the CLK (PC9) idles high and the data lines are sampled just before high-to-low CLK transition. If you useGPIOE_BSRR,(1<<4)then CLK is PE4, idles low, and the data lines are sampled just before a low-to-high CLK transition. The code can be adjusted to any other clocking scheme, though.

author: DavidAlfa
author_rank: Super Contributor
timestamp: January 19, 2023, 02:35:23 am
content: How about not using SPI at all?Use a Timer with 2 PWM channels.CH1 runs 50% PWM (Clock generation), CH2 runs 55% PWM (Read trigger), only CH2 is DMA-enabled.Configure the DMA as circular, from GPIO->ODR to RAM, word size, len=48, enable half/full transfer ISRs.Now the PWM will be generating the clock signal.Slaves can use SPI, preparing the data when the clock is high, transfering when it goes low.Moments later, CH2 triggers a DMA transfer from GPIO to RAM, storing the whole GPIO port.When you get a DMA ISR, process the 24 words (48 bytes) before the next half fills up.If you want effective 100KSps, then you need a PWM of about 2.4MHz.DMA ISR will trigger every 10us, seems a bit tight, you'll have 1700 clocks between each ISR!At least the CPU won't waste power on running/capturing the signals.Recomposing the original 24bit word is pretty simple, but might not be the most efficient, unrolling the loop might increase performance:Code:volatile uint32_t slave_data; // Only 24bit used.volatile uint16_t buffer; // DMA buffervoid traspose(uint8_t index){for(uint8_t s=0; s<16; s++){ // Clear old dataslave_data = 0;}for(uint8_t bit=0; bit<24; bit++){ // Process only "1s"for(uint8_t s=0; s<16; s++){slave_data |= ((buffer & 1<<s) && 1)<<bit;}}}void traspose_unrolled(uint8_t index){for(uint8_t s=0; s<16; s++){ // Clear old dataslave_data = 0;}for(uint8_t bit=0; bit<24; bit++){ // Process only "1s"slave_data |= ((buffer & 1<<0 ) && 1)<<bit;slave_data |= ((buffer & 1<<1 ) && 1)<<bit;slave_data |= ((buffer & 1<<2 ) && 1)<<bit;slave_data |= ((buffer & 1<<3 ) && 1)<<bit;slave_data |= ((buffer & 1<<4 ) && 1)<<bit;slave_data |= ((buffer & 1<<5 ) && 1)<<bit;slave_data |= ((buffer & 1<<6 ) && 1)<<bit;slave_data |= ((buffer & 1<<7 ) && 1)<<bit;slave_data |= ((buffer & 1<<8 ) && 1)<<bit;slave_data |= ((buffer & 1<<9 ) && 1)<<bit;slave_data |= ((buffer & 1<<10) && 1)<<bit;slave_data |= ((buffer & 1<<11) && 1)<<bit;slave_data |= ((buffer & 1<<12) && 1)<<bit;slave_data |= ((buffer & 1<<13) && 1)<<bit;slave_data |= ((buffer & 1<<14) && 1)<<bit;slave_data |= ((buffer & 1<<15) && 1)<<bit;}}Code:void DMA_TC_ISR{ // Full transfertraspose(1);}void DMA_HT_ISR{ // Half transfertraspose(0);}Demo:https://jdoodle.com/ia/CsjCode:Raw buffer:55559866E0780180FE0000000000000055559866E0780180FE0000000000000055559866E0780180FE00000000000000Output data:CH1:010101CH2:020202CH3:030303CH4:040404CH5:050505CH6:060606CH7:070707CH8:080808CH9:090909CH10:101010CH11:111111CH12:121212CH13:131313CH14:141414CH15:151515CH16:161616

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 19, 2023, 04:51:12 am
content: Stealing the 8x8-bit transpose from Hacker's Delight, I would considerCode:#include <stdint.h>typedef struct {union {uint32_t u32;uint8_t u8;};} word32;typedef struct {union {uint32_t u32;uint8_t u8;};} pack128;static inline pack128 transpose_8x8_pair(const uint8_t *s){pack128 src = { .u8 = { s, s, s, s, s, s, s, s,s, s, s, s, s, s, s, s }};pack128 tmp;tmp.u32 = (src.u32 ^ (src.u32 >> 7)) & UINT32_C(0x00AA00AA);tmp.u32 = (src.u32 ^ (src.u32 >> 7)) & UINT32_C(0x00AA00AA);tmp.u32 = (src.u32 ^ (src.u32 >> 7)) & UINT32_C(0x00AA00AA);tmp.u32 = (src.u32 ^ (src.u32 >> 7)) & UINT32_C(0x00AA00AA);src.u32 ^= tmp.u32 ^ (tmp.u32 << 7);src.u32 ^= tmp.u32 ^ (tmp.u32 << 7);src.u32 ^= tmp.u32 ^ (tmp.u32 << 7);src.u32 ^= tmp.u32 ^ (tmp.u32 << 7);tmp.u32 = (src.u32 ^ (src.u32 >> 14)) & UINT32_C(0x0000CCCC);tmp.u32 = (src.u32 ^ (src.u32 >> 14)) & UINT32_C(0x0000CCCC);tmp.u32 = (src.u32 ^ (src.u32 >> 14)) & UINT32_C(0x0000CCCC);tmp.u32 = (src.u32 ^ (src.u32 >> 14)) & UINT32_C(0x0000CCCC);src.u32 ^= tmp.u32 ^ (tmp.u32 << 14);src.u32 ^= tmp.u32 ^ (tmp.u32 << 14);src.u32 ^= tmp.u32 ^ (tmp.u32 << 14);src.u32 ^= tmp.u32 ^ (tmp.u32 << 14);tmp.u32 = ((src.u32 & UINT32_C(0x0F0F0F0F)) << 4) | (src.u32 & UINT32_C(0x0F0F0F0F));tmp.u32 = ((src.u32 & UINT32_C(0x0F0F0F0F)) << 4) | (src.u32 & UINT32_C(0x0F0F0F0F));tmp.u32 = (src.u32 & UINT32_C(0xF0F0F0F0)) | ((src.u32 & UINT32_C(0xF0F0F0F0)) >> 4);tmp.u32 = (src.u32 & UINT32_C(0xF0F0F0F0)) | ((src.u32 & UINT32_C(0xF0F0F0F0)) >> 4);return tmp;}void transpose_24x16_to_16x32(uint32_t d, const uint16_t s){const uint8_t *const src = (const uint8_t *const)s;word32 *const dst = (word32 *const)d;pack128 tmp;int i;tmp = transpose_8x8_pair(src);for (i = 0; i < 16; i++) dst.u32 = tmp.u8;tmp = transpose_8x8_pair(src + 16);for (i = 0; i < 16; i++) dst.u8 = tmp.u8;tmp = transpose_8x8_pair(src + 32);for (i = 0; i < 16; i++) dst.u8 = tmp.u8;}assuming little-endian byte order (as typically used on Cortex-M MCUs).It isn't optimized –– I'm pretty sure one can do thepack128 srcbetter –– but it isn't too bad.(Only quickly checked on x86-64, also a little-endian architecture, to ensure the results look okay.)