Tag: PWM

LED Dimmer with Raspberry Pi

LED Dimmer with Raspberry Pi

In this simple hardware setup, we will explore different coding possibilities using a Raspberry Pi with only a push button and an LED. It will be a good opportunity to compare Python classes vs functions, with code that shows the benefits of the former. More specifically, classes offer modularity and the possibility of reuse through multiple instances.

The diagram shows the very straightforward setup where the button and LED have one of their legs connected to a (distinct) GPIO pin and the other to a ground pin. A 330 Ω resistor must be connected in series with the LED, whose intensity is controlled by a PWM output. That is as simple as it gets.

Note: If the button doesn’t seem to be working, try connecting a different pair of legs, since some of them could be common.

While the dimmer functionality could be achieved using solely electronic circuitry and without a Raspberry Pi, the main point of this post is to explore coding and see what can be done with software!

Let’s start with the requirements of how we want the dimmer to work. By pressing and holding the button, the LED light intensity should increase linearly until reaching its maximum value and then decrease until it reaches its minimum value (LED off). This cycle repeats itself until the button is released, causing the light intensity to stay constant at its last value. By pressing the button again, the cycle resumes from where it stopped.

The plots provide a visual understanding of the requirements. The PWM output controlling the LED intensity ramps up (green) and down (red) while the button is pressed. As it is released, the last PWM output level is then held until the button is pressed again.

In terms of control logic, I use an integer to count ramp transitions. Even values correspond to upward ramps and odd values to downward ramps. Every time the button is released and then pressed again, the counter is reset to zero.

There are two main components in the control logic for the dimmer:

  • The calc_ramp() function that creates the triangular wave output in three steps. 1) Linear output generation, 2) Converting linear output to a tooth saw wave, thus limiting it between 0 and 1, and 3) Flipping odd count sections to generate a triangular wave.
  • Time shifting and counter reset to ensure smooth transitions every time the button is released and then pressed again. When a ramp is interrupted due to a button release event, it it will restart from where it stopped so the effective ramp duration (discounting the elapsed released time) is always the same.

Python Function

The code below shows how the function calc_ramp() and the time shifting are integrated inside an execution loop that controls the LED light intensity. As usual, the interface with the Raspberry Pi is done using GPIO Zero classes, more specifically DigitalInputDevice for the button pin and PWMOutputDevice for the LED pin.

# Importing modules and classes
import time
import numpy as np
from gpiozero import DigitalInputDevice, PWMOutputDevice
from utils import plot_line

# Assigning dimmer parameter values
pinled = 17  # PWM output (LED input) pin
pinbutton = 5  # button input pin
pwmfreq = 200  # PWM frequency (Hz)
pressedbefore = False  # Previous button pressed state
valueprev = 0  # Previous PWM value
kprev = 0  # Previous PWM ramp segmment counter
tshift = 0  # PWM ramp time shift (to start where it left off)
tramp = 2  # PWM output 0 to 100% ramp time (s)
# Assigning execution loop parameter values
tsample = 0.02  # Sampling period for code execution (s)
tstop = 30  # Total execution time (s)

# Preallocating output arrays for plotting
t = [] # Time (s)
value = [] # PWM output duty cycle value
k = [] # Ramp segment counter


def calc_ramp(t, tramp):
    """
    Creates triangular wave output with amplitude 0 to 1 and period 2 x tramp.

    """
    # Creating linear output so the value is 1 when t=tramp
    valuelin = t/tramp
    # Creating time segment counter
    k = t//tramp
    # Shifting output down by number of segment counts
    value = valuelin - k
    # Flipping odd count output to create triangular wave
    if (k % 2) == 1:
        value = 1 - value
    return value, k


# Creating button and PWM output (LED input) objects
button = DigitalInputDevice(pinbutton, pull_up=True, bounce_time=0.1)
led = PWMOutputDevice(pinled, frequency=pwmfreq)

# Initializing timers and starting main clock
tpressed = 0
tprev = 0
tcurr = 0
tstart = time.perf_counter()
# Initializing PWM value and ramp time segment counter
valuecurr = 0
kcurr = -1

