Archive for the ‘大事记’ Category

最近做的几件事情

各位朋友,首先说说上一篇博客里提到的像锅盖一样的东西吧。

那是一个康达效应飞行器,英文叫Coanda effect。锅盖型的表面,可以把横向的高速气流变成向下,同时产生向上的升力。它的性能理论上和螺旋桨的相当,安全性会好一些。

不过图片里的这个盖子已经摔坏,新的还在继续搞 :)

另外,这段时间做了一件大事,开发了一个为创客们做技术交流用的网站: 创酷网

这是网站的说明:

创酷网(ChuangKoo.com)是一个基于科技产品制作分享的“泛创客”社会化平台。我们希望能够激发公众的创造创新热情,让更多的人动手做起来,产生更多的创意和作品;让青年人和孩子们体验数字智造的魅力,培养创新能力;让创客们更加方便的碰撞与交流,并发掘创作中的价值,营造良好的创客生态圈。

目前功能包括上传作品和发表随笔,很快会有讨论群组和创客地图的功能。因为刚刚上线,注册还需要邀请码。加邀请码是因为现在还比较初期,槽点太多,希望来的都是朋友,多提意见 :)  有需要的同学可以给我发邮件:sys@chuangkoo.com

另外,最近也有不少朋友问到电动滑板的一些细节,其实我老早就想把攻略发出来了,因为正在做这个新网站,所以一直没来得及写攻略。未来我会把博客慢慢的迁移到新的平台上,电动滑板的详细步骤会发在上面。有兴趣的朋友也可以在上面发作品和随笔。

最近正在做一个好玩的东西

先放个图,等调好了再放视频和攻略 :)

有点儿像飞碟

有点儿像飞碟

no zuo no die的小实验:触摸直流高压电的一端危险吗?

上次做完电棍之后,一直手痒痒的想测试一下是否好用, 可惜没有人愿意当测试志愿者 :D (好像是废话,哈哈)

上周末假惺惺的要给老婆做酸菜鱼,弄了一条草鱼回来。用电棍电了几秒钟,那家伙剧烈抖动了一会儿以后晕过去了,但是鱼这种生物你知道的,就算是把头剁掉,电的时候还是会乱动。而且鱼的身体比较小,依然不知道对人的效果如何。

回到这次的主题:直流高压电到底有多危险,如果只摸高压电中的一级,会不会触电呢?就像这样:(这张图里的电源没有接通)

用手碰直流高压电的一级

用手碰直流高压电的一级

很多人可能会直观的认为,碰高压电的一端肯定会触电,毕竟从小到大都一直听到“高压危险”的警告。但是咱们可以做这样的一个假想实验:把很多很多节电池串联起来,那么这个电池组的电压应该非常非常大,这时候如果碰电池组的一端,会不会触电呢?

关于直流高压的假想实验

关于直流高压的假想实验

我相信大多数朋友看到这个图以后,会认为电池组虽然电压高,但是不会触电,因为没有形成回路,不会有电流通过人体。我也是这么想的,电棍的两端虽然电压非常高,但是本质上也相当于一个大电池,如果只摸一端的话,不形成回路,感觉应该是安全的。不过本着安全第一的精神,我还是先用电笔试了试,结果严重出乎我的意料,电笔剧烈的闪动着;甚至不用碰到那个螺丝,距离5毫米的时候就开始闪烁:

电笔测试是有电流的

电笔测试是有电流的

你没有看错,这个不是手电筒,那些光是电火花产生的,如果没有用电笔的话,这会儿我可能还在地上抽搐着……

思考了一下其中的原因,猜想如下:人体相当于是一个电容,虽然电容量很小很小,但是电压足够大的时候,对人体充电也会产生一定的电流;如果这是个静态高压电源,那么一次充满电之后,就不会再有电流,所以感觉应该是麻一下然后就没事了。但这个电棍是在不断放电的,它的两个电极积累了足够大的电压以后,把空气击穿,一瞬间把电荷几乎都放完了,然后继续升压放电。也就是说,两个电极之间的电压是不断波动的,相应的人体电容就会不断地充电放电。这就是电笔剧烈闪烁的原因。

为了验证这个想法,我翻出了家里的电蚊拍。查了下资料,它的电压大约是2000V左右,可以电死蚊子,但是没有强大到击穿空气的程度。我用电笔碰了一下其中一级,果然电笔亮了一下就熄灭了。然后移过去碰另外一级,又是亮一下就熄灭了。这就是一个手动的充放电周期 :) 我用电笔接触了一会儿其中一级以后,鼓起勇气直接用手按了上去,果然没有任何感觉:

用手接触电蚊拍

用手接触电蚊拍

注意事项:如果你一定要像我一样摸电玩,记住用手指背面去碰,因为人触电的时候肌肉会收紧,手背就会自动离开电源。用正面去碰的话,触电的时候可能会越抓越紧!

我相信如果这时候用手去摸电蚊拍的另外一级,一定会麻一下的。这里不得不赞一下电蚊拍的设计,它是三层的铁丝网设计,两侧的铁丝网应该是同一级,中间的那一层是另外一级。所以蚊子飞过的话会被电死,而双手同时摸到两侧的话却不会被电到。

小结一下:

1. 对于电蚊拍这样的静态直流电源来说,触摸其中一级,可能会被电一下,然后就没有电流经过人体了;

2. 对于交流电源来说,即使只有220V的家庭电源,也是非常危险的,因为会有持续的电流通过人体;

3. 对于电棍这样不断放电的高压直流电源来说,触摸其中一级相当于交流电源,同样是非常危险的!

4. 请注意上面说的都是“只碰其中的一级”,如果你同时摸两级形成回路的话,36V以上的不管直流还是交流电源都是危险的!

对于第一条来说,这个“电一下”大概有多严重,完全取决于电压的高低。其实大家在生活中肯定被电过,尤其是冬天,身上经常噼里啪啦的放电。在网上查了下:

  • 静电电压在你能感觉到时已经达到3500伏
  • 静电电压在你能听到时已经达到4500伏
  • 静电电压在你能看到时已经达到5000伏
  • 人穿的化纤衣服摩擦产生的静电,电压可以达到几万伏

