This code is so broken it some how fixes itself. - Page 1
Forum section: electronics / general

author: paulca
author_rank: Super Contributor
timestamp: January 27, 2023, 08:21:59 pm
content: So this code works. It shouldn't, but it does. (Friend wrote it, acchmm). I just find it amusing how broken it is that it seems to fix itself.The SAI's are in 16bit extended.The for loop is nuts. There are not 8 16bit ints there. There are 8 8bit ints. Yet it works. In fact a whole bunch of different viarities work also.for (int i = 0; i < 8; i+=4)Is my favourite.for (int i = 0; i < 2; i++) // Nope , not even with a int32_t buffer.for (int i = 0; i < 4; i++) // Nopefor (int i = 0; i < 8; i++) // YESfor (int i = 0; i < 8; i+=2) // YESfor (int i = 0; i < 8; i+=4) // YESI think it's just lucky word alignment.Code:int16_t buffer;float gain = 0.75f;while (1) {HAL_SAI_Receive(&hsai_BlockA1, (uint8_t*) buffer, 8, 10);for (int i = 0; i < 8; i++) {double sample = buffer;sample = sample * gain; // pre-gainsample = PeakFilter_Update(&bassBoost, sample);sample = PeakFilter_Update(&midCut, sample);sample = PeakFilter_Update(&highBoost, sample);buffer = (int16_t) sample;}HAL_SAI_Transmit(&hsai_BlockA2, (uint8_t*) buffer, 8, 10);}EDIT: I'm laughing at myself but this is a classic example. Classic. Your code works first time. "That was easy". Never pay any attention. "Some time later...", "That doesn't look right... I'll take a look....""Wait. How does this work? How did this ever work? What the hell?"No... I'm not looking for help to fix it. I can see a symetry of problems that if fixes will make it make sense, but I just found it funny. Symmetrical insanity just happens to work.EDIT2: Oh and it has many other issues to boot.

author: Sherlock Holmes
author_rank: Frequent Contributor
timestamp: January 27, 2023, 09:33:35 pm
content: Quote from: paulca on January 27, 2023, 08:21:59 pmHow do you distinguish between "works" and "broken"?

author: Sherlock Holmes
author_rank: Frequent Contributor
timestamp: January 27, 2023, 09:37:58 pm
content: Note:Code:HAL_SAI_Receive(&hsai_BlockA1, (uint8_t*) buffer, 8, 10);If that 8 is the size of the buffer to write to, then only 8 bytes are being used despite the fact the declared buffer is 16 bytes long...The code declares a16 byte buffer,but it only ever needs and uses an8 byte buffer, so long as buffer is at least8 bytes long, everything is fine - is that what you're talking about though?

author: thm_w
author_rank: Super Contributor
timestamp: January 27, 2023, 10:27:14 pm
content: Quote from: Sherlock Holmes on January 27, 2023, 09:37:58 pm16 bytes are used, assumingHAL_SAI_Recieveis set to 16-bit (instead of 8-bit) in the initialization. Which OP is saying it is.I think the point is that HAL_SAI_Receive is writing to an address using 16-bit each time, but then OP thought the loop was pulling out data 8-bit at a time?Which I don't think it is, because buffer is a 16-bit array, right?int16_t buffer = {1, 2, 3, 4};buffer = 1buffer = 2

