最近,我想为创客创建一个基于 Arduino 的低功耗物联网 (IoT) 设备,该设备具有内置传感器,可用于将传感器数据从任何位置传送到云端,并可能控制连接的设备,例如恒温器、灯、门锁和其他家庭自动化产品。 在此过程中,我了解到,从想法到原型再到最终产品,创建一个新的物联网设备并不像我想象的那么简单,而且没有“即用型”的开发设备可供入门。 然而,通过弄清楚如何做到这一点,我创建了一个名为 Siguino 的新产品,这是一个 开源 物联网电路板,我希望它能让其他人更容易、更快速地创建他们自己的物联网产品。
Siguino 基于 Arduino Pro Mini 的低功耗版本,该版本具有板载传感器和天线,并由单节电池供电。 它还利用了 Sigfox,这是一种旨在将物联网设备连接到云端的低功耗广域网络。
本文介绍了从非常混乱的面包板(但可工作)原型到最终定制设计的印刷电路板 (PCB) 的各个阶段,希望其他人能够使用它。

opensource.com
原型 1 与最终原型对比
1. 面包板原型
正如所有优秀的创客项目开始时一样,我首先通过 面包板 搭建了一个概念电路。 这涉及到确定您希望设备具有的功能以及您将使用的组件。 我希望我的设备能够:
- 低功耗且基于 Arduino(例如,Arduino Pro Mini,它使用 ATmega328P 芯片)
- 包含 Sigfox 芯片,以便可以通过 Sigfox 网络发送消息。 我选择 WiSOL SFMR10 芯片的原因有两个:
- 它是一个仅发送芯片,而不是收发器,我没有双向通信的计划
- 它有一个 DevKit 可用(对于面包板原型和原型设计非常有用)
- 具有四个基本传感器:
- 温度(SparkFun DS18B20),用于连接的恒温器
- 光照强度(标准光电池),用于连接的灯
- 磁性检测“霍尔效应”,用于门打开/关闭,例如,门是否已打开或保持打开状态(DigiKey AH9246-W-7)
- 运动检测,用于设备安全、跌倒检测、周界运动检测等等。 我尝试了行程开关、水银开关等等,但最终决定加速度计 (Adafruit LIS3DH) 是创客的最佳选择,因为它开启了电路板的本机可能性。(请注意,该组件的分线板 不是低功耗的,尽管原始芯片是。)
结果是一组相当混乱(但功能齐全!)的组件

opensource.com
一旦一切正常工作,我花了一些时间使用面包板跳线整理出一个更整洁的版本

opensource.com
2. 编写 Arduino 代码
下一步是编写基本代码,使我的面包板设备能够完成我想要它做的事情。 其中一些是标准的,并包含在每个组件的现有示例代码中。 例如,使用 DS18B20 测量温度 的代码如下所示:
#include <DallasTemperature.h>
#include <OneWire.h>
// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 2
// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);
// Pass our oneWire reference to Dallas Temperature.
DallasTemperature temp_sensor(&oneWire);
void setup(){
Serial.begin(9600);
temp_sensor.begin();
Serial.println("DS18B20 Temperature Test\n\n");
delay(300);//Let system settle
}//end "setup()"
void loop(){
Serial.print("Requesting temperatures...");
temp_sensor.requestTemperatures(); // Send the command to get temperatures
Serial.print("Temperature is: ");
float temp_reading = temp_sensor.getTempCByIndex(0);
Serial.println(temp_reading);
delay(1000);
}// end loop()
有许多第三方库提供了 Arduino Pro Mini 低功耗使用的选项。 我选择了 Rocket Scream 库,该库在 GitHub 上可用。 家庭自动化社区 和 Andreas Rohner 提供了有关修改 Arduino Pro Mini 以降低功耗的良好信息。 该项目的示例用法将是:
// **** INCLUDES *****
#include "LowPower.h"
void setup()
{
// No setup is required for this library
}
void loop()
{
// Enter power down state for 8 s with ADC and BOD module disabled
LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
// Do something here
// Example: Read sensor, data logging, data transmission.
}
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++ 中的浮点数使用 4 个字节的内存,但是如果不需要,您不想占用 12 字节消息中的 4 个字节来发送一个数字。 通常,您只需要知道温度值精确到半度即可,这使您可以将整个可能的温度范围压缩到 8 位(1 字节)中,因为通过将 -40 到 +80 的范围限制为半度增量,您只有 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 位,像这样打包:

opensource.com
我改编了一组 位打包函数,这些函数将获取所有传感器数据,以及我想要用于每个传感器数据的位数,并将它们打包到一个 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,这是一款用于布局条带板或 万用板 电路的出色软件。 它允许您 设计 一个虚拟电路,您可以在条带板上复制该电路。 我的原型电路在 Fritzing 中看起来像这样:

opensource.com
这导致了这个实际的(可工作的)电路:

opensource.com
4. 设计和打印 PCB
为了设计我的 PCB,我使用了 Autodesk Eagle,这是一款出色的软件,对于小型电路板(<80 厘米)可以免费使用,并且拥有许多组件库(包括优秀的第三方库,例如,SparkFun 和 AdaFruit 的所有组件)。
我从这些 SparkFun 教程中学习了关于 Eagle 我需要知道的一切:
根据我的经验,我提出以下一些建议:
- 经常保存!
- 每次更改后,无论多么小,都要始终进行 设计规则检查(和重新检查)。 在铺铜后重新检查,即使更改不应该影响铺铜。(铜铺地是通过用铜填充开放的、未使用的区域来创建的,通常在电路板的外层上,然后将铜填充物与缝合 过孔 连接到地。 铺地在缺少实体参考平面的双层板上很有用;它可以减少由于容性耦合引起的串扰。)
- 当使用非常小的组件(例如,FPGA 表面贴装组件)进行布线时,尽量不要在组件下方有任何孔,以避免在您手工焊接或表面贴装组件以进行原型测试时出现问题,因为没有专业的工具(例如,回流焊炉、贴片机等)。 很难确保手工涂抹的焊料或焊膏不会位于组件下方并流入下方的布线孔中(您看不到的地方)。 在布线时也很容易忘记其中一些组件有多小。
换句话说,不要这样做:

opensource.com
而是这样做:

opensource.com
- 对于较大的组件,尽量不要在组件引脚或焊盘附近有布线孔,原因与上述相同。
我的最终、完全布线的电路板布局看起来像这样:

opensource.com
5. 焊接表面贴装组件
对于这个项目的开始,一个很大的未知数是如何构建包含表面贴装组件 (SMC) 的原型。 使用通孔 (PTH) 组件进行原型设计(例如,面包板原型)要容易得多,但您不会为最终产品选择 PTH 组件,因为 SMC 更小更整洁。
当您使用理想的 SMC 组件设计 PCB 布局,打印出来,并且想要将它们组装在一起并进行测试,但您没有任何表面贴装设备(如贴片机或回流焊炉)时,会发生什么? 您可以 构建自己的回流焊炉,但是如果您正在构建自己的电路,我认为这种偏离重点有点耗时。 而且,这在很大程度上是不必要的,因为通过足够的练习,您可以手工焊接几乎所有的 SMC,并且可以使用相对便宜的热风枪来使工作更容易。
我使用 EEVBlog YouTube 频道学习了 SMC 焊接 的基础知识,最终我手工焊接了所有东西,一直到 0402 组件(非常小,如果您呼吸太重就会丢失它们!)。 对于上下文,请参阅此组件尺寸比较图:

opensource.com
我不建议在您的电路中使用 0402 组件。(我别无选择,因为它们是天线下射频网络的一部分,更大的组件可能会影响天线性能。) 事实上,0602 组件也非常小且难以焊接,但经过一些练习,一切都非常可行。 我建议在第一批订单中额外订购几个 PCB,仅用于焊接练习,因为您很可能会搞砸您的第一次尝试。
所需的工具包括:
- 烙铁: 绝对值得多花一点钱购买优质的烙铁。 我最初使用了一个便宜的烙铁, 几周后,我把它换成了 更好的烙铁,一切都变得容易得多。
- 热风焊枪: 我还购买了一把 热风枪;虽然事实证明它比我希望的更难使用(将气压调整到合适的位置,以免将小组件从电路板上吹走是一门艺术!),但它使焊接一些较小的(VFLGA)封装集成电路(如 LIS3DH)变得容易得多。(我什至不确定我如何仅用烙铁做到这一点,尽管显然这是可能的。)它还可以轻松地在您搞砸某些东西时移除组件。
- 镊子: 一套优质、非常精细尖端的 镊子 对于拾取非常小的组件至关重要。
- 放大镜/放大镜: 为了放大焊接区域以检查不良焊点、焊桥、焊球、遗漏引脚等,我发现珠宝商的放大镜(最好带有内置灯)非常有用。
6. 测量功耗
功耗测量是一个非常困难但非常重要的过程部分。 我希望我的设备是超低功耗的,以便它可以在一块小电池(即 900mAh CR2)上工作一年。 这意味着要确保静态电流(恒定电流消耗)尽可能小,降至低至 µA 范围,同时考虑到消息发送期间偶尔较高的电流消耗。 尽管有许多方法可以评估电路的电流需求,但大多数方法在非常低的端都分辨率较差。 手动机制,例如跨电源线连接的电流表,使用起来很麻烦,并且只能提供特定时间使用了多少电流的快照(在某些情况下,反应不够快,无法进行任何可靠的测量)。
在我尝试的各种选项中,最终唯一有效的是 Nordic Semiconductor 的 Power Profiler Kit (PPK)。 它不太贵(套件和底板总共约 100 美元),而且效果非常好。(我唯一的不满是,即使它是一个 Python 程序,我也无法使其在 Linux 上可靠地工作,因此我不得不启动到 Windows 才能使用它。)
PPK 既可以产生低至非常低分辨率 (<1µA) 的功耗恒定视图,也可以产生时间窗口的运行平均值(这正是我电池寿命计算所需要的):

opensource.com
7. 编程 ATmega 引导加载程序
您可能已焊接到 PCB 上的原始 ATmega 芯片可能没有硬编码正确的熔丝位设置(见下文)或已编程引导加载程序,因此您可能需要对其进行配置以使其电路板正常运行。 对于第一次设计/构建 PCB 的人来说,这非常令人困惑!
将从芯片供应商处收到的原始 ATmega 芯片设置为需要解决的三个主要任务。(注意:详细信息是指 ATmega328P,但其中大部分也适用于 ATmega 系列中的其他部件)
熔丝位设置
熔丝位是定义芯片行为方式的许多可编程方面的非易失性位。 有三个熔丝字节,每个字节有 8 位:低字节、高字节和扩展字节。 例如,这些控制什么类型的时钟驱动芯片或欠压检测器 (BOD) 在什么电压下触发。 BOD 在设定的电压下停止代码执行,以避免在功率太低时出现不可靠的操作。
默认值在工厂提供的芯片中设置。 这些可能适合芯片的预期用途。 但如果不是,则需要更改它们。 这是通过 SPI 总线使用合适的接口(例如,Ardiuno Uno 板)完成的。 这里和这里有一些关于此过程的良好指南:这里 和 这里。
引导加载程序
运行项目应用程序所需的代码需要加载到芯片中。 通常,使用 FTDI 接头设备通过 USB 将芯片连接到编程 PC。 在这种情况下,芯片需要安装引导加载程序,以方便此操作。 实际上,这是一个加载程序的程序,但它通过 SPI 总线使用合适的接口加载。
对于本项目,我使用单独的 Arduino UNO 来引导加载我的 ATmega 芯片,如下所示:
- 对于引导加载程序,请使用 Nick Gammon 的 ATmega 芯片编程器
- 下载 ZIP 文件
- 提取 ATmega_Board_Programmer 文件夹(例如,到 Arduino IDE Libraries 目录)
- 打开 ATmega_Board_Programmer 草图
- 将标准 Arduino Uno 连接到您的 PC
- 将电路板设置为“Arduino/Genuino Uno”并设置正确的端口
- 上传 ATmega_board_programmer 草图
- 断开 Uno 与 PC 的连接,并按如下方式将其连接到目标芯片:
Uno | 目标 |
---|---|
D10 | 复位 |
D11 | MOSI |
D12 | MISO |
D13 | SCK |
Gnd | Gnd |
+5V | Vcc |
- 将 Uno 重新连接到 PC -> 设置端口 -> 运行串行监视器 115200 波特
- 引导加载程序应立即运行并在串行监视器窗口中显示结果; 按照串行窗口中的说明操作(例如,“L”表示加载引导加载程序)
- 请注意,引导加载程序会将芯片设置为使用内部 8MHz 时钟; 如果您有外部晶振,则可以修改此设置(请参阅草图中的注释)
程序代码加载
一旦芯片安装了引导加载程序,就可以通过 FTDI 接口加载程序代码。 在开发人员 PC 上运行的 Arduino IDE 可以通过此接口将应用程序代码直接加载到芯片中。
8. 打印 PCB、购买组件、制造和组装
要从面包板原型过渡到批量制造,您将需要各种资源:
- 硬件组件: 要在面包板上搭建电路,您将需要电阻器、电容器、传感器、集成电路等组件。 您可以在亚马逊等主流网站上找到其中一些,但我建议使用一些硬件专用网站作为更好的选择。 我主要使用 DigiKey; Mouser 和 Farnell 也不错。
- PCB 印刷: 一旦您设计了 PCB 并创建了 Gerber 文件(指定如何打印),您将需要找到一家公司来打印它。 SparkFun 在“选择 PCB 制造商”下有 一些建议,可能值得一看。 我使用了 Multi-CB,发现他们非常好、及时且价格具有竞争力,尽管我必须通过银行转账付款,因为他们不提供在线支付选项。
- PCB 制造: 一旦您的 PCB 完全设计完成,您的组件已购买并手工焊接,并且您的最后一个原型已测试,现在是时候批量制造它了。 我收到了 PCBCart 的非常合理的报价,其中包括组装和 ATmega 芯片编程。 由于我尚未制造电路板,因此我无法评论他们的质量或服务。
9. 进行后端开发
因此,您已经构建了您的设备,并且它在 Sigfox 网络上发送消息(基本上是发送到 Sigfox 服务器)……现在怎么办?! 您将如何处理这些消息,以及您将如何处理它们?
Sigfox 回调
首先要做的是让 Sigfox 服务器将您的设备收到的任何消息转发到您控制的 Web 服务器或服务。 Sigfox 系统有很多关于如何做到这一点的选项,但我认为最简单的方法是构建您自己的 RESTful Web 服务(如下所述),并让 Sigfox 服务器使用消息数据向您的新服务发出 HTTP(S) 请求。 这可以在 Sigfox 后端通过对您的设备使用回调机制来完成,您可以在其中从可用变量列表中指定发布的变量或 URL 参数,其中包括原始消息数据:

opensource.com
RESTFul Web 服务
RESTful Web 服务是现代 API,在 Web 上无处不在。 有很多方法可以创建它们,但我决定使用 Go 编程语言,首先是因为这是一种我想学习的语言,其次是因为它很容易通过 Docker 部署。 Web 服务(保存到 MongoDB 数据库)在 Go 中的基本结构如下所示:
// 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 Then That (IFTTT),它是许多不同 API 和系统的组合。 一旦您 将您的设备连接到 IFTTT,您就可以访问现有的后续操作。 例如,“如果 [您的设备发送 X],则 [向 Y 发送电子邮件],或 [让 Alexa 说 Y],或 [打开 Y 房间的 Philips Hue 灯光]”,或任何其他无数选项。
展望未来
我的 Siguino 项目的下一步是为其开发 3D 外壳,完成 Sigfox 设备认证计划,调整天线以充分利用其性能,以及为设备的首批生产运行提供资金和组织。
由于我这个项目的主要目的是学习这项技术,我已经将所有软件代码和硬件开源在 GitHub 上。 如果您有任何问题或发现此信息有价值,请在评论中告诉我。
3 条评论