lgpio 0.1.4 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,83 @@
1
+ module LGPIO
2
+ class HardwarePWM
3
+ NS_PER_S = 1_000_000_000
4
+ NS_PER_US = 1_000
5
+ SYS_FS_PWM_PATH = "/sys/class/pwm/"
6
+
7
+ attr_reader :period, :duty, :enabled
8
+
9
+ def initialize(chip, channel, frequency: nil, period: nil)
10
+ @chip = chip
11
+ @channel = channel
12
+
13
+ # Accept either frequency (in Hz) or period in nanoseconds.
14
+ if (frequency && period) || (!frequency && !period)
15
+ raise "either period: or frequency: is required, but not both"
16
+ end
17
+
18
+ period ? self.period = period : self.frequency = frequency
19
+ enable
20
+ end
21
+
22
+ def path
23
+ @path ||= "#{SYS_FS_PWM_PATH}pwmchip#{@chip}/pwm#{@channel}/"
24
+ end
25
+
26
+ def period_path
27
+ @period_path ||= "#{path}period"
28
+ end
29
+
30
+ def duty_path
31
+ @duty_path ||= "#{path}duty_cycle"
32
+ end
33
+
34
+ def enable_path
35
+ @enable_path ||= "#{path}enable"
36
+ end
37
+
38
+ def frequency=(freq)
39
+ self.period = (NS_PER_S / freq.to_f).round
40
+ end
41
+
42
+ def period=(p)
43
+ old_period = File.read("#{path}period").strip.to_i
44
+ unless (old_period == 0)
45
+ File.open(duty_path, 'w') { |f| f.write("0") }
46
+ end
47
+ File.open(period_path, 'w') { |f| f.write(p) }
48
+ @period = p
49
+ end
50
+
51
+ def duty_percent
52
+ return 0.0 if (!duty || !period) || (duty == 0)
53
+ (duty / period.to_f) * 100.0
54
+ end
55
+
56
+ def duty_percent=(d)
57
+ raise "duty_cycle: #{d} % cannot be more than 100%" if d > 100
58
+ d_ns = ((d / 100.0) * @period.to_i).round
59
+ self.duty = d_ns
60
+ end
61
+
62
+ def duty_us=(d_us)
63
+ d_ns = (d_us * NS_PER_US).round
64
+ self.duty = d_ns
65
+ end
66
+
67
+ def duty=(d_ns)
68
+ raise "duty cycle: #{d_ns} ns cannot be longer than period: #{period} ns" if d_ns > period
69
+ File.open(duty_path, 'w') { |f| f.write(d_ns) }
70
+ @duty = d_ns
71
+ end
72
+
73
+ def disable
74
+ File.open(enable_path, 'w') { |f| f.write("0") }
75
+ @enabled = false
76
+ end
77
+
78
+ def enable
79
+ File.open(enable_path, 'w') { |f| f.write("1") }
80
+ @enabled = true
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,117 @@
1
+ module LGPIO
2
+ class I2CBitBang
3
+ VALID_ADDRESSES = (0x08..0x77).to_a
4
+
5
+ attr_reader :handle, :scl, :sda
6
+
7
+ def initialize(handle, scl, sda)
8
+ @handle = handle
9
+ @scl = scl
10
+ @sda = sda
11
+ @sda_state = nil
12
+ initialize_pins
13
+ end
14
+
15
+ def initialize_pins
16
+ LGPIO.gpio_claim_output(handle, LGPIO::SET_PULL_NONE, scl, LGPIO::HIGH)
17
+ LGPIO.gpio_claim_output(handle, LGPIO::SET_OPEN_DRAIN | LGPIO::SET_PULL_UP, sda, LGPIO::HIGH)
18
+ end
19
+
20
+ def set_sda(value)
21
+ return if (@sda_state == value)
22
+ LGPIO.gpio_write(handle, sda, @sda_state = value)
23
+ end
24
+
25
+ def write_form(address)
26
+ (address << 1)
27
+ end
28
+
29
+ def read_form(address)
30
+ (address << 1) | 0b00000001
31
+ end
32
+
33
+ def start
34
+ LGPIO.gpio_write(handle, sda, 0)
35
+ LGPIO.gpio_write(handle, scl, 0)
36
+ end
37
+
38
+ def stop
39
+ LGPIO.gpio_write(handle, sda, 0)
40
+ LGPIO.gpio_write(handle, scl, 1)
41
+ LGPIO.gpio_write(handle, sda, 1)
42
+ end
43
+
44
+ def read_bit
45
+ set_sda(1)
46
+ LGPIO.gpio_write(handle, scl, 1)
47
+ bit = LGPIO.gpio_read(handle, sda)
48
+ LGPIO.gpio_write(handle, scl, 0)
49
+ bit
50
+ end
51
+
52
+ def write_bit(bit)
53
+ set_sda(bit)
54
+ LGPIO.gpio_write(handle, scl, 1)
55
+ LGPIO.gpio_write(handle, scl, 0)
56
+ end
57
+
58
+ def read_byte(ack)
59
+ byte = 0
60
+ i = 0
61
+ while i < 8
62
+ byte = (byte << 1) | read_bit
63
+ i = i + 1
64
+ end
65
+ write_bit(ack ? 0 : 1)
66
+ byte
67
+ end
68
+
69
+ def write_byte(byte)
70
+ i = 7
71
+ while i >= 0
72
+ write_bit (byte >> i) & 0b1
73
+ i = i - 1
74
+ end
75
+ # Return ACK (SDA pulled low) or NACK (SDA stayed high).
76
+ (read_bit == 0)
77
+ end
78
+
79
+ def read(address, length)
80
+ raise ArgumentError, "invalid I2C address: #{address}. Range is 0x08..0x77" unless VALID_ADDRESSES.include?(address)
81
+ raise ArgumentError, "invalid Integer for read length: #{length}" unless length.kind_of?(Integer)
82
+
83
+ start
84
+ ack = write_byte(read_form(address))
85
+ return nil unless ack
86
+
87
+ # Read length bytes, and ACK for all but the last one.
88
+ bytes = []
89
+ (length-1).times { bytes << read_byte(true) }
90
+ bytes << read_byte(false)
91
+ stop
92
+
93
+ bytes
94
+ end
95
+
96
+ def write(address, bytes)
97
+ raise ArgumentError, "invalid I2C address: #{address}. Range is 0x08..0x77" unless VALID_ADDRESSES.include?(address)
98
+ raise ArgumentError, "invalid byte Array to write: #{bytes}" unless bytes.kind_of?(Array)
99
+
100
+ start
101
+ write_byte(write_form(address))
102
+ bytes.each { |byte| write_byte(byte) }
103
+ stop
104
+ end
105
+
106
+ def search
107
+ found = []
108
+ VALID_ADDRESSES.each do |address|
109
+ start
110
+ # Device present if ACK received when we write its address to the bus.
111
+ found << address if write_byte(write_form(address))
112
+ stop
113
+ end
114
+ found
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,8 @@
1
+ module LGPIO
2
+ class Infrared < HardwarePWM
3
+ def transmit(pulses, duty: 33.333)
4
+ self.duty_percent = duty
5
+ tx_wave_ook(duty_path, @duty.to_s, pulses)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,171 @@
1
+ module LGPIO
2
+ class OneWire
3
+ # Constants
4
+ READ_POWER_SUPPLY = 0xB4
5
+ CONVERT_T = 0x44
6
+ SEARCH_ROM = 0xF0
7
+ READ_ROM = 0x33
8
+ SKIP_ROM = 0xCC
9
+ MATCH_ROM = 0x55
10
+ ALARM_SEARCH = 0xEC
11
+ READ_SCRATCH = 0xBE
12
+ WRITE_SCRATCH = 0x4E
13
+ COPY_SCRATCH = 0x48
14
+ RECALL_EEPROM = 0xB8
15
+
16
+ attr_reader :handle, :gpio, :found_addresses
17
+
18
+ def initialize(handle, gpio)
19
+ @handle = handle
20
+ @gpio = gpio
21
+ @found_addresses = []
22
+ LGPIO.gpio_claim_output(handle, LGPIO::SET_OPEN_DRAIN | LGPIO::SET_PULL_UP, gpio, LGPIO::HIGH)
23
+ end
24
+
25
+ def reset
26
+ # LGPIO.one_wire_reset returns 0 if device present on bus.
27
+ return (LGPIO.one_wire_reset(handle, gpio) == 0)
28
+ end
29
+
30
+ def read(byte_count)
31
+ read_bytes = []
32
+ byte_count.times do
33
+ byte = 0b00000000
34
+ 8.times { |i| byte |= LGPIO.one_wire_bit_read(handle, gpio) << i }
35
+ read_bytes << byte
36
+ end
37
+ read_bytes
38
+ end
39
+
40
+ def write(byte_array, parasite: nil)
41
+ byte_array.each do |byte|
42
+ 8.times { |i| LGPIO.one_wire_bit_write(handle, gpio, (byte >> i) & 0b1) }
43
+ end
44
+ # Drive bus high to feed parasite capacitor after write.
45
+ LGPIO.gpio_write(handle, gpio, LGPIO::HIGH) if parasite
46
+ end
47
+
48
+ def search
49
+ branch_mask = 0
50
+ high_discrepancy = 0
51
+
52
+ loop do
53
+ reset
54
+ write [SEARCH_ROM]
55
+ result = search_pass(branch_mask)
56
+
57
+ address, high_discrepancy = parse_search_result(result)
58
+ @found_addresses << address
59
+
60
+ # No unsearched discrepancies left.
61
+ break if high_discrepancy == -1
62
+
63
+ # Force highest new discrepancy to be a 1 on the next search.
64
+ # i.e. Go as deep as possible into each branch found then back out.
65
+ #
66
+ branch_mask = branch_mask | (2 ** high_discrepancy)
67
+
68
+ # Clear bits above high_discrepancy so we don't repeat branches.
69
+ # When high_discrepancy < MSB of branch_mask, this moves us
70
+ # one node out, closer to the root, and finishing the search.
71
+ #
72
+ unset_mask = 0xFFFFFFFFFFFFFFFF >> (63 - high_discrepancy)
73
+ branch_mask = branch_mask & unset_mask
74
+ end
75
+ end
76
+
77
+ # Read a single 64-bit address and complement from the bus.
78
+ def search_pass(mask)
79
+ bytes = []
80
+
81
+ 8.times do |i|
82
+ addr = 0
83
+ comp = 0
84
+ 8.times do |j|
85
+ addr |= LGPIO.one_wire_bit_read(handle, gpio) << j
86
+ comp |= LGPIO.one_wire_bit_read(handle, gpio) << j
87
+
88
+ # A set (1) mask bit means we're searching a branch with that bit set.
89
+ # Force it to be 1 on this pass. Write 1 to both the bus and address bit.
90
+ #
91
+ # Don't change the complement bit from 0, Even if the bus said 0/0,
92
+ # send back 1/0, hiding known discrepancies, only sending new ones.
93
+ #
94
+ # Mask is a 64-bit number, not byte array.
95
+ if ((mask >> (i*8 + j)) & 0b1) == 1
96
+ LGPIO.one_wire_bit_write(handle, gpio, 1)
97
+ addr |= 1 << j
98
+
99
+ # Whether there was no "1-branch" marked for this bit, or there is no
100
+ # discrepancy at all, just echo address bit to the bus. We will
101
+ # compare addr/comp to find discrepancies for future passes.
102
+ else
103
+ LGPIO.one_wire_bit_write(handle, gpio, (addr >> j) & 0b1)
104
+ end
105
+ end
106
+ bytes << addr
107
+ bytes << comp
108
+ end
109
+
110
+ # 16 bytes, address and complement bytes interleaved LSByteFIRST.
111
+ # DON'T CHANGE! #split_search_result deals with it. denko uses #search_pass
112
+ # directly on Linux, but MCUs send it this format to save RAM. Always match it.
113
+ return bytes
114
+ end
115
+
116
+ def parse_search_result(result)
117
+ address, complement = split_search_result(result)
118
+
119
+ raise "OneWire device not connected, or disconnected during search" if (address & complement) > 0
120
+ raise "CRC error during OneWire search" unless OneWire.crc(address)
121
+
122
+ # Gives 0 at every discrepancy we didn't write 1 for on this pass.
123
+ new_discrepancies = address ^ complement
124
+
125
+ high_discrepancy = -1
126
+ (0..63).each { |i| high_discrepancy = i if new_discrepancies[i] == 0 }
127
+
128
+ [address, high_discrepancy]
129
+ end
130
+
131
+ # Convert interleaved address/complement bytes to 64-bit numbers.
132
+ def split_search_result(data)
133
+ address = 0
134
+ complement = 0
135
+ data.reverse.each_slice(2) do |comp_byte, addr_byte|
136
+ address = (address << 8) | addr_byte
137
+ complement = (complement << 8) | comp_byte
138
+ end
139
+ [address, complement]
140
+ end
141
+
142
+ # CRC Class Methods
143
+ def self.crc(data)
144
+ calculated, received = self.calculate_crc(data)
145
+ calculated == received
146
+ end
147
+
148
+ def self.calculate_crc(data)
149
+ if data.class == Integer
150
+ bytes = address_to_bytes(data)
151
+ else
152
+ bytes = data
153
+ end
154
+
155
+ crc = 0b00000000
156
+ bytes.take(bytes.length - 1).each do |byte|
157
+ for bit in (0..7)
158
+ xor = byte[bit] ^ crc[0]
159
+ crc = crc ^ ((xor * (2 ** 3)) | (xor * (2 ** 4)))
160
+ crc = crc >> 1
161
+ crc = crc | (xor * (2 ** 7))
162
+ end
163
+ end
164
+ [crc, bytes.last]
165
+ end
166
+
167
+ def self.address_to_bytes(address)
168
+ [address].pack('Q<').split("").map(&:ord)
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,28 @@
1
+ module LGPIO
2
+ class PositionalServo < HardwarePWM
3
+ FREQUENCY = 50
4
+
5
+ attr_reader :angle
6
+
7
+ def initialize(chip, channel, min_us, max_us, min_angle, max_angle)
8
+ super(chip, channel, frequency: FREQUENCY)
9
+
10
+ raise "min_us: #{min_us} cannot be lower than max_us: #{max_us}" if max_us < min_us
11
+ @min_us = min_us
12
+ @max_us = max_us
13
+ @us_range = @max_us - @min_us
14
+
15
+ @min_angle = min_angle
16
+ @max_angle = max_angle
17
+ end
18
+
19
+ def angle=(a)
20
+ ratio = (a - @min_angle).to_f / (@max_angle - @min_angle)
21
+ raise "angle: #{a} outside servo range" if (ratio < 0) || (ratio > 1)
22
+
23
+ d_us = (@us_range * ratio) + @min_us
24
+ self.duty_us = d_us
25
+ @angle = a
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,109 @@
1
+ module LGPIO
2
+ class SPIBitBang
3
+ attr_reader :handle, :clock, :input, :output
4
+
5
+ def initialize(options={})
6
+ @handle = options[:handle]
7
+ @clock = options[:clock] || options[:sck] || options[:clk]
8
+ @input = options[:input] || options[:poci] || options[:miso]
9
+ @output = options[:output] || options[:pico] || options[:mosi]
10
+
11
+ raise ArgumentError, "a gpiochip :handle is required" unless @handle
12
+ raise ArgumentError, "either input/:poci/:miso OR :output/:pico/:mosi pin required" unless (@input || @output)
13
+ raise ArgumentError, ":clock/:sck/:clk pin required" unless @clock
14
+
15
+ @output_state = nil
16
+ initialize_pins
17
+ end
18
+
19
+ def initialize_pins
20
+ LGPIO.gpio_claim_output(handle, LGPIO::SET_PULL_NONE, clock, LGPIO::LOW)
21
+ LGPIO.gpio_claim_input(handle, LGPIO::SET_PULL_NONE, input) if input
22
+ LGPIO.gpio_claim_output(handle, LGPIO::SET_PULL_NONE, output, LGPIO::LOW) if output
23
+ end
24
+
25
+ def set_output(level)
26
+ return if (level == @output_state)
27
+ LGPIO.gpio_write(handle, output, @output_state = level)
28
+ end
29
+
30
+ def transfer_bit(write_bit, reading: false, mode: 0)
31
+ case mode
32
+ when 0
33
+ set_output(write_bit) if write_bit
34
+ LGPIO.gpio_write(handle, clock, 1)
35
+ read_bit = LGPIO.gpio_read(handle, input) if reading
36
+ LGPIO.gpio_write(handle, clock, 0)
37
+ when 1
38
+ LGPIO.gpio_write(handle, clock, 1)
39
+ set_output(write_bit) if write_bit
40
+ LGPIO.gpio_write(handle, clock, 0)
41
+ read_bit = LGPIO.gpio_read(handle, input) if reading
42
+ when 2
43
+ set_output(write_bit) if write_bit
44
+ LGPIO.gpio_write(handle, clock, 0)
45
+ read_bit = LGPIO.gpio_read(handle, input) if reading
46
+ LGPIO.gpio_write(handle, clock, 1)
47
+ when 3
48
+ LGPIO.gpio_write(handle, clock, 0)
49
+ set_output(write_bit) if write_bit
50
+ LGPIO.gpio_write(handle, clock, 1)
51
+ read_bit = LGPIO.gpio_read(handle, input) if reading
52
+ else
53
+ raise ArgumentError, "invalid SPI mode: #{mode} given"
54
+ end
55
+ return reading ? read_bit : nil
56
+ end
57
+
58
+ def transfer_byte(byte, reading: false, order: :msbfirst, mode: 0)
59
+ read_byte = reading ? 0 : nil
60
+
61
+ if (order == :msbfirst)
62
+ i = 7
63
+ while i >= 0
64
+ write_bit = byte ? (byte >> i) & 0b1 : nil
65
+ read_bit = transfer_bit(write_bit, mode: mode, reading: reading)
66
+ read_byte = (read_byte << 1) | read_bit if reading
67
+ i = i - 1
68
+ end
69
+ else
70
+ i = 0
71
+ while i < 8
72
+ write_bit = byte ? (byte >> i) : nil
73
+ read_bit = transfer_bit(write_bit, mode: mode, reading: reading)
74
+ read_byte = read_byte | (read_bit << i) if reading
75
+ i = i + 1
76
+ end
77
+ end
78
+
79
+ read_byte
80
+ end
81
+
82
+ def transfer(write: [], read: 0, select: nil, order: :msbfirst, mode: 0)
83
+ # Idle clock state depends on SPI mode.
84
+ case mode
85
+ when 0..1
86
+ LGPIO.gpio_write(handle, clock, 0)
87
+ when 2..3
88
+ LGPIO.gpio_write(handle, clock, 1)
89
+ else
90
+ raise ArgumentError, "invalid SPI mode: #{mode} given"
91
+ end
92
+ raise ArgumentError, "invalid Array for write: #{write}" unless write.kind_of?(Array)
93
+ raise ArgumentError, "invalid Integer for read: #{read}" unless read.kind_of?(Integer)
94
+
95
+ read_bytes = (read > 0) ? [] : nil
96
+ LGPIO.gpio_write(handle, select, 0) if select
97
+
98
+ i = 0
99
+ while (i < read) || (i < write.length)
100
+ read_byte = transfer_byte(write[i], reading: (i < read), order: order, mode: mode)
101
+ read_bytes << read_byte if read_byte
102
+ i = i + 1
103
+ end
104
+
105
+ LGPIO.gpio_write(handle, select, 1) if select
106
+ read_bytes
107
+ end
108
+ end
109
+ end
data/lib/lgpio/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module LGPIO
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.6"
3
3
  end
