Skip to main content

Threading: Run Multiple Functions at Once

Threading refers to running multiple code blocks, or threads, at the same time. In the context of the Quarto, it is about running multiple functions in parallel (as opposed to sequentially) and controlling when those functions run. While the Quarto hardware can only execute one instruction at a time, by pausing the execution of one function to run another function, we can give the appearance of running multiple functions at the same time. To program such behavior, we will utilize the qNimble variant of the protoThreads library.

Overview

Normally, a function executes sequentially from beginning to end without stopping1. protoThreads changes this and enables a function to stop executing (return) in the middle of its execution, but allows the function to resume where it stopped when the function is called again. So if you have two functions, you would call them both over and over in the main loop:

void loop() {
Function1();
Function2();
}

Function1 would run first but after some amount of execution, it would stop and return to loop. Then Function2 would start and run for a bit and then return. The loop would then have finished one cycle and start again, causing Function1 to run again, but starting where it left off. When it returns, then Function2 would run, starting from where it left off, etc. In this way, both functions appear to run concurrently, but really they are getting broken up into smaller segments and each segment is running sequentially.

Syntax For Threaded Functions

A basic threaded function has the following syntax:

#include <protothreads.h> // must load the protoThreads library to use protoThreads

ptFunc threadedFunction(void) {
PT_FUNC_START(pt);

//code to run

PT_FUNC_END(pt);
}

A (proto)-threaded function must have a return type of ptFunc and take no arguments. It must start with the the PT_FUNC_START function and end with the PT_FUNC_END function2. Additionally, it should not use the return command as this is done internally by protoThread functions. With this structure, the function can use these new threading functions (for a full list see protoThreads documentation):

Example 1: Blinking Light

Here's a simple threaded function that turns on and off the Quarto's front-panel LED every second.

#include <protothreads.h>

ptFunc blinkGreen(void) {
PT_FUNC_START(pt);
while(true) {
toggleLEDGreen();
PT_SLEEP(pt,500); //wait 500ms between toggling the LED
}
PT_FUNC_END(pt);
}

void loop() {
blinkGreen();
}

Let's look at how this works. The main loop calls blinkGreen which after initializing with PT_FUNC_START goes into the while loop and runs toggleLEDGreen(). Then it runs PT_SLEEP which checks if 500ms has elapsed since it was first run. Since this hasn't happened yet, it will exit and the Quarto starts executing from the main loop. Since there is only one function in the main loop after exiting blinkGreen(), the loop is done and it will start back at the top and run blinkGreen() again. blinkGreen() runs PT_FUNC_START again which will jump the program execution to where it left off, which in this case is line 7, PT_SLEEP. If 500ms haven't elapsed since the first run of PT_SLEEP the program will again exit and this pattern will keep going. Eventually, enough time will have elapsed that when the program gets to PT_SLEEP it will have been enough time and then the program execution will continue, finishing while loop and running toggleLEDGreen() a second time and then starting a run of PT_SLEEP. Now this new PT_SLEEP will wait for 500ms from its first execution until it lets the program continue, so again we will wait 500ms before to finish the while loop. So now every 500ms, we run toggleLEDGreen() and LED will blink on and then off every second.

Now, we could have skipped using protoThreads and used delay instead of PT_SLEEP, but delay causes the Quarto to do nothing for the delay time. PT_SLEEP lets the Quarto run other tasks while it is waiting for the time to elapse. Note, you could also use an intervalTimer to accomplish the same task. Please see the section on protoThreads vs Interrupts for a discussion on the pros and cons of each.

Example 1b: Blinking Light

A slightly different way for writing the blinking light threaded function is to replace the while loop with a single call of PT_FUNC_RESTART which causes the thread to start back at the top of the function, which is basically what the while loop is doing:

#include <protothreads.h>

ptFunc blinkGreen(void) {
PT_FUNC_START(pt);
toggleLEDGreen();
PT_SLEEP(pt,500); //wait 500ms between toggling the LED
PT_FUNC_RESTART(pt); // jump back to the top of the function
PT_FUNC_END(pt);
}

void loop() {
blinkGreen();
}
warning

In both these examples, the program never executes PT_FUNC_END(). However, you still must include the line at the end of the function for the code to compile and work properly. (This is because protoThreads uses macros and not pure C/C++.)

Example 2: Two Blinking Lights

We can take this threaded function that blinks the light and copy it to make a second threaded function that toggles a different LED color at a different frequency:

#include <protothreads.h>

ptFunc blinkGreen(void) {
PT_FUNC_START(pt);
while(true) {
toggleLEDGreen();
PT_SLEEP(pt,500); //wait 500ms between toggling the LED
}
PT_FUNC_END(pt);
}

ptFunc blinkRed(void) {
PT_FUNC_START(pt);
while(true) {
toggleLEDRed();
PT_SLEEP(pt,450); //wait 450ms between toggling the LED
}
PT_FUNC_END(pt);
}


void loop() {
blinkGreen();
blinkRed();
}

The two functions run independently and the red sometimes turns on when the green LED is on and sometimes when the green LED is off. Writing software to match this behavior with the delay function would be challenging.

Example 3: Dependent Blinking

Threads can also respond to dynamic variables. In this example, we will have the second thread only toggle the red LED when the green LED is off:

#include <protothreads.h>
bool isGreenOn = false;

ptFunc blinkGreen(void) {
PT_FUNC_START(pt);
while(true) {
isGreenOn = !isGreenOn; //
setLEDGreen(isGreenOn);
PT_SLEEP(pt,500); //wait 500ms between toggling the LED
}
PT_FUNC_END(pt);
}

ptFunc blinkRed(void) {
PT_FUNC_START(pt);
while(true) {
PT_WAIT_WHILE(pt,isGreenOn); // wait if isGreenOn is true
toggleLEDRed();
PT_SLEEP(pt,50); //wait 50ms between toggling the LED
}
PT_FUNC_END(pt);
}

void loop() {
blinkGreen();
blinkRed();
}

So now, the LED blinks red very quickly when the green LED is off, but holds the red LED state when the green LED is one. This example uses PT_WAIT_WHILE() to pause execution until a condition has been met; PT_WAIT_UNTIL() does the thing with inverted logic.

Memory & Threading Functions

Let's say I want to sum the square of all numbers between 1 and 100. But this calculation isn't that time-critical so I want to let other functions run while this calculation is happening. So I might write:

ptFunc calcSumSquared(void) {
PT_FUNC_START(pt);
uint i;
uint total = 0;
for(i = 0; i <= 100; i++) {
total += i*i;
PT_YIELD(pt); // pause here for other threads to run
}
PT_FUNC_END(pt);
}

Once the calculation is finished, how to store the result since one can't use the return function in threaded functions? Usually, the best approach is to use a global variable for the data. So we can then access this data from other functions (threaded or otherwise). So now we have:

uint total = 0;
ptFunc calcSumSquared(void) {
PT_FUNC_START(pt);
uint i;
uint inProgressTotal = 0;
for(i = 0; i <= 100; i++) {
inProgressTotal += i*i;
PT_YIELD(pt);
}
total = inProgressTotal;
PT_FUNC_END(pt);
}

Note, we could combine total and inProgressTotal but then other functions can read the data as it is changing, so often it is best to only do updates to globals when the calculation is complete.

But if you compile this function, you'll get two very important warnings