# Executing loop
print('Running code for', tstop, 'seconds ...')
while tcurr <= tstop:
    # Executing code every `tsample` seconds
    if (np.floor(tcurr/tsample) - np.floor(tprev/tsample)) == 1:
        # Getting button properties only once
        pressed = button.is_active
        # Checking for button press
        if pressed and not pressedbefore:
            # Calculating ramp time shift based on last PWM value
            if (kprev % 2) == 0:
                tshift = valueprev*tramp
            else:
                tshift = (1-valueprev)*tramp + tramp
            # Starting pressed button timer
            tpressed = time.perf_counter() - tstart
            # Updating previous button state
            pressedbefore = True
        # Checking for button release
        if not pressed and pressedbefore:
            # Storing PWM value and ramp segment counter
            valueprev = led.value
            kprev = kcurr
            # Updating previous button state
            pressedbefore = False
        # Updating PWM output (LED intensity)
        if pressed:
            valuecurr, kcurr = calc_ramp(tcurr-tpressed+tshift, tramp)
            led.value = valuecurr
        # Appending current values to output arrays
        t.append(tcurr)
        value.append(valuecurr)
        k.append(kcurr)
    # Updating previous time and getting new current time (s)
    tprev = tcurr
    tcurr = time.perf_counter() - tstart
print('Done.')
# Releasing pins
led.close()
button.close()

# Plotting results
plot_line([t]*2, [value, k],
        yname=['PWM Output', 'Segment Counter'], axes='multi')
plot_line([t[1::]], [1000*np.diff(t)], yname=['Sampling Period (ms)'])

Python Class

Having the function and the time shifting “mixed” with the code makes for a less clean execution loop section, which can get pretty busy if there are more devices being controlled inside of it. The next piece of code shows how to integrate the two main components of the dimmer control inside a Python class. In addition to removing complexity from the execution loop, it makes it really easy to run multiple dimmers at the same time! This circles back to the modularity that was mentioned at the top of the post. You can create different classes for more complex devices and have them all be part of a module, such as gpiozero_extended.py. In the example below, I create two LED dimmer objects that can be controlled independently.

# Importing modules and classes
import time
import numpy as np
from gpiozero import DigitalInputDevice, PWMOutputDevice


class Dimmer:
    """
    Class that represents an LED dimmer.

    """
    def __init__(self, pinbutton=None, pinled=None):
        # Assigning GPIO pins
        self._pinbutton = pinbutton  # button input pin
        self._pinled = pinled  # PWM output (LED input) pin
        # Assigning dimmer parameter values
        self._pwmfreq = 200  # PWM frequency (Hz)
        self._tshift = 0  # PWM ramp time shift (to start where it left off)
        self._tramp = 2  # PWM output 0 to 100% ramp time (s)
        self._tpressed = 0  # Time button was pressed (s)
        self._tstart = 0  # Starting time of dimmer execution
        self._pressed = False  # Current button pressed state
        self._pressedbefore = False  # Previous button pressed state
        self._valueprev = 0  # Previous PWM value
        self._kcurr = -1  # Current PWM ramp segment counter
        self._kprev = 0  # Previous PWM ramp segmment counter
        # Creating button and PWM output (LED input) objects
        self._button = DigitalInputDevice(self._pinbutton, pull_up=True, bounce_time=0.1)
        self._led = PWMOutputDevice(self._pinled, frequency=self._pwmfreq)

    def _calc_ramp_value(self, t):
        # Creating linear output so the value is 1 when t=tramp
        valuelin = t/self._tramp
        # Creating time segment counter
        self._kcurr = t//self._tramp
        # Shifting output down by number of segment counts
        value = valuelin - self._kcurr
        # Flipping odd count output to create triangular wave
        if (self._kcurr % 2) == 1:
            value = 1 - value
        return value

    def reset_timer(self, tstart):
        # Resets dimmer timer to `tstart`
        self._tstart = tstart

    def update_value(self, t):
        # Getting button properties only once
        self._pressed = self._button.is_active
        # Checking for button press
        if self._pressed and not self._pressedbefore:
            # Calculating ramp time shift based on last PWM value
            if (self._kprev % 2) == 0:
                self._tshift = self._valueprev*self._tramp
            else:
                self._tshift = (1-self._valueprev)*self._tramp + self._tramp
            # Starting pressed button timer
            self._tpressed = time.perf_counter() - self._tstart
            # Updating previous button state
            self._pressedbefore = True
        # Checking for button release
        if not self._pressed and self._pressedbefore:
            # Storing PWM value and ramp segment counter
            self._valueprev = self._led.value
            self._kprev = self._kcurr
            # Updating previous button state
            self._pressedbefore = False
        # Updating PWM output (LED intensity)
        if self._pressed:
            self._led.value = self._calc_ramp_value(t-self._tpressed+self._tshift)

    def __del__(self):
        self._button.close()
        self._led.close()