data/lib/lgpio.rb CHANGED
@@ -1,4 +1,13 @@
1
1
  require_relative 'lgpio/lgpio'
2
+ require_relative 'lgpio/version'
3
+
4
+ require_relative 'lgpio/i2c_bitbang'
5
+ require_relative 'lgpio/spi_bitbang'
6
+ require_relative 'lgpio/one_wire'
7
+
8
+ require_relative 'lgpio/hardware_pwm'
9
+ require_relative 'lgpio/positional_servo'
10
+ require_relative 'lgpio/infrared'
2
11
 
3
12
  module LGPIO
4
13
  LOW = 0
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lgpio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - vickash
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-05 00:00:00.000000000 Z
11
+ date: 2024-09-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Use GPIO / PWM / I2C / SPI / UART on Linux SBCs in Ruby
14
14
  email: mail@vickash.com
@@ -24,18 +24,31 @@ files:
24
24
  - examples/bench_in.rb
25
25
  - examples/bench_out.rb
26
26
  - examples/blink.rb
27
+ - examples/dht.rb
27
28
  - examples/group_in.rb
28
29
  - examples/group_out.rb
30
+ - examples/hcsr04.rb
29
31
  - examples/i2c_aht10.rb
30
32
  - examples/i2c_aht10_zip.rb
