shihezichen 发表于 2017-1-17 19:34:41

树莓派使用PCF8574扩展GPIO口成功实践和代码共享

本帖最后由 shihezichen 于 2017-1-17 22:16 编辑

使用树莓派很容易发生GPIO不够用的情况.
例如我做智能小车, 需要三个超声波探头/两个红外探头/2个电机控制板, 所需的GPIO口远多于当前树莓派能提供的接口.

此时有两种解决方法:
1. 使用树莓派做上位机, Arduino做下位机, 通过Arduion来对接各种外设, 然后树莓派再通过串口与Arduion通信. 完成各种数据的采集.
2. 使用PCF8574T扩展树莓派的GPIO口(理论上PCF8575也是可以的, 而且一片可以扩充出16个口)

本文讲述如何使用PCF8574T扩展的方式.

1) PCF8574模块的购买:
   在某宝购买即可, 大概十几块钱
2) PCF8574与树莓派连接:
   本质上就是I2C设备的连接, PCF8574作为slave接入树莓派.
   连接: VCC, GND 连接树莓派的+5V口和GND, SDA和SCL连接树莓派的P02(SDA1)和P03(SCL1)
   若有多个PCF8574, 则顺序串接到前一个PCF8574的尾部插槽上.

3) PCF8574上的跳线:
   即A0,A1,A2.如果只有一片PCF8574, 可以保持默认跳线不动;此时它在树莓派总线上的地址为0x20;
   若有多片PCF8574,则需要跳线使得它们地址不同.
   三个跳线帽, 可以产生8种组合,因此最多可以串接8片PCF8574, 总共扩展出8*8=64个口.

3)外设与PCF8574的连接:
   电机驱动板:
         以L298N为例, 它有两种口, 一种是IN口, 一种是END口.IN口可以通过PCF8574控制;END口需要输入PWM信号, 所以我用树莓派GPIO口来直接控制(若有人试验成功, 请告知我如何通过PCF8574设置PWM,我理解是不行的)
    超声波模块:
         以HC-SR04为例,它有两种口, 一种是Echo口, 一种是Trig口. 其中Echo口需要不断检测高低电平,而且存在电平自动变化的情况. 这个在I2C总线上估计是做不到的(若有人试验成功, 请告知我如何通过PCF8574持续监测某一个pin的高电平变为低电平,我理解是不行的).
         因此, Echo口直接连接树莓派GPIO, Trig口可以连接到PCF8574上.
   其他模块:
         类似的,只要是对端口是高低电平控制的, 都可以通过PCF8574来控制,减少对树莓派GPIO口的占用.

硬件连接完毕后. 下面进入到程序部分:

写了三个程序文件:
1) i2c_io.py : 负责对i2c的端口进行读写的类
2) general_io.py: 提供G_IO类, 对i2c端口和GPIO端口进行了统一封装, 上层使用者可以不再区分树莓派主板自带的GPIO端口和PCF8574T扩展出来的IO口, 可使用相同的函数进行访问.
3) l298N.py: 对电机的操作类, Moto_Ctrl.对电机进行前进/后退/左右转向的操作, 以及PWM调速.会用到general_io.py中的G_IO类.

第一个文件:i2c_io.py

#!/usr/bin/python
#coding=utf-8

# Author: 边城量子(QQ:1502692755)

# write and read the pin of PCF8574 which connect to raspberry pi over the i2c bus.
#only support the write and read of the pin;
#will support the event in the future

import smbus
from log import debug, log