author: hans
author_rank: Super Contributor
timestamp: January 27, 2023, 11:34:35 pm
content: That function looks like typical C HAL hackery mess - where 8 defines the number of items and not the number of bytes, and the item size is configured elsewhere. It increments the dataPtr by 2 bytes at a time, and only after byte increments casts it to 16-bits for the write operation. That is functionally correct, but IMO very odd way of dealing with sizes, pointers and casts. But I rest my case, C does not really allow for a type generic implementation without either macros (= bigger mess) or C++ templates..Anyhow, if we assume that OP tested a SAI passthrough which works and does not discard any samples, then it kind of is a mystery what behaviour is observed. As Sherlock says, what does broken and what does working look like? Are there really zero differences to spot for the 3 different "YES" situations listed?Considering that the filter behaviour can be tested independently.. one may be able to generate a sweep of input frequencies into a buffer and pull it through the suspecting code. The output samples can then be plotted in e.g. a bode plot. It's a bit tedious work to set up the testbench, and perhaps there are frameworks or tools that don't require you to write everything yourself (e.g. thinking of MATLAB/Octave), but it could give some insight what the numerical operations are affecting to your raw ADC/DAC samples.However, I think some sort of unit testing is always beneficial for DSP or business logic code like this. The HAL functions serve their purposes of transferring in ADC/DAC data with some real-time element attached to it (not to cause time-related glitches etc.), but other than that the calculations can be modelled very well on a PC with unit tests instead. If that catches 95% of the mistakes before uploading to hardware, it can reduce the head scratching alot between distrusting the HAL configuration/usage and/or your own tested application code.

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 27, 2023, 11:39:48 pm
content: Quote from: hans on January 27, 2023, 11:34:35 pmSo? How is a function that takes a number of items as parameter instead of bytes hackery?The HAL is poorly documented, and that's the real issue (but we all know documentation is hard shit to get right.) When using the HAL, I've always looked at its source code when using HAL functions. Rather than trying to guess. May sound annoying, but that's as good as you'll get.

author: helius
author_rank: Super Contributor
timestamp: January 28, 2023, 12:02:02 am
content: Without refering to the code for HAL_SAI_Receive(), it's not possible to say whether it is correct, or not. I looked at the github page linked above by thm_w, but couldn't understand the workings of the function enough to say. I will note, in passing, that the function is written in an almost unbelievably stupid way, effectively testing whether it is in 8 or 16 bit modefor every single word transferred.Simply because the code calls that function using a pointer cast to (uint8_t *) doesn't really mean anything. It definitely does not mean that "there are 8 8bit ints". It doesn't even mean that the ints are unsigned! All it does is mollify the type checker that the function is being called with arguments compatible with the function's prototype. The data representation is unchanged, and this cast has no effect on further uses ofbufferlater in the caller.I've observed that many C programmers imbue the cast with magical, ineffable powers. For example, the code inside the loop which doesCode:buffer = (int16_t) sample;uses a totally pointless cast expression. When the lvalue and the rvalue in an assignment are both arithmetic types, conversion isautomatic.

author: thm_w
author_rank: Super Contributor
timestamp: January 28, 2023, 12:29:59 am
content: I don't disagree that code is not optimized, I wouldn't call it unbelievably stupid though. I doubt the performance difference is significant.

author: helius
author_rank: Super Contributor
timestamp: January 28, 2023, 12:59:37 am
content: I guess it's an opportunity for another gripe which is that hardware companies seldom understand software enough to use it properly. If the prototype for HAL_SAI_Receive() declared that the type of theDataargument wasvoid *, then callers could pass in their 8-bit or 16-bit or 32-bit arrays without any cast at all. Declaring the type asuint8_t*is very misleading since audio samples are always signed!

author: SiliconWizard
author_rank: Super Contributor
timestamp: January 28, 2023, 01:52:36 am
content: Quote from: thm_w on January 28, 2023, 12:29:59 amOptimizers almost certainly optimize this by factoring the tests. So while not pretty to read, it probably doesn't make a difference at all, unless maybe you compile with zero optimization.As to 'uint8_t *' for parameters taking buffers, except if said buffers actually make sense as a succession of 8-bit words, I'm fully against that style. It requires casting if the pointer passed is not actually a 'uint8_t *', and could be misleading indeed regarding any alignment requirement.Use 'void *', and properly document if there is any alignment requirement or not. No cast needed, no possible confusion.Note: also, it's pretty straightforward to check alignment at run time from a pointer, and I don't think they do it in the HAL (I would have to check that back.)For instance, 16-bit aligned:Code:if (((uintptr_t) pointer) % 2 == 0) {... }Not rocket science.

