Implementing the SP0256 'Task'

A project log for Look Who's Talking 0256

A BluePill Driver/Simulator/Emulator for the GI SP0256-AL2

ziggurat29ziggurat29 05/10/2020 at 15:400 Comments


A FreeRTOS 'task' (aka thread) is used to manage the interface with the physical SP0256 and stream phonemes from a circular buffer.


Since this project is ultimately going to have several functions, including the previously described command processor, but also the phoneme receiver and text-to-speech component, I decided to make the handler for the physical SP0256 a task-oriented component.  The gist is that there is an interface where you 'push' phoneme data to your heart's content, and internally there is a 'thread' that removes that data and sends it on to the chip in a way that works with the chip's hardware flow control signals.  I've used this approach in some other projects, and it helps keep the design/implementation modular and more loosely coupled.

In this case, there are several hardware resources that the SP0256 Task manages:

This chip is really slow, and we are wiggling the lines programmatically, so I use some delay loops.  One way I tend to do that on these ARM parts when possible is use the 'Debug module'.  This is an optional module intended for debugging, but one handy thing it has is a cycle counter.  This is a 32-bit up counter that is clocked by the CPU clock.  By using this (if available on your particular part) I can avoid using the timer resources.  For short delays and even profiling code it can be quite handy.  The module has to be explicitly enabled, and that is done very early in main().

I use a circular buffer to receive the phonemes from outside this module.  This is some common code I have written that I use across projects.  Since this is manipulated by two threads, I protect it with a mutex.  OK, some things about FreeRTOS:  many functions have two variants:  an 'ordinary' variant, and a 'ISR-friendly' variant.  The synchronization-related stuff in particular is in this class.  Mutexes are what FreeRTOS calls a 'binary semaphore', and you use the semaphore-related functions to acquire and release them.  HOWEVER, for reasons that are not clear to me, mutexes are incompatible with ISRs.  If you really need to do mutual exclusion and within an ISR, you must use the binary semaphore.  FreeRTOS suggests that mutexes are useful for 'simple mutual exclusion'.  Well, I think my application is 'simple' so I am going with the mutex, but I put a caveat in the comments on the API that the various methods are NOT to be called from an ISR.  This isn't a problem for my project, but one day I may re-use this and forget and somehow deadlock the system and have to spend time debugging.  Best to comment.

Speaking of ISRs, there is presently one interrupt source used:  the nLRQ line is configured as an EXTI source, on falling edge.  The idea is that when the nLRQ falls, it means that the SP0256 can accept more data, so if there is pending data in the circular buffer, send it on.  I don't do this work in the ISR, though.  Here's it's a problem because of the mutex, but I generally avoid that if practical as a rule anyway so that the ISR can return as quickly as possible to the system.  Instead, I let the worker thread do that.  So the ISR just signals for the worker thread to wake later and handle the data.  There are several mechanisms for that in FreeRTOS, but the one I usually like to use are called 'Task Notifications'.

Task Notifications are a FreeRTOS concept, and essentially each task has a 32-bit value associated with it.  You can interpret this 32-bit value in whatever way you want, but I (and presumably others) generally interpret them as a vector of flags.  They are lightweight compared to other synchronization primitives, and have limitations, but for many use-cases they are sufficient.  I usually have a single header 'task_notification_bits.h' that defines all the values for my project -- this is just my preference.  The task calls 'xTaskNotifyWait()' which causes the thread to sleep until awakened by having a notification posted by 'xTaskNotify()' or 'xTaskNotifyFromISR()'.  In my case I use the latter since I post the notification from the EXTI ISR.  My bit definition is named 'TNB_LRQ' since this is the task notification bit for the 'Load Request Line'.

When the task awakens, it tests all the bits it knows about handles them accordingly.  (You must test ALL the bits you know about because it is possible that more than one notification has been posted and those bits are automatically cleared, so you don't get a second chance.)  In this case the task dequeues phonemes from the circular buffer and pushes them into the SP0256 until either there are no more phonemes or because the nLRQ line went high (and we must stop for now).  All this is done while holding the queue's mutex, so other threads are prevented from damaging the queue while we're using it.  The process of dequeuing and sending is fast, so I just hold the mutex for the whole time rather than be more surgical around the dequeue operation only.  One can view this task as the 'consumer' of the phoneme stream.  I have provided a notification mechanism that allows some arbitrary code to be executed when the phoneme stream is depleted, though I don't imagine I will be using it.  It's just habit for me to provide such.

Other tasks will 'produce' phonemes into the queue.  This might be the serial port, or it could be the output of the text-to-speech module yet to be developed.  This  'push' operation is slightly more involved than the 'pull' because several scenarios must be handled:

By handling those three scenarios in the sequence presented, the order of the phoneme stream will be maintained, and the queue will be used to buffer excess, which will be automatically handled for us.  As mentioned, it is important to feed the synth to the point that it indicates it can take no more.  The reason is that we need to make sure that eventually there will be a falling transition on the nLRQ line so that the interrupt handler will send the task notification to cause the rest of the data to be fed in.  It is OK if the amount of data being pushed is not enough to make this happen, but if the amount of data IS enough, then we must cram it until we can't cram any more.  Any excess data gets enqueued.

The last feature of the 'push' API is that it returns how much data was consumed in the call.  This is important because it's possible that there is not enough room in the queue to take it all.  By reporting how much was taken, the caller can make multiple sequential calls to eventually push it all in.

OK, this activity took longer than I wanted because I misread the datasheet about the behaviour of the nLRQ line, so I had to re-write some of the code.  Additionally, an annoyance with the STM32CubeMX application is that it is not sufficient to declare a pin as being an EXTI source -- you also have to go into the NVIC settings and turn on the EXTI interrupt.  This is happened before, and it seems I never learn (or rather I always forget).  Strictly, you have to do this separate 'enable the interrupt source' step for other peripherals, too, but for some reason it seems obvious in those cases.

At length, I was able to push a manually-crafted phoneme sequence in and have it play back correctly.


The test was just with a hard-coded call to the 'push' API.  Now I need to implement the code that will be making that call.  This will be in a module that receives data over the serial port, and pushes that data into the SP0256 task.