33
+ - examples/i2c_bitbang-rb_aht10.rb
34
+ - examples/i2c_bitbang-rb_ssd1306_bench.rb
35
+ - examples/i2c_bitbang_aht10.rb
36
+ - examples/i2c_bitbang_search.rb
37
+ - examples/i2c_bitbang_ssd1306_bench.rb
31
38
  - examples/i2c_ssd1306_bench.rb
39
+ - examples/infrared.rb
32
40
  - examples/momentary.rb
41
+ - examples/one_wire_ds18b20.rb
42
+ - examples/one_wire_search.rb
33
43
  - examples/pwm.rb
34
44
  - examples/reports.rb
35
45
  - examples/rotary_encoder.rb
36
46
  - examples/rotary_encoder_led.rb
47
+ - examples/servo.rb
48
+ - examples/spi_bitbang_loopback.rb
49
+ - examples/spi_bitbang_ssd1306_bench.rb
50
+ - examples/spi_bitbang_ssd1306_sim_bench.rb
37
51
  - examples/spi_loopback.rb
38
- - examples/spi_read.rb
39
52
  - examples/spi_ws2812.rb
40
53
  - examples/spi_ws2812_bounce.rb
41
54
  - examples/wave.rb
@@ -43,6 +56,12 @@ files:
43
56
  - ext/lgpio/lgpio.c
44
57
  - lgpio.gemspec
45
58
  - lib/lgpio.rb
59
+ - lib/lgpio/hardware_pwm.rb
60
+ - lib/lgpio/i2c_bitbang.rb
61
+ - lib/lgpio/infrared.rb
62
+ - lib/lgpio/one_wire.rb
63
+ - lib/lgpio/positional_servo.rb
64
+ - lib/lgpio/spi_bitbang.rb
46
65
  - lib/lgpio/version.rb
47
66
  homepage: https://github.com/denko-rb/lgpio
48
67
  licenses:
data/examples/spi_read.rb DELETED
@@ -1,14 +0,0 @@
1
- require 'lgpio'
2
-
3
- SPI_DEV = 1
4
- SPI_CHAN = 0
5
- SPI_MODE = 0
6
- SPI_BAUD = 1_000_000
7
-
8
- spi_handle = LGPIO.spi_open(SPI_DEV, SPI_CHAN, SPI_BAUD, SPI_MODE)
9
-
10
- # Pull MISO high or low. High gives array of 255, low array of 0.
11
- rx_bytes = LGPIO.spi_read(spi_handle, 8)
12
- puts "RX bytes: #{rx_bytes.inspect}"
13
-
14
- LGPIO.spi_close(spi_handle)