Painfully slow SPI on STM32F405 - Page 1
Forum section: electronics / general

author: tyguy2
author_rank: Unknown
timestamp: July 30, 2020, 07:16:49 pm
content: Hello all!I've been working on building a project based on the Adafruit STM32F405 Feather Express. My main goal is to be able to control an 8 channel DAC, with each channel being able to put out at maximum a 4 kHz sawtooth wave independently of each other (i.e. channel 1 outputs a 255 Hz sawtooth, channel 2 outputs a 4 kHz sawtooth, etc etc). On paper, this should work, as the DAC I've selected, the LTC2636, can handle SPI bus speeds up to 50 MHz. Even if the SPI bus runs at a slower speed, I should have ample headroom to do this. The STM32F405 runs at 168 MHz, and even if the SPI bus ran at a fraction of that speed, it would still be well within 50 Mhz.Based on this line of reasoning, I built up a PCB based on the Adafruit STM32F405 schematics (Seen here), with the only alteration being that the SPI bus going to the SPI flash instead goes to the LT2636. After a few edits to the Feather's variant.h files (changing the default SPI bus the SPI library uses to SPI1 instead of SPI2) for Arduino, I had the SPI bus communicating with the DAC. To my surprise, however, the communication speed was ***painfully*** slow. Running the code I have below, I was only able to muster 1.69 kHZ on 1 channel. At first I thought the digitalWrite function was slowing me down, but even with direct port manipulation (see other code below, I replaced digitalWrite with the sections labelled HIGH and LOW), I was still bottle-necked, with no improvement to my transfer speed. Based on readings from my (unfortunately analog) oscilloscope, as well as some tinkering with the clock speed on the code, I *think* the bus is being limited to 12 Mhz, but it could be much less, it's hard to tell with the instruments I currently have. Based on my reading of the datasheet for the STM32405, SPI1 is limited to 42 Mbits/s, which would have a clock speed well over 12 Mhz.I don't know why this is happening, does anyone know how I can increase the transfer speed of the SPI bus? I would seriously appreciate it!Code:#include <SPI.h>#define CS 7int i = 0;void setup() {pinMode(CS, OUTPUT);digitalWrite(CS, HIGH);SPI.begin();}void loop() {i = i + 30;if (i > 1023){i = 0;}dac8Write(1,i);}void dac8Write(byte channel, int input) {//Warning: Channel must be less than 7!//0011 Command - Write to and Update DAC//0xxx Address - Binary address of DAC's 1-8//xxxxxxxxxx - 10 bit analog valuechannel = 0x30 | channel;input = input << 6;// Send as command, address, then 10 bit valueSPI.beginTransaction(SPISettings(50000000, MSBFIRST, SPI_MODE0));digitalWrite(CS, LOW);SPI.transfer(channel);SPI.transfer(highByte(input));SPI.transfer(lowByte(input));digitalWrite(CS, HIGH);SPI.endTransaction();}Code:void setup() {// put your setup code here, to run once:pinMode(PA15, OUTPUT);//PA15 -> Arduino Pin 7}void loop() {//(HIGH)GPIOA->BSRR = 0b1000000000000000;//(LOW)GPIOA->BSRR = 0b1000000000000000 << 16;}

author: andersm
author_rank: Super Contributor
timestamp: July 30, 2020, 07:36:20 pm
content: Check your clock configurations, and verify the device is actually running at the speed you think it is.

author: tyguy2
author_rank: Unknown
timestamp: July 30, 2020, 08:02:51 pm
content: Checked the clock config files (see attached photos), and based onthis website, the equation for the core clock is:SystemCoreClock = ((INPUT_CLOCK (HSE_OR_HSI_IN_HZ) / PLL_M) * PLL_N) / PLL_PFrom my variant.cpp file, this equation would come out to : ((12000000 / 12) * 336) / 2 = 168,000,000 Hz = 168 MhzCould it have something to do with the RCC_ClkInitStruct.APB1CLKDivider / RCC_ClkInitStruct.APB2CLKDivider instructions?I don't really know how to physically check the speed the micro is running, is there a way to do it with a J Link or something similar? The delay and millis() functions seem to be matching what I would expect, timing-wise, but that's not really an indicator of internal speed.

author: thm_w
author_rank: Super Contributor
timestamp: July 30, 2020, 08:46:38 pm
content: QuoteSo the clock frequency you see on the oscilloscope is 12MHz (83ns)?SPI1 can do 42mbit, but it looks like SPI2/3 are limited to 21.42mbits/s = 42MHz.Its likely with arduino that they've limited it to a safe bus speed, to allow operation with legacy peripherals, slower parts etc.You can use CubeMX to see how the clock system works. If your SYSCLK is 168MHz then you need to ensure that:AHB prescaler = divide by 1RCC_ClkInitStruct.APB2CLKDivider = divide by 2SPI1 baud prescaler = 2 (SPI_BAUDRATEPRESCALER_2)edit: from above those first two are there, so just need the third, and I assume your crystal used is 12MHz.

author: tyguy2
author_rank: Unknown
timestamp: July 30, 2020, 09:06:37 pm
content: Quote from: thm_w on July 30, 2020, 08:46:38 pm~12 Mhz is when altering the specified clock speed in the code made no more difference in physical update rate of the DAC, I can go back to the board with the oscilloscope and see what else I can figure out. The issue is my scope is analog and my probes aren't the best, so it can't trigger well on SPI signals and looks hazy. I'll go in with some better probe grounding and see if I can confirm 12 Mhz.Quote from: thm_w on July 30, 2020, 08:46:38 pmYes, forgot to mention, my crystal is 12 Mhz. How would I implement these edits into the Arduino environment/ the firmware of the STM32F405? Would I make edits to the variants.h file? Would I use CubeMX and upload directly to the chip?