author: westfw
author_rank: Super Contributor
timestamp: January 28, 2023, 09:30:48 am
content: QuoteWhy do you say that? The buffer is clearly 8 16bit ints. So it's all dependent on how HAL_SAI_RECEIVE works.If HAI_SAI_RECEIVE() "Size" parameter is actually a count of data whose size was specified sometime back in initialization and is stored in the hsai handle data structure.That appears to be the case. So it's not your co-workers code that sucks, it's the HAL library (yet again.)Here's an edited copy of the function (from some STM32F4xx HAL version), with some annotations added.Code:HAL_StatusTypeDef HAL_SAI_Receive(SAI_HandleTypeDef *hsai, uint8_t *pData, uint16_t Size, uint32_t Timeout){// BLAH BLAHhsai->pBuffPtr = pData;hsai->XferCount = Size;// BLAH/* Receive data */while (hsai->XferCount > 0U) {if ((hsai->Instance->SR & SAI_xSR_FLVL) != SAI_FIFOSTATUS_EMPTY) {//***** Pick up "item" size from handle, copy that many bytesif ((hsai->Init.DataSize == SAI_DATASIZE_8) && (hsai->Init.CompandingMode == SAI_NOCOMPANDING)) {(*hsai->pBuffPtr++) = hsai->Instance->DR;} else if (hsai->Init.DataSize <= SAI_DATASIZE_16) {*((uint16_t *)hsai->pBuffPtr) = hsai->Instance->DR;//***** adjust the pointer by the size from the handle.hsai->pBuffPtr += 2U;} else {*((uint32_t *)hsai->pBuffPtr) = hsai->Instance->DR;hsai->pBuffPtr += 4U;}//****** Decrement "Size" parameter value by 1hsai->XferCount--;}// More BLAH}

author: paulca
author_rank: Super Contributor
timestamp: January 28, 2023, 10:01:55 am
content: It has many issues, not one. The size of what SAI reads and how the loop goes through it appear to only be part of the problem.Anyway. It shall be rewritten. First off, the TX should be interrupt, the handling code should be in the ISR. I shoudn't be calling the SAI HAL function, because I explicitly want to copy the contents of the SAI FIFO only. I also don't want the FIFOs to block or be consumed if that makes sense, I just want them to free run. If I read an input SAI at twice the sample frequency I want to receive duplicate FIFO contents. If HAL decides to go around it's 1ms Tick delay loop it will get ugly. I also really don't need it to check anything, at this stage all I want it to do really is give me the contents of the Rx FIFO, verbatim as 2x32bit ints. I'll do the rest. That should be a simple read and shift the 8 bytes out of the hardware array. No need for HAL abstractions for that part. I have a limited cycle budget obviously.I just found it funny that it worked, like I wrote it out in more or less one go (i've played a lot with the eq params), ran it and it played music with EQ without distortion. I've been using it on trial for a week now. It was only when I read what I'd written I went, "Wait... hang on."

author: paulca
author_rank: Super Contributor
timestamp: January 28, 2023, 10:03:20 am
content: Quote from: helius on January 28, 2023, 12:02:02 amI believe the term used is "explicit cast" where an "implicit cast" would have occurred anyway. There is a reason why it exists and even has a name for the technique.

author: Siwastaja
author_rank: Super Contributor
timestamp: January 28, 2023, 04:06:12 pm
content: Being explicit over implicit is not "totally pointless"; code is written to communicate the intent to both human readers, and compilers.I really struggle to see why most libraries are so poorly documented. I mean, given typical 3-4 arguments for a function, it takes maybe 5-10 minutes to write a simple comment which lists all the arguments and clearly explains what they are. Heck, I consider myself a lazy documentation writer yet my own internal functions are usually better documented than ones in widely used libraries.There should never be confusion if a parameter is a count or size. Count should be named count, and size named size, and then the comment should read:// number of itemsor// size in bytesIt's sad such a thing even needs to be discussed, but result of absolute laziness during code writing wastes a lot of time afterwards. Why don't standards like MISRA C mandate commenting the purpose and usage of each argument and return value of non-static functions?

author: paulca
author_rank: Super Contributor
timestamp: January 28, 2023, 04:27:15 pm
content: Quote from: Siwastaja on January 28, 2023, 04:06:12 pmIndeed. It can also catch some fairly nasty surprise bugs should someone want to attempt changing the type of the receiver variable to see what happens. Probably not a big concern in a single C function, but good habits are habits.

