Fork me on GitHub

使用Ping来检查网络连通性 2013-06-01

树莓派使用了一个无线网卡连接家里的无线路由器,在实际使用过程中发现连续运行多天后会掉线,而且掉线后基本上就再也连不上网了,需要重启树莓派才能恢复,十分麻烦。

假设无线路由器IP是192.168.1.1,于是每隔15分钟检查一下,是否能从树莓派上ping通路由器;如果不能则重启无线网络,脚本如下:

network.sh

#!/bin/bash

export PATH=/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin

ping_count() {
  count=0
  `timeout 5 ping 192.168.1.1 | while read LINE; do
  {
        if [[ "${LINE}" =~ "64 bytes from" ]]; then
                let "count = $count + 1"
                echo "export count=$count"
        fi
  }
  done`
  echo $count
}


if [[ $(ping_count) < 1 ]]; then
        ifconfig wlan0
        ifconfig wlan0 down
        sleep 1
        ifconfig wlan0 up
        sleep 1
        netcfg -r wlan0-Hugo-Nas
        sleep 5
        if [[ $(ping_count) < 1 ]]; then
                echo "Fatal error: wifi is down, rebooting now..."
                reboot
        fi
fi

树莓派的GPIO接口输出电流限制 2013-05-14

树莓派提供了一个连接头让我们访问CPU的17个GPIO接口,如下图

这些接口可配置成输入或输出。本文主要讨论GPIO引脚作为输出时电流的限制。

阻抗 (impendance)

阻抗和和电阻的区别(resistance)在于电阻的阻值是固定的,不会随着电流变化,阻抗则不然,可能随着外部变化,如电流或频率变化。从另一个角度来说,电阻是线性的,但阻抗不是。比如放大器的阻抗会随着输出的信号频率变化。

树莓派的的每个GPIO引脚都有一个寄存器可以设置引脚的驱动强度,也就是在保持输出电压为逻辑0和1的情况下,可以改变阻抗的大小从而改变GPIO引脚的输出电流大小。

通过如下电路测量相同电流下不同阻抗对应的GPIO电压输出(其中用到了一个电位器调节电流保持恒定):

通过计算后,下表是当输出电流为2,4 … 16mA时,对应的阻抗大小以及如果发生短路时的短路电流大小。

使用8位移位寄存器74HC595扩展树莓派的IO端口 2013-05-13

树莓派的GPIO接口数目有限,驱动一个步进电机需要占用4个, 一个Nokia 5110液晶也要占4个, 传感器输入至少需要一个,多玩几个外设后接口就不够用了。如果接口可以复用就可以让树莓派驱动更多的外设了,本文讨论如何使用74HC595集成电路芯片来扩展树莓派的I/O接口。

芯片介绍

SN74HC595N是德州仪器公司生产的集成电路芯片,是一个8位串行输入变串行输出或并行输出移位寄存器,具有高阻关断,高电平和低电平三态输出。在IO扩充上,可以最多串联15片,也就是高达120个IO扩充。

(注意到芯片上的小凹槽了吗,拿芯片的时候以这个为参考物就不会搞反了)

接口的常用命名方式有以下两种:

接口代号(编号) 说明 接口代号(编号) 说明
Q7’(9) serial data output QH’ (9) serial data output
MR (10) Master Reset (Active Low) SRCLR (10) Shift register CLeaR
SH_CP (11) shift register clock input SRCLK (11) Shift Register CLocK input
ST_CP (12) storage register clock input RCLK (12) storage Register CLocK input
OE (13) output enable input (Active Low) OE (13) Output Enable
DS (14) serial data input SER (14) SERial data input
Qx (15,1-7) data output Qx (15,1-7) data output

树莓派网站容灾:利用DNSPod,Google App Engine和Github 2013-04-22

背景介绍

把网站托管在树莓派上后如果家里停电或是宽带故障,会造成网站中断。本文提供一个免费的解决方案(前提是你需要有自己的一个域名,并由DNSPod解析)

DNSPod

首先需要在DNSPod里设置好需要failover的域名CNAME:比如hugozhu.myalert.info

其中默认指向pi.myalert.info, 这是一个域名的A Record,会由运行在树莓派上的脚本来更新动态IP,国外则指向github。当停电时我们需要自动把`默认`这条纪录修改成github。

使用下面命令获得相应CNAME的domain_id:

curl -k https://dnsapi.cn/Domain.List -d "login_email=xxx&login_password=xxx" 

使用下面命令获得相应CNAME的record_id:

使用Goroutine和Channel实现按键超时交互 2013-04-21

背景介绍

前面的文章(见参考链接)已经介绍了如何使用按键作为树莓派的输入。在实际应用中可以通过按下按键循环显示预先设定的脚本输出到显示屏幕,需求如下:

  1. 如果按键不被触动,则定时5秒执行脚本获取最新内容显示;
  2. 因为不同的脚本获取内容速度会不一样,我们要求如果超过500ms脚本还未返回,需要在屏幕上显示“loading…”这样的过渡内容,如果脚本在500ms内返回,则不显示。

使用Goroutine和Channel可以很方便的实现这个需求。

代码

var screen_chan chan int
var switch_chan = make(chan bool)

func main() {
    //a goroutine: 检查按键是否被按
    go func() {
        last_time := time.Now().UnixNano() / 1000000
        btn_pushed := 0
        total_mode := 3
        for msg := range WiringPiISR(PIN_GPIO_6, INT_EDGE_FALLING) {
            if msg > -1 {
                n := time.Now().UnixNano() / 1000000
                delta := n - last_time
                if delta > 300 { //如果两次按键变化的间隔时间<300ms,是因为接触信号不稳定可以忽略掉
                    last_time = n
                    btn_pushed++
                    screen_chan <- btn_pushed % total_mode
                }
            }
        }
    }()

    //a goroutine: 根据管道消息刷新屏幕
    go loop_update_display()

    //选择确实的屏幕内容脚本编号
    screen_chan <- 0

    //a goroutine: 定时5s向管道发送更新屏幕内容的信号
    ticker := time.NewTicker(5 * time.Second)
    go func() {
        for {
            <-ticker.C
            screen_chan <- -1
        }
    }()
    
    ... 
}

func loop_update_display() {
    current_screen := 0
    for msg := range screen_chan {
        switch_screen := false
        if msg >= 0 {
           //说明是按钮触发的消息,而不是定时器触发的(-1)
            if msg != current_screen {
                //btn pushed
                current_screen = msg
                switch_screen = true
                go func() {
                    select {
                    case <-time.After(500 * time.Millisecond):
                        display_loading()
                        <-switch_chan
                    case <-switch_chan:
                    }
                }()
            }
        }
        switch current_screen {
        case 0:
            display_screen0()
        case 1:
            display_screen1()
        case 2:
            display_screen2()
        }
        if switch_screen {
            switch_chan <- true
        }
    }
}

Go语言内存模型 2013-04-20

名词定义

执行体 - Go里的Goroutine或Java中的Thread

背景介绍

内存模型的目的是为了定义清楚变量的读写在不同执行体里的可见性。理解内存模型在并发编程中非常重要,因为代码的执行顺序和书写的逻辑顺序并不会完全一致,甚至在编译期间编译器也有可能重排代码以最优化CPU执行, 另外还因为有CPU缓存的存在,内存的数据不一定会及时更新,这样对内存中的同一个变量读和写也不一定和期望一样。

Java的内存模型规范类似,Go语言也有一个内存模型,相对JMM来说,Go的内存模型比较简单,Go的并发模型是基于CSP(Communicating Sequential Process)的,不同的Goroutine通过一种叫Channel的数据结构来通信;Java的并发模型则基于多线程和共享内存,有较多的概念(violatie, lock, final, construct, thread, atomic等)和场景,当然java.util.concurrent并发工具包大大简化了Java并发编程。

Go内存模型规范了在什么条件下一个Goroutine对某个变量的修改一定对其它Goroutine可见。

Happens Before

在一个单独的Goroutine里,对变量的读写和代码的书写顺序一致。比如以下的代码:

package main

import (
    "log"
)

var a, b, c int

func main() {
    a = 1
    b = 2
    c = a + 2
    log.Println(a, b, c)
}

树莓派I2C编程 2013-04-18

(!未完!)

除了SPI协议外,树莓派还支持I2C。I2C是为了连接低速周边装置设计的,只需要用两根线(SDA和SCL,也就是树莓派的端口8和9-wiringPi编号)。

I2C

上图是一个主控使用I2C驱动3个设备的示意图

参考链接

  1. http://zh.wikipedia.org/wiki/I²C
  2. https://projects.drogon.net/raspberry-pi/wiringpi/i2c-library/

使用Go语言在树莓派上编程 2013-04-14

WiringPi是树莓派上比较好的一个开发库,是用C语言写的。使用cgo,我们可以在Go语言里方便的调用WiringPI的函数,于是我包装了一个WiringPi-Go,目前支持wiringPi的基本功能,硬件SPI协议驱动Nokia 5110屏幕,以及中断,未来还会增加PWM和I2C协议的支持。

下面是一个完整的使用例子,结合了之前的两个电路:链接1链接2

通过push button可以切换液晶屏显示不同脚本的输出内容。

lcd_switch.go

package main

import (
    . "github.com/hugozhu/rpi"
    "github.com/hugozhu/rpi/pcd8544"
    "log"
    "os/exec"
    "time"
)

const (
    DIN        = PIN_MOSI
    SCLK       = PIN_SCLK
    DC         = PIN_GPIO_2
    RST        = PIN_GPIO_0
    CS         = PIN_CE0
    PUSHBUTTON = PIN_GPIO_6
    CONTRAST   = 40 //may need tweak for each Nokia 5110 screen
)

var screen_chan chan int
var TOTAL_MODES = 3

func init() {
    WiringPiSetup()
    pcd8544.LCDInit(SCLK, DIN, DC, CS, RST, CONTRAST)
    screen_chan = make(chan int, 1)
}

func main() {
    //a goroutine to check button push event
    go func() {
        last_time := time.Now().UnixNano() / 1000000
        btn_pushed := 0
        for pin := range WiringPiISR(PUSHBUTTON, INT_EDGE_FALLING) {
            if pin > -1 {
                n := time.Now().UnixNano() / 1000000
                delta := n - last_time
                if delta > 300 { //software debouncing
                    log.Println("btn pushed")
                    last_time = n
                    btn_pushed++
                    screen_chan <- btn_pushed % TOTAL_MODES //switch the screen display
                }
            }
        }
    }()

    //a groutine to update display every 5 seconds
    go loop_update_display()

    //set screen 0 to be default display
    screen_chan <- 0

    ticker := time.NewTicker(5 * time.Second)

    for {
        <-ticker.C
        screen_chan <- -1 //refresh current screen every 5 seconds
    }
}

func loop_update_display() {
    current_screen := 0
    for screen := range screen_chan {
        if screen >= 0 {
            if screen != current_screen {
                //btn pushed
                current_screen = screen
                display_loading()
            }
        }
        switch current_screen {
        case 0:
            display_screen0()
        case 1:
            display_screen1()
        case 2:
            display_screen2()
        }
    }
}

func display_loading() {
    pcd8544.LCDclear()
    pcd8544.LCDdrawstring(0, 20, "Loading ...")
    pcd8544.LCDdisplay()
}

func display_screen0() {
    out, err := exec.Command("/bin/screen_0.sh").CombinedOutput()
    if err != nil {
        out = []byte(err.Error())
    }

    pcd8544.LCDclear()
    pcd8544.LCDdrawstring(0, 0, string(out))
    pcd8544.LCDdisplay()
}

func display_screen1() {
    out, err := exec.Command("/bin/screen_1.sh").CombinedOutput()
    if err != nil {
        out = []byte(err.Error())
    }

    pcd8544.LCDclear()
    pcd8544.LCDdrawstring(0, 0, string(out))
    pcd8544.LCDdisplay()
}

func display_screen2() {
    out, err := exec.Command("/bin/screen_2.sh").CombinedOutput()
    if err != nil {
        out = []byte(err.Error())
    }

    pcd8544.LCDclear()
    pcd8544.LCDdrawstring(0, 0, string(out))
    pcd8544.LCDdisplay()
}

使用tsar记录和监控树莓派CPU温度 2013-04-13

夏天到了,树莓派的CPU温度也开始节节攀升,虽然我们也可以用云服务cosm来监控,但每5分钟采样一次精度不够高,每分钟采样一次则上传次数又太多了点。最好的方法还是使用tsar这样的工具本地高频(如每1分钟)采样,然后再定时将5分钟的均值上传到cosm绘图。

Tsar是淘宝的一个用来收集服务器系统和应用信息的采集报告工具,如收集服务器的系统信息(cpu,mem等),以及应用数据(nginx、swift等),收集到的数据存储在服务器磁盘上,可以随时查询历史信息,也可以将数据发送到nagios报警。Tsar能够比较方便的增加模块,只需要按照tsar的要求编写数据的采集函数和展现函数,就可以把自定义的模块加入到tsar中。

更新

[2013-04-14] mod_rpi已经被合并到了主干代码:https://github.com/alibaba/tsar/blob/master/modules/mod_rpi.c 只需要增加文件:/etc/tsar/conf.d/rpi.conf,内容为以下即可开始使用mod_rpi模块:

mod_rpi on

####add it to tsar default output
output_stdio_mod mod_rpi

mod_rpi模块开发方法

首先按照安装说明,见https://github.com/alibaba/tsar将tsar和tsardevel安装好。

备份Raspberry Pi 2013-04-08

树莓派的操作系统安装在SD卡,使用一段时间后还是很有必要备份一下,以防哪天SD卡就坏了。

备份的目的地最方便的还是使用网络存储,我使用的是西部数据的MyBooklive3T网络硬盘。挺不错的一个产品,功能基本满足我的需求。

准备好备份目标盘,将Nas的备份目录mount到树莓派:

mkdir /mnt/backup
mount -t cifs //mybooklive/Public/Backup /mnt/backup -o guest

完整备份

确定相应的SD卡设备ID

root@raspberrypi2 ~/bin # fdisk -l

Disk /dev/mmcblk0: 1973 MB, 1973420032 bytes, 3854336 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x0004f23a

        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1   *        2048      186367       92160    c  W95 FAT32 (LBA)
/dev/mmcblk0p2          186368     3667967     1740800   83  Linux

Disk /dev/sda: 2107 MB, 2107637760 bytes, 4116480 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes