65.9K
CodeProject 正在变化。 阅读更多。
Home

RIOT 教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (12投票s)

Nov 9, 2014

CPOL

12分钟阅读

viewsIcon

100073

使用和开发 RIOT

使用和开发 RIOT

RIOT logo

作者: Oliver "Oleg" Hahm

日期 08/09/2014

更新者: Cenk Gündoğan (2016/12/19)

目录

引言

本教程将为您提供 RIOT 操作系统的总体概览。它将解释如何设置开发 RIOT 的构建环境,如何将其部署到硬件,以及如何使用它。为了便于入门,本教程将通过示例代码来说明 RIOT 应用程序的开发。最后,它将展示如何通过使用 IoT-Lab 测试平台或 RIOT 强大的 native 端口(一种 RIOT 的虚拟机)在没有直接访问 IoT 硬件的情况下使用 RIOT。

启动 RIOT

设置构建环境

基本上,开发 RIOT 所需的所有东西就是一个可用的 C/C++ (交叉)编译器和 make 构建系统。根据您的目标平台,您可能还需要额外的工具来与您的开发板交互,例如 OpenOCD。为了获得最佳的开发体验,我们建议使用基于 Linux 的系统。FreeBSD 和 Mac OS X 也应该可以正常工作,而对 MS Windows 的支持尚未经过测试。

要创建构建环境,有许多选项,下面将讨论最方便的几个。

直接在您的机器上安装工具

在 RIOT 上开发最直接的方法是在您的 PC 上直接安装工具链。您可以在 这里 找到针对您的目标平台的具体说明。有关此步骤,另请参阅 下方

使用虚拟机

第二个选项是在虚拟机中使用 Linux 安装。有关如何在 VirtualBox 中创建和配置 Ubuntu 虚拟机以进行 RIOT 开发的更多信息,请参阅 此处

使用 Docker 文件

RIOT 提供了一个即用型 Docker 文件,用于设置一个专用于 RIOT 开发的 Docker 容器。有关更多信息,请参阅 此处

使用 Vagrant

Vagrant 是轻松开始 RIOT 的另一种选择。为此,RIOT 的根目录中包含了一个 Vagrantfile,它会设置一个可重现的轻量级环境,并包含所有必需的工具链。有关详细信息,请参阅 此处

选择正确的工具链

如果您决定自己设置工具链,则应阅读本段,否则可以跳过。我们在为基于微控制器系统的软件开发过程中积累的经验表明,不幸的是,在不同的工具链下,并非所有功能都表现得完全相同,即使该工具链声称支持所选的微控制器。因此,RIOT 社区建议为特定平台使用建议的工具链,以避免出现奇怪的行为。在 RIOT wiki 中,您可以找到每个支持的开发板的推荐列表。一般来说,GCC ARM Embedded (Launchpad) 工具链在大多数 ARM Cortex-M 平台上都能正常工作,而对于 ARM7,CodeSourcery 工具链版本 2013.11 已知运行良好。对于 TI MSP430 微控制器,MSP430 GCC 是一个不错的选择,而您可能会在旧版本中遇到问题。在 AVR 上,您可以使用 AVR GCC。

从 GitHub 获取代码

GitHub Logo 您可以通过两种方式获取最新的 RIOT 版本

  1. 通过运行以下命令克隆 git 仓库

    git clone https://github.com/RIOT-OS/RIOT.git

  2. GitHub 下载压缩版本

本教程请同时克隆 applications 仓库。您也可以将其下载为 zip 压缩包

理解 RIOT

架构和文件夹结构

在下图所示的 RIOT 架构中,我们以 IoT-LAB M3 节点这一特定硬件为例。

RIOT Architecture for the M3 node

在此图中,您可以看到所有相关层,从硬件依赖代码到应用程序。

硬件依赖代码被分成两个目录:cpuboards。对于许多微控制器,CPU 依赖代码进一步分为与架构相关的代码(例如 cortexm_common)和与微控制器相关的代码(例如 stm32f1)。第一部分通常实现堆栈初始化、上下文切换或中断处理等功能。控制器特定的部分实现系统调用和外围设备驱动程序,如 SPI 或 I²C。boards 目录中的代码实现平台特定的功能,主要包含一些头文件,定义了诸如 LED 闪烁宏之类的东西,以及外围设备配置,定义了引脚的连接布局等。

在此硬件抽象接口之上,所有上层都可以以相同的方式访问所有支持的平台。然后是平台无关的代码。其中有微内核,它提供多线程、进程间通信、确定性抢占式调度器、互斥锁和定时器抽象。drivers 目录包含传感器或(无线)收发器等附加设备的设备驱动程序。在 sys 文件夹中,您会找到您期望的完整的嵌入式设备操作系统所提供的所有实用程序和库。它包括一个 shell、数据结构、一个 POSIX 包装器,当然还有各种网络堆栈。

RIOT 还附带了一个第三方标准 C 库。对于基于 ARM 的平台,它使用 Newlib;对于 MSP430,它使用 MSP430-libc;对于基于 AVR 的系统,它使用 avrlib。这允许您使用所有常见的 C 函数,如 memcpy()printf()atoi()

附加功能和实用程序

除了包含操作系统代码本身的这些目录外,您还会找到一些额外的目录。dist 文件夹为您提供模板、辅助和测试脚本,以及用于处理 IoT 设备的实用程序。API 文档位于 doc 中。在 examples 目录中,您将找到 RIOT 的应用程序,这些应用程序演示了操作系统的重要功能。

RIOT 还提供了使用类似 BSD 端口的系统来使用外部库的选项。这样,您就可以通过包含这些项目的上游代码库来为您的 RIOT 应用程序使用像 libcoapOpenWSN 这样的外部库。此外,RIOT 还附带了一些测试应用程序和方便的脚本,任何持续集成 (CI) 基础设施都可以使用它们。

RIOT 编程最佳实践

RIOT 开发的一些“要”和“不要”

  • 使用静态内存。
  • 仔细选择优先级。
  • 使用 DEVELHELPCREATE_STACKTEST 最小化堆栈使用。
  • 利用 IPC 使用线程来提高灵活性、模块化和健壮性。

不要

  • 不要使用过多的线程。尽量每个模块不要使用超过一个线程。不要为一次性任务创建线程。通常,许多任务都可以由在单个线程中运行的事件循环处理。
  • 从头开始实现时,不要使用 POSIX 包装器。

使用 RIOT

使用示例

native 上编译和运行

native 端口为您提供了一个强大的工具,用于在将软件部署到 IoT 硬件之前对其进行调试和测试。它允许您在 Linux 或 BSD/Mac OS 主机系统上将任何 RIOT 应用程序作为进程运行。它甚至允许您将多个进程在虚拟网络中相互连接——就像它们在实际 IoT 部署中通过网络连接一样。

要编译 RIOT native 端口,您需要某些软件包的 32 位版本。所有依赖项都在相应的 RIOT wiki 页面 中进行了描述。

那么,让我们准备一个简单的虚拟网络。这需要安装 sudobridge-utils,并且还需要超级用户权限来进行设置。转到您的 RIOT 主目录并执行

[RIOT]$ dist/tools/tapsetup/tapsetup -c 2
creating tapbr0 ...
creating tap0 ...
creating tap1 ...

现在我们来构建并启动第一个虚拟节点。为此,我们将切换到默认的示例文件夹。

[RIOT]$ cd examples/default/
[default]$ make all term
Building application default for native w/ MCU native.
"make" -C /home/oleg/git/RIOT/cpu/native
...
RIOT native interrupts/signals initialized.
LED_RED_OFF
LED_GREEN_ON
RIOT native board initialized.
RIOT native hardware initialization complete.

main(): This is RIOT! (Version: 2017.01-devel-154-gbe702-zoidberg)
Native RTC initialized.
Welcome to RIOT!
>
> help
help
Command              Description
---------------------------------------
reboot               Reboot the node
ps                   Prints information about running threads.
rtc                  control RTC peripheral interface
ifconfig             Configure network interfaces
txtsnd               Sends a custom string as is over the link layer
saul                 interact with sensors and actuators using SAUL</code><code>

make term 命令将为您提供节点的(串行)接口,以便您可以看到其输出并向其发送一些命令。

设置第二个节点

现在我们想通过第二个节点来扩展我们的场景。

为此,我们通过(重新)使用 PORT 环境变量来指定一个tap 接口。我们可以使用上一步中创建的任何tap 接口。(默认为tap0。)节点启动后,我们可以使用 ifconfig 命令查询其硬件地址。

[default]$ PORT=tap1 make term
Welcome to RIOT!
> ifconfig
ifconfig
Iface  4   HWaddr: 76:7c:09:2d:53:f4

           Source address length: 6

我们在第一个节点上执行相同的操作,并使用 ifconfig 命令查询其硬件地址。

连接两个节点

现在是发送第一个数据包的时候了(假设第一个节点仍在运行)

> txtsnd 4 76:7c:09:2d:53:f4 riotlab

通过调用不带任何参数的 txtsnd 可以获得有关所需参数的更多信息。

在第一个节点上,我们应该会看到类似以下内容

> PKTDUMP: data received:
~~ SNIP  0 - size:   7 byte, type: NETTYPE_UNDEF (0)
000000 72 69 6f 74 6c 61 62
~~ SNIP  1 - size:  20 byte, type: NETTYPE_NETIF (-1)
if_pid: 4  rssi: 0  lqi: 0
flags: 0x0
src_l2addr: 76:7c:09:2d:53:f4
dst_l2addr: 76:ab:08:1b:5f:de
~~ PKT    -  2 snips, total size:  27 byte

接收到的数据包在内部包含两个具有不同 协议类型 的片段

  • NETTYPE_NETIF - 内部 通用网络接口头
  • NETTYPE_UNDEF - 数据包的有效负载(72 69 6f 74 6c 61 62 "riotlab"

在真实硬件上使用相同的示例

要在真实节点上运行相同的示例,我们需要执行与 native 端口几乎相同的步骤。

我们使用环境变量 BOARD 来指定目标平台 - 并且可能使用 PORT 指定串行端口。make term 将打开一个用于串行通信的终端程序。make flash 将启动适用于指定节点的正确编程工具并设置相应的参数。

[default]$ BOARD=iotlab-m3 make all flash term
Building application default for iotlab-m3 with MCU stm32f1.
"make" -C /home/oleg/git/RIOT/cpu/stm32f1
...
wrote 55296 bytes from file RIOT/examples/default/bin/iot-lab_M3/default.hex
in 2.454112s (22.004 KiB/s)
...
shutdown command invoked
RIOT/dist/tools/pyterm/pyterm -p /dev/ttyUSB2 -b 500000
INFO # Connect to serial port /dev/ttyUSB2
Welcome to pyterm!
Type '/exit' to exit.
help
> help
Command              Description
---------------------------------------
reboot               Reboot the node
ps                   Prints information about running threads.
ifconfig             Configure network interfaces
txtsnd               Sends a custom string as is over the link layer
saul                 interact with sensors and actuators using SAUL

…以及在测试平台

  • 如果您想在大规模(非常)测试平台中测试您的 IoT 应用程序,可以使用 IoT-LAB
  • 要在那里运行您的实验,只需注册一个帐户,然后按照他们页面上的(非常好的)教程进行操作,以便熟悉他们的界面。
  • 为了在 IoT-LAB 上运行 RIOT 应用程序,只需调用 BOARD=iotlab-m3 make all 来构建二进制文件(会构建 elf 和 hex)。
  • 像往常一样闪存节点(使用 IoT-LAB 网页界面或 CLI)。
  • 默认情况下,IoT-LAB 建议使用 netcat 来访问节点。如果您想比使用 netcat 更方便一些,可以使用与本地连接节点相同的工具:pyterm。

    您可以在 dist/tools/pyterm 中找到它。

  • 例如,在 grenoble.iot-lab.info 上运行 pyterm -ts m3-7:20000

使用自定义应用程序

Shell 简介

  • 在此步骤中,我们将使用 gnrc_networking 示例应用程序。
  • 您可以配置 RIOT 以提供一些默认系统 shell 命令。
  • 通过调用 help 可以显示所有可用的 shell 命令和一些在线帮助。
> help
help
Command              Description
---------------------------------------
udp                  send data over UDP and listen on UDP ports
reboot               Reboot the node
ps                   Prints information about running threads.
ping6                Ping via ICMPv6
random_init          initializes the PRNG
random_get           returns 32 bit of pseudo randomness
ifconfig             Configure network interfaces
txtsnd               Sends a custom string as is over the link layer
fibroute             Manipulate the FIB (info: 'fibroute [add|del]')
ncache               manage neighbor cache by hand
routers              IPv6 default router list
rpl                  rpl configuration tool ('rpl help' for more information)

命令的选择取决于您的应用程序的配置(并且可能因平台而略有不同)。

让我们来通信

gnrc_networking 示例应用程序为您提供了一个自定义的(应用程序定义的)shell 命令

  • udp: netcat 的一个非常基础的变体,用于任意 UDP 连接。

闪存两个节点后,我们将必须使用 ifconfig 查找它们的 IPv6 地址。

> ifconfig
Iface  7   HWaddr: 06:02  Channel: 26  Page: 0  NID: 0x23
           Long HWaddr: 36:32:48:33:46:d8:86:02
           TX-Power: 0dBm  State: IDLE  max. Retrans.: 3  CSMA Retries: 4
           ACK_REQ  CSMA  MTU:1280  HL:64  6LO  RTR  IPHC
           Source address length: 8
           Link type: wireless
           inet6 addr: ff02::1/128  scope: local [multicast]
           inet6 addr: fe80::3432:4833:46d8:8602/64  scope: local
           inet6 addr: ff02::1:ffd8:8602/128  scope: local [multicast]
           inet6 addr: ff02::1a/128  scope: local [multicast]

           Statistics for Layer 2
            RX packets 110  bytes 4688
            TX packets 28 (Multicast: 28)  bytes 1681
            TX succeeded 28 errors 0
           Statistics for IPv6
            RX packets 110  bytes 6998
            TX packets 28 (Multicast: 28)  bytes 1778
            TX succeeded 28 errors 0

首先,我们可以尝试 ping 另一个节点。ping 命令由 RIOT 提供。

> ping6 fe80::3432:4833:46d8:8602
bytes from fe80::3432:4833:46d8:8602: id=83 seq=1 hop limit=64 time = 14.036 ms
bytes from fe80::3432:4833:46d8:8602: id=83 seq=2 hop limit=64 time = 17.874 ms
bytes from fe80::3432:4833:46d8:8602: id=83 seq=3 hop limit=64 time = 16.595 ms
--- fe80::3432:4833:46d8:8602 ping statistics ---
packets transmitted, 3 received, 0% packet loss, time 2.0660412 s
rtt min/avg/max = 14.036/16.168/17.874 ms

如果这可行,我们还可以尝试通过 UDP 发送数据包。为此,我们在第一个节点上使用 udp 打开一个 UDP 套接字进行接收。

>udp server start 1234
Success: started UDP server on port 1234

现在,我们使用第二个节点通过 UDP 将文本消息发送到第一个节点 - 再次使用 udp

>udp send fe80::3432:4833:46d8:8602 1234 hello
Success: sent 5 byte to [fe80::3432:4833:46d8:8602]:1234

在第一个节点上,我们现在应该会看到类似这样的内容

PKTDUMP: data received:
~~ SNIP  0 - size:   5 byte, type: NETTYPE_UNDEF (0)
68 65 6c 6c 6f
~~ SNIP  1 - size:   8 byte, type: NETTYPE_UDP (4)
   src-port:  1234  dst-port:  1234
   length: 13  cksum: 0x420dc
~~ SNIP  2 - size:  40 byte, type: NETTYPE_IPV6 (2)
traffic class: 0x00 (ECN: 0x0, DSCP: 0x00)
flow label: 0x00000
length: 13  next header: 17  hop limit: 64
source address: fe80::3432:4833:46d8:8802
destination address: fe80::3432:4833:46d8:8602
~~ SNIP  3 - size:  24 byte, type: NETTYPE_NETIF (-1)
if_pid: 7  rssi: 59  lqi: 255
flags: 0x0
src_l2addr: 36:32:48:33:46:d8:88:02
dst_l2addr: 36:32:48:33:46:d8:86:02
~~ PKT    -  4 snips, total size:  77 byte

这次,接收到的数据包有两个额外的 协议类型NETTYPE_IPv6NETTYPE_UDP

虚拟化您的网络

Wireshark

为 RIOT 编写应用程序

设置应用程序

Makefile

要创建您自己的 RIOT 示例,您只需要两个文件

  1. 一个带有 main 函数的 C (或 C++) 文件
  2. 一个 Makefile

您可以在 dist/Makefile 中找到 Makefile 的模板。

####
#### Sample Makefile for building applications with the RIOT OS
####
#### The example file system layout is:
#### ./application Makefile
#### ../../RIOT
####

# Set the name of your application:
APPLICATION = foobar

# If no BOARD is found in the environment, use this default:
BOARD ?= native

# This has to be the absolute path to the RIOT base directory:
RIOTBASE ?= $(CURDIR)/../../RIOT

# Uncomment this to enable scheduler statistics for ps:
#CFLAGS += -DSCHEDSTATISTICS

# If you want to use native with valgrind, you should recompile native
# with the target all-valgrind instead of all:
# make -B clean all-valgrind

# Uncomment this to enable code in RIOT that does safety checking
# which is not needed in a production environment but helps in the
# development process:
#CFLAGS += -DDEVELHELP

# Change this to 0 show compiler invocation lines by default:
QUIET ?= 1

# Modules to include:

#USEMODULE += shell
#USEMODULE += posix
#USEMODULE += xtimer

# If your application is very simple and doesn't use modules that use
# messaging, it can be disabled to save some memory:

#DISABLE_MODULE += core_msg

#export INCLUDES += -Iapplication_include

# Specify custom dependencies for your application here ...
# APPDEPS = app_data.h config.h

include $(RIOTBASE)/Makefile.include

# ... and define them here (after including Makefile.include,
# otherwise you modify the standard target):
#proj_data.h: script.py data.tar.gz
#    ./script.py

main() 函数

您的应用程序中唯一必需的函数是具有以下原型的 main 函数

int main(void)

可以使用标准 C 库,并且可以通过 RIOT 中的包装器访问 POSIX 的一部分。

#include <stdio.h>
#include <unistd.h>

#define WAIT_USEC   (1000 * 1000)

int main(void)
{
    puts("Hello!");
    usleep(WAIT_USEC);
    puts("Good night!");

    return 0;
}

采用 RIOT 的方式

而不是使用 POSIX 函数,通常建议(并且更有效)直接使用原生的 RIOT 函数。为此,请在应用程序的 Makefile 中包含 USEMODULE += xtimer,并将 main 函数修改为以下内容。

#include <stdio.h>
#include "xtimer.h"

int main(void)
{
    puts("Let's throw a brick!");
    xtimer_usleep(SEC_IN_USEC);
    puts("System terminated");

    return 0;
}

一些有用的功能

查看 gnrc_networking 示例的源代码会揭示 RIOT 的一些有用功能。在本节中,我们将仔细研究代码,以了解如何使用 shell 和网络部分。

启动 shell

Shell 可以运行在任何提供读写字符功能的之上。通常这将是串行接口。通过在 Makefile 中添加 USEMODULE += shell 来提供 shell。如果我们使用 USEMODULE += shell_commands 在应用程序的 Makefile 中,RIOT 将提供一些默认的系统 shell 处理程序。此外,我们可以通过创建并传递一个 shell_command_t 结构数组给 shell_run() 来提供我们自己的自定义 shell 命令。此外,还必须提供 shell 的缓冲区。现在,我们可以运行 shell 了(它会无限循环运行)。

#include <stdio.h>
#include <shell.h>

...
const shell_command_t shell_commands[] = {
    {"udp", "send data over UDP and listen on UDP ports", udp_cmd },
    {NULL, NULL, NULL}
};
...

/* start shell */
char line_buf[SHELL_DEFAULT_BUFSIZE];
shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE);

…以及网络

如果您在 Makefile 中指定使用 IPv6 网络(有关更多信息,请参阅 此处),RIOT 的 auto_init 功能将处理必要的初始化。这还包括将 6LoWPAN 用作 802.15.4 设备上 IPv6 的适配层。

现在,您可能对嗅探所有传入 IPv6 数据包感兴趣。为此,您可以注册您的线程以接收包含 GNRC_NETTYPE_IPV6 类型 IPv6 数据包的 IPC 消息。您的监视线程必须运行 msg_receive()(最好是在无限循环中)来获取信息。由于线程可能已注册到多个层或从其他目的地接收其他消息,因此在接收后检查消息类型是必要的。一旦我们检查确认它确实包含 IPv6 数据包,我们就可以检查并解析数据包的内容。

/* First, initialize a message queue for this thread */
static msg_t _msg_q[8];
msg_init_queue(_msg_q, 8);

/* register thread with currently active pid for messages of type GNRC_NETTYPE_IPV6 */
gnrc_netreg_entry reg = GNRC_NETREG_ENTRY_INIT_PID(GNRC_NETREG_DEMUX_CTX_ALL, sched_active_pid);
gnrc_netreg_register(GNRC_NETTYPE_IPV6, &reg);

/* packets are stored in gnrc_pktsnip_t structs */
gnrc_pktsnip_t *pkt = NULL;

/* we receive endlessly .. */
while (1) {
    msg_receive(&msg);
    switch (msg.type) {
        case GNRC_NETAPI_MSG_TYPE_RCV:
            pkt = msg.content.ptr;
            /* if pkt->type is of type GNRC_NETTYPE_IPV6,
             * then this message is an incoming IPv6 packet */
            assert (pkt->type == GNRC_NETTYPE_IPV6)
            ...
            break;
        ...
    }
}

您可以在 文档 中找到有关 RIOT 的默认网络堆栈 (GNRC) 的更多信息。

达到传输层

可以直接与 GNRC 网络堆栈交互,以便在传输层进行通信。对于 UDP,这在 gnrc_networking 示例的 udp.c 文件中进行了显示。

...
gnrc_pktsnip_t *payload, *udp, *ip;
payload = gnrc_pktbuf_add(NULL, data, strlen(data), GNRC_NETTYPE_UNDEF);
udp = gnrc_udp_hdr_build(payload, port, port); /* source port - destination port */
ip = gnrc_ipv6_hdr_build(udp, NULL, &addr);
gnrc_netapi_dispatch_send(GNRC_NETTYPE_UDP, GNRC_NETREG_DEMUX_CTX_ALL, ip);
...

虽然这种方法很简单,但它仅适用于打算仅在 GNRC 网络堆栈上运行的应用程序。一个更具可移植性的选择是使用 Sock API。如果您的应用程序也计划为其他非 RIOT 系统编译,那么 RIOT 套接字还有一个 POSIX 包装器

例如,要使用 RIOT 套接字的 POSIX 抽象,RIOT 根目录的 examples 文件夹中有一个名为 posix_sockets 的应用程序。

练习:读取传感器

动手实践

完成本教程后,您应该已经准备好编写自己的应用程序,或扩展现有应用程序。作为练习,您可以尝试扩展 gnrc_networking 示例并在 IoT-LAB 上进行测试。

  • 请查阅传感器 API 的文档,例如 光传感器
  • 在应用程序开头初始化所有四个传感器。
  • 扩展 udp 命令,使其可以选择性地发送测量到的传感器数据。
  • 在接收端打印测量值。

加入 RIOT

如果您对本教程或 RIOT 有任何疑问,我们诚挚邀请您联系 RIOT 社区并寻求帮助。以下是与社区联系的常用方式。

进一步帮助

© . All rights reserved.