mlbo · 2020年03月13日

从零开始使用Arduino自定义物联网硬件

从面包板到制造开始你的开源物联网设备的开发之旅。
作者:iotsharer
首发:https://zhuanlan.zhihu.com/p/64160520

最近,我想为创客创建一个基于Arduino的低功耗物联网(IoT)设备,内置传感器可用于将传感器数据从任何位置传送到云端,并可能控制连接设备,如恒温器,灯,门锁和其他家庭自动化产品。在此过程中,我了解到创建一个新的IoT设备,从构思到原型再到最终产品,并不像我想象的那么简单,并且没有“随时可用”的开发设备。然而,通过弄清楚如何做到这一点,我创建了一个名为Siguino的新产品 ,一个开源的物联网电路板,我希望这将使其他人更容易,更快地创建他们自己的物联网产品。

Siguino基于Arduino Pro Mini的低功耗版本 ,具有板载传感器和天线,并使用单个电池供电。它还利用了Sigfox,一种用于将物联网设备连接到云端的低功耗广域网。

本文介绍了从一个非常凌乱的面包板(但仍在工作)原型到一个最终的、定制设计的印刷电路板(PCB)的各个阶段,其他人有望使用这些电路板。

1.面包板

和众多优秀的创客项目一样,我从一个概念电路开始。这涉及识别您希望设备具有的功能以及您将使用的组件。我想要我的设备:

  • 低功耗并基于Arduino(例如,使用ATmega328P芯片的Arduino Pro Mini )
  • 包括一个Sigfox芯片,以便它可以通过Sigfox网络发送消息。我选择WiSOL SFMR10芯片有两个原因:
  • 它是一个只发送而不是收发器的芯片,我没有选择双向通信
  • 有一个DevKit 可用(对于面包板和原型设计非常有用)
  • 有四个基本传感器:
  • 温度(SparkFun DS18B20)用于连接的恒温器
  • 用于连接灯的光电平(标准光电池)
  • 用于门打开/关闭的磁性检测“霍尔效应”,例如门是打开还是打开(DigiKey AH9246-W-7
  • 用于设备安全的运动检测、跌倒检测、周边运动检测等等。我试验了行程开关、水银开关等,但我认为加速度计(Adafruit LIS3DH)是创客的最佳选择,因为它开启了电路板的固有可能性。(注意,虽然原始芯片是低功耗的,但该组件的分接板不是低功耗的。)

结果是相当混乱(但功能齐全!)的组件集:

一切正常后,我花了一点时间用线路板跳线组装一个更整洁的版本:

2.编写Arduino代码

下一步是编写基本代码,让我的面包板设备完成我想要它做的事情。其中一些是标准的,并包含在每个组件的现有示例代码中。例如,使用DS18B20 测量温度的代码如下所示:

#include <DallasTemperature.h>
#include <OneWire.h>
​
//数据线插入Arduino上的端口2 
#define ONE_WIRE_BUS 2
//设置oneWire实例与任何OneWire设备通信(不仅仅是Maxim / Dallas温度IC )
OneWire oneWire(ONE_WIRE_BUS);
//将我们的oneWire引用传递给Dallas Temperature。
DallasTemperature temp_sensor(&oneWire);
​
void setup(){
  Serial.begin(9600);
  temp_sensor.begin();
​
  Serial.println("DS18B20 Temperature Test\n\n");
​
  delay(300);//让系统稳定
​
}//end "setup()"
​
void loop(){
​
  Serial.print("Requesting temperatures...");
  temp_sensor.requestTemperatures(); //发送命令获取温度
  
  Serial.print("Temperature is: ");
  float temp_reading = temp_sensor.getTempCByIndex(0);
  Serial.println(temp_reading);
​
  delay(1000);
}// end loop()

有许多第三方库提供Arduino Pro Mini的低功耗使用选项。我选择了GitHub上Rocket Scream库。家庭自动化社区Andreas Rohner提供了有关修改Arduino Pro Mini以实现低功耗的良好信息。此项目的示例用法是:

// **** INCLUDES *****
​
#include“LowPower.h” 
​
void setup()
​
{ 
​
//此库无需设置
​
} 
​
void loop()
​
{ 
​
//使用ADC进入8秒的断电状态和BOD模块禁用
​
LowPower.powerDown(SLEEP_8S,ADC_OFF,BOD_OFF); 
​
//在这里做点什么
​
//示例:读取传感器,数据记录,数据传输。
​
}

WiSOL Sigfox芯片可以使用标准AT命令进行通信(产品数据表中包含基本示例)。对于这个项目,我只需要两个功能:

  • 发送消息: 我将底层的AT命令封装起来,以便更容易发送命令,例如,测试设备和消息:
String send_at_command(String command, int wait_time){
  altSerial.println(command);
  delay(wait_time);
  return recv_from_sigfox();
}
​
void test_sigfox_chip(){
  Serial.println("Sigfox Comms Test\n\n");
  altSerial.begin(9600);
  delay(300);//Let system settle
  
  Serial.println("Check awake with AT Command...");
  chip_response = send_at_command("AT", 50);  
  Serial.println("Got reponse from sigfox module: " + chip_response);
  
  Serial.println("Sending comms test...");
  chip_response = send_at_command("AT", 50);  
  Serial.println("Comms test reponse from sigfox module: " + chip_response);
​
  chip_response = send_at_command("AT$I=10", 50);  
  Serial.println("Dev ID reponse from sigfox module: " + chip_response);
​
  chip_response = send_at_command("AT$I=11", 50);  
  Serial.println("PAC Code reponse from sigfox module: " + chip_response);
}
​
//message send
chip_response = send_at_command("AT$SF=" + hex_bits, 10000);
Serial.println("Reponse from sigfox module: " + chip_response);
  • 进入低功耗(睡眠)模式: 我选择了基本的睡眠模式,虽然这个芯片也支持“深度睡眠”选项。从1.5μA到<1μA,似乎没有什么必要,因为1.5μA的静态电流消耗超出了我的目的。睡眠/唤醒周期代码如下所示:
//Sigfox sleep mode enabled via AT$P=1 command
// to wake need to set UART port low (see AX-SIGFOX-MODS-D.PDF for further details)
void set_sigfox_sleep(bool go_sleep){
  String chip_response;
  if (go_sleep){
    //send go sleep AT command
    chip_response = send_at_command("AT$P=1", 100);  
    Serial.println("Set sleep response: " + chip_response);
  }else{
    //wake up sigfox chip
    altSerial.end();
    pinMode(TX_PIN, OUTPUT);
    digitalWrite(TX_PIN, LOW); 
    delay(100);
    altSerial.begin(9600);    
  }
}

位组合:

我决定对 Sigfox消息使用 位组合 ; 由于Sigfox消息最多为12个字节,因此最好将尽可能多的数据压缩到每个消息中。例如,假设温度传感器返回的温度将在-40到+80摄氏度之间浮动。C ++中的float使用4个字节的内存,但如果没有必要,您不希望使用12字节消息的4个字节发送数字。通常,您只需要知道一个半精度的温度值,这样就可以将整个可能的温度范围压缩为8位(1个字节),因为通过将-40到+80的范围限制为一半-degree增量,您只有240个可能的值,如下所示:

0b00000000 [0] = -40 
0b00000001 [1] = -39.5 
0b00000010 [2] = -39 
... 
0b11101111 [239] = 79.5 
0b11110000 [240] = 80

为了节省更多空间,我将范围限制在-10到+50摄氏度,精度为半度,温度需要7位,光照水平需要5位(0到1,000),打开/关闭需要1位或者设备移动,4位消息序列号,所以我可以发现任何错过的消息。所以,我的基本传感器只需要使用我12字节可用消息空间中的18位,如下所示:

我调整了一位组合功能,它们将获取所有传感器数据,以及我想要用于每个传感器数据的位数,并将它们打包成一个12字节的值:

#ifndef BITPACKER_H_INCLUDED
#define BITPACKER_H_INCLUDED
​
#include <stdint.h>
​
#define BIT(n)                  ( 1UL<<(n) ) //UL = unsigned long, forces chip to use 32bit int not 16
​
#define BIT_SET(y, mask)        ( y |=  (mask) )
#define BIT_CLEAR(y, mask)      ( y &= ~(mask) )
#define BIT_FLIP(y, mask)       ( y ^=  (mask) )
​
/*
        Set bits        Clear bits      Flip bits
y        0x0011          0x0011          0x0011
mask     0x0101 |        0x0101 &~       0x0101 ^
        ---------       ----------      ---------
result   0x0111          0x0010          0x0110
*/
​
//! Create a bitmask of length \a len.
#define BIT_MASK(len)           ( BIT(len)-1 )
​
//! Create a bitfield mask of length \a starting at bit \a start.
#define BF_MASK(start, len)     ( BIT_MASK(len)<<(start) )
​
//! Prepare a bitmask for insertion or combining.
#define BF_PREP(x, start, len)  ( ((x)&BIT_MASK(len)) << (start) )
​
//! Extract a bitfield of length \a len starting at bit \a start from \a y.
#define BF_GET(y, start, len)   ( ((y)>>(start)) & BIT_MASK(len) )
​
//! Insert a new bitfield value \a x into \a y.
#define BF_SET(y, x, start, len)    \
    ( y= ((y) &~ BF_MASK(start, len)) | BF_PREP(x, start, len) )
​
namespace BitPacker {
    static uint32_t get_packed_message_32(unsigned int values[], unsigned int bits_used[], int num_vals){
        uint32_t retval = 0x0;
        int j = 0;
        for (int i=0;i<num_vals;i++){
            BF_SET(retval, values[i], j, j + bits_used[i]);
            j += bits_used[i];
        }
        return retval;
    }
​
    static uint64_t get_packed_message_64(unsigned int values[], unsigned int bits_used[], int num_vals){
        uint64_t retval = 0x0;
        int j = 0;
        for (int i=0;i<num_vals;i++){
            BF_SET(retval, values[i], j, j + bits_used[i]);
            j += bits_used[i];
        }
        return retval;
    }
​
}
#endif // BITPACKER_H_INCLUDED

3.原型电路

在为您的设备定制PCB电路之前,有必要确定一个更小,更整洁的原型电路。我选择了 这个电路的 条形板版本。最终结果应该是电路的更整洁和更紧凑的版本,这对于帮助修整最终的PCB设计非常有用。(这很重要,因为根据经验,PCB越大,成本越高。)它还可以很好地了解最终产品可能需要哪种外壳。

我还使用了Fritzing,这是一款用于布置条形板或Veroboard电路的软件。它允许您设计可以在条形板上复制的虚拟电路。我的原型电路在Fritzing看起来像这样:

这导致了这个实际(工作)电路:

4.设计和印刷PCB

为了设计我的PCB,我使用了 Autodesk Eagle,这是一款可以免费用于小板(<80cm)的软件,并且有许多组件库(包括良好的第三方库,例如所有SparkFun和AdaFruit组件)。

我从这些SparkFun教程中学到了我需要了解的所有关于Eagle的知识:

根据我的经验,我想提出一些建议:

  • 经常保存!
  • 无论多么小,每次更改后都要进行设计规则检查(和重新检查)。地面浇筑后重新检查,即使更改_不应_影响地面浇筑。(通过在铜板的外层填充开放的未使用区域,通常在板的外层上,然后将铜填充物与缝合过连接到地面来创建铜接地浇注。浇注的地面在缺少固体参考平面的双层板上是有用的。它可以减少由于电容耦合引起的串扰。)
  • 当使用非常小的元件(例如,FPGA表面贴装元件)进行布线时,尽量不要在元件下方留下任何孔,以避免在没有专业工具(例如,焊料回流焊)的情况下手工焊接或表面贴装元件进行原型测试时出现问题,取放机械等)。很难确保手工焊接或焊膏不会位于元件下方并流入下方的布线孔(您无法看到的位置)。当路由这些组件中的一些有多小时,也很容易忘记。