author: westfw
author_rank: Super Contributor
timestamp: January 28, 2023, 08:48:06 pm
content: QuoteAh, but those are "failures of design" rather than "failures of coding."

author: Leeima
author_rank: Contributor
timestamp: January 28, 2023, 09:11:39 pm
content: Quote from: helius on January 28, 2023, 12:59:37 amIn my experience I've noticed the inverse is often true! Software companies seldom understand hardware enough to use it properly!

author: helius
author_rank: Super Contributor
timestamp: January 28, 2023, 09:21:31 pm
content: FromThe Hacker's Dictionary:Quote

author: paulca
author_rank: Super Contributor
timestamp: January 29, 2023, 02:59:10 pm
content: Well, this is my second attempt at the same:Caveats: It's hardcoded for 1 Rx 1 Tx for now and fresh out of the fingers.Code:uint32_t rx1Buffer;uint32_t tx1Buffer;void SAI_Tx_Interrupt(SAI_HandleTypeDef *hsai) {// Get rx1 it statusuint32_t ite = NVIC_GetEnableIRQ(SAI1_IRQn);// Shhh itNVIC_DisableIRQ(SAI1_IRQn);// Move its FIFO to Tx buffertx1Buffer = rx1Buffer;tx1Buffer = rx1Buffer;// Release itif (ite) {NVIC_EnableIRQ(SAI1_IRQn);}EQ_Run(tx1Buffer);/////////////////////////////* Transmit 2x32 bits of data */hsai->Instance->DR = tx1Buffer;hsai->Instance->DR = tx1Buffer;}void SAI_Rx_Interrupt(SAI_HandleTypeDef *hsai) {// Dont want the Tx reading as we writeuint32_t ite = NVIC_GetEnableIRQ(SAI2_IRQn);NVIC_DisableIRQ(SAI2_IRQn);/////////////////////////////* Receive 2x32 bits of data */rx1Buffer = hsai->Instance->DR;rx1Buffer = hsai->Instance->DR;if (ite) {NVIC_EnableIRQ(SAI2_IRQn);}}Much less code. A basic "borrow and strip" approach from HAL in most cases. "Works" in that it through-puts music with EQ. I have noticed one or two clicks, so it might need a bit of tweaking.The handling of the actual 16bit (in this case) audio, now becomes much cleared.The raw 32bit words contain right hand justified signed 16bit words.Code:void EQ_Run(uint32_t*buf) {int16_t *buffer = (int16_t*)buf;for (int i = 0; i < 4; i+=2) {double sample = buffer;Now that's a bit clearer, I can try 24bit.EDIT: I'm still a little concerned about:for (int i = 0; i < 4; i+=2) {Which could be "left" justified surely?

author: Siwastaja
author_rank: Super Contributor
timestamp: January 29, 2023, 03:53:54 pm
content: Geez, if you want to control how interrupts pre-empt each other, just set the priorities accordingly (NVIC_SetPriority(IRQn, priority)) once. Enabling and disabling other interrupt sources from other ISRs is an ugly hack, there is absolutely no need for this kind of crap, it very likely has some nasty corner cases.

author: paulca
author_rank: Super Contributor
timestamp: January 29, 2023, 04:55:45 pm
content: Quote from: Siwastaja on January 29, 2023, 03:53:54 pmI know. Giving them equal prio and equal sub-prio and they shouldn't interrupt each other.But.... the thought I had, was, when there are more than one Rx SAI, I wanted to limit the granularity of the interrupts, not disable them entirely. EG:Silence Rx1Copy Rx1 bufferRelease Rx1Silence Rx2Copy Rx2 bufferRelease Rx2Silence Rx3Copy Rx3 bufferRelease Rx3Might be over kill / unnecessary... or might make things wose!

author: paulca
author_rank: Super Contributor
timestamp: January 30, 2023, 05:18:36 pm
content: Good news, bad news.Good news, it works and after some tweaking and testing, it's verified as working.Bad news. 1 stereo input + 6 peak filters = 69% duty cycle. Actually that makes it sound better than it is, as many things upset it skipping into half sample rating the output to 24k. That's 48k 16bit too. Dreams of 96k and 24bit are dashed entirely.I can optimize, but I'm not sure I'm going to make the following filter code at least twice as fast.??Code:float PeakFilter_Update(PeakFilter_t *filt, float in) {filt->x = filt->x;filt->x = filt->x;filt->x = in;filt->y = filt->y;filt->y = filt->y;filt->y = (filt->a * filt->x + filt->a * filt->x+ filt->a * filt->x+ (filt->b * filt->y + filt->b * filt->y)) * filt->b;return (filt->y);}