author: thm_w
author_rank: Super Contributor
timestamp: July 30, 2020, 10:13:06 pm
content: Quote from: tyguy2 on July 30, 2020, 09:06:37 pmMight help to have a small loop that constantly sends data non-stop, could be easier to trigger.Digital scope or cheap logic analyzer would be good to have ($10 = 24MHz).QuoteNo cubemx is just to visualize the settings, or if you wanted to create an entirely new project.The baud rate divider could be in variant.cpp or in the arduino SPI setup function.This page indicates there is a function "SPI.setClockDivider" that might do it.https://circuitdigest.com/microcontroller-projects/stm32-spi-communication-tutorial

author: westfw
author_rank: Super Contributor
timestamp: July 31, 2020, 02:47:42 am
content: QuoteI'm pretty sure that you don't need to call beginTransaction() for every transfer, especially if you are managing your own CS pin.beginTransaction() completely re-initializes the SPI peripheral, which is bound to be slow.I don't think that you need to call it at all, despite the comment. It looks like begin() does all the necessary stuff.

author: tyguy2
author_rank: Unknown
timestamp: July 31, 2020, 07:33:28 am
content: Quote from: westfw on July 31, 2020, 02:47:42 amThis definitely sped things up, I'm up to 4.82kHz on one channel, but I'm still not getting the speed I need.Did more probing, and found that the CS line on the chip is low for 5.27 microseconds. Since 24 bits are being sent over the bus in this 5.27 microseconds, I can take (5.27*10^-6)seconds/24 bits = 2.19583333e-7 seconds/ bit. Taking the inverse of that we find the bus is operating at around 4 Mhz. I noticed in the SPI library C files the default bus speed is 4Mhz, so sounds like it's not using my specified speed. Any ideas?

