背景介绍
前面的文章(见参考链接)已经介绍了如何使用按键作为树莓派的输入。在实际应用中可以通过按下按键循环显示预先设定的脚本输出到显示屏幕,需求如下:
- 如果按键不被触动,则定时5秒执行脚本获取最新内容显示;
- 因为不同的脚本获取内容速度会不一样,我们要求如果超过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里的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)
}
(!未完!)
除了SPI协议外,树莓派还支持I2C。I2C是为了连接低速周边装置设计的,只需要用两根线(SDA和SCL,也就是树莓派的端口8和9-wiringPi编号)。
I2C

上图是一个主控使用I2C驱动3个设备的示意图
参考链接
- http://zh.wikipedia.org/wiki/I²C
- https://projects.drogon.net/raspberry-pi/wiringpi/i2c-library/
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()
}
夏天到了,树莓派的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安装好。
背景介绍
树莓派的GPIO引脚不仅可以输出高低电平,也可以当做输入端口(可以想象成键盘输入),当GPIO接入的是高电平,GPIO的值可以认为是1,如果是低电平则是0。如下图所示,可以使用一个Push Button开关按键来控制GPIO 25(BCM Numbering)的高低电平以达到控制的目的。

GPIO 25和VCC(3.3V)之间通过R1(10K欧姆)和R2(1K欧姆)上拉电阻相连,当按键未被按下时,GPIO 25上拉到VCC,程序可以读到1,当按键按下时,GPIO 25被下拉电阻R2拉到GND(0V),程序可以读到0。如果不加R1,而GPIO 25不小心被设置成输出低电平时,将直接和VCC相连而造成短路,这样可能会烧掉这个引脚,所以加上限流电阻R1后,即使发生这样的情况,也不会出现短路情况。
应用
如果我们需要根据GPIO 25的值来控制树莓派,比如按下按钮时希望点亮某个LED或在液晶上显示当前时间,就需要通过程序来获取状态的变化。
一种常见的做法是在循环里不断读取该引脚的状态,当发生对应的变化的时执行控制逻辑,但显而易见,这种做法很消耗CPU,如果在循环增加sleep(1000)
这样的调用,又很容易错过按键变化。较好的做法则是通过中断来实现。
最新的树莓派Raspbian和Arch Linux内核都已经包含了GPIO的中断处理支持。但使用前需要将指定GPIO引脚输出,方法如下:
首先可以通过命令echo 25 > /sys/class/gpio/export
导出GPIO 25端口,执行成功后在相应的目录下看到以下文件,得益于Linux下一切都是文件的设计理念,GPIO的状态可以通过value
文件来获取,这样就可以利用Linux的poll/epoll来获取value
文件的变化(这点和Linux高性能网络编程是类似的)。
树莓派的操作系统安装在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
什么是SPI
SPI (Serial Peripheral Interface),是一种高速,全双工,同步的通信总线协议,基于SPI的设备需要4根线:

- SDO / MOSI - 主设备数据输出,从设备数据输入
- SDI / MISO - 主设备数据输入,从设备数据输出
- SCLK / CLK - 时钟信号,由主设备产生
- CS / SS - 从设备使能信号,由主设备控制
通过CS,主设备可以控制和哪个从设备通信。
Bit Banging
Bit-banging是一种用软件替代专职硬件的串行通信的技术。软件直接对微处理器的管脚的状态进行设置和采样,其功能涵盖诸如:时钟,电平,同步等所有参数。与此不同的是(传统的串行通信技术中),专职硬件诸如 modem、UART 或者 位移寄存器等一般是用来处理这些参数并且提供一个(缓存)的数据接口,软件在这种情况下同信号处理无关。
bit-banging 具有明显优点诸如:让相同的设备运行不同的协议而只需很小的(甚至不需)硬件的改动。借助很少的额外设备,我们也许可以从数字管脚(数字终端)可以得到视频信号。
前面提到了有关个人网站的实时在线人数问题,本文要讨论的是如何自己来实现一个这样的统计服务。因为网站也同时部署在Github上,海外用户访问Github镜像网站的访问日志Pi是拿不到的,这怎么办?
Google Channel Service
Google Channel Service允许应用和GAE (Google App Engine) 保持一个长连接,允许应用实时发送消息给JavaScript客户端,而不用让客户端用效率很低的定时轮询获取新消息。这个服务是允许有多个发布者和多个订阅者,也能创建多个主题来关联发布者和订阅者。
使用这个服务分两步:
客户端请求服务器端(部署在GAE上)获取一个Channel的Token:

客户端根据Channel Token和服务器建立长连接,并开始接收消息,这时其它的客户端(或服务器端)可以想这个通道发送消息

在线人数统计实现
在页面上部署beacon
通常的网站流量统计是依赖部署在页面上的beacon(Javascript或图片标签)来实现的,这样做的好处是可以直接过滤掉一些机器流量,并且可以将日志集中存储在日志收集服务器上,和网站分离开。
于是可以利用GAE实现一个简单的Beacon服务,这里采用Go语言来实现,用Java或Python也是可以的。
今天拿到一块Arduino UNO R3板,迫不及待就开始试用了。相比Raspberry Pi是一个全能的电脑,Arduino则是个硬件开源的单片机,因为开源,资料和配件网上就很很多了,也就容易让初学者上手了。
Arduino特点:
- 开源,硬件标准化,配套传感器等模块很多;
- 结构简单
- 实时系统,稳定,启动只要0.5秒
Arduino IDE
下载Arduino IDE
上电测试
用USB线接在电脑USB口,然后在GND和PIN 13上插一个二极管,注意二极管正极插在PIN 13上, 如下图:

(注:还应该串联一个300欧姆的限流电阻才保险!)
上传代码
在Arduino IDE编辑好下面的代码,然后点Upload后就会运行了,会看到LED一闪一闪。