如何在淘宝买电子模块

想做DIY,科研竞赛,或者一些课程,都会用到微控制器和电子模块。

如果住在深圳、广州这种地方,去店里找东西很方便。Arduino.cc曾说深圳是硬件的好莱坞。但是如果在杭州这种地方,想找个轮式机器人用的巡迹模块可不容易。淘宝就成为了唯一可行的选择。 继续阅读 →

气球飞艇

二十四只氦气球,三个螺旋桨,像飞屋环游记里一样乘着气球飞翔!

下面描述技术细节: 继续阅读 →

使用键值对进行串口通讯

在Arduino与电脑/手机之间通过串口通讯,比较麻烦的地方有以下几点:

  • 确定一条指令的开始与结尾
  • 防止字节数据丢失引起的错误
  • 编码与解码

简单的办法通常是每个字节作为一个指令,也就是一个BYTE数值或字符。这样做的缺点是表示的数据范围小,而且不够灵活。如果要发送几种不同类型的指令,这种方法也不行。

而表示一个复合命令,则需要断开每一个字段。使用键值对是一个很便利的方式。能够表示大部分操作指令:

name,value;

我们以逗号和分号作为分隔符和结束标记,只要将指令表示成键值对发送就可以了。在接收的一端,按照分隔符将字串断开,获取名称和数值,然后进行解释。

示例代码如下:

char input;
String value;
String name;

const int NAME = 1;
const int VALUE = 2;

int readProcess = NAME;

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
}

void loop() {
  if(Serial.available()){
    input = Serial.read();
    process();
  }
}

void process() {
  if(input==','){
    readProcess = VALUE;
    Serial.println(name);
  }
  else if(input==';'){
    Serial.println(value);
    run(name, value);
    name = "";
    value = "";
    readProcess = NAME;
  }
  else if(readProcess==NAME){
    name+=input;
  }
  else if(readProcess==VALUE){
    value+=input;
  }
}

void run(String name, String value){
  if(name=="speed"){
    int num = value.toInt();
    if(num==-1023){
      digitalWrite(13, HIGH);
    }
    else{
      digitalWrite(13, LOW);
    }
  }
}

在run()方法里,是对键值对的解释程序。

有分隔符能够很有效地避免传递时丢失字节的问题,保证前面发生的错误不会影响后面数据的传递。一次传输错误最多只影响一个指令的执行,不会对后续的数据传递造成麻烦。

自行车测速器

今天做了一个为自行车测速的传感器。原理很明了:在自行车轮圈上均布贴上黑色贴条,而轮圈接近白色,转动时灰度传感器就会采集到黑白交替的频率,以此确定速度。

灰度传感器模块由光电对射管和放大器电路组成,能以某一基准分辨反射灰度是偏黑还是偏白。这个基准可以通过调节电位器来改变。灰度传感器采集的是反射灰度,而非颜色实际的灰度,反射面的角度和距离都会产生影响。即使是黑色,在足够近的情况下也能反射足够多红外线。因此在使用过程中要调节。

采集频率是简单的,但如何采集到可靠稳定的数据就有不少麻烦。于是我们采用了连续采样拟合的办法,尽量避免速度的异常波动。

在控制指示灯的过程中,我们的电路装在自行车上,不易于连接电脑调试。我们用蓝牙模块接在Arduino板上,通过手机接收,解决了这个麻烦。

Arduino与Processing的串口通信

在网络上普遍存在的Arduino与Processing互动的例子,都具有一个很简单的构造:Arduino上用Serial.print()发送,在Processing中用Serial.readString()读取,或者反过来。然而实际应用过程中大家就会发现这个简单的逻辑模型会发生莫名其妙的错误。最显著的是有时候会收到空值,即使用Serial.available()检测,也会有时收到间断的字符串或者多个字符串混在一起了。

下面是一个经典的Processing与Arduino通信实例:

//Processing Code
import processing.serial.*;

Serial myPort;