换句话说,不要这样做:

而是这样做:

  • 对于较大的元件,尽量不要在元件支脚或焊盘附近设置布线孔,原因与上述相同。

我最终完全布线的电路板布局如下所示:

5.焊接表面贴装元件

在这个项目开始时,我不知道的是如何构建包含表面贴装元件(SMC)的原型。使用电镀通孔(PTH)元件进行原型制作(例如面包板)要容易得多,但是最终产品不会选择PTH元件,因为SMC更小更整洁。

当您使用理想的SMC组件设计PCB布局,打印出来并想要将它们放在一起并进行测试时会发生什么,但是您没有任何表面贴装机器(如拾取和放置机器或焊料回流炉)?你可以建立自己的回流炉,但如果你正在建造自己的回路,我认为这种偏离的重点是有点耗时。并且,它几乎是不必要的,因为您可以通过足够的练习手工焊接几乎所有的SMC,并且您可以使用相对便宜的焊接气枪来使工作更容易。

我使用 EEVBlog YouTube频道来学习SMC焊接的基础知识,最后我将所有东西都焊接到0402组件(如果你呼吸太大,你会失去它们!)。有关上下文,请参阅此组件大小比较图表:

我不建议在电路中使用0402组件。(我别无选择,因为它们是天线下的射频网络的一部分,更大的组件可能会影响天线性能。)事实上,0602组件也非常小而且很难焊接,但有一点练习它是非常可行的。我建议第一批订购额外的PCB,纯粹用于焊接实践,因为你很可能会弄乱你的第一次尝试。