也就是说,冬天里把你电的头皮发麻的静电,大约是几万伏的量级。而这个DIY电棍的高压模块,号称可以达到60万伏。经验丰富的老薛同学从电弧长度估算电压至少是5万伏,不过我试过这个东西的电弧可以拉到3cm以上,只是因为制作电棍的晾衣杆直径不够,所以放电尖端的距离在1cm左右。不管卖家是否奸商,看上去至少10万伏应该是有的;而相应的2000伏电蚊拍摸一下应该问题不大 :)

以下是老薛同学的留言,贴出来供大家参考:

老薛的留言

老薛的留言

作为一个no zuo no die的好奇心严重的家伙,我纠结了一天要不要试着电一下自己看看效果,这个准交流的高压模块危险性可能还高于老薛说的大电容。最后让我想到一个好办法,因为之前的实验中,电笔没有接触到电极的时候就已经开始闪烁,所以我可以用一根铁丝慢慢靠近电极,万一电的受不了的时候还可以即时离开。晚上终于忍不住去试验了,铁丝大概距离电极5mm的时候,就开始有非常毛细的小电火花出现;然后我打算慢慢的靠近;可能因为手抖了一下距离太近了,整个胳膊像被撞了一下,铁丝直接被扔出了阳台。还好不是直接碰到电极,没有体验到老薛说的“刻骨铭心的疼痛感”,检查了一下也没有尿裤子。不过手掌好像现在还有点儿发麻,我相信这玩艺儿戳到歹徒身上或者只是四肢上,应该是可以瞬间放倒的 :)

注意:高压危险,非专业人士请勿模仿!

PS:思考:第二张图里的蓝精灵,面对一百万节串联电池的时候,会不会被电到呢?

最后:阿弥陀佛,请草鱼施主安息吧,感谢您为科学做出的贡献……

好书推荐:我们都是科学家

最近一段时间忙忙碌碌,除了上班以外,一直在做一个小东西,前几天刚刚完工。

做的东西其实没啥技术含量,不过挺好玩的,今天先不说了。不过这几天总算有时间把老薛的书仔细看一遍了。

我们都是科学家

我们都是科学家

话说和老薛在网上已经很是很久了,但是知道老薛长啥样,还是在这本书的封页里看到的 :)

这本书介绍了很多有趣的物理实验,最重要的是,其中有很多高深的物理实验,是我等非物理专业人士在家可以实现的。其中磁悬浮、激光全息(感谢老薛赠送的全息胶片)已经有很多朋友实现了。

这本书的另一个特点是由浅入深,像我这样搞工程的人,往往不求甚解,而老薛则更愿意了解现象背后的深层次原理, 比如“指南针为什么不会飞向两级”,我等凡人一般都不会去思考这个问题。另一个例子是磁悬浮,当时我也买过一个玩具,就是那种拧了以后浮的很高,不需要用电的那种。我也想过怎么通过电磁场激励,让它保持旋转。但是一方面理论知识不够,另一方面也是怕麻烦,后来老薛很轻松的就实现了。等有空了,我准备按书上的攻略再测试一把 :)

多的不说了,贴个目录在这里,有兴趣的朋友买一本来看看吧,支持下老薛同学!

目录

1. 透过太阳眼镜,看到半个世界   11
几个关于光的偏振性的实验,所需材料非常简单,有一副偏振片太阳镜就行了。
2. 揭秘神奇的光:激光   23
动手拆开一台氦氖激光器和一只激光笔,我们能够看到神奇的激光是怎样产生的。
3. 沿弧线传播的光   41
把糖溶解在水中能制造出一种“神奇”的溶液,一束激光通过它时不再沿直线传播,而是划出一道美丽的弧线。
4. 探测微波炉泄漏及测量光速   53
自制简单的天线装置和检测电路,以检测微波炉的电磁波泄漏(如果身边没有微波炉,也可以用它来检测手机的电磁波)。
5. 说磁   67
探索磁性这一神奇的自然现象,验证铝是被磁铁吸引的、铜是被磁铁排斥的、胡萝卜也具有磁性等有趣而且违背常识的现象。
6. 电机总动员   79
几种有趣的直流电动机,通过亲手制作,了解人类的好帮手——电动机的基本原理,让第二次工业革命的浪潮涛声依旧。
7. 逆磁悬浮   91
什么是磁悬浮?什么是逆磁性材料?你将了解如何用简单的逆磁材料以及强磁铁来实现磁悬浮。
8. 永远悬浮的陀螺   105
本章开始于一个非常好玩而且物美价廉的玩具:磁悬浮陀螺。我们将会解密陀螺稳定悬浮的真正原因。
9. 激光传声   121
重返光的缤纷乐园,你将了解到如何通过搭建一个简单的电路,利用激光充当看不见的导线,在两地之间传递声音信息。
10. Feed the Monkey   131
本章将带领大家制作一件非常有趣的装置,生动地演示力学中一个古老的问题。
11. 做老百姓自己的全息   143
一种非常有趣的制作3D照片的方法:全息技术。
12. 五角星引发的物理学   161
由分析五角星涉及的光波的衍射现象入手,你会发现只有5个角的星星是不存在的。
13. 大音叉 小音叉   173
各种音叉的有趣故事。通过测量普通音叉在发声时声音强度在空间中的分布,我们可以探索声波的干涉现象,并澄清一个由来已久的误解。
14. 给太阳量体温   185
如何用非常简单的材料自制光谱仪。通过分析太阳的光谱成分,我们可以遥测出太阳的温度。
15. 像“砖家”一样使用照相机   201
本章从业余科学家的角度出发,介绍摄影的基本要素,以解除长期以来只会使用自动模式拍照的广大文艺和科学青年的困惑。
16. PID控制原理与实践   211
这是一个非常好用的控制方法:PID控制。理解了它的来龙去脉后,你会发现它其实非常直观易懂。

真是不好意思

好久没打开博客,看到好多留言。因为在一个创业公司工作,最近有大量的变动,非常忙碌。这星期刚刚签了一些单子,还不知道是喜是忧……

留言就不一一回复了,在这里向大家一并表示感谢和歉意。

用一块Arduino输出多路PWM

【经wwwtiger同学提醒,关于Arduino原生PWM的频率,我之前的理解是错的,应该是490Hz左右,特此更正】

周末有朋友来家里玩,看到了放在角落里的那个大的四轴飞行器。拿下来看了看,发现上面已经落满了灰尘。应该有几个月没动了,当时写的攻略目录也好久没更新了。这段时间打算抽空陆陆续续把这个页面里的目录内容都补齐,免得成为烂尾工程。可惜实验还没有成功,所以不能算是经验,各位兄弟权当教训看吧 :)

