ナゾテックの名前で模型・ジオラマ用のLED電飾コントローラーや配線済みチップLEDの販売などを行っている者です。
想定読者
MicrochipのPICマイコンを使ってNeopixelLED(フルカラーシリアルLED)をコントロールして見たいなと言う方。
その中でも色指定をHSVを使って制御したいと言う方へ。
目次
経緯
Neopixelの光らせ方
HSVからRGBへの変換関数
変換関数を使って見る
グラデーションしてみる
もう少し応用
コード全体
経緯
Neopixelの色指定は基本的にRGB(Red Blue Green)の値をそれぞれ256段階で指定して光らせます。これにより理論的には約1700万色の色指定が可能になっています。
単純にRGBで指定でき場合はもちろん問題有りませんが、たとえば一つの色であっても徐々に明るく、暗くするなどの場合はRGB指定では計算が難しかったりしますし、グラデーション表現をしたい場合などはさらに難しくなります。
これをHSV(Hue Saturation Value)で指定すると簡単に出来るよな~って所からです。なおHSVは「色相(Hue)」「彩度(Saturation)」「明度(Value)」の事です。
前述したようにNeopixelの色指定はRGBで行います。HSVでこの指定を行いたい場合はHSVからRGBへの変換処理が必要になります。
当記事はこの変換処理をどのように書くのかが趣旨です。
NEOPIXELの光らせ方
NeopixelをPICマイコンで光らせる方法の基礎は下記記事を参照してください。
PICマイコンのCLCを使って超簡単にNeopixelLED(フルカラーシリアルLED)をコントロールする
HSVからRGBへの変換関数
毎度の事なんですが、正直何故そうするのか?などの理屈は分かっていませんので、コードを載せちゃいます。
変換関数としてHSVtoRGBを定義し呼び出す形式にします。
/*
RGBを返すための構造体。r,g,b共に0~255の範囲
*/
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
}RGB;
/*
hueは0~359。359を超えている場合は360で割った余りを使っています。
satは0~1の実数。負の値は0に、1を超える値は1に補正されます。
valは0~1の実数。負の値は0に、1を超える値は1に補正されます。
戻り値はRGB構造体。
*/
RGB HSVtoRGB(uint16_t hue, double sat, double val) {
RGB rgb;
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
val = val < 0 ? 0 : val;
val = val > 1 ? 1 : val;
if(sat <= 0){
rgb.r = (uint8_t)(val * 255);
rgb.g = rgb.r;
rgb.b = rgb.r;
return rgb;
}
sat = sat > 1 ? 1 : sat;
hue = hue % 360;
int hi = hue / 60;
double f = (double)hue / 60 - hi;
double m = val * (1.0 - sat);
double n = val * (1.0 - sat * f);
double k = val * (1.0 - sat * (1.0 - f));
double r, g, b;
switch(hi){
case 0:
r = val;
g = k;
b = m;
break;
case 1:
r = n;
g = val;
b = m;
break;
case 2:
r = m;
g = val;
b = k;
break;
case 3:
r = m;
g = n;
b = val;
break;
case 4:
r = k;
g = m;
b = val;
break;
case 5:
r = val;
g = m;
b = n;
break;
}
rgb.r = (uint8_t)(r * 255);
rgb.g = (uint8_t)(g * 255);
rgb.b = (uint8_t)(b * 255);
return rgb;
}
最初に置いた構造体はRGBを一括で返すための物です。
このコードをmain.cの最初の方に記述しても良いですし、ヘッダーファイルを関数のファイルに分けて独立したものとして置いても構いません。
変換関数を使って見る
では実際に使って見ましょう。
まずは徐々に明るく・暗くするコードです。
色、つまりHとSは何でもいいのですが、Vの明るさだけを0~1の間で徐々に変化させてNeopixelへ送り出します。
変化の度合いは1/100ずつ明るく・暗くするようにしています。
また、徐々に明るくして最大の明るさ=1.0になったら今度は徐々に暗くし最小の明るさ=0になったらまた明るくしていくと言う事を繰り返しています。
double v_add = 1.0/100.0; //100段階で明るさを変化させたいので増分値として1/100を用意する
double val = 0; //valの初期値
while (1)
{
RGB rgb = HSVtoRGB(120, 1, val); //Hue=120は緑。Sは1固定で明るさをvalで指定
for(int i=0;i<LEDNUM;i++){ //関数を使わずに直接Neopixelへデータを送るループ
SSP1BUF = rgb.g; //Green //全てのLEDが同じ色に光ります
while (!SSP1STATbits.BF);
SSP1BUF = rgb.r; //Red
while (!SSP1STATbits.BF);
SSP1BUF = rgb.b; //Blue
while (!SSP1STATbits.BF);
}
val += v_add; //明るさに増分値を加算
if(val < 0 || val > 1){ //明るさが最小または最大になったら
v_add *= -1; //増分値の+-を反転する
val += v_add;
}
DELAY_milliseconds(50); //ちょっと待つ
}
ここでは全部のLEDに同じRGBデータを送信するので配列は使っていません。またデータ送信も関数を使わずインラインに記述しています。
この段階のmain.c全体を「コード1」として掲載しています。
またこの様子の動画をYoutubeに上げています
グラデーションしてみる
では次はグラデーションをやって見ましょう。
ここでは赤から緑、黄色へとグラデーションして見ます。
HSVの場合赤はHに0、黄色は180を指定します。SとVは1.0のままでやります。
今回はLEDが60個なので、180段階を3段階づつカウントアップしながらグラデーションします。この段階が狭いほどグラデーションの度合いが緩やかに変化していきます。
このコードでは60個のRGBデータを一旦配列に入れて一気に送信します。
これはNeopixelへ各LEDのデータを送信するときに途切れなくすべてのデータを送信する必要があり、たとえばHSVからRGBへの変換コードのようなものでも途中で行うとタイミングがナノ秒単位でずれてしまい、正しく光らなくなってしまうためです。
RGB ledTable[LEDNUM];
なお、この宣言はmain関数の外で行ってください。main関数のローカル変数とするとサイズが大きすぎて(LEDの数にもよりますが)コンパイルエラーになってしまいます。
uint16_t start = 0;
uint16_t end = 180;
uint16_t delta = (end - start) / LEDNUM;
for(int i=0, h = start;i<LEDNUM;i++, h+=delta){ //関数を使わずに直接Neopixelへデータを送るループ
ledTable[i] = HSVtoRGB(h, 1, 1); //Hue=120は緑。Sは1固定で明るさをvalで指定
}
for(int i=0;i<LEDNUM;i++){ //関数を使わずに直接Neopixelへデータを送るループ
SSP1BUF = ledTable[i].g; //Green
while (!SSP1STATbits.BF);
SSP1BUF = ledTable[i].r; //Red
while (!SSP1STATbits.BF);
SSP1BUF = ledTable[i].b; //Blue
while (!SSP1STATbits.BF);
}
while (1)
{
}
この段階のmain.c全体を「コード2」として掲載しています。
もう少し応用
やはり動きが無いのは寂しいので全体がずれながらグラデーションしていく様子を表現してみます。
最初にHを0から360まで、つまり赤->黄->緑->水色->青->紫->赤と変化するように配列を設定します。ここではLEDが60しかないのでグラデーションは6ずつ変化します。
uint16_t start = 0;
uint16_t end = 360;
uint16_t delta = (end - start) / LEDNUM;
for(int i=0, h = start;i<LEDNUM;i++, h+=delta){ //予め全体のLEDデータを作成しておくループ
ledTable[i] = HSVtoRGB(h, 1, 1); //S,Vは固定の1
}
次に、LEDへ配列データを送信するときに、最初は配列の0番要素から、次は配列の1番要素、その次は2番要素となるように、徐々に送信するデータをずらしながら送ると、LED全体がグラデーションしながら移動するように見えます。
int ledPos = 0;
while (1)
{
int idx = ledPos; //送信し始める要素を決める。
for(int i=0;i<LEDNUM;i++){
SSP1BUF = ledTable[idx].g; //Green
while (!SSP1STATbits.BF);
SSP1BUF = ledTable[idx].r; //Red
while (!SSP1STATbits.BF);
SSP1BUF = ledTable[idx].b; //Blue
while (!SSP1STATbits.BF);
idx++;
if(idx>=LEDNUM){
idx=0;
}
}
DELAY_milliseconds(50); //少し待つ
ledPos++; //送信し始める要素を一つ進める
if(ledPos>=LEDNUM){ //配列数を超えたら0にもどる
ledPos = 0;
}
}
この段階のmain.c全体を「コード3」として掲載しています。
またこの様子の動画をYoutubeに上げています。開始から7秒くらいのところです。
コード全体
コード全体と言っても最初の"#include"が出てくるまでのコメント部分は削除しています。
またコード2、コード3が見えない場合は一旦コード1を開いてその後ろを見てください。
コード1
#include "mcc_generated_files/mcc.h"
#define LEDNUM 60
/*
RGBを返すための構造体。r,g,b共に0-255の範囲
*/
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
}RGB;
/*
hueは0-359。359を超えている場合は360で割った余りを使っています。
satは0-1の実数。負の値は0に、1を超える値は1に補正されます。
valは0-1の実数。負の値は0に、1を超える値は1に補正されます。
戻り値はRGB構造体。
*/
RGB HSVtoRGB(uint16_t hue, double sat, double val) {
RGB rgb;
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
val = val < 0 ? 0 : val;
val = val > 1 ? 1 : val;
if(sat <= 0){
rgb.r = (uint8_t)(val * 255);
rgb.g = rgb.r;
rgb.b = rgb.r;
return rgb;
}
sat = sat > 1 ? 1 : sat;
hue = hue % 360;
int hi = hue / 60;
double f = (double)hue / 60 - hi;
double m = val * (1.0 - sat);
double n = val * (1.0 - sat * f);
double k = val * (1.0 - sat * (1.0 - f));
double r, g, b;
switch(hi){
case 0:
r = val;
g = k;
b = m;
break;
case 1:
r = n;
g = val;
b = m;
break;
case 2:
r = m;
g = val;
b = k;
break;
case 3:
r = m;
g = n;
b = val;
break;
case 4:
r = k;
g = m;
b = val;
break;
case 5:
r = val;
g = m;
b = n;
break;
}
rgb.r = (uint8_t)(r * 255);
rgb.g = (uint8_t)(g * 255);
rgb.b = (uint8_t)(b * 255);
return rgb;
}
/*
Main application
*/
void main(void)
{
// initialize the device
SYSTEM_Initialize();
// When using interrupts, you need to set the Global and Peripheral Interrupt Enable bits
// Use the following macros to:
// Enable the Global Interrupts
//INTERRUPT_GlobalInterruptEnable();
// Enable the Peripheral Interrupts
//INTERRUPT_PeripheralInterruptEnable();
// Disable the Global Interrupts
//INTERRUPT_GlobalInterruptDisable();
// Disable the Peripheral Interrupts
//INTERRUPT_PeripheralInterruptDisable();
SPI1_Open(SPI1_DEFAULT);
for(int i=0;i<LEDNUM;i++){ //一旦LEDを消灯する。
SSP1BUF = 0; //Green
while (!SSP1STATbits.BF);
SSP1BUF = 0; //Red
while (!SSP1STATbits.BF);
SSP1BUF = 0; //Blue
while (!SSP1STATbits.BF);
}
double v_add = 1.0/100.0; //100段階で明るさを変化させたいので増分値として1/100を用意する
double val = 0; //valの初期値
while (1)
{
RGB rgb = HSVtoRGB(120, 1, val); //Hue=120は緑。Sは1固定で明るさをvalで指定
for(int i=0;i<LEDNUM;i++){ //関数を使わずに直接Neopixelへデータを送るループ
SSP1BUF = rgb.g; //Green //全てのLEDが同じ色に光ります
while (!SSP1STATbits.BF);
SSP1BUF = rgb.r; //Red
while (!SSP1STATbits.BF);
SSP1BUF = rgb.b; //Blue
while (!SSP1STATbits.BF);
}
val += v_add; //明るさに増分値を加算
if(val < 0 || val > 1){ //明るさが最小または最大になったら
v_add *= -1; //増分値の+-を反転する
val += v_add;
}
DELAY_milliseconds(10); //ちょっと待つ
}
}
/**
End of File
*/
========================================
コード2
#include "mcc_generated_files/mcc.h"
#define LEDNUM 60
/*
RGBを返すための構造体。r,g,b共に0-255の範囲
*/
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
}RGB;
/*
hueは0-359。359を超えている場合は360で割った余りを使っています。
satは0-1の実数。負の値は0に、1を超える値は1に補正されます。
valは0-1の実数。負の値は0に、1を超える値は1に補正されます。
戻り値はRGB構造体。
*/
RGB HSVtoRGB(uint16_t hue, double sat, double val) {
RGB rgb;
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
val = val < 0 ? 0 : val;
val = val > 1 ? 1 : val;
if(sat <= 0){
rgb.r = (uint8_t)(val * 255);
rgb.g = rgb.r;
rgb.b = rgb.r;
return rgb;
}
sat = sat > 1 ? 1 : sat;
hue = hue % 360;
int hi = hue / 60;
double f = (double)hue / 60 - hi;
double m = val * (1.0 - sat);
double n = val * (1.0 - sat * f);
double k = val * (1.0 - sat * (1.0 - f));
double r, g, b;
switch(hi){
case 0:
r = val;
g = k;
b = m;
break;
case 1:
r = n;
g = val;
b = m;
break;
case 2:
r = m;
g = val;
b = k;
break;
case 3:
r = m;
g = n;
b = val;
break;
case 4:
r = k;
g = m;
b = val;
break;
case 5:
r = val;
g = m;
b = n;
break;
}
rgb.r = (uint8_t)(r * 255);
rgb.g = (uint8_t)(g * 255);
rgb.b = (uint8_t)(b * 255);
return rgb;
}
RGB ledTable[LEDNUM];
/*
Main application
*/
void main(void)
{
// initialize the device
SYSTEM_Initialize();
// When using interrupts, you need to set the Global and Peripheral Interrupt Enable bits
// Use the following macros to:
// Enable the Global Interrupts
//INTERRUPT_GlobalInterruptEnable();
// Enable the Peripheral Interrupts
//INTERRUPT_PeripheralInterruptEnable();
// Disable the Global Interrupts
//INTERRUPT_GlobalInterruptDisable();
// Disable the Peripheral Interrupts
//INTERRUPT_PeripheralInterruptDisable();
SPI1_Open(SPI1_DEFAULT);
for(int i=0;i<LEDNUM;i++){ //一旦LEDを消灯する。
SSP1BUF = 0; //Green
while (!SSP1STATbits.BF);
SSP1BUF = 0; //Red
while (!SSP1STATbits.BF);
SSP1BUF = 0; //Blue
while (!SSP1STATbits.BF);
}
uint16_t start = 0;
uint16_t end = 180;
uint16_t delta = (end - start) / LEDNUM;
for(int i=0, h = start;i<LEDNUM;i++, h+=delta){ //予め全体のLEDデータを作成しておくループ
ledTable[i] = HSVtoRGB(h, 1, 1); //S,Vは固定の1
}
for(int i=0;i<LEDNUM;i++){ //関数を使わずに直接Neopixelへデータを送るループ
SSP1BUF = ledTable[i].g; //Green
while (!SSP1STATbits.BF);
SSP1BUF = ledTable[i].r; //Red
while (!SSP1STATbits.BF);
SSP1BUF = ledTable[i].b; //Blue
while (!SSP1STATbits.BF);
}
while (1)
{
}
}
/**
End of File
*
========================================
コード3
#include "mcc_generated_files/mcc.h"
#define LEDNUM 60
/*
RGBを返すための構造体。r,g,b共に0-255の範囲
*/
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
}RGB;
/*
hueは0-359。359を超えている場合は360で割った余りを使っています。
satは0-1の実数。負の値は0に、1を超える値は1に補正されます。
valは0-1の実数。負の値は0に、1を超える値は1に補正されます。
戻り値はRGB構造体。
*/
RGB HSVtoRGB(uint16_t hue, double sat, double val) {
RGB rgb;
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
val = val < 0 ? 0 : val;
val = val > 1 ? 1 : val;
if(sat <= 0){
rgb.r = (uint8_t)(val * 255);
rgb.g = rgb.r;
rgb.b = rgb.r;
return rgb;
}
sat = sat > 1 ? 1 : sat;
hue = hue % 360;
int hi = hue / 60;
double f = (double)hue / 60 - hi;
double m = val * (1.0 - sat);
double n = val * (1.0 - sat * f);
double k = val * (1.0 - sat * (1.0 - f));
double r, g, b;
switch(hi){
case 0:
r = val;
g = k;
b = m;
break;
case 1:
r = n;
g = val;
b = m;
break;
case 2:
r = m;
g = val;
b = k;
break;
case 3:
r = m;
g = n;
b = val;
break;
case 4:
r = k;
g = m;
b = val;
break;
case 5:
r = val;
g = m;
b = n;
break;
}
rgb.r = (uint8_t)(r * 255);
rgb.g = (uint8_t)(g * 255);
rgb.b = (uint8_t)(b * 255);
return rgb;
}
RGB ledTable[LEDNUM];
/*
Main application
*/
void main(void)
{
// initialize the device
SYSTEM_Initialize();
// When using interrupts, you need to set the Global and Peripheral Interrupt Enable bits
// Use the following macros to:
// Enable the Global Interrupts
//INTERRUPT_GlobalInterruptEnable();
// Enable the Peripheral Interrupts
//INTERRUPT_PeripheralInterruptEnable();
// Disable the Global Interrupts
//INTERRUPT_GlobalInterruptDisable();
// Disable the Peripheral Interrupts
//INTERRUPT_PeripheralInterruptDisable();
SPI1_Open(SPI1_DEFAULT);
for(int i=0;i<LEDNUM;i++){ //一旦LEDを消灯する。
SSP1BUF = 0; //Green
while (!SSP1STATbits.BF);
SSP1BUF = 0; //Red
while (!SSP1STATbits.BF);
SSP1BUF = 0; //Blue
while (!SSP1STATbits.BF);
}
uint16_t start = 0;
uint16_t end = 360;
uint16_t delta = (end - start) / LEDNUM;
for(int i=0, h = start;i<LEDNUM;i++, h+=delta){ //予め全体のLEDデータを作成
ledTable[i] = HSVtoRGB(h, 1, 1); //S,Vは固定の1
}
int ledPos = 0;
while (1)
{
int idx = ledPos; //送信し始める要素を決める。
for(int i=0;i<LEDNUM;i++){
SSP1BUF = ledTable[idx].g; //Green
while (!SSP1STATbits.BF);
SSP1BUF = ledTable[idx].r; //Red
while (!SSP1STATbits.BF);
SSP1BUF = ledTable[idx].b; //Blue
while (!SSP1STATbits.BF);
idx++;
if(idx>=LEDNUM){
idx=0;
}
}
DELAY_milliseconds(50); //少し待つ
ledPos++; //送信し始める要素を一つ進める
if(ledPos>=LEDNUM){ //配列数を超えたら0にもどる
ledPos = 0;
}
}
}
/**
End of File
*/```
</dev></details>
========================================