# Assigning execution loop parameter values
tsample = 0.02 # Sampling period for code execution (s)
tstop = 30 # Total execution time (s)

# Creating dimmer objects
dimmer1 = Dimmer(pinled=17, pinbutton=5)
dimmer2 = Dimmer(pinled=18, pinbutton=6)

# Initializing timers and starting main clock
tprev = 0
tcurr = 0
tstart = time.perf_counter()
# Resettting dimmer timer
dimmer1.reset_timer(tstart)
dimmer2.reset_timer(tstart)

# Executing loop
print('Running code for', tstop, 'seconds ...')
while tcurr <= tstop:
    # Executing code every `tsample` seconds
    if (np.floor(tcurr/tsample) - np.floor(tprev/tsample)) == 1:
        # Updating dimmer outputs
        dimmer1.update_value(tcurr)
        dimmer2.update_value(tcurr)
    # Updating previous time and getting new current time (s)
    tprev = tcurr
    tcurr = time.perf_counter() - tstart
print('Done.')

# Releasing pins
del dimmer1
del dimmer2

If you watch the video, you’ll notice that the light intensity change is not as linear as one would expect, even when the PWM output is. One way to improve this is to add a non-linear transfer function between the ramp function output and the actual PWM set point value.

Also, it would be nice to have the ability to switch the dimmer off by, for instance, pressing and releasing the button for a short period of time. The improved class that does that can be found on my GitHub page.

H-Bridge and DC Motor with Raspberry Pi

H-Bridge and DC Motor with Raspberry Pi

An H-bridge is an electronic circuit that can switch the polarity of the voltage applied to a load. In our case the load is an electric DC motor that is capable of rotating forward and backward. By using a PWM as an input to the system defined by the H-bridge and the motor, we can go from the digital to the analog domain, similarly to what we did with the Raspberry Pi DAC. In this case however, the motor replaces the RC filter and the output is a continuous motor torque/speed instead of a voltage value.

Looking at the bottom half of the diagram introduced in Analog-to-Digital Conversion, the power amplification stage is integrated into the H-bridge and the actuator is the motor. Together they constitute a digital-to-analog converter in a broader sense.

Since the Pi has a limited power output capability, the amplification stage is achieved with an external power supply or a set of batteries that can produce both the higher voltage and current required by a DC motor application. For this post, I’ll use a variable DC power supply set to 12 V and a small DC motor with an integrated encoder. While the encoder won’t be required at this point, it will be used in future posts. And last but not least, there’s the H-bridge, which can be in a single IC chip or part of an IC board. The two types will be considered for some Python code running on a Raspberry Pi 4B.

SN754410 quadruple half-H driver (chip)

The Texas Instruments SN754410 can be used as 4 half-H drivers or 2 H-bridge drivers, depending on the input configuration. The latter can be used to independently control two DC motors with forward and backward rotation. The figure below shows the pins on the chip and the corresponding GPIO pins on the Raspberry Pi.

For a single motor, in addition to the 5V and GND pins, only 3 GPIO pins are required. Unlike the DAC with RC filters, a much lower PWM frequency can be used for the DC motor. Therefore, any 3 GPIO pins can be chosen since the software-implemented PWM at 100 Hz is sufficient.

The breadboard diagram illustrates the wiring for the DC motor control. For example, the Enable signal can be connected to GPIO16, while PWM Forward and PWM Backward could be connected respectively to GPIO17 and GPIO18. The forward speed of the motor is adjusted through the duty cycle of the PWM Forward signal. Correspondingly, the backward speed uses the PWM Backward signal.

Later on in this post, I will go over a Python Class where the three signals can be used to easily set the motor rotation (in an open-loop fashion for now).