四轴飞行器最终的控制手段,就是分别调节4个电机的转速。对于常见的无刷电机,一般通过高电平在0.9ms~2.1ms左右的PWM来控制转速。这就涉及到如何用arduino输出多路PWM的问题,在Arduino中,一般可以通过下面这些方式来输出PWM方波:

1. 用原生的PWM输出功能
把pinMode定义为输出,然后用analogWrite就可以输出PWM。以Arduino Mega 1280为例,它有14路PWM输出。analogWrite的参数范围是0~255,对应的是0~2ms的高电平。
一般情况下,原生的PWM频率固定为490Hz,通过一些参数的修改,可以提高PWM频率,但是参数范围只能是0~255。

int pin = 8; //0~13

void setup()
{
    pinMode(pin, OUTPUT);
}   

void loop()
{
    analogWrite(pin, 128);
    delay(500);
}

2. 通过普通的输出管脚手动控制PWM

PWM归根结底无非就是输出0,1序列的方波,所以我们还可以简单的通过digitalWrite和delay来生成PWM方波。
这种方式的特点是可以非常精确的控制高电平时间和PWM周期,缺点是CPU时间不好充分利用,多路时编程有点复杂。

void loop()
{
  long t0 = micros(); //记录进入loop的时间
  double T = 20000; //一个周期的微秒值
  double len = 900;  //高电平时间
  for(int i = 0; i < 4; i++) digitalWrite(pins[i], HIGH);
  delayMicroseconds(len);
  for(int i = 0; i < 4; i++) digitalWrite(pins[i], LOW);
  /*
   *在这里干别的事情
   */
  int leftMs = (int) (t0 + T - micros()); //剩余时间
  if(leftMs < 0 ) leftMs = 0; //万一超出周期,只能立刻中止
  delayMicroseconds(leftMs);
}

3. 用servo库
Servo库是Arduino用来控制伺服电机的,印象中是使用了内部中断来触发的,所以主程序只管干自己的事情,时间到了PWM就会自动触发。
它默认的频率是50Hz,但是可以在头文件中修改。文件位置是arduino-0023\libraries\Servo.h,修改下面这行可以把PWM频率升到400Hz:
#define REFRESH_INTERVAL 2500

具体的细节在之前的一篇小结里提到了:Arduino自带的Servo库小实验

先记录这些,希望在把全部的“教训”写完之前,能有空把四轴飞起来 :)

Arduino自带的Servo库小实验

之前有很多朋友问过我,为什么不用Arduino自带的Servo库来控制电机。我在动手做四轴之前,其实也大概看了下Servo库的说明,当时看到

servo.write(angle) 这个函数,其中angle(int)是角度,取值范围是0 到 180

我想当然的认为,这个servo库最多也就是180级的梯度来设置PWM,这样说来精度完全不够啊;另外也没有看到有地方能修改PWM频率。

总结了下我当时不用Servo库的原因:
1. 控制的级数太少,不够精确;
2. 不可修改PWM频率;
3. 需要使用特殊的几个脚;(不知道在什么地方看错了,留下的错误印象)

这几天obK仔给了很多建议,我也看了看源代码。现在看来,上面这几点都是我瞎操心了。

首先,0~180是servo库对应的从min到max的控制,对应于0.9ms到2.1ms的电调来说,angle参数加1对应的时间变化为:
(2.1-0.9)*1000/180 = 6.7微秒,已经是比较精确了。
即使这样如果还觉得不够精确,还可以使用servo.writeMicroseconds,这个函数可以直接用微秒作为参数。

其次,PWM频率是可以修改的,在Servo.h文件中,修改下面这行可以把PWM频率升到400Hz:
#define REFRESH_INTERVAL 2500
不过这只能在编译之前改,不能在程序运行的过程中动态修改。当然修改源代码也可以解决这个问题,把这个参数做成一个变量,用接口进行修改控制即可。

最后,Servo库可以绑定所有digitalWrite的脚,这一点来说,比原生的PWM还方便!

做了一个简单的小程序,测试Servo库同时控制4个电机的情况。实验结果看上去还不错,不过没有测试4个电机转速各不相同的情况。
代码果然看上去简洁多了:

Servo powerServo[4];
void setup()  
{
  motorPins[0] = 38;   //x1
  motorPins[1] = 43;   //x2
  motorPins[2] = 42;   //y1
  motorPins[3] = 39;   //y2
  for(int i = 0; i < 4; i++) powerServo[i].attach(motorPins[i]);
}
void loop()
{
  long t0 = micros(); //记录进入loop的时间
  while(Serial.available()) {
    // 这里是获取控制指令
    pushData(Serial.read());
  }
  double T = 2500; // 一个周期的微秒值
  double len = 900 + ctrlPower * 5;  //(0~200对应总周期0.9ms~1.9ms)
  for(int i = 0; i < 4; i++) powerServo[i].writeMicroseconds(len);

  int leftMs = (int) (t0 + T - micros());
  delayMicroseconds(leftMs);
}

截止到3月1号的四轴飞行器代码

应SSS_SXS建议,把目前的代码贴出来供大家分析。其中注释有点乱,有英文有中文,凑合看吧。

【特别感谢】有一位非常热心的朋友wnq送了我几套木质机架,还有一套CACM的飞控+GPS模块。所以最近正在计划重新用成品飞控搭一套四轴找找感觉。原来的四轴暂时不打算拆,正在淘宝各种散件和遥控器。另外电机轴也摔歪了,所以准备按wnq的攻略把几根轴都换一换。

下面这段代码是3月1号最后一次修改,很遗憾最近这段时间一直没啥更新。
大致思路是先计算出飞行姿态角AngleX和AngleY,然后根据角度和角速度,计算出blanceX和blanceY。
这两个参数的含义是,blanceX表示x轴两个电机的参数分配比例,例如balanceX = 1,表示x1和x2两个电机转速相同,
balanceX = 0.5,表示x1的转速是0.5,x2转速是1.5。
Y轴的概念是类似的,之所以这样处理,是期望x轴两个电机的转速和大致和y轴两个电机相同,这样飞行器不至于打转。
另外,还有个balanceXY,用于在电机转速不均匀的时候,微调x轴电机和y轴电机的转速比,这个本来是打算在电子罗盘的应用中加上的,现在暂时保持等于1。

