在上一篇教程中,我们踩到一个坑,因为OLED的内容显示拖慢了节拍器的速度。
今天我们使用多线程任务来解决这个问题,把OLED的显示和节拍器计时发声分成2个线程,分别独立运行互不干扰。
什么叫多线程?那就先说什么是单线程。把代码都写到loop()函数里,顺序执行,后面的代码必须等前面的代码执行完毕才可以运行,包括delay(),后面的代码也要等着,这就是单线程。
多线程就是把2个或者更多的代码分别独立拿出去运行,彼此不用等待。
Arduino里执行多线程的方法有很多,今天就使用一个封装好的类库来轻松的实现多线程。
这个类库叫做SCoop, 代码从Github下载:https://github.com/fabriceo/SCoop
接下来,先搭建个简单的电路。
一个电位器,A0口接收它的电压值; 9号引脚和2号引脚pinMode设为OUTPUT,分别控制LED1和LED2的闪烁。
下面写一段代码,来实现LED1和LED2分别以不同的速率闪烁,同时,旋转电位器还可以调整LED1的闪烁速度,而LED2并不受影响。
#include <SCoop.h>//引入头文件
int speedTask1 = 0;
defineTask(Task1);//定义线程1
defineTask(Task2);//定义线程2
//Task1线程自己的的setup和loop
void Task1::setup()
{
pinMode(9, OUTPUT);
}
void Task1::loop()
{
digitalWrite(9, HIGH);
//要注意,线程里应使用sleep()做延迟,这样只是该线程延迟不会影响其它。
//如果使用delay()那么全局都会被阻塞延迟。
sleep(speedTask1);
digitalWrite(9, LOW);
sleep(speedTask1); //要用sleep()重要的事情说2遍
}
//Task2线程自己的的setup和loop
void Task2::setup()
{
pinMode(2, OUTPUT);
}
void Task2::loop()
{
digitalWrite(2, HIGH);
sleep(1000); //要用sleep()重要的事情说3遍
digitalWrite(2, LOW);
sleep(1000);
}
void setup() {
//执行多线程的setup(),此时线程并没有开始运行哦,
//别看它的方法叫start(),其实叫setup或者ready更合适。
mySCoop.start();
}
void loop()
{
//从电位器读取值,speedTask1影响task1中LED的闪烁速度
speedTask1 = analogRead(A0);
//多线程的loop()开始运行
yield();
}
怎么样,是不是感觉到多线程带来的好处了? 下面我们修改上一篇教程的代码,给节拍器也加上多线程。
我把改进版的代码贴在下面,代码都有注释。
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SCoop.h>
//define---------------------------------------------------
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
#define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define BPM_MIN 20
#define BPM_MAX 240
#define PIN_BPM A0
#define PIN_BEEP 9
//end define---------------------------------------------------
//---------------------------------------------------
int bpm = 0;
char strBPM[3];
defineTask(Task1);//定义线程1
defineTask(Task2);//定义线程2
//Task1线程自己的的setup和loop
void Task1::setup()
{
}
void Task1::loop()
{
bpm = map(analogRead(A0), 0, 1023, BPM_MIN, BPM_MAX);
itoa(bpm, strBPM, 10); //把数字转成字符串
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(0, 10); //显示的坐标位置
display.println(F("BPM: "));
display.setTextSize(3);
display.setCursor(60, 10); //显示的坐标位置
display.println(strBPM);
display.display(); // Show text
//稍作停顿,按理说这里不用停顿
//不过好像是这款OLED驱动代码的问题
//不停顿会有异常,下次换块OLED试试看
sleep(100);
}
//Task2线程自己的的setup和loop
void Task2::setup()
{
}
void Task2::loop()
{
//BPM即每分钟有多少拍,那么一分钟(60000毫秒)除以BPM就是每拍的时间
//再除以4,就是四分之一拍的时间长度
float tick = 60000 / bpm /4;
tone(PIN_BEEP, 440); //播放440Hz的声音
sleep(tick); //播放时为四分之一拍
noTone(PIN_BEEP); //停止播放声音
sleep(tick*3); //空四分之三拍的时间
}
//---------------------------------------------------
void setup() {
Serial.begin(9600);
// 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
}
// Clear the buffer 这里如果不清理buffer,默认的显示内容为Adafruit类库的LOGO
display.clearDisplay();
pinMode(PIN_BPM, INPUT);
pinMode(PIN_BEEP, OUTPUT);
delay(1000);
mySCoop.start();
}
void loop() {
yield();
}
测试结果,节拍信号速度正常准确。好了,第二版的节拍器到这里就做好了。
改进内容:
- 使用多线程,将OLED内容显示和节拍器发音的工作分别分为2个独立线程运行。
- 修正了节拍速度不准确的问题。
现在节拍器速度正常,旋转调节电位器时候响应速度也快了。
又到了挖坑时间,现在提出更多的需求。
- 目前节拍器不能表现出节奏型,比如当前的拍子是3拍的还是4拍的?如何切换节奏型?
- 每小节第一拍的声音要有所区别。
- 节拍器没有开始和停止功能。
- BPM的值会因为电位器而抖动,即便没有拧动电位器。
喜欢自己动脑动手的小童鞋可以先自己试试去实现上述功能。下一篇我来讲讲这些需求如何实现,就先讲到这里啦 ヾ( ̄▽ ̄)ByeBye