L298 dual H-bridge motor speed controller (board)

Another option is to use an IC board which avoids the need of a breadboard and can therefore be used in a more permanent DIY setup. Most importantly, the reason I chose this particular board is that it has a fundamental difference in how the forward and backward rotation is implemented, when compared to the SN754410 chip.

While the wiring is very similar to the previous setup, by taking a closer look (based on the wiring color scheme), you will notice a different number of Enable and PWM signals. This board has 2 Enable signals and only 1 PWM signal. Rotation direction is now controlled with Enable Forward and Enable Backward, while speed is adjusted with a single PWM duty cycle.

Python Motor Class

Even though GPIO Zero has a Motor Class, the class is not ideal for applications where the motor output speed has to continuously transition between forward and backward rotation direction. The Motor Class implemented in the code below allows for that behavior. Additionally, by coding the class from scratch, we can see how the enable and PWM signals are used, as well as how to accommodate for the two types of wiring setup.

If you have been following along on my blog, in one of my previous posts (VS Code on Raspberry Pi) I encourage the creation of a folder dedicated to your Python modules. With that said, this code should be saved in a module named gpiozero_extended.py alongside utils.py in the /home/pi/python/modules folder on your Raspberry Pi. Also, the fully commented Class can be found on my GitHub page.

from gpiozero import DigitalOutputDevice, PWMOutputDevice


class Motor:

    def __init__(self, enable1=None, enable2=None, pwm1=None, pwm2=None):

        # Identifying motor driver type and assigning appropriate GPIO pins
        if pwm1 and pwm2:
            # Driver with 1 enable and 2 PWM inputs
            # Example: SN754410 quadruple half-H driver chip
            if not enable1:
                raise Exception('"enable1" pin is undefined.')
            self._dualpwm = True
            self._enable1 = DigitalOutputDevice(enable1)
            self._pwm1 = PWMOutputDevice(pwm1)
            self._pwm2 = PWMOutputDevice(pwm2)
        elif enable1 and enable2:
            # Driver with 2 enables and 1 PWM input
            # Example: L298 dual H-bridge motor speed controller board
            if not pwm1:
                raise Exception('"pwm1" pin is undefined.')
            self._dualpwm = False
            self._enable1 = DigitalOutputDevice(enable1)
            self._enable2 = DigitalOutputDevice(enable2)
            self._pwm1 = PWMOutputDevice(pwm1)
        else:
            raise Exception('Pin configuration is incorrect.')
        # Initializing output value
        self._value = 0

    def __del__(self):

        # Releasing GPIO pins
        if self._dualpwm:
            self._enable1.close()
            self._pwm1.close()
            self._pwm2.close()
        else:
            self._enable1.close()
            self._enable2.close()
            self._pwm1.close()

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, _):
        print('"value" is a read only attribute.')

    def set_output(self, output, brake=False):

        # Limiting output
        if output > 1:
            output = 1
        elif output < -1:
            output = -1
        # Forward rotation
        if output > 0:
            if self._dualpwm:
                self._enable1.on()
                self._pwm1.value = output
                self._pwm2.value = 0
            else:
                self._enable1.on()
                self._enable2.off()
                self._pwm1.value = output
        # Backward rotation
        elif output < 0:
            if self._dualpwm:
                self._enable1.on()
                self._pwm1.value = 0
                self._pwm2.value = -output
            else:
                self._enable1.off()
                self._enable2.on()
                self._pwm1.value = -output
        # Stop motor
        elif output == 0:
            if brake:
                if self._dualpwm:
                    self._enable1.off()
                    self._pwm1.value = 0
                    self._pwm2.value = 0
                else:
                    self._enable1.off()
                    self._enable2.off()
                    self._pwm1.value = 0
            else:
                if self._dualpwm:
                    self._enable1.on()
                    self._pwm1.value = 0
                    self._pwm2.value = 0
                else:
                    self._enable1.on()
                    self._enable2.on()
                    self._pwm1.value = 0
        # Updating output value property
        self._value = output

The following example creates a motor object and runs a sinusoidal excitation with an amplitude of 1 for one full period. If you execute the program, you will notice that the direction of the motor rotation changes as the sign of the sine wave changes.