void setup(){
  myPort = new Serial(this,"/dev/ttyACM0", 115200); //Set Serial Port

}

void draw(){
  if(myPort.available()>0){
    String message = myPort.readString();
    println(message);
  }
}
//Arduino code
int data=12345; 
void setup()
{
  Serial.begin(115200);//rate
}
void loop()
{
  Serial.print(data); //send data
  delay(1000);
}

然后我们期待着每次获取“12345”并显示在屏幕上,但事与愿违,我们得到的情况是这样的:

12345
123
45
12345
12345

输出时的中断是怎样产生的呢?要探究这个问题的根源,需要重新审视串口通信的原理。

串口通讯就像一趟公共汽车,每个字节是一个在等车的人。他来到车站(发送数据),车还没有来,所以新来的人就一直等待(缓存)。当公共汽车来了的时候,将这些人一次接走(读取数据),当然车也是有容量的,只能载一定数量的人(缓存大小)。现在有一个旅行团(字符串/字符数组),一部分人走在前面刚刚赶上了车(被读取),而另一部分人没赶上,只能等待下一班车(下一次读取)。另一种情况是,两个旅行团都在车站等车,被同一班车接走了。这就是为什么我们读取的时候字符串会断成两节,或者并起来。

核心原因是:串口流通的数据都是bytes而没有字符串概念,所有发送数据都会按一个byte一个byte缓存,不论是否是连续字符串;而读取时会取走所有缓存bytes,不论它们是否是一个、半个还是多个字符串。

Arduino和Processing的数据收发速度是不一样的。如果用Arduino延时较长时间,Processing可能读取一个字符串或字符串的一部分。如果Arduino延时较短,Processing可能读取多个字符串,但不一定完整。在读取字符串的时候,无法确定上一个字符串是否被读取了,当前字符串是否缓存完毕,因为字符串都已经切成了bytes,连成一串。这个问题是串口通信本身造成的,一定会出现。

一种解决方法是,通过在接收端缓存数据来解决这个问题。为传输数据设置一个结束标记,如'\n'(换行符),就能在接收到的数据流中识别到一个字符串的结尾。当未遇到结束标记,就一直将串口数据保存在一个buffer变量中,继续接收。

Processing的SerialEvent事件类型就提供了这种方式,使用bufferUntil(ch)可以在遇到某个指定字符时才完成缓存。

程序实例:

//Processing Code
import processing.serial.*;

Serial myPort;

void setup(){
  myPort = new Serial(this,"/dev/ttyACM0", 115200);  //in fact, any rate is ok...
  myPort.bufferUntil('\n');  //buffer until meet '\n', then call the event listener
}

void draw(){
  
}

//listen to the event. when buffer filled, run this method
void serialEvent(Serial p) {
  String inString = p.readString();
  print(inString);
}

//then the buffer will reveive all the bytes
//Arduino Code
int data=12345; 
void setup()
{
  Serial.begin(115200);//rate
}
void loop()
{
  Serial.println(data); //send data, end up with '\n'
  delay(1000);
}

当然,这种方法也可以用在普通的串口通信中,不必使用SerialEvent。接收的数据不直接使用,而是作为缓存。若未遇到结束标记,就继续读取下一次。当遇到结束标记,即完成缓存。

程序实例:

//Processing Code
import processing.serial.*;

String message;
String temp;
Serial myPort;

void setup(){
  myPort = new Serial(this,"/dev/ttyACM0", 115200); //Set Serial Port
}

void draw(){
  if(myPort.available()>0){
    temp = myPort.readString(); //temp for read bytes
    for(int i = 0; i < temp.length(); i++){
      //if meet the end mark
      if(temp.charAt(i) == '\n'){
        println(message);
        message = "";  //clean string
      }
      else
        message += temp.charAt(i);  //store byte
    }
  }
}
//Arduino Code
int data=12345; 
void setup()
{
  Serial.begin(115200);//rate
}
void loop()
{
  Serial.println(data); //send data, end up with '\n'
  delay(1000);
}

遥控变色灯

课程作业总是没完没了。有时候不得不日复一日地做同样的事情,虽然明明知道不久就会全忘掉。

早晨起来就开始琢磨这次信息产品课要做点什么。淘宝上买的蓝牙模块还没有送到,电脑无线互动做不了。而且第一周是基本练习,包括数字输入输出,模拟输入输出,串口上载和下载,开关。原材料也很少,电阻,LED,一个全彩LED,遥控器,还有光敏二极管,电位器,开关,小键盘什么的。最后选择了全彩LED和红外遥控器。全彩LED我是第一次用,红外遥控看起来很酷。他们拼起来便是一个遥控变色灯。

原理设想比较简单,通过红外遥控输入控制信号,使R,G,B值增减,单片机控制三路PWM输出的占空比,从而达到变幻颜色的目的。电路也很快搭好了,只接了两个元件:全彩LED,红外接收器。全彩LED使用了9,10,11三个PWM端口,而红外接收器接在13端口。
遥控彩灯原理图示意
Arduino有现成的红外接收库。一开始就非常顺利地接收了红外遥控的信号,并记录下了每个键所对应的值,这是通过串口发送电脑回来的。很特别的是,当一直按下一个键时,后续会重复返回FFFFFFFF。然后根据按键控制红灯的亮度,并设计成若一直按下,则会重复动作,这样就不必按很多次按钮了。

当我们如法炮制,添加了绿灯,一切正常,添加了蓝灯,却无法工作。按下蓝灯控制键,程序便会中断。这是为什么呢?我们开始一直以为是遥控的原因,更换了遥控上的其他键来控制,情况是一样的。后来我们又检查程序,将可能的错误修正。但依旧是红绿灯正常,而蓝灯不能使用。三个灯应该是对等的,如果其他两个没有问题,说明程序是可行的。既然该换了按键也没有用,说明遥控这部分也没有问题。我们将蓝绿调换,发现蓝色可以正常工作,说明全彩LED正常。所以最终有问题的是电路板硬件或者连接。蓝灯接入的11端口可能存在问题,便将其该换到6号端口,如此就解决了问题。

AVR单片机的6路PWM是有差异的。Arduino将其同质化处理,虽然简化了认知过程,但导致很多问题无法解释。机器人协会曾经也讨论过是否要引进Arduino,但后来还是否定了这个计划。因为Arduino的思维方式虽然在早期能够快速入门,但却导致在进阶的过程中处处遭遇困难。以后再去认识硬件细节的设计,摒弃原有的理解,是非常困难的。

IMG_0054

IMG_0055

IMG_0056

#include "IRremote.h"

int RECV_PIN = 13;

int red = 0;
int blue = 0;
int green = 0;

int last = 0;
int now = 0;
 
IRrecv irrecv(RECV_PIN);
 
decode_results results;
 
void setup() {
  Serial.begin(9600);
  irrecv.enableIRIn();
  pinMode(9, OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(6, OUTPUT);
}
 
void loop() {
  
  if (irrecv.decode(&results)) {
    now = results.value;
    Serial.println(now);
    
    if(now==-1)
      now = last;
    
    switch(now){
      case -23971:
	if(red==0)
	  red=255;
	else
	  red--;
	break;
      case 25245:
	if(red==255)
	  red=0;
	else
	  red++;
	break;
      case 8925:
	if(green==0)
	  green=255;
	else
	  green--;
	break;
      case 765:
	if(green==255)
	  green=0;
	else
	  green++;
	break;
      case -8161:
	if(blue==0)
	  blue=255;
	else
	  blue--;
	break;
      case -22441:
	if(blue==255)
	  blue=0;
	else
	  blue++;
	break;
      case -7651:
	red=0;
	green=0;
	blue=0;
	break;
      default:break;
    }
    irrecv.resume();
    last = now;
  }
  
  color(red, green, blue);            
}

void color(int r, int g, int b){
  analogWrite(9, r);
  analogWrite(10, g);
  analogWrite(6, b);
}