Summary
A FreeRTOS 'task' (aka thread) is used to manage the interface with the physical SP0256 and stream phonemes from a circular buffer.
Deets
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:- the 'address' lines. Since there are only 64 phonemes, I decided to relinquish the top two bits for other purposes, so this would up using PA 0-5. These are not 5V tolerant, but being as they are strictly output, this is OK.
- the 'not Address Load' (nALD) line. This is what strobes the data into the SP0256. This is put on PB 1, which similarly is not 5V tolerant, but since it's going out of the BluePill and into the SP0256, this is OK.
- the 'not Load Request' (nLRQ) line. This is part of the hardware handshaking, and it goes low to indicate that it is OK to send data to the SP0256. Since this is an input, it needs to be on a 5V tolerant pin, and it is put on PB 11.
- the 'Standby' (SBY) line. This indicates that the SP0256 is finished with all phonemes, and could be put into low(er!)-power mode. I don't plan on using it, but nonetheless I wired it to PB 10 in case I change my mind.
- I also decided to manage the reset line explicitly, and I put that on PA 6 with an NPN transistor open collector. The data sheet seems to imply that Reset needs to go up to 5V, not just be a digital high, so that's why I did this.
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:
- the queue is not empty (and concomitantly is being serviced by the consumer thread), and the SP0256 is ready to accept data. In this case we want to first dequeue and feed the SP0256. We want to do this until the SP0256 is no longer ready to accept more data. This step is important, because we want the data to go to the synth in the order we pushed it. So we need to flush out as much previous data as possible before processing the current data.
- the queue is empty, and the SP0256 is ready to accept data. In this case we want to feed the SP0256 from the start of the current data, not even putting it in the queue, and continue doing that until the SP0256 brings the nLRQ line high (indicating it cannot accept more). It is important to feed as much data as possible for reasons that will soon be apparent.
- the SP0256 is not ready to accept data. In this case we simply want to add the data to the queue.
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.
Next
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.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.