# Importing modules and classes
import time
import numpy as np
from gpiozero_extended import Motor

# Assigning parameter values
T = 4  # Period of sine wave (s)
u0 = 1  # Motor output amplitude
tstop = 4  # Sine wave duration (s)
tsample = 0.01  # Sampling period for code execution (s)

# Creating motor object using GPIO pins 16, 17, and 18
# (using SN754410 quadruple half-H driver chip)
mymotor = Motor(enable1=16, pwm1=17, pwm2=18)

# Initializing current time stamp and starting clock
tprev = 0
tcurr = 0
tstart = time.perf_counter()

# Running motor sine wave output
print('Running code for', tstop, 'seconds ...')
while tcurr <= tstop:
    # Getting current time (s)
    tcurr = time.perf_counter() - tstart
    # Doing I/O every `tsample` seconds
    if (np.floor(tcurr/tsample) - np.floor(tprev/tsample)) == 1:
        # Assigning motor sinusoidal output using the current time stamp
        mymotor.set_output(u0 * np.sin((2*np.pi/T) * tcurr))
    # Updating previous time value
    tprev = tcurr

print('Done.')
# Stopping motor and releasing GPIO pins
mymotor.set_output(0, brake=True)
del mymotor

DC Motor Remarks

If you’re using a motor with an integrated gear box and try some low output values with the motor object, you’ll notice that there’s a dead band where there’s no speed response as the sign changes around small duty cycle values. That happens because it is the DC motor torque (not the angular velocity) that is proportional to the input current (or very simplistically the PWM duty cycle). For small duty cycle values, the torque is being used to overcome the friction losses in the motor bearings and the gear box before the output shaft starts to turn.

There are several low-budget small DC motors that can be purchased online, usually with a wide range of gear ratios for the same overall packaging constraints. There should be an output shaft speed / torque combination that meets your project requirements. Keep in mind that the SN74410 has a limit of 1 Ampère per driver, which might affect the output that can be achieved. Board controllers are generally rated at much higher currents (5 A and above). The power supply capability should also be taken into account, by the way.

Finally, as I mentioned at the top, a motor with an integrated encoder will become essential if we want to incorporate the angular position of the output shaft into the Motor Class. That will enable closed-loop control of the motor speed and/or angular position, making for a good automation example.

Digital-to-Analog Conversion

Digital-to-Analog Conversion

As the counterpart of analog-to-digital conversion, DAC will take a digital signal and convert it to an analog one. Paraphrasing what I mentioned in previous posts, ADC and DAC walk hand-in-hand bridging the gap between the continuous and discrete domains that modern machines and devices have to deal with.

Even though there are several types of DACs, I will talk about the PWM-based one, where a reference input voltage is switched (in the form of a digital train of pulses) into a low-pass analog filter, producing an analog output. Before we get there, let’s first talk about Pulse Width Modulation, a very clever way to go from the discrete to the continuous domain, invented in the mid-seventies. A PWM signal has two main characteristics: its frequency and its duty cycle.

The figure illustrates the frequency and the concept of duty cycle for a 10 Hz PWM signal. In this case, the period of the PWM is 1/fPWM = 0.1 s. The chosen duty cycle is 25 %, which corresponds to the time the output is “on” (0.025 s). During the remainder of the duration of the PWM period (0.075 s) the output is “off”. As a matter of fact, it’s the modulation of the duty cycle that will control the output level of the DAC system.

Since it’s either fully “on” or fully “off”, the PWM signal is fundamentally a digital one. It’s the frequency and duty cycle of that “on-off” train of pulses, coupled with the response of the sub-system it’s interacting with, that will result in an analog behavior of the output of the overall system, in our case the digital-to-analog converter.

As a side note, the Python code used to generate the graphs in this post can be found on my GitHub page. Make sure to check it out and experiment with the DAC parameters.

The simplest DAC implementation is shown on the left. It consists of a low-pass first-order passive RC filter connected to the PWM output signal Vref. Typically, for a TTL circuit, the voltage can be either 0 (low) or 3.3V (high).

The cutoff frequency (rad/s) of the filter is fc = 1/(RC). Along with the PWM frequency, it can be used to obtain the desired behavior of this simple DAC.

Output Ripple

