As an improvement, I switch to using DMA for driving the sample data output instead of code in an ISR.
The system worked and seemed responsive on the monitor, but it tweaked my spidey sense to have so much stuff going on in the ISR. The ISR gets triggered every sample -- 11025 times per second -- and each time context has to be saved/restored, and code run. The STM32F103 has a lovely DMA unit, so I might as well make use of it!
Getting DMA running is a multi-step process of fiddling with stuff in CubeMX (in this case, setting the DMA tab in the TIM4 resource, and configuring it as memory-to-peripheral, and the input size as 'byte' and the output size as 'half word', and setting the memory address to increment, but not the peripheral address. This is all obscure because the documentation for CubeMX leaves a lot to be desired, so a bunch of experimentation had to be done to figure out the appropriate recipe.After that, then the right code incantations need to be spoken. Much as with CubeMX, the HAL library's documentation leaves a lot to be desired -- it has a 'doxygen' taste to it, so I find that it's more or less fruitless to go to the documentation, and rather I wind up reverse-engineering the source to see what the methods actually do. Really, I think that just programming at the register level and commenting the code would be clearer in almost all cases /except/ for USB, which is a beast. It definitely would cut down on the flash and ram usage. Anyway, at length I found the relevant incantations, which in general are 'turn on the timer 4 clock', then 'set up the DMA controller to transfer', then 'set timer 4 to trigger the DMA' (this starts output immediately). There are several interrupts from the DMA that you need to handle:
- transfer complete -- you'll want to send another buffer
- half-transfer complete -- this is your heads-up to start preparing another buffer so you'll be ready to go when 'transfer complete' comes in
- transfer aborted -- if you care
- error -- if you care
The DMA has a nifty 'circular' mode that seems like it should be useful, but I can't figure out how I can actually make use of it in this project.
I did a trivial test of using DMA with the PCM test data: start a transfer, then start another in the 'transfer complete' event. I used the PCM 'hello' sample set from before, and it worked as expected.
On to Implementation...
My existing interrupt-driven design was based on receiving a 'half-complete' event that alerted the producing task to prepare additional buffer loads. This worked out neatly because the DMA similarly generates a 'half-complete' event and a 'fully complete event'. Most of the code from the timer ISR was reused/restructured into the 'fully complete' event, and the 'half-complete' simply sent a task notification to the SP0256 task. Actually, a bunch of code was deleted, since we didn't have to clock out individual samples -- we just needed to start a new transfer when the present one completed. The only addition was to 'kick start' the first transfer. This wasn't needed in the previous design because it was effectively always polling in the ISR as to whether there was a buffer to transfer at all. In this case, once it's done it's done, so you have to explicitly start the first transfer to kick off the process. So long as you keep producing buffers in a timely manner, the process is continuous.
Most of the time was spent figuring out how to use the DMA peripheral appropriately (the initial test), and the porting of the actual design fortunately went rather quickly since I had done the interrupt-driven design in a sort of dma way. The build is bigger; thanks HAL:
arm-none-eabi-size "BluePillSP0256AL2.elf" text data bss dec hex filename 113328 2024 16496 131848 20308 BluePillSP0256AL2.elf
But I've still got a little more code space for something....
One day I will implement the streaming of phoneme data/text directly into this without using the command line. Maybe tomorrow is that day?