原由
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歲的孩子, 看得可清楚了.