The PWM signal can be thought as a sequence of low-to-high and high-to-low step inputs into the RC filter. Even with a constantly switching input, this first order system will eventually produce a near constant output. The figures below show the effect of the RC filter cutoff frequency fc as well as the PWM frequency fPWM on the amplitude of the output ripple.

As it can be observed in the plots above, the output voltage will converge to (duty cycle) x Vref , which for the duty cycle of 25 % (used in the example) produces an output of 0.825 V when Vref = 3.3 V. Also, reducing the filter cutoff frequency or increasing the PWM frequency are both effective in reducing the output signal ripple. While having a slow RC filter can greatly reduce the ripple, that will negatively affect the DAC response time when it’s subject to a change in the desired output value.

DAC Step Response

In the case of our DAC, its response time is essentially the response time of the RC filter, i.e. 1/fc = RC (with fc in rad/s). Leaving the details for a future post on analog and digital filtering, for the first order system under consideration, the response time can be defined as the time it takes for the output to reach about 63 % of its final (steady-state) value, after a step input is applied.

The next figure shows how an appropriate combination of a higher PWM frequency and a higher filter cutoff frequency can produce a faster response time, while maintaining roughly the same amplitude of the DAC output ripple. For our example, the response time based on the RC value goes from 0.16 s down to 0.04 s.

Cascaded Filters

The output ripple can be further improved by cascading two first-order RC filters, as shown below. The newly formed second-order filter will have twice the attenuation (dB/decade) above the cutoff frequency. It’s an easy-to-do improvement which is often adopted.

Practical Considerations

Just like its ADC counterpart, DACs also have an inherent quantization in the signal. The digital PWM, generated from the microprocessor clock, has a finite resolution (duty cycle levels) given by the number of bits of the PWM. Typical resolutions range from 8 to 12 bits.

Actual PWM frequencies for DAC devices are much higher than the ones used in the examples above, which were chosen mainly to illustrate the concepts. For applications involving testing of physical systems or automation, frequencies between 500 and 2000 Hz are reasonable. In audio applications, several times the highest frequency of the audible range (20 kHz) seems to be the case. That is, 100 to 200 kHz. However, just as we observed when using higher order filtering, there’s wiggle room to reduce these numbers while still having a good compromise between response time and ripple.

It’s important to make a distinction at this point: if the PWM is used to drive a DC motor directly (or with some power amplification), the motor itself will behave approximately as a first order system. Hence, there’s no need for the RC filter. In this type of application, PWM frequencies of 100 to 200 Hz are just fine.

Amplification of the DAC output beyond the typical 3.3V Vref, can be achieved by using an Op Amp (Operational Amplifier). For a simple non-inverting amplifier, the output voltage is given by:

V2 = V1(1+R1/R2)

By choosing R1 = R2 = 10 kOhm, we can double the output to accommodate a more typical 0 to 5V requirement.

In my next post, we will explore the implementation of a DAC using a Raspberry Pi and a few resistors and capacitors. The Pi has hardware enabled PWM on two of its GPIO pins, which can reach frequencies in the kHz range! That should be an interesting project.

Prescribed PWM duty cycle

Prescribed PWM duty cycle

Within the concept of execution loops, let’s see how to apply a prescribed PWM duty cycle. The ideas presented here are applicable to an analog output (voltage) as well, if your DAQ device supports it. These examples use a Raspberry Pi with an LED and a series resistor of 300 to 1000 Ohm, as shown in this GPIO Zero recipe setup. They also use the plotting function plot_line defined in previous posts.

The first code example shows how to apply a pre-defined sequence of steps pwmvalue at regular time intervals tstep.

import time
import numpy as np
from utils import plot_line
from gpiozero import PWMOutputDevice

# Assigning parameter values
pinled = 17  # PWM output (LED input) pin
pwmfreq = 200  # PWM frequency (Hz)
pwmvalue = [0.4, 0.6, 0.8, 1.0, 0.1, 0.3, 0.5, 0.7, 0.9]
tstep = 1.0  # Interval between step changes (s)
tsample = 0.02  # Sampling period for code execution (s)
tstop = tstep * (len(pwmvalue)+1)  # Total execution time (s)
# Preallocating output arrays for plotting
t = []  # Time (s)
value = []  # PWM output duty cycle value

