Assignment Instructions

This assignment requires you to roll back to Zephyr 3.7.1 or earlier. Follow this guide to rollback your Zephyr installation before starting on the assignment, if you have not already done so!

Part I: First encounter with the synthesizer

Before making any changes to the system, it is first necessary to understand the structure of the system, the tasks it performs and other relevant parameters, that must be considered in the new design.

The synthesizer performs four main tasks:

  • Task 1: check if the state of any peripheral (switch or rotary encoder) has been changed and update the appropriate synthesizer data structures.
  • Task 2: check and process the input keys from the keyboard.
  • Task 3: synthesize/generate the sound to be played.
  • Task 4: send the generated sound to the audio amplifier.

Each of the tasks have been mapped to an LED. Refer to the source code, the development kit documentation and logic analyzer documentation for the correct pin mappings.

For this part, you don't have to modify the code, but you might need to read parts of the code to understand what each task does.

In the lectures, quizzes and exercises, you have been mostly working with fixed execution times. However, in real life, execution times are not always fixed and depend on Use Cases. This is also the case with the synthesizer you are going to redesign.

  • [8 points] Question 1:
    • For tasks 3 and 4, measure with the logic analyzer the average (AECT) and worst case execution time (WCET) when SW_OSC1 is in the up position and one note is playing.

      NOTE: In case of Task 4, make sure to measure the execution time right after start-up as the task's execution time increases with time due to some compiler issues.

    • For tasks 1 and 2:
      • First define Use Cases that will lead to the average and worst case execution time. Explain these Use Cases in your report.
      • Recreate these use cases (by playing notes or triggering the switches or the rotary encoders) and measure the average and worst case execution times using the logic analyzer
      • Translate the average and worst case execution time in % of Processor Utilization (PU) and record these values in your report.
    • Task 3 is the less time-sensitive task. Therefore, in theory, it could execute when none of the other tasks are running. Reason how much PU you would assign to Task 3 inside the superloop.

Part II: Synthesizer tasks overhaul

As you might have noticed, executing all the tasks sequentially in a superloop is not a great idea, as each task has different latency requirements. Additionally, some tasks might overrun their expected utilization time and make other tasks miss their deadlines. This can partially be solved by limiting the processor utilization or by taking into account the worst-case execution time of each task, these decisions have a huge impact on the performance of the system. For this reason, you will assign Zephyr threads to the 4 tasks.

This does not necessarily mean that you have to use a separate thread for each task. You can use one thread for several tasks if you believe this to be the best design.

We recommend reading Zephyr's Scheduling and Threads documentation before designing your implementation.

However, before you start coding:

  • [4 points] Question 2: Determine the upper and lower bounds of each task's period. Think about what values are reasonable in regard to for example: system responsivity, timing constraints and preventing overloading. In your report, state the period's lower and upper bound for each task, and choose the period that you want to use for your implementation. Justify your answers. You are free to change your answers based on your observations post implementation as long as you justify your answers

Tip: the synthesizer's rotary encoders have 96 slits per turn.

  • [4 points] Question 3: Set a priority for each thread. Justify your answer. Furthermore, consider the role of the thread executing main() and clearly define its functionality in your design. The section Thread priorities might be useful to read.

Now you can start coding!

  • [10 points] Question 4: Separate the superloop into threads

    • Separate the superloop into the threads you defined in the previous questions. Check that you can play a note with the two oscillators on and that the pitch and volume encoders are working fine. You can modify your previous answers of part II based on the results that you get here.
    • Task 4 needs to be executed with precision, so make sure to use an appropriate method for timing. You can find all timing options available here
    • For some tasks like sound synthesis (Task 3), the execution time scales linearly with the period, leaving the processor utilization almost unchanged. For others, it might be constant and thus the processor utilization greatly increases when you lower the period. Compare the average and worst-case execution time and processor utilization of Task 1 (the peripherals task) from Question 1 with the ones obtained now, after separating the superloop into threads.
  • [8 points] Question 5: Some resources are shared between threads. Identifying race conditions becomes very important when using multiple threads as they may lead to undesired behavior. For instance, shared data structures and peripherals such as buses are considered resources and thus can produce a race condition.

    • Review the source code and identify all race conditions and report these.

    • In your report, for each race condition:

      • State whether it is tolerable or needs to be fixed and justify why
      • State which threads are involved
      • Explain when they may appear (which read/write operation may trigger them)
      • Describe what impact it has on overall performance
      • If you chose to fix it, explain your design and the consequences it has on the performance of the synth

      If you want a refresher on what is a race condition, you can read this guide and/or this guide, as well as the Software Systems course lecture notes. You can find Zephyr's documentation for mutexes here. Of course, you are free to use the other synchronization methods.

      Note: since the printulu functions are considered debug tools, you don't have to fix any of them. You don't have to fix race conditions associated with the audio buffer either. However, you have to describe them.

