32F4 - 3 ways to detect end of DMA transfer - Page 1
Forum section: electronics / general

author: peter-h
author_rank: Super Contributor
timestamp: November 17, 2022, 11:53:46 am
content: This topic has been all around the place, with much debate and much posted about which methods are more reliable.It is related to feeding SPI. You need to raise CS to the SPI slave device; not too early.Method 1is to wait until the transfer counter NDTR = 0. This articlehttp://efton.sk/STM32/gotcha/g20.htmlsuggests that this can be unreliable.Method 2is to wait for the End of transfer interrupt pending bit. There is a guy on the ST forum who, in between beating you up, has suggested that is better. No need for interrupts; you can poll it.Method 3, suggested to me by the above guy, is to wait for the DMA Enable bit to clear. He suggests the timing of this method may be more reliable. This method is curious but works because the DMA is started byyousetting EN and then it clears ENby itselfwhen it has finished.There is a claimed issue there in that the EN bit may remain set for a while after the last transfer, and the config registers are inaccessible when EN=1, so you have to wait until EN=0 before doing any config.One obvious issue is that if you set EN but for some reason this is not immediately reflected in the register (the 32F4 peripherals run at a much slower clock than the CPU) then you will see EN=0 and think it has finished. But in reality it seems to work.My DMA-SPI code below includes all 3 methods and I timed them all.Code:/**** For use where fast transfers are needed, on the limit of SPI3 speed so with zero gaps. This is impossible* to do by polling at 10.5mbps or 21mbps and is probably marginal at 5.25mbps. The 16 bit SPI mode just* manages gap-free with polling but works only with even block sizes, and has the "first byte problem" which* DMA gets around.** ** DMA ONLY SO NO CCM ACCESS SO THE TWO BUFFERS HAVE TO BE "STATIC" **** This function is blocking so the caller can set CS=1 right away (check device data sheet!). A non-blocking* version would make sense only if transmitting only (txonly=true) but the caller would have to tidy up* the DMA and SPI3->CR2.** spi3_set_mode() can still be used to set up the clock speed, phase, etc; this function does nothing* to SPI3 other than to enable/disable DMA. It does however make sense only if SPI3 is a Master.** Two modes, obviously mutually exclusive, for tx-only and rx-only:* If txonly, dumps rx data so you don't need to allocate a buffer for it* If rxonly, transmits all-0x00 so you don't need to feed SPI with some "known garbage"** The rx-only mode is superfluous in most cases but it does avoid shifting non-0x00 data to the device* while we are reading data out of it. With some devices this can matter. The ADS1118 ADC is one such.** Because this function needs to work with spi3_set_mode(), care is taken to not modify the SPI config.** The yield parameter just releases control to RTOS, so equal-priority tasks don't have to wait until DMA* finished. This might be useful for long/slow DMA transfers. Usually not useful.* 31/8/22 if true, worsens corruption of incoming USB VCP data.** Returns (false) if memory is in CCM, size=0, null pointers...** NULL pointers are allowed if using the txonly or rxonly modes; then the unused one can be NULL.***/bool SPI3_DMA_TransmitReceive(uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, bool txonly, bool rxonly, bool yield){// Check for invalid inputsif ( Size==0 ) return (false);if ( (pTxData==NULL) && !rxonly ) return (false);if ( (pRxData==NULL) && !txonly ) return (false);uint32_t txadd = (uint32_t) pTxData;uint32_t rxadd = (uint32_t) pRxData;static uint8_t txonly_target=0;// must not be in CCMstatic uint8_t rxonly_source=0;// must not be in CCM// Test for CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K (linkerscript.ld)if ( ((txadd>=0x10000000) && (txadd<0x10010000)) || ((rxadd>=0x10000000) && (rxadd<0x10010000)) ){return(false);}// DMA ch 1 clock enable already done in main.c//RCC->AHB1ENR |= (1u << 21);// DMA1EN=1 - DMA1 clock enable//hang_around_us(1);// give it a chance to wake upSPI3->CR1 &= ~SPI_CR1_SPE;// disable SPI// DMA1 Ch 0 Stream 0 is SPI3 RXDMA1_Stream0->CR = 0;// disable DMA so all regs can be written// Wait until DMA clears the EN bit by itself.// Should be pointless since the previous transfer waited for end of transfer (hardware bug)while ( (DMA1_Stream0->CR & DMA_SxCR_EN) != 0) {};DMA1->LIFCR = (0x03d << 0);// clear int flags & transfer complete - 111101 stream 0DMA1_Stream0->NDTR = Size;if (txonly){DMA1_Stream0->M0AR = (uint32_t) &txonly_target;// memory address to dump rx data to}else{DMA1_Stream0->M0AR = rxadd;// memory address in normal mode}DMA1_Stream0->PAR = (uint32_t) &(SPI3->DR);// peripheral addressDMA1_Stream0->FCR = 0;// direct modeif (txonly){DMA1_Stream0->CR = 0 << 25// CHSEL: ch 0| 0 << 23// MBURST: memory burst - single transfer| 0 << 21// PBURST: peripheral burst - single transfer| 3 << 16// PL: highest priority| 0 << 15// PINCOS: no peripheral address increment offset| 0 << 13// MSIZE: memory data size: byte| 0 << 11// PSIZE: peripheral data size: byte| 0 << 10// MINC: memory address increment: 0| 0 << 9// PINC: peripheral address increment: 0| 0 << 8// CIRC: no circular mode| 0 << 6// DIR: peripheral to memory| 0 << 5// PFCTRL: DMA is flow controller| 1 << 0;// EN: enable stream}else{DMA1_Stream0->CR = 0 << 25// CHSEL: ch 0| 0 << 23// MBURST: memory burst - single transfer| 0 << 21// PBURST: peripheral burst - single transfer| 3 << 16// PL: highest priority| 0 << 15// PINCOS: no peripheral address increment offset| 0 << 13// MSIZE: memory data size: byte| 0 << 11// PSIZE: peripheral data size: byte| 1 << 10// MINC: memory address increment: 1| 0 << 9// PINC: peripheral address increment: 0| 0 << 8// CIRC: no circular mode| 0 << 6// DIR: peripheral to memory| 0 << 5// PFCTRL: DMA is flow controller| 1 << 0;// EN: enable stream}// DMA1 Ch 0 Stream 7 is SPI3 TXDMA1_Stream7->CR = 0;// disable DMA so all regs can be written// Wait until DMA clears the EN bit by itself.// Should be pointless since the previous transfer waited for end of transfer (hardware bug)while ( (DMA1_Stream7->CR & DMA_SxCR_EN) != 0) {};DMA1->HIFCR = (0x03d << 22);// clear int flags & transfer complete - 111101 stream 7DMA1_Stream7->NDTR = Size;if (rxonly){DMA1_Stream7->M0AR = (uint32_t) &rxonly_source;// memory address to fetch dummy tx data from}else{DMA1_Stream7->M0AR = txadd;// memory address in normal mode}DMA1_Stream7->PAR = (uint32_t) &(SPI3->DR);// peripheral addressDMA1_Stream7->FCR = 0;// direct modeif (rxonly){DMA1_Stream7->CR = 0 << 25// CHSEL: ch 0| 0 << 23// MBURST: memory burst - single transfer| 0 << 21// PBURST: peripheral burst - single transfer| 0 << 16// PL: priority low| 0 << 15// PINCOS: no peripheral address increment offset| 0 << 13// MSIZE: memory data size: byte| 0 << 11// PSIZE: peripheral data size: byte| 0 << 10// MINC: memory address increment: 0| 0 << 9// PINC: peripheral address increment: 0| 0 << 8// CIRC: no circular mode| 1 << 6// DIR: memory to peripheral| 0 << 5// PFCTRL: DMA is flow controller| 1 << 0;// EN: enable stream}else{DMA1_Stream7->CR = 0 << 25// CHSEL: ch 0| 0 << 23// MBURST: memory burst - single transfer| 0 << 21// PBURST: peripheral burst - single transfer| 0 << 16// PL: priority low| 0 << 15// PINCOS: no peripheral address increment offset| 0 << 13// MSIZE: memory data size: byte| 0 << 11// PSIZE: peripheral data size: byte| 1 << 10// MINC: memory address increment: 1| 0 << 9// PINC: peripheral address increment: 0| 0 << 8// CIRC: no circular mode| 1 << 6// DIR: memory to peripheral| 0 << 5// PFCTRL: DMA is flow controller| 1 << 0;// EN: enable stream}// Config SPI3 to let DMA handle the data. These need to be cleared when transfer complete!// This starts the transferSPI3->CR2 |= 3;// TXDMAEN, RXDMAEN: 11 - both set in one goSPI3->CR1 |= (1<<6);// SPE=1 enable SPI// Wait for DMA to finish. Blocking is necessary to prevent device CS=1 too early.// There could be a timeout here but a failure is impossible short of duff silicon, because// we are a Master and generating the SPI clock.while(true){/*// Either end-transfer detection method below works but the NDTR may be less reliable// http://efton.sk/STM32/gotcha/g20.html#if 0uint16_t temp1;temp1 = DMA1_Stream0->NDTR;if ( temp1 == 0 ) break;// transfer count = 0#elseuint32_t temp2;temp2 = DMA1->LISR;if ( (temp2 & (1<<5)) !=0 ) break;// TCIF0#endifif (yield) osDelay(1);// release to RTOS (see notes in comments)*/// Third method of detecting end of transfer: wait for DMA to set EN to 0.// All 3 methods produce the same time from last SPI clock to CS=1 (1.8us).if ( (DMA1_Stream0->CR & DMA_SxCR_EN) == 0) break;}SPI3->CR2 &= ~3;// TXDMAEN, RXDMAEN: 00 - both cleared in one go// Clear int pending flags. They get cleared at the top of this function anyway, but...DMA1->LIFCR = (0x03d << 0);// clear int flags & transfer complete - 111101 stream 0DMA1->HIFCR = (0x03d << 22);// clear int flags & transfer complete - 111101 stream 7// Clear any rx data and the overrun flag in case not all received data was read// A small delay is needed because the very last transfer may not propagate to DR.hang_around_us(1);SPI3->DR;SPI3->DR;SPI3->SR;SPI3->CR1 &= ~(1<<6);// SPE=0 disable SPIreturn (true);}I checked all 3 methods on a scope.I also tried a 4th variant, where the EN=0 bit is checked for SPITX(stream 7) which, as expected, produces an earlier end-transfer detection - because the SPI RX (stream 0) includes a delay in the SPI peripheral. This is not useful because it doesn't work! Probably it doesn't work because the SPI RX channel data is immediately chucked away at that point.I am posting this because there is so much written on this and clearly most of it is wrong. All 3 methods produce almost exactly the same timing. The SPI clock is 21MHz.Scope images below

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 17, 2022, 11:56:55 am
content: Unless you do a trillion small-sized transfers (Wouldn't make much sense), just enable DMA IRQ, when it triggers set CS low, done.

author: AndyC_772
author_rank: Super Contributor
timestamp: November 17, 2022, 12:58:54 pm
content: The obvious 'gotcha' with all of these, is that they don't explicitly take into account the fact that the last word written to the SPI interface takes time to clock out.Once the DMA peripheral writes that last word to the SPI transmit register, its job is done. Zero transfers remain.However, at this point, the SPI peripheral has only just started transmitting the penultimate word. It still has that word to complete, plus the last one which the DMA controller wrote, which is sitting in its short FIFO.This could take a considerable amount of time; after all, the SPI clock could be arbitrarily slow.Ergo, when the DMA transfer is complete, you then need to check that the SPI device is idle, before confidently de-asserting CS.

author: peter-h
author_rank: Super Contributor
timestamp: November 17, 2022, 01:51:10 pm
content: Actually no, because I am checking the SPIRXchannel and thus it is safe to raise CS immediately, because all the data has been received and there are no more SPI clocks.My 4th case above is doing what you suggest, and it doesn't work, for the reason you giveQuoteThat is equivalent to my case #2. The problem is that you then get anonblocking DMA transfer, which is generally not helpful because you have to wait for the data to all arrive (because you e.g. want to read it) so you then need to set a "end transfer" flag inside the ISR which the foreground code polls. So you gain nothing, unless the foreground task is nothing to do with the data being transferred (e.g. sending a buffer repetitively to an LCD display).

author: Siwastaja
author_rank: Super Contributor
timestamp: November 17, 2022, 01:54:45 pm
content: Quote from: peter-h on November 17, 2022, 11:53:46 amSuch thing should not happen. While clock domain crossing introduces a delay, the order of operations to the same register should definitely finish in the same order. If you write PERIPHx->REGy and then read PERIPHx->REGy, you will definitely get the correct value back. The only instance where this does not happen is if the peripheral is purposely designed to return something else, like a shadow register. This should be then mentioned in the manual, and this is not the case with DMA enable bit. As you say, you writing it to '1' updates it to '1', and when finished, it is written to '0' by the peripheral logic. I see absolutely no problem.And yes, I agree DMA RX channel interrupt should be good for deasserting the CS.

author: peter-h
author_rank: Super Contributor
timestamp: November 17, 2022, 01:58:29 pm
content: I agree; I think this is BS.I also think the claimed need to wait for EN=0 before doing DMA config (widely written about) is BSassuming thatyou waited for the previous transfer to finish. I am leaving that test in there because it has a negligible impact on performance.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 17, 2022, 02:03:11 pm
content: I like to turn things around.Usual pattern is: thing X should be true, but because we can't prove it, we lazily add a while() loop to poll for thing X to be true. "Impoved" version is poll with timeout, with no error handling. Even more "improved" is poll + timeout + error handling.Instead what I do: do my best to prove Xistrue, don't add polling loop, but instead add an assert + error handling (at least "give up and report").I agree that if you get DMA completion interrupt, that should mean the peripheral has disabled itself. Hence I would add if(CR.EN) error(1234);Because of clock domains, the excess test might not be totally negligible. Sometimes performance matters. DMA is usually used in such cases, so if transfers are small and you are wasting say 10 CPU clock cycles extra per transfer, that is already same order of magnitude as the ISR entry many people fear.

author: peter-h
author_rank: Super Contributor
timestamp: November 17, 2022, 02:22:48 pm
content: I also agree, but we are stuck with ST not supporting its products, which has given rise to a bunch of people on forums who profess to have some kind of inside knowledge, and often push this aggressively.Re DMA performance: you will never get any if you do short transfers, because it takes several writes (to aslowperipheral) to set up the DMA. And you need to set up most of it each time. Unlike some DMA controllers I have used many years ago, there are no holding registers for the addresses, and obviously NDTR=0 at the end. And if you are doing long transfers then DMA wins with speed, and then the extra EN=0 check is immaterial.I still use DMA-SPI even for 1-2 byte transfers from ADCs, just for code uniformity, and with a conv time in milliseconds, it's fine.I am 100% sure that EN=0 check is BSEDIT: I removed the two EN=0 checks and it reduces the DMA init time from 2us to 1.8us. Negligible. But the total DMA init time of ~2us is not negligible in some applications, and a memcpy() type loop, especially if moving 32 bits, will be much faster than DMA. A standard GCC memcpy does not optimise (I posted a thread on it) which surprised me.However I find that this bit of code is not neededCode:// Clear int pending flags. They get cleared at the top of this function anyway, but...DMA1->LIFCR = (0x03d << 0);// clear int flags & transfer complete - 111101 stream 0DMA1->HIFCR = (0x03d << 22);// clear int flags & transfer complete - 111101 stream 7// Clear any rx data and the overrun flag in case not all received data was read// A small delay is needed because the very last transfer may not propagate to DR.hang_around_us(1);SPI3->DR;SPI3->DR;SPI3->SR;SPI3->CR1 &= ~(1<<6);// SPE=0 disable SPI

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 17, 2022, 07:10:50 pm
content: This is a PITA to read:Code:DMA1->LIFCR = (0x03d << 0);// clear int flags & transfer complete - 111101 stream 0DMA1->HIFCR = (0x03d << 22);// clear int flags & transfer complete - 111101 stream 7SPI3->CR1 &= ~(1<<6);Why not use the provided CMSIS macros?Code:WRITE_REG( DMA1->LIFCR, DMA_LIFCR_CDMEIF0 | DMA_LIFCR_CTEIF0 | DMA_LIFCR_CHTIF0 | DMA_LIFCR_CTCIF0 );WRITE_REG( DMA1->HIFCR, DMA_HIFCR_CDMEIF7 | DMA_HIFCR_CTEIF7 | DMA_HIFCR_CHTIF7 | DMA_HIFCR_CTCIF7 );CLEAR_BIT( SPI3->CR1, SPI_CR1_SPE );I mean, in 2 months you'll completely forget which bits were what, requiring to check everything in the RM. This way at least you know the bit names.

author: SiliconWizard
author_rank: Super Contributor
timestamp: November 17, 2022, 07:13:11 pm
content: I see no reason to use any other method than the interrupt flag. Then whether you poll it or use an ISR is down to your use case. I see no point in using any other method, and they all look flaky.

author: peter-h
author_rank: Super Contributor
timestamp: November 17, 2022, 09:54:46 pm
content: QuoteBecause I didn't know they were there when I wrote that code.QuoteThat's why I comment my code heavily - most programmers don'tAnyway this is off topic. I think some of the code is pointless e.g.Code:hang_around_us(1);SPI3->DR;SPI3->DR;SPI3->SR;is pointless because the RX DMA has already extracted everything from the RX channel. The cases where ST code uses this is where they transmit via SPI but aren't taking the returned data out, so they get a receiver overrun, with all the problems that causes. With DMA, there is no penalty for reading the RX SPI channel and if you set up the DMA RX to not increment the address, you can dump all the unwanted data into a single memory location (which should not be in the CCM, although you don't get a hard fault if it is, curiously).Now I get a much sooner CS=1; around 500nsI tend to agree that all 3 methods are equivalent (equally tacky, maybe) and what is suprising is the timing is practically identical for all 3.Each of the SPI3->DR; lines is worth 100ns, which shows how much slower the peripheral access is than the CPU speed. PCLK1=PCLK2=42MHz so it is taking two of those clocks to execute that.In particular I think the 1us delay should be worthless.

author: wek
author_rank: Frequent Contributor
timestamp: November 18, 2022, 08:51:23 am
content: In case of the dual-port DMA as implemented also in STM32F4xx, NDTR==0 - as the above linked article says, too - indicates the last peripheral-side transaction (i.e. it has been at least initiated). In this particular case it is sure to come later than the last SPI_SR.RXNE, so any signal and action you'd derive from SPI_SR.RXNE e.g. in a polling-based implementation is equally good being driven/taken being derived from NDTR==0.OTOH, NDTR==0 does not ensure that the memory-side transaction of the last transfer has already occurred.In another words, NDTR==0 is earliest of the three and can be used for the CS signal (unless the controlled chip is unusually picky about the last-edge-to-CS timing) and for any SPI-related housekeeping, but not good to pick the data from memory nor to do DMA-related housekeeping.For the latter, as others said above, I personally would check the transfer-complete flag, because it's primary purpose is exactly this. While checking DMA_SxCR.EN is most probably equally good, I see absolutely no reason to do it as there already is a dedicated flag, and there are hints of doubts such as:QuoteDMA_SxCR.EN *is* purposely designed to return something else than the written-to value, namely, the internal state of the given stream. This *is* mentioned in manual:Quote from: RM0090 chapter DMA, subchapter Stream configuration procedureThis said, I do believe that there is no delay (as there's no reason for it) between setting a previously known zero DMA_SxCR.EN to 1, and reading it back as 1.As a disclaimer, I must add here, that this belief of mine is only an anecdotal evidence, as I have no insider knowledge of the intimate details of given incarnation of given hardware, I am in no way connected to ST other than being a garage-scale consumer, and I am not aware of me ever claiming otherwise. I also can't remember seeing such claims by others on the forum where nobody ever answers, but then I also don't monitor venues like this one or stackoverflow or similar regularly. I'd also add, that ST is a commercial entity so they adjust their expenses in order to produce profit, however nasty that is from the point of view of public good; and I also positively know that they are absolutely willing to extensively support their products, would they see enough incentives as expressed in $$$$$$s of purchases.JW

author: peter-h
author_rank: Super Contributor
timestamp: November 18, 2022, 09:10:41 am
content: QuoteYes... a careful look at the first two screenshots in my original post above shows a difference (NDTR=0 versus LISR) of 50-100ns.So NDTR gets decremented by therequestingperipheral. Interesting. I wonder if it gets decremented the the request (to the DMA) is made, or by the completion of the request. I reckon the former, because the latter would decrement NDTR much faster.

author: paulca
author_rank: Super Contributor
timestamp: November 18, 2022, 10:16:35 am
content: Quote from: Siwastaja on November 17, 2022, 02:03:11 pmI went round and round in circles of those processes the other night debugging something. Asserts with { //Meh ErrorHandler()} to prove that, that condition is never true. Of course when you code finds itself in that error handler you find it's often not the best approach when you have a connected PC application on USB. They take all too kindly to break points either.I have had clauses like:if( readPtr > writePtr ) {return; // run away!}Then it becomes:/* This wont happen, trust me.if( readPtr > writePtr ) {return; // run away!}*/Then it's back to:if( readPtr > writePtr ) {ErrorHandler(); // Oh}The approach I go from there really irritates C programmers, especially low level embedded ones. I expand everything into encapsulated functions with individual tasks for which they can do in peace, not be bothered by outside influence, not work in tandem with anything else and can even be unit tested. Usually I find a lot of my "Uuhh? That can't happen!" go away once you are only juggling ONE ball in each bit of code. Downside is, it expands you symbol file, you call heirarchy, your .text size and halves your performance.I have also discovered as a beginner (beyond arduino FW) ... tutorial code may be a starting block, but once you run into issues, the FIRST thing you have to do is implement the error clauses, callbacks and check your return values. I have wasted hours and hours wondering why something wasn't working, but if I'd looked at the first Error Callback I would have noted the typo in the config.

author: peter-h
author_rank: Super Contributor
timestamp: November 18, 2022, 04:34:09 pm
content: The thing is that the above code cannot fail unless the silicon is faulty.If you have DMA feeding SPI, and the SPI is a Master, itmustcomplete.In an RTOS environment, if you have a forever loop, other stuff will still run anyway, and you are just wasting some CPU time with the hung-up thread.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 18, 2022, 04:38:52 pm
content: Quote from: peter-h on November 18, 2022, 04:34:09 pmI like this approach, but you must be quite confident about the assumptions, keep them simple (avoid long chains of assumptions), document them, and using some kind of assert() system is a good idea.SPI in master mode is of course very predictable, variance in timing is only due to clock domain crossings and this is pretty insignificant. With DMA, there is possibility of bus being congested, but you can easily rule this out by eliminating high-bandwidth peripherals or memory-to-memory DMA operations - or at least understanding how the DMA controller arbitrates and how the priority levels work.

author: paulca
author_rank: Super Contributor
timestamp: November 18, 2022, 05:22:04 pm
content: Quote from: peter-h on November 18, 2022, 04:34:09 pmIt's something I had to come to terms with in embedded MCU world that is rarely done in the big iron world and that is hot loop while idle. Don't get me wrong, spinlocks exist when latency is critical, but they are a burden for everyone and everything else on a multi-threaded system.I think it's why I've seen the IDLE watchdog timers become manditory to try and drill it home when you are in a concurrent environment, single or multi core you have to be respectful to others.

author: AndyC_772
author_rank: Super Contributor
timestamp: November 18, 2022, 05:54:17 pm
content: Quote from: peter-h on November 18, 2022, 04:34:09 pmI highly recommend incorporating a time-out condition anyway. Hardware bugs are a thing.FYI I'm not just making this up, I speak from recent, first hand experience.

author: peter-h
author_rank: Super Contributor
timestamp: November 18, 2022, 06:52:42 pm
content: QuoteSure... but with the SPI DMA moving 1 byte every 500ns, and the CPU running at 168MHz (7ns cycle) how would it be possible for the DMA to be unable to access the bus for any significant time?It is actually possible for a DMA running off a 42MHz clock to completely block out a CPU running at 168MHz? Even if one programmed a DMA channel to move a word from one circular buffer to another, with a 64k transfer count (or even using a second DMA channel to keep reloading the same value into NDTR), how could you hang up the CPU?The most I have in this system is SPI2 feeding an Adesto serial FLASH at 21MHz, and SPI3 feeding an SPI RAM also at 21MHz. These two could be concurrent. SPI3 serves a dozen devices but SPI3 is mutexed so the 21+21 is the worst case.And I have all this running 24/7, concurrently with ETH transfers, USB FS transfers, a web server, NTP, SPI GPS, ARINC429, and a ton of other stuff - about 20 FreeRTOS tasks, and it's all running just fine.QuoteThey are but then this processor would be useless.There is a 1.6 second hardware watchdog but that is a last-resort thing.

author: tabemann
author_rank: Contributor
timestamp: November 19, 2022, 03:30:50 am
content: Quote from: peter-h on November 18, 2022, 06:52:42 pmHave you seen the errata sheet for a real processor?

author: peter-h
author_rank: Super Contributor
timestamp: November 19, 2022, 08:47:52 am
content: Sure.I posted this thread for comments, and hoped for useful (specific) commentsIf a DMA transfer termination had to be detected with a timeout, that would becompletely useless.On a PC you can pop up a window with an error message, because- the product is completely unsupported- there is a user interfaceOn an embedded system, neither of the above options are (usually) available.One-liners tend to be completely useless also

author: Siwastaja
author_rank: Super Contributor
timestamp: November 19, 2022, 10:06:12 am
content: I agree it's highly unlikely that the DMA has a HW bug which manifests itself by transfer not finishing once every million times. It would make the product nearly useless.However, the problem isn't in what you can imagine, but what you can not. Having error checking, logging and recovery mechanisms are beneficial for things you did not expect. Examples? I'm not giving any, the point is that something can happen neither you or I ever imagined.And, in the end, you assume the silicon cannot be faulty, but yet in reality it so happens you made a complex mistake where certain chain of events causes a classic software bug, caused by you and no one else, to pop up in a weird corner case, andthatis what makes an "obvious" assumption like "DMA must finish" fail, while DMA works just fine.

author: nctnico
author_rank: Super Contributor
timestamp: November 19, 2022, 11:22:48 am
content: Quote from: AndyC_772 on November 18, 2022, 05:54:17 pmAgreed. Never have waiting loops for a hardware state without a time-out.Where it comes to SPI: a transfer is typically send so quickly that polling the busy bit (after getting an interrupt saying the transfer is finished) may not even need 1 loop to wait for it. So the impact of polling is minimal.

author: voltsandjolts
author_rank: Unknown
timestamp: November 19, 2022, 12:00:02 pm
content: Quote from: nctnico on November 19, 2022, 11:22:48 amFor mature mcu's with stable errata sheet I think this is bit of over kill.An alternative is to have some monitor code in systick isr to detect stuck background code (like watchdog var, decremented in isr, reset by background code). You can then capture status info before hard reset, which is likely required in any case. Captures all cases of lockup without bloat.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 19, 2022, 12:32:26 pm
content: For simple peripherals where you know a register writewillstart an operation whichwillend, and set a flag, a blocking loop without timeout is obviously fine:Code:SPI.MODE = MASTER;SPI.TX = 0x55;while(!SPI.BYTE_SENT) ;It only becomes a problem with complex / iffy peripherals, or chains of events, for example setting a timer somewhere, which triggers a DMA transfer, which triggers a peripheral, and then you somewhere assume the peripheral has finished. There is more surface area for bugs.The devil in timeouts is in error handling. What do you do in case of timeout? Let's analyse a few options:1) No timeout. Code is stuck forever in that while(1) loop. If you can attach a debugger, you get exact information about the problem (at very least where it got stuck)2) Have timeout, do nothing but break out of loop. Can be better, maybe the software recovers on its own and tries again. Can also be A LOT worse, software might act on wrong data or assumptions because you did not handle the timeout properly, and do wrong things.3) Have timeout, call some kind of simple error() handler, which ends up in reset. Possibly worse than (1), you lose all traces of problem, can't attach a debugger to see what went wrong.4) Simply use watchdog. Crap solution, too, you have random resets and no idea what caused them.5) Have timeout, log error and some kind of state transition graph etc. on non-volatile memory (or part of SRAM), reset, store the information somewhere or send it to cloud if possible. -> a lot of work. The system still malfunctions and resets spuriously but at least you may have a chance to fix it.6) Have timeout, write (nearly) non-intrusive recovery code: for example, deinit of relevant parts, some kind of self-checks, and retry. This is MASSIVE amount of work to get working reliably, you need to simulate all kinds of possible error scenarios and how do you know if your recovery code actually works? Let's say the DMA has some magical error flag which gets activated during full moon and will not be reset through the normal flag reset sequence, which prevents DMA from working until full chip reset? How do youknowin advance this flag exists and will cause a problem? So how your reset sequence can try to rectify this? Maybe the problem only gets corrected by reseting both DMA and the related peripheral through RCC reset registers? So in essence, you need multiple levels of recovery procedures, which is guesswork, all of which need to be tested to prove they are non-intrusive enough.7) combination of 5) and 6).But then finally a reality check: I have used non-guarded polling loops many many many times, and I don't remember them causing any issues in production, beyond initial testing. Instead, bugs have been elsewhere. But this is probably because I use non-guarded polling loops only in very simple cases.Just make sure the cure is not worse than the disease. If you can't afford the time to do it properly, it's probably best not to invest much time at all to half-baked error checking and timeouts. For example, applications written using ST's HAL are hard to analyze because every call can return an error code, and application writers need to either ignore error codes, prompting the question about the robustness of their code, or implement error handling, which is more work than most people think. Why is this a problem? Because 95% of the functions that return an error code, could be proven to never return an error, and could just be void of return values. Especially ST has the ability to prove such because they have internal understanding of the peripherals used, but instead they chose the lazy way which also "looks" robust because "hey, there is error handling, cool, MISRA and all!" But in reality, they are just pushingallthe responsibilities to the application programmers, and what's worst, they are generating extra responsibilities that are not needed.

author: nctnico
author_rank: Super Contributor
timestamp: November 19, 2022, 05:19:50 pm
content: The above is a nice summary but it still seems to be under the assumption that the code is actually executed as intended. This may not be the case at all time (think of radiation). Now these events will be rare but if you run enough devices long enough, it will start to pop-up as users reporting strange behaviour. That is why I have range checks at various levels in my software. At least the software doesn't continue processing with a wrong value. Since many applications do things is a loop anyway (read input, process, create output), not doing one loop and recovering the next loop does not affect device functionality in many cases.

author: voltsandjolts
author_rank: Unknown
timestamp: November 19, 2022, 05:34:43 pm
content: Quote from: nctnico on November 19, 2022, 05:19:50 pmNah.Then you are into the realms of random bit flips in sram or whateverSpend your life writing error handlers that never get used?Unless you're in space applications, don't worry about it.

author: nctnico
author_rank: Super Contributor
timestamp: November 19, 2022, 05:58:15 pm
content: There are other ways as well like external noise. Don't get stuck on just one example.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 19, 2022, 06:15:50 pm
content: That's why signal integrity is handled on a different level - HW level. We almost always work under the assumption that bit-flips do simply not happen in MCU. Even with radiation, rad-hard parts are usually used so that this guarantee can still be held.I mean, writing software that can recover from internal HW data corruption is super-duper-hyper-mega difficult to write, orders of magnitude harder than points 5&6 in my previous post.This is why we have EMC testing and qualification, products are subjected to much larger levels of external noise than is ever present in presence of even remotely compliant devices, and normal operation during these artificial conditions is verified. This is why we look at eye diagrams on communication buses. For example, RAM address and data buses are not checksummed or anything, yet computers work reliably! (And ECC DRAM exists because of bitflips in DRAM itself, which is part of DRAM technology and compromise between price, energy consumption, and amount of memory available.)But of course, if you find a mental model of thinking about radiation or noise induced bitflips helps you write more robust code in general, that's fine. Whatactuallycauses failure, is plain old bug.

author: nctnico
author_rank: Super Contributor
timestamp: November 19, 2022, 06:28:32 pm
content: Google did some actual research into this field:https://www.cnet.com/culture/google-computer-memory-flakier-than-expected/Regardless of the actual cause of an error the fact of life is that they do happen. 1 PC may run reliably when it is turned on/off every day but start putting 1000's of PCs together and suddenly you will be able to get a measurable number of errors that can not be explained. Microcontrollers are no different. So if you add a bit of resillience into your code -doesn't need to be super fancy- you'll be adding reliability to your product.One of the interesting things I have observed over the past decades is that when a microcontroller goes off the rails and has the program counter at a random address in the address range where the code is, quite often it will get back on track executing the code properly again. It is almost as if instruction opcodes are designed to do that. As a test try to disassemble some code and you'll see the disassembler will get back on track after a few instructions.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 19, 2022, 06:41:53 pm
content: Corruption and bit-flips in DRAM is completely normal, it's specified operation. This is why ECC exists. Google's result about water being wet is not interesting when we discuss oranges.Microcontrollers are totally different, they are designed to be nearly free of such bitflips, which is why they use internal SRAM. Of course, cheap parts do notguaranteethis and you have to pay premium for getting the guarantee, but in practice cheap parts are just as good.Bitflips can and will happen, you can even calculate the rate at which metastability propagates in the logic when using 2 or 3 flipflops to synchronize inputs. But the question is, while others assume this rate as zero, you probably overestimate it by many orders of magnitude. So exactly how much effort you are going to put in making thecoderad-hard? You know how difficult this is, don't you?In any case, I'm 100% sure your "robust" code does not survive random bitflips. Writing software which eliminates (or even significantly reduces) the impact of random physical memory corruption is much more than "do not use function pointers" or other stupid nearly irrelevant bullshit. Maybe some simple measures are slightly better than nothing, maybe not. But many of those not scientifically proven half-baked measures are still very useful against plain old bugs, so go for it.An example case in point: add canaries between global variables, or between stack and .data. Overindexing by +1 is usual mental fuck-up (even if you validated inputs). The bug is caught, good. Now consider random bitflip corrupting memory address in memory, or program counter, or whatever. The resulting address is now totally random in memory. Very unlikely to hit the canary. Likely to hit an invalid region, causing hardfault, which is good, but unless that happens, it likely hits somethingvalidin memory and totally wreaks havoc, and there is nothing (simple) you can do. If you are truly concerned about this, having two CPUs with separate memories running in lock-step is a classic solution, but expensive as hell, and there's still the question of how to handle the error, and who handles it.

author: peter-h
author_rank: Super Contributor
timestamp: November 19, 2022, 06:54:42 pm
content: So, instead of that forever loop (which one breaks out of if the transfer is finished) should the loop break out if some large integer overflows?How large should it be?One needs to quantify that experimentally. Or one could go for some massive margin: a uint32 takes some seconds to overflow, so that should work.Alternatively one could use a timer - the 1kHz "systick" one usually present. That is more dangerous because in an RTOS environment any bit of your code could get delayed (possibly by tens of ms if you are writing FLASH, as a rare process). And sometimes (like in my boot block code) there are no interrupts at all.And there is no assurance that exiting that loop will produce a working system. A watchdog is the only way. Various ways to trigger it eg. an overflow of the above uint32 sets a flag, various tasks can set this flag, and you have an RTOS task which is dedicated to checking that flag and various other flags, and sets off the watchdog.Metastability is a different issue. There is probably clock sync going on between the CPU clock and the PCLK1/2 peripherals, but the latter is divided down from the former so unless they did something dumb and got the edges in exactly the wrong place, they will have a constant and safe margin. Also this is old science - 40 years ago you had chips like the Z85C30 SCC which were totally async to the CPU yet they worked 100%, and AFAIK they used only a double sync. That said, I would be amazed if there was a meta issue between the CPU and the peripherals, since the clock division can be done fully sync i.e. the peripheral gets its edge a sub nanosecond after the CPU gets its edge.

author: nctnico
author_rank: Super Contributor
timestamp: November 19, 2022, 08:44:33 pm
content: Quote from: Siwastaja on November 19, 2022, 06:41:53 pmYou are missing the point here. My point is that with some simple checks in place your device will survive a bit-flip due to whatever cause without going off the rails completely. There really is no need to obsess over exact causes, graceful recovery strategies, etc. For most generic, consumer and industrial products having some resillience against using (obviously) wrong values will be enough. Ofcourse things are different when lives depend on it but those are not the products I'm referring to.Quote from: peter-h on November 19, 2022, 06:54:42 pmUsing a 'large enough number' to exit the loop is one way. Using a hardware timer or counter that increments by itself is another (no interrupts needed; just record the start count and break the loop if the number of counts is different enough). Time spent in interrupts or other processor doesn't matter for as long as you check whether the loop ended because the hardware finished the job or the timer expired.

author: peter-h
author_rank: Super Contributor
timestamp: November 19, 2022, 09:55:35 pm
content: Can anyone think of a way that DMA transfer might get stuck, without defective silicon?Only by some bug like a stray pointer (in another RTOS task) poking a value into NDTR, so it never reaches zero, etc...How about this?uint32_t x=0xffffffff;while (true){if (transfer ended) break;x--;if (x==0) break;}Although I can't see how much better this is than triggering the watchdog if x=0.

author: nctnico
author_rank: Super Contributor
timestamp: November 19, 2022, 10:05:45 pm
content: It all depends on how critical the DMA transfer is. If it is for an ethernet packet, it is not critical at all as either packet loss is allowed or the packet will be send again. Same for many other protocols that run over SPI, I2C, UART. Heck, even for sampling an ADC or audio stream it may not be a problem. But is all depends on whether the system is designed to re-synchronise itself or recover (for example by re-trying). If the system can re-synchronise / recover itself at a higher level then a failed DMA transfer isn't critical at all. Think about writing to an external flash. If a DMA failure causes a write or verify to fail, the high level can decide to retry or skip to the next sector. There really is no need to trigger a reset for such an event.

author: peter-h
author_rank: Super Contributor
timestamp: November 20, 2022, 08:22:25 am
content: It was only to continue the RTOS task if the DMA mysteriously failed to finish.EDIT: after extensive testing I have put this bit (in the code in my 1st post) back inCode:hang_around_us(1);SPI3->DR;SPI3->DR;SPI3->SR;Somebody somewhere went down the same rabbit hole and didn't document why, but itisneeded in some cases. Exactly which ones, would take me days or weeks of my life to establish. On the face of it, the RX DMA should always remove all data from the SPI RX channel, so maybe the SR read is what actually matters.It wastes only 1.3us so not worth worrying about. In any performance critical application one would not use DMA for moving so little data that 1.3us is relevant. In fact, various other code (e.g. the use of memcpy and such in low level USB and even ETH code) suggests that the CPU moving 32 bits at a time, 168MHz, outperforms a DMA (typically running at no more than 42MHz and probably half that) a lot of the time. I use DMA mainly to move 512 bytes but since the SPI is run in byte mode, I don't have a 16 bit or 32 bit option.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 20, 2022, 01:49:07 pm
content: Quote from: nctnico on November 19, 2022, 08:44:33 pmI got your point perfectly clear; it's just that it is wrong. Completely and totally wrong. Random bit-flips are colossally difficult to deal with, "some simple checks" are very unlikely to offer any or much help, because simple checks (such as function argument range checks, peripheral error flag checks) cover some 0.1% of memory. Clearly you have never done rad-hard (where random bitflips are considered; actual reason doesn't need to be radiation); neither have I, but I know enough to leave it to others. But, the silver lining is, you are still doing the right thing because these simple checks have much better coverage against your own bugs (or HW peripheral bugs!), and those are the real causes for problems.Communication links over unreliable physical medium is of course different. And there are those nasty cases where you are not certain whether your physical link is classified as unreliable or not. Better assume that way when unsure. But then again, bitflips in such interface can only affect the interface itself, so surface area for errors is again very small, so trivial checks like bounds checking data length field are easy to implement.