author: paulca
author_rank: Super Contributor
timestamp: January 30, 2023, 05:25:51 pm
content: Caps showing the Tx and Rx interrupt duty cycles.First with 2 samples.Second with 8 samples.As an aside. I had a rather unpleasant practical in Nyquist aliasing and artefacts when I tripple checked the 24kHz output sample rate, but.... playing a 20kHz sine wave through it. My recommendation is... Do not try this at home. It's unpleasant. I don't think the PCM5102's filterless output filter liked it. As best I could determine on an FFT it was warbing between 3.8 and 4.2 kHz!

author: paulca
author_rank: Super Contributor
timestamp: February 01, 2023, 09:45:25 am
content: It would seem I under-estimated just how well the code optimizes. -Ofast and -O3 both take it from a 67% ish DC to about 21%. Impressive.That changes the outlook towards the positive. I've already spoken too soon on this bit of code about 3 times now, but ... a basic test, non conclusive, suggests I might squeeze a 5 Band EQ + a 3 Band EQ and dual outputs and at 96kHz.Testing should reveal. I expect it might be tight, but in theory it can be tight and still work if interrupts are under control.

author: cv007
author_rank: Frequent Contributor
timestamp: February 01, 2023, 02:43:45 pm
content: The use of unions/structs can also be easier to use than casting, especially when done on a single type in various places. The casting gets eliminated and each use will be easier to understand (your 'casting' equivalent takes place only once when you create the union/struct). There are many ways one could go about this, but the following is one example-Code:static const uint32_t SAI_BUF_SIZE = 2;typedef union {volatile uint32_t dr_u32;volatile int16_t eq_i16;} sai_buffer_t;sai_buffer_t rx1Buffer;sai_buffer_t tx1Buffer;//usage//maybe always 2, don't know, but can use loop in any case as optimizer will figure it out and produce optimal codefor(int i = 0; i < SAI_BUF_SIZE; i++ ){tx1Buffer.dr_u32 = rx1Buffer.dr_u32;}EQ_Run( tx1Buffer );void EQ_Run(sai_buffer_t* buf) {for (int i = 0; i < SAI_BUF_SIZE; i++) {double sample = buf.eq_i16;...Obviously lots of options and whether this one is better or not, I like to eliminate casting if there is a better option. In this case I would rather deal with the union than to have to (re)think about how the underlying data fits together when casting.

author: paulca
author_rank: Super Contributor
timestamp: February 01, 2023, 03:23:33 pm
content: Quote from: cv007 on February 01, 2023, 02:43:45 pmTrue. I was looking into unions for something else yesterday I can see how they can help with the constant duplicity of their types between signed 16 and unsigned 32.It also depends on how the compiler optimises things.That part of the code will change, as I have to try 24bit and you can't just "cast" right justified signed 24bit as easily as 16.

author: cv007
author_rank: Frequent Contributor
timestamp: February 01, 2023, 04:04:10 pm
content: QuoteCode:typedef union {volatile uint32_t dr_u32;volatile int16_t eq_i16;volatile int32_t eq_i24 : 24;} sai_buffer_t;When used, the compiler will sign extend the eq_i24 to an int32_t.edit-could also have a single member which would work the same for i16/i24, each sign extending to i32 when used-Code:static const unsigned SAI_DATA_BITS = 16;union sai_buffer_t {volatile uint32_t dr_u32;volatile int32_t data : SAI_DATA_BITS;};

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 01, 2023, 05:32:57 pm
content: I habitually useCode:typedef struct {union {double d;float f;uint64_t u64;int64_t i64;uint32_t u32;int32_t i32;uint16_t u16;int16_t i16;uint8_t u8;int8_t i8;unsigned char uc;signed char sc;char c;};} word64;typedef struct {union {float f;uint32_t u32;int32_t i32;uint16_t u16;int16_t i16;uint8_t u8;int8_t i8;unsigned char uc;signed char sc;char c;};} word32;to examine the various parts of data elements, and reinterpret the storage as another type. For example,((word32){ .f =value}).u32returns the 32-bit storage representation offloatvalue(and the code kinda-sorta assumes IEEE 754 Binary64 and Binary32 floating point formats).When optimized, the machine code simplifies either to nothing (ARMv7-a with hardware floating point support) or a single register move (x86-64), too.If you have a buffer with 24-bit signed integer or fixed-point samples, you can easily expand it to 32-bit signed integer or fixed point usingCode:static inline void i24_to_i32_quad(int32_t *dst, const uint32_t src0, const uint32_t src1, const uint32_t src2){*(dst++) = ((int32_t)( src0 << 8 )) >> 8;*(dst++) = ((int32_t)( (src1 << 16) | ((src0 >> 24) << 8) )) >> 8;*(dst++) = ((int32_t)( ((src1 >> 16) << 8) | (src2 << 24) )) >> 8;*(dst++) = ((int32_t)( src2 )) >> 8;}void i24_to_i32(int32_t *buf, const size_t count){const size_t quads = (count / 4) + !!(count & 3);const uint32_t *src = (uint32_t *)buf + 3*quads;int32_t *dst = buf + 4*quads;while (dst > buf) {dst -= 4;src -= 3;i24_to_i32_quad(dst, src, src, src);}}but the buffer size must be padded with zeroes to a multiple of 16 bytes (orcounta multiple of 4), and have room for 4 bytes per 24-bit value. As it progresses backwards from end of array to beginning of array, the conversion is done in-place, in units of 12 bytes in, 16 bytes out. This does do sign extension correctly, too.Note that it does expect(negative integer) >>Nto be arithmetic shift right; it is on GCC and Clang.With GCC and Clang, one can also useCode:typedef struct {union {unsigned int u24:24;int i24:24;uint8_t u8;int8_t i8;char c;} __attribute__((packed));} __attribute__ ((packed)) word24;typedef struct {union {uint32_t u32;int32_t i32;word24 w24;uint16_t u16;int16_t i16;uint8_t u8;int8_t i8;char c;};} word24x4;void unpack_word24x4(int32_t out, const uint32_t in){const word24x4 temp = { .u32 = { in, in, in } };out = temp.w24.i24;out = temp.w24.i24;out = temp.w24.i24;out = temp.w24.i24;}but this relies on thepackedtype attribute limiting the size of theword24type to exactly three bytes, with byte alignment; this is true for GCC and Clang, but is not "standard" C. (Plus right shift with signed integer types being arithmetic shift right.)As shown,unpack_word24x4()takes 12 bytes (in three 32-bit unsigned integers), and expands them to four signed 32-bit integers (with sign extension as expected).Note thati24_to_i32_quad()is faster thanunpack_word24x4()on 32-bit ARMv7 at least, if one examines the code at Compiler Explorer.That is,Code:void unpack_word24x4(int32_t out, const uint32_t in){out = ((int32_t)( in << 8 )) >> 8;out = ((int32_t)( (in << 16) | ((in >> 24) << 8) )) >> 8;out = ((int32_t)( ((in >> 16) << 8) | (in << 24) )) >> 8;out = ((int32_t)( in )) >> 8;}is faster but harder to maintain than the other version above, on ARMv7 at least; both do yield the exact same results.