# Creating PWM output object (LED input)
led = PWMOutputDevice(pinled, frequency=pwmfreq)
# initializing other variables used in the loop
count = 0
valuecurr = 0.2

# Initializing timers and starting main clock
tprev = 0
tcurr = 0
tstart = time.perf_counter()
# Running execution loop
print('Running code for', tstop, 'seconds ...')
while tcurr <= tstop:
    # Updating PWM output every `tstep` seconds
    # with values from prescribed sequence
    if (np.floor(tcurr/tstep) - np.floor(tprev/tstep)) == 1:
        valuecurr = pwmvalue[count]
        led.value = valuecurr
        count += 1
    # Acquiring digital data every `tsample` seconds
    # and appending values to output arrays
    if (np.floor(tcurr/tsample) - np.floor(tprev/tsample)) == 1:
        t.append(tcurr)
        value.append(valuecurr)
# Updating previous time and getting new current time (s)
tprev = tcurr
tcurr = time.perf_counter() - tstart

# Releasing pins
led.close()
print('Done.')

# Plotting results
plot_line([t], [value], yname='PWM OUtput')
plot_line([t[1::]], [1000*np.diff(t)], yname='Sampling Period (ms)')

The second example shows how to apply a pre-defined sequence of ramps where each segment has a duration rampduration and final value rampvalue.

To run this example, the SciPy package needs to be installed on the Raspberry Pi (pi@raspberrypi:~$ sudo apt install scipy). You will need to fix a conflict between the numpy packages used by SciPy and Raspberry Pi, by installing this library: pi@raspberrypi:~$ sudo apt-get install libatlas-base-dev .

import time
import numpy as np
from utils import plot_line
from scipy.interpolate import interp1d
from gpiozero import PWMOutputDevice

# Assigning parameter values
pinled = 17  # PWM output (LED input) pin
pwmfreq = 200  # PWM frequency (Hz)
rampduration = [0, 0.5, 1.5, 0.5, 1.5, 0.5, 1.5, 0.5, 1.5, 0.5, 1.5]
rampvalue = [0, 0.2, 0.0, 0.4, 0.0, 0.6, 0.0, 0.8, 0.0, 1.0, 0.0]
tsample = 0.02  # Sampling period data sampling (s)
tstop = np.sum(rampduration)  # Total execution time (s)
# Preallocating output arrays for plotting
t = []  # Time (s)
value = []  # PWM output duty cycle value

# Creating interpolation function for ramp sequence
tramp = np.cumsum(rampduration)
framp = interp1d(tramp, rampvalue)
# Creating PWM output object (LED input)
led = PWMOutputDevice(pinled, frequency=pwmfreq)

# Initializing timers and starting main clock
tprev = 0
tcurr = 0
tstart = time.perf_counter()
# Running execution loop
print('Running code for', tstop, 'seconds ...')
while tcurr <= tstop:
    # Updating PWM output every loop step with
    # interpolated ramp values at the current time
    valuecurr = framp(tcurr)
    led.value = valuecurr
    # Acquiring digital data every `tsample` seconds
    # and appending values to output arrays
    if (np.floor(tcurr/tsample) - np.floor(tprev/tsample)) == 1:
        t.append(tcurr)
        value.append(valuecurr)
    # Updating previous time and getting new current time (s)
    tprev = tcurr
    tcurr = time.perf_counter() - tstart

# Releasing pins
led.close()
print('Done.')

# Plotting results
plot_line([t], [value], yname='PWM OUtput')
plot_line([t[1::]], [1000*np.diff(t)], yname='Sampling Period (ms)')

By choosing the proper values for rampduration and rampvalue, virtually any trace composed of linear segments can be generated.

rampduration = [0, 0.5, 1.0, 1.0, 1.5, 0.5, 0.5, 0.5, 1.5, 0.5, 1.5, 1.0]
rampvalue = [0, 0.2, 0.2, 0.4, 0.4, 0.2, 0.2, 1.0, 1.0, 0.6, 0.4, 0.4]

And since the current time is used for the interpolation of the trace current value, a very accurate signal can be generated, regardless of fluctuations in the duration of the execution steps.

If you liked the featured image at the top of the post, feel free to get the Python source code on my GitHub page.