所需工具包括:

  • 烙铁: 绝对值得为优质烙铁付出更多。我从一个廉价的开始,几个星期后,我放弃了它,用了一个更好的,一切都更容易。
  • 热风焊枪:我还买了一个 热风枪 ; 虽然它已被证明比我希望的更难使用(获得正确的气压,所以你不会从板上吹掉小元件是一种艺术形式!),它已经焊接了一些较小的(VFLGA)封装集成电路,就像LIS3DH一样,更容易。(我甚至不确定如何单独用烙铁做这件事,虽然显然它是可能的。)当你搞砸了东西时,它也可以很容易地移除部件。
  • 镊子:质量好,非常精细的镊子组对于拾取非常小的组件至关重要。
  • 放大镜/放大镜:为了放大焊接以检查焊接不良,焊接桥,斑点,漏针等,我发现了一个珠宝商的放大镜,最好带有内置灯,非常实用。

6.测量功耗

功耗测量是一个非常困难的过程,但是非常重要。我希望我的设备能够实现超低功耗,因此可以使用小型电池工作一年(即900mAh CR2)。这意味着确保静态电流(恒定电流消耗)尽可能小,降至低μA范围,同时考虑到消息发送期间偶尔会有更高的电流消耗。尽管有许多方法可以评估电路的电流要求,但大多数方法的极低分辨率都很差。手动机制,例如连接在电源线上的电流表,使用起来很麻烦,并且只给出了在特定时间使用了多少电流的快照(在某些情况下,对于任何可靠的测量都没有足够快的反应速度)。

