BBBCSIO

PWMPort Example Code

 

About the PWMPort Class

The PWMPort class controls one of the six Pulse Width Modulation On Chip Peripheral (OCP) outputs integrated into the Beaglebone Black CPU. In the Linux operating system there are two ways of accessing the PWM devices. The first, file system access using device drivers, is the standard and approved Linux technique. If the existence of a PWM port is detected during boot time, the PWM device driver interface will be exposed as files in the /sys/class/pwm/ directory of the Beaglebone Black file system. The PWM ports will only be detected if that port is appropriately configured in the Device Tree.

The second type of access is called Memory Mapped access and this treats the Beaglebone Black's RAM memory as if it were a file. The PWM port is manipulated as if it were a bit at an offset in a virtual file.

There are twelve possible pins on the Beaglebone Black P8 and P9 headers which could, theoretically, output PWM signals. However, there are complications. Some of the pins are connected to the same PWM output and will only ever output the same signal. Also, some of the PWM outputs use the same base PWM OCP Device and, although they can output signals with independent duty cycles, the frequency of each module must be the same.

A listing of all PWM Devices and their P8/P9 Headers is given below

              PWM Name         H8/H9 Pins          PWMDevice_Output
               PWM0_A,        PWM_P9_22_or_P9_31     (EHRPWM0_A)
               PWM0_B,        PWM_P9_21_or_P9_29     (EHRPWM0_B)    
                            
               PWM1_A,        PWM_P9_14_or_P8_36     (EHRPWM1_A)
               PWM1_B,        PWM_P9_16_or_P8_34     (EHRPWM1_B)
            
               PWM2_A,        PWM_P8_19_or_P8_45     (EHRPWM2_A)
               PWM2_B,        PWM_P8_13_or_P8_46     (EHRPWM2_B)

See the comments in the example code below for an explanation of the meaning of the above table.

Warnings - READ THIS!!!

The header pin used in the examples below is P9_14. This means the PWM port PWMPortEnum.PWM1_A is used. This, according to the table above, is connected to the EHRPWM1A OCP PWM device. The PinMux state for P9_14 (although P8_36 could also be used) must be configured in the Device Tree in order for it to be available for use. Be aware that if you also create another PWMPortFS object on PWM1_B (pins P9_16 or P8_34) the frequency of the PWM port must be configured to be identical (they share the EHRPWM1 PWM device) but they could have an independent duty cycle. It is up to you to make sure that the header pin you choose to use is available for use (i.e. it is not in use by some other device on the Beaglebone Black) and is appropriately configured with a Device Tree Overlay.

Make sure you configure the duty cycle after you set the frequency on the port. The frequency is used to set the proper timings for the duty cycle and the duty cycle is not automatically adjusted if you change the base frequency of the PWM device.

The armhf Linux running on the Beaglebone Black is not a real-time operating system. However, an OCP peripherals such as the PWM Devices have their own internal clock and so, once the frequency and duty cycle information is given to the port, that waveform will maintained with no interruptions. However, the act of making changes to the configuration (the duty cycle - for example) can, and will, have variable timing as the process is pre-emotively swapped in and out by the kernel.

An Example of the PWMPort Usage

The code below illustrates how to use the PWMPortFS class to configure a PWM device output a variety of duty cycle waveforms.

        /// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=
        /// <summary>
        /// Tests a PWM Port (and presumably a servo attached to that port)
        /// by changing the pulse width duty cycle to 25%, 50% and 75% and
        /// back again. Once that is done, the pulse width is changed over the
        /// same range but much more smoothly. 
        /// 
        /// NOTE: 
        ///   This code assumes that the PWM device has been configured in the 
        ///   device tree. If it is not, then the PWM Port will probably not
        ///   be available. Usually this is done by adding the 
        ///   overlay to the uEnv.txt file. The Beaglebone Black and Device Tree Overlays
        ///   technical note has more information.
        /// 
        /// http://www.ofitselfso.com/BeagleNotes/Beaglebone_Black_And_Device_Tree_Overlays.php
        ///
        /// Any of the entries below placed in the /boot/uEnv.txt file will enable the 
        /// specified PWM ports. You may need to edit the .dts source if you only want A and 
        /// not B etc.
        /// uboot_overlay_addr4=/lib/firmware/BB-PWM0-00A0.dtbo (PWM0_A & PWM0_B)
        /// uboot_overlay_addr5=/lib/firmware/BB-PWM1-00A0.dtbo (PWM1_A & PWM1_B)
        /// uboot_overlay_addr6=/lib/firmware/BB-PWM2-00A0.dtbo (PWM2_A & PWM2_B)
        ///   
        ///   You should conduct the usual tests to make sure the PWM header pin
        ///   you wish to use is available and not in use by anything else on 
        ///   the Beaglebone Black PinMux. 
        /// 
        /// NOTE:
        ///    Be aware that although there are 12 pins available for PWM output
        ///    on the Beaglebone Black P8 and P9 headers in many cases two pins
        ///    share the same PWM output. In addition, in many cases two PWM 
        ///    outputs share the same PWM module. P8/P9 Header pins that 
        ///    share the same PWM output will always have identical signals in
        ///    frequency, pulse width and timing. They _are_ the same signal!
        /// 
        ///    PWM Outputs that share the same PWM OCP device MUST have the same
        ///    frequency but can have independent pulse widths. The trigger
        ///    timing of the start of the high part of pulse waveform is 
        ///    simultaneous.
        /// 
        ///    PWM Name         H8/H9 Pins          PWMDevice_Output
        ///    PWM0_A,        PWM_P9_22_or_P9_31     (EHRPWM0_A)
        ///    PWM0_B,        PWM_P9_21_or_P9_29     (EHRPWM0_B)    
        ///                 
        ///    PWM1_A,        PWM_P9_14_or_P8_36     (EHRPWM1_A)
        ///    PWM1_B,        PWM_P9_16_or_P8_34     (EHRPWM1_B)
        /// 
        ///    PWM2_A,        PWM_P8_19_or_P8_45     (EHRPWM2_A)
        ///    PWM2_B,        PWM_P8_13_or_P8_46     (EHRPWM2_B)
        /// 
        ///    This is IMPORTANT so I will say it again. If you configure 
        ///    two PWM outputs in the same device (PWM1_A, PWM1_B for example) 
        ///    then they MUST be configured with the same frequency. 
        /// 
        ///    They will use the same frequency anyways and if
        ///    you change one you change the other. Changing the frequency on the
        ///    B output will instantly change the frequency on the A output and
        ///    will really mess up any pulse widths/duty cycles the A output is using
        /// 
        ///    Always set the frequency first then the Pulse Width/duty cycle. The pulse 
        ///    width is calculated from whatever frequency is currently set it is
        ///    not adjusted if the frequency is later changed.
        ///
        /// From the above information, if you connect servos to both P9_14 and 
        /// P8_36 you will see the servos behave identically. If you connect
        /// servos to P9_14 and P9_16 the frequency must be identical (because
        /// the are both on PWM module EHRPWM1 but the pulse widths can be
        /// independently controlled. If you connect servos to P9_22 and
        /// P9_14 you can have distinct frequencies and pulse widths because
        /// the two PWM devices are fully independent.
        /// 
        /// </summary>
        /// <param name="pwmID">The pwmID</param>
        /// <history>
        ///    07 Mar 19  Cynic - Originally written
        /// </history>
        public void SimpleTestPWM_FS(PWMPortEnum pwmID)
        {
            const uint DEFAULT_PERIOD_NS = 250000;
            const uint DEFAULT_DUTY_50PERCENT = (uint)(DEFAULT_PERIOD_NS * (0.5));
            const uint DEFAULT_DUTY_75PERCENT = (uint)(DEFAULT_PERIOD_NS * (0.75));
            const uint DEFAULT_DUTY_25PERCENT = (uint)(DEFAULT_PERIOD_NS * (0.25));

            // open the port
            PWMPortFS pwmPort = new PWMPortFS(pwmID);

            // set the PWM waveform period
            pwmPort.PeriodNS = DEFAULT_PERIOD_NS;
            // we could also use FrequencyHz which does the same thing
            // pwmPort.FrequencyHz = 4000;

            // set the PWM waveform Duty Cycle
            pwmPort.DutyNS = DEFAULT_DUTY_50PERCENT;
            // we could also use DutyPercent which does the same thing
            // pwmPort.DutyPercent = 50;

            // set the run state to begin the output of the PWM waveform
            pwmPort.RunState = true;

            Console.WriteLine("PeriodNS is: " + pwmPort.PeriodNS.ToString());
            Console.WriteLine("DutyNS is: " + pwmPort.DutyNS.ToString());
            Console.WriteLine("FrequencyHz is: " + pwmPort.FrequencyHz.ToString());
            Console.WriteLine("DutyPercent is: " + pwmPort.DutyPercent.ToString());
            Console.WriteLine("RunState is: " + pwmPort.RunState.ToString());

            // change the PWM duty cycle (i.e. rotate the servo)
            // until we get a key press on the console
            while (true)
            {
                // first we rotate the servo in steps
                Console.WriteLine("");
                Console.WriteLine("Now Step Rotating Servo");

                // set the Duty Cycle low 
                pwmPort.DutyNS = DEFAULT_DUTY_25PERCENT;
                if (Console.KeyAvailable == true) break;
                Console.WriteLine("DutyPercent is: " + pwmPort.DutyPercent.ToString());
                Thread.Sleep(1000);
                // set the Duty Cycle midway
                pwmPort.DutyNS = DEFAULT_DUTY_50PERCENT;
                if (Console.KeyAvailable == true) break;
                Console.WriteLine("DutyPercent is: " + pwmPort.DutyPercent.ToString());
                Thread.Sleep(1000);
                // set the Duty Cycle high
                pwmPort.DutyNS = DEFAULT_DUTY_75PERCENT;
                if (Console.KeyAvailable == true) break;
                Console.WriteLine("DutyPercent is: " + pwmPort.DutyPercent.ToString());
                Thread.Sleep(1000);
                // set the Duty Cycle midway
                pwmPort.DutyNS = DEFAULT_DUTY_50PERCENT;
                if (Console.KeyAvailable == true) break;
                Console.WriteLine("DutyPercent is: " + pwmPort.DutyPercent.ToString());
                Thread.Sleep(1000);

                Console.WriteLine("");
                Console.WriteLine("Now Smoothly Rotating Servo");

                // now we rotate the servo smoothly from
                // 25.123% to 75.456% using a totally arbitrary increment
                for (float i = 25.123f; i < 75.345f; i = i + 0.678f)
                {
                    pwmPort.DutyPercent = i;
                    if (Console.KeyAvailable == true) break;
                    Console.WriteLine("DutyPercent is: " + pwmPort.DutyPercent.ToString());
                    Thread.Sleep(50);
                }
            }

            // we are done, stop running
            pwmPort.RunState = false;

            // close the port
            pwmPort.ClosePort();
            pwmPort.Dispose();
        }
In the above code, the pwmID is passed in when the function was called. This value is a member of the PWMPortEnum class which lists all possible PWM ports which can be present on the Beaglebone Black. The above code is called via a line which looks like:
SimpleTestPWM_FS(PWMPortEnum.PWM1_A);