源代码:https://download.eeworld.com.cn/detail/eew_dy9f48/633952
非常高兴可以参加follow me活动,这次活动我购买了一块 Adafruit Circuit Playground Express开发板和一块raspberry pi zero 2w开发板。
为了完成任务,我还自己准备了一个9g舵机和一个ADS1115模块。
开发环境我使用的是Arduino,因为在Arduino上有非常丰富的例程,其中就包括多app切换的例程,正好适合这次活动,每个任务写成一个独立的app,然后通过按键来在app之间进行切换。
首先我们要在arduino中下载对应的board和library:
接着连接上开发板,就可以开始编程了。程序是基于库中mega_demo这个例程修改得到的。
入门任务
开发环境在上面已经配置好,点灯十分简单,只要把下面这两行添加到setup函数中就可以了。
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
在完成其他任务前,先看一下主程序结构,看是如何实现多app切换的。然后我们再分别在各个app中完成后面的一系列任务。
下面是主程序的代码:
#include <Adafruit_CircuitPlayground.h>
#include <Wire.h>
#include <SPI.h>
#include "Adafruit_SleepyDog.h"
// Include all the demos, note that each demo is defined in a separate class to keep the sketch
// code below clean and simple.
#include "Demo.h"
#include "RainbowCycleDemo.h"
#include "VUMeterDemo.h"
#include "CapTouchDemo.h"
#include "TiltDemo.h"
#include "SensorDemo.h"
#include "ProximityDemo.h"
// Create an instance of each demo class.
RainbowCycleDemo rainbowCycleDemo;
VUMeterDemo vuMeterDemo;
CapTouchDemo capTouchDemo;
TiltDemo tiltDemo;
SensorDemo sensorDemo;
ProximityDemo proximityDemo;
// Make a list of all demo class instances and keep track of the currently selected one.
int currentDemo = 0;
Demo* demos[] = {
&rainbowCycleDemo,
&sensorDemo,
&proximityDemo,
&tiltDemo,
&capTouchDemo,
&vuMeterDemo
};
void setup() {
// Initialize serial port and circuit playground library.
Serial.begin(115200);
Serial.println("Circuit Playground MEGA Demo!");
CircuitPlayground.begin();
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
}
void loop() {
// Check if slide switch is on the left (false) and go to sleep.
while (!CircuitPlayground.slideSwitch()) {
// Turn off the pixels, then go into deep sleep for a second.
CircuitPlayground.clearPixels();
Watchdog.sleep(1000);
}
// Check for any button presses by checking their state twice with
// a delay inbetween. If the first press state is different from the
// second press state then something was pressed/released!
bool leftFirst = CircuitPlayground.leftButton();
bool rightFirst = CircuitPlayground.rightButton();
delay(10);
// Run current demo's main loop.
demos[currentDemo]->loop();
// Now check for buttons that were released.
bool leftSecond = CircuitPlayground.leftButton();
bool rightSecond = CircuitPlayground.rightButton();
// Left button will change the current demo.
if (leftFirst && !leftSecond) {
// Turn off all the pixels when entering new mode.
CircuitPlayground.clearPixels();
// Increment the current demo (looping back to zero if at end).
currentDemo += 1;
if (currentDemo >= (sizeof(demos)/sizeof(Demo*))) {
currentDemo = 0;
}
Serial.print("Changed to demo: "); Serial.println(currentDemo, DEC);
}
// Right button will change the mode of the current demo.
if (rightFirst && !rightSecond) {
demos[currentDemo]->modePress();
}
}
从以上代码中可以看到,我们可以把每个不同的任务写在一个单独的h文件中的class里面,然后把这些class在主程序中创建好实例,再把这些实例装入一个列表中。然后在主循环loop中读取按键的读数,如果按键被按一次,则将index加一,通过这个index来选择需要运行的app,然后进入app后运行app中的loop函数。看到这里就可以明白了,我们只需要在每个单独的app文件中定义一个class,在这个class里面定义一个构造函数,这就相当于是标准arduino的setup函数,只运行一次;另外再定义一个名为loop的成员函数,这个函数会在主程序的loop中被加载,那么也就相当于是标准arduino的loop函数。而另一个按钮用作mode变化的功能,用来触发app中的modePress函数。
除此以外,我们还需要写一个Demo.h,里面装着所有app的父类,确保所有app类里的方法都具备一致性;同时还定义了一个线性外推函数,这个函数会在多个app中被使用,这样就不需要重复定义:
#ifndef DEMO_H
#define DEMO_H
// Define each mode with the following interface for a loop and modePress
// function that will be called during the main loop and if the mode button
// was pressed respectively. It's up to each mode implementation to fill
// these in and add their logic.
class Demo {
public:
virtual ~Demo() {}
virtual void loop() = 0;
virtual void modePress() = 0;
};
// Linear interpolation function is handy for all the modes to use.
float lerp(float x, float xmin, float xmax, float ymin, float ymax) {
if (x >= xmax) {
return ymax;
}
if (x <= xmin) {
return ymin;
}
return ymin + (ymax-ymin)*((x-xmin)/(xmax-xmin));
}
#endif
基础任务一
跑马灯可以直接使用库中自带的CircuitPlayground.colorWheel函数来实现颜色的计算。在代码中可以添加一个速度选择功能,当另一个按钮安触发时,切换速度列表中的下一个速度。
新建一个RainbowCycleDemo.h文件,写入下面内容就可以了。
#ifndef RAINBOWCYCLEDEMO_H
#define RAINBOWCYCLEDEMO_H
#include "Demo.h"
// Animation speeds to loop through with mode presses. The current milliseconds
// are divided by this value so the smaller the value the faster the animation.
static int speeds[] = { 5, 10, 50, 100 };
class RainbowCycleDemo: public Demo {
public:
RainbowCycleDemo() { currentSpeed = 0; }
~RainbowCycleDemo() {}
virtual void loop() {
// Make an offset based on the current millisecond count scaled by the current speed.
uint32_t offset = millis() / speeds[currentSpeed];
// Loop through each pixel and set it to an incremental color wheel value.
for(int i=0; i<10; ++i) {
CircuitPlayground.strip.setPixelColor(i, CircuitPlayground.colorWheel(((i * 256 / 10) + offset) & 255));
}
// Show all the pixels.
CircuitPlayground.strip.show();
}
virtual void modePress() {
// Increment through the available speeds.
currentSpeed += 1;
if (currentSpeed >= sizeof(speeds)/sizeof(int)) {
currentSpeed = 0;
}
}
private:
int currentSpeed;
};
#endif
基础任务二
这个任务中我们使用CircuitPlayground库直接读取光线强度和温度,然后把板载的LED分成两部分,蓝色指代温度,红色指代光强,用LED亮起的数量来代表温度和光强的数值。mode按钮可以用来切换指示的区间。
#ifndef SENSORDEMO_H
#define SENSORDEMO_H
#include "Demo.h"
// Define small, medium, large range of light sensor values.
static int minLight[] = { 0, 0, 0 };
static int maxLight[] = { 50, 255, 1023 };
// Define small, medium, large range of temp sensor values (in Fahrenheit).
static float mintempC[] = { 30.0, 25.0, 20.0 };
static float maxtempC[] = { 35.0, 38.0, 40.0 };
// Define color for light sensor pixels.
#define LIGHT_RED 0xFF
#define LIGHT_GREEN 0x00
#define LIGHT_BLUE 0x00
// Define color for temp sensor pixels.
#define TEMP_RED 0x00
#define TEMP_GREEN 0x00
#define TEMP_BLUE 0xFF
class SensorDemo : public Demo {
public:
SensorDemo() {
mode = 0;
}
~SensorDemo() {}
virtual void loop() {
// Reset all lights to off.
for (int i = 0; i < 10; ++i) {
CircuitPlayground.strip.setPixelColor(i, 0);
}
// Measure the light level and use it to light up its LEDs (right half).
uint16_t light = CircuitPlayground.lightSensor();
int level = (int)lerp(light, minLight[mode], maxLight[mode], 0.0, 5.0);
for (int i = 9; i > 9 - level; --i) {
CircuitPlayground.strip.setPixelColor(i, LIGHT_RED, LIGHT_GREEN, LIGHT_BLUE);
}
// Measure the temperatue and use it to light up its LEDs (left half).
float tempC = CircuitPlayground.temperature();
level = (int)lerp(tempC, mintempC[mode], maxtempC[mode], 0.0, 5.0);
for (int i = 0; i < level; ++i) {
CircuitPlayground.strip.setPixelColor(i, TEMP_RED, TEMP_GREEN, TEMP_BLUE);
}
// Light up the pixels!
CircuitPlayground.strip.show();
Serial.print("Light: ");
Serial.print(light);
Serial.print("; ");
Serial.print("Temperature: ");
Serial.println(tempC);
}
virtual void modePress() {
// Switch to one of three modes for small, medium, big ranges of measurements.
mode += 1;
if (mode > 2) {
mode = 0;
}
}
private:
int mode;
};
#endif
当我用手指遮住亮度传感器时,可以看到红色指示灯的变化:
我把热风机的功率调到最小,轻轻吹一下热敏电阻,也可以看到蓝色的温度指示灯发生变化。
基础任务三
这个任务我们使用板载的IR发射管来发送一个脉冲,接着再用IR接收管来检测红外强度。当有障碍物时,反射回来的红外线会被IR接收管观测到,因此可以测量出一个模拟量来。我们同样用LED亮起的数量来显示接近传感的位置,超过阈值后驱动蜂鸣器发出报警。
#ifndef PROXIMITYDEMO_H
#define PROXIMITYDEMO_H
#include "Demo.h"
class ProximityDemo : public Demo {
public:
ProximityDemo() {
mode = 0;
pinMode(CPLAY_IR_EMITTER, OUTPUT);
pinMode(CPLAY_IR_EMITTER, LOW);
pinMode(A10, INPUT);
}
~ProximityDemo() {}
virtual void loop() {
// Reset all lights to off.
for (int i = 0; i < 10; ++i) {
CircuitPlayground.strip.setPixelColor(i, 0);
}
// Measure the proximity level and use it to light up its LEDs.
digitalWrite(CPLAY_IR_EMITTER, HIGH);
delay(1);
digitalWrite(CPLAY_IR_EMITTER, LOW);
int prox = analogRead(A10);
int level = (int)lerp(prox, 300, 500, 0.0, 10.0);
for (int i = 0; i < level; ++i) {
CircuitPlayground.strip.setPixelColor(i, 0, 0, 255);
}
// Light up the pixels!
CircuitPlayground.strip.show();
if (level > 5) {
CircuitPlayground.playTone(330, 100);
}
Serial.print("Proximity: ");
Serial.println(prox);
}
virtual void modePress() {
}
private:
int mode;
};
#endif
可以看到手指接近时,蓝灯通过亮起数量显示了手指接近的程度。这里选用蓝色主要是为了避免红光可能会对红外线带来的干扰。
进阶任务
如果把板子水平放置在不倒翁体内,那么此时板载加速度计的Z轴是竖直向下的,理论Z轴读取到的加速度应该为9.8,也就是重力加速度。当板子发生倾斜时,z轴不再正对地心,因此加速度值会有所减少。利用这个原理可以通过观察Z轴加速度的值来间接观察不倒翁的倾倒程度。当完全水平时,板载LED显示蓝色;若倾倒到最大,这里我设定的是Z轴加速度减小到8时,板载LED变为红色。LED会在红色和蓝色之间线性变化。通过按模式按钮,还可以切换对X轴和Y轴加速的观测。
#ifndef TILTDEMO_H
#define TILTDEMO_H
#include "Demo.h"
// Define range of possible acceleration values.
#define MIN_ACCEL 8.0
#define MAX_ACCEL 10.0
// Define range of colors (min color and max color) using their red, green, blue components.
// First the min color:
#define MIN_COLOR_RED 0xFF
#define MIN_COLOR_GREEN 0x00
#define MIN_COLOR_BLUE 0x00
// Then the max color:
#define MAX_COLOR_RED 0x00
#define MAX_COLOR_GREEN 0x00
#define MAX_COLOR_BLUE 0xFF
class TiltDemo: public Demo {
public:
TiltDemo() { mode = 2; }
~TiltDemo() {}
virtual void loop() {
// Grab the acceleration for the current mode's axis.
float accel = 0;
switch (mode) {
case 0:
accel = CircuitPlayground.motionX();
break;
case 1:
accel = CircuitPlayground.motionY();
break;
case 2:
accel = CircuitPlayground.motionZ();
break;
}
// Now interpolate the acceleration into a color for the pixels.
uint8_t red = (uint8_t)lerp(accel, MIN_ACCEL, MAX_ACCEL, MIN_COLOR_RED, MAX_COLOR_RED);
uint8_t green = (uint8_t)lerp(accel, MIN_ACCEL, MAX_ACCEL, MIN_COLOR_GREEN, MAX_COLOR_GREEN);
uint8_t blue = (uint8_t)lerp(accel, MIN_ACCEL, MAX_ACCEL, MIN_COLOR_BLUE, MAX_COLOR_BLUE);
// Gamma correction makes LED brightness appear more linear
red = CircuitPlayground.gamma8(red);
green = CircuitPlayground.gamma8(green);
blue = CircuitPlayground.gamma8(blue);
// Light up all the pixels the interpolated color.
for (int i=0; i<10; ++i) {
CircuitPlayground.strip.setPixelColor(i, red, green, blue);
}
CircuitPlayground.strip.show();
}
virtual void modePress() {
// Change the mode (axis being displayed) to a value inside 0-2 for X, Y, Z.
mode += 1;
if (mode > 2) {
mode = 0;
}
}
private:
int mode;
};
#endif
完全水平放置时,灯光为蓝色。
当发生倾斜时,灯光逐渐变红:
创意任务二
这个任务稍微复杂一些,板子通过读取麦克风的音量大小,来驱动LED灯指示音量变化。同时,驱动舵机来操作章鱼哥触角变化。但在任务中我并没有直接用开发板来驱动舵机,而是使用到了板载唯一的一个DAC接口A0。我将识别到的音量大小通过DAC以模拟量发送出去,由接在树莓派上的16位ADC模块ADS1115接收,再通过树莓派来根据接收到的模拟量驱动舵机旋转。
开发板的app代码:
// This demo is based on the vumeter demo in the Adafruit Circuit Playground library.
#ifndef VUMETERDEMO_H
#define VUMETERDEMO_H
#include <math.h>
#include "Demo.h"
#define SAMPLE_WINDOW 10 // Sample window for average level
#define PEAK_HANG 24 // Time of pause before peak dot falls
#define PEAK_FALL 4 // Rate of falling peak dot
#define INPUT_FLOOR 56 // Lower range of mic sensitivity in dB SPL
#define INPUT_CEILING 110 // Upper range of mic sensitivity in db SPL
static byte peak = 16; // Peak level of column; used for falling dots
static unsigned int sample;
static byte dotCount = 0; //Frame counter for peak dot
static byte dotHangCount = 0; //Frame counter for holding peak dot
static float mapf(float x, float in_min, float in_max, float out_min, float out_max);
static void drawLine(uint8_t from, uint8_t to, uint32_t c);
class VUMeterDemo : public Demo {
public:
VUMeterDemo() {
currentCeiling = 0;
}
~VUMeterDemo() {}
virtual void loop() {
int numPixels = CircuitPlayground.strip.numPixels();
float peakToPeak = 0; // peak-to-peak level
unsigned int c, y;
//get peak sound pressure level over the sample window
peakToPeak = CircuitPlayground.mic.soundPressureLevel(SAMPLE_WINDOW);
//limit to the floor value
peakToPeak = max(INPUT_FLOOR, peakToPeak);
// Serial.println(peakToPeak);
//Fill the strip with rainbow gradient
for (int i = 0; i <= numPixels - 1; i++) {
CircuitPlayground.strip.setPixelColor(i, CircuitPlayground.colorWheel(map(i, 0, numPixels - 1, 30, 150)));
}
c = mapf(peakToPeak, INPUT_FLOOR, INPUT_CEILING, numPixels, 0);
// Turn off pixels that are below volume threshold.
if (c < peak) {
peak = c; // Keep dot on top
dotHangCount = 0; // make the dot hang before falling
}
if (c <= numPixels) { // Fill partial column with off pixels
drawLine(numPixels, numPixels - c, CircuitPlayground.strip.Color(0, 0, 0));
}
// Set the peak dot to match the rainbow gradient
y = numPixels - peak;
CircuitPlayground.strip.setPixelColor(y - 1, CircuitPlayground.colorWheel(map(y, 0, numPixels - 1, 30, 150)));
CircuitPlayground.strip.show();
// Frame based peak dot animation
if (dotHangCount > PEAK_HANG) { //Peak pause length
if (++dotCount >= PEAK_FALL) { //Fall rate
peak++;
dotCount = 0;
}
} else {
dotHangCount++;
}
analogWrite(A0, mapf(peakToPeak, INPUT_FLOOR, INPUT_CEILING, 0, 255));
}
virtual void modePress() {
}
private:
int currentCeiling;
};
static float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
//Used to draw a line between two points of a given color
static void drawLine(uint8_t from, uint8_t to, uint32_t c) {
uint8_t fromTemp;
if (from > to) {
fromTemp = from;
from = to;
to = fromTemp;
}
for (int i = from; i <= to; i++) {
CircuitPlayground.strip.setPixelColor(i, c);
}
}
#endif
下面是树莓派部分。我们同样使用adafruit的blinka来驱动ADS1115以及舵机。先进行blinka以及DAS1115库的安装和配置:
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get install python3-pip
sudo apt install --upgrade python3-setuptools
cd ~
sudo apt install python3-venv
python3 -m venv env --system-site-packages
source env/bin/activate
pip3 install --upgrade adafruit-python-shell
wget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py
sudo -E env PATH=$PATH python3 raspi-blinka.py
pip3 install adafruit-circuitpython-ads1x15
完成上面步骤后,可以在树莓派中新建一个main.py的文件,开始编写代码。代码读取到ADS1115的值后,转化成500-2500之间的脉宽,并通过18号脚发送出去。
import time
import board
import pwmio
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
# Create the I2C bus
i2c = busio.I2C(board.SCL, board.SDA)
# Create the ADC object using the I2C bus
ads = ADS.ADS1115(i2c)
# Create single-ended input on channel 0
A0 = AnalogIn(ads, ADS.P0)
# Initialize PWM output for the servo (on pin BCM18):
servo = pwmio.PWMOut(board.D18, frequency=50)
# Create a function to simplify setting PWM duty cycle for the servo:
def servo_duty_cycle(pulse_us, frequency=50):
period_us = 1.0 / frequency * 1000000.0
duty_cycle = int(pulse_us / (period_us / 65535.0))
return duty_cycle
print("{:>5}\t{:>5}".format('raw', 'volt'))
while True:
try:
value = A0.value
voltage = A0.voltage
print("{:>5}\t{:>5.3f}".format(value, voltage))
dc = value / 16000.0 * 2000.0 + 500.0
print(dc)
servo.duty_cycle = servo_duty_cycle(dc)
except:
pass
finally:
time.sleep(0.1)
接线的话首先把playground开发板,树莓派,ADS1115模块和舵机都共地。树莓派40pin接口上有多个GND这时候就可以派上用途。接着SDA和SCL分别接在BCM2和BCM3上,舵机接在BCM18上,舵机使用树莓派5V引脚供电,ADS1115模块使用3.3V引脚供电。
只需要对板子吹气,就可以在麦克风处产生噪音,舵机和LED都做出了相应的反应:
创意任务三
水果钢琴是利用了板子的引脚在被触摸后电容发生变化的原理实现的,板子上只要支持电容测量的引脚都支持作为水果钢琴的键盘。这里由于我没有额外的接线夹,因此就不连接水果了,直接触摸引脚。当某个引脚被触发后,亮起相应的LED,并播放对应的声音,就可以实现钢琴效果。
#ifndef CAPTOUCHDEMO_H
#define CAPTOUCHDEMO_H
#include "Demo.h"
#define CAP_SAMPLES 20 // Number of samples to take for a capacitive touch read.
#define TONE_DURATION_MS 100 // Duration in milliseconds to play a tone when touched.
class CapTouchDemo: public Demo {
public:
uint16_t CAP_THRESHOLD = 200; // Threshold for a capacitive touch (higher = less sensitive).
CapTouchDemo() {
playSound = true;
if (CircuitPlayground.isExpress()) {
CAP_THRESHOLD = 800;
} else {
CAP_THRESHOLD = 200;
}
}
~CapTouchDemo() {}
virtual void loop() {
// Clear all the neopixels.
for (int i=0; i<10; ++i) {
CircuitPlayground.strip.setPixelColor(i, 0);
}
// Check if any of the cap touch inputs are pressed and turn on those pixels.
// Also play a tone if in tone playback mode.
if (CircuitPlayground.readCap(0, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(3, CircuitPlayground.colorWheel(256/10*3));
if (playSound) {
CircuitPlayground.playTone(330, TONE_DURATION_MS); // 330hz = E4
}
}
if (CircuitPlayground.readCap(1, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(4, CircuitPlayground.colorWheel(256/10*4));
if (playSound) {
CircuitPlayground.playTone(349, TONE_DURATION_MS); // 349hz = F4
}
}
if (CircuitPlayground.readCap(2, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(1, CircuitPlayground.colorWheel(256/10));
if (playSound) {
CircuitPlayground.playTone(294, TONE_DURATION_MS); // 294hz = D4
}
}
if (CircuitPlayground.readCap(3, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(0, CircuitPlayground.colorWheel(0));
if (playSound) {
CircuitPlayground.playTone(262, TONE_DURATION_MS); // 262hz = C4
}
}
if (CircuitPlayground.readCap(6, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(6, CircuitPlayground.colorWheel(256/10*6));
if (playSound) {
CircuitPlayground.playTone(440, TONE_DURATION_MS); // 440hz = A4
}
}
if (CircuitPlayground.readCap(9, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(8, CircuitPlayground.colorWheel(256/10*8));
if (playSound) {
CircuitPlayground.playTone(494, TONE_DURATION_MS); // 494hz = B4
}
}
if (CircuitPlayground.readCap(10, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(9, CircuitPlayground.colorWheel(256/10*9));
if (playSound) {
CircuitPlayground.playTone(523, TONE_DURATION_MS); // 523hz = C5
}
}
if (CircuitPlayground.readCap(12, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(5, CircuitPlayground.colorWheel(256/10*5));
if (playSound) {
CircuitPlayground.playTone(392, TONE_DURATION_MS); // 392hz = G4
}
}
// Light up the pixels.
CircuitPlayground.strip.show();
}
virtual void modePress() {
// Turn sound on/off.
playSound = !playSound;
}
private:
bool playSound;
};
#endif
至此,就完了所有的任务。在开篇的主程序文件中,我们已经把这些任务的文件都include在了主程序中,这样就可以通过板载的D4按钮来按顺序切换任务。
这次活动让我学会了多app管理调度的方法,我觉得收获非常大,希望下次活动还能继续参加,收获更多。
本帖最后由 eew_dy9f48 于 2024-8-8 18:40 编辑