author: paulca
author_rank: Super Contributor
timestamp: November 20, 2022, 09:15:01 pm
content: I know this won't be a productive addition to the conversation, but everytime I see this thread now I can't help but think..."When you take the annoying thing back out of the microwave, the DMA transfer WILL be over."Had another evening of STM32 mystery tour of non working DMA.

author: peter-h
author_rank: Super Contributor
timestamp: November 20, 2022, 09:52:16 pm
content: The 32F4 DMAs are complicated.FWIW this is now my current code, with a primitive timeout of some tens of seconds which is way longer than any possible transferCode:volatile uint32_t limit = 0;while(true){limit--;if (limit==0) break;// Either end-transfer detection method below works but the NDTR may be less reliable// http://efton.sk/STM32/gotcha/g20.html// https://www.eevblog.com/forum/microcontrollers/32f4-3-ways-to-detect-end-of-dma-transfer/#if 0uint16_t temp1;temp1 = DMA1_Stream0->NDTR;if ( temp1 == 0 ) break;// transfer count = 0#elseuint32_t temp2;temp2 = DMA1->LISR;if ( (temp2 & (1<<5)) !=0 ) break;// TCIF0#endif// Third method of detecting end of transfer: wait for DMA to set EN to 0.// All 3 methods produce the same time from last SPI clock to CS=1 (1.8us).//if ( (DMA1_Stream0->CR & DMA_SxCR_EN) == 0) break;}I made "limit" volatile because I think the compiler would otherwise optimise it away.