呼吸燈 PWM LED

      在〈呼吸燈 PWM LED〉中尚無留言

原由

Raspberry的GPIO不是輸出0V 就是輸出3.3V. 假設在GPIO上接了一個Led, 想要讓這個Led有著如呼吸燈的功能, 由亮到暗, 再由暗到亮呢? 控制明亮度需調整電壓值. 但GPIO是固定在0-3.3V, 那這樣不就辦不到了嗎, 除非加個可變電阻, 然後叫個人轉來轉去的.

PWM原理

Pulse-width modulation, 原本只是個天真的想法, 如果在一秒鐘之內, 前500ms點亮3.3V, 後500ms輸出0V, 那這一秒鐘之內不就平圴只有50%的亮度嗎? 嗯, 數學上平圴值是 50%的亮度沒錯. 但人的眼睛看到的, 是明暗的閃爍變化, 眼睛很痛, 頭也很暈.

那如果再加快呢, 1ms亮, 1ms暗 ? 這下真的暗下來了. 接下來是 1ms亮, 10ms暗呢? 那就更暗了.

Pi4j 軟体 PWM

下面的程式碼把GPIO 2 連接一個Led, 製作出呼吸燈的效果.

package softpwm;
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalInput;
import com.pi4j.io.gpio.GpioPinPwmOutput;
import com.pi4j.io.gpio.PinState;
import com.pi4j.io.gpio.RaspiPin;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;

public class SoftPWM {
    static boolean closeFlag=false;
    private static int PIN_NUMBER = 2;
    public static void main(String[] args) {
        GpioController gpio=GpioFactory.getInstance();
        GpioPinDigitalInput power=gpio.provisionDigitalInputPin(RaspiPin.GPIO_00);
        GpioPinPwmOutput pwm=gpio.provisionSoftPwmOutputPin(RaspiPin.GPIO_02);
        pwm.setPwmRange(100);
        Thread thread=new Thread(()->{
            while (!closeFlag){
                for (int i=1;i<=100;i++){ pwm.setPwm(i); 
                    try {Thread.sleep(5);} catch (InterruptedException ex) {} 
                } 
                for (int i=100;i>=1;i--){
                    pwm.setPwm(i);
                    try {Thread.sleep(5);} catch (InterruptedException ex) {}
                }                
            }
        });
        thread.start();
        power.addListener(new GpioPinListenerDigital(){
            @Override
            public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
                closeFlag=true;
                thread.interrupt();
            }
        });        
        while(!closeFlag){try {Thread.sleep(500);} catch (InterruptedException ex) {}}
        power.removeAllListeners();
        power.setShutdownOptions(true, PinState.LOW);
        pwm.setShutdownOptions(true, PinState.LOW);
        gpio.shutdown();
    }
}

 WiringPi SDK 軟体PWM

由上一篇的說明, wiringpi SDK 的效能比Pi4j的效能高, 所以說明一下如何使用wiringPi來撰寫.

下面的程式碼使用wiringpi SDK, SoftPwm.softPwmCreate(GPIO_PWM, 0, 100) 先設定GPIO 2要使用軟体PWM, 再使用SoftPwm.softPwmWrite(GPIO_PWM, i) 動態將值改變.

package breathled;
import com.pi4j.wiringpi.Gpio;
import com.pi4j.wiringpi.GpioInterrupt;
import com.pi4j.wiringpi.SoftPwm;
public class BreathLed {
    static boolean closeFlag=false;
    public static final int GPIO_POWER = 0;
    private static final int GPIO_PWM = 2;
    public static void main(String[] args) {
        GpioInterrupt.addListener((event)->{
            System.out.printf(String.format("PIN %s is in State %s",event.getPin(), event.getState()));
            switch(event.getPin()){
                case GPIO_POWER:
                    if(event.getState()){//按下按鈕時
                        closeFlag=true;
                        System.out.println("Game over");
                    }
                break;
            }
        });        
        
        if (Gpio.wiringPiSetup() == -1) {
            System.out.println(" ==>> GPIO SETUP FAILED");
            return;
        }
        Gpio.pinMode(0, Gpio.INPUT);
        GpioInterrupt.enablePinStateChangeCallback(GPIO_POWER);

        SoftPwm.softPwmCreate(GPIO_PWM, 0, 100);
        Thread t=new Thread(()->{
            while(!closeFlag){
                for (int i = 0; i <= 100; i++) { 
                    SoftPwm.softPwmWrite(GPIO_PWM, i); 
                    try {Thread.sleep(5);} catch (InterruptedException ex) {} 
                } 
                for (int i = 100; i >= 0; i--) {
                    SoftPwm.softPwmWrite(GPIO_PWM, i);
                    try {Thread.sleep(5);} catch (InterruptedException ex) {}
                }               
            }
        });
        t.start();
        while(!closeFlag)delay(500);
        GpioInterrupt.disablePinStateChangeCallback(GPIO_POWER);
    }
    public static void delay(int t){
        try {Thread.sleep(t);} catch (InterruptedException ex) {}
    }
}

