rgpio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +103 -0
- data/LICENSE +21 -0
- data/PLAN.md +347 -0
- data/README.md +969 -0
- data/examples/adc.rb +60 -0
- data/examples/adc_led.rb +44 -0
- data/examples/button.rb +33 -0
- data/examples/lcd.rb +47 -0
- data/examples/lcd_thermometer.rb +57 -0
- data/examples/led.rb +31 -0
- data/examples/lowlevel/blink.rb +46 -0
- data/examples/lowlevel/button.rb +69 -0
- data/examples/lowlevel/servo.rb +76 -0
- data/examples/motion_sensor.rb +70 -0
- data/examples/motor.rb +38 -0
- data/examples/pwm_info.rb +68 -0
- data/examples/pwm_jitter.rb +139 -0
- data/examples/pwm_led.rb +56 -0
- data/examples/rgb_balance.rb +65 -0
- data/examples/rgb_led.rb +72 -0
- data/examples/servo.rb +70 -0
- data/examples/temperature.rb +53 -0
- data/lib/rgpio/bytes.rb +12 -0
- data/lib/rgpio/chip.rb +271 -0
- data/lib/rgpio/devices/adt7410.rb +116 -0
- data/lib/rgpio/devices/device.rb +41 -0
- data/lib/rgpio/devices/input_device.rb +150 -0
- data/lib/rgpio/devices/mcp3208.rb +104 -0
- data/lib/rgpio/devices/motor.rb +51 -0
- data/lib/rgpio/devices/output_device.rb +66 -0
- data/lib/rgpio/devices/pwm_channel.rb +25 -0
- data/lib/rgpio/devices/pwm_output_device.rb +109 -0
- data/lib/rgpio/devices/rgb_led.rb +175 -0
- data/lib/rgpio/devices/servo.rb +161 -0
- data/lib/rgpio/devices/st7032.rb +234 -0
- data/lib/rgpio/i2c.rb +175 -0
- data/lib/rgpio/line_request.rb +184 -0
- data/lib/rgpio/native.rb +238 -0
- data/lib/rgpio/pwm.rb +321 -0
- data/lib/rgpio/software_pwm.rb +290 -0
- data/lib/rgpio/spi.rb +208 -0
- data/lib/rgpio/version.rb +3 -0
- data/lib/rgpio.rb +99 -0
- metadata +152 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# PWM generated in Ruby on any GPIO line, for the pins the hardware PWM
|
|
3
|
+
# peripheral cannot reach.
|
|
4
|
+
#
|
|
5
|
+
# {HardwarePWM} is jitter-free but needs a dtoverlay in config.txt and only
|
|
6
|
+
# reaches GPIO12/13/18/19, two channels at a time on the 40-pin header. This
|
|
7
|
+
# class needs no configuration at all and works on every line, at the cost of
|
|
8
|
+
# timing accuracy — the same trade-off Python's gpiozero makes, which drives
|
|
9
|
+
# all of its PWM through `lgpio.tx_pwm` ("software timed PWM").
|
|
10
|
+
#
|
|
11
|
+
# Usage (block form — recommended):
|
|
12
|
+
# Rgpio::SoftwarePWM.open(18) do |pwm|
|
|
13
|
+
# pwm.frequency = 100
|
|
14
|
+
# pwm.duty_cycle = 0.25
|
|
15
|
+
# pwm.enable
|
|
16
|
+
# sleep 2
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Accuracy: the generating thread sleeps until shortly before each edge and
|
|
20
|
+
# then spins for the last {#spin_us} microseconds, because a bare `sleep`
|
|
21
|
+
# overshoots a microsecond-scale deadline badly — with no spin at all, a 50 Hz
|
|
22
|
+
# 1500 us pulse measured on a Pi 5 spread over 72..6756 us. Spinning holds the
|
|
23
|
+
# GVL, so it is a tax on the main thread, capped at {MAX_SPIN_FRACTION} of the
|
|
24
|
+
# period per edge.
|
|
25
|
+
#
|
|
26
|
+
# Nothing can fix the other direction: while the main thread holds the GVL in
|
|
27
|
+
# a long computation, this thread cannot wake at all. Python has the same
|
|
28
|
+
# limitation with the GIL.
|
|
29
|
+
class SoftwarePWM
|
|
30
|
+
# gpiozero's default for PWMLED, and fast enough that an LED does not
|
|
31
|
+
# visibly flicker.
|
|
32
|
+
DEFAULT_FREQUENCY = 100
|
|
33
|
+
|
|
34
|
+
# How long before each edge to stop sleeping and start spinning. Measured
|
|
35
|
+
# on a Pi 5 at 50 Hz: 0 us gives a 386 us standard deviation and pulses as
|
|
36
|
+
# long as 6.7 ms, 100 us gives 29 us, 300 us gives 13 us, and 1000 us is no
|
|
37
|
+
# better than 300.
|
|
38
|
+
DEFAULT_SPIN_US = 300
|
|
39
|
+
|
|
40
|
+
# Cap on the spin as a fraction of the period, per edge. Spinning holds the
|
|
41
|
+
# GVL, so a fixed 300 us would cost 60% of a core at 1 kHz; this keeps the
|
|
42
|
+
# tax at 10% of one core whatever the frequency, and a frequency that high
|
|
43
|
+
# is driving an LED, where a few microseconds of edge placement is invisible.
|
|
44
|
+
MAX_SPIN_FRACTION = 0.05
|
|
45
|
+
|
|
46
|
+
# The range lgpio accepts, for parity. Above roughly 1 kHz the duty cycle
|
|
47
|
+
# of a Ruby-generated waveform stops being accurate — measure before
|
|
48
|
+
# trusting it.
|
|
49
|
+
FREQUENCY_RANGE = (0.1..10_000)
|
|
50
|
+
|
|
51
|
+
# Open a channel, yield it, then close it.
|
|
52
|
+
# @return [SoftwarePWM, Object] the channel, or the block's value
|
|
53
|
+
def self.open(gpio, **)
|
|
54
|
+
pwm = new(gpio, **)
|
|
55
|
+
return pwm unless block_given?
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
yield pwm
|
|
59
|
+
ensure
|
|
60
|
+
pwm.close
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param gpio [Integer] GPIO line offset (BCM numbering)
|
|
65
|
+
# @param frequency [Numeric] Hz
|
|
66
|
+
# @param duty_cycle [Float] 0.0..1.0
|
|
67
|
+
# @param spin_us [Integer] microseconds to spin before each edge
|
|
68
|
+
# @param chip [Chip, nil] chip to share, or nil to open one
|
|
69
|
+
# @param consumer [String] name shown in the kernel's request list
|
|
70
|
+
def initialize(gpio, frequency: DEFAULT_FREQUENCY, duty_cycle: 0.0, spin_us: DEFAULT_SPIN_US,
|
|
71
|
+
chip: nil, consumer: "rgpio")
|
|
72
|
+
@gpio = gpio
|
|
73
|
+
@spin = validate_spin(spin_us) / 1_000_000.0
|
|
74
|
+
@mutex = Mutex.new
|
|
75
|
+
@frequency = validate_frequency(frequency)
|
|
76
|
+
@duty = validate_duty(duty_cycle)
|
|
77
|
+
@running = false
|
|
78
|
+
@closed = false
|
|
79
|
+
@owns_chip = chip.nil?
|
|
80
|
+
@chip = chip || Chip.new
|
|
81
|
+
@request = @chip.request_lines(
|
|
82
|
+
offsets: [gpio],
|
|
83
|
+
direction: :output,
|
|
84
|
+
initial_value: :inactive,
|
|
85
|
+
consumer: consumer
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Integer] the GPIO line this channel drives
|
|
90
|
+
attr_reader :gpio
|
|
91
|
+
|
|
92
|
+
# @return [Numeric] frequency in Hz
|
|
93
|
+
attr_reader :frequency
|
|
94
|
+
|
|
95
|
+
# @return [Float] duty cycle as a ratio, 0.0..1.0
|
|
96
|
+
def duty_cycle
|
|
97
|
+
@duty
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Alias for parity with {HardwarePWM}, which calls it duty_ratio.
|
|
101
|
+
alias duty_ratio duty_cycle
|
|
102
|
+
|
|
103
|
+
# @return [Integer] microseconds spent spinning before each edge
|
|
104
|
+
def spin_us
|
|
105
|
+
(@spin * 1_000_000).round
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def frequency=(hz)
|
|
109
|
+
hz = validate_frequency(hz)
|
|
110
|
+
@mutex.synchronize { @frequency = hz }
|
|
111
|
+
hz
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def duty_cycle=(ratio)
|
|
115
|
+
ratio = validate_duty(ratio)
|
|
116
|
+
@mutex.synchronize { @duty = ratio }
|
|
117
|
+
ratio
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Set the high time directly, the way a servo is addressed.
|
|
121
|
+
# @param us [Numeric] pulse width in microseconds
|
|
122
|
+
def pulse_width_us=(us)
|
|
123
|
+
raise ArgumentError, "pulse width must not be negative, got #{us}" if us.negative?
|
|
124
|
+
|
|
125
|
+
self.duty_cycle = us / period_us
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @return [Float] current pulse width in microseconds
|
|
129
|
+
def pulse_width_us
|
|
130
|
+
@duty * period_us
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Start generating. Safe to call when already running.
|
|
134
|
+
def enable
|
|
135
|
+
raise Error, "SoftwarePWM on GPIO#{@gpio} is closed" if @closed
|
|
136
|
+
return self if @running
|
|
137
|
+
|
|
138
|
+
@running = true
|
|
139
|
+
@thread = Thread.new { generate }
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Stop generating and leave the line inactive.
|
|
144
|
+
def disable
|
|
145
|
+
return self unless @running
|
|
146
|
+
|
|
147
|
+
@running = false
|
|
148
|
+
@thread&.join
|
|
149
|
+
@thread = nil
|
|
150
|
+
@request.set_value(@gpio, :inactive)
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def enabled?
|
|
155
|
+
@running
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Stop, release the line, and close the chip if this channel opened it.
|
|
159
|
+
# Safe to call multiple times.
|
|
160
|
+
def close
|
|
161
|
+
return if @closed
|
|
162
|
+
|
|
163
|
+
disable
|
|
164
|
+
@closed = true
|
|
165
|
+
@request.release
|
|
166
|
+
@chip.close if @owns_chip
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def closed?
|
|
170
|
+
@closed
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def inspect
|
|
174
|
+
format("#<%s gpio=%d frequency=%gHz duty_cycle=%.3f%s>",
|
|
175
|
+
self.class, @gpio, @frequency, @duty, @running ? " running" : "")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def now
|
|
181
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def period_us
|
|
185
|
+
1_000_000.0 / @frequency
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The generating loop. Deadlines are absolute so that a late wake-up does
|
|
189
|
+
# not push every later edge back by the same amount.
|
|
190
|
+
#
|
|
191
|
+
# The settings are re-read after the falling edge rather than before the
|
|
192
|
+
# rising one: any work done between the deadline and the rising edge is
|
|
193
|
+
# taken straight out of the high time, and reading them under the mutex
|
|
194
|
+
# cost a measurable 17 us of every pulse when it sat there.
|
|
195
|
+
def generate
|
|
196
|
+
period = high = nil
|
|
197
|
+
read_settings do |p, h|
|
|
198
|
+
period = p
|
|
199
|
+
high = h
|
|
200
|
+
end
|
|
201
|
+
spin = effective_spin(period)
|
|
202
|
+
deadline = now
|
|
203
|
+
|
|
204
|
+
while @running
|
|
205
|
+
# A line that is fully on or fully off needs no edges at all, and
|
|
206
|
+
# toggling it anyway would put a one-cycle glitch in the output.
|
|
207
|
+
if high <= 0 || high >= period
|
|
208
|
+
@request.set_value(@gpio, high <= 0 ? :inactive : :active)
|
|
209
|
+
else
|
|
210
|
+
# Time the high phase from the edge that actually happened, not from
|
|
211
|
+
# the nominal deadline: the rise lands a little after it (waking from
|
|
212
|
+
# the long low phase overshoots), and measuring from the deadline took
|
|
213
|
+
# that lateness straight out of the pulse — a measured 15 us of every
|
|
214
|
+
# one. The period stays locked to the absolute deadline below, so only
|
|
215
|
+
# the falling edge's phase drifts, which nothing can observe.
|
|
216
|
+
#
|
|
217
|
+
# The clock is read *before* each set_value so that the call's own
|
|
218
|
+
# latency cancels: it delays both edges by the same amount.
|
|
219
|
+
# Warm the call path with a write of the level the line already holds:
|
|
220
|
+
# the first set_value after waking from the long low phase is both slow
|
|
221
|
+
# and erratic (cold cache, possibly another core), and that lands
|
|
222
|
+
# entirely inside the pulse. Paying it before the clock read leaves the
|
|
223
|
+
# real edge to a warm path.
|
|
224
|
+
@request.set_value(@gpio, :inactive)
|
|
225
|
+
rise = now
|
|
226
|
+
@request.set_value(@gpio, :active)
|
|
227
|
+
wait_until(rise + high, spin)
|
|
228
|
+
@request.set_value(@gpio, :inactive)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
deadline += period
|
|
232
|
+
read_settings do |p, h|
|
|
233
|
+
period = p
|
|
234
|
+
high = h
|
|
235
|
+
end
|
|
236
|
+
# If the thread was starved for longer than a whole cycle, catching up
|
|
237
|
+
# would mean running with no waits at all. Give up the lost cycles.
|
|
238
|
+
deadline = now if deadline < now - period
|
|
239
|
+
spin = effective_spin(period)
|
|
240
|
+
wait_until(deadline, spin)
|
|
241
|
+
end
|
|
242
|
+
rescue StandardError => e
|
|
243
|
+
warn "rgpio: SoftwarePWM on GPIO#{@gpio} stopped: #{e.class}: #{e.message}"
|
|
244
|
+
ensure
|
|
245
|
+
@running = false
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# The spin is capped relative to the period so that a high frequency cannot
|
|
249
|
+
# turn the generating thread into a busy loop.
|
|
250
|
+
def effective_spin(period)
|
|
251
|
+
[@spin, period * MAX_SPIN_FRACTION].min
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Yield the period and high time in seconds. Kept allocation-free: building
|
|
255
|
+
# a two-element array here showed up as jitter.
|
|
256
|
+
def read_settings
|
|
257
|
+
@mutex.synchronize { yield 1.0 / @frequency, @duty / @frequency }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Sleep for the bulk of the wait, then spin: sleep alone overshoots a
|
|
261
|
+
# microsecond-scale deadline by more than the pulse widths we are aiming for.
|
|
262
|
+
def wait_until(target, spin)
|
|
263
|
+
coarse = target - now - spin
|
|
264
|
+
sleep coarse if coarse.positive?
|
|
265
|
+
nil while now < target
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def validate_frequency(hz)
|
|
269
|
+
unless hz.is_a?(Numeric) && FREQUENCY_RANGE.cover?(hz)
|
|
270
|
+
raise ArgumentError, "frequency must be in #{FREQUENCY_RANGE} Hz, got #{hz.inspect}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
hz
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def validate_duty(ratio)
|
|
277
|
+
unless ratio.is_a?(Numeric) && (0.0..1.0).cover?(ratio)
|
|
278
|
+
raise ArgumentError, "duty cycle must be in 0.0..1.0, got #{ratio.inspect}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
ratio.to_f
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def validate_spin(us)
|
|
285
|
+
raise ArgumentError, "spin_us must not be negative, got #{us}" unless us.is_a?(Numeric) && !us.negative?
|
|
286
|
+
|
|
287
|
+
us
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
data/lib/rgpio/spi.rb
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
require "fiddle"
|
|
2
|
+
|
|
3
|
+
module Rgpio
|
|
4
|
+
# A device on a Linux spidev bus (/dev/spidevB.D).
|
|
5
|
+
#
|
|
6
|
+
# Like {I2C}, this is ioctl work on a character device, so it needs no
|
|
7
|
+
# libgpiod and works even where Rgpio.available? is false.
|
|
8
|
+
#
|
|
9
|
+
# Usage (block form — recommended):
|
|
10
|
+
# Rgpio::SPI.open(bus: 0, device: 0, speed_hz: 1_000_000) do |spi|
|
|
11
|
+
# rx = spi.transfer([0x06, 0x00, 0x00])
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# SPI is full duplex: every transfer clocks the same number of bytes in each
|
|
15
|
+
# direction, so {#transfer} always answers with as many bytes as it was given.
|
|
16
|
+
# {#write} and {#read} are that same transfer with one direction ignored.
|
|
17
|
+
#
|
|
18
|
+
# The header bus is SPI0 (GPIO10 = MOSI, GPIO9 = MISO, GPIO11 = SCLK, GPIO8 =
|
|
19
|
+
# CE0, GPIO7 = CE1), and only appears once it is enabled — see README for the
|
|
20
|
+
# dtparam line.
|
|
21
|
+
class SPI
|
|
22
|
+
# ioctl numbers from <linux/spi/spidev.h>. They are built here rather than
|
|
23
|
+
# written out so the derivation stays checkable: direction in bits 30-31,
|
|
24
|
+
# payload size in 16-29, a 'k' magic in 8-15, and the request in 0-7.
|
|
25
|
+
IOC_MAGIC = 0x6b
|
|
26
|
+
IOC_WRITE = 1
|
|
27
|
+
IOC_READ = 2
|
|
28
|
+
|
|
29
|
+
# struct spi_ioc_transfer is { __u64 tx_buf; __u64 rx_buf; __u32 len;
|
|
30
|
+
# __u32 speed_hz; __u16 delay_usecs; __u8 bits_per_word, cs_change,
|
|
31
|
+
# tx_nbits, rx_nbits, word_delay_usecs, pad; } — exactly 32 bytes, and the
|
|
32
|
+
# header promises the same layout in 32- and 64-bit userspace. The two
|
|
33
|
+
# buffer fields are __u64 even where a pointer is 32 bits wide.
|
|
34
|
+
TRANSFER_SIZE = 32
|
|
35
|
+
|
|
36
|
+
DEFAULT_SPEED_HZ = 1_000_000
|
|
37
|
+
DEFAULT_BITS_PER_WORD = 8
|
|
38
|
+
|
|
39
|
+
# Clock polarity and phase, as the kernel numbers them.
|
|
40
|
+
MODE_RANGE = (0..3)
|
|
41
|
+
|
|
42
|
+
# @return [Integer] the ioctl request for a message of `count` transfers
|
|
43
|
+
def self.message_ioctl(count)
|
|
44
|
+
ioctl_number(IOC_WRITE, 0, TRANSFER_SIZE * count)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Integer] an encoded ioctl request
|
|
48
|
+
def self.ioctl_number(direction, request, size)
|
|
49
|
+
(direction << 30) | (size << 16) | (IOC_MAGIC << 8) | request
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
IOC_WR_MODE = ioctl_number(IOC_WRITE, 1, 1)
|
|
53
|
+
IOC_RD_MODE = ioctl_number(IOC_READ, 1, 1)
|
|
54
|
+
IOC_WR_LSB_FIRST = ioctl_number(IOC_WRITE, 2, 1)
|
|
55
|
+
IOC_WR_BITS_PER_WORD = ioctl_number(IOC_WRITE, 3, 1)
|
|
56
|
+
IOC_RD_BITS_PER_WORD = ioctl_number(IOC_READ, 3, 1)
|
|
57
|
+
IOC_WR_MAX_SPEED_HZ = ioctl_number(IOC_WRITE, 4, 4)
|
|
58
|
+
IOC_RD_MAX_SPEED_HZ = ioctl_number(IOC_READ, 4, 4)
|
|
59
|
+
|
|
60
|
+
# Pack a struct spi_ioc_transfer. Kept at class level so the layout can be
|
|
61
|
+
# checked without a bus present.
|
|
62
|
+
# @return [String]
|
|
63
|
+
def self.pack_transfer(tx_addr, rx_addr, len, speed_hz, bits_per_word: DEFAULT_BITS_PER_WORD,
|
|
64
|
+
delay_us: 0, cs_change: false)
|
|
65
|
+
# "Q", not "J": the two address fields are __u64 even where a pointer is
|
|
66
|
+
# only 32 bits wide.
|
|
67
|
+
[tx_addr, rx_addr].pack("Q2") +
|
|
68
|
+
[len, speed_hz].pack("LL") +
|
|
69
|
+
[delay_us].pack("S") +
|
|
70
|
+
[bits_per_word, cs_change ? 1 : 0, 0, 0, 0, 0].pack("C6")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Array<Array(Integer, Integer)>] [bus, device] of every spidev node
|
|
74
|
+
def self.devices
|
|
75
|
+
Dir.glob("/dev/spidev*").filter_map do |path|
|
|
76
|
+
match = path.match(%r{/dev/spidev(\d+)\.(\d+)\z})
|
|
77
|
+
[match[1].to_i, match[2].to_i] if match
|
|
78
|
+
end.sort
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Open a device, yielding it and closing it afterwards when a block is given.
|
|
82
|
+
# @return [SPI, Object] the device, or the block's value
|
|
83
|
+
def self.open(**)
|
|
84
|
+
spi = new(**)
|
|
85
|
+
return spi unless block_given?
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
yield spi
|
|
89
|
+
ensure
|
|
90
|
+
spi.close
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param bus [Integer] spidev bus number; 0 is the 40-pin header
|
|
95
|
+
# @param device [Integer] chip-select index on that bus
|
|
96
|
+
# @param speed_hz [Integer] clock rate
|
|
97
|
+
# @param mode [Integer] 0..3, clock polarity and phase
|
|
98
|
+
# @param bits_per_word [Integer] word size in bits
|
|
99
|
+
def initialize(bus: 0, device: 0, speed_hz: DEFAULT_SPEED_HZ, mode: 0,
|
|
100
|
+
bits_per_word: DEFAULT_BITS_PER_WORD)
|
|
101
|
+
@bus = bus
|
|
102
|
+
@device = device
|
|
103
|
+
@path = "/dev/spidev#{bus}.#{device}"
|
|
104
|
+
unless File.exist?(@path)
|
|
105
|
+
raise SPIError,
|
|
106
|
+
"#{@path} not found. Enable the header bus with `dtparam=spi=on` in /boot/firmware/config.txt"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@io = File.open(@path, "r+b")
|
|
110
|
+
@closed = false
|
|
111
|
+
self.mode = mode
|
|
112
|
+
self.bits_per_word = bits_per_word
|
|
113
|
+
self.speed_hz = speed_hz
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @return [Integer] spidev bus number
|
|
117
|
+
attr_reader :bus
|
|
118
|
+
|
|
119
|
+
# @return [Integer] chip-select index
|
|
120
|
+
attr_reader :device
|
|
121
|
+
|
|
122
|
+
# @return [String] path of the bus character device
|
|
123
|
+
attr_reader :path
|
|
124
|
+
|
|
125
|
+
# @return [Integer] clock rate in Hz
|
|
126
|
+
attr_reader :speed_hz
|
|
127
|
+
|
|
128
|
+
# @return [Integer] 0..3
|
|
129
|
+
attr_reader :mode
|
|
130
|
+
|
|
131
|
+
# @return [Integer] word size in bits
|
|
132
|
+
attr_reader :bits_per_word
|
|
133
|
+
|
|
134
|
+
def speed_hz=(hz)
|
|
135
|
+
raise ArgumentError, "speed_hz must be positive, got #{hz.inspect}" unless hz.is_a?(Integer) && hz.positive?
|
|
136
|
+
|
|
137
|
+
@io.ioctl(IOC_WR_MAX_SPEED_HZ, [hz].pack("L"))
|
|
138
|
+
@speed_hz = hz
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def mode=(value)
|
|
142
|
+
raise ArgumentError, "mode must be in #{MODE_RANGE}, got #{value.inspect}" unless MODE_RANGE.cover?(value)
|
|
143
|
+
|
|
144
|
+
@io.ioctl(IOC_WR_MODE, [value].pack("C"))
|
|
145
|
+
@mode = value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def bits_per_word=(bits)
|
|
149
|
+
unless bits.is_a?(Integer) && bits.positive?
|
|
150
|
+
raise ArgumentError,
|
|
151
|
+
"bits_per_word must be positive, got #{bits.inspect}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
@io.ioctl(IOC_WR_BITS_PER_WORD, [bits].pack("C"))
|
|
155
|
+
@bits_per_word = bits
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Clock bytes out and in at the same time.
|
|
159
|
+
# @param bytes [Array<Integer>, String] bytes to send
|
|
160
|
+
# @param speed_hz [Integer, nil] override the clock for this transfer only
|
|
161
|
+
# @param delay_us [Integer] hold the chip select this long afterwards
|
|
162
|
+
# @return [Array<Integer>] the bytes that came back, one per byte sent
|
|
163
|
+
def transfer(bytes, speed_hz: nil, delay_us: 0)
|
|
164
|
+
out = Bytes.pack(bytes)
|
|
165
|
+
raise ArgumentError, "transfer needs at least one byte" if out.empty?
|
|
166
|
+
|
|
167
|
+
tx = buffer(out)
|
|
168
|
+
rx = Fiddle::Pointer.malloc(out.bytesize, Fiddle::RUBY_FREE)
|
|
169
|
+
message = self.class.pack_transfer(tx.to_i, rx.to_i, out.bytesize, speed_hz || @speed_hz,
|
|
170
|
+
bits_per_word: @bits_per_word, delay_us: delay_us)
|
|
171
|
+
@io.ioctl(self.class.message_ioctl(1), message)
|
|
172
|
+
rx[0, out.bytesize].unpack("C*")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Send bytes, ignoring what comes back.
|
|
176
|
+
# @return [Integer] number of bytes sent
|
|
177
|
+
def write(*bytes)
|
|
178
|
+
transfer(bytes).size
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Clock in `count` bytes, sending zeros.
|
|
182
|
+
# @return [Array<Integer>]
|
|
183
|
+
def read(count)
|
|
184
|
+
raise ArgumentError, "count must be positive, got #{count}" unless count.positive?
|
|
185
|
+
|
|
186
|
+
transfer([0] * count)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def close
|
|
190
|
+
return if @closed
|
|
191
|
+
|
|
192
|
+
@closed = true
|
|
193
|
+
@io.close
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def closed?
|
|
197
|
+
@closed
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def buffer(str)
|
|
203
|
+
ptr = Fiddle::Pointer.malloc(str.bytesize, Fiddle::RUBY_FREE)
|
|
204
|
+
ptr[0, str.bytesize] = str
|
|
205
|
+
ptr
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
data/lib/rgpio.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require_relative "rgpio/version"
|
|
2
|
+
require_relative "rgpio/native"
|
|
3
|
+
require_relative "rgpio/chip"
|
|
4
|
+
require_relative "rgpio/line_request"
|
|
5
|
+
require_relative "rgpio/pwm"
|
|
6
|
+
require_relative "rgpio/software_pwm"
|
|
7
|
+
require_relative "rgpio/bytes"
|
|
8
|
+
require_relative "rgpio/i2c"
|
|
9
|
+
require_relative "rgpio/spi"
|
|
10
|
+
require_relative "rgpio/devices/device"
|
|
11
|
+
require_relative "rgpio/devices/output_device"
|
|
12
|
+
require_relative "rgpio/devices/input_device"
|
|
13
|
+
require_relative "rgpio/devices/motor"
|
|
14
|
+
require_relative "rgpio/devices/pwm_channel"
|
|
15
|
+
require_relative "rgpio/devices/pwm_output_device"
|
|
16
|
+
require_relative "rgpio/devices/rgb_led"
|
|
17
|
+
require_relative "rgpio/devices/servo"
|
|
18
|
+
require_relative "rgpio/devices/adt7410"
|
|
19
|
+
require_relative "rgpio/devices/st7032"
|
|
20
|
+
require_relative "rgpio/devices/mcp3208"
|
|
21
|
+
|
|
22
|
+
# Ruby bindings for libgpiod v2 (Linux GPIO character device), bound through
|
|
23
|
+
# the stdlib `fiddle`. Targets Debian Trixie (libgpiod >= 2.1) on Raspberry Pi.
|
|
24
|
+
#
|
|
25
|
+
# Quick start — GPIO output:
|
|
26
|
+
# Rgpio::Chip.open do |chip|
|
|
27
|
+
# req = chip.request_lines(offsets: [17], direction: :output, consumer: "led")
|
|
28
|
+
# req.set_value(17, :active)
|
|
29
|
+
# sleep 1
|
|
30
|
+
# req.set_value(17, :inactive)
|
|
31
|
+
# req.release
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# Quick start — Hardware PWM (servo):
|
|
35
|
+
# Rgpio::HardwarePWM.open(gpio: 18) do |pwm|
|
|
36
|
+
# pwm.frequency = 50
|
|
37
|
+
# pwm.duty_cycle = 0.075
|
|
38
|
+
# pwm.enable
|
|
39
|
+
# sleep 2
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# Quick start — high-level devices:
|
|
43
|
+
# led = Rgpio::LED.new(4)
|
|
44
|
+
# led.on
|
|
45
|
+
#
|
|
46
|
+
# button = Rgpio::Button.new(17)
|
|
47
|
+
# button.when_pressed { puts "Pressed" }
|
|
48
|
+
# Rgpio.pause
|
|
49
|
+
#
|
|
50
|
+
# Quick start — I2C devices:
|
|
51
|
+
# puts Rgpio::ADT7410.new.temperature
|
|
52
|
+
#
|
|
53
|
+
# lcd = Rgpio::ST7032.new
|
|
54
|
+
# lcd.message = "Hello\nrgpio"
|
|
55
|
+
module Rgpio
|
|
56
|
+
# Raised for gem-level errors not covered by stdlib Errno classes.
|
|
57
|
+
class Error < StandardError; end
|
|
58
|
+
|
|
59
|
+
# Raised when libgpiod shared library cannot be loaded on the current system.
|
|
60
|
+
class NotAvailableError < Error; end
|
|
61
|
+
|
|
62
|
+
# Raised for PWM-related errors.
|
|
63
|
+
class PWMError < Error; end
|
|
64
|
+
|
|
65
|
+
# Raised for I2C-related errors not reported as an Errno by the kernel.
|
|
66
|
+
class I2CError < Error; end
|
|
67
|
+
|
|
68
|
+
# Raised for SPI-related errors not reported as an Errno by the kernel.
|
|
69
|
+
class SPIError < Error; end
|
|
70
|
+
|
|
71
|
+
# @return [Boolean] whether the libgpiod shared library is loaded
|
|
72
|
+
def self.available?
|
|
73
|
+
Native::LIBRARY_AVAILABLE
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Raise NotAvailableError unless libgpiod is loaded.
|
|
77
|
+
def self.assert_available!
|
|
78
|
+
return if available?
|
|
79
|
+
|
|
80
|
+
raise NotAvailableError,
|
|
81
|
+
"libgpiod shared library not found. " \
|
|
82
|
+
"Install on Debian/Raspbian: sudo apt install libgpiod3"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [String, nil] libgpiod version string (e.g. "2.1.3"), or nil if unavailable
|
|
86
|
+
def self.version
|
|
87
|
+
return nil unless available?
|
|
88
|
+
|
|
89
|
+
Native.gpiod_api_version
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Block the main thread until Ctrl-C, letting device callbacks run.
|
|
93
|
+
# The Ruby counterpart of Python's signal.pause().
|
|
94
|
+
def self.pause
|
|
95
|
+
sleep
|
|
96
|
+
rescue Interrupt
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|