BeagleBone Black 开发入门实验

写篇文章记录一下学校的学科入门实验——基于 BeagleBone Black 的嵌入式 Linux 系统开发。第一次玩这种东西觉得还有点意思……

以下内容为图省事大量地摘编自我的实验报告

使用脚本程序控制 BeagleBone Black 自带的 LED 灯

实现目标及所需硬件资源

BeagleBone Black 自带 4 个 LED 灯(分别是 USR0, USR1, USR2, USR3),本次实验的实现目标是通过 Bash 脚本语言控制这四个 LED 灯,按照人为定义的规则实现亮灭。

软件流程

BeagleBone Black 的 LED 灯设备驱动文件位于 /sys/class/leds 下,在该目录下执行 ls 命令可以看到四个灯的控制文件分别所对应的目录为 beaglebone:green:usr0-3,进入其中一个目录,可以看到一些文件,这些文件各自有各自的用途:

  1. trigger可以用来指定 LED 的触发状态,可以通过 cat 命令查看当前 LED 由哪个设备或事件触发;前文已知第三个 LED 灯指示的是 CPU 的使用状态,如在 beaglebone:green:usr2 目录下执行 cat trigger 命令,返回的结果说明 LED3 的出发状态由 cpu0 所控制。对 trigger 写入内容则可以更改 LED的触发规则:例如执行 echo none > trigger 对 trigger 写入 “none”,则会断开对应 LED 与其它设备的连接;执行 echo timer > trigger对 trigger 写入 “timer”,则将当前 LED 设置为延时触发模式。

  2. brightness 文件用于控制当前 LED 灯的亮度。若执行 echo 0 > brightness 将0写入亮度文件中,那么对应 LED 灯就会熄灭;相反执行 echo 1 > brightness 将 1 写入亮度文件中,那么对应 LED 灯就会亮起。

  3. 如果执行 echo timer > trigger向 trigger 中写入 “timer”, 那么当前 LED 的触发模式会变为延时触发,此时当前目录下会多出 delay_off 和 delay_on 两个控制文件,分别控制该 LED 熄灭和亮起的延迟时间,单位为 ms.

  4. 如果执行 echo gpio > trigger 向 trigger 中写入 “gpio”,那么当前 LED 的触发方式会由 GPIO 的电平状态决定,此时当前目录下会多出 desired_brightness, gpio 两个文件,可能代表期望亮度和监听的 GPIO 编号。

因为本次实验我们只控制自带的 LED 灯而不涉及 GPIO 的操作,因此程序的流程如下:

  1. 首先对四个 LED 灯对应的 trigger 写入 none, 切断四个 LED 灯与其它设备的关联;

  2. 写入一个 [0, 255] 的整数到 brightness 文件中,控制 LED 灯的亮度;使用 Bash 函数控制 LED 灯的亮灭规则;

  3. 再对四个 LED 灯对应的 trigger 写入 timer,更改 LED 灯为延时触发模式;

  4. 分别设置四个 LED 灯的 brightness 和 delay_on, delay_off 控制 LED 灯的亮灭规则。

    关键代码分析及现象结果

以下 Bash 程序实现了让 BeagleBone Black 的四个指示灯一个接一个亮起、熄灭的功能:

#!/bin/bash
# reset leds' trigger
echo "Setting LEDs trigger to none..."
for i in {0..3}
do
    echo none > /sys/class/leds/beaglebone\:green\:usr$i/trigger
done

# turn on leds, controlling by sleep
echo "Turning on/off LEDs one after another..."
while :
do
    # turn on leds
    for i in {0..3}
    do
        echo 1 > /sys/class/leds/beaglebone\:green\:usr$i/brightness
        sleep 0.5s      # pause for 500 milliseconds
    done

    sleep 0.5s

    # turn off leds
    for i in {3..0}
    do
        echo 0 > /sys/class/leds/beaglebone\:green\:usr$i/brightness
        sleep 0.5s
    done
    sleep 0.5s
done

代码分析:首先将 LED 的触发器设置为 “none” 断开 LED 与其它硬件设备间的关联,然后不断执行两段循环:第一段循环会将编号为 0-3 的 LED 分别亮起;每亮起一颗 LED 后,sleep 命令会使得程序暂停 0.5s,然后再亮起下一颗 LED. 第二段循环则每隔 0.5s 按照 3, 2, 1, 0 的顺序熄灭一颗 LED. 一轮操作完成后,四颗 LED 回到全部熄灭的初始状态,然后重复上述步骤。

除了使用系统自带的 sleep 命令以外,还可以设置 delay_on 和 delay_off 来控制延时亮灭。以下 Bash 程序实现了让四颗 LED 灯按不同的间隔时间分别闪烁的功能:

#!/bin/bash
#reset leds' trigger
for i in {0..3}
do
    echo none > /sys/class/leds/beaglebone\:green\:usr$i/trigger
done

# set triggers to 'timer'
for i in {0..3}
do
    echo timer > /sys/class/leds/beaglebone\:green\:usr$i/trigger
done

# set delay time for triggers
for i in {0..3}
do
    # calculate current led's delay time
    base=200
    multiply=`expr $multiply + 1`
    delaytime=`expr $multiply \* $base`
    echo $delaytime > /sys/class/leds/beaglebone\:green\:usr$i/delay_on
    echo $delaytime > /sys/class/leds/beaglebone\:green\:usr$i/delay_off
done

代码分析:首先将 LED 的触发器设置为 “none” 断开 LED 与其它硬件设备间的关联,然后再将其设置为 “time”设置 LED 的触发规则为延时模式,此时在对应 LED 的目录下会多出 delay_on, delay_off 两个文件。接下来执行一个循环,循环体内首先根据当前操作的 LED 的编号 (0, 1, 2, 3) 计算对应的间隔时间(上述代码中,对于编号为 i 的 LED,亮起和熄灭的间隔时间定义为 \(\left(i+1\right)\times200 ms\),再将这个间隔时间写入到 delay_on 和 delay_off 两个文件中。执行该程序的现象:四颗 LED 分别以 200, 400, 600, 800ms 的亮灭间隔闪烁。

使用C语言程序控制外接 LED 设备

实现目标和所需硬件资源

实现目标:外接一个三色 LED 设备,使用 C 语言程序控制三颗 LED 灯闪烁;并实现点击外接 LED 上的按钮,控制 LED 灯的闪烁。

所需硬件资源:一块外接 LED, 杜邦线若干。

BeagleBone Black 的 GPIO 和引脚接口

GPIO (General Purpose Input Output, 通用输入/输出接口),又称总线拓展器,是一个能用于输入/输出二进制控制信号的接口,可用于 BeagleBone Black 与外接设备之间的数据交互。每个 GPIO 接口可以被人为设定为 Input 和 Output 两种模式,分别用于接收外接设备输入的信号,或向外接设备输出信号。GPIO 的值可以为 0 或 1,分别代表低电平和高电平。

与自带 LED 相同,对 GPIO 的读写和操作,同样是通过操作其对应的设备文件来实现。GPIO 的设备文件位于 /sys/class/gpio 下。执行命令 ls /sys/class/gpio,可以看到该目录具有的文件信息.BeagleBone Black 具有四组 GPIO (GPIO0-3), 每组下分别有若干个 GPIO 接口。对于每个接口,都具有一个 Name 属性(GPIOx_y, x 为该接口的所属分组,y 为一个整数)、Head_pin 属性(Px_y, Px 表示该接口位于 P8/P9 排引脚上,y表示该接口在引脚上的编号)、GPIO No.(GPIO 接口编号)。其中,Head_pin 用于确定该接口在 BBB 上的位置,确保外接设备连接到了正确的接口;GPIO No. 用于在 Linux 下通过此编号操作对应的 GPIO 接口,计算 GPIOx_y 编号的规则为 \((x\ \times32\ +\ y)\)。

以上属性都可以在 BeagleBone Black 的引脚速查表上找到。需要注意,并不是所有的 GPIO 口都可用,有些 GPIO 口可能已经被占用。

BeagleBone Black 操作 GPIO 口的方法:当我们需要操作 GPIO 口时,首先需要根据编号导出 (export) 所需 GPIO 口的设备文件。导出 GPIO 接口设备文件的方法是执行如下命令:echo gpio_no > /sys/class/gpio/export,其中 gpio_no 是所需 GPIO 的编号 (GPIO No.)。执行以上命令后,在 /sys/class/gpio 目录下会多出一个名为 gpio + 编号的目录,该目录内即为控制所需 GPIO 口的设备文件。 例如,若需要使用位于 P8 引脚排上的 11 号接口对应的 GPIO1_13,其对应的 GPIO No. 为45,则执行:echo 45 > /sys/class/gpio/export . 此时, /sys/class/gpio 目录下会产生一个名为 gpio45 的目录。

执行 cd /sys/class/gpio/gpio45 && ls,查看该目录下的文件;本次实验中我们只涉及其中两个设备文件:direction 和 value。direction 的值可以为 in/out,用于设置当前 GPIO 口的数据流动方向; value 的值可以为 0/1, 用于设置当前 GPIO 口是高电平还是低电平。

如果不再使用一个 GPIO 接口,可以使用 echo gpio_no > /sys/class/gpio/unexport 来释放这个 GPIO 接口。

硬件连线

首先分析外接设备三颗 RGB LED灯和按键对应的电路图:

电路图

根据LED的电路图可知,RGB LED 灯一端与 VDD(电源电压)相连,为高电平;另一端是三种颜色对应的引脚 P26, P27, P28. 只需将这三个引脚连接到 BeagleBone Black 的三个 GPIO 口上,设置对应的 GPIO 为低/高电平即可点亮/熄灭某个 LED.

按键电路图

本次实验以选择按键 K2 为例,将 P22 引脚连接到 BeagleBone Black 的一个 GPIO 口上,则当按下 K2 后,对应的 GPIO 口会变为高电平,此时 GPIO 的 value 值为 1;松开后会变为低电平,GPIO 的 value 值为 0. 本次实验选择 BeagleBone 的 GPIO 编号为 26(位于P8_14), 48(位于P9_15), 49(位于P9_17), 60(位于P9_12),分别连接三个 LED 引脚 P26, P27, P28 和 K2 按键引脚 P22;此外还需要连接 LED 的VDD到 BeagleBone Black 的 DC3.3V (位于P9_03/04) 、连接LED的GND到BeagleBone 的 GND(位于P9_01)。

实现流程

  1. 导出(export) 本实验需要使用的四个 GPIO;
  2. 编写 C 语言程序,从控制台读入参数,设置 LED 工作模式(亮起、熄灭、闪烁);
  3. 通过 C 语言的文件操作函数读写对应的设备文件,实现对 LED 和按键的控制。

关键代码分析及现象结果 (C++ 实现)

#include <cstdio>
#include <cstdlib>
#include <cstring>
#define GPIO_PATH "/sys/class/gpio/"
int gpio_ports[] = {60, 48, 49, 26};
int gpio_count = 4;
/**
 * 导出对应编号的 GPIO 设备文件
 * @param number 要导出的 GPIO 编号
 */
void export_gpio(int number) {
    // do not use `w+` or you will meet segmatation fault for permission denied
    FILE *f = fopen(GPIO_PATH"export", "w");
    printf("%s", GPIO_PATH"export");
    fprintf(f, "%d", number);
    printf("gpio port %d exported.\n", number);
    fclose(f);
}
/**
 * 熄灭 RGB, 原理是设置 GPIO 为高电平
 * @param index RGB所对应的GPIO编号
 */
void stop_light(int index) {
    char path[200];
    sprintf(path, "%sgpio%d/value", GPIO_PATH, index);

    FILE *f = fopen(path, "w");
    fprintf(f, "1");
    fclose (f);
    printf("stopping light at gpio %d...\n", index);
}
/**
 * 亮起 RGB, 原理是设置 GPIO 为低电平
 * @param index    RGB所对应的GPIO编号
 * @param need_out 是否需要先设置 direction 为 out
 */
void start_light(int index, bool need_out) {
    char path[200];
    if (need_out) {
        sprintf(path, "%sgpio%d/direction", GPIO_PATH, index);
    
        // set direction to out
        FILE *f = fopen(path, "w");
        fprintf(f, "out");
        fclose(f);
        printf("setting direction to 'out' for gpio %d...\n", index);
    }
    memset(path, 0, sizeof path);
    FILE *f;
    sprintf(path, "%sgpio%d/value", GPIO_PATH, index);
    f = fopen(path, "w");
    fprintf(f, "0");
    fclose(f);
    printf("led connected at gpio %d should be lighted now.\n", index);
}
/**
 * 闪烁 RGB LED
 * @param limit 闪烁次数限制,-1 表示无限
 */
void flash_lights(int limit) {
    printf("show time!\n");
    long long cnt = 0LL;
    while (1) {
        // before flash, stop all lights
        for (int i = 0; i < gpio_count - 1; i++)
            stop_light(gpio_ports[i]);
        usleep(500000);
        for (int i = 0; i < gpio_count - 1; i++) {
            start_light(gpio_ports[i], cnt == 0);
            usleep(200000);
        }
        for (int i = 0; i < gpio_count - 1; i++) {
            stop_light(gpio_ports[i]);
            usleep(200000);
        }
        cnt++;
        if (cnt > limit && limit != -1)
            break;
    }
}
int main(int argc, char* argv[]) {
    // 判断参数个数
    if (argc != 2) {
        printf("Usage: led [on/off/flash/keymode]\n");
        exit(0);
    }
    // 导出 GPIO 设备文件
    for (int i = 0; i < gpio_count; i++)
        export_gpio(gpio_ports[i]);
    // 根据 argv[1] 参数内容不同,执行不同的操作
    if (strcmp("on", argv[1]) == 0) {
        for (int i = 0; i < gpio_count - 1; i++)
            start_light(gpio_ports[i], true);       // 亮灯
    } else if (strcmp("off", argv[1]) == 0) {
        for (int i = 0; i < gpio_count - 1; i++)
            stop_light(gpio_ports[i]);              // 灭灯
    } else if (strcmp("flash", argv[1]) == 0) {
        flash_lights(-1);                           // 闪烁
    } else if (strcmp("keymode", argv[1]) == 0) {
        // 按键模式,点按按键闪烁,松开按键停止
        for (int i = 0; i < gpio_count - 1; i++)
            stop_light(gpio_ports[i]); 
        // 设置 direction 为 in
        char path[200];
        sprintf(path, "%sgpio%d/direction", GPIO_PATH, gpio_ports[3]);
        FILE *f = fopen(path, "w");
        fprintf(f, "in");
        fclose(f);
        // set path to stream
        memset(path, 0, sizeof path);
        sprintf(path, "%sgpio%d/value", GPIO_PATH, gpio_ports[3]);
        char a[10];
        bool isFlashing = false, changed = false;
        while (1) {
            f = fopen(path, "r+");
            fscanf(f, "%s", a);
            // 如果接收到按键信号,且当前仍在闪烁,则停止闪烁
            if (a[0] == '1' && isFlashing) {
                isFlashing = false;
                printf("stopped flash:) farewell \n");
                for (int i = 0; i < gpio_count - 1; i++)
                    stop_light(gpio_ports[i]);
                sleep(2);
            }
            // 如果接收到按键型号,且当前没有闪烁,则开始闪烁
            else if (a[0] == '1' && !isFlashing) {
                isFlashing = true;
                flash_lights(1);
            }
            // 如果没有接收到按键信号,则继续闪烁
            else if (isFlashing) {
                flash_lights(1);
            }
            usleep(100000);
        }
    }
    return 0;
}

在终端执行编译命令:g++ -o led led.cpp,编译上述程序,然后执行:

./led on      # 亮起LED
./led off     # 熄灭LED
./led flash   # 闪烁LED
./led keymode # 按键模式,按下闪烁,再按熄灭

物联网概念及其在 BeagleBone Black 下的实现

BeagleBone Black 实现物联网的几种架构

(1)BeagleBone Black 作为 Web 服务器:用户可以通过浏览器发送 HTTP 请求到 BeagleBone Black 的 Web 服务器上,获取 BBB 的设备信息或与其相连的传感器的信息;

(2) BeagleBone Black 作为 Web 客户端:可以编写程序使 BeagleBone Black 与其它 Web 服务器进行通信;

(3)TCP 通信:一台作为 TCP 服务端,一台作为 TCP 客户端,两者间可以通过 Socket 发送请求、接收请求;

(4)通过PaaS(Platform as a Service, 平台即服务):BeagleBone Black 上的传感器信息可以通过 HTTP/RESTful API 与 PaaS 平台通信,用户从浏览器端访问该 PaaS 服务器即可获取来自 BBB 的信息。

动态 CGI 页面实现

CGI (通用网关接口,Common Gateway Interface) 是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据库API 与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。格式化为HTML文档后,发送给浏览器,也可以将从浏览器获得的数据放到数据库中。几乎所有服务器都支持CGI,可用任何语言编写CGI,包括流行的C、C ++ 等。

在 BeagleBone Black 中,利用 CGI 可以实现 Web 页面与物理环境的交互:如读取传感器,驱动 LED 等。CGI 脚本的位置默认位于 /usr/lib/cgi-bin 中,可以通过 Apache 2 服务器访问,地址是 http://192.168.7.2:8000/cgi-bin/ .

如,在该目录下创建一个 CGI 脚本页面,使我们访问该页面时,显示 BeagleBone Black 的运行时间、用户、负载等信息。在 /usr/lib/cgi-bin 创建文件 info.cgi, 插入以下代码:

#!/bin/bash
echo "Content-Type: text/html"
echo ""
echo "<!doctype html>"
echo "<html>"
echo "<head>"
echo "<meta charset=\"utf-8\">"
echo "<title>BeagleBone Black System Info</title>"
echo "</head>"
echo "<body>"
echo "<h1>BeagleBone Black system info</h1>"
echo "Host" 
hostname
echo " has been up "
uptime
echo "</body>"
echo "</html>"

执行命令:chmod +x /usr/lib/cgi-bin/info.cgi, 然后浏览器访问页面 http://192.168.7.2:8080/cgi-bin/info.cgi 即可。

CGI 脚本还可以调用 C/C++ 程序,只需在脚本中调用目标程序的路径即可,目标程序的标准输出则会被转发到浏览器中。

BeagleBone Black 与 Windows 宿主机共享网络

让 BeagleBone Black 连接网络。可以直接将网络电缆连接到 BeagleBone Black 的 RJ45 接口上,或者使用 USB 与宿主机共享网络。本实验的宿主机为 Windows 10, 与 BeagleBone Black 共享网络需要进行如下操作:

Windows 下:安装 BBB 的驱动,设置可访问互联网的网络适配器与 BeagleBone Black 共享网络,如宿主机与互联网建立连接的网卡为 “WLAN”, BeagleBone Black 在系统中的适配器为“以太网3 (Linux USB Ethernet/RNDIS Gadget)”, 则需要:右击 WLAN => 属性 => 共享 => 允许其它用户通过此计算机的 Internet 连接来连接 => 勾选“以太网3”。再设置“以太网3”的 IP 地址为 192.168.7.1, 子网掩码为 255.255.255.0.

Linux 下:执行命令 route add default gw 192.168.7.1 添加路由表;然后编辑/etc/resolv.conf,在文件头部加入:

nameserver 127.0.0.1 
nameserver 8.8.8.8
nameserver 114.114.114.114

配置 BBB 的 DNS 解析服务器。

BeagleBone 实现实时温度检测系统

目的及原理

使用 BeagleBone Black 连接德州仪器的 TMP75 传感器,通过 i2c 协议通信,数据交由运行在 BeagleBone Black 上的后端 Web Server 处理,在浏览器端显示温度数据、绘制温度曲线并进行交互,效果如图:

screenshot

原理:TMP75 采用 i2c 协议来发送温度数据。在 BeagleBone Black 的 Debian 系统下,我们可以运行 i2cdetecct -y -r 1 来获得 TMP75 的总线地址;当我们运行 i2cget -y -f 1 0xyy(0xyy 是从 i2cdetect 获取的总线地址) 时,会返回一个代表温度的十六进制数。我们可以整合这些操作到一个前后端方案中。

连线

TMP75 V33 to BBB DC3.3V (P9_03)

TMP75 GND to BBB GND (P9_01)

TMP75 SCL to BBB SCL(P9_19, do not connect to 17 which is not occupied:) )

TMP75 SDA to BBB SDA(P9_20, do not connect to 18 which is not occupied:) )

TMP75 I/O to BBB GPIO(whichever has not been occupied is OK)

实现代码

实现的方案代码已托管于 GitHub: https://github.com/kirainmoe/beaglebone-temperature-monitor