#include 
#include 

/**
 * 记录:目前较为稳定的实验参数为33, 66,挂绳貌似能垂直起降,但是真正试飞时依然侧飞
 * 试飞时,在3秒内飞到10米高,测距周期是100ms,对应的距离是33cm
 * 起飞时:
 * 1. 遥控在0~directCtrlRange时,按n*5计算转速,方便调试
 * 2. 遥控>directCtrlRange时,按n-directCtrlRange+10计算高度,油门按一定速度缓慢上升,直到离地指定的高度
 * 3. 超声波测距量程是2~180cm,不知道为什么超出量程时会读出7cm的数值,必须注意纠错
 */
boolean connectToPc = false;
long stableTime = 1500000;
int directCtrlRange = 40;
double fastStep = 1.8;
double maxThrottle = 650;

/* Gyro sensor */
int GYRO_ADDR = 0x68; // 7-bit address: 1101000, change to 0x69 if SDO is High
byte GYRO_READ_START = 0xA8; //Start register address, total 6 bytes: (XL,XH,YL,YH,ZL,ZH)
int gyroXYZ[3];
int gyroOffset[3];
int gyroCS_Pin = 4;
int gyroSDO_Pin = 5;

/*  Accessory sensor  */
int ACC_ADDR = 0x53; // 7-bit address, change to 0x53 if SDO is High
byte ACC_READ_START = 0x32; //Start register address, total 6 bytes: (XL,XH,YL,YH,ZL,ZH)
int accXYZ[3];
int accOffset[3];

/* Compass sensor */
int COMP_ADDR = 0x3C >> 1;
byte COMP_READ_START = 0x03;
int compXYZ[3];   //Notice that it is XZY

/* Motor settings */
int motorPins[4];
int minPWM = 900; //电机控制的PWM最低幅值为0.9ms,最高幅值为2.1ms
int maxPWM_Delay = 1100; //电机控制的PWM范围,这里记录它们的差并留一点裕度
int ctrlPower = 0; //从遥控器发来的控制指令
double throttle = 0;  //油门范围,从0到1200,对应从minPWM到2100
int motorPowers[4];
int motorCtrl[8]; //电机控制的顺序和时间,值表示从0.9ms开始,(delayMs-PinIdx)*4

/* Ultrasonic sensor */
int trigPin = 3;
int echoPin = 2;  //对应的中断号是0
volatile double ultraT1 = 0;
volatile double ultraT2 = 0;
double distance = 0; //测量的离地距离,单位是厘米
double preDistance = 0;
double stablePower = 0;  //正好可以离地而起的马力(假设保持这个马力,可以匀速上升或下降)
int maxDistance  = 250; //离地可测量的最大高度
int targetDistance = 10; //目标离地高度
int ultraCount = 0;

/* For testing */
int reportType = 0;
int reportArr[3];
int reportT[3];

/* Read from control */
int buffer[6];   //0,1: 0xFF, 2: Power, 3: Log Type, 4: P_Value, 5: I_Value
int bufferIndex = 0;

/* PID console */
double P_Control, I_Control;
double balanceGyro = 0.99; //用于融合陀螺仪和加速度计的角度
double balanceAcc = 0.9; // 加速度传感器读数平均,用于消除抖动
double balanceSpeed = 0.05; // 用于陀螺仪读数平均(陀螺仪比较精确)
double balanceXY = 1, balanceX = 1, balanceY = 1;
double angleX, angleY, angleZ, speedX, speedY, accAngleX, accAngleY;
double testAngleX;
double compAngle;
boolean hasReadCompAngle = false;

/* Main control start */
void setup()
{
  motorPins[0] = 38;   //x1
  motorPins[1] = 43;   //x2
  motorPins[2] = 42;   //y1
  motorPins[3] = 39;   //y2
  for(int i = 0; i < 4; i++) pinMode(motorPins[i], OUTPUT);
  Serial.begin(9600);
  Wire.begin();
  pinMode(gyroCS_Pin, OUTPUT);
  pinMode(gyroSDO_Pin, OUTPUT);
  digitalWrite(gyroCS_Pin, 1); //enable I2C for GYRO
  digitalWrite(gyroSDO_Pin, 0); //set I2C address last bit
  setupGyroSensor(250);
  setupAccSensor();
  setupCompSensor();
  pinMode(trigPin, OUTPUT);
  attachInterrupt(0, blink, CHANGE);
  digitalWrite(trigPin, 0);
  // 从EEPROM中读取零点的校准值
  for(int i = 0; i < 3; i++) {
    gyroOffset[i] = EEPROM.read(2 * i + 1) << 8;
    gyroOffset[i] |= EEPROM.read(2 * i);

    accOffset[i] = EEPROM.read(7 + 2 * i) << 8;
    accOffset[i] |= EEPROM.read(6 + 2 * i);
  }
}

// 用于处理超声波传感器产生的中断,尽量减少在中断函数中处理逻辑
void blink()
{
  if(ultraT1 == 0) ultraT1 = micros();
  else ultraT2 = micros();
}