class I2C_IO:
    # i2c_index:the i2c chip index ,
    #    using '$ sudo i2cdetect -l' to check how many i2c chip
    # i2c_addr: the device address, e.g. 0x20, 0x23 , etc.
    #    usging '$ sudo i2cdetect -1' to check how many devices at chip 1
    def __init__(self, i2c_index, i2c_addr):
      self.index= i2c_index
      self.addr = i2c_addr
      self.bus = smbus.SMBus(i2c_index)

    # pin: pin number,from 0~7
    # value: 0/1 or False/True or LOW/HIGH
    #only modify the bit according to the pin
    def output(self, pin, value):
      data = self.bus.read_byte( self.addr )
      if value == 1:
            value = (1 << pin) | data
      else:
            value = (~(1 << pin)) & data
      debug("I2C_IO::output(): old:%s,new: %sto pin %s" % (bin(data), bin(value),pin) )
      return self.bus.write_byte( self.addr, value)

    # pin: pin number,from 0~7
    # return:
    #   the state of the pin, HIGH or LOW
    #   only return the bit according to the pin
    def input(self, pin):
      data = self.read_byte(self.addr)
      mask = i<<pin
      if mask & data == mask:
            return 1
      else:
            return 0

    def setup(self, pin, direction):
      pass


第二个文件:general_io.py


#!/usr/bin/python
#coding=utf-8

# Author: 边城量子(QQ:1502692755)
# suport write and read for GPIO and I2C IO using the unify class and functions
#the GPIO port like12, 26, or "12","26"
#the I2C port like"I7","I3", must starts with "I"


import i2c_io
import RPi.GPIO as GPIO
from log import debug, log


class G_IO:
      # GPIO_mode:GPIO_BCM or GPIO_BOARD
      # i2c_index:the i2c chip index ,
      #    using '$ sudo i2cdetect -l' to check how many i2c chip
      # i2c_addr: the device address, e.g. 0x20, 0x23 , etc.
      #    usging '$ sudo i2cdetect -1' to check how many devices at chip 1
      def __init__(self, GPIO_mode=None, i2c_index=None, i2c_addr=None):
                if i2c_index != None and i2c_addr != None:
                        self.i2c_io = i2c_io.I2C_IO( i2c_index, i2c_addr)
                if GPIO_mode != None:
                        GPIO.setmode(GPIO_mode)

      # using the 'port' parameter to determin whether it is a GPIO port
      def _is_GPIO(self, port):
                # it is the GPIO port when its type is int
                if type(port) == type(1):
                        return True
                elif type(port) == type("str"):
                        # it is the GPIO port when it can be translate to int,
                        #because the i2c io start with 'I'
                        try:
                              int(port)
                              return True
                        except ValueError:
                              return False
                else:
                        error( "G_IO::_is_GPIO():Error! the type of 'port' should be str or int. now is %s. " % port)
                        return None

      # get the port value from a port string like 'I2', 'I7'
      def get_port_value(self, port):
                return int( port )


      # setup the io direction, only effect to GPIO io
      def setup(self, port, direction ):
                #debug("G_IO::setup(): set direciton %d for port %s" % (direction,port) )
                if self._is_GPIO(port):
                        GPIO.setup(port, direction)
                else:
                        self.i2c_io.setup(port, direction)

      # output to the port
      # the value only suport the HIGH or LOW
      def output(self, port, value):
                debug("G_IO::output(): set output value %d for port %s" % (value,port) )
                if self._is_GPIO(port):
                        GPIO.output(port, value)
                else:
                        port = self.get_port_value(port)
                        self.i2c_io.output(port,value)

      # input from a port
      def input(self, port):
                debug("G_IO::output(): get input value from port %s" % (port) )
                if self._is_GPIO(port):
                        return GPIO.input(port)
                else:
                        port = self.get_port_value(port)
                        return self.i2c_io.input(port)


第三个文件:L298N.py

#!/usr/bin/python
#coding=utf-8

# Author: 边城量子(QQ:1502692755)
# 树莓派通过PCF8574来控制L298N控制的例子, 包括PWM调速功能
# 属于智能语音控制树莓派小车的车辆控制部分

#连接方式:
#ENDx口直接连树莓派GPIO
#INx口 连接PCF8574的pin口, 然后PCF8574通过I2C连接到树莓派I2C接口

#控制方式:
#1. 针对ENDx口的PWM调速,直接通过GPIO口下发.
#2. 针对INx口的电机转向控制, 通过PCF8574下发.
#以上两种方式的控制,都通过general_io库封装对调用者不可见, 使用统一G_IO接口:
#   general_io库会自动识别,若端口是"I2"、'I7'这种类型,则使用i2c方式设置端口,
#   否则使用GPIO方式


