实物演示视频请转向哔站
1. 起源
一直想做一个多种模式显示的灯阵控制小玩意作为床头灯, 这样每次一个人在乌漆嘛黑的卧室刷手机时能够给自己带来一丝暖意!!! 此外在大约5年前玩单片机的时候正好有过一阵子玩ardiuno, 想来ardiuno的性能足够实现对灯阵的控制要求了.
于是乎我在哔站找了几个视频对于床头灯的DIY方案, 本次借鉴的是利用ESP8266控制ws2812B闪烁模式, 实现手机联网操作的方案. 因为不需要联网只需要手动控制即可, 所以选择了性能跟接口都更好的ardiuno nanoV3, 对于手动控制的想法, 我是想做一个方块, 对于不同的摆放, 会有不同的闪烁模式, 比如呼吸灯, 流水灯, 亮度控制灯.
本项目的核心点是对多任务并行实现, 因为ardiuno本身没操作系统来实现多任务功能, 对此本项目利用millis()
来实现多任务轮训的功能. 这是由于每条指令的执行时间近乎不计, 系统时间当部分被delay()
函数给消耗掉了(delay
函数是独占系统时间), 因此本项目摒弃delay
函数, 通过不断读取millis()
, 计算时间段, 完成对应任务的延时.
ps: 可能有人会刚, 并行任务不能使用定时器来控制么? 但是我在实践过程中确实发现定时器对于定时扫描MPU6050并将结果显示到OLED上会失效, 至于原因, 希望刚的人能够解释😄.
1.1 DIY清单
硬件
- ardiuno nanoV3 (typeC接口带下载的模块)
- mpu6050 (陀螺仪, IIC接口, 作为系统输入)
- oled (IIC接口, 作为系统输出的屏幕)
- ws2812B (IIC接口, 作为系统输出的灯光)
软件
- Ardiuno IDE 2.0
- 软件库
//直接在库中搜索下载即可
#include <Adafruit_MPU6050.h> // mpu6050库
#include <Adafruit_SSD1306.h> // oled库
#include <Adafruit_Sensor.h> // 数据格式库
#include <Adafruit_NeoPixel.h> // WS2812B库
2 ardiuno基础玩法
2.1 串口数据打印以及key输入和led输出
void setup() {
//设置按键管脚上拉输入模式, 默认为高电平, 触发读取后是低电平
pinMode(PD2, INPUT_PULLUP);
// 初始化自带的LED灯珠引脚
pinMode(LED_BUILTIN, OUTPUT);
// 串口初始化
Serial.begin(115200);
}
bool led_flag = HIGH;
void loop() {
if(digitalRead(PD2) == LOW) {
led_flag = !led_flag;
Serial.print("串口打印数据: led_flag=");
Serial.println(led_flag);
}
digitalWrite(LED_BUILTIN, led_flag);
delay(500);
}
2.2 随机数
因为后面的炫彩模式, 需要用到随机闪烁
void setup() {
randomSeed(analogRead(0)); // 设置随机数种子, 下次随机会同样的随机数
}
void loop() {
int i = random(1, 20); // 随机1-20之间
}
2.3 非delay()
延时封装实现led闪烁任务
class Flasher {
int ledPin;
long OnTime; // 开的时间长度
long OffTime; // 关的时间长度
int ledState; // ledState used to set the LED
unsigned long previousMillis; // 开始时刻标记
public:
Flasher(int pin, long on, long off) {
ledPin = pin;
pinMode(ledPin, OUTPUT);
OnTime = on;
OffTime = off;
ledState = LOW;
previousMillis = 0;
}
void Update() { // 当时间长度满足要求, 执行命令
unsigned long currentMillis = millis();
if ((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
ledState = LOW; // Turn it off
previousMillis = currentMillis; // Remember the time
digitalWrite(ledPin, ledState); // Update the actual LED
}
else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
ledState = HIGH; // turn it on
previousMillis = currentMillis; // Remember the time
digitalWrite(ledPin, ledState); // Update the actual LED
}
}
};
Flasher led1(LED_BUILTIN, 123, 400);
Flasher led2(D3, 350, 350); // D3和D4接个灯, 就能看到三个灯同时按照顺序进行闪烁了
Flasher led3(D4, 200, 222);
void setup() {
Serial.begin(115200);
}
void loop() {
led1.Update();
led2.Update();
led3.Update();
}
2.4 MPU6050通过oled的显示demo
#include <Adafruit_MPU6050.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
Adafruit_MPU6050 mpu;
Adafruit_SSD1306 display = Adafruit_SSD1306(128, 32, &Wire);
void setup() {
Serial.begin(115200);
// while (!Serial);
Serial.println("MPU6050 OLED demo");
if (!mpu.begin()) {
Serial.println("Sensor init failed");
while (1)
yield();
}
Serial.println("Found a MPU-6050 sensor");
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
Serial.println(F("SSD1306 allocation failed"));
for (;;)
; // Don't proceed, loop forever
}
display.display();
delay(500); // Pause for 2 seconds
display.setTextSize(1);
display.setTextColor(WHITE);
display.setRotation(0);
}
void loop() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
display.clearDisplay();
display.setCursor(0, 0);
Serial.print("Accelerometer ");
Serial.print("X: ");
Serial.print(a.acceleration.x, 1);
Serial.print(" m/s^2, ");
Serial.print("Y: ");
Serial.print(a.acceleration.y, 1);
Serial.print(" m/s^2, ");
Serial.print("Z: ");
Serial.print(a.acceleration.z, 1);
Serial.println(" m/s^2");
display.println("Accelerometer - m/s^2");
display.print(a.acceleration.x, 1);
display.print(", ");
display.print(a.acceleration.y, 1);
display.print(", ");
display.print(a.acceleration.z, 1);
display.println("");
Serial.print("Gyroscope ");
Serial.print("X: ");
Serial.print(g.gyro.x, 1);
Serial.print(" rps, ");
Serial.print("Y: ");
Serial.print(g.gyro.y, 1);
Serial.print(" rps, ");
Serial.print("Z: ");
Serial.print(g.gyro.z, 1);
Serial.println(" rps");
display.println("Gyroscope - rps");
display.print(g.gyro.x, 1);
display.print(", ");
display.print(g.gyro.y, 1);
display.print(", ");
display.print(g.gyro.z, 1);
display.println("");
display.display();
delay(100);
}
2.5 ws2812Bdemo
#include "Adafruit_NeoPixel.h" //直接在库中搜索 大约第四个就是
#define PIN A0 // 定义的引脚图
#define NUMWS2812B 16
Adafruit_NeoPixel WS2812B(NUMWS2812B, PIN, NEO_GRB + NEO_KHZ800); //(灯总数,使用引脚,WS2812B一般都是800这个参数不用动)
/* 呼吸灯曲线表 呼吸函数中取得离散点 */
const uint16_t index_wave[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4,
4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12,
13, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 22, 23, 24, 25, 25, 26, 27, 28, 30, 31, 32, 33,
34, 36, 37, 38, 40, 41, 43, 45, 46, 48, 50, 52, 54, 56, 58, 60, 62, 65, 67, 70, 72, 75, 78, 81, 84, 87, 90,
94, 97, 101, 105, 109, 113, 117, 122, 126, 131, 136, 141, 146, 152, 158, 164, 170, 176, 183, 190, 197, 205,
213, 221, 229, 238, 247, 254, 254, 247, 238, 229, 221, 213, 205, 197, 190, 183, 176, 170, 164, 158, 152, 146,
141, 136, 131, 126, 122, 117, 113, 109, 105, 101, 97, 94, 90, 87, 84, 81, 78, 75, 72, 70, 67, 65, 62, 60, 58,
56, 54, 52, 50, 48, 46, 45, 43, 41, 40, 38, 37, 36, 34, 33, 32, 31, 30, 28, 27, 26, 25, 25, 24, 23, 22, 21, 20,
20, 19, 18, 18, 17, 16, 16, 15, 15, 14, 14, 13, 13, 12, 12, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 7, 6,
6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
void setup() {
WS2812B.begin(); // 初始化
WS2812B.clear(); // 将所有像素初始化为关闭
// WS2812B.setBrightness(255);// 设置亮度
WS2812B.setBrightness(145);// 设置亮度
}
void loop() {
colorful();
breathing();
}
/********************
设置灯带中某一个灯的颜色。
单灯颜色(灯位置,红,绿,蓝)
********************/
void Dan_Deng_Yan_Se(int d, int R, int G, int B) {
WS2812B.setPixelColor(d - 1, (((R & 0xffffff) << 16) | ((G & 0xffffff) << 8) | B));
WS2812B.show();
delay(10);
}
void breathing(){//呼吸灯
for(int i=0;i<300;i++) {
WS2812B.setBrightness(index_wave[i]);
WS2812B.show();
delay(10);
}
}
void colorful() //炫彩光效
{
int lumm = 40;
WS2812B.setBrightness(lumm);
for(int i=0;i<=NUMWS2812B;i++){
WS2812B.setPixelColor(i+1, WS2812B.Color(255,0,0)); //红
WS2812B.setPixelColor(i+3, WS2812B.Color(255,255,0)); //黄
WS2812B.setPixelColor(i+5, WS2812B.Color(0,255,0)); //绿
WS2812B.setPixelColor(i+7, WS2812B.Color(0,0,255)); //蓝
WS2812B.setPixelColor(i+9, WS2812B.Color(0,255,255)); //青
WS2812B.setPixelColor(i+11, WS2812B.Color(255,0,255)); //紫
WS2812B.show();
delay(500);
}
}
3. ardiuno读取MPU6050进行oled显示和控制ws2812B灯阵模式显示
3.1 引脚接线图
3.2 任务流程图
3.3 最终的代码
/* 汇总:
引脚
1. A4A5接oled和mpu6050的硬IIC
2. A0接 彩灯ws2812B 16矩阵灯珠的in
任务:
1. led1: nano自带led的闪烁, 表示程序运行正常
2. task1: 不断读取更新mpu6050数据, 通过oled显示
3. task2: 不断读取mpu6050数据, 根据数据变化选择不同的灯光模式
4. Flasher_ws2812B: 设置灯光模式
*/
#include <Adafruit_MPU6050.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_NeoPixel.h" //直接在库中搜索 大约第四个就是
#define PIN_WS2812B A0 // 这里如果是
#define NUMWS2812B 16
Adafruit_NeoPixel WS2812B(NUMWS2812B, PIN_WS2812B, NEO_GRB + NEO_KHZ800); //(灯总数,使用引脚,WS2812B一般都是800这个参数不用动)
Adafruit_MPU6050 mpu;
Adafruit_SSD1306 display = Adafruit_SSD1306(128, 32, &Wire);
class Flasher_led {
// Class Member Variables
// These are initialized at startup
int ledPin;
long OnTime;
long OffTime; // the number of the LED pin
// milliseconds of on-time
// milliseconds of off-time
// These maintain the current state
int ledState; // ledState used to set the LED
unsigned long previousMillis; // will store last time LED was updated
// Constructor - creates a Flasher
// and initializes the member variables and state
public:
Flasher_led(int pin, long on, long off) {
ledPin = pin;
pinMode(ledPin, OUTPUT);
OnTime = on;
OffTime = off;
ledState = LOW;
previousMillis = 0;
}
void Update() {
// check to see if it's time to change the state of the LED
unsigned long currentMillis = millis();
if ((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
ledState = LOW; // Turn it off
previousMillis = currentMillis; // Remember the time
digitalWrite(ledPin, ledState); // Update the actual LED
}
else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
ledState = HIGH; // turn it on
previousMillis = currentMillis; // Remember the time
digitalWrite(ledPin, ledState); // Update the actual LED
}
}
};
Flasher_led led1(LED_BUILTIN, 123, 400);
sensors_event_t a, g, temp;
void display_mpu6050_info() {
mpu.getEvent(&a, &g, &temp);
Serial.print("Temperature: ");
Serial.print(temp.temperature);
Serial.println(" degC");
display.clearDisplay();
display.setCursor(0, 0);
Serial.print("Accelerometer ");
Serial.print("X: ");
Serial.print(a.acceleration.x, 1);
Serial.print(" m/s^2, ");
Serial.print("Y: ");
Serial.print(a.acceleration.y, 1);
Serial.print(" m/s^2, ");
Serial.print("Z: ");
Serial.print(a.acceleration.z, 1);
Serial.println(" m/s^2");
display.println("Accelerometer - m/s^2");
display.print(a.acceleration.x, 1);
display.print(", ");
display.print(a.acceleration.y, 1);
display.print(", ");
display.print(a.acceleration.z, 1);
display.println("");
Serial.print("Gyroscope ");
Serial.print("X: ");
Serial.print(g.gyro.x, 1);
Serial.print(" rps, ");
Serial.print("Y: ");
Serial.print(g.gyro.y, 1);
Serial.print(" rps, ");
Serial.print("Z: ");
Serial.print(g.gyro.z, 1);
Serial.println(" rps");
display.println("Gyroscope - rps: temp");
display.print(g.gyro.x, 1);
display.print(", ");
display.print(g.gyro.y, 1);
display.print(", ");
display.print(g.gyro.z, 1);
display.print("; ");
display.print(temp.temperature, 1);
display.println("");
display.display();
}
// 每个任务的轮询延长时间, 比如让oled和mpu6050不停轮询更新数据
class Flasher_task {
long OnTime;
unsigned long previousMillis; // will store last time LED was updated
void (*func)();
public:
Flasher_task(void (*task)(), long on) {
OnTime = on;
previousMillis = 0;
func = task;
}
void Update() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= OnTime) {
previousMillis = currentMillis; // Remember the time
func();
}
}
};
Flasher_task task1(display_mpu6050_info,150); // 让oled和mpu6050不停轮询更新数据
class Flasher_ws2812B {
unsigned char brightness; // 亮度
unsigned long previousMillis_colorful;
unsigned long previousMillis_breathing;
unsigned int delay_color; // 炫彩灯延时时间
unsigned int delay_breath; // 呼吸灯延时时间
int breath_i=2; // 不然1-(-5) = 655555了就
bool breath_flag=true;
public:
Flasher_ws2812B(unsigned int delay_color=100, // 炫光延时
unsigned int delay_breath=10, // 呼吸灯延时
unsigned char brightness=20) { // 默认灯亮度
this->brightness = brightness;
this->delay_color = delay_color;
this->delay_breath = delay_breath;
this->previousMillis_breathing = 0;
this->previousMillis_colorful = 0;
}
static void init() {
WS2812B.begin(); // 初始化
WS2812B.clear(); // 将所有像素初始化为关闭
WS2812B.setBrightness(145);// 设置亮度
}
// 设置某一个灯的 RGB 红绿蓝
void set_one_led_color(int d, int R, int G, int B) {
WS2812B.setPixelColor(d - 1, (((R & 0xffffff) << 16) | ((G & 0xffffff) << 8) | B));
WS2812B.show();
}
void breathing() { //呼吸灯
unsigned long currentMillis = millis();
if (currentMillis - this->previousMillis_breathing >= delay_breath) {
this->previousMillis_breathing = currentMillis;
WS2812B.setBrightness(breath_i);// 设置亮度
WS2812B.show();
Serial.print(breath_i);
Serial.print("---");
Serial.println(breath_flag);
if(breath_flag) {
breath_i+=3;
if(breath_i >100) breath_i+=20;
if(breath_i >=255) {
breath_i = 254;
breath_flag=false;
}
}else{
breath_i-=3;
if(breath_i >100) breath_i-=20;
if(breath_i <2) {
breath_i = 2;
breath_flag=true;
}
}
}
}
void colorful_display() {
unsigned long currentMillis = millis();
if (currentMillis - this->previousMillis_colorful >= delay_color) {
this->previousMillis_colorful = currentMillis;
this->colorful();
}
}
// http://t.csdn.cn/3SXbb
void colorful() { //炫彩光效
int i = random(1, 20)%NUMWS2812B;
//红
WS2812B.setPixelColor(i , WS2812B.Color(255,0,0));
WS2812B.setPixelColor(i+1, WS2812B.Color(255,0,0));
//黄
WS2812B.setPixelColor(i+2, WS2812B.Color(255,255,0));
WS2812B.setPixelColor(i+3, WS2812B.Color(255,255,0));
//绿
WS2812B.setPixelColor(i+4, WS2812B.Color(0,255,0));
WS2812B.setPixelColor(i+5, WS2812B.Color(0,255,0));
//蓝
WS2812B.setPixelColor(i+6, WS2812B.Color(0,0,255));
WS2812B.setPixelColor(i+7, WS2812B.Color(0,0,255));
//青
WS2812B.setPixelColor(i+8, WS2812B.Color(0,255,255));
WS2812B.setPixelColor(i+9, WS2812B.Color(0,255,255));
//紫
WS2812B.setPixelColor(i+10, WS2812B.Color(255,0,255));
WS2812B.setPixelColor(i+11, WS2812B.Color(255,0,255));
//紫
WS2812B.setPixelColor(i-1, WS2812B.Color(255,0,255));
WS2812B.setPixelColor(i-2, WS2812B.Color(255,0,255));
//青
WS2812B.setPixelColor(i-3, WS2812B.Color(0,255,255));
WS2812B.setPixelColor(i-4, WS2812B.Color(0,255,255));
//蓝
WS2812B.setPixelColor(i-5, WS2812B.Color(0,0,255));
WS2812B.setPixelColor(i-6, WS2812B.Color(0,0,255));
//绿
WS2812B.setPixelColor(i-7, WS2812B.Color(0,255,0));
WS2812B.setPixelColor(i-8, WS2812B.Color(0,255,0));
//黄
WS2812B.setPixelColor(i-9, WS2812B.Color(255,255,0));
WS2812B.setPixelColor(i-10, WS2812B.Color(255,255,0));
//红
WS2812B.setPixelColor(i-11, WS2812B.Color(255,0,0));
//刷新颜色
WS2812B.show();
}
// 设置所有的灯光, 默认设置为所有的灯光为白色
void set_all_color(int R=255, int G=255, int B=255) {
for(int i=0; i<NUMWS2812B; i++) {
WS2812B.setPixelColor(i, (((R & 0xffffff) << 16) | ((G & 0xffffff) << 8) | B));
}
WS2812B.show();
}
};
Flasher_ws2812B ws2812B(500,15); // 这里其实只是设置了炫光跟呼吸灯的延时时间, 任务是由另一个task2进行调度的
void set_brightness() {
// 根据x轴变化改变亮度
int b = a.acceleration.x*10;
if(b<=0) b=10;
WS2812B.setBrightness(b);// 设置亮度
}
// 根据x和y的反应, 显示不同的灯光效应
void select_model() {
// 根据y轴变化改变闪烁模式
if(a.acceleration.y <= 3.3) {
set_brightness();
ws2812B.colorful_display(); // colorful_display默认就是炫彩+x轴翻转变亮度
}else if(a.acceleration.y <=6.6) {
ws2812B.breathing();
}else {
set_brightness();
ws2812B.set_all_color(); // 单纯设置为白色灯光, 亮度未变
}
}
Flasher_task task2(select_model,200); // 让oled和mpu6050不停轮询更新数据
void setup() {
pinMode(PD2, INPUT_PULLUP); //设置按键管脚上拉输入模式, 默认为高电平, 触发读取后是低电平
randomSeed(analogRead(0)); // 设置随机数
Serial.begin(115200);
if (!mpu.begin()) {
Serial.println("Sensor init failed");
while (1)
yield();
}
Serial.println("Found a MPU-6050 sensor");
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
Serial.println(F("SSD1306 allocation failed"));
for (;;)
; // Don't proceed, loop forever
}
display.display();
delay(500); // Pause for 2 seconds
display.setTextSize(1);
display.setTextColor(WHITE);
display.setRotation(0);
Flasher_ws2812B::init();
}
void loop() {
led1.Update();
task1.Update();
task2.Update();
}
// 1. 通过x轴翻转控制亮度: 随着翻转角度的增加亮度发生变化
// 2. 通过y轴翻转空灯光模式: 随着翻转角度的模式有变化: 模式1: 纯白, 模式2:炫彩, 模式3 呼吸灯
4. 总结
目前只有上述三种闪光模式(炫彩/呼吸/纯白), 有好想法可以在下方留言
作为并行多任务的一种基础思想, 希望能对读者有所帮助, 感谢阅读!