├── .gitignore ├── README.rdoc ├── i2c.gemspec ├── lib ├── i2c.rb └── i2c │ ├── backends │ └── i2c-dev.rb │ ├── drivers │ └── mcp230xx.rb │ └── i2c.rb ├── rules └── 88-i2c.rules └── test └── mcp230xx_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *~ 3 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = I2C - Ruby I2C library 2 | 3 | == About 4 | 5 | Interface to Linux I2C (a.k.a. TWI) implementations. Right now targeted at the 6 | Raspberry Pi, but should work with any linux i2c-dev I2C-hardware. 7 | 8 | == Structure 9 | 10 | The library is split into two parts: 11 | * A backend for accessing the I2C bus (right now only through the i2c-dev 12 | in Linux, other impementations are possible). 13 | * Drivers for I2C enabled ICs. 14 | 15 | == Installation 16 | 17 | To use the i2c-dev backend it is necessary to load the "i2c-dev" linux 18 | kernel module. This is not done automatically even if the module for the 19 | underlying I2C-hardware is. To automatically load the i2c-dev driver on startup 20 | add it to /etc/modules. Also the device file (usually /dev/i2c-0) must be 21 | user accessible. A working (at least on a RaspberryPi running Raspbian) udev 22 | rule file is available (rules/88-i2c.rules) and can be installed to 23 | /etc/udev/rules.d/. 24 | 25 | == Backends 26 | 27 | The backends are instantiated through the #I2C::create method. depending 28 | on the format of the passed bus descriptor the correct backend is invoked. 29 | 30 | Right now there is only a i2c-dev backend available 31 | 32 | === i2c-dev Backend 33 | 34 | Backend for the Linux I2C implementation. Accepts device file names as 35 | bus descriptors. E.g. 36 | 37 | I2C.create("/dev/i2c-0") returns an instance of I2C::Dev attached to the 38 | first I2c bus on the system. 39 | 40 | == Drivers 41 | 42 | === MCP23017 43 | 44 | 16 bit IO-Expander MCP23017 from Microchip. Provides a wiringpi-ruby compatible 45 | API and may therefore be used as a drop-in replacement for IO tasks on a 46 | Raspberry Pi. 47 | Datasheet: http://ww1.microchip.com/downloads/en/DeviceDoc/21952b.pdf 48 | 49 | === MCP23008 50 | 51 | 8 bit IO-Expander MCP23008 from Microchip. Basically a small version of the 52 | MCP23017. 53 | Datasheet: http://ww1.microchip.com/downloads/en/DeviceDoc/21919e.pdf 54 | 55 | == Acknowledgements 56 | 57 | The low-level IO (mainly in i2c-dev.rb) was extracted from Ruby-I2C 58 | (http://rubyforge.org/projects/i2c/) by Jonas Bähr 59 | 60 | === Contributions 61 | * [Gaëtan Duchaussois](https://github.com/gaetronik) I2C::Dev.read_byte. 62 | 63 | === Bugfixes 64 | * Pierre Paques 65 | 66 | == Copyright / Licence 67 | 68 | This code may be used under the terms of the GNU General Public Licence, Version 2. 69 | 70 | Copyright (c) 2018 David Bailey 71 | Copyright (c) 2012 Christoph Anderegg 72 | Copyright (c) 2008 Jonas Bähr 73 | -------------------------------------------------------------------------------- /i2c.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'i2c' 3 | s.version = '0.4.2' 4 | s.date = '2018-06-15' 5 | s.summary = "I2C access library (for Linux)." 6 | s.description = "Interface to I2C (aka TWI) implementations. Also provides abstractions for some I2c-devices. Created with the Raspberry Pi in mind." 7 | s.authors = ["Christoph Anderegg", "David Bailey"] 8 | s.email = ['davidbailey.2889@gmail.com', 'christoph@christoph-anderegg.ch'] 9 | s.license = 'GPL-3.0' 10 | 11 | 12 | s.files = [ "lib/i2c.rb", 13 | "lib/i2c/i2c.rb", 14 | "lib/i2c/backends/i2c-dev.rb", 15 | "lib/i2c/drivers/mcp230xx.rb", 16 | "test//mcp230xx_spec.rb", 17 | "rules/88-i2c.rules"] 18 | s.extra_rdoc_files = ['README.rdoc'] 19 | s.homepage = 'https://github.com/andec/i2c' 20 | end 21 | -------------------------------------------------------------------------------- /lib/i2c.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # I2C gem setup. 3 | # 4 | # Copyright (c) 2012 Christoph Anderegg 5 | # This file may be distributed under the terms of the GNU General Public 6 | # License Version 2. 7 | # 8 | 9 | require 'i2c/i2c.rb' 10 | require 'i2c/backends/i2c-dev.rb' 11 | require 'i2c/drivers/mcp230xx.rb' 12 | #require 'i2c/drivers/mcp23017.rb' 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/i2c/backends/i2c-dev.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # I2C - Linux i2c-dev backend. 3 | # 4 | # Copyright (c) 2012 Christoph Anderegg 5 | # Copyright (c) 2008 Jonas Bähr, jonas.baehr@fs.ei.tum.de 6 | # This file may be distributed under the terms of the GNU General Public 7 | # License Version 2. 8 | # 9 | module I2C 10 | class Dev 11 | # see i2c-dev.h 12 | I2C_SLAVE = 0x0703 13 | 14 | def self.create(device_path) 15 | raise Errno::ENOENT, "Device #{device_path} not found." unless File.exist?(device_path) 16 | @instances ||= Hash.new 17 | @instances[device_path] = Dev.new(device_path) unless @instances.has_key?(device_path) 18 | @instances[device_path] 19 | end 20 | 21 | attr_reader :comsMutex 22 | 23 | # this tries to lock the coms mutex, unless already held, 24 | # then sends every param, begining with +params[0]+ 25 | # If the current param is a Fixnum, it is treated as one byte. 26 | # If the param is a String, this string will be sent byte by byte. 27 | # You can use Array#pack to create a string from an array 28 | # For Fixnum there is a convenient function to_short which transforms 29 | # the number to a string this way: 12345.to_short == [12345].pack("s") 30 | def write(address, *params) 31 | if(@comsMutex.owned?) 32 | keepLock = true; 33 | else 34 | @comsMutex.lock; 35 | end 36 | 37 | begin 38 | setup_device(address); 39 | raw_write(params); 40 | ensure 41 | @comsMutex.unlock() unless keepLock; 42 | end 43 | end 44 | 45 | # this tries to lock the coms mutex (unless already held), 46 | # then sends *params, if given, and then tries to read 47 | # +size+ bytes. The result is a String which can be treated with 48 | # String#unpack afterwards 49 | def read(address, size, *params) 50 | if(@comsMutex.owned?) 51 | keepLock = true; 52 | else 53 | @comsMutex.lock; 54 | end 55 | 56 | begin 57 | setup_device(address); 58 | raw_write(params) unless params.empty? 59 | result = raw_read(size); 60 | ensure 61 | @comsMutex.unlock() unless keepLock; 62 | return result; 63 | end 64 | end 65 | 66 | # Read a byte from the current address. Return a one char String which 67 | # can be treated with String#unpack 68 | def read_byte(address) 69 | read(address, 1); 70 | end 71 | 72 | private 73 | # Set up @device for a I2C communication to address 74 | def setup_device(address) 75 | @device.ioctl(I2C_SLAVE, address); 76 | end 77 | 78 | # Read size bytes from @device, if possible. Raise an error otherwise 79 | def raw_read(size) 80 | return @device.sysread(size) 81 | end 82 | 83 | # Write "params" to @device, unrolling them first should they be an array. 84 | # params should be a string, formatted with Array.pack as explained for write() 85 | def raw_write(params) 86 | data = String.new(); 87 | data.force_encoding("US-ASCII") 88 | 89 | if(params.is_a? Array) 90 | params.each do |i| data << i; end 91 | else 92 | data << params; 93 | end 94 | 95 | @device.syswrite(data); 96 | end 97 | 98 | def initialize(device_path) 99 | @comsMutex = Mutex.new(); 100 | 101 | @device = File.new(device_path, 'r+') 102 | # change the sys* functions of the file object to meet our requirements 103 | class << @device 104 | alias :syswrite_orig :syswrite 105 | def syswrite(var) 106 | begin 107 | syswrite_orig var 108 | rescue Errno::EREMOTEIO, Errno::EIO 109 | raise AckError, "No acknowledge received" 110 | end 111 | end 112 | alias :sysread_orig :sysread 113 | def sysread(var) 114 | begin 115 | sysread_orig var 116 | rescue Errno::EREMOTEIO, Errno::EIO 117 | raise AckError, "No acknowledge received" 118 | end 119 | end 120 | end # virtual class 121 | end # initialize 122 | end # Class 123 | end # Module 124 | -------------------------------------------------------------------------------- /lib/i2c/drivers/mcp230xx.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # I2C IO-Expander drivers. 3 | # 4 | # Copyright (c) 2012 Christoph Anderegg 5 | # This file may be distributed under the terms of the GNU General Public 6 | # License Version 2. 7 | # 8 | 9 | require 'i2c/i2c.rb' 10 | 11 | # Constants for mode() 12 | INPUT = 1 unless defined? INPUT 13 | OUTPUT = 0 unless defined? OUTPUT 14 | 15 | # Constants for write() 16 | HIGH = 1 unless defined? HIGH 17 | LOW = 0 unless defined? LOW 18 | 19 | module I2C 20 | module Drivers 21 | # Driver class for the Microchip MPC230xx IO-Expanders. 22 | # 23 | # The interface is mostly compatible to the interface 24 | # of the WiringPi gem. PWM is not supported though. 25 | # On the other hand a more rubyesque interface is also 26 | # provided. 27 | module MCP230xx 28 | # Creates an instance representing exactly one 29 | # MCP230xx on one I2C-bus. 30 | # 31 | # @todo This implementation currently assumes 32 | # that all registers of the same type are 33 | # on a continuos address range. Implement 34 | # a (less efficient) case for other 35 | # situations. 36 | # 37 | # @param device [IO, String] I2C-device file 38 | # (usually /dev/i2c-0). Or an intantiated 39 | # io class that supports the necessary 40 | # operations (#read, #write and #ioctl). 41 | # @param address [Integer] Device address on the bus. 42 | def initialize(device, address) 43 | if device.kind_of?(String) 44 | @device = ::I2C.create(device) 45 | else 46 | [ :read, :write ].each do |m| 47 | raise IncompatibleDeviceException, 48 | "Missing #{m} method in device object." unless device.respond_to?(m) 49 | end 50 | @device = device 51 | end 52 | @address = address 53 | 54 | @iodir = Array.new 55 | max_port_no.times { @iodir << 0xFF } # Direction is input initially 56 | @device.write(@address, port_count, iodir[0], *@iodir) 57 | 58 | @data = Array.new 59 | max_port_no.times { @data << 0xFF } 60 | @unpack_string = "C" * port_count 61 | @data = @device.read(@address, port_count, gpio[0]).unpack(@unpack_string) 62 | @dir = @device.read(@address, port_count, iodir[0]).unpack(@unpack_string) 63 | end 64 | 65 | # Reads the mode of a IO-pin. 66 | # @param pin [Integer] Pin number to check. 67 | # @return [Integer] Pin mode. Either INPUT or OUTPUT. 68 | def mode?(pin) 69 | check_pin(pin) # Raises if the pin is not valid 70 | @dir = @device.read(@address, port_count, iodir[0]).unpack(@unpack_string) 71 | index = port_for_pin(pin) 72 | 73 | (@dir[index[0]] >> index[1]) & 0x01 74 | end 75 | 76 | # Sets the mode of a IO-pin. 77 | # @param pin [Integer] Pin number to set. 78 | # @param pin_mode [Integer] Pin mode. Either INPUT or OUTPUT. 79 | def mode(pin, pin_mode) 80 | check_pin(pin) # Raises if the pin is not valid 81 | raise ArgumentError, 'invalid value' unless [0,1].include?(pin_mode) 82 | index = port_for_pin(pin) 83 | 84 | @dir[index[0]] = set_bit_value(@dir[index[0]], index[1], pin_mode) 85 | @device.write(@address, iodir[0], *@dir) 86 | end 87 | 88 | # Sets a IO-pin value. 89 | # @param pin [Integer] Pin number to set. 90 | # @param value [Integer] Pin value. Either HIGH or LOW. 91 | def []=(pin, value) 92 | check_pin(pin) # Raises if the pin is not valid 93 | raise ArgumentError, 'invalid value' unless [0,1].include?(value) 94 | index = port_for_pin(pin) 95 | @data[index[0]] = set_bit_value(@data[index[0]], index[1], value) 96 | @device.write(@address, gpio[0], *@data) 97 | end 98 | 99 | # Alias for a WiringPi compatible naming. 100 | alias :write :[]= 101 | 102 | # Reads a IO-pin value. 103 | # @param pin [Integer] Pin number to set. 104 | # @return [Integer] Pin value. Either HIGH or LOW. 105 | def [](pin) 106 | check_pin(pin) # Raises if the pin is not valid 107 | index = port_for_pin(pin) 108 | @data = @device.read(@address, port_count, gpio[0]).unpack(@unpack_string) 109 | index = port_for_pin(pin) 110 | 111 | (@data[index[0]] >> index[1]) & 0x01 112 | end 113 | 114 | # Alias for a WiringPi compatible naming. 115 | alias_method :read, :[] 116 | 117 | # private 118 | # Checks a pin number for validity. 119 | # Raises an exception if not valid. Returns nil otherwise. 120 | # @param pin [Integer] IO pin number. 121 | # @return nil Raises an exception in all other cases. 122 | def check_pin(pin) 123 | raise ArgumentError, "Pin not 0-#{max_pin_no}" unless (0..max_pin_no).include?(pin) 124 | nil 125 | end 126 | 127 | # Checks a port number for validity. 128 | # Raises an exception if not valid. Returns nil otherwise. 129 | # @param no [Integer] IO port number. 130 | # @return nil Raises an exception in all other cases. 131 | def check_port(no) 132 | raise ArgumentError, "Only Ports 0-#{max_port_no} available." unless (0..max_port_no).include?(no) 133 | nil 134 | end 135 | 136 | # Returns a port no, index in port pair for a pin number. 137 | # 138 | # E.g. Pin 14 is the is bit 7 of port 1. So the method returns [1,7]. 139 | # 140 | # @param pin [Integer] Pin number, begining at 0. 141 | # @return [Array] Port number and index in the port 142 | # of the passed continuos pin number. 143 | def port_for_pin(pin) 144 | check_pin(pin) 145 | [pin / 8, pin % 8] 146 | end 147 | 148 | # Sets a bit in a byte to a defied state not touching the other bits. 149 | # 150 | # @param byte [Integer] Byte to manipulate (LSB). 151 | # @param bit [Integer] Bit number to manipulate. 152 | # @param value [Integer] 1 or 0; the new value of the bit. 153 | # @return [Integer] The new byte 154 | def set_bit_value(byte, bit, value) 155 | [byte, bit, value].each do |p| 156 | raise ArgumentError, "Arguments must be Integer" unless p.kind_of? Integer 157 | end 158 | raise ArgumentError, "Only bits 0..7 are available." unless bit < 8 159 | raise ArgumentError, "Value needs to be 0 or 1." unless (value == 0) or (value == 1) 160 | mask = 0x00 161 | mask = (0x01 << bit) 162 | case value 163 | when 0 164 | byte = (byte & ((~mask) & 0xFF)) & 0xFF 165 | when 1 166 | byte = (byte | mask) & 0xFF 167 | else 168 | raise ArgumentError, "Bit not 0-7." 169 | end 170 | byte 171 | end 172 | 173 | # @!method iodir(no) 174 | # Returns the address of the IO direction register 175 | # for an IO port. 176 | # 177 | # @param no [Integer] Port number, begining at 0. 178 | # @return Address of the IO direction register 179 | # corresponding to passed IO port number. 180 | 181 | # @!method gpio(no) 182 | # Returns the address of the GPIO register 183 | # for an IO port. 184 | # 185 | # @param no [Integer] Port number, begining at 0. 186 | # @return [Integer] Address of the GPIO register 187 | # corresponding to passed IO port number. 188 | 189 | # @!method pin_count 190 | # Returns the number of pins in the io expander. 191 | # 192 | # @return [Integer] Number of pins. 193 | 194 | # @!method max_pin_no 195 | # Returns the highest pin index. Usually #pin_count - 1. 196 | # 197 | # @return [Integer] Highest pin index. 198 | 199 | # @!method port_count 200 | # Returns the number of ports in the io expander. 201 | # 202 | # @return [Integer] Number of ports. 203 | 204 | # @!method max_port_no 205 | # Returns the highest port index. Usually #port_count - 1. 206 | # 207 | # @return [Integer] Highest port index. 208 | end 209 | 210 | # Defines a class for a chip implementation. 211 | # 212 | # @param name Class name 213 | def self.define_mcp230xx_chip(name, parameters) 214 | raise ArgumentError, "Expecting options hash." unless parameters.kind_of? Hash 215 | [ [ :pin_count, Integer ], 216 | [ :port_count, Integer ], 217 | [ :iodir, Array ], 218 | [ :gpio, Array ] ].each do |expected_key| 219 | raise ArgumentError, "Missing option #{expected_key[0]}" unless 220 | parameters.has_key? expected_key[0] 221 | raise ArgumentError, "Option #{expected_key[0]} expected to be a #{expected_key[1]}" unless 222 | parameters[expected_key[0]].kind_of? expected_key[1] 223 | end 224 | chip_class = self.const_set(name.to_sym, Class.new) 225 | chip_class.instance_eval do 226 | include MCP230xx 227 | parameters.each do |method_name, return_value| 228 | #puts "Defining #{name}##{method_name.to_sym}" 229 | define_method method_name.to_sym do 230 | return_value 231 | end 232 | end 233 | define_method :max_pin_no do 234 | parameters[:pin_count] - 1 235 | end 236 | define_method :max_port_no do 237 | parameters[:port_count] - 1 238 | end 239 | end 240 | chip_class 241 | end 242 | define_mcp230xx_chip :MCP23008, 243 | :pin_count => 8, 244 | :port_count => 1, 245 | :iodir => [ 0x00 ], 246 | :gpio => [ 0x09 ] 247 | define_mcp230xx_chip :MCP23017, 248 | :pin_count => 16, 249 | :port_count => 2, 250 | :iodir => [ 0x00, 0x01 ], 251 | :gpio => [ 0x12, 0x13 ] 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /lib/i2c/i2c.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Entry point to the I2C library. 3 | # 4 | # Essentially this requires the correct backend 5 | # driver, right now this means the linux i2c-dev driver. 6 | # This could be extended to do some system specific 7 | # 8 | # Copyright (c) 2012 Christoph Anderegg 9 | # Copyright (c) 2008 Jonas Bähr, jonas.baehr@fs.ei.tum.de 10 | # 11 | # This file may be distributed under the terms of the GNU General Public 12 | # License Version 2. 13 | # 14 | 15 | require 'i2c/backends/i2c-dev.rb' 16 | 17 | module I2C 18 | # some common error classes 19 | class AckError < StandardError; end 20 | 21 | # Returns an instance of the current backend 22 | # driver. 23 | # 24 | # Is there a system agnostic way to do this? 25 | # 26 | # +bus_descriptor+ describes the bus to use. This is 27 | # of course system specific. For the 28 | # Linux i2c-dev driver this is the 29 | # device file (e.g. /dev/i2c-0"). 30 | def self.create(bus_descriptor) 31 | I2C::Dev.create(bus_descriptor) 32 | end 33 | end 34 | 35 | 36 | -------------------------------------------------------------------------------- /rules/88-i2c.rules: -------------------------------------------------------------------------------- 1 | KERNEL=="i2c-[0-9]", GROUP="i2c", MODE="0660" 2 | -------------------------------------------------------------------------------- /test/mcp230xx_spec.rb: -------------------------------------------------------------------------------- 1 | require 'i2c' 2 | 3 | class MockI2CIO 4 | attr_reader :registers 5 | attr_reader :last_address 6 | 7 | def initialize 8 | @registers = Hash.new 9 | # Initialize according to data sheet 10 | (0x00..0x01).each do |reg| 11 | @registers[reg] = 0xFF 12 | end 13 | (0x02..0x13).each do |reg| 14 | @registers[reg] = 0x00 15 | end 16 | end 17 | 18 | def write(address, *params) 19 | @last_address = address 20 | if params.count >= 1 21 | reg_addr = params.shift 22 | index = 0 23 | params.each do |p| 24 | @registers[reg_addr+index] = p 25 | index += 1 26 | end 27 | end 28 | end 29 | 30 | def read(address, size, *params) 31 | @last_address = address 32 | answer = String.new 33 | answer.force_encoding("US-ASCII") 34 | if (size > 0) && (params.count >= 1) 35 | reg_addr = params.shift 36 | size.times do |index| 37 | answer << (@registers[reg_addr+index] & 0xFF) 38 | end 39 | end 40 | answer 41 | end 42 | end 43 | 44 | describe I2C::Drivers::MCP23017, "#port_count" do 45 | it "returns the number of ports." do 46 | io = MockI2CIO.new 47 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 48 | mcp.port_count.should eq(2) 49 | end 50 | end 51 | 52 | describe I2C::Drivers::MCP23017, "#max_port_no" do 53 | it "returns the highest port number." do 54 | io = MockI2CIO.new 55 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 56 | mcp.max_port_no.should eq(1) 57 | end 58 | end 59 | 60 | describe I2C::Drivers::MCP23017, "#pin_count" do 61 | it "returns the number of pins." do 62 | io = MockI2CIO.new 63 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 64 | mcp.pin_count.should eq(16) 65 | end 66 | end 67 | 68 | describe I2C::Drivers::MCP23017, "#max_pin_no" do 69 | it "returns the highest pin number." do 70 | io = MockI2CIO.new 71 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 72 | mcp.max_pin_no.should eq(15) 73 | end 74 | end 75 | 76 | describe I2C::Drivers::MCP23017, "#check_pin" do 77 | it "raises an exception for out of range pin indices." do 78 | io = MockI2CIO.new 79 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 80 | [-1000, -1, 16, 1000].each do |pin| 81 | expect { mcp.check_pin(pin) }.to raise_error 82 | end 83 | end 84 | 85 | it "returns nil for valid indices." do 86 | io = MockI2CIO.new 87 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 88 | 16.times do |pin| 89 | mcp.check_pin(pin).should eq nil 90 | end 91 | end 92 | end 93 | 94 | describe I2C::Drivers::MCP23017, "#check_port" do 95 | it "raises an exception for out of range port indices." do 96 | io = MockI2CIO.new 97 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 98 | [-1000, -1, 2, 1000].each do |port| 99 | expect { mcp.check_port(port) }.to raise_error 100 | end 101 | end 102 | 103 | it "returns nil for valid indices." do 104 | io = MockI2CIO.new 105 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 106 | 2.times do |port| 107 | mcp.check_port(port).should eq nil 108 | end 109 | end 110 | end 111 | 112 | describe I2C::Drivers::MCP23017, "#port_for_pin" do 113 | it "returns correctly split pin and port numbers for 16bit IO expanders." do 114 | io = MockI2CIO.new 115 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 116 | expected = 117 | [[0, 0], 118 | [0, 1], 119 | [0, 2], 120 | [0, 3], 121 | [0, 4], 122 | [0, 5], 123 | [0, 6], 124 | [0, 7], 125 | [1, 0], 126 | [1, 1], 127 | [1, 2], 128 | [1, 3], 129 | [1, 4], 130 | [1, 5], 131 | [1, 6], 132 | [1, 7]] 133 | expected.each_index do |pin| 134 | mcp.port_for_pin(pin).should eq(expected[pin]) 135 | end 136 | end 137 | 138 | it "raises an exception for out of range arguments." do 139 | io = MockI2CIO.new 140 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 141 | [1000, 16, -1, -1000].each do |pin| 142 | expect { mcp.port_for_pin(pin) }.to raise_error 143 | end 144 | end 145 | end 146 | 147 | describe I2C::Drivers::MCP23017, "#set_bit_value" do 148 | it "raises on non-integer arguments for the first argument" do 149 | io = MockI2CIO.new 150 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 151 | expect { mcp.set_bit_value("Hello", 0, 0) }.to raise_error 152 | end 153 | it "raises on non-integer arguments for the second argument" do 154 | io = MockI2CIO.new 155 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 156 | expect { mcp.set_bit_value(0, "Hello", 0) }.to raise_error 157 | end 158 | it "raises on arguments other than 0 and 1 for the third argument" do 159 | io = MockI2CIO.new 160 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 161 | [1000, 2, -1, -1000].each do |value| 162 | expect { mcp.set_bit_value(0, 0, value) }.to raise_error 163 | end 164 | end 165 | it "sets the correct bits in a byte" do 166 | io = MockI2CIO.new 167 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 168 | # Original, bit no, new value, result 169 | [ 170 | [ 0b11111111, 0, 0, 0b11111110 ], 171 | [ 0b00000000, 0, 1, 0b00000001 ], 172 | [ 0b11111111, 7, 0, 0b01111111 ], 173 | [ 0b00000000, 7, 1, 0b10000000 ], 174 | [ 0b11111111, 0, 1, 0b11111111 ], 175 | [ 0b00000000, 0, 0, 0b00000000 ] 176 | ].each do |test| 177 | mcp.set_bit_value(test[0], test[1], test[2]).should eq test[3] 178 | end 179 | end 180 | end 181 | 182 | describe I2C::Drivers::MCP23017, "#mode" do 183 | it "raises an exception for out of range pin indices." do 184 | io = MockI2CIO.new 185 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 186 | [-1000, -1, 16, 1000].each do |pin| 187 | expect { mcp.mode(pin, HIGH) }.to raise_error 188 | end 189 | end 190 | end 191 | 192 | describe I2C::Drivers::MCP23017, "#mode?" do 193 | it "raises an exception for out of range pin indices." do 194 | io = MockI2CIO.new 195 | mcp = I2C::Drivers::MCP23017.new(io, 0x20) 196 | [-1000, -1, 16, 1000].each do |pin| 197 | expect { mcp.mode?(pin) }.to raise_error 198 | end 199 | end 200 | 201 | it "initially returns 1 for all pin modes" do 202 | io = MockI2CIO.new 203 | mcp23017 = I2C::Drivers::MCP23017.new(io, 0x20) 204 | (0..15).each do |pin| 205 | mcp23017.mode?(pin).should eq(1) 206 | end 207 | end 208 | 209 | it "returns what has been set through #mode" do 210 | io = MockI2CIO.new 211 | mcp23017 = I2C::Drivers::MCP23017.new(io, 0x20) 212 | (0..500).each do |pin| 213 | pin = rand(16) 214 | mode = rand(2) 215 | mcp23017.mode(pin, mode) 216 | mcp23017.mode?(pin).should eq(mode) 217 | end 218 | end 219 | end 220 | 221 | describe I2C::Drivers::MCP23017, "#[]" do 222 | it "initially returns 0 for all I/O pins" do 223 | io = MockI2CIO.new 224 | mcp23017 = I2C::Drivers::MCP23017.new(io, 0x20) 225 | (0..15).each do |pin| 226 | mcp23017[pin].should eq(0) 227 | end 228 | end 229 | it "returns what has been set through #[]=" do 230 | io = MockI2CIO.new 231 | mcp23017 = I2C::Drivers::MCP23017.new(io, 0x20) 232 | (0..500).each do |pin| 233 | pin = rand(16) 234 | value = rand(2) 235 | mcp23017[pin] = value 236 | mcp23017[pin].should eq(value) 237 | end 238 | end 239 | end 240 | --------------------------------------------------------------------------------