author: westfw
author_rank: Super Contributor
timestamp: July 31, 2020, 09:37:18 am
content: Did you leave in at least one call to beginTransaction? I don't see the "speed' getting set anywhere else.I'm just looking at source code:https://github.com/adafruit/Arduino_Core_STM32/blob/master/cores/arduino/stm32/spi_com.chttps://github.com/adafruit/Arduino_Core_STM32/blob/master/libraries/SPI/src/SPI.cppfrom sort of "general principles"; I don't actually know anything about SPI on the ST chips :-((And the source code gets difficult to read because of all the conditionals and such. At this point, I'd probably write some code to dump the SPI peripheral registers to the serial port, and see if they match what the datasheet says. It's educational, and I've done similar things on other processors... (likehttps://github.com/WestfW/ArduinoZero-PMUX-report))

author: newbrain
author_rank: Super Contributor
timestamp: July 31, 2020, 09:50:01 am
content: At a first glance it would seem that the SPI is initialized if the first time .transfer is called the pins have not been set.Still, better add an explicit initialization.Since the library is internally using the HAL, plus other multiple layers of indirections (C++ wrapper on C wrapper on HAL wrapper on the actual registers...), I would experiment with putting your three byte sized values in a small array, and initiate a single transfer using the:Code:void SPIClass::transfer(uint8_t _pin, void *_buf, size_t _count, SPITransferMode _mode)overloaded member function.At least all the overheads are incurred only once per transfer.EtA: As usual, (6a) ofRFC1925applies...

author: AndyC_772
author_rank: Super Contributor
timestamp: July 31, 2020, 01:17:22 pm
content: It sounds to me as though there's a couple of unrelated problems here.One is that you don't know for sure what the SCK rate is. I use the exact same ST device all the time and SPI1 definitely works happily at 42 MHz. Make sure the GPIOs used for SPI (I use PA5 as SCK and PA7 as MOSI) are configured to high speed mode, AF 5. Read out the contents of SPI1->CR1 and check the value against the reference manual. Also try writing a tight loop that just writes 0x55 into SPI1->DR and probe SCK and MOSI to see what comes out.The other is that you don't know what the library functions are doing or how much time they're wasting unnecessarily. Try putting a GPIO write to a test point in between each function call, and use your scope to measure the interval between each GPIO state change - it'll give you an idea of where the CPU is spending time. Seriously consider not using HAL but instead reading the reference manual and writing register settings directly; my code became a whole lot easier to write, more compact and more reliable as soon as I made this decision.To actually achieve 42 MHz transfer rate you'll need to use DMA. If you're sending a known, repetitive signal, then you should be able to do that with a circular buffer which may require no further code to run at all once it's set up.

author: newbrain
author_rank: Super Contributor
timestamp: July 31, 2020, 02:23:10 pm
content: Quote from: AndyC_772 on July 31, 2020, 01:17:22 pmThat's what I like in Arduino.It lures newcomers with the mirage of effortless programming, and it works for for simple things.As soon as you need to do something non trivial, one more or less has to ditch or work around it, so it becomes a hindrance rather than a help, with the rigid program structure and the lasagna style libraries...But hey, it's always a good opportunity to learn and a decent bait.The HAL for SPI does the usual bit of housekeeping, then transmits the payload. But, at the end, it will wait for everything being transmitted on the wire: sending a byte at at the time will cost a lot of wasted cycles, waiting for the end of physical transmission, rather than an empty transmit register.Coalescing the three calls into a single transfer would already be an improvement.Using DMA with this specific DAC is not easy, as it needs the rising edge of CS to execute the received 24 bits command/data.So, there goes the possibility of a circular buffer with a continuous DMA...Doing DMA for 3 bytes at the time would still give a boost, but probably not a night to day change, once the surrounding housekeeping is counted in.

author: AndyC_772
author_rank: Super Contributor
timestamp: July 31, 2020, 03:25:57 pm
content: Quote from: newbrain on July 31, 2020, 02:23:10 pmAh, that's a pity. If I were feeling masochistic, I might suggest programming one of the timers to generate the chip select. Or maybe use it as an excuse to add a CPLD.I'm not sure I could willingly give up the elegance of having the DMA controller do all the shuffling of bytes around, so my CPU could focus exclusively on other stuff.

author: hans
author_rank: Super Contributor
timestamp: July 31, 2020, 09:16:38 pm
content: I think you can use 1 timer to create the sample clock. You can use 1 compare/match output to generate the SPI CS using a PWM output. A second (internal) compare channel (e.g. DMA request stream/channel TIMx_CH2) can initiate a SPI DMA transfer by writing to the SPI data register, and the timer auto-repetition or overflow value resets the timer with the right period, thus creating the desired sample frequency.Using 2 compare channels for CS and DMA req allows you to program their relative delay, in which you can stretch CS for X us and then initiate the SPI transfer at Y us delay.If you are really mistrusting the settings programmed by the library into the registers, you can also inspect them yourself. E.g. take a look at the value of print SPI2->CR1, and look at bits 5:3 for the baudrate control. From the user manual, it looks like the output clock is fPCLK/ 2^(1+n), where n is the decimal value of bits 5:3Also, I think that if you write a loop of while(1) { SPI2->DR = 0xF0; } should give an indication of how fast the SPI runs. F0h means bits 11110000 are sent, so the measured frequency on the data pin is 1/8 of the SPI clock, and should be easy to trigger on (even with a slower scope) since the digital waveform is very repetitive..

author: Silenos
author_rank: Regular Contributor
timestamp: July 31, 2020, 09:50:59 pm
content: My advice, before my moaning against that arduino/HAL piles, is to make your current code to set the SPI speed to minimal 42MHz/256* or alike, so your tools are able to measure the SCK frequency and dead times between CS toggles. Trigger on CS edge. And then assume you use your libraries correctly and they actually work. If the clock is still bad/unexpected then I guess not much remains between you and Reference Manual: dumping all registers related to clocks settings and analyze their contents.*Such value come from clock tree and available prescaler values. I doubt all those libraries are aware of all these constraints. I wonder what happens if you tell your SPIsettings to run with 1,3457 MHz clock.Oooh, don't those F4s have that crappy SPI ip without working NSS full hardware control? Yes, that maaybe miiiight be 'masochistically' worked around, but I wouldn't tell my boss "I'll make this F4 run in no time 40 MHz dmaed or not, anytime, perfectly in DAC specs, no worries". Not even mentioning Arduino or w/e. And that max 16 bit frame, oh.I would test it on dev board first before commiting and running custom board.

author: westfw
author_rank: Super Contributor
timestamp: July 31, 2020, 10:22:04 pm
content: QuoteSo, wait a sec...4kHz output * 1024 samples/waveform * 3bytes/sample * 8bits/byte = 90Mbits/s(just for one channel!)

author: tyguy2
author_rank: Unknown
timestamp: July 31, 2020, 10:59:44 pm
content: Quote from: westfw on July 31, 2020, 10:22:04 pmI don't need 1024 samples/ waveform, I just need something semi-decent looking at this point.I'm talking with a few other's on the STM32 forums, and someone is suggesting this as a replacement for SPI.transfer:Code:void SPI_TRANSFER (uint8_t x) { // 0.3uS overhead between bytes when optimized // 3.25 uS unoptimized//SPI.transfer(x); // send byteSPI1->DR = (x) ;while (!(SPI1->SR & SPI_SR_TXE) ); // wait untill send buffer is empty};I'm guessing this is the writing to buffer someone is referring to, unfortunately it doesn't seem to work. Apologies if I'm slow to understand, I haven't worked this in-depth with microcontroller coms before. How would I implement DMA/ any kind of improvement, at a code level? Could someone provide an example of some snippet of code I can start to dissect?

author: westfw
author_rank: Super Contributor
timestamp: July 31, 2020, 11:23:14 pm
content: QuoteBut the test code you posted DOES 1024 samples/waveform.If you're getting 4+kHz out of that code, that could be proof of quite reasonable SPI performance...

author: tyguy2
author_rank: Unknown
timestamp: July 31, 2020, 11:31:50 pm
content: Apologies! I forgot to edit that code, it's not i++, it's currently i = i + 30, so I'm not getting 1024 samples. Here's the current code, with the edits we've made so far:Code:#include <SPI.h>//#define CSDAC8 13#define CS 7int i = 0;void setup() {pinMode(CS, OUTPUT);digitalWrite(CS, HIGH);SPI.begin();SPI.beginTransaction(SPISettings(50000000, MSBFIRST, SPI_MODE0));//SPI.setClockDivider(1);}void loop() {i = i + 30;if (i > 1023){i = 0;}dac8Write(1,i);}void dac8Write(byte channel, int input) {//Warning: Channel must be less than 8!//0011 Command - Write to and Update DAC//0xxx Address - Binary address of DAC's 1-8//xxxxxxxxxx - 10 bit analog valuechannel = 0x30 | channel;input = input << 6;// Send as command, address, then 10 bit value//SPI.beginTransaction(SPISettings(42000000, MSBFIRST, SPI_MODE0));digitalWrite(CS, LOW);SPI.transfer(channel);SPI.transfer(highByte(input));SPI.transfer(lowByte(input));digitalWrite(CS, HIGH);//SPI.endTransaction();}

author: newbrain
author_rank: Super Contributor
timestamp: August 01, 2020, 12:31:47 am
content: Quote from: tyguy2 on July 31, 2020, 10:59:44 pmNo need to apologise, you seem well disposed to learn and listen!That was me, talking about the buffer and yes that code should almost work, though it might have a problem.Let's take this point by point.How to create a single SPI "transaction" fro that DACWhich SPI flags to checkHow to optimize it for the best speed without DMA1. The first is something you already know, we need to send 24bits, bracketed by a falling and a rising edge of the CS, since you had a working program, no problems here.2. The SPI peripheral of STM32 has a good number of registers, but I will assume that the initial setup has been performed by the library (we don't need it to be extra efficient), so we can focus on two registers that we are going to use: SPI1->DR is the data register, where we need to write the values to send.SPI1->SR is the status register where we can find several flags.The code you were given writes a byte to the DR, then check the transmitter empty (TXE) flag: in this way as soon as the data goes from the DR to the shift register to be serialized, we can write a new one. Unfortunately, this might not be enough.Code:void SPI_TRANSFER(uint8_t x){SPI1->DR = x;while (!(SPI1->SR & SPI_SR_TXE)); // wait until send buffer is empty};Consider what happens when you write the last (3rd) byte: the data is transferred to the shift register, TXE becomes set and the function returns.At this point you raise the CS - but our data might still be shifting out on the physical wire!For this reason we need to check another flag in SPI1->SR, the busy (BSY) flag. This one will clear when the last bit has physically left the SPI.Only at this point we can raise CS.There is no need to check the BSY flag at every byte, though, we can write a new one as soon as TXE is set.So, before raising CS, we need to add something like:Code:while(SPI1->SR & SPI_SR_BSY);3. DMA is a good solution, as it frees the CPU to do its stuff, while the memory to peripheral transfers are taken of. I would not like to seem patronizing, but before using it, I would attempt to go for the easier solution. If that does not provide the needed performance, we'll explore the DMA option too.Coming to the optimization, compilers do a good job today, especially with some hint on our side..Code:static inline void SpiTransfer(uint8_t x){SPI1->DR = x;while (!(SPI1->SR & SPI_SR_TXE));};static inline void WaitSPIfree(void){while(SPI1->SR & SPI_SR_BSY);}static inline void SetCS(void){GPIOA->BSRR = 0x8000u; /* Was this the right port/pin? substitute as needed! */}static inline void ResetCS(void){GPIOA->BSRR = 0x8000u << 16; /* Honestly, I find binary hard to read, were there 15 or 14 zeros? */}The function above should be inlined by the compiler, and since they are quite short, the gain in speed is certainly worth the small trade off in size.At his point, we have all the bricks to build our complete transaction:Code:...ResetCS();SpiTransfer(channel);SpiTransfer(input >> 8);SpiTransfer(input & 0xFF); /* The & is, in fact, redundant */WaitSPIfree();SetCS();...If you want to have a look at the generated assembler,here is the final result. Note that to rip the benefit of inline and streamlined use of registers, the optimization flag should be at least -Og (optimize for debugging)Edit: several tpyos. Even in the code.

author: Siwastaja
author_rank: Super Contributor
timestamp: August 01, 2020, 06:46:28 am
content: Just forget the stupid library. You would have already written the entire thing yourself - it's about 10-20 lines of code, 5 for initializing SPI, 5 for initializing DMA, and 5-10 to control transfers - by the time it takes you to debug the damn library trying to figure out how it's supposed to work.Start by reading the SPI and DMA sections of the reference manual. Read them 2-3 times, then look at the register descriptions and start writing code. Refer back to the peripheral descriptions.SPI and DMA are both fairly simple peripherals.Traps in STM32 SPI:* Between device families, the SPI peripherals look the same, but there are subtle differences.* The nSS hardware management modes usually do not work in real-world cases, and you may need to drive the nSS pin manually. You usually can't offload the CPU completely.* If you drive nSS manually, as is usually the case, you must set the SSM and SSI control bits high in the configuration regs, otherwise the whole peripheral errors out and refuses to work. (Some implementations may name these bits differently, or add more trap bits - SSOE which also needs to be set high comes into mind. I have written a comment in my code: "SSOE - another new incorrectly documented trap bit". If I recall correctly, this was documented as just being nSS output enable bit; obviously, with myself driving the nSS as GPIO, I do not need nor want to set SS Output Enable high, right? Wrong! The peripheral errors out if the bit is not set, despite the peripheral is not even configured to control nSS, and can't control it.)Tips:* You can start DMA with just two register writes: clear status bits as required by the reference manual, write CR to enable the channel. All other DMA registers can be configured beforehand. This is some 10x faster than using HAL functions to start DMA.* Not all, but some STM32 families have SPI peripherals with FIFOs that support wider-than-SPI-datagram read/write operations. This may end up being faster than using DMA, especially if the transaction is 4 bytes or shorter. For example, if you transfer three 8-bit SPI words, and have SPI with FIFO available, you just need to do 16-bit write and a 8-bit write, two back-to-back bus operations, exactly same number of writes as would be starting the DMA. Example:Code:void interrupt_handler_where_you_know_the_spi_has_finished(){previous_result = (*(volatile uint16_t*)&SPI1->RXDR)<<8previous_result |= (*(volatile uint8_t*)&SPI1->RXDR);// Start the next:(*(volatile uint16_t*)&SPI1->TXDR) = 0x1234;(*(volatile uint8_t*)&SPI1->TXDR) = 0x56;}

author: newbrain
author_rank: Super Contributor
timestamp: August 01, 2020, 01:48:04 pm
content: Well well,since I am stubborn and had some time to waste, I went and implemented a small proof of concept.I used an STM32F407 Discovery board, which is almost a perfect match for the OP's HW: system clock 168MHz, same peripherals for what we are concerned.With the code I proposed above (harnessed in a LL CubeMX project, call me lazy...), I was able to attain a maximum speed of about 1.1MS/s (1 Sample = 8 bits command + 16 bits value), so in excess of 25 Mb/s.Note that this includes 8 channels updated DDS style with 32 bits phase accumulators.Of course, it's bad to rely on SW loops for timing, so I settled to a sample rate of 100 KS/s per channel, for a total of 800 KS/s to have some margin.It's really pointless trying to go faster, as the DAC has a settling time of 4.4 µs!I used TIM1 (any other could be used) with a reload register of 1679 (168 MHz / 1680) and did not use either interrupt or DMA.The channels are synchronized, by only updating the DAC registers with the last write to the Input Registers, so initializing the phase accumulators with a phase shift would produce fairly accurate results.The inner loop could be slightly optimized by unrolling it, and possibly synchronizing only the last write with the timer, but we are really in the land of diminishing returns.Here is the DDS and SPI code:Code:static inline void SpiTransfer(uint8_t x) {SPI1->DR = x;while (!(SPI1->SR & SPI_SR_TXE));};static inline void WaitSPIfree(void) {while (SPI1->SR & SPI_SR_BSY);}static inline void SetCS(void) { SPI_CS_GPIO_Port->BSRR = SPI_CS_Pin; }static inline void ResetCS(void) { SPI_CS_GPIO_Port->BSRR = SPI_CS_Pin << 16; }/* Implement a simple DDS with 32 bit phase counters */#define SAMPLE_FREQ 100000 /* in Hertz *//* Get the best possible precision out of our phase accumulator, about 1E5/2^32 ~= 23 µHz */#define DELTA_PHASE(freq) (((0x200000000ull * (freq)) / SAMPLE_FREQ + 1) / 2)/* Just initialize every element with the desired frequency */static const uint32_t deltas = {DELTA_PHASE(100), DELTA_PHASE(200), DELTA_PHASE(500), DELTA_PHASE(4000),DELTA_PHASE(1000), DELTA_PHASE(1500), DELTA_PHASE(235), DELTA_PHASE(123),};/* DDS style frequency generation, since we need a sawtooth, just return the current phase MSBs */static inline uint16_t value(uint32_t ch) {static uint32_t acc;acc += deltas; /* Exploits unsigned arithmetic roll over at 2^32 */return acc >> 16;}Inner loop code:Code:/* Wait for the 10 µs timer to expire */while (!(TIM1->SR & TIM_SR_UIF));/* And clear all flags */TIM1->SR = 0;for (uint32_t channel = 0; channel < 8; channel++) {input = value(channel);ResetCS();if (channel < 7)SpiTransfer(channel); /* Just write the input register, no update */elseSpiTransfer(channel + 0x20); /* Write last input register, transfer all to DAC registers */SpiTransfer(input >> 8);SpiTransfer(input & 0xFF); /* The & 0xFF is, in fact, redundant */WaitSPIfree();SetCS();}I'm still of the opinion that any DMA or interrupt based solution could do very little to improve the timing, see the attachments for a single transaction and a full 8 samples cycle (hoping thedogforum's SW doesn't chew my homework).Not that we are left with > 2.5 µs for further processing.Please be tolerant of the outrageous probing technique (ground bounce and ringing...), it's a Saturday morning POC.This being a Rigol DS1054Z, the 42 MHz clock is of course closer to a sine than to a square wave.EtA: The last attachments shows what happens when one does not wait on the BSY flag: the last byte is chopped by raising end of CS.If every nanosecond counts, one could insert some useful processing between the transmission and setting CS to one, to delay it as needed (e.g, in our case, the DDS accumulator update).

author: newbrain
author_rank: Super Contributor
timestamp: August 01, 2020, 02:45:59 pm
content: Quote from: Siwastaja on August 01, 2020, 06:46:28 amUnfortunately, not the case for the SPI in F405/7: to change the word size from 8 to 16 bit one must change a flag in CR1 configuration register:QuoteReally, the one in a lowly F0 is already a nicer peripheral, with 4 to 16 bits configurable word size and 4 bytes FIFO...still with a mostly pointless hardware nSS.About SSOE, I have no problems with keeping it reset, at least on this F4: the only error I see (and expect) is OVR, as Rx data is never read.The same goes with a F072 I have at hand, F7 can't remember. I'm curious about the problem you encountered?

author: Siwastaja
author_rank: Super Contributor
timestamp: August 01, 2020, 03:42:32 pm
content: Quote from: newbrain on August 01, 2020, 02:45:59 pmThe part was STM32H743. Don't remember the problem more exactly anymore, it's two years ago, just that without SSOE bit set, the peripheral didn't work, and with SSOE enabled, it started working so I wrote that grumpy comment. I was working with both DMA, and non-DMA modes, and software nSS management, obviously, which is exactly why I expected SSOE should NOT be set. The reference manual only documents the functionality of SSOE bit when SSM=0, hardware SS management, and the block diagram shows SSOE only connects to the pin output driver logic, as expected from the name. So I was surprised (not really) it had undocumented side effects. The code:https://github.com/siwastaja/robotboard2-fw/blob/master/imu.c#L1672

author: tyguy2
author_rank: Unknown
timestamp: August 01, 2020, 05:52:53 pm
content: This is above and beyond the help I needed, I really appreciate all of this!Unfortunately, the code you provided is not working. Here's what I put into the IDE:Code:#include <SPI.h>#define CS 7int input = 0;void setup() {// put your setup code here, to run once:pinMode(CS, OUTPUT);digitalWrite(CS, HIGH);SPI.begin();SPI.beginTransaction(SPISettings(42000000, MSBFIRST, SPI_MODE0));}void loop() {// put your main code here, to run repeatedly:/* Wait for the 10 µs timer to expire */while (!(TIM1->SR & TIM_SR_UIF));/* And clear all flags */TIM1->SR = 0;for (uint32_t channel = 0; channel < 8; channel++) {input = value(channel);ResetCS();if (channel < 7){SpiTransfer(channel);} /* Just write the input register, no update */else{SpiTransfer(channel + 0x20);} /* Write last input register, transfer all to DAC registers */SpiTransfer(input >> 8);SpiTransfer(input & 0xFF); /* The & 0xFF is, in fact, redundant */WaitSPIfree();SetCS();}}static inline void SpiTransfer(uint8_t x) {SPI1->DR = x;while (!(SPI1->SR & SPI_SR_TXE));};static inline void WaitSPIfree(void) {while (SPI1->SR & SPI_SR_BSY);}static inline void SetCS(void) { GPIOA->BSRR = 0b1000000000000000; }static inline void ResetCS(void) { GPIOA->BSRR = 0b1000000000000000 << 16; }/* Implement a simple DDS with 32 bit phase counters */#define SAMPLE_FREQ 100000 /* in Hertz *//* Get the best possible precision out of our phase accumulator, about 1E5/2^32 ~= 23 µHz */#define DELTA_PHASE(freq) (((0x200000000ull * (freq)) / SAMPLE_FREQ + 1) / 2)/* Just initialize every element with the desired frequency */static const uint32_t deltas = {DELTA_PHASE(100), DELTA_PHASE(200), DELTA_PHASE(500), DELTA_PHASE(4000),DELTA_PHASE(1000), DELTA_PHASE(1500), DELTA_PHASE(235), DELTA_PHASE(123),};/* DDS style frequency generation, since we need a sawtooth, just return the current phase MSBs */static inline uint16_t value(uint32_t ch) {static uint32_t acc;acc += deltas; /* Exploits unsigned arithmetic roll over at 2^32 */return acc >> 16;}The Set/Reset CS functions weren't compiling as you had written them, so I ended up hard coding them just to see if it would work. I probed the board, and I'm seeing the CS go high, then nothing. No signals on SCK, SDI, and CS just stays high. My guess is the execution is getting stuck in either SPI transfer or WaitSPIFree. I am using PB3, PB4, PB5 for the SPI pins, just as a heads up. Any ideas, what am I missing?

author: newbrain
author_rank: Super Contributor
timestamp: August 01, 2020, 07:28:36 pm
content: Quote from: tyguy2 on August 01, 2020, 05:52:53 pmYup, timer TIM1 initialization - as said, being a lazy ass, I used the CubeMx code generator to set up everything for me, but you need to do it on your own.The loop will wait forever the update event, as the timer is not running.I do not know the Arduino platform enough (read at all, especially on STM32) to say whether:TIM1 is available or used by some system function:This not a big deal, any timer would do.If there's some library that sets it up for you.However, initializing a timer as simple counter is, as someone across my east border would say, no more that 3 or 4 lines of code.Code:RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; /* Enable clock to TIM1 */TIM1->CR1 = 0; /* This is the reset value, no special mode, just up counter */TIM1->PSC = 0; /* Prescale set to fastest clock */TIM1->ARR = 1679; /* 100 kHz repeat rate */TIM1->EGR |= TIM_EGR_UG; /* Force reload of the prescaler and counter */TIM1->CR1 = TIM_CR1_CEN; /* Start it! */Six lines, in fact. To add to the setup() function.A couple are redundant, as they just rewrite the reset value...I have now added some comments.This works if no one else uses/has used/will use this timer.

author: tyguy2
author_rank: Unknown
timestamp: August 01, 2020, 09:18:21 pm
content: Quote from: newbrain on August 01, 2020, 07:28:36 pmTried this code, unfortunately it didn't work. Tried switching to a few different timers, which didn't solve the problem either. I'm having the same issue, where it either gets stuck with the CS line pulled high, or, in the case of adding your TIM1 setup() code, the CS line stays low.I tried using TIM2, TIM8, and TIM10, which my approach was taking anything that said TIM1 and replacing it with TIM2, TIM8, and TIM10, which didn't work. I would assume Arduino wouldn't be using all 14 timers, would it?

author: newbrain
author_rank: Super Contributor
timestamp: August 01, 2020, 10:45:01 pm
content: Since CS goes low, it means the timer is working, otherwise it would never reach that point.So now it's probably blocking while waiting for an SPI flag.Are you sure you are using SPI1?I really don't have the heart to deep dive into the Arduino library...The loop code performs one action at the time, if you can't use a debugger, maybe try with inserting serial print statements or GPIOs' set/resets between the various steps to understand where it stops - we don't care about speed at the moment.Another thing: to reach this speed, I have used -O3, but starting with -Og it works (with very little margin).

author: tyguy2
author_rank: Unknown
timestamp: August 02, 2020, 12:12:16 am
content: Quote from: newbrain on August 01, 2020, 10:45:01 pmWe've had a small breakthrough. Was talking with some others on the STM forum, made the point that the SPI pins I was using were "alternative" pins for SPI1. Uploaded this code and am now getting about 14 kHz on a single channel:Code://*** SPI ***const PinMap PinMap_SPI_MOSI[] = {//{PA_7, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PB_5, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},//{PB_5, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{PB_15, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_3, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_12, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{NC, NP, 0}};const PinMap PinMap_SPI_MISO[] = {//{PA_6, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PB_4, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},//{PB_4, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{PB_14, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_2, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_11, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{NC, NP, 0}};const PinMap PinMap_SPI_SCLK[] = {//{PA_5, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PB_3, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PB_3, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{PB_10, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PB_13, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_10, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{NC, NP, 0}};const PinMap PinMap_SPI_SSEL[] = {{PA_4, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},// {PA_4, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},// {PA_15, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PA_15, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{PB_9, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PB_12, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{NC, NP, 0}};#include <SPI.h>#define CS PA15int i = 0;void setup() {pinMode(CS, OUTPUT);digitalWrite(CS, HIGH);SPI.setMOSI(PB5);SPI.setMISO(PB4);SPI.setSCLK(PB3);SPI.begin();SPI.beginTransaction(SPISettings(50000000, MSBFIRST, SPI_MODE0));}void loop() {i = i + 30;if (i > 1023) {i = 0;}dac8Write(1, i);}void dac8Write(byte channel, int input) {//Warning: Channel must be less than 7!//0011 Command - Write to and Update DAC//0xxx Address - Binary address of DAC's 1-8//xxxxxxxxxx - 10 bit analog valuechannel = 0x30 | channel;input = input << 6;// Send as command, address, then 10 bit valuedigitalWrite(CS, LOW);SPI_TRANSFER(channel);SPI_TRANSFER(highByte(input));SPI_TRANSFER(lowByte(input));digitalWrite(CS, HIGH);}void SPI_TRANSFER (uint8_t x) { // 0.3uS overhead between bytes when optimized // 3.25 uS unoptimized// SPI.transfer(x); // send byteSPI1->DR = (x) ;while ((SPI1->SR & SPI_SR_BSY ) | (SPI1->SR & (!SPI_SR_TXE)) ) {}; // wait until - Transmit buffer NOT Empty - Busy flag SET}I'm going to try your code with the edits to the pin mapping and see what happens.

author: tyguy2
author_rank: Unknown
timestamp: August 02, 2020, 12:21:51 am
content: I've got good and bad news. Good news is your code works now with the new pin mapping. The bad news is it's VERY jittery on the scope, and it's not a problem with my probing or triggering, the actual waveform is very rough. I'll post some pictures of the output very soon.Code://*** SPI ***const PinMap PinMap_SPI_MOSI[] = {//{PA_7, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PB_5, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},//{PB_5, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{PB_15, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_3, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_12, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{NC, NP, 0}};const PinMap PinMap_SPI_MISO[] = {//{PA_6, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PB_4, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},//{PB_4, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{PB_14, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_2, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_11, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{NC, NP, 0}};const PinMap PinMap_SPI_SCLK[] = {//{PA_5, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PB_3, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PB_3, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{PB_10, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PB_13, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PC_10, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{NC, NP, 0}};const PinMap PinMap_SPI_SSEL[] = {{PA_4, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},// {PA_4, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},// {PA_15, SPI1, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI1)},{PA_15, SPI3, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF6_SPI3)},{PB_9, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{PB_12, SPI2, STM_PIN_DATA(STM_MODE_AF_PP, GPIO_PULLUP, GPIO_AF5_SPI2)},{NC, NP, 0}};#include <SPI.h>#define CS 7int input = 0;void setup() {pinMode(CS, OUTPUT);digitalWrite(CS, HIGH);SPI.begin();SPI.beginTransaction(SPISettings(42000000, MSBFIRST, SPI_MODE0));// put your setup code here, to run once:RCC->APB2ENR |= RCC_APB2ENR_TIM8EN; /* Enable clock to TIM1 */TIM8->CR1 = 0; /* This is the reset value, no special mode, just up counter */TIM8->PSC = 0; /* Prescale set to fastest clock */TIM8->ARR = 1679; /* 100 kHz repeat rate */TIM8->EGR |= TIM_EGR_UG; /* Force reload of the prescaler and counter */TIM8->CR1 = TIM_CR1_CEN; /* Start it! */}void loop() {// put your main code here, to run repeatedly:/* Wait for the 10 µs timer to expire */while (!(TIM8->SR & TIM_SR_UIF));/* And clear all flags */TIM8->SR = 0;for (uint32_t channel = 0; channel < 8; channel++) {input = value(channel);ResetCS();if (channel < 7){SpiTransfer(channel);} /* Just write the input register, no update */else{SpiTransfer(channel + 0x20);} /* Write last input register, transfer all to DAC registers */SpiTransfer(input >> 8);SpiTransfer(input & 0xFF); /* The & 0xFF is, in fact, redundant */WaitSPIfree();SetCS();}}static inline void SpiTransfer(uint8_t x) {SPI1->DR = x;while (!(SPI1->SR & SPI_SR_TXE));};static inline void WaitSPIfree(void) {while (SPI1->SR & SPI_SR_BSY);}static inline void SetCS(void) { GPIOA->BSRR = 0b1000000000000000; }static inline void ResetCS(void) { GPIOA->BSRR = 0b1000000000000000 << 16; }/* Implement a simple DDS with 32 bit phase counters */#define SAMPLE_FREQ 100000 /* in Hertz *//* Get the best possible precision out of our phase accumulator, about 1E5/2^32 ~= 23 µHz */#define DELTA_PHASE(freq) (((0x200000000ull * (freq)) / SAMPLE_FREQ + 1) / 2)/* Just initialize every element with the desired frequency */static const uint32_t deltas = {DELTA_PHASE(100), DELTA_PHASE(200), DELTA_PHASE(500), DELTA_PHASE(4000),DELTA_PHASE(1000), DELTA_PHASE(1500), DELTA_PHASE(235), DELTA_PHASE(123),};/* DDS style frequency generation, since we need a sawtooth, just return the current phase MSBs */static inline uint16_t value(uint32_t ch) {static uint32_t acc;acc += deltas; /* Exploits unsigned arithmetic roll over at 2^32 */return acc >> 16;}Edit: Added scope pictures

author: newbrain
author_rank: Super Contributor
timestamp: August 02, 2020, 01:07:51 am
content: Good.And bad.I have no temporal jitter worth of note (for this kind of generator at those frequencies) << 100 ns, but I have no comparable DAC to try (maybe tomorrow I'll dig out something, one channel).Also your pictures do not seem to show jitter in the time axis, rather the waveform seems to be stuck in the initial codes, and glitches up and down elsewhere.For this reason, I suspect a signal integrity problem.To check for jitter, could you show a CS trace? On my scope, the jitter of the raising and falling edges is contained below 100 ns after 300 ms from the trigger point.The DDS algorithm is as simple as can be, I seriously doubt there's fault - but, hey, check it and simulate it!

author: tyguy2
author_rank: Unknown
timestamp: August 02, 2020, 05:36:20 am
content: I think I had my camera's shutter speed set too high to show the jitter, the scope's trace was over exposing the camera. Honestly, I'm not getting much, if any, jitter on the other implementation, and I'm able to ramp up all the channels to well beyond 4kHz at a responsible sample rate. I'm sure it's a signal integrity thing, I didn't lay out the traces on the PCB with the most care (2 layer board, vias, split ground planes, other signal traces running close to this one, etc etc), but I figured since it was a relatively slow bus, it didn't matter too much, and at this point I'm getting the performance I need.For fear of sending this thread a bit off topic, how would I do this in reverse, i.e. read data in from the SPI bus without relying on the Arduino library? I also have 2 8 channel SPI based ADC's on this board, and anything I can do to squeeze more performance out of them the better.

author: newbrain
author_rank: Super Contributor
timestamp: August 02, 2020, 12:29:55 pm
content: From your code:Code:while ((SPI1->SR & SPI_SR_BSY ) | (SPI1->SR & (!SPI_SR_TXE)) ) {};This line is silly, bordering on meaningless.Where did you get it?The author seems to have a foggy understanding of binary versus logical operators.A binary OR, |, is being used instead of a logical one. It does not affect the outcome, but it is the wrong operator (and doesn't allow shortcut evaluation)The part after | will always be zero, so it does not affect the result:SR is being binary ANDed with the logical negation, !, of a non-zero value, i.e. with zero, always yielding a a zero result.The net result is that is waiting only on the BSY flag, wasting time for each byte transfer.I also fear it might work just by chance, the diagrams in the RM show a delay between TXE going down to BSY going up: if the read goes through in this time gap, BSY will be zero, but the SPI has not even begun shifting out the bits.After all, the receommendation is to use TXE AND BSY (or RXNE, see below).To read from the SPI, just check on the RXNE (received data not empty) and read form DR.This is the recommended way to synchronize bidirectional transactions.I could have used it instead of the BSY flag (as it is set only when the last bit has been shifted == last bit shifted out), but it needs the read from DR to be reset, so why bother?If my code is jittery, you can try a couple of things:Recompile with -O3Disable interrupts in the loopRemove the timer check and see what happens.14 kHz with a single channel and the +30 increment means about 478 kHz, less than half of what is possible.An aside, why use a DDS-like algorithm:With the naïve step generator in your implementation, you only have a very coarse and non linear control of the output frequency (apart from it not being locked to a precise timer), with large spans of values generating the same frequency; e.g. all increments 103-113 will yield the same frequency (1/10th), and the next step up or down can only be 1/9th or 1/11th of the loop frequency.This can be improved using a modulo (which is optimized to an AND for 2^n values anyway) operation, rather than zeroing the accumulator.The resolution will still be coarse (1/1024 of the loop frequency) but at least linear.Using the MSBs of a large phase accumulator improves the resolution (to pointless values, admittedly) and does away with modulo if the size is the same as that of an unsigned integer.It has the added benefit of being able to precisely offset the phase of two generators, if desired.Less operations, better frequency and phase performance.Then there's the whole can of worms of the reconstruction filter at the DAC output, if the ZOH is not satisfactory...

author: hans
author_rank: Super Contributor
timestamp: August 02, 2020, 04:18:07 pm
content: Disabling ISRs for the whole duration of the DAC being written to, is a very long period. From your scope screenshot that's almost 75% of the time. In principle it works, but IME disabling interrupts for a very long time is always a sign of less than optimal design. It's OK if the interrupt latency is not that crucial (e.g. the peripheral interrupt service can wait 7.5us), then fine.Still, if you leave interrupts enabled during the timer 8 wait spinlock, some jitter can still be introduced if an ISR fires just before the timer underflow occurs.I think a more direct approach in handling the timer underflow with an interrupt can help. For example, in the current code, an TIM8 underflow could be missed if some other interrupt routine takes >10us to process (which admittedly, is very long for a 168MHz CPU).Some jitter could also be introduced because the latching being performed at the end of the DAC update code. Any ISRs that occurs during the writing of the DAC would introduce some jitter.. It's also possible to have the first DAC transaction start with latching the DAC values from the last iteration, that is if you can spare the a little higher latency (which at least, is now constant). Then any jitter introduced during writing to the DAC does not influence the output signal. Perhaps you still need to disable interrupts for some time (e.g. the SPI latch period is still critical), or perhaps latch the DAC inside the timer underflow ISR if it needs to be more precise..

author: newbrain
author_rank: Super Contributor
timestamp: August 02, 2020, 04:48:17 pm
content: Hans, I do not disagree at all.That list was more a investigation one than a design one (well, optimizationareneeded to reach that speed).

author: tyguy2
author_rank: Unknown
timestamp: August 03, 2020, 06:13:25 pm
content: Quote from: newbrain on August 02, 2020, 12:29:55 pmYou're completely right, tested your method and it just provides better performance. Idk why I went with that, wasn't thinking straight. I was able to fix the jitter/ glitching, I was having some issues with the voltage reference connected to the DAC.Quote from: newbrain on August 02, 2020, 12:29:55 pmSo, based on my understanding, the read SPI code would be this:Code:int8_t readSPI(void) {int8_t x = 0;while (SPI1->SR & SPI_SR_RXNE){SPI1->DR = x;}return x;}And the inner loop would look like this:Code:...ResetCS();SpiTransfer(channel);SpiTransfer(input >> 8);SpiTransfer(input & 0xFF); /* The & is, in fact, redundant */WaitSPIfree();readSPI(); //New Line of codeSetCS();...

author: newbrain
author_rank: Super Contributor
timestamp: August 03, 2020, 07:22:45 pm
content: Glad to hear it's finally starting to work as intended.Quote from: tyguy2 on August 03, 2020, 06:13:25 pmEhm, there's a number of problems with that snippet...erroneous use of the flag, plus you arewritingto the DR, while you should be reading.This,Code:static inline uint8_t readSPI(void) {while (!(SPI1->SR & SPI_SR_RXNE));return SPI1->DR;}is more like it - Note: I've neither tested nor even compiled it!But then, I fear that your inner loop show a problem in the understanding of the SPI mechanism:You are readingafterthe transmission has completed, this is wrong:An SPI bus, in bidirectional master mode, will shift in bits from MISO while shifting out bits on MOSI. So reading must be "at the same time", in this case right after wrting each byte, when the written byte has left the shift register and the received byte has finished getting read in.You are reading from the same SPI peripheral we are writing to. As you have separate ADCs and DACs, this is not what you need to do!There's the need for a separate CS and a completely separate transaction. Depending on the ADC, you might also not be able to keep a 42 MHz CLK, so possibly another SPI altogether is needed - if you don't want to reconfigure it at every transcaction. At this point, the simple solution we concocted might no longer be enough.You need to accumulate the read data somewhere, then process it in the dead time between cycles.Forget to be able to keep 8 channels at 100 kS/s if there's any non-trivial processing. A DMA approach would be best, probably.Thereference manualdescribes very well how to read o write (or both) to the SPI, please go through it and make sure to have a complete grasp of the possible use cases.I would also really advise to start reading up on how 'dearduinize' this thing...