硬体PWM腳位

所有的GPIO針腳都可以使用上面的程式碼進行軟体模擬PWM. 間歇性供電與斷電, 造成Led的明與暗, 再利用人類的視覺暫留, 創造出明暗的效果. 但請注意, 這種方式除了極耗CPU的資源外, CPU會被中斷去作其他的事情, 所以輸出的訊號波就會含有雜訊.

那樹莓派是否有支援硬体PWM呢? 有的, 而且樹莓派只有 GPIO 1 一支針腳支援硬体PWM, 並確保可以輸出乾淨的訊號波.

Arduino贏了

所以, 在PWM這方面, Arduino真的是徹底的贏了, 因為Arduino支援更多的硬体PWM, 這點請莓粉們要徹底的接受事實.

Pi4j 硬体PWM

請注意, 底下程式只能將Led接到GPIO 1的接腳

PWM有balanced 和 mark:space二種模式, 在Java中的參數為

balanced : PWM_MODE_BAL : 極平穩, 不閃爍
mark:space : PWM_MODE_MS : 閃爍極其嚴重

package pwmtest;
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalInput;
import com.pi4j.io.gpio.GpioPinPwmOutput;
import com.pi4j.io.gpio.PinState;
import com.pi4j.io.gpio.RaspiPin;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;
import com.pi4j.wiringpi.Gpio;
public class PwmTest {
    static boolean closeFlag=false;
    public static void main(String[] args) {
        GpioController gpio=GpioFactory.getInstance();
        GpioPinDigitalInput power=gpio.provisionDigitalInputPin(RaspiPin.GPIO_00);
        GpioPinPwmOutput pwm = gpio.provisionPwmOutputPin(RaspiPin.GPIO_01);
        Gpio.pwmSetMode(com.pi4j.wiringpi.Gpio.PWM_MODE_BAL);
        Gpio.pwmSetRange(1000);//設定範圍, 預設為1024
        Gpio.pwmSetClock(500);//設定頻率
        Thread thread=new Thread(()->{
            while(!closeFlag){
                for(int i=0;i<10;i++){ pwm.setPwm(i*100); 
                    try {Thread.sleep(50);} catch (InterruptedException ex) {} 
                } 
                for(int i=9;i>=0;i--){
                    pwm.setPwm(i*100);
                    try {Thread.sleep(50);} catch (InterruptedException ex) {}
                }
            }            
        });
        thread.start();
        power.addListener(new GpioPinListenerDigital(){
            @Override
            public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
                closeFlag=true;
                thread.interrupt();
            }
        }); 
        while(!closeFlag){
            try {Thread.sleep(500);} catch (InterruptedException ex) {}
        }
        power.removeAllListeners();
        pwm.setShutdownOptions(true, PinState.LOW);
        power.setShutdownOptions(true, PinState.LOW);
        gpio.shutdown();
    }
}

wiringpi 硬体PWM

package pwmtest;
import com.pi4j.wiringpi.Gpio;
import com.pi4j.wiringpi.GpioInterrupt;
public class PwmTest {
    static boolean closeFlag=false;
    public static void main(String[] args) {
        if (Gpio.wiringPiSetup() == -1) {
            System.out.println("GPIO SETUP FAILED");
            return;
        }
        Gpio.pinMode(1, Gpio.PWM_OUTPUT);
        Gpio.pwmSetMode(Gpio.PWM_MODE_BAL);
        Gpio.pwmSetRange(1000);
        Gpio.pwmSetClock(500);
        Thread thread=new Thread(()->{
            while(!closeFlag){
                for (int i=1;i<=10;i++){ 
                    Gpio.pwmWrite(1, i*100); 
                    try {Thread.sleep(50);} catch (InterruptedException ex) {} 
                } 
                for (int i=10;i>=1;i--){
                    Gpio.pwmWrite(1, i*100);
                    try {Thread.sleep(50);} catch (InterruptedException ex) {}
                }
            }
        });
        thread.start();
        Gpio.pinMode(0, Gpio.INPUT);
        GpioInterrupt.enablePinStateChangeCallback(0);
        GpioInterrupt.addListener((event)->{
            System.out.println("Raspberry Pi PIN [" + event.getPin() +"] is in STATE [" + event.getState() + "]");
            switch(event.getPin()){
                case 0:
                    closeFlag=true;
                    thread.interrupt();
                    break;
            }
        });
        while(!closeFlag){
            try {Thread.sleep(500);} catch (InterruptedException ex) {}
        }
        GpioInterrupt.disablePinStateChangeCallback(0);
        Gpio.pwmWrite(1, 0);
    }
}