author: paulca
author_rank: Super Contributor
timestamp: February 01, 2023, 05:44:40 pm
content: Most of that went over my head. However, the basic filters / processing I am using are only there because they were the first ones I found in a palatable (theivable) form.I really should try and get biquad equivalents and use the arm math library with DSP optimisations. I might for example get a wider range of high order filters.So I will return and review the data packing/unpacking at the same time.For now I think I'm going to do a blunt ignorant shift and or like the HAL code doesOn the optimisation... my assumption was that the C compiler would include things like hardware DP FPU and DSP extensions if they existed with no optimisations.However now I'm wondering if it actually disables all of that so you can step over some floating point calcs in the debugger, where as if they are offloaded to teh FPU that becomes difficult?I mean how much "extra" stuff is turned on for -O3, -Ofast

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 01, 2023, 08:45:52 pm
content: Quote from: paulca on February 01, 2023, 05:44:40 pmSorry!To summarise, if you have arrays of 24-bit samples, three bytes per sample, I showed that it is best to handle them in groups of four samples (12 bytes), and very efficiently sign-expand to 32-bit. The rest was details, and two different approaches you can choose from, although one has better performance.Quote from: paulca on February 01, 2023, 05:44:40 pmSTM32F4 series has an ARM Cortex-M4 core, and thus ARMv7E-M architecture.ST'sAN4841: Digital signal processing for STM32 microcontrollers using CMSISdescribes its DSP features (but also includes some of the STM32F7 series).Since GCC and Clang try to implement ARM C Language Extensions,ACLE (ihi0053)is also useful.In general, C is not an easy language to automatically SIMD-vectorize. For STM32F4, there are instructions that add, subtract, and/or multiply using pairs of signed or unsigned 16-bit values in each register, or quartets of 8-bit signed or unsigned values in each registers.As of right now, you do need to use the ACLE macros to SIMD-vectorize your C code.For example, let's say you have an array of 16-bit signed integer samplesdata, another array of 16-bit signed integer coefficientscoefficient, both arrays havingcountelements (even; multiple of twocount), and both arrays being 32-bit aligned (__attribute__((align (4)))), you can obtain the 64-bit sum (which cannot overflow since each product is between -0x3FFF8000 and +0x40000000, i.e. 31 bits):Code:#include <arm_acle.h>typedef struct {union {uint32_t u32;int32_t i32;uint16_t u16;int16_t i16;uint16x2_t u16x2;int16x2_t i16x2;uint8_t u8;int8_t i8;uint8x4_t u8x4;int8x4_t i8x4;char c;};} word32;int64_t sum_i16xi16(const int16_t *data, const int16_t *coeff, const uint32_t count){const word32 *dz = (const word32 *)data + (count / 2);const word32 *d = (const word32 *)data;const word32 *c = (const word32 *)coeff;int64_t result = 0;while (d < dz)result = __smlald((*(d++)).i16x2, (*(c++)).i16x2, result);return result;}using-O2 -march=armv7e-m+fp -mtune=cortex-m4 -mthumb(with gcc; use -Os instead of -O2 with clang).The inner loop will compile to.L3:ldr r0, , #4 ;2 cyclesldr r2, , #4 ;2 cyclescmp ip, r3, #1 ;1 cyclesmlald r4, r5, r2, r0 ;1 cyclebhi .L3 ;2-5 cycles when takenwhich by my count should yield 8 cycles, or 4 cycles per sample, or equivalently 0.25 samples per cycle.Furthermore, the result is trivial to shift and then clamp to the proper range, so it is just a few additional cyclesper bufferto adjust for the fractional bits in samples and/or coefficients.Me Like Dis.Ifcountis odd, the last sample and coefficient will be ignored. I suggest you ensure your array sizes are even, and set the final element of both arrays to zero, so you can roundcountup to the next even number without affecting the result.The inner loop is equivalent toCode:for (uint32_t i = 0; i < count/2; i++)result = __smlald(d.i16x2, c.i16x2, result);but is written in a form that both gcc and clang can optimize to the above code or equivalent.Each loop iteration computes two 16-bit products, summing them toresult.

author: paulca
author_rank: Super Contributor
timestamp: February 01, 2023, 09:24:12 pm
content: It's an H7, but I get the idea

author: paulca
author_rank: Super Contributor
timestamp: February 01, 2023, 09:26:22 pm
content: While I have you(sorry).Is there a quick win to gain access to the DTCM SRAM? Googling finds me long linker script modifications.Can't I just ignorantly use hard coded offsets by casting an integer like 0x200000 to a pointer? I'm only declaring 4 tiny arrays.