在我尝试过的各种选项中,唯一最终使用的选项是Nordic Semiconductor 的 Power Profiler Kit(功率分析器套件)。它不是太贵(它和基板都大约100美元)而且效果非常好。(我唯一不满的是,我无法让它在Linux上可靠地工作,即使它是一个Python程序,所以我不得不在Windows下使用它。)

PPK不仅可以生成一个非常低分辨率(< 1a)的功耗恒定视图,还可以生成一个运行时间窗的平均值(这正是我计算电池寿命所需要的):

7.对ATmega引导程序进行编程

您可能焊接到PCB上的原始ATmega芯片可能无法使用正确的保险丝设置(参见下文)或使用编程的引导加载程序进行硬编码,因此您可能需要对其进行配置,以便正确操作电路板。对于第一次接触印刷电路板的设计者/建造者来说,这十分令人困惑!

在设置从芯片供应商处收到的原始ATmega芯片时,有三个主要任务需要解决。(注意:细节参考ATmega328P,但其中大部分也适用于ATmega系列中的其他部分):

保险丝设置:

熔丝是非易失性位,定义了芯片行为方式的许多可编程方面。有三个熔丝字节,每个字节有8位:低字节,高字节和扩展字节。例如,它们控制什么类型的时钟驱动芯片或控制掉电检测器(BOD)触发的电压。BOD在设定电压下停止代码执行,以避免在功率过低时不可靠的操作。

_默认值_在工厂提供的芯片中设置。这些可能适合芯片的预期用途。但如果没有,他们需要改变。这是通过SPI总线使用合适的接口完成的,例如Ardiuno Uno板。这里这里有一些很好的指导。

引导程序:

运行项目应用程序所需的代码需要加载到芯片中。通常使用FTDI头设备通过USB将芯片连接到PC上。在这种情况下,芯片需要安装引导加载程序以便于此操作。实际上,这是一个加载程序的程序,但是它是通过使用适当接口的SPI总线加载的。

对于这个项目,我使用一个单独的Arduino UNO来启动我的ATmega芯片,如下所示:

  1. 对于引导加载程序,请使用Nick Gammon 的 ATmega芯片编程器
  2. 下载ZIP文件
  3. 解压ATmega\_Board\_Programmer文件夹(例如,到Arduino IDE Libraries目录)
  4. 打开ATmega\_Board\_Programmer程序(译者注:.ino文件)
  5. 将标准Arduino Uno连接到PC
  6. 将板设置为“Arduino / Genuino Uno”并设置正确的端口
  7. 上传ATmega\_board\_programmer程序
  8. 断开Uno与PC的连接并将其连接到目标芯片,如下所示:
    UnoTargetD10ResetD11MOSID12MISOD13SCKGndGnd+5VVcc9.将Uno重新连接到PC - >设置端口 - >运行串行监视器115200波特
    10.引导加载程序应立即运行并在串行监视器窗口中显示结果; 按照串行窗口中的说明进行操作(例如,“L”表示加载引导加载程序)
    11.请注意,引导加载程序会将芯片设置为使用内部8MHz时钟; 如果你有一个外部晶振,这可以修改(见程序中的注释)

程序代码加载:

一旦芯片安装了引导加载程序,就可以通过FTDI接口加载程序代码。在开发人员PC上运行的Arduino IDE可以通过该接口将应用程序代码直接加载到芯片。

8.打印PCB,购买组件,制造和组装

要从面包板转向批量生产,您需要各种资源(译者注:国内在某宝上都可以购买):

  • 硬件组件:对于电路板,你需要各种元件,如各种电阻,电容,传感器,集成电路等。你可以在像亚马逊这样的主流网站上找到一些,但我推荐一些特定于硬件的网站作为更好的选择。我主要使用 DigiKey ; MouserFarnell 也很好。
  • PCB打印: 一旦您设计了PCB并创建了指定打印方式的Gerber文件,您将需要找到一家公司进行打印。SparkFun 在“挑选PCB制造商”下提出了一些值得一看的建议。我使用 Multi-CB 并发现它们非常好,及时且价格具有竞争力,但我不得不通过银行转账支付,因为他们不提供在线支付选项。
  • PCB制造: 一旦您的PCB完全设计,您的组件就会被购买并手工焊接,并且您的最后一个原型进行了测试,现在是时候批量生产了。我从PCBCart得到了一个非常合理的报价,其中包括汇编和ATmega芯片编程。由于我还没有制造电路板,我不能评论它们的质量或服务。

9.做后端开发

所以你已经构建了你的设备,它在Sigfox网络上发送消息(主要是在Sigfox服务器上),现在该怎么办?您将如何处理这些消息以及如何处理它们

Sigfox回调

首先要做的是让Sigfox服务器将您设备收到的任何消息转发给您控制的Web服务器或服务。关于如何实现这一点,Sigfox系统有很多选项,但我认为最简单的方法是构建自己的RESTful Web服务(如下所述),并让Sigfox服务器使用消息数据向您的新服务发出HTTP请求。这可以在Sigfox后端通过使用设备的回调机制来完成,您可以根据需要从包括原始消息数据在内的可用变量列表中指定发布的变量或URL参数:

RESTFul Web服务

RESTful Web服务是现代API接口,在Web上无处不在。有很多方法可以创建它们,但我决定先使用Go编程语言,因为它是我想要学习的语言,其次,因为它很容易通过Docker进行部署。Go中Web服务的基本结构(保存到MongoDB数据库)如下所示:

// Handler for HTTP Post - "/sensordata"
// Register new sensor data
func NewSensorData(w http.ResponseWriter, r *http.Request) {
    var dataResource SensorDataResource
    // Decode the incoming Task json
    err := json.NewDecoder(r.Body).Decode(&dataResource)
    if err != nil {
        common.DisplayAppError(
            w,
            err,
            "Invalid Sensor Data format",
            500,
        )
        return
    }
    sensorData := &dataResource.Data
    context := NewContext()
    defer context.Close()
    c := context.DbCollection("SensorData")
    repo := &db.SensorDataRepository{c}
    // Insert a sensor data document
    repo.Create(sensorData)
    if j, err := json.Marshal(SensorDataResource{Data: *sensorData}); err != nil {
        common.DisplayAppError(
            w,
            err,
            "An unexpected error has occurred",
            500,
        )
        return
    } else {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        w.Write(j)
    }
}

您可能构建的用于从Sigfox服务器基本处理原始数据的大多数简单Web服务都具有类似的结构。

我发现对Sigfox消息解析特别有用的东西是比特解包(为我之前在Arduino代码中使用比特拆包来将尽可能多的数据压缩到我的Sigfox消息中)。用于解压缩数据的相应Go代码如下所示:

func bit(n uint64) uint64 {
    return 1<<n
} 
​
func bit_set(y uint64, mask uint64) uint64 {
    return y | mask
}
​
func bit_clear(y uint64, mask uint64) uint64 {
    return y & ^mask
}
​
func bit_flip(y uint64, mask uint64) uint64 {
    return y ^ mask
}
​
func bit_mask(len uint64) uint64 {
    return bit(len) - 1
}
​
func Bf_mask(start uint64, len uint64) uint64 {
    return bit_mask(len) << start
}
​
func Bf_prep(x uint64, start uint64, len uint64) uint64 {
    return (x & bit_mask(len)) << start
}
​
func Bf_get(y uint64, start uint64, len uint64) uint64 {
    return (y>>start) & bit_mask(len)
}
​
func Bf_set(y uint64, x uint64, start uint64, len uint64) uint64 {
    return (y & ^Bf_mask(start, len)) | Bf_prep(x, start, len)
}

IFTTT集成

最后,就使您的设备完成数据日志记录之外的功能而言,将其与其他设备或生态系统集成的最简单方法可能是通过If This That That(译者注:IFTTT连接你所有的应用和设备。),它是许多不同API和系统的融合。一旦您将设备连接到IFTTT,您可以访问现有的后续行动。例如,“如果[您的设备发送X],然后[发送电子邮件给Y],或[让Alexa说Y],或[打开Y室中的飞利浦Hue灯],”或者其他无数的选项。

后续规划

Siguino项目的下一步是为它开发3D外壳,通过Sigfox设备认证计划,调整天线以充分利用它,以及资助和组织设备的第一次生产运行。

由于我这个项目的主要目的是了解技术,我已经在GitHub对所有软件代码和硬件开源 。如果您有任何疑问或找到有价值的信息,请在评论中告诉我。


关于作者
本文作者Scott Tattersall是一名自由软件顾问。擅长将最新技术(Blockchain,Docker等)的知识与传统和现代数据库(关系,非关系,文档存储,列数据库等)和云部署,架构和基础架构(Docker,Kubernetes,AWS)的丰富经验相结合。


原文链接:How to build custom IoT hardware with Arduino

定期更新,更多AIoT相关技术知识请关注动手学AIoT专栏。
推荐阅读
关注数
1215
内容数
19
关于AIoT相关的技术文章以及相关资源。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息