The last part that needs to be fixed is the audio buffer. Zephyr offers a kernel object to handle big chunks of memory of fixed size: memory slabs. Currently, the audio is stored in a single buffer that is shared between the sound generation primitive and the data transfer to the audio amplifier (through DMA).

This is a typical example of a race condition, where the synthesizer is writing to the buffer at one speed and the audio buffer is reading from the buffer at another speed. In these cases, a common solution is to use one buffer to write the data and another one to read/transfer the data. This is known as double buffering.

  • [8 points] Question 6: Implement double buffering for the audio buffer and explain your design. Ensure that:
    • The DMA never reads the same buffer as the sound function
    • Buffers are freed and allocated correctly
      • Only memory that is necessary is allocated
      • The total allocated memory must never exceed the size of the Zephyr slab

We recommend reading up on how the k_mem_slab_alloc() and i2s_write() function calls work before designing your implementation

If the double buffering has been implemented correctly, the resulting sound (when playing a single note) should be clear, without any clipping or beep.

Part III: Overload

If you play many notes at the same time and/or you apply too many sound effects, you will notice that the microcontroller's processor cannot compute the sound in time. Depending on how you have implemented Part II, you will hear an annoying sound when that happens. After implementing Question 7, no race condition should occur under normal operation. However, if the system is overloaded, you will run into a race condition.

  • [8 points] Question 7: Implement overload detection, and handle overloading. Ensure you design meets the following requirements:
    • The sound-synthesis task must identify when it drives the processor into overload and just before its deadline, must stop computing the sound of the current buffer, called frame and start computing the following one.
    • For the next frame, LED D7 (red LED) must light up to indicate the user that they are driving the synthesizer into overload.
    • Ensure that the buffer is properly reset before sending the frame to the audio amplifier as it may not be possible to fill the buffer for a given frame

      Properly reset implies the part of the buffer that is not filled must be set to 0. Do not use any other approach

Part IV: Interrupts

IMPORTANT: If you arrive to this part of the assignment, provide at the start of your report the hash address of your implementation with and without part IV.

Currently, to know if a peripheral has been updated, the processor periodically polls every peripheral and compares its current value with the previous one stored in the synthesizer's data structures. If the two values are different, then the state of the peripheral has changed.

Although this is simple to implement, there is a potential issue associated with this approach. If two consecutive state changes occur between two polls, one of them will be lost and, in the case of the encoders, an invalid state transition will be registered.

Here, you are asked to use interrupts instead of polling to check the peripherals. Bear in mind that interrupts have extremely high priority, so their execution time should be extremely short. You should not execute all the peripheral update code in the Interrupt Service Routine (ISR). You should simply capture the new peripheral's state and offload the work to a workqueue or another thread.

Here are some documents that you might find useful, when attempting Part IV:

  • [8 points] Question 8: Move the code for updating the state of the 4 switches (8 pins) from the peripheral thread into separate interrupt handlers. You can find a very nice guide (based on an older version of Zephyr) here. Explain your design.

For each switch action, only one callback should be raised. Thus, you also need to implement software debouncing for the full points in this question. Document how debouncing been implemented in your report.

NOTE: The suggested guide makes use of an older definition of the gpio_callback_handler_t struct. The latest definition can be found here. Ensure that your design uses the new definition and not the old one, found in some the snippets in the guide (which no longer compiles).

Tip: using anonymous functions can considerably reduce your code size and make it more readable. C++ implemented them in their 2011 version, which is the one Zephyr supports. You can find a guide here.

  • [Bonus: 8 points] Question 9: Do the same but with the port expander that controls all the rotary encoders. Again, explain your design in the report.