RaspberryPiZeroを使ってソフトウェアPWMで三色LEDを光らせる

# はじめに
[PCを好きな色に光らせる](https://qiita.com/milmilk/items/2b94434fa8bb57953fc7)ために書いた記事です．**RGBの三色LED**を好きな色に光らせるプログラムを作ってみました。**RaspberryPiZero**にあるGPIOピンを使ってLEDを光らせます。**ソフトウェアでPWM制御**することで色の強弱を表現します。ソースコードは最後にまとめて載せます。

# 環境
Raspberry Pi Zero WH
Raspbian 10.0

# 説明

### アルゴリズム
PWMの1周期で、最初に赤、緑、青すべてのLEDを点灯させ、RGB値に応じて適切なタイミングで消灯させます。
まず、RGB値を小さい順に並べます。次に、RGB値の小さいものから順に消灯するために、消灯する時間間隔を計算します。

LEDをON,OFFするスレッドを作り、RGB値の並べ替えや時間間隔を計算するのとは別にします。

### ファイル
`ledmain.c`に点灯させる色の流れを記述しておきます。`ledmain.c`から`ledgpio.c`を呼び出してGPIOを制御します。`ledgpio.c`では**pigpio**を使います。

最初に`pinInitialise()`を呼び、最後に`pinTerminate()`を呼びます。`colorLed()`または`graduateLed()`で色と点灯時間を指定してLEDを光らせます。

# 実行方法

1. 下記の`ledgpio.c`、`ledgpio.h`、`ledmain.c`を同じディレクトリに入れます。

2. `ledmain.c`を適当に書き換えます。

3. `$ gcc -O3 -o ledmain -lpigpio -lpthread ledgpio.c ledmain.c`で**コンパイル**します。`ledmain`という実行ファイルができます。

4. `$ sudo ./ledmain`で**実行**します。


# ソースコード

```c:ledmain.c
#include <pigpio.h>
#include "ledgpio.h"

int main(void){
	int i;
	int pin[3] = {17, 18, 19}; // 赤: GPIO17ピン, 緑: GPIO18ピン, 青: GPIO19ピン
	pinInitialise(pin);

	for(i=0; i<100; i++){
		colorLed(1, 0, 0, 1000000, pin); // 赤、1000000マイクロ秒
		colorLed(0, 1, 0, 1000000, pin); // 緑、1000000マイクロ秒
		colorLed(0, 0, 1, 1000000, pin); // 青、1000000マイクロ秒
		graduateLed(0, 0, 1, 1, 0, 1, 2000000, pin); // 青→マゼンタ、2000000マイクロ秒
		graduateLed(1, 0, 1, 1, 0, 0, 2000000, pin); // マゼンタ→赤、2000000マイクロ秒
		graduateLed(1, 0, 0, 1, 1, 0, 2000000, pin); // 赤→黄、2000000マイクロ秒
		graduateLed(1, 1, 0, 0, 1, 0, 2000000, pin); // 黄→緑、2000000マイクロ秒
		graduateLed(0, 1, 0, 0, 1, 1, 2000000, pin); // 緑→シアン、2000000マイクロ秒
		graduateLed(0, 1, 1, 1, 1, 1, 2000000, pin); // シアン→白、2000000マイクロ秒
		colorLed(1, 1, 1, 1000000, pin);  // 白、1000000マイクロ秒
	}

	pinTerminate(pin);
	return 0;
}

```

```c:ledgpio.h
#ifndef _LEDGPIO_H
#define _LEDGPIO_H

void pinInitialise(int pin[3]);
void pinTerminate(int pin[3]);
void colorLed(double red, double green, double blue, long time_usec, int pin[3]);
void graduateLed(double red1, double green1, double blue1,
                double red2, double green2, double blue2, long time_usec, int pin[3]);

#endif

```

```c:ledgpio.c
#include <pigpio.h>
#include <unistd.h>
#include <stdio.h>
#include <time.h>
#include <pthread.h>

long period_usec = 5000; // PWMの周期

// スレッドに値を渡すための変数
pthread_t thread;
volatile int loop_on, renew, pin0, pin1, pin2;
volatile useconds_t time0, time1, time2, time3;

/**
 * @fn
 * LEDをON,OFFさせるためのスレッド
 */
void *loop_thread(){
	int pin0L, pin1L, pin2L;
	useconds_t time0L, time1L, time2L, time3L;
	while(loop_on){
		if(renew){ // 色を更新する。
			renew = -1;
			pin0L = pin0;
			pin1L = pin1;
			pin2L = pin2;
			time0L = time0;
			time1L = time1;
			time2L = time2;
			time3L = time3;
			renew = 0;
		}
		gpioWrite(pin0L, PI_ON);
		gpioWrite(pin1L, PI_ON);
		gpioWrite(pin2L, PI_ON);
		usleep(time0L);
		gpioWrite(pin0L, PI_OFF);
		usleep(time1L);
		gpioWrite(pin1L, PI_OFF);
		usleep(time2L);
		gpioWrite(pin2L, PI_OFF);
		usleep(time3L);
	}
}

/**
 * @fn
 * 初期化する関数。最初に呼ぶ。
 * @param (pin) GPIOのピン番号
 */
void pinInitialise(int pin[3]){
	if(gpioInitialise() < 0) return;
	gpioSetMode(pin[0], PI_OUTPUT);
	gpioSetMode(pin[1], PI_OUTPUT);
	gpioSetMode(pin[2], PI_OUTPUT);
	gpioWrite(pin[0], PI_OFF);
	gpioWrite(pin[1], PI_OFF);
	gpioWrite(pin[2], PI_OFF);
	loop_on = 1;
	pin0 = pin[0];
	pin1 = pin[1];
	pin2 = pin[2];
	time0 = 0;
	time1 = 0;
	time2 = 0;
	time3 = period_usec;
	renew = 1;
	pthread_create(&thread, NULL, &loop_thread, NULL);
}

/**
 * @fn
 * 最後に呼ぶ。
 * @param (pin) GPIOのピン番号
 */
void pinTerminate(int pin[3]){
	loop_on = 0;
	pthread_join(thread, NULL);
	gpioWrite(pin[0], PI_OFF);
	gpioWrite(pin[1], PI_OFF);
	gpioWrite(pin[2], PI_OFF);
	gpioTerminate();
}

/**
 * @fn
 * 指定の色で光らせる。
 * @param (red) RGBの赤色成分
 * @param (green) RGBの緑色成分
 * @param (blue) RGBの青色成分
 * @param (time_usec) 光らせる時間。マイクロ秒。
 * @param (pin) GPIOのピン番号
 */
void colorLed(double red, double green, double blue, long time_usec, int pin[3]){
	double tr, tg, tb;
	clock_t start = clock();
	tr = period_usec * red;
	tg = period_usec * green;
	tb = period_usec * blue;
	while(renew == -1);
	// RGB値の小さい順にソート
	if(tr < tg){
		if(tb < tr){
			time0 = tb;
			time1 = tr - tb;
			time2 = tg - tr;
			time3 = 1.0 - tg;
			pin0 = pin[2];
			pin1 = pin[0];
			pin2 = pin[1];
		}else if(tg < tb){
			time0 = tr;
			time1 = tg - tr;
			time2 = tb - tg;
			time3 = 1.0 - tb;
			pin0 = pin[0];
			pin1 = pin[1];
			pin2 = pin[2];
		}else{
			time0 = tr;
			time1 = tb - tr;
			time2 = tg - tb;
			time3 = 1.0 - tg;
			pin0 = pin[0];
			pin1 = pin[2];
			pin2 = pin[1];
		}
	}else{
		if(tr < tb){
			time0 = tg;
			time1 = tr - tg;
			time2 = tb - tr;
			time3 = 1.0 - tb;
			pin0 = pin[1];
			pin1 = pin[0];
			pin2 = pin[2];
		}else if(tg < tb){
			time0 = tg;
			time1 = tb - tg;
			time2 = tr - tb;
			time3 = 1.0 - tr;
			pin0 = pin[1];
			pin1 = pin[2];
			pin2 = pin[0];
		}else{
			time0 = tb;
			time1 = tg - tb;
			time2 = tr - tg;
			time3 = 1.0 - tr;
			pin0 = pin[2];
			pin1 = pin[1];
			pin2 = pin[0];
		}
	}
	renew = 1;
	usleep(time_usec);
}

/**
 * @fn
 * 色1から色2にだんだん変わるように光らせる。
 * @param (red1) 開始色の赤色成分
 * @param (green1) 開始色の緑色成分
 * @param (blue1) 開始色の青色成分
 * @param (red2) 終了色の赤色成分
 * @param (green2) 終了色の緑色成分
 * @param (blue2) 終了色の青色成分
 * @param (time_usec) 光らせる時間。マイクロ秒。
 * @param (pin) GPIOのピン番号
 */
void graduateLed(double red1, double green1, double blue1,
		double red2, double green2, double blue2, long time_usec, int pin[3]){
	long t, switchTime;
	int i, o, o0[4], o1[4], o2[4], oo0, oo1, oo2, p0, p1, p2;
	double ts[3], te[3], x, x01, x02, x12, xo[5], r_time_usec, aa0, aa1, aa2, aa3, bb0, bb1, bb2, bb3;
	clock_t start = clock();

	ts[0] = period_usec * red1;
	ts[1] = period_usec * green1;
	ts[2] = period_usec * blue1;
	te[0] = period_usec * red2;
	te[1] = period_usec * green2;
	te[2] = period_usec * blue2;

	// RGBを小さい順にソート
	if(ts[0] < ts[1]){
		if(ts[1] < ts[2]){
			o0[0] = 0;
			o1[0] = 1;
			o2[0] = 2;
		}else if(ts[0] < ts[2]){
			o0[0] = 0;
			o1[0] = 2;
			o2[0] = 1;
		}else{
			o0[0] = 2;
			o1[0] = 0;
			o2[0] = 1;
		}
	}else{
		if(ts[0] < ts[2]){
			o0[0] = 1;
			o1[0] = 0;
			o2[0] = 2;
		}else if(ts[1] < ts[2]){
			o0[0] = 1;
			o1[0] = 2;
			o2[0] = 0;
		}else{
			o0[0] = 2;
			o1[0] = 1;
			o2[0] = 0;
		}
	}
	if(ts[o0[0]] == ts[o2[0]] && te[o1[0]] > te[o2[0]]){
		o = o1[0];
		o1[0] = o2[0];
		o2[0] = o;
	}
	if(ts[o0[0]] == ts[o1[0]] && te[o0[0]] > te[o1[0]]){
		o = o0[0];
		o0[0] = o1[0];
		o1[0] = o;
	}
	if(ts[o1[0]] == ts[o2[0]] && te[o1[0]] > te[o2[0]]){
		o = o1[0];
		o1[0] = o2[0];
		o2[0] = o;
	}

	// RGBのグラフの交点を求める
	if((ts[o0[0]]>=ts[o1[0]]) != (te[o0[0]]>=te[o1[0]]) && ts[o0[0]] != ts[o1[0]]){
		x = ts[o1[0]] - ts[o0[0]];
		x01 = x / (x - te[o1[0]] + te[o0[0]]);
	}else{
		x01 = 1.0;
	}
	if((ts[o0[0]]>=ts[o2[0]]) != (te[o0[0]]>=te[o2[0]]) && ts[o0[0] != ts[o2[0]]]){
		x = ts[o2[0]] - ts[o0[0]];
		x02 = x / (x - te[o2[0]] + te[o0[0]]);
	}else{
		x02 = 1.0;
	}
	if((ts[o1[0]]>=ts[o2[0]]) != (te[o1[0]]>=te[o2[0]]) && ts[o1[0]] != ts[o2[0]]){
		x = ts[o2[0]] - ts[o1[0]];
		x12 = x / (x - te[o2[0]] + te[o1[0]]);
	}else{
		x12 = 1.0;
	}
	xo[0] = 0.0;
	if(x01 < x02){
		if(x02 < x12){
			xo[1] = x01;
			xo[2] = x02;
			xo[3] = x12;
			o0[1] = o1[0];
			o1[1] = o0[0];
			o2[1] = o2[0];
			o0[2] = o1[0];
			o1[2] = o2[0];
			o2[2] = o0[0];
			o0[3] = o2[0];
			o1[3] = o1[0];
			o2[3] = o0[0];
		}else if(x01 < x12){
			xo[1] = x01;
			xo[2] = x12;
			xo[3] = x02;
			o0[1] = o1[0];
			o1[1] = o0[0];
			o2[1] = o2[0];
			o0[2] = o2[0];
			o1[2] = o0[0];
			o2[2] = o1[0];
			o0[3] = o0[0];
			o1[3] = o2[0];
			o2[3] = o1[0];
		}else{
			xo[1] = x12;
			xo[2] = x01;
			xo[3] = x02;
			o0[1] = o0[0];
			o1[1] = o2[0];
			o2[1] = o1[0];
			o0[2] = o1[0];
			o1[2] = o2[0];
			o2[2] = o0[0];
			o0[3] = o1[0];
			o1[3] = o0[0];
			o2[3] = o2[0];
		}
	}else{
		if(x01 < x12){
			xo[1] = x02;
			xo[2] = x01;
			xo[3] = x12;
			o0[1] = o2[0];
			o1[1] = o1[0];
			o2[1] = o0[0];
			o0[2] = o2[0];
			o1[2] = o0[0];
			o2[2] = o1[0];
			o0[3] = o1[0];
			o1[3] = o0[0];
			o2[3] = o2[0];
		}else if(x02 < x12){
			xo[1] = x02;
			xo[2] = x12;
			xo[3] = x01;
			o0[1] = o2[0];
			o1[1] = o1[0];
			o2[1] = o0[0];
			o0[2] = o1[0];
			o1[2] = o2[0];
			o2[2] = o0[0];
			o0[3] = o0[0];
			o1[3] = o2[0];
			o2[3] = o1[0];
		}else{
			xo[1] = x12;
			xo[2] = x02;
			xo[3] = x01;
			o0[1] = o0[0];
			o1[1] = o2[0];
			o2[1] = o1[0];
			o0[2] = o2[0];
			o1[2] = o0[0];
			o2[2] = o1[0];
			o0[3] = o2[0];
			o1[3] = o1[0];
			o2[3] = o0[0];
		}
	}
	xo[3] = 1.0;

	// PWMで光らせる．
	r_time_usec = 10.0 / time_usec;
	for(i=0; xo[i]<1.0; i++){
		switchTime = (long)(time_usec/10 * xo[i+1]);
		oo0 = o0[i];
		oo1 = o1[i];
		oo2 = o2[i];
		bb0 = ts[oo0];
		bb1 = ts[oo1]     - ts[oo0];
		bb2 = ts[oo2]     - ts[oo1];
		bb3 = period_usec - ts[oo2];
		aa0 = (te[oo0]               - bb0) * r_time_usec;
		aa1 = (te[oo1]     - te[oo0] - bb1) * r_time_usec;
		aa2 = (te[oo2]     - te[oo1] - bb2) * r_time_usec;
		aa3 = (period_usec - te[oo2] - bb3) * r_time_usec;
		while(renew == -1);
		pin0 = pin[oo0];
		pin1 = pin[oo1];
		pin2 = pin[oo2];
		t = clock() - start;
		while(t <= switchTime){
			while(renew == -1);
			time0 = aa0 * t + bb0;
			time1 = aa1 * t + bb1;
			time2 = aa2 * t + bb2;
			time3 = aa3 * t + bb3;
			renew = 1;
			usleep(period_usec);
			t = clock() - start;
		}
	}

}

```

速度重視でプログラム組んだらソースコード長くなってしまいました（汗）

> 2019/10/16追記
> @fujitanozomu 様からコメントをいただき，グローバル変数をvolatile宣言しました．

# エラー対処
`initMboxBlock: init mbox zaps failed` というエラーが出ることがあります。`$ sudo reboot`でRaspberryPiZeroを再起動すればなおります。

# 参考

### PWM制御
[明るさを変化させてみよう（PWMでアナログ出力）](http://jkoba.net/prototyping/arduino/led_brightness.html)

### GPIO
[Raspberry Pi ZeroでLチカ - Developers Note](http://developersnote.jp/knowhow/raspberry-pi-zero-kawaii-yo.html)

### pigpio
[C言語でpigpio.hを使う（raspberry pi-pigpio_if2.h）](https://qiita.com/fujita06/items/ca265fd4aa8f9474d966)

[pigpioのCインターフェイスを使いたい デジタル入出力編](https://qrunch.net/@zennihon/entries/NIDLVgszJEXdCJGY)