自製軟体PWM

底下的程式碼是完全使用Java撰寫軟体模擬PWM達成呼吸燈的效果, 這種方法的確比較麻煩, 效果也不好, 所以建議還是以上面Wiringpi SDK的方式撰寫.

為了不使CPU的資源消耗過大, 僅使用Thread.sleep()作毫秒級的間歇性供電與斷電. 所以還是會看到閃爍的現像.

若想解決閃爍的現像, 則必需使用while(nanoTime<duration){}空迴圈, 並使System.nanoTime() 計時. 但CPU的資源會消耗過大, 所以就不討論了.

請注意, 下述紅色部份, 二個for迴圈加起來共1秒, 就可以完成1秒呼吸1次的效果.

package breathled;
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalInput;
import com.pi4j.io.gpio.GpioPinDigitalOutput;
import com.pi4j.io.gpio.GpioPinOutput;
import com.pi4j.io.gpio.PinPullResistance;
import com.pi4j.io.gpio.PinState;
import com.pi4j.io.gpio.RaspiPin;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;
public class BreathLed {
    static boolean closeFlag=false;
    public static void main(String[] args) {
        GpioController gpio=GpioFactory.getInstance();
        GpioPinDigitalInput buttonPower=gpio.provisionDigitalInputPin(RaspiPin.GPIO_00, PinPullResistance.PULL_DOWN);
        buttonPower.addListener(new GpioPinListenerDigital(){
            @Override
            public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
                PinState state=event.getState();
                if(state.isHigh()){
                    closeFlag=true;
                }
            }
        });
        GpioPinDigitalOutput pinLed=gpio.provisionDigitalOutputPin(RaspiPin.GPIO_02, PinState.LOW);
        PwmLed led=new PwmLed(pinLed);
        Thread t=new Thread(()->{
            while(!closeFlag){
                for (int i=0;i<10;i++){ 
                    led.setPwm(i*2); 
                    delay(50); 
                } 
                for (int i=9;i>=0;i--){
                    led.setPwm(i*2);
                    delay(50);
                }                
            }
        });
        t.start();
        while(!closeFlag)delay(500);
        pinLed.setShutdownOptions(true, PinState.LOW);
        buttonPower.removeAllListeners();
        gpio.shutdown();
    }
    public static void delay(int t){
        try {Thread.sleep(t);} catch (InterruptedException ex) {}
    }
}
class PwmLed{
    boolean runFlag=true;
    Thread thread;
    GpioPinDigitalOutput pin;
    public PwmLed(GpioPinDigitalOutput pin){
        this.pin=pin;
    }
    public void setPwm(final int t){
        if(thread!=null){
            runFlag=false;
            thread.interrupt();
            try {Thread.sleep(1);} catch (InterruptedException ex) {}
            runFlag=true;
        }
        thread=new Thread(()->{
            while(runFlag){
                pin.setState(PinState.HIGH);
                try {Thread.sleep(1);} catch (InterruptedException ex) {}
                if(t!=0){
                    pin.setState(PinState.LOW);
                    try {Thread.sleep(t);} catch (InterruptedException ex) {}
                }
            }        
        });
        thread.start();
    }
    public void shutdown(){
        if(thread!=null){
            runFlag=false;
            thread.interrupt();
            pin.setShutdownOptions(true, PinState.LOW);
        }
    }
}

上述程式碼中,  這種只有毫秒間的變化, 有的人看不太出來閃爍的狀況. 但別忘了一件事, 人的眼睛是會隨著年紀而衰退的, 所以你看不到閃爍, 不代表就沒閃爍, 因不滿10歲的孩子, 看得可清楚了.

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *