go gpio


原文链接: go gpio

Linux下用文件IO的方式操作GPIO(/sys/class/gpio) - 卢小喵的学习笔记 - CSDN博客
Golang笔记–04–GPIO与中断 - wbuntu - Do you remember?
linux/gpio.txt 中文文档
https://github.com/brian-armstrong/gpio

gpio epoll in
https://github.com/SpaceLeap/go-embedded

/* Bit 0 express polarity */
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1

/* Bit 1 express single-endedness */
#define GPIO_PUSH_PULL 0
#define GPIO_SINGLE_ENDED 2

/*

  • Open Drain/Collector is the combination of single-ended active low,
  • Open Source/Emitter is the combination of single-ended active high.
    */
    #define GPIO_OPEN_DRAIN (GPIO_SINGLE_ENDED | GPIO_ACTIVE_LOW)
    #define GPIO_OPEN_SOURCE (GPIO_SINGLE_ENDED | GPIO_ACTIVE_HIGH)
    常会见到两种模式:

    推挽(push-pull)

    优点:(1)可以吸电流,也可以贯电流;(2)和开漏输出相比,push-pull的高低电平由IC的电源低定,不能简单的做逻辑操作等。
    缺点:一条总线上只能有一个push-pull输出的器件;

    开漏(open-drain,漏极开路)

    优点:
    (1)对于各种电压节点间的电平转换非常有用,可以用于各种电压节点的Up-translate和Down-translate转换
    (2)可以将多个开漏输出的Pin脚,连接到一条线上,形成“与逻辑”关系,即“线与”功能,任意一个变低后,开漏线上的逻辑就为0了。这也是I2C,SMBus等总线判断总线占用状态的原理。
    (3)利用 外部电路的驱动能力,减少IC内部的驱动。当IC内部MOSFET导通时,驱动电流是从外部的VCC流经R pull-up ,MOSFET到GND。IC内部仅需很小的栅极驱动电流。
    (4)可以利用改变上拉电源的电压,改变传输电平,如图, IC的逻辑电平由电源Vcc1决定,而输出高电平则由Vcc2决定。这样我们就可以用低电平逻辑控制输出高电平逻辑了。

缺点:开漏Pin不连接外部的上拉电阻,则只能输出低电平。当输出电平为低时,N沟道三极管是导通的,这样在Vcc'和GND之间有一个持续的电流流过上拉电阻R和三极管Q1。这会影响整个系统的功耗。采用较大值的上拉电阻可以减小电流。但是,但是大的阻值会使输出信号的上升时间变慢。即上拉电阻R pull-up的阻值 决定了逻辑电平转换的沿的速度。阻值越大,速度越低功耗越小。反之亦然。

前言[](#前言)

这段时间对 Go 语言的使用依旧是对接物联网设备,解析协议与管理连接,同时 review 了一下之前同事写的代码改 bug,踩到了 MySQL 的坑,具体放到下一篇来讲。

另外本周在树莓派上通过串口调试模块,一开始选择了 C 语言,接着又切到 Python,依旧卡在配置开发环境,一些树莓派上的 C Native 库在 macOS 无法编译通过,最后还是使用 Go,两天时间写代码和调试,用 Go 来控制 GPIO 和串口真的非常方便。

Golang 面向对象[](#golang面向对象)

在 iOS 及 macOS 中面向对象是天生的,操作的都是对象,对象都是指针类型,很多人也从运行时源码详细解析了 Objective-C 面向对象原理,而 Golang 在语法上就没有继承的概念,不过可以嵌入和接口来实现。

假定现在有一种数据包 Packet,Packet 可能有响应 BytesResp,类型为 []byte,不通类型的数据包具有各自的解析方式、推送后台接口 URL、JSON 数据格式、插入数据库的 SQL,但他们具有相同的头部(包含设备信息、数据域长度、校验值等等)、起始符和终止符,设备通过 TCP 长连接与程序保持通信,引用上一篇 Golang 笔记中的例子,我们在拿到二进制数据后,解析数据,并返回响应。

p, err := model.NewPacketWithByte(data)
if err != nil {
    log.WithField("err", err).Info("数据解析错误")
    continue
}
pushData(p.JSONData(), p.PushURL())
go func() {
    err = dataBase.InsertPacket(p)
    if err != nil {
        log.WithField("err", err).Info("插入数据失败")
    }
}()
resp := p.BytesResp()
if resp != nil {
    conn.Write(resp)
}

第一行代码从二进制数据解析到一个实现了 Packet 接口的对象,定义如下

type Packet interface {
    InsertSQL() string
    BytesData() []byte
    BytesResp() []byte
    JSONData() []byte
    PushURL() string
}

最初的做法是在第一行解析数据时,返回不同的 struct,每个 struct 包含一个 Header 字段,各自实现 Packet 定义的方法,而设备通信协议中定义了大概 16 种类型,按照一个请求对应一个响应,总共定义了 32 种 struct,编写和改动都很麻烦,十分冗杂。而且对于来自设备的主动请求,总需要在最后返回响应,对于服务器主动下发的请求,获得设备响应后并不需要再返回响应,一些数据只需要记录头部信息与原始数据,另一部分则需要逐字节解析并入库。

原先的 Header 以及心跳包 HeartBeat 定义如下,HeartBeat 实现了 Packet 定义的全部方法,多了很多冗余的代码。

type Header struct {
    DevEUI          string `json:"DevEUI"`
    ControlCode     string `json:"ControlCode"`
    DataFieldLength int    `json:"DataFieldLength"`
    CS              int    `json:"CS"`
}
type HeartBeat struct {
    Header                Header  `json:"Header"`
    UTCTime               string  `json:"UTCTime"`
    Latitude              float64 `json:"Latitude"`
    Longitude             float64 `json:"Longitude"`
    Preserve              string  `json:"Preserve"`
}
func (h *HeartBeat) InsertSQL() string {
        ......
}
......
func (h *HeartBeat) PushURL() string {
        ......
}

HeartBeat 实现了 Packet 定义的全部方法,可以当作 Packet 使用,但如果 Header 实现了 Packet 的全部方法并作为匿名字段嵌入 HeartBeat,那么 HeartBeat 也会获得 Header 的全部方法,如果在 HeartBeat 上重新实现 Packet 定义的 BytesResp() 方法,直接调用时就会覆盖内部 Header 的实现,但可以通过显式指定 Header 来调用,修改后如下。

type Header struct {
    DevEUI          string `json:"DevEUI"`
    ControlCode     string `json:"ControlCode"`
    DataFieldLength int    `json:"DataFieldLength"`
    CS              int    `json:"CS"`
}
type HeartBeat struct {
    Header               
    UTCTime               string  `json:"UTCTime"`
    Latitude              float64 `json:"Latitude"`
    Longitude             float64 `json:"Longitude"`
    Preserve              string  `json:"Preserve"`
}
func (h *Header) InsertSQL() string {
        ......
}
......
func (h *Header) PushURL() string {
        ......
}

通过在 Header 中提供了默认的接口实现,减少冗余代码,需要特殊处理的数据包可以自行实现接口,覆盖方法。

Golang 串口及 GPIO 编程[](#golang串口及gpio编程)

由于需要采集地磁场变化数据,如果让硬件工程师重新设计 PCB,打样加编程,估计这个月都搞不完,所以想到用树莓派来对接,树莓派 3 自带 40Pin 引脚,关于开启串口编程的功能也是整了很久,都可以写一篇文章来讲了,这里指记录下如何通过串口和 GPIO 调用模块。

手上的模块带有 12 个引脚,其中:

一个 VCC 接 3.3V 电源输入;

两个 GND 接地;

一个 RST 复位,低电平 5ms 以上有效;

一个 IO1 输入读取上位机状态,低电平时才发送数据;

一个 INTin 输入用于触发接受指令,下降沿触发后,等待 80ms 接受指令;

一个 INTout 输出用于触发上位机发送数据,下降沿触发后发送数据;

另外两个是 TX 和 RX,供串口读写,保留三个引脚。

用到两个第三方库:

github.com/stianeikeland/go-rpiogithub.com/tarm/serial分别操作 GPIO 以及串口。按照上述流程,程序启动后(1)初始化 GPIO;(2)初始化串口;(3)异步轮询 INTout,检测下降沿则开始读取数据;(4)通过发送命令发送,发送指令初始化模块。将上述的步骤用方法实现

下面是一些全局变量

const (
    io1    = uint8(5)
    rst    = uint8(6)
    intIn  = uint(13)
    intOut = uint(19)
)
var s *serial.Port
var outPin rpio.Pin
var inPin rpio.Pin

初始化 GPIO

go-rpio 默认使用 bcm 编码,将模块的四个引脚分别接到了对应的 GPIO 口上

func startGPIO() {
    err := rpio.Open()
    if err != nil {
        log.Fatal(err)
    }
    log.Info("open rpio successfully")
        // io1供模块读取,维持低电平
    io1Pin := rpio.Pin(io1)
    io1Pin.Output()
    io1Pin.Low()
        // rst一般情况下不触发,维持高电平
    rstPin := rpio.Pin(rst)
    rstPin.Output()
    rstPin.High()
        // intOut由模块输出,在树莓派3上设置为输入,并检测下降沿
    outPin = rpio.Pin(intOut)
    outPin.Input()
    outPin.Detect(rpio.FallEdge)
        // intOut由树莓派3向模块输出,下降沿触发,初始状态设置为高电平
    inPin = rpio.Pin(intIn)
    inPin.Output()
    inPin.High()

}

初始化串口[](#初始化串口)

成功开启树莓派 3 的串口编程后,默认设备为 /dev/ttyAMA0,大多数设备默认的数据位为 8,停止字为 1,校验为 None,波特率自行设置,读超时时间为可以用来设置非阻塞的串口读取。

func startSeril() {
    c := &serial.Config{Name: "/dev/ttyAMA0", Baud: 115200, ReadTimeout: time.Second * 5}
    p, err := serial.OpenPort(c)
    s = p
    if err != nil {
        log.Fatal(err)
    }
    log.Info("open serial port successfully")
}

异步轮询 INTout[](#异步轮询intout)

若检测到下降沿,开始读取串口,否则休眠 10ms。

读取串口,检测到结束符或读超时(上面设置为 5 秒)才停止读取,数据解析自行实现,最后清除串口缓冲区。

func startRead() {
    go func() {
        for {
                        // 检测下降沿
            if outPin.EdgeDetected() {
                read()
            } else {
                        // 主动休眠
                time.Sleep(time.Millisecond * 10)
            }
        }
    }()
}
func read() {
        // 开辟data用于存储数据
    data := []byte{}
        // 循环读写
    for {
                // 自行设定读缓冲区长度
        buf := make([]byte, 50)
        n, err := s.Read(buf)
        if err != nil {
            log.WithField("err", err).Info("read error")
            break
        }
        dataRead := buf[:n]
        data = append(data, dataRead...)
                // 检测到停止字
        if dataRead[n-1] == 0xfe || dataRead[n-1] == 0x21 {
            break
        }
    }
        if len(data) > 0 {
                // 解析数据
        decodeBytes(data)
    }
        // 清除串口缓存,包括未读取与未发送的数据
    s.Flush()
}

初始化模块[](#初始化模块)

这里需要编写一个发送命令方法,控制 GPIO 与串口。

func sendCMD(cmd string) bool {
    result := false
        // 拉低电平,触发下降沿
    inPin.Low()
        // 等待2ms后拉高电平
    time.Sleep(time.Millisecond * 2)
    inPin.High()
        // 休眠80ms
    time.Sleep(time.Millisecond * 80)
        // 通过串口下发指令,最多尝试3次,若失败则休眠2ms再继续
    data := []byte(cmd)
    for i := 0; i < 3; i++ {
        _, err := s.Write(data)
        if err != nil {
            log.WithField("err", err).Info("failed to send cmd: " + cmd)
            time.Sleep(time.Millisecond * 2)
        } else {
            log.Info("REQ: " + cmd)
            result = true
            break
        }
    }
    return result
}

在 main 方法中依次调用这些方法,完成对模块的初始化和调用,其他模块的对接应该也可以使用类似的流程。

总结[](#总结)

学习和使用了这么久的 Golang,花了漫长才理解如何面向对象,之前对接的设备,数据包之间都没有太大关联性,也就没有继承复用的情况,一门语言还是需要通过不断在各种场景下实战才能累积经验。

Golang 很适合树莓派,从底层地址操作到 Kubernetes 都能胜任,灵活而强大,主要还是配置和开发方便,在 PC 上写好代码,拷贝到树莓派上直接编译运行,另外可以接入 4G 模块,在户外采集处理后直接上报服务器,需要微调源码时也能直接在树莓派上操作,总之有很大的想象空间的。

使用Epoll读取GPIO的状态

package gpio

import (
	"fmt"
	"log"
	"os"
	"syscall"
)

const (
	EPOLLPRI      = 0x002
	EPOLL_CTL_ADD = 1
)

func GPIOInterrupt(number int) (ch chan bool, err error) {
	ch = make(chan bool, 1)
	if _, err := os.Open(fmt.Sprintf("/sys/class/gpio/gpio%d", number)); err != nil {
		log.Println("exporting")
		ef, err := os.OpenFile("/sys/class/gpio/export", os.O_WRONLY, 0666)
		if err == nil {
			ef.WriteString(fmt.Sprintf("%d\n", number))
			ef.Close()
		}
	}

	if ef, err := os.OpenFile(fmt.Sprintf("/sys/class/gpio/gpio%d/edge", number), os.O_WRONLY, 0666); err == nil {
		log.Println("setting edge")
		ef.Write([]byte("both"))
		ef.Close()
	}

	if f, err := os.Open(fmt.Sprintf("/sys/class/gpio/gpio%d/value", number)); err == nil {
		epfd, err := syscall.EpollCreate(1)
		if err != nil {
			return nil, err
		}
		ee := syscall.EpollEvent{EPOLLPRI, 0, int32(f.Fd()), 0}
		if err = syscall.EpollCtl(epfd, EPOLL_CTL_ADD, int(f.Fd()), &ee); err != nil {
			return nil, err
		}
		b := make([]byte, 1)
		if _, err := f.Read(b); err != nil {
			return nil, err
		}
		events := []syscall.EpollEvent{ee}
		go func() {
			for {
				if nr, err := syscall.EpollWait(epfd, events, -1); err != nil {
					log.Println("Error:", err)
					break
				} else if nr < 1 {
					continue
				}
				if _, err = f.Seek(0, 0); err != nil {
					log.Println("Error:", err)
					break
				}
				if _, err := f.Read(b); err != nil {
					log.Println("Error:", err)
					break
				}
				value := b[0] == '1'
				ch <- value
			}
			close(ch)
			f.Close()
		}()
	}
	return ch, nil
}

`