Thus far our discussions have extended mostly to the data acquisition side of a control system. In particular, how to sample data from a sensor. There is also one crucial property of a control system that we still need to discuss before we can do much of anything useful. In this post we will attempt to use an accurate time signal to trigger a PWM signal from userspace, using what we learned about triggering GPIOs in the previous post. Since we do not have a special PWM hardware block to help us with this, we will investigate the performance we can get with a purely software driven signal.
1.0 Linux Timekeeping Mechanisms
In Linux there are two main timing mechanisms which we will be taking a look at. The first, is the standard timing mechanism which has been around for quite a long time, namely the timer wheel. The second mechanism has been introduced relatively recently with Linux kernel 2.6, specifically the high resolution timer, hrtimer for short.
The timer wheel should mostly be used for applications that do not require high performance and high accuracy timers. It is a great timer to use in userspace due to its simplicity. The HR timer has been designed specifically for high performance applications having nanosecond precision (though in practice this is usually much smaller, as we will see). In kernel space, we can use the HR timer in a real interrupt and thus avoid time penalties accrued by system calls, however for simplicity sake, we will play with the HR Timer in userspace first.
2.0 HR-Timer Structures
There are essentially 3 structures we need to use, to obtain a time signal at a specific interval from Linux.
1. A struct itimerspecA structure which contains the timer information, frequency, interval etc.
2. A struct sigaction -This structure denotes what we do when we get the above signal upon timer expiry, specifically, we can register a callback for the signal.
3. A struct sigevent – In this structure we can specify the type of signal we wish to receive from Linux upon the expiration of our timer eg: SIGALRM, SIGUSR1, SIGUSR2 etc, and other signals are available for our choosing.
3.0 HR-Timer Functions
Now that we have all our structures, we need to register with the kernel using the timer APIs.
1. The function int sigaction(2) we use to tie our callback aka struct sigaction with the signal of our choosing. For the purposes of this example I selected the signal SIGUSR1 since it has no pre-defined meaning and is specifically reserved for a user program.
2. The function int timer_create(3) will tie our struct sigevent to our struct itimerspec this ensures that whenever the timer triggers, the kernel will generate the event we specified which will in turn call the callback we registered.
3. And last but not least function int timer_settime(3) will start the timer.
4.0 Building a PWM Signal
Now that we have all the pieces in place, we can build a PWM signal using the knowledge we gained about our GPIOs in our previous discussion and the timer.
First let’s think about it on a high level. We will need our main function to create the timer and register the signal handler with the timer signal. Moreover, since a PWM signal with a set duty cycle is not very useful, the main function should wait for a percentage to be input from the keyboard and change the duty cycle accordingly.
4.1 ON Phase
What we will refer as the ON phase refers specifically to the time duration in which the GPIO is held high.
This occurs first after the main() function initially sets up the itmer to just before the timer expires. From that point on, the ON phase will occur every other timer expiration after every OFF phase.
- Our signal handler, will be triggered first on the ON period of the phase by our main function.
- The signal handler will set GPIO 38 to an ON state.
- It will then have to change the timeout value of the timer which invoked it to the ON duration of the period.
- The signal handler will then exit.
At this point GPIO 38 will be left ON by the signal handler and will remain so until the timer expires again.
4.2 OFF Phase
What we will refer as the OFF phase refers specifically to the time duration in which the GPIO is held low. This occurs when the timer expires again, in the signal handler, after being re-programmed during the ON phase. During this segment of time, the following occur:
- Since the timer expiry value was reprogrammed during the ON phase for the ON phase duration, as soon as the signal handler is invoked again, it will set the GPIO low.
- The timer will then re-program the timeout value of the timer, to the OFF duration of the phase.
- The signal handler will then exit.
At this point GPIO 38 will be left low by the signal handler and will remain so until the timer expires again.
The key point to note is that the signal handler will run much faster than the duration of each of the phases, as such we must re-program the timer to keep the GPIO in the correct state after we exit.
Now, since we plan on changing the duty cycle during our main function, we will have a race condition on the ON and OFF timer durations since the main function may try to re-calculate those values as the signal handler is reading them.
To fix this problem we must use a pthread mutex to serialize accesses to our PWM modulation variable.
5.0 Variable Duty Cycle
While the timer is busy expiring and being reset by the signal handler, our main function will continuously loop waiting for the user to input a new PWM duty cycle value. As soon as it detects that a new duty cycle value has been entered by the user, the main function will try to acquire the mutex to change the PWM duty cycle variable as well as the values for the timer expiry values being programmed during the ON and OFF periods.
As soon as the ON and OFF timer values have been re-programmed during our main function, the main function will program the timer expiry value to the new ON period duration.
So now that we have the basic theory of operation down, we can present the code available on GitHub here.
As the code works on a timer period of 1 second it is not particularly taxing for the PandaboardES to keep up with. There are of course some performance optimizations we can implement.
It is important to avoid calls to printf() in the context of the signal handler, as they are computationally expensive. Furthermore, other system calls should also be kept to a minimum as to avoid trapping into the kernel unnecessarily.
I did however want to see at what period values the system fails to keep up with the signals. A failure would appear if we program a timer expiration value so short, that we cannot complete the write to the sysfs attribute before the timer expires again. This would result in erratic behavior on the GPIO since the GPIO driver which handles the sysfs attribute may not be able to turn the pin on or off.
So at what value does the system ceases to function? If we set a period of 1 ms with a 50% duty cycle, the system seems to usually meet the specification, but the pulse widths have a higher degree of imprecision.
If we set the duty cycle to 40%, the system stays around 42.9% duty cycle, wavering significantly. Setting the duty cycle to 30% causes the output to stay around 35.7%.
Setting the duty cycle to between 25%- 10% keeps the actual duty cycle at 35.7%.
Setting the duty cycle to values below 10%, the output becomes completely unstable with long wait periods between bursty edges.
Since we are not dealing with an RTOS, it is not surprising that the period which we set is not guaranteed.
- UART Communication from Linux Userspace
- Introduction to Kernel Modules