author: thm_w
author_rank: Super Contributor
timestamp: February 01, 2023, 10:21:59 pm
content: Guess its harder than ITCM for some reason?Quotehttps://github.com/STMicroelectronics/STM32CubeH7/blob/master/Projects/STM32H747I-EVAL/Templates_LL/readme.txthttps://community.st.com/s/question/0D53W00001sKgU0SAK/stm32cubeide-doesn-not-support-declaring-variable-in-dtcm-on-stm32h7-directly

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 01, 2023, 11:18:13 pm
content: Quote from: paulca on February 01, 2023, 09:24:12 pmH7 is Cortex-M7, so also ARMv7E-M, but with newer FP with double precision support (+fp.dp), so you'll want to usegcc -O2 -march=armv7e-m+fp.dp -mtune=cortex-m7 -mthumb ...orclang -Os -target=arm-arm-none-eabi -mcpu=cortex-m7+fp -mthumb ...to compile the code. I use these for i.MX RT1062 (Teensy 4.x), too: it has a very similar ARM Cortex-M7 core (but NXP, not ST).The same documentation applies.The cycle counts are likely completely off, though, since I can find them only for Cortex-M0 and Cortex-M4, not Cortex-M7.Quote from: paulca on February 01, 2023, 09:26:22 pmYou can use a fixed pointer to access DTCM SRAM. Thing is, how will you ensure the addresses you use are not used by the compiler and/or linker already?That is why modifying your linker script, typically by declaring a section that will be placed in the correct addresses, is used. If you ensure section "dtcm" is placed in the correct region, thenstatictypearrayname __attribute__((section "dtcm"));puts the array at the correct section, so it will end up in the correct memory addresses, and the linker will decide its exact address. Without modifications or support from the startup (startup.s, I believe), it will be uninitialized (have random contents) after bootup.If you can show the linker script you already use (look for "Linker script for STM32H7 series", file name probably ends with .ld), and it does contain the"This software component is licensed by ST under BSD 3-Clause license", then you are allowed to publish it here and I can show how to modify it to add such a section. Most likely, it is just one added line.

author: paulca
author_rank: Super Contributor
timestamp: February 14, 2023, 06:25:46 pm
content: Looked back into this today.It would seem the linker script is already setup to use the DTC RAM. However, most things get allocated in RAM_D1 'above' it.Linker script attached. I believe it's BSD license.Any quick wins?

author: eutectique
author_rank: Frequent Contributor
timestamp: February 14, 2023, 06:58:05 pm
content: Quote from: paulca on February 14, 2023, 06:25:46 pmWhat are these things? What the map file shows? Any insights fromarm-none-eabi-objdump -h yourfile.elf? Are some symbols matching.data.*pattern located in RAM_D1 and some in DTCMRAM?BTW, your linker script does not have section RAM_D1. Are you sure about it?

author: paulca
author_rank: Super Contributor
timestamp: February 14, 2023, 07:37:34 pm
content: Quote from: eutectique on February 14, 2023, 06:58:05 pmSorry, there are two linkers. The other one is attached,.The "other" things are general variables, buffers, (.bss and .data I will assume)The idea is to create an labelled section like .dtcm and allocate some of the buffers in there.

author: eutectique
author_rank: Frequent Contributor
timestamp: February 14, 2023, 08:47:38 pm
content: Quote from: paulca on February 14, 2023, 07:37:34 pmOk, then create it!Add the following to the linker script:Code:.dtcm_section {KEEP(*dtcm*)} > DTCMRAMand the following, as suggested by Nominal Animal, to your source code:Code:static type arrayname __attribute__((section "dtcm"));

author: paulca
author_rank: Super Contributor
timestamp: February 21, 2023, 04:50:30 pm
content: Thanks! Got it working.Adding /modified a few bits though.Code:__attribute__((section(".dtcm_section"))) static uint32_t rx1Buffer;__attribute__((section(".dtcm_section"))) static uint32_t rx2Buffer;__attribute__((section(".dtcm_section"))) static uint32_t tx1Buffer ;__attribute__((section(".dtcm_section"))) static uint32_t tx2Buffer;Code:.dtcm_section :{. = ALIGN(4);KEEP(*dtcm*). = ALIGN(4);} > DTCMRAMI might test how much impact it has with the scope next time I'm checking the performance/timing.