#端口标识: 其中I0,I2这种表示通过PCF8574连接到树莓派; 20,16这种方式表示直连GPIO

'''
use two L298N to control 4 engine
CTRL 1:
          ENDA   1    2    3    4    ENDB
          黄   橙   红棕   黑    白
BCM:P21    I0   I1I2   I3    P20

CTRL 2:
          ENDA 1    2    3    4    ENDB
          黑   白   灰   紫   蓝   绿
BCM:P12I7   I6   I5   I4   P16

'''


import RPi.GPIO as GPIO   # 使用GPIO常量, 例如GPIO.HIGH
from general_io import G_IO# I2C接口和GPIO接口 统一调用库
from log import debug, log# 日志
import time


CTRL1 = { 'ENDA':21, 'IN1':'I0', 'IN2':'I1', 'IN3':'I2', 'IN4':'I3', 'ENDB':20   }
CTRL2 = { 'ENDA':12, 'IN1':'I7', 'IN2':'I6', 'IN3':'I5', 'IN4':'I4', 'ENDB':16   }


class Moto_Ctrl:
      def __init__(self):
                #
                # GPIO_mode: GPIO.BCM or GPIO.BOARD, 若用到GPIO端口需设置
                # i2c_index: 树莓派3一般是1, 表示chip. i2cdetect -l查到
                # i2c_addr: i2c 设备的地址, 例如0x20, 此处为PCF8574的I2C地址.
                #         可使用i2cdetect -1 查到(其中1代表i2c_index的值)
                self.gio = G_IO( GPIO_mode=GPIO.BCM, i2c_index=1, i2c_addr=0x20)
                for key in CTRL1:
                        port = CTRL1
                        #debug("Moto_Ctrl::__init__(): set GPIO.OUT for port %s" % port )
                        self.gio.setup(port,GPIO.OUT)

                for key in CTRL2:
                        port = CTRL2
                        #debug("Moto_Ctrl::__init__(): set GPIO.OUT for port %s" % port )
                        self.gio.setup(port,GPIO.OUT)
      
                # make all output port LOW
                self.stop()

                # setup the pwm for speed controller
                self.pwms = []
                # four control channels from two L198N controller
                channels =[ CTRL1["ENDA"], CTRL1["ENDB"], CTRL2["ENDA"], CTRL2["ENDB"] ]      
                for channel in channels:
                        debug("Moto_Ctrl::__init__(): set pwm for channel %s" % channel )
                        # the frequency is set to 150HZ
                        p = GPIO.PWM( channel , 150)
                        # the duty cycle is init to zero
                        p.start( 0 )
                        self.pwms.append( p )

      def __del__(self):
                self.pwms = []
                self.gio = None

      # set the speed
      # duty_cycle:
      def set_speed(self, duty_cycle):
                # set the speed using the duty_cycle
                for p in self.pwms:
                        debug("Moto_Ctrl::set_speed(): set ducy cycle for channel %s" % p )
                        p.ChangeDutyCycle( duty_cycle )

      #stop
      def stop(self):
                for key in CTRL1:
                        if key.startswith("IN"):
                              port = CTRL1
                              #debug("Moto_Ctrl::stop(): set GPIO.LOWport %s" % port )
                              self.gio.output(port,GPIO.LOW)

                for key in CTRL2:
                        if key.startswith("IN"):
                              port = CTRL2
                              #debug("Moto_Ctrl::stop(): set GPIO.LOWport %s" % port )
                              self.gio.output(port,GPIO.LOW)      

      
      def left_forward(self):
                # left rear wheels
                self.gio.output( CTRL2["IN3"], GPIO.LOW )
                self.gio.output( CTRL2["IN4"], GPIO.HIGH )
                # lefe front wheels
                self.gio.output( CTRL2["IN1"], GPIO.HIGH )
                self.gio.output( CTRL2["IN2"], GPIO.LOW )

      def rigth_forward(self):
                # right rear wheels
                self.gio.output( CTRL1["IN1"], GPIO.HIGH )
                self.gio.output( CTRL1["IN2"], GPIO.LOW )
                # right front wheels
                self.gio.output( CTRL1["IN3"], GPIO.HIGH )
                self.gio.output( CTRL1["IN4"], GPIO.LOW )

      def right_backward(self):
                # right rear wheels
                self.gio.output( CTRL1["IN1"], GPIO.LOW )
                self.gio.output( CTRL1["IN2"], GPIO.HIGH )
                # right front wheels
                self.gio.output( CTRL1["IN3"], GPIO.LOW )
                self.gio.output( CTRL1["IN4"], GPIO.HIGH )

      def left_backward(self):
                # left rear wheels
                self.gio.output( CTRL2["IN3"], GPIO.HIGH )
                self.gio.output( CTRL2["IN4"], GPIO.LOW )
                # lefe front wheels
                self.gio.output( CTRL2["IN1"], GPIO.LOW )
                self.gio.output( CTRL2["IN2"], GPIO.HIGH )
      
      # 控制小车行为,对外暴露接口
      # action: string, 'forward','backwoard','turnleft','turnright'
      #         'back_turnright', 'back_turnleft', 'stop'
      # speed: int, range
      # duration: 持续时间, 若不传入, 则持续时间由外部控制
      def control(self, action, speed, duration=None):
                self.set_speed( speed )
                if action == 'forward':
                        debug("Moto_Ctrl::control(): forward")
                        self.left_forward()
                        self.rigth_forward()
                elif action == 'backward':
                        debug("Moto_Ctrl::control(): backward")
                        self.right_backward()
                        self.left_backward()
                elif action == 'turnleft':
                        debug("Moto_Ctrl::control(): turn left")
                        self.stop()
                        self.rigth_forward()
                elif action == 'turnright':
                        debug("Moto_Ctrl::control(): turn right")
                        self.stop()
                        self.left_forward()
                elif action == 'back_turnright':
                        debug("Moto_Ctrl::control(): back turn right")
                        self.stop()
                        self.left_backward()
                elif action == 'back_turnleft':
                        debug("Moto_Ctrl::control(): back turn left")
                        self.stop()
                        self.right_backward()                        
                elif action == 'stop':
                        debug("Moto_Ctrl::control(): turn right")
                        self.stop()
                ifduration != None:
                        time.sleep(duration)
               
if __name__ == '__main__':
      ctrl = Moto_Ctrl()
      ctrl.control('backward', 90, 3)

      ctrl.control('turnright', 60, 3)

      ctrl.control('turnleft', 60, 3)

      ctrl.control('forward', 60, 3)

      ctrl.control('back_turnleft', 60, 3)

      ctrl.control('back_turnright', 60, 3)

      
      GPIO.cleanup()


my2jia 发表于 2017-1-18 00:12:32

谢谢分享!

yy918 发表于 2017-1-19 11:46:46

谢谢分享!

Excellence 发表于 2017-1-20 09:01:56

谢谢分享。。。

gmyu 发表于 2017-1-20 09:41:19

IIC速度没有SPI快

ghostxdy 发表于 2017-1-20 10:18:43

gmyu 发表于 2017-1-20 09:41
IIC速度没有SPI快

兄台有SPI扩展IO的方案?

avr-arm 发表于 2017-1-20 20:34:11

I3C一出来,就更屌了

kevin_me 发表于 2017-1-20 22:47:56

avr-arm 发表于 2017-1-20 20:34
I3C一出来,就更屌了

哥们你是在开玩笑,还是我太孤陋寡闻了,还有I3C???

donglaile 发表于 2017-1-21 09:44:09

kevin_me 发表于 2017-1-20 22:47
哥们你是在开玩笑,还是我太孤陋寡闻了,还有I3C???

有的啊,不信你去查查
页: [1]
查看完整版本: 树莓派使用PCF8574扩展GPIO口成功实践和代码共享