int loopIndex = 0;
void loop()
{
  if(reportType == 103) {
    /* 校准模式,在静止的平地上测量N次读数,记录零点误差
       校准隔一段时间做一次即可,例如季节变化,地点变化等 */
    gyroOffset[0] = gyroOffset[1] = gyroOffset[2] = 0;
    accOffset[0] = accOffset[1] = accOffset[2] = 0;
    int repeatTime = 100;
    for(int i = 0; i < repeatTime; i++) {
      // 单次读数大约6ms,校准读数总耗时在0.3秒左右
      readGyroSensor();
      readAccSensor();
      for(int j = 0; j < 3; j++) {
        gyroOffset[j] += gyroXYZ[j];
        accOffset[j] += accXYZ[j];
      }
      delay(10);
    }
    // 读数完毕写入EEPROM,每个字节需要3.3ms写入
    for(int i = 0; i < 3; i++) {
      gyroOffset[i] /= repeatTime;
      EEPROM.write(2 * i, (byte) (gyroOffset[i] & 0xFF));
      EEPROM.write(2 * i + 1, (byte) ((gyroOffset[i] >> 8) & 0xFF));
      accOffset[i] /= repeatTime;
      EEPROM.write(6 + 2 * i, (byte) (accOffset[i] & 0xFF));
      EEPROM.write(7 + 2 * i, (byte) (accOffset[i] >> 8 & 0xFF));
    }
    // 校准完成,恢复成普通模式
    reportType = 0;
  }
  /*
   * 采样率为350Hz,所以每个周期2.857ms。每个周期中都需要读一次陀螺仪和加速度计,
   * 用时大约0.7ms;然后剩余的时间分别用来做不同的事情,例如控制电机,计算角度等。
   * 然后进行角度姿态的融合,大约需要0.7ms。这部分放在控制电机的后面,因为0.9ms开始
   * 就需要控制电机,时间不够用,放在后面则刚好够用。
   * 电机的控制周期是20ms,也就是每7次循环修改一次电机功率
   */
  long t0 = micros(); //记录进入loop的时间
  if(loopIndex == 1) {
    for(int i = 0; i < 4; i++) digitalWrite(motorPins[i], HIGH);
  } else {
    // digitalWrite需要20微秒,为了读数时间均匀,其他情况统一做相应延时
    delayMicroseconds(20);
  }
  // 读取陀螺仪和加速度计的读数,前1.5秒等待传感器稳定
  if(t0 > stableTime) {
    readGyroSensor();
    readAccSensor();
  }
  if(connectToPc && loopIndex == ctrlPower % 7) reportT[0] = (int) (micros() - t0);
  double newSpeedX, newSpeedY, newAngleX, newAngleY, power, powerX, powerY, adjust;
  int loopCountLimit;
  switch(loopIndex) {
    case 0:
      // 这时候角度数值已就绪:angleX, angleY, angleZ, speedX, speedY
      // 但是需要修正45度安装的换算
      newSpeedX = (speedX + speedY) * 0.707;
      newSpeedY = (speedY - speedX) * 0.707;
      // 假设四轴翻转不会超过90度
      newAngleX = asin((sin(angleX) + sin(angleY)) * 0.707);
      newAngleY = asin((sin(angleY) - sin(angleX)) * 0.707);
      // 计算平衡参数
      adjust = 1;
      if(throttle > 350) adjust = 500.0 / throttle;
      balanceX = 1 - (newSpeedX * P_Control + newAngleX * I_Control) * adjust;
      balanceX = min(max(balanceX, 0), 2);
      balanceY = 1 - (newSpeedY * P_Control + newAngleY * I_Control) * adjust;
      balanceY = min(max(balanceY, 0), 2);
      // 计算油门值
      if(ctrlPower <= directCtrlRange) {
        if(throttle > ctrlPower * 5) {
          // 缓冲下降20ms下降0.8,1秒钟下降40
          throttle -= fastStep;
        } else {
          // 上升之前可以不考虑缓冲问题
          throttle = ctrlPower * 5;
        }
      } else {
        // 目标离地高度为ctrlPower - directCtrlRange + 10;
        targetDistance = ctrlPower - directCtrlRange + 10;
        if(stablePower == 0 && distance > preDistance + 2) {
          // 100ms内爬升了2cm以上,设置2主要是为了避免读数跳动误差
          stablePower = throttle;
        }
        if(stablePower == 0) {
          // 处于起飞状态
          throttle += fastStep;
        } else {
          // 可以认为已经离地
          double newThrottle = stablePower + (targetDistance - distance) * 0.11- (distance - preDistance) * 0.13;
          if(newThrottle > throttle + fastStep) throttle += fastStep;
          else if(newThrottle < throttle - fastStep) throttle -= fastStep;
          else throttle = newThrottle;
        }
        // 油门保护,不超过某个最大值(避免飞太快超出控制范围)
        if(throttle > maxThrottle) throttle = maxThrottle;
        // directCtrlRange优先控制权
        if(throttle < directCtrlRange * 5) throttle = directCtrlRange * 5;
      }
      // 计算电机功率
      power = throttle;
      powerX = power * balanceXY;
      powerY = power * 2 - powerX;
      motorPowers[0] = (int) (powerX * balanceX);
      motorPowers[1] = (int) (powerX * 2) - motorPowers[0];
      motorPowers[2] = (int) (powerY * balanceY);
      motorPowers[3] = (int) (powerY * 2) - motorPowers[2];
      motorCtrl[0] = 9999;
      for(int i = 0; i < 4; i++) {
        motorPowers[i] += 5 * i;
        if(motorPowers[i] > maxPWM_Delay) motorPowers[i] = maxPWM_Delay;
        if(motorPowers[i] < motorCtrl[0]) {
          motorCtrl[0] = motorPowers[i];
          motorCtrl[1] = i;
        }
      }
      // 排序, 设置motorCtrl
      for(int i = 1; i < 4; i++) {
        int pre = motorCtrl[i * 2 - 2];
        int preIdx = motorCtrl[i * 2 - 1];
        motorCtrl[i * 2] = 9999;
        for(int j = 0; j < 4; j++) {
          if(motorPowers[j] > pre || (motorPowers[j] == pre && j > preIdx)) {
            if(motorPowers[j] < motorCtrl[i * 2]) {
              motorCtrl[i * 2] = motorPowers[j];
              motorCtrl[i * 2 + 1] = j;
            }
          }
        }
      }
      // 计算delayMs
      for(int i = 0; i < 3; i++) {
        motorCtrl[6 - i * 2] -= (motorCtrl[4 - i * 2] + 5);
      }
      break;
    case 1:
      // 控制电机转速,首先运行到0.9ms
      delayMicroseconds(minPWM + t0 - micros());
      for(int i = 0; i < 4; i++) {
        // 根据间隔时间设置低电平
        if(motorCtrl[i * 2] > 0) delayMicroseconds(motorCtrl[i * 2]);
        digitalWrite(motorPins[motorCtrl[i * 2 + 1]], LOW);
      }
      break;
    case 2:
      // 电子指南针读数,这个不需要太精确,所以20ms读一次
      if(t0 > stableTime) {
        readCompSensor();
      }
      // 从串口获取遥控信息
      loopCountLimit = 18;
      while(Serial.available() && loopCountLimit--) {
        pushData(Serial.read());
      }
      break;
    case 3:
      // 发送各种报告
      if(connectToPc) {
        if(reportType == 1) writeBuffer(gyroOffset, 3);
        if(reportType == 2) writeBuffer(accOffset, 3);
        if(reportType == 3) writeBuffer(compXYZ, 3);
        //if(reportType == 1) writeNums(motorCtrl[0],motorCtrl[1],motorCtrl[2]);
        //if(reportType == 2) writeNums(motorCtrl[3],motorCtrl[4],motorCtrl[5]);
        //if(reportType == 3) writeNums(motorCtrl[6],motorPowers[2],motorPowers[3]);
        if(reportType == 4) writeBuffer(reportArr, 3);
        if(reportType == 5) writeNums((int)distance, (int)(speedX * 1000), motorPowers[0]);
        //if(reportType == 5) writeNums(reportArr[1], motorPowers[2], motorPowers[3]);
        if(reportType == 6) writeBuffer(reportT, 3);
      }
      break;
    case 4:
      // 测量离地距离,音速340m/s = 34cm/ms = 0.034cm/μs,测量的是来回距离,所以需要除以2
      if(ultraT1 > 0 && ultraT2 > ultraT1) {
        double newDistance = (ultraT2 - ultraT1) * 0.017;
        if(newDistance < 250) {
          preDistance = distance;
          if(distance > 100 && newDistance < 10) {
            // 高度突然变小的情况(从100以上变到小于10,可以认为是超出量程)
            distance = maxDistance;
          } else {
            distance = newDistance;
          }
        }
      }
      // 考虑到回音等问题,把测量的时间间隔增加到100ms,相当于17米的反射
      ultraCount++;
      if(ultraCount >= 5) {
        ultraT1 = 0;
        ultraT2 = 0;
        // 因为每次执行到这个循环是20ms,所以计数器增长到5时进行一次测量
        ultraCount = 0;
        // 用10μs的高电平发出触发信号
        digitalWrite(trigPin, 1);
        delayMicroseconds(10);
        digitalWrite(trigPin, 0);
      }
      break;
    default:
      // 其他扩展功能
      break;
  }
  if(connectToPc && loopIndex == ctrlPower % 7) reportT[1] = (int) (micros() - t0);
  // 计算角度angleX, angleY, speedX, speedY, accAngleX, accAngleY
  if(t0 > stableTime) {
    // 量程为250时,测量值单位是8.75 mdps/digi;
    // 正确:180/3.1415926*1000/8.75 = 6548.09
    // 错误:3754.94 = (65536/1000)*(180/3.1415926)
    double readSpeedX = (gyroXYZ[0] - gyroOffset[0]) / 6548.09; //单位:弧度/秒
    speedX = balanceSpeed * speedX + (1 - balanceSpeed) * readSpeedX;
    accAngleX = accAngleX * balanceAcc + atan2(accXYZ[0] - accOffset[0], accXYZ[2]) * (1 - balanceAcc);
    angleX = (angleX + readSpeedX * 0.002857) * balanceGyro + accAngleX * (1 - balanceGyro);
    testAngleX = testAngleX + readSpeedX * 0.002857;
    reportArr[0] = (int) (angleX * 1000);
    reportArr[1] = gyroXYZ[0] ;
    reportArr[2] = (int) (testAngleX * 1000);
    double readSpeedY = (gyroXYZ[1] - gyroOffset[1]) / 6548.09; //单位:弧度/秒
    speedY =  balanceSpeed * speedY + (1 - balanceSpeed) * readSpeedY;
    accAngleY = accAngleY * balanceAcc + atan2(accXYZ[1] - accOffset[1], accXYZ[2]) * (1 - balanceAcc);
    angleY = (angleY + readSpeedY * 0.002857) * balanceGyro + accAngleY * (1 - balanceGyro);
  }
  if(loopIndex == ctrlPower % 7) reportT[2] = (int) (micros() - t0);
  // 更新loopIndex
  loopIndex++;
  if(loopIndex >= 7) loopIndex = 0;
  // 截止到2.857ms
  t0 = micros() % 2857;
  delayMicroseconds(2817 - t0);
}

/* get data from bluetooth devices */
void pushData(byte data)
{
  //验证是否数据的开始
  if (bufferIndex < 1 && data != (byte)0xFF) {
    bufferIndex = 0;
  } else {
    buffer[bufferIndex] = data;
    if (bufferIndex == 5) {
      setParams();
    }
    bufferIndex = (bufferIndex + 1) % 6;
  }
}

void setParams()
{
  // 因为超声波中断可能导致数据丢失,所以需要额外的一个字节用于校验
  if(buffer[1] ^ buffer[2] ^ buffer[3] ^ buffer[4] == buffer[5]) {
    ctrlPower = buffer[1];
    reportType = buffer[2];
    //P_Control = (20.0 + buffer[3] / 4.0) / 200.0;
    //I_Control = (20.0 + buffer[4] / 4.0) / 200.0;
    P_Control = buffer[3] / 100.0;
    I_Control = buffer[4] / 100.0;
    if(reportType == 100 && !connectToPc) {
      Serial.end();
      connectToPc = true;
      Serial.begin(115200);
    }
    if(reportType == 101) {
      // 设置为上拉绳调试模式
      directCtrlRange = maxThrottle / 5;
    }
    if(reportType == 102) {
      // 设置为下拉绳调试模式
      directCtrlRange = 40;
    }
    //if(reportType == 103) {} //校准模式
  }
}

/*****************************************************************************/

void writeBuffer(int arr[], int len) {
  byte buff[len * 2 + 2];
  buff[0] = 0xFF;
  buff[1] = 0xFF;
  for(int i = 0; i < len; i++) {
    buff[i * 2 + 2] = (byte) (arr[i] >> 8 & 0xFF);
    buff[i * 2 + 3] = (byte) (arr[i] & 0xFF);
  }
  Serial.write(buff, len * 2 + 2);
}

void writeNums(int n1, int n2, int n3) {
   byte buff[8];
   buff[0] = 0xFF;
   buff[1] = 0xFF;
   buff[2] = (byte) (n1 >> 8 & 0xFF);
   buff[3] = (byte) (n1 & 0xFF);
   buff[4] = (byte) (n2 >> 8 & 0xFF);
   buff[5] = (byte) (n2 & 0xFF);
   buff[6] = (byte) (n3 >> 8 & 0xFF);
   buff[7] = (byte) (n3 & 0xFF);
   Serial.write(buff, 8);
}

/*****************************************************************************/

void writeRegister(byte deviceAddress, byte address, byte val) {
    Wire.beginTransmission(deviceAddress); // start transmission to device
    Wire.send(address);       // send register address
    Wire.send(val);         // send value to write
    Wire.endTransmission();     // end transmission
}

byte readRegister(int deviceAddress, byte address){
    Wire.beginTransmission(deviceAddress);
    Wire.send(address);
    Wire.endTransmission();
    Wire.requestFrom(deviceAddress, 1);
    while(!Wire.available()) {}
    return Wire.receive();
}

/*****************************************************************************/

int setupGyroSensor(int scale){
  byte GYRO_CTRL_REG1 = 0x20;
  byte GYRO_CTRL_REG2 = 0x21;
  byte GYRO_CTRL_REG3 = 0x22;
  byte GYRO_CTRL_REG4 = 0x23;
  byte GYRO_CTRL_REG5 = 0x24;
  writeRegister(GYRO_ADDR, GYRO_CTRL_REG1, 0x0f);
  writeRegister(GYRO_ADDR, GYRO_CTRL_REG2, 0x00);
  writeRegister(GYRO_ADDR, GYRO_CTRL_REG3, 0x08);
  if(scale == 250){
    writeRegister(GYRO_ADDR, GYRO_CTRL_REG4, 0x00);
  }else if(scale == 500){
    writeRegister(GYRO_ADDR, GYRO_CTRL_REG4, 0x10);
  }else{
    writeRegister(GYRO_ADDR, GYRO_CTRL_REG4, 0x30);
  }
  writeRegister(GYRO_ADDR, GYRO_CTRL_REG5, 0x00);
}

void readGyroSensor(){
    Wire.beginTransmission(GYRO_ADDR);
    Wire.send(GYRO_READ_START);
    Wire.endTransmission();
    Wire.requestFrom(GYRO_ADDR, 6);
    for(int i = 0; i < 3; i++) {
      while(!Wire.available()) {}
      gyroXYZ[i] = Wire.receive();  //Low byte
      while(!Wire.available()) {}
      gyroXYZ[i] |= (Wire.receive() << 8); //High byte
    }
}

/*****************************************************************************/
void readAccSensor(){
    Wire.beginTransmission(ACC_ADDR);
    Wire.send(ACC_READ_START);
    Wire.endTransmission();
    Wire.requestFrom(ACC_ADDR, 6);
    for(int i = 0; i < 3; i++) {
      while(!Wire.available()) {}
      accXYZ[i] = Wire.receive();  //Low byte
      while(!Wire.available()) {}
      accXYZ[i] |= (Wire.receive() << 8); //High byte
    }
}

int setupAccSensor(){
  writeRegister(ACC_ADDR, 0x2D, 0x28);  //Power control
}

/*****************************************************************************/

void readCompSensor(){
    Wire.beginTransmission(COMP_ADDR);
    Wire.send(COMP_READ_START);
    Wire.endTransmission();
    Wire.requestFrom(COMP_ADDR, 6);
    for(int i = 0; i < 3; i++) {
      while(!Wire.available()) {}
      compXYZ[i] = Wire.receive();  //High byte
      while(!Wire.available()) {}
      compXYZ[i] = Wire.receive() | (compXYZ[i] << 8); //Low byte
    }
    // Switch xzy to xyz
    int temp = compXYZ[1];
    compXYZ[1] = compXYZ[2];
    compXYZ[2] = temp;
}

int setupCompSensor() {
  writeRegister(COMP_ADDR, 0x00, 0x70); //Mod setting
  writeRegister(COMP_ADDR, 0x02, 0x00); //Mod setting
}

/*****************************************************************************/

四轴飞行器第二次试飞小结

春节期间进行了第二次不拴绳的试飞。总体来说,革命尚未成功,同志仍需努力……

和第一次相比,主要做了以下变化:

1. 给油门控制添加了一点渐变处理,当不小心把油门推高或者降到0的时候,飞行器不会猛升猛降,油门会较为平稳的变化。
好的飞控不会有这个功能,因为控制比较平稳,尤其是玩暴力飞行的朋友,肯定不能有延时。这里主要是为了在调试阶段不要损伤器件。

2. 添加了超声波测距传感器,把离地距离限制在1.5米以内,超出这个高度后,油门就会适当下降,这个主要是为了避免飞出控制范围和直接坠落。

3. 把采样频率从原来的50Hz提高到了350Hz,本来想提高到400Hz,但是某些情况下单个周期里的运算时间会超过2.5ms一点儿,所以最后改为350Hz。

4. 增加了从上面拴绳和下面拴绳两种调试方法,主要是为了调试PID时使用。

这是加了超声波大眼睛的四轴飞行器:

四轴飞行器上的超声波测距传感器

四轴飞行器上的超声波测距传感器

飞行视频:

从飞行的状态看,超声波测距起了一定的作用,四轴没有像上次那样飞的太高和直线下降。但是起飞以后侧飞的非常严重,看上去像是在很大尺度上摆动。做一个小小的总结,下个阶段准备在以下几个方面进行改进:

1. 减少震动,把控制板用弹簧或者橡胶垫稍微做点隔离,减少电机震动对传感器的影响
2. 校准螺旋桨,刚刚知道我买的螺旋桨(5元一对)平衡性能不是很好,不校准的话震动很大。建议的校准可以用一根细的针穿过中心轴,看看不会向一侧偏。轻的一边用透明胶带绕几圈配重。但是不知道透明胶带会不会影响气动性能
3. 没有做误差标定,就是把四轴放在平地上,在启动电机前记录平地时的各种读数
4. 除了1和2中的物理减震外,还应该做软件减震,按老薛的建议,打算把平均滤波改为卡曼滤波
5. 挂绳的试飞和无绳的试飞并不完全相同,挂绳时有垂直或水平的作用力,会辅助四轴平衡。所以这个四轴在挂绳时有时表现的还凑合,一到实战就横着飞
6. 初期犯了个经验主义错误,我以为气压计只能测量较大尺度上的高度,毕竟上下几米气压几乎没有变化;另外也担心在快速上升下降的过程中,气压会受到风速的影响。但是通过网友们的实战看来,气压计的高度估计可以精确到0.25米左右,并且在高速飞行时的表现也还不错。真是小看了当前的电子设备,这个在下一步可以考虑加上。

另外还有一个小小的经验可以分享:刚开始挂绳时,发现挂绳也不安全。当四轴上升的时候,会把挂绳吸进螺旋桨,结果叮叮咣咣,你懂的……
后来找了一些叫做易拉宝的东西,本来是用来拴钥匙门卡的,自重小而且伸缩性挺好,绳子也挺结实。四轴上升下降的时候,绳子也跟着伸缩,这样就不怕螺旋桨卷绳子啦。

用可伸缩的挂绳来测试四轴

用可伸缩的挂绳来测试四轴

四轴飞行器攻略2:器件介绍(下)

首先说下现在的进度:目前已经测试了用Arduino读取陀螺仪传感器,加速度传感器,电子罗盘的读数;并且可以用Android手机通过蓝牙向Arduino发送指令;焊了一个小小的扩展板,并完成了初步的组装。接下来的主要工作是传感器读数的分析和姿态平衡算法的研究。发个近照:

组装完毕,等待编写程序的四轴飞行器

组装完毕,等待编写程序的四轴飞行器

圈妈学过一点心理学,忧心忡忡的对我说“据说提前说出目标的事情就不容易成功”,所以她不建议我还没做好就发攻略。但是攻略这种东西,想写的时候没写下来,过几天就忘了。我只好用男生追mm的例子做了解释:闷骚型的男生如果被人知道了暗恋对象,有的人变得更紧张,见到mm就躲;有的人则是硬着头皮展开攻势,最终满载而归。所以公布目标没啥问题,关键是看你把它当成动力还是压力。

言归正传,下面继续介绍四轴飞行器的器件们。

陀螺仪传感器
陀螺仪传感器又称角速度传感器,它的作用是测量物体运动的角速度(废话啊)。从测量维度分,陀螺仪传感器有单轴的,双轴的和三轴的;以传输方式分,又有数字型的和模拟型的。
对于四轴飞行器,最好使用三轴传感器。因为水平方向上就已经需要测量x,y两个维度的角速度,z轴方向上的自转如果有电子指南针的话,倒是也可以省略。所以很流行的另一种方案是用两个垂直安装的单轴传感器(ENC-03MB 村田角速度传感器),价格会便宜好多。
至于数字型的还是模拟型的,初学者开始会觉得模拟型的好用,它会引出3条线,分别输出0~5V之间的电压,表示xyz三轴的加速度值。
看上去似乎相当简单,用analogRead一下就好了。
从我的实践经验看来,它的问题在于线太多!陀螺仪需要3根线,加速度又3根线,指南针再3根线….随着器件的增加,Arduino的模拟输入口完全不够用。
而数字型则简单了很多,以I2C总线为例,数据通信只需要2根线,而且很多个设备(最多支持256个设备)都是公用这两条线。反正看你愿意花力气在焊板子上还是写代码上了。对于我这样的IT民工来说,显然觉得敲代码比焊板子舒服很多,所以我最终选择的是L3G4200D三轴数字式陀螺仪传感器。

加速度传感器
加速度传感器又称重力感应器,它可以感知物体受到的重力。例如水平静止放置的重力感应器,xy方向上重力加速度都是0,z轴方向上的读数则是9.8;而倾斜放置的传感器,在xy分量上就可以读出所受的重力。和陀螺仪相比,加速度传感器测量的是绝对的“倾角”,而陀螺仪测量的是“倾角的变化速度”。
这些传感器的最终目标,就是让四轴飞行器保持平稳。有人可能觉得用重力感应器就足够了,因为它可以测量绝对角度。但是请注意的是,这个倾角必须是在物体静止或者匀速的情况下测量的,飞行器在运动过程中是测不准的。就好比闭眼蹦极的人,在失重的时候是不知道上下左右的。所以一般需要两个传感器配合,一个用来测变化量,一个用来修正误差。
加速度传感器同样分为数字型的和模拟型的,我看上的是ADXL345数字型三轴加速度传感器。

电子指南针
电子指南针也叫电子罗盘,也有人叫磁通传感器。它的原理跟指南针类似,通过测量某个截面内的磁通量大小来判断方向。它也可以是三轴的,其中垂直于南北极磁力线的那个截面会有最大的读数。这个指南针不是四轴飞行器必须的,因为平稳飞行靠前面两个传感器就够了。
但是试想一下这样的情况,我们遥控一个直升机的时候,可以看到机头的朝向,所以我们可以让他“前进/后退/左移/右移”。
而四轴飞行器是对称的,当你发出“前进”指令的时候,它并不知道你期望的是哪个方向的“前进”。
网上一种流行的方案是,给四轴飞行器的某条腿涂上鲜艳的颜色,作为“机头”,前进后退的参照系都是以它为准。但是这样就需要对遥控者有一定要求,需要能够迅速分辨方向。
我加上这个电子指南针,是希望它能和手机遥控器上的指南针对应起来,永远以遥控者的面向方向作为“前方”。当然这样会给算法带来更多的工作量,大家祝我好运吧 :)
我最终选择的芯片是HMC5883L三轴数字型电子指南针

其他还有一些零七八碎的东西,包括:
超声波测距传感器,用来测量离地高度;
蓝牙适配器,用来和手机通信;
洞洞板,用来组合这些传感器;
各种插针、排母、导线、开关;
机架,这个也可以自己做,现成的比较好看;

最后是某些网友非常关心的价格问题,下面的价格表仅供参考,最终还是请大家自己擦亮眼睛,货比三家。
必须提到的是一个小插曲,很多芯片的价格分为散件价格和模块价格。例如L3G4200D芯片,单买芯片据说30元就可以搞定;如果买现成的小电路板+所有电容电阻等配件,那么需要70元左右;如果这些小配件都帮你焊好的成品,大概需要160元左右。
看到这些价格之后,勤劳勇敢的圈妈大骂奸商,说要帮我焊上,省下这100块钱!结果散件回来以后我们都傻眼了,这是一种叫LGA的封装方式,花椒一样大的小芯片,肚子下面有十几根脚。而且这些脚是在芯片肚子下面,不是伸出来的!

新手焊不了的小芯片

新手焊不了的小芯片

圈妈硬着头皮焊了几下,发现基本所有的脚都连在一起了,最后弄掉了焊盘,彻底报废。所以建议新手们,如果没有专业焊接工具的话,就不要尝试省这100块钱了。

价格表(仅供参考!)
格氏11.1V2200mA25C锂电   128
B6充电器     160
郎宇A2212电机   62×4
天行者20A电调  48×4
四轴机架     88
三轴陀螺仪模块L3G4200D       160
三轴加速度ADXL345模块     50
三轴磁阻HMC5883L模块      70
蓝牙模块JY-MCU          40
超声波测距            16