ProcessData.ino:27:10: warning: 'inProgressTotal' may be used uninitialized in this function
27 | uint inProgressTotal = 0;
| ^~~~~~~~~~~~~~~
ProcessData.ino:28:5: warning: 'i' may be used uninitialized in this function
28 | for(i = 0; i <= 100; i++) {
| ^~~

As always, compiler warnings are worth understanding and addressing. In this case its a reminder that we need to think about the program execution as C++ program. In C++, local variables have no persistent memory -- they get assigned when the function loads and initialized as the function executes. That works fine the first time our function is called. But then it hits PT_YIELD and exits. When the function is called again the local variables get assigned to memory and PT_FUNC_START causes the execution to jump forward to PT_YIELD, skipping over the code that sets initialized the variables. So the variables will be read and used without being initialized. And this is exactly what the compiler is warning about. Practically, this means these variables could have unknown values depending on what happens to have been previously stored in the memory locations they use. So your loop might finish very early (if i is 1000). Or never, if i is always 0. What we want is for the value if i to keep its value even when the function exits. The solution is to use the keyword static for that memory, which lets the compiler know that needs to be permeant memory that needs permeant storage.

uint total = 0;
ptFunc calcSumSquared(void) {
PT_FUNC_START(pt);
static uint i;
static uint inProgressTotal = 0;
for(i = 0; i <= 100; i++) {
inProgressTotal += i*i;
PT_YIELD(pt);
}
total = inProgressTotal;
PT_FUNC_END(pt);
}

And now the compiler warnings go away and our function runs as expected.

Note, that you don't need all your memory to be assigned as static. Only memory that needs to be persistent between PT calls like PT_YIELD andPT_SLEEP, etc. For example, if we have

uint total = 0;
ptFunc calcSomething(void) {
PT_FUNC_START(pt);
static uint i;
static uint inProgressTotal = 0;
for(i = 0; i <= 100; i++) {
uint temp = i*i + inProgressTotal/(5*i);
inProgressTotal += i*i + temp;
PT_YIELD(pt);
}
total = inProgressTotal;
PT_FUNC_END(pt);
}

Here program execution starts either at the top or at PT_YIELD on line 9. So unwrapping the for loop we have:

  PT_YIELD(pt); // (or start of function)
uint temp = i*i + inProgressTotal/(5*i);
inProgressTotal += i*i + temp;
PT_YIELD(pt);

since the variable temp is completely contained between the PT functions (including its initialization), it can be a temporary local variable. Its only if its value is needed between PT section that we need make that variable static so its value can be stored across multiple executions of the function. Note that whenever you are using += or similar operators that change the current value of a variable, they are using the previous value of that variable and usually that means it will need to be a static variable.

Static Memory & Threaded Functions

There's one more subtle point about static memory and threaded functions. Take the following example:

uint total = 0;
ptFunc calcSumSquared(void) {
PT_FUNC_START(pt);
static uint inProgressTotal = 0;
for(static uint i = 0; i <= 100; i++) {
inProgressTotal += i*i;
PT_YIELD(pt);
}
total = inProgressTotal;
PT_FUNC_END(pt);
}

This would compile without errors or warnings. And when you run that thread, it will work as expected. However, it has a subtle bug. If you were to run that thread a second time (via restarting the thread), it wouldn't give the same result. The reason is that static variables are only initialized once, and they are initialized when the device boots up. So line 5 doesn't initialize i to zero when the function is run. i starts at zero and then increments to 100 when the function runs the first time. But when you restart the thread, it won't set i = 0 , it will just run the for loop with the previous value of i of 100 and then the evaluation if i <= 100 will be false so nothing in the for loop will run. Additionally, inProgressTotal will never get reset to 0 so it'll just keep summing the result for multiple restarts of the thread, which is often not desired. To initialize these values when the function runs, it must be done with a separate line from that static declaration:

uint total = 0;
ptFunc calcSumSquared(void) {
PT_FUNC_START(pt);
static uint i;
static uint inProgressTotal = 0; // value set at boot only
inProgressTotal = 0; // value set to zero here
for(i = 0; i <= 100; i++) {
inProgressTotal += i*i;
PT_YIELD(pt);
}
total = inProgressTotal;
PT_FUNC_END(pt);
}
Coding Recommendation

Usually the easiest, safest thing is to never initialize static variables when they are declared, and instead always set them in the code. This way you have to explicit control the initialization of variables.

Rewriting the above code to follow this recommendation, you get:

uint total = 0;
ptFunc calcSumSquared(void) {
PT_FUNC_START(pt);
static uint i; //do not initialize
static uint inProgressTotal; //do not initialize
inProgressTotal = 0; // initialize here
for(i = 0; i <= 100; i++) { // i is initialized here
inProgressTotal += i*i;
PT_YIELD(pt);
}
total = inProgressTotal;
PT_FUNC_END(pt);
}

Example 4: Run Slow Function While Blinking the LED

The previous example showed two slow or infrequent loops running, but what if we want our main function that is processing data to run quickly but we will still want an LED to blink while its processing the data. In this case, let's have the Quarto blink red every 500ms while the Quarto is processing some data, but then change to blinking green once the processing is done.

For the function that blinks the LED, it will look very similar to the previous example, except we will query if the processData thread has completed to determine the LED color to blink:

ptFunc blinkLED(void) {
PT_FUNC_START(pt);

while(true) {
PT_SLEEP(pt, 500);
if (PT_IS_RUNNING(processData)) {
setLED(1,0,0); //turn on RED LED
} else {
setLED(0,1,0); //turn on Green LED
}
PT_SLEEP(pt, 500);
setLED(0,0,0); //turn off LEDs
}

PT_FUNC_END(pt);
}

The PT_IS_RUNNING function returns false if threaded has finished, and true if it still is running, so we can use that to query if the function has completed.

As an example, for the data processing function, we will do the following slow calculation:

double processData(void) {
double calc = 1.234;
for(uint i=0; i< 50000000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
}
return calc;
}

The first step is to convert the processData function into a protoThread function. We will make the following changes:

  • Change to function return type to ptFunc
  • Add PT_FUNC_START and PT_FUNC_END wrappers
  • Remove the return line
  • Use static variables for calc and i

With those changes we have:

ptFunc processData(void) {
PT_FUNC_START(pt);
static uint i;
static double calc;
calc = 1.234;
for(i=0; i< 5000000;i++) {
double temp = cos(calc*(calc-1.23*i)); //temp variable can be local
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(pt); // pause here to check if other threads need to be run
}

Serial.printf("Result of calculation is %f\n",calc);
PT_FUNC_END(pt);
}

Click here for the full code
#include <protothreads.h>

ptFunc blinkLED(void) {
PT_FUNC_START(pt);

while(true) {
PT_SLEEP(pt, 500);
if (PT_IS_RUNNING(processData)) {
setLED(1,0,0); //turn on RED LED
} else {
setLED(0,1,0); //turn on Green LED
}
PT_SLEEP(pt, 500);
setLED(0,0,0); //turn off LEDs
}
PT_FUNC_END(pt);
}

ptFunc processData(void) {
PT_FUNC_START(pt);
static uint i;
static double calc;
calc = 1.234;
for(i=0; i< 5000000;i++) {
double temp = cos(calc*(calc-1.23*i)); //temp variable can be local
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(pt); // pause here to check if other threads need to be run
}


Serial.printf("Result of calculation is %f\n",calc);
PT_FUNC_END(pt);
}


void loop() {
// Run the two threads
processData();
blinkLED();
}

Example 5: Restarting a Completed Thread

In example #4, once the processData thread completes, it never runs again. If, however, we wanted to re-run a thread that has completed, we can do that with the function PT_RESTART. However, to do that PT_RESTART needs the (pointer to the) pt object that stores the thread's internal state and in the above examples that object is not exposed outside of the thread itself. So we'll have to change the structure a bit to create the pt object outside the function so we have access to it in other functions. First we create that pt object and a pointer to it as globals by instantiating them outside of any function with:

pt ptProcessData = {0};
pt* ptProcess = &ptProcessData;

Then when we create the processThread function, we will start it with PT_FUNC_START_EXT(ptProcess) instead of the PT_FUNC_START(pt). Other PT functions like PT_FUNC_END and PT_SLEEP, etc will take ptProcess, the pointer to the global object as the argument instead of pt, which typically is created (locally) by the PT_FUNC_START function. So now we have:

#include <protothreads.h>

pt ptProcessData = {0};
pt* ptProcess = &ptProcessData;

ptFunc processData(void) {
PT_FUNC_START_EXT(ptProcess);

static double calc = 0.1234;
static uint i=0;

for(i=0; i< 2500000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(ptProcess); // pause here to check if other threads need to be run
}
PT_FUNC_END(ptProcess);
}

Finally, we will create a new thread which waits for the processData thread to finish, then waits 10 seconds and then restarts the processData thread. Here's how that function is written:

ptFunc reProcess(void) {
PT_FUNC_START(pt);
while(true) {
PT_WAIT_THREAD(pt,processData); //wait until processData thread completes
PT_SLEEP(pt,10000); //wait 10s after processData is done
PT_RESTART(ptProcess); //Restart the finished thread
}
PT_FUNC_END(pt);
}

Putting that all together, the Quarto will blink red while it is processing data, then it will wait for 10 seconds while blinking green and then it will start processing data again and blink red, and repeat that pattern indefintely.

Click here for the full code
#include <protothreads.h>

pt ptProcessData = {0};
pt* ptProcess = &ptProcessData;

ptFunc blinkLED(void) {
PT_FUNC_START(pt);
while(true) {
PT_SLEEP(pt, 500);
if (PT_IS_RUNNING(processData)) {
setLED(1,0,0); //turn on RED LED
} else {
setLED(0,1,0); //turn on Green LED
}
PT_SLEEP(pt, 500);
setLED(0,0,0); //turn off LEDs
}
PT_FUNC_END(pt);
}

ptFunc processData(void) {
PT_FUNC_START_EXT(ptProcess);
static double calc = 0.1234;
static uint i=0;

for(i=0; i< 2500000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(ptProcess); // pause here to check if other threads need to be run
}
PT_FUNC_END(ptProcess);
}

ptFunc reProcess(void) {
PT_FUNC_START(pt);
while(true) {
PT_WAIT_THREAD(pt,processData); //wait until processData thread completes
PT_SLEEP(pt,10000); //wait 10s after processData is done
PT_RESTART(ptProcess); //Restart the finished thread
}
PT_FUNC_END(pt);
}

void loop() {
// Run the two threads
processData();
blinkLED();
reProcess();
}

Example 6: Threads Calling Threads

In this last example, we can have threads call (or spawn) other threads. In this example, we will reproduce the behavior of example #5 but instead of having the main loop run three threads (blinkLED, processData, reProcess), we need only two: blinkLED and reProcess and that second thread will spawn the other thread, processData. Instead of restarting the processData thread that runs in loop, we simply spawn it as needed from another thread. The command to run a new thread is

PT_SPAWN(pt,thread_pt,thread);

where pt is the (pointer to the) pt object of the current thread. thread_pt is the (pointer to the) pt object for the thread we are going to run, so as in example #5, that object will need to be defined externally. Finally, the last argument is the threaded function itself.

Now we can slightly alter the reProcess thread to run PT_SPAWN and not PT_WAIT_THREAD or PT_RESTART

ptFunc reProcess(void) {
PT_FUNC_START(pt);
while(true) {
//PT_WAIT_THREAD(pt,processData()); //replaced with PT_SPAWN
PT_SPAWN(pt,ptProcess,processData);
PT_SLEEP(pt,10000); //wait 10s after processData is done
//PT_RESTART(ptProcess); //also repalced by PT_SPAWN
}
PT_FUNC_END(pt);
}

Additionally, we can remove processData from the main loop as it now gets called on demand, instead of being restarted.

Click here for the full code
#include <protothreads.h>

pt ptProcessData = {0};
pt* ptProcess = &ptProcessData;

ptFunc blinkLED(void) {
PT_FUNC_START(pt);
while(true) {
PT_SLEEP(pt, 500);
if (PT_IS_RUNNING(processData)) {
setLED(1,0,0); //turn on RED LED
} else {
setLED(0,1,0); //turn on Green LED
}
PT_SLEEP(pt, 500);
setLED(0,0,0); //turn off LEDs
}
PT_FUNC_END(pt);
}

ptFunc processData(void) {
PT_FUNC_START_EXT(ptProcess);
static double calc = 0.1234;
static uint i=0;

for(i=0; i< 2500000;i++) {
double temp = cos(calc*(calc-1.23*i));
calc = temp*sin((calc-0.23*i)*3.456)+calc*calc/9.8765;
PT_YIELD(ptProcess); // pause here to check if other threads need to be run
}
PT_FUNC_END(ptProcess);
}

ptFunc reProcess(void) {
PT_FUNC_START(pt);
while(true) {
//PT_WAIT_THREAD(pt,processData()); //replaced with PT_SPAWN
PT_SPAWN(pt,ptProcess,processData);
PT_SLEEP(pt,10000); //wait 10s after processData is done
//PT_RESTART(ptProcess); //also repalced by PT_SPAWN
}
PT_FUNC_END(pt);
}

void loop() {
// Run the two threads
blinkLED();
reProcess();
}

protoThreads vs Interrupts

You may have noticed that many of these examples could have been accomplished by having the LED blinking controlled by a function that runs via a Internal Timer interrupt (see the Arbitrary Output Waveform for an example). A simple couple lines of code could have a LED-toggling function run every 500ms:

IntervalTimer LED_Toggle;
void setup() {
LED_Toggle.begin(runFunction,500e3);
}

More generally, interrupts can be used instead of threads to allow jumping between different functions. Below we will look at some of the strengths of using interrupts instead of threads.

Interrupt-based timing has some distinct advances:

  • Excellent Timing Precision
  • No processing overhead except when interrupt fires
  • Not required to use static (or global) memory for local function memory
  • No software modifications needed to pause and restart execution of main loop

Because the interrupt interrupts the processor, it can do so, for example, exactly every 500ms instead of the first time after 500ms that the processor happens to check if the thread needs to run. With protoThreads, this timing and delay jitter can be small (<1µs) or arbitrarily large depending on how often your program gets to a PT_YIELD or PT_SLEEP function and how many threads you have running. Additionally, with protoThreads, there is some processing overhead with constantly checking if other threads need to run. The lower the timing delay and jitter, the more often we are checking on the threads, the higher the overhead and visa-versa. That said, checking on the status of threads typically takes about 200ns, so even processes that run for up to no more than 10µs (thus 10µs of timing jitter and delay if the other threads' delay is negligible ) run at 98% full processing power. By 1ms, that goes to 99.98%.

Also with interrupts, the Quarto keeps the memory state of the function that gets interrupted, so there is no need to declare local variables as static like there is with protoThreads. In certain cases this can save significant memory. In fact no changes to the main loop need to be made to support an interrupt function unless that function needs to interact with the main code.

protoThread-based timing has some distinct advantages:

  • Unlimited Number of protoThreads
  • Can interrupt other interrupts
  • More flexible prioritization, can runs multiple loops as fast as possible
  • Does not crash if Quarto cannot keep up

One limitation is that the Quarto only supports 4 IntervalTimers running at the same time, while the number of protoThreads is not limited. Additionally, all IntervalTimer interrupts run at the same priority, and they cannot interrupt each other. This means that if one interrupt is slow, it will block and delay the running of another interrupt, which can undermine the precise timing possible with InternalTimer interrupts. Additionally, with interval interrupts, the only way to prioritize one interrupt over the other is to disable the one you don't want until the other on finishes. This can be done, but it can get complicated while protoThreads has a simple PT_WAIT_THREAD() function for one thread to wait until the other completes.

Also with protoThreads, by controlling where you place PT_YIELD and the like, you control when a function gets pre-empted, which can be tricky with interrupts. If you have two threads interacting with the same data, this can be very helpful because you can guarantee that, say, a consumer of the data never runs while a different thread is in the middle of writing that data.

Perhaps most importantly, you can have two or more threads running without delay with protoThreads, and yielding to other threads when they don't have work to. Reproducing that behavior with interrupts would be very challenging.

Additionally, if you have an interrupt that runs every 1ms, the function it runs must finish within 1ms. If it doesn't, then the Quarto will get back-logged with interrupts and never run anything else and a watchdog timer will catch this situation and reboot into the boot loader. (See the Troubleshooting Guide for more details on how the Quarto can crash.) With protoThreads, if you want to run a function every 1ms, but it takes longer than one 1ms to run, the timing will get pushed out but the Quarto will continue to execute all its threads.

Final Thoughts

Hopefully these examples show how flexible and powerful protoThreads can be. You can have lots of different threads, some waiting on timers, others waiting on threads to complete, and others running as fast as they can. In general, IntervalTimers are great for very fast functions (toggle an LED, set a flag, etc) that need precise timing. But for functions with greater complexity, especially if these functions interact with each other, protoThreads can be a great way to move beyond running every function sequentially.

Footnotes

  1. This ignores interrupts, but that is handled by hardware. From a software execution flow, interrupts can be ignored.

  2. Alternatively, a (proto)-threaded function can start with PT_FUNC_START_EXT instead of PT_FUNC_START, but this is a more advanced structure.