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
data/README.md
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
# rgpio
|
|
2
|
+
|
|
3
|
+
Ruby bindings for [libgpiod v2](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git) — the modern Linux GPIO character device API.
|
|
4
|
+
|
|
5
|
+
Provides GPIO input/output and jitter-free hardware PWM control on Raspberry Pi, targeting the `uAPI v2` ioctl interface instead of the deprecated sysfs GPIO interface. No C extension — calls `libgpiod.so` directly through the stdlib [`fiddle`](https://github.com/ruby/fiddle), which (unlike the precompiled `ffi` gem) is built with the interpreter and works on every Pi, including ARMv6 boards (Pi Zero / Pi 1).
|
|
6
|
+
|
|
7
|
+
> **Status:** GPIO + hardware PWM verified on Raspberry Pi 5 and Raspberry Pi 4
|
|
8
|
+
> (Trixie, libgpiod 2.x). The device API (`LED` / `Button` / `Motor`) is verified
|
|
9
|
+
> on Pi 5. Hardware PWM (sysfs) also works on a Bookworm Pi 4, where the libgpiod
|
|
10
|
+
> GPIO path is unavailable (v1). Multi-board support, the roadmap, and planned
|
|
11
|
+
> APIs are tracked in [PLAN.md](PLAN.md); released changes are recorded in
|
|
12
|
+
> [CHANGELOG.md](CHANGELOG.md).
|
|
13
|
+
|
|
14
|
+
## Why libgpiod?
|
|
15
|
+
|
|
16
|
+
| Approach | Pi 5 works? | Notes |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `sysfs` GPIO (`/sys/class/gpio`) | No | Deprecated since kernel 4.8 |
|
|
19
|
+
| Direct register access (`pigpio`, old `RPi.GPIO`) | No | RP1 chip not supported |
|
|
20
|
+
| `libgpiod` (GPIO character device) | **Yes** | Modern, Pi-model agnostic |
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- **OS:** Debian Trixie (13) or later — verified target. Bookworm ships libgpiod 1.x, which is not supported.
|
|
25
|
+
- **Hardware:** Raspberry Pi 5 and Pi 4 (verified). Other Pi models: see [PLAN.md](PLAN.md).
|
|
26
|
+
- **Library:** `libgpiod2` (>= 2.1)
|
|
27
|
+
- **Ruby:** >= 3.3 (CRuby) — matches Trixie's default `ruby`
|
|
28
|
+
|
|
29
|
+
Install the runtime library on the Pi:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
sudo apt update
|
|
33
|
+
sudo apt install libgpiod3
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
To verify libgpiod is available and working:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
gpiodetect # lists GPIO chips
|
|
40
|
+
gpioinfo --chip gpiochip0 # lists lines on chip 0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Add to your `Gemfile`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
gem "rgpio"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or install directly:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
gem install rgpio
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Device API
|
|
58
|
+
|
|
59
|
+
`LED`, `Button`, `Motor`, `PWMLED`, `RGBLED` and `Servo` wrap `Chip` /
|
|
60
|
+
`LineRequest` in one object per piece of hardware, in the style of Python's
|
|
61
|
+
gpiozero. A device opens its own chip
|
|
62
|
+
unless you hand it one with `chip:`, and `#close` releases only what it owns.
|
|
63
|
+
Devices still under hardware validation are listed in [PLAN.md](PLAN.md).
|
|
64
|
+
|
|
65
|
+
### LED
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
require "rgpio"
|
|
69
|
+
|
|
70
|
+
led = Rgpio::LED.new(4) # GPIO4 = physical pin 7
|
|
71
|
+
|
|
72
|
+
5.times do
|
|
73
|
+
led.on
|
|
74
|
+
sleep 1
|
|
75
|
+
led.off
|
|
76
|
+
sleep 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
led.close # releases the line and the chip
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`LED` is an `OutputDevice`, which also offers `#toggle`, `#value` / `#value=`
|
|
83
|
+
and `#on?`. Pass `active_low: true` when the LED is wired to sink current
|
|
84
|
+
(`#on` then drives the line low), and `initial_value: true` to have it lit the
|
|
85
|
+
moment the line is claimed.
|
|
86
|
+
|
|
87
|
+
### Button
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
require "rgpio"
|
|
91
|
+
|
|
92
|
+
button = Rgpio::Button.new(4)
|
|
93
|
+
|
|
94
|
+
button.when_pressed { puts "Pressed" }
|
|
95
|
+
button.when_released { puts "Released" }
|
|
96
|
+
|
|
97
|
+
Rgpio.pause # block until Ctrl-C while callbacks run
|
|
98
|
+
button.close
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Callbacks run on a watcher thread that waits on kernel edge events, one thread
|
|
102
|
+
per device, started by the first callback and stopped by `#close`. A callback
|
|
103
|
+
that raises is reported on `$stderr` without taking the watcher down.
|
|
104
|
+
`Rgpio.pause` is the counterpart of Python's `signal.pause()`.
|
|
105
|
+
|
|
106
|
+
The default bias is pull-**down**, for a switch wired between the GPIO line and
|
|
107
|
+
3.3 V; pass `pull_up: true` for one wired to GND. Presses are debounced in the
|
|
108
|
+
kernel for 5 ms (`debounce_us:` to change it), so one press fires one callback.
|
|
109
|
+
|
|
110
|
+
`active_low: true` inverts the logic, and the callbacks follow it: the kernel
|
|
111
|
+
reports edges in logical terms, so `when_pressed` fires when the line goes
|
|
112
|
+
*low*.
|
|
113
|
+
|
|
114
|
+
### Motor
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
require "rgpio"
|
|
118
|
+
|
|
119
|
+
motor = Rgpio::Motor.new(forward: 2, backward: 14)
|
|
120
|
+
|
|
121
|
+
motor.forward
|
|
122
|
+
sleep 5
|
|
123
|
+
motor.backward
|
|
124
|
+
sleep 5
|
|
125
|
+
motor.stop
|
|
126
|
+
|
|
127
|
+
motor.close
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Written for a two-input driver such as the DRV8835 or SN754410 in IN/IN mode:
|
|
131
|
+
one line drives the motor forward, the other backward, and `Motor` drops one
|
|
132
|
+
before raising the other so the driver is never asked to source and sink the
|
|
133
|
+
same output at once. Wire the motor across the driver's two outputs
|
|
134
|
+
(`AOUT1`/`AOUT2`) — with one terminal on GND only one direction works. Speed
|
|
135
|
+
control would need PWM on both lines and is not implemented.
|
|
136
|
+
|
|
137
|
+
### PWMLED
|
|
138
|
+
|
|
139
|
+
`LED` is on or off; `PWMLED` has a level in between, from a PWM channel.
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
led = Rgpio::PWMLED.new(4)
|
|
143
|
+
led.value = 0.25 # a quarter bright (#brightness is an alias)
|
|
144
|
+
led.on # 1.0
|
|
145
|
+
led.toggle # inverts the level: 0.25 becomes 0.75
|
|
146
|
+
led.close
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The level holds with no further calls — one assignment, and the channel keeps
|
|
150
|
+
generating. Any line works: the default channel is {Rgpio::SoftwarePWM}, which
|
|
151
|
+
needs no dtoverlay. See [Software PWM](#software-pwm) for `pwm: :hardware`.
|
|
152
|
+
|
|
153
|
+
### RGBLED
|
|
154
|
+
|
|
155
|
+
Three PWM channels as one object, one per colour.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
led = Rgpio::RGBLED.new(red: 17, green: 27, blue: 22)
|
|
159
|
+
led.color = :magenta # or any name in Rgpio::RGBLED::COLORS
|
|
160
|
+
led.color = [1.0, 0.4, 0.0] # or a triple, 0.0..1.0 each
|
|
161
|
+
led.blue = 0.5 # or one channel at a time
|
|
162
|
+
led.off
|
|
163
|
+
led.close
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The default wiring is common cathode: each line drives its colour through its own
|
|
167
|
+
resistor and the common leg goes to GND. For a common-anode part, tie the common
|
|
168
|
+
leg to 3.3 V and pass `active_low: true`.
|
|
169
|
+
|
|
170
|
+
White comes out tinted unless the channels are scaled to match, because the three
|
|
171
|
+
dies are not equally bright for equal duty:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
led = Rgpio::RGBLED.new(red: 17, green: 27, blue: 22, balance: [1.0, 0.8, 0.8])
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`balance:` scales every level asked for, so named colours come out right too, and
|
|
178
|
+
`#color` keeps reporting what was asked for. The right value belongs to the part,
|
|
179
|
+
not to the gem — `ruby examples/rgb_balance.rb` steps through candidates with the
|
|
180
|
+
LED showing white so you can pick one by eye.
|
|
181
|
+
|
|
182
|
+
### Servo
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
servo = Rgpio::Servo.new(4)
|
|
186
|
+
servo.max # one end of the travel
|
|
187
|
+
servo.mid # centre
|
|
188
|
+
servo.angle = 45 # or by angle, -90..90 by default
|
|
189
|
+
servo.detach # stop the pulses: the horn goes limp
|
|
190
|
+
servo.close
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
A servo holds its position only while pulses keep arriving, so `#detach` stops
|
|
194
|
+
sending them: the horn can then be turned by hand, and the servo stops drawing
|
|
195
|
+
the current — and making the heat — of holding against a load. It has no grip on
|
|
196
|
+
anything while detached. Setting `#value` or `#angle` again resumes;
|
|
197
|
+
`#attached?` reports which it is.
|
|
198
|
+
|
|
199
|
+
Pulse widths vary by servo. The default 1000..2000 us is the range every hobby
|
|
200
|
+
servo understands, and many reach further:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
servo = Rgpio::Servo.new(4, min_pulse_us: 500, max_pulse_us: 2500,
|
|
204
|
+
min_angle: 0, max_angle: 180)
|
|
205
|
+
servo.pulse_width_us = 2300 # drive a width directly, to find the real travel
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A servo driven past its travel buzzes and heats up, so widen the range only as
|
|
209
|
+
far as the part allows, and by measuring rather than by trusting.
|
|
210
|
+
|
|
211
|
+
### Sharing one chip
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
chip = Rgpio::Chip.new
|
|
215
|
+
red = Rgpio::LED.new(17, chip: chip)
|
|
216
|
+
green = Rgpio::LED.new(27, chip: chip)
|
|
217
|
+
|
|
218
|
+
red.on
|
|
219
|
+
green.off
|
|
220
|
+
|
|
221
|
+
[red, green].each(&:close) # the chip stays open — the devices did not open it
|
|
222
|
+
chip.close
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## GPIO Usage
|
|
228
|
+
|
|
229
|
+
The classes the device API is built on. Use them when you need control the
|
|
230
|
+
device classes do not expose — batch I/O across lines, raw edge-event
|
|
231
|
+
timestamps, or a specific `/dev/gpiochipN`.
|
|
232
|
+
|
|
233
|
+
### LED blink (output)
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
require "rgpio"
|
|
237
|
+
|
|
238
|
+
# Block form ensures the chip is closed on exit.
|
|
239
|
+
# With no path, the 40-pin header GPIO controller is auto-detected by label
|
|
240
|
+
# (pass "/dev/gpiochipN" to select one explicitly).
|
|
241
|
+
Rgpio::Chip.open do |chip|
|
|
242
|
+
puts chip.path # "/dev/gpiochip0"
|
|
243
|
+
puts chip.label # "pinctrl-rp1" on Pi 5
|
|
244
|
+
puts chip.num_lines # 54
|
|
245
|
+
|
|
246
|
+
request = chip.request_lines(
|
|
247
|
+
offsets: [17], # GPIO17 = physical pin 11
|
|
248
|
+
direction: :output,
|
|
249
|
+
consumer: "my-app" # visible in gpioinfo output
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
5.times do
|
|
253
|
+
request.set_value(17, :active)
|
|
254
|
+
sleep 0.5
|
|
255
|
+
request.set_value(17, :inactive)
|
|
256
|
+
sleep 0.5
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
request.release
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Button input with edge detection
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
require "rgpio"
|
|
267
|
+
|
|
268
|
+
Rgpio::Chip.open do |chip|
|
|
269
|
+
request = chip.request_lines(
|
|
270
|
+
offsets: [27], # GPIO27 = physical pin 13
|
|
271
|
+
direction: :input,
|
|
272
|
+
edge: :both, # detect press and release
|
|
273
|
+
bias: :pull_up, # internal pull-up resistor
|
|
274
|
+
active_low: true, # button connects pin to GND
|
|
275
|
+
consumer: "button-reader"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
puts "Waiting for button events (Ctrl-C to stop)..."
|
|
279
|
+
loop do
|
|
280
|
+
events = request.read_edge_events(timeout: nil) # block indefinitely
|
|
281
|
+
events.each do |event|
|
|
282
|
+
state = event[:type] == :rising ? "RELEASED" : "PRESSED"
|
|
283
|
+
puts "GPIO#{event[:offset]} #{state} at #{event[:timestamp_ns]} ns"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
ensure
|
|
287
|
+
request&.release
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`read_edge_events` returns an array of hashes:
|
|
292
|
+
|
|
293
|
+
| Key | Type | Description |
|
|
294
|
+
|---|---|---|
|
|
295
|
+
| `:type` | `:rising` / `:falling` | Edge direction |
|
|
296
|
+
| `:offset` | Integer | GPIO line offset |
|
|
297
|
+
| `:timestamp_ns` | Integer | Kernel monotonic timestamp (nanoseconds) |
|
|
298
|
+
|
|
299
|
+
### `request_lines` options
|
|
300
|
+
|
|
301
|
+
| Option | Values | Default | Notes |
|
|
302
|
+
|---|---|---|---|
|
|
303
|
+
| `offsets:` | `Array<Integer>` | — | Required |
|
|
304
|
+
| `direction:` | `:input`, `:output` | — | Required |
|
|
305
|
+
| `edge:` | `:none`, `:rising`, `:falling`, `:both` | `:none` | Input only |
|
|
306
|
+
| `bias:` | `:as_is`, `:disabled`, `:pull_up`, `:pull_down` | `:as_is` | |
|
|
307
|
+
| `active_low:` | `true` / `false` | `false` | |
|
|
308
|
+
| `initial_value:` | `:active`, `:inactive` | `:inactive` | Output only |
|
|
309
|
+
| `consumer:` | String | `nil` | Shown in `gpioinfo` |
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Software PWM
|
|
314
|
+
|
|
315
|
+
`Rgpio::SoftwarePWM` generates PWM in Ruby on any GPIO line. It needs no
|
|
316
|
+
dtoverlay and no config.txt entry, which is why the device classes above use it
|
|
317
|
+
by default.
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
Rgpio::SoftwarePWM.open(18) do |pwm|
|
|
321
|
+
pwm.frequency = 100
|
|
322
|
+
pwm.duty_cycle = 0.25
|
|
323
|
+
pwm.enable
|
|
324
|
+
sleep 2
|
|
325
|
+
pwm.pulse_width_us = 1500 # or set the high time directly
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
It presents the same interface as `HardwarePWM` — `frequency=`, `duty_cycle=`,
|
|
330
|
+
`pulse_width_us=`, `enable`/`disable`, `close` — so the device classes take
|
|
331
|
+
either. Pass `pwm: :hardware` on GPIO12/13/18/19 for the peripheral instead:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
servo = Rgpio::Servo.new(12, pwm: :hardware) # needs the dtoverlay, see below
|
|
335
|
+
led = Rgpio::PWMLED.new(13, pwm: channel) # or share a channel you own
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Which to use
|
|
339
|
+
|
|
340
|
+
| | Software PWM | Hardware PWM |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| Setup | none | `dtoverlay` in config.txt |
|
|
343
|
+
| Lines | any | GPIO12/13/18/19, two at a time on the header |
|
|
344
|
+
| Accuracy | 6 us of spread at 50 Hz on an idle Pi 5 | exact |
|
|
345
|
+
| Cost | 2.7% of one core per channel at 50 Hz | none |
|
|
346
|
+
|
|
347
|
+
The generating thread sleeps until shortly before each edge and then spins for
|
|
348
|
+
the last `spin_us` (300 by default, capped at 5% of the period), because sleeping
|
|
349
|
+
the whole way overshoots a microsecond-scale deadline badly. Spinning holds the
|
|
350
|
+
GVL, which is where the CPU figure comes from.
|
|
351
|
+
|
|
352
|
+
Measured on a Pi 5 with a jumper between two header pins and the kernel's own
|
|
353
|
+
edge timestamps (`examples/pwm_jitter.rb`): a 50 Hz 1500 us pulse — a servo's
|
|
354
|
+
centre — came out at 1504.9 us with 6.2 us of standard deviation, or about half a
|
|
355
|
+
degree of travel. A busy machine widens that: while the main thread holds the
|
|
356
|
+
GVL, the generating thread cannot wake. Python has the same limitation with the
|
|
357
|
+
GIL, and gpiozero's PWM is software-timed too.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Hardware PWM Usage
|
|
362
|
+
|
|
363
|
+
Hardware PWM is controlled through the Linux PWM sysfs interface
|
|
364
|
+
(`/sys/class/pwm/pwmchipN/`). No FFI required — pure file I/O.
|
|
365
|
+
|
|
366
|
+
### Step 1 — Enable the PWM overlay
|
|
367
|
+
|
|
368
|
+
Add the appropriate line to `/boot/firmware/config.txt` and **reboot**:
|
|
369
|
+
|
|
370
|
+
| GPIO pin | PWM channel | Alt function | config.txt entry |
|
|
371
|
+
|---|---|---|---|
|
|
372
|
+
| GPIO12 (pin 32) | PWM0 | Alt0 | `dtoverlay=pwm,pin=12,func=4` |
|
|
373
|
+
| GPIO13 (pin 33) | PWM1 | Alt0 | `dtoverlay=pwm,pin=13,func=4` |
|
|
374
|
+
| GPIO18 (pin 12) | PWM0 | Alt5 | `dtoverlay=pwm,pin=18,func=2` |
|
|
375
|
+
| GPIO19 (pin 35) | PWM1 | Alt5 | `dtoverlay=pwm,pin=19,func=2` |
|
|
376
|
+
|
|
377
|
+
> **`func` is the pin's Alt function, not the channel number:** GPIO12/13 use
|
|
378
|
+
> `func=4` (Alt0), but GPIO18/19 use `func=2` (Alt5). Using the wrong `func`
|
|
379
|
+
> loads the overlay without routing the pin to PWM — the pin stays `input` and
|
|
380
|
+
> nothing reaches it. Verify with `pinctrl get <n>` (expect e.g. `a0` = Alt0).
|
|
381
|
+
|
|
382
|
+
To enable two channels simultaneously (e.g. GPIO18 + GPIO19):
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
dtoverlay=pwm-2chan,pin=18,func=2,pin2=19,func2=2
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Without rebooting** (volatile, for quick testing) you can load the same
|
|
389
|
+
overlay at runtime — pass the parameters space-separated instead of as a CSV:
|
|
390
|
+
|
|
391
|
+
```sh
|
|
392
|
+
sudo dtoverlay pwm pin=12 func=4 # = dtoverlay=pwm,pin=12,func=4
|
|
393
|
+
sudo dtoverlay -l # list loaded overlays
|
|
394
|
+
sudo dtoverlay -r pwm # unload
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
> **Always pass `pin` and `func` explicitly.** With no arguments
|
|
398
|
+
> `sudo dtoverlay pwm` defaults to `pin=18,func=2` (GPIO18), so a servo wired to
|
|
399
|
+
> GPIO12 gets no signal even though the program runs to completion.
|
|
400
|
+
|
|
401
|
+
### Step 2 — Verify sysfs entry
|
|
402
|
+
|
|
403
|
+
After reboot, PWM chips should appear:
|
|
404
|
+
|
|
405
|
+
```sh
|
|
406
|
+
ls /sys/class/pwm/
|
|
407
|
+
# pwmchip0 pwmchip2
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
On Pi 5 the RP1 GPIO-header PWM chip typically appears as `pwmchip2` with 4 channels (`npwm=4`), but the number can vary with kernel version. `HardwarePWM` auto-detects the correct chip.
|
|
411
|
+
|
|
412
|
+
### Step 3 — Drive a servo
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
require "rgpio"
|
|
416
|
+
|
|
417
|
+
# gpio: auto-selects chip and channel for GPIO18 on Pi 5
|
|
418
|
+
Rgpio::HardwarePWM.open(gpio: 18) do |pwm|
|
|
419
|
+
puts "Using pwmchip#{pwm.chip_num}, channel #{pwm.channel}"
|
|
420
|
+
|
|
421
|
+
pwm.frequency = 50 # Hz — standard servo period (20 ms)
|
|
422
|
+
pwm.duty_cycle = 0.075 # 7.5 % = 1.5 ms pulse = center position
|
|
423
|
+
pwm.enable
|
|
424
|
+
|
|
425
|
+
sleep 1
|
|
426
|
+
|
|
427
|
+
pwm.pulse_width_us = 1000 # 1.0 ms — minimum position
|
|
428
|
+
sleep 1
|
|
429
|
+
pwm.pulse_width_us = 2000 # 2.0 ms — maximum position
|
|
430
|
+
sleep 1
|
|
431
|
+
pwm.pulse_width_us = 1500 # back to center
|
|
432
|
+
sleep 1
|
|
433
|
+
end
|
|
434
|
+
# PWM is automatically disabled and unexported here
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### GPIO-to-PWM mapping
|
|
438
|
+
|
|
439
|
+
The mapping is board-specific and auto-detected from the device-tree model.
|
|
440
|
+
|
|
441
|
+
**Pi 5 (RP1)** — one 4-channel chip:
|
|
442
|
+
|
|
443
|
+
| GPIO | Physical pin | RP1 PWM channel |
|
|
444
|
+
|---|---|---|
|
|
445
|
+
| GPIO12 | 32 | 0 |
|
|
446
|
+
| GPIO13 | 33 | 1 |
|
|
447
|
+
| GPIO18 | 12 | 2 |
|
|
448
|
+
| GPIO19 | 35 | 3 |
|
|
449
|
+
|
|
450
|
+
**Pi 4 (BCM2711)** — one 2-channel chip; the two pins on a channel are
|
|
451
|
+
alternatives (use one at a time):
|
|
452
|
+
|
|
453
|
+
| GPIO | Physical pin | BCM2711 PWM channel |
|
|
454
|
+
|---|---|---|
|
|
455
|
+
| GPIO12 | 32 | 0 (PWM0) |
|
|
456
|
+
| GPIO18 | 12 | 0 (PWM0) |
|
|
457
|
+
| GPIO13 | 33 | 1 (PWM1) |
|
|
458
|
+
| GPIO19 | 35 | 1 (PWM1) |
|
|
459
|
+
|
|
460
|
+
### Manual chip/channel specification
|
|
461
|
+
|
|
462
|
+
The `gpio:` and `chip: :auto` paths detect the board from the device-tree model
|
|
463
|
+
(the resolved family is available as `pwm.board`, e.g. `:pi5` on a Pi 5) and pick
|
|
464
|
+
the header PWM chip and channel accordingly. If auto-detection fails, specify the
|
|
465
|
+
chip number explicitly:
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
pwm = Rgpio::HardwarePWM.new(chip: 2, channel: 0)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
You can also force the board family (e.g. when the model string is unusual):
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
pwm = Rgpio::HardwarePWM.new(gpio: 18, board: :pi5)
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
List available chips:
|
|
478
|
+
|
|
479
|
+
```ruby
|
|
480
|
+
Rgpio::HardwarePWM.available_chips
|
|
481
|
+
# => [{chip: 0, npwm: 2, path: "/sys/class/pwm/pwmchip0"},
|
|
482
|
+
# {chip: 2, npwm: 4, path: "/sys/class/pwm/pwmchip2"}]
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## I2C Usage
|
|
488
|
+
|
|
489
|
+
`Rgpio::I2C` talks to a device on a Linux i2c-dev bus (`/dev/i2c-N`). It is
|
|
490
|
+
plain `ioctl` work on a character device, so it needs no libgpiod — it works
|
|
491
|
+
even where `Rgpio.available?` is `false`.
|
|
492
|
+
|
|
493
|
+
### Step 1 — Enable the header bus
|
|
494
|
+
|
|
495
|
+
The 40-pin header bus (GPIO2 = SDA, GPIO3 = SCL) is bus 1, and is off by
|
|
496
|
+
default:
|
|
497
|
+
|
|
498
|
+
```sh
|
|
499
|
+
sudo raspi-config nonint do_i2c 0 # or add dtparam=i2c_arm=on to /boot/firmware/config.txt
|
|
500
|
+
sudo reboot
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Step 2 — Verify
|
|
504
|
+
|
|
505
|
+
```sh
|
|
506
|
+
ls /dev/i2c-1
|
|
507
|
+
i2cdetect -y 1 # lists the addresses that answer (i2c-tools package)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
Rgpio::I2C.buses # => [1, 13, 14]
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Step 3 — Talk to a device
|
|
515
|
+
|
|
516
|
+
```ruby
|
|
517
|
+
require "rgpio"
|
|
518
|
+
|
|
519
|
+
# Block form closes the bus device on exit.
|
|
520
|
+
Rgpio::I2C.open(address: 0x48) do |i2c|
|
|
521
|
+
# Write, then read back without releasing the bus (repeated START) — this is
|
|
522
|
+
# what a device with a register pointer expects.
|
|
523
|
+
msb, lsb = i2c.read_register(0x00, 2)
|
|
524
|
+
|
|
525
|
+
# Or drive the two halves separately.
|
|
526
|
+
i2c.write(0x03, 0x80) # write 0x80 to register 0x03
|
|
527
|
+
bytes = i2c.read(2) # => [Integer, Integer]
|
|
528
|
+
end
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Reads return byte arrays. Writes take integers, strings, or a mix, so a control
|
|
532
|
+
byte and a payload can go out in one transaction:
|
|
533
|
+
|
|
534
|
+
```ruby
|
|
535
|
+
i2c.write(0x40, "Hello") # => 6
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Failures surface as the kernel's own `Errno` exceptions: `Errno::EREMOTEIO`
|
|
539
|
+
when nothing acknowledges the address, `Errno::EBUSY` when a kernel driver
|
|
540
|
+
already holds it (pass `force: true` to claim it anyway), `Errno::EACCES` when
|
|
541
|
+
the user is not in the `i2c` group.
|
|
542
|
+
|
|
543
|
+
### Temperature sensor — `Rgpio::ADT7410`
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
sensor = Rgpio::ADT7410.new # address 0x48 on bus 1
|
|
547
|
+
puts sensor.temperature # => 27.25 (degrees Celsius)
|
|
548
|
+
sensor.close
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
The address is set by the A1/A0 pins: 0x48 with both low (the default on the
|
|
552
|
+
breakout boards) through 0x4b. The sensor powers up in 13-bit mode, resolving
|
|
553
|
+
0.0625 degC; `resolution: 16` resolves 0.0078 degC.
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
sensor = Rgpio::ADT7410.new(address: 0x49, resolution: 16)
|
|
557
|
+
sensor.detected? # => true when the ID register reports Analog Devices
|
|
558
|
+
sensor.resolution = 13 # switch back at runtime
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
The first conversion after power-up takes 240 ms
|
|
562
|
+
(`Rgpio::ADT7410::CONVERSION_TIME`); read before it finishes and the register
|
|
563
|
+
still holds its 0 degC reset value.
|
|
564
|
+
|
|
565
|
+
### Character LCD — `Rgpio::ST7032`
|
|
566
|
+
|
|
567
|
+
For the ST7032-based modules — Akizuki AQM0802 (8x2) and AQM1602 (16x2) — which
|
|
568
|
+
answer at 0x3e.
|
|
569
|
+
|
|
570
|
+
```ruby
|
|
571
|
+
lcd = Rgpio::ST7032.new(columns: 8) # 3.3 V defaults: contrast 0x20, booster on
|
|
572
|
+
lcd.message = "Hello\nrgpio" # clear, then print; a newline is the next row
|
|
573
|
+
|
|
574
|
+
lcd.move_to(0, 1) # column, row
|
|
575
|
+
lcd.print("27.2 C".rjust(8)) # overwrite in place — clearing every update flickers
|
|
576
|
+
lcd.contrast = 0x28 # 0..63
|
|
577
|
+
lcd.close # the panel keeps whatever was written last
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Text that would run past the last column of a row is dropped rather than
|
|
581
|
+
wrapped: the controller's DDRAM addresses are not contiguous between rows, so an
|
|
582
|
+
overrun scatters characters into invisible addresses instead of continuing on the
|
|
583
|
+
next line.
|
|
584
|
+
|
|
585
|
+
A panel showing nothing is almost always contrast, which is the one setting the
|
|
586
|
+
controller cannot read back — sweep `contrast:` across 0x10..0x38. Modules run at
|
|
587
|
+
5 V want the boost converter off (`booster: false`).
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## SPI Usage
|
|
592
|
+
|
|
593
|
+
`Rgpio::SPI` talks to a device on a Linux spidev bus (`/dev/spidevB.D`). Like the
|
|
594
|
+
I2C support it is `ioctl` work on a character device, so it needs no libgpiod.
|
|
595
|
+
|
|
596
|
+
### Step 1 — Enable the header bus
|
|
597
|
+
|
|
598
|
+
SPI0 is off by default. On the header it is GPIO10 (MOSI), GPIO9 (MISO), GPIO11
|
|
599
|
+
(SCLK), GPIO8 (CE0) and GPIO7 (CE1):
|
|
600
|
+
|
|
601
|
+
```sh
|
|
602
|
+
sudo raspi-config nonint do_spi 0 # or add dtparam=spi=on to /boot/firmware/config.txt
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
```sh
|
|
606
|
+
ls /dev/spidev0.0
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
```ruby
|
|
610
|
+
Rgpio::SPI.devices # => [[0, 0], [0, 1]] as [bus, chip-select]
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Step 2 — Talk to a device
|
|
614
|
+
|
|
615
|
+
```ruby
|
|
616
|
+
Rgpio::SPI.open(bus: 0, device: 0, speed_hz: 1_000_000) do |spi|
|
|
617
|
+
# SPI is full duplex: one byte goes out for every byte that comes in, so
|
|
618
|
+
# #transfer answers with as many bytes as it was given.
|
|
619
|
+
received = spi.transfer([0x06, 0x00, 0x00])
|
|
620
|
+
|
|
621
|
+
spi.write(0x40, "Hello") # transfer, ignoring what came back
|
|
622
|
+
spi.read(4) # transfer of zeros, keeping what came back
|
|
623
|
+
end
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
`mode:` selects clock polarity and phase (0..3), and `speed_hz`, `mode` and
|
|
627
|
+
`bits_per_word` can all be changed on an open device. A single transfer can take
|
|
628
|
+
a `speed_hz:` of its own.
|
|
629
|
+
|
|
630
|
+
Failures surface as the kernel's own `Errno` exceptions — `Errno::EACCES` when
|
|
631
|
+
the user is not in the `spi` group, `Errno::ENODEV` when the bus is not enabled.
|
|
632
|
+
|
|
633
|
+
### Analogue input — `Rgpio::MCP3208`
|
|
634
|
+
|
|
635
|
+
Eight 12-bit channels over SPI, the MCP3204's four channels being the same part
|
|
636
|
+
and protocol.
|
|
637
|
+
|
|
638
|
+
```ruby
|
|
639
|
+
adc = Rgpio::MCP3208.new # bus 0, CE0, 1 MHz, VREF 3.3 V
|
|
640
|
+
adc.read(0) # => 0..4095, the raw code
|
|
641
|
+
adc.value(0) # => 0.0..1.0, a fraction of VREF
|
|
642
|
+
adc.voltage(0) # => volts
|
|
643
|
+
adc.read_all # => every channel, consecutively
|
|
644
|
+
adc.read(0, differential: true) # => the CH0/CH1 pair rather than CH0
|
|
645
|
+
adc.close
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
```ruby
|
|
649
|
+
adc = Rgpio::MCP3208.new(channels: 4, reference_voltage: 5.0, speed_hz: 500_000)
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Wiring: VDD **and VREF** to 3.3 V, AGND and DGND to ground, CLK/DOUT/DIN to
|
|
653
|
+
SCLK/MISO/MOSI, CS to CE0. A forgotten VREF is the usual reason every channel
|
|
654
|
+
reads 0 or sticks at full scale.
|
|
655
|
+
|
|
656
|
+
The clock rate is a matter of correctness, not just speed: the datasheet allows
|
|
657
|
+
1 MHz at 2.7 V and 2 MHz at 5 V, and a converter clocked past its sampling rate
|
|
658
|
+
answers with values that look plausible and are wrong. The 1 MHz default is
|
|
659
|
+
inside the envelope for the 3.3 V supply a Pi provides.
|
|
660
|
+
|
|
661
|
+
Unconnected channels float and read whatever is nearby; that is not a fault.
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Running the examples
|
|
666
|
+
|
|
667
|
+
All examples require root (or `gpio` group membership):
|
|
668
|
+
|
|
669
|
+
```sh
|
|
670
|
+
# Blink an LED on GPIO4
|
|
671
|
+
ruby examples/led.rb
|
|
672
|
+
|
|
673
|
+
# Print Pressed / Released for a switch on GPIO4
|
|
674
|
+
ruby examples/button.rb
|
|
675
|
+
|
|
676
|
+
# Drive a DC motor forward and backward through a DRV8835
|
|
677
|
+
ruby examples/motor.rb
|
|
678
|
+
|
|
679
|
+
# Fade an LED with PWM on GPIO4
|
|
680
|
+
ruby examples/pwm_led.rb
|
|
681
|
+
|
|
682
|
+
# Cycle a full-colour LED through the colour cube on GPIO17/27/22
|
|
683
|
+
ruby examples/rgb_led.rb
|
|
684
|
+
|
|
685
|
+
# Find the per-channel balance that makes that LED's white look white
|
|
686
|
+
ruby examples/rgb_balance.rb
|
|
687
|
+
|
|
688
|
+
# Sweep a servo on GPIO4, by value and by angle
|
|
689
|
+
ruby examples/servo.rb
|
|
690
|
+
|
|
691
|
+
# Report which PWM chip and channel each header GPIO resolves to
|
|
692
|
+
ruby examples/pwm_info.rb
|
|
693
|
+
|
|
694
|
+
# Print the ADT7410 temperature once a second
|
|
695
|
+
ruby examples/temperature.rb
|
|
696
|
+
|
|
697
|
+
# Write text and a counter to an ST7032 LCD
|
|
698
|
+
ruby examples/lcd.rb
|
|
699
|
+
|
|
700
|
+
# Show the temperature on the LCD — both I2C devices on one bus
|
|
701
|
+
ruby examples/lcd_thermometer.rb
|
|
702
|
+
|
|
703
|
+
# Print all eight channels of an MCP3208
|
|
704
|
+
ruby examples/adc.rb
|
|
705
|
+
|
|
706
|
+
# Dim an LED from a potentiometer through the MCP3208
|
|
707
|
+
ruby examples/adc_led.rb
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
`examples/lowlevel/` holds the same LED and button demos written directly
|
|
711
|
+
against `Chip` / `LineRequest`, for when you need control the device classes do
|
|
712
|
+
not expose:
|
|
713
|
+
|
|
714
|
+
```sh
|
|
715
|
+
sudo ruby examples/lowlevel/blink.rb
|
|
716
|
+
sudo ruby examples/lowlevel/button.rb
|
|
717
|
+
|
|
718
|
+
# The servo driven straight from the PWM peripheral (dtoverlay required)
|
|
719
|
+
sudo ruby examples/lowlevel/servo.rb
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
`examples/pwm_jitter.rb` measures what a PWM channel really puts on the line,
|
|
723
|
+
using the kernel's edge timestamps and a jumper between two header pins:
|
|
724
|
+
|
|
725
|
+
```sh
|
|
726
|
+
ruby examples/pwm_jitter.rb --hz 50 --duty 0.075 --seconds 5
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## API reference
|
|
732
|
+
|
|
733
|
+
### `Rgpio`
|
|
734
|
+
|
|
735
|
+
| Method | Description |
|
|
736
|
+
|---|---|
|
|
737
|
+
| `.available?` | `true` if `libgpiod.so` was found |
|
|
738
|
+
| `.version` | libgpiod version string (e.g. `"2.1.3"`) |
|
|
739
|
+
|
|
740
|
+
### `Rgpio::Chip`
|
|
741
|
+
|
|
742
|
+
| Method | Description |
|
|
743
|
+
|---|---|
|
|
744
|
+
| `.new(path = nil)` | Open chip; auto-detects header controller when `path` is nil |
|
|
745
|
+
| `.open(path = nil) { \|chip\| }` | Block form; closes on exit |
|
|
746
|
+
| `.list` | Array of `{path:, name:, label:, num_lines:}` for every gpiochip |
|
|
747
|
+
| `.detect_path` | Device path of the header GPIO controller (Pi 5 / 4 / Zero) |
|
|
748
|
+
| `#path` | Device path this chip was opened with |
|
|
749
|
+
| `#name` | Kernel name (`"gpiochip0"`) |
|
|
750
|
+
| `#label` | Controller label (`"pinctrl-rp1"`) |
|
|
751
|
+
| `#num_lines` | Number of GPIO lines |
|
|
752
|
+
| `#request_lines(...)` | Returns a `LineRequest` |
|
|
753
|
+
| `#close` | Close the chip |
|
|
754
|
+
|
|
755
|
+
### `Rgpio::LineRequest`
|
|
756
|
+
|
|
757
|
+
| Method | Description |
|
|
758
|
+
|---|---|
|
|
759
|
+
| `#get_value(offset)` | `:active` or `:inactive` |
|
|
760
|
+
| `#set_value(offset, value)` | Set output level |
|
|
761
|
+
| `#get_values(offsets = all)` | Read several lines atomically → `{offset => :active/:inactive}` |
|
|
762
|
+
| `#set_values(hash)` | Write several lines atomically from `{offset => value}` |
|
|
763
|
+
| `#wait_edge_events(timeout:)` | `true` if event ready |
|
|
764
|
+
| `#read_edge_events(timeout:, capacity:)` | Array of event hashes |
|
|
765
|
+
| `#release` | Release kernel request |
|
|
766
|
+
|
|
767
|
+
### `Rgpio::OutputDevice` (and `Rgpio::LED`)
|
|
768
|
+
|
|
769
|
+
| Method | Description |
|
|
770
|
+
|---|---|
|
|
771
|
+
| `.new(gpio, active_low:, initial_value:, chip:, consumer:)` | Claim a line as an output |
|
|
772
|
+
| `#on` / `#off` / `#toggle` | Drive the line to its active / inactive level |
|
|
773
|
+
| `#value` / `#value=` | Current level as `true` / `false` (`#on?` is an alias of `#value`) |
|
|
774
|
+
| `#gpio` | Line offset this device drives |
|
|
775
|
+
| `#close` / `#closed?` | Release the line (and the chip, if it opened one) |
|
|
776
|
+
|
|
777
|
+
### `Rgpio::InputDevice` (and `Rgpio::Button`)
|
|
778
|
+
|
|
779
|
+
| Method | Description |
|
|
780
|
+
|---|---|
|
|
781
|
+
| `.new(gpio, pull_up:, active_low:, debounce_us:, chip:, consumer:)` | Claim a line as an input; `pull_up:` takes `true` / `false` / `nil` (no bias) |
|
|
782
|
+
| `#value` | `true` when the line is at its active level (`#active?`, and `#pressed?` on `Button`) |
|
|
783
|
+
| `#when_pressed { }` / `#when_released { }` | `Button` edge callbacks, run on a watcher thread |
|
|
784
|
+
| `#gpio` | Line offset this device reads |
|
|
785
|
+
| `#close` / `#closed?` | Stop the watcher and release the line |
|
|
786
|
+
|
|
787
|
+
### `Rgpio::Motor`
|
|
788
|
+
|
|
789
|
+
| Method | Description |
|
|
790
|
+
|---|---|
|
|
791
|
+
| `.new(forward:, backward:, chip:, consumer:)` | Claim both lines of a two-input driver |
|
|
792
|
+
| `#forward` / `#backward` | Run at full speed in one direction |
|
|
793
|
+
| `#stop` | Drop both lines |
|
|
794
|
+
| `#close` | Stop, then release both lines |
|
|
795
|
+
|
|
796
|
+
### `Rgpio::I2C`
|
|
797
|
+
|
|
798
|
+
| Method | Description |
|
|
799
|
+
|---|---|
|
|
800
|
+
| `.new(address:, bus: 1, force: false)` | Open `/dev/i2c-N` and claim a 7-bit address |
|
|
801
|
+
| `.open(address:, bus:) { \|i2c\| }` | Block form; closes on exit |
|
|
802
|
+
| `.buses` | Bus numbers with a `/dev/i2c-N` node |
|
|
803
|
+
| `#write(*bytes)` | Write integers / strings in one transaction |
|
|
804
|
+
| `#read(count)` | Read `count` bytes → `Array<Integer>` |
|
|
805
|
+
| `#write_read(bytes, count)` | Write then read with a repeated START |
|
|
806
|
+
| `#read_register(register, count = 1)` | `write_read([register], count)` |
|
|
807
|
+
| `#write_register(register, *bytes)` | Write a register in one transaction |
|
|
808
|
+
| `#address` / `#bus` / `#path` | What this device was opened on |
|
|
809
|
+
| `#close` / `#closed?` | Close the bus device |
|
|
810
|
+
|
|
811
|
+
### `Rgpio::SPI`
|
|
812
|
+
|
|
813
|
+
| Method | Description |
|
|
814
|
+
|---|---|
|
|
815
|
+
| `.new(bus: 0, device: 0, speed_hz:, mode:, bits_per_word:)` | Open `/dev/spidevB.D` |
|
|
816
|
+
| `.open(...) { \|spi\| }` | Block form; closes on exit |
|
|
817
|
+
| `.devices` | `[bus, chip-select]` of every spidev node |
|
|
818
|
+
| `#transfer(bytes, speed_hz:, delay_us:)` | Full-duplex transfer → `Array<Integer>` |
|
|
819
|
+
| `#write(*bytes)` | Transfer, ignoring what came back |
|
|
820
|
+
| `#read(count)` | Transfer of zeros, keeping what came back |
|
|
821
|
+
| `#speed_hz` / `#mode` / `#bits_per_word` (and `=`) | Bus settings |
|
|
822
|
+
| `#bus` / `#device` / `#path` | What this device was opened on |
|
|
823
|
+
| `#close` / `#closed?` | Close the bus device |
|
|
824
|
+
|
|
825
|
+
### `Rgpio::MCP3208`
|
|
826
|
+
|
|
827
|
+
| Method | Description |
|
|
828
|
+
|---|---|
|
|
829
|
+
| `.new(channels:, reference_voltage:, bus:, device:, speed_hz:, spi:)` | Open the converter; `spi:` shares a bus device |
|
|
830
|
+
| `#read(channel, differential: false)` | Raw code, 0..4095 |
|
|
831
|
+
| `#value(channel, ...)` | Fraction of VREF, 0.0..1.0 |
|
|
832
|
+
| `#voltage(channel, ...)` | Volts |
|
|
833
|
+
| `#read_all` | Every channel, sampled consecutively |
|
|
834
|
+
| `#channels` / `#reference_voltage` / `#spi` | What it was configured with |
|
|
835
|
+
| `#close` / `#closed?` | Close the bus device, if this converter opened it |
|
|
836
|
+
|
|
837
|
+
### `Rgpio::ADT7410`
|
|
838
|
+
|
|
839
|
+
| Method | Description |
|
|
840
|
+
|---|---|
|
|
841
|
+
| `.new(address: 0x48, bus: 1, resolution: 13, i2c: nil)` | Open the sensor; `i2c:` shares an existing bus device |
|
|
842
|
+
| `.convert(msb, lsb, resolution = 13)` | Raw register pair → degrees Celsius |
|
|
843
|
+
| `#temperature` | Temperature in degrees Celsius (`#value` is an alias) |
|
|
844
|
+
| `#raw_temperature` | The two temperature bytes, MSB first |
|
|
845
|
+
| `#resolution` / `#resolution=` | 13 or 16 bits |
|
|
846
|
+
| `#id` / `#detected?` | ID register, and whether it reports Analog Devices |
|
|
847
|
+
| `#i2c` | The bus device readings go through |
|
|
848
|
+
| `#close` / `#closed?` | Close the bus device, if this sensor opened it |
|
|
849
|
+
|
|
850
|
+
### `Rgpio::ST7032`
|
|
851
|
+
|
|
852
|
+
| Method | Description |
|
|
853
|
+
|---|---|
|
|
854
|
+
| `.new(address: 0x3e, bus: 1, columns: 8, rows: 2, contrast: 0x20, booster: true, i2c: nil)` | Open and initialise the display |
|
|
855
|
+
| `.open(...) { \|lcd\| }` | Block form; closes on exit |
|
|
856
|
+
| `#message=(text)` | Clear, then print |
|
|
857
|
+
| `#print(text)` | Write at the cursor; a newline moves to the next row |
|
|
858
|
+
| `#move_to(col, row = 0)` | Move the cursor (`#set_cursor` is an alias) |
|
|
859
|
+
| `#clear` / `#home` | Blank the display / return the cursor |
|
|
860
|
+
| `#contrast` / `#contrast=` | 0..63 |
|
|
861
|
+
| `#display_on` / `#display_off` | Blank the panel without losing its contents |
|
|
862
|
+
| `#command(byte)` / `#write_data(text)` | Raw instruction / display-data transfer |
|
|
863
|
+
| `#reset` | Re-run the power-on initialisation sequence |
|
|
864
|
+
| `#columns` / `#rows` / `#i2c` | Geometry, and the bus device writes go through |
|
|
865
|
+
| `#close` / `#closed?` | Close the bus device, if this display opened it |
|
|
866
|
+
|
|
867
|
+
### `Rgpio::SoftwarePWM`
|
|
868
|
+
|
|
869
|
+
| Method | Description |
|
|
870
|
+
|---|---|
|
|
871
|
+
| `.new(gpio, frequency:, duty_cycle:, spin_us:, chip:, consumer:)` | Claim a line and prepare a channel |
|
|
872
|
+
| `.open(gpio, ...) { \|pwm\| }` | Block form; closes on exit |
|
|
873
|
+
| `#frequency` / `#frequency=` | Hz, 0.1..10000 |
|
|
874
|
+
| `#duty_cycle` / `#duty_cycle=` | 0.0..1.0 (`#duty_ratio` is an alias) |
|
|
875
|
+
| `#pulse_width_us` / `#pulse_width_us=` | High time in microseconds |
|
|
876
|
+
| `#enable` / `#disable` / `#enabled?` | Start and stop generating |
|
|
877
|
+
| `#spin_us` | Microseconds spent spinning before each edge |
|
|
878
|
+
| `#close` / `#closed?` | Stop, release the line, close an owned chip |
|
|
879
|
+
|
|
880
|
+
### `Rgpio::PWMOutputDevice` (and `Rgpio::PWMLED`)
|
|
881
|
+
|
|
882
|
+
| Method | Description |
|
|
883
|
+
|---|---|
|
|
884
|
+
| `.new(gpio, frequency:, initial_value:, active_low:, pwm:, chip:, consumer:)` | Drive a line from a PWM channel |
|
|
885
|
+
| `#value` / `#value=` | Level, 0.0..1.0 (`#brightness` on `PWMLED`) |
|
|
886
|
+
| `#on` / `#off` / `#toggle` | 1.0 / 0.0 / the complement of the current level |
|
|
887
|
+
| `#on?` | True when not fully off (`#active?` is an alias) |
|
|
888
|
+
| `#frequency` / `#frequency=` | Delegated to the channel |
|
|
889
|
+
| `#pwm` | The channel behind this device |
|
|
890
|
+
| `#close` / `#closed?` | Stop the channel, releasing it if the device opened it |
|
|
891
|
+
|
|
892
|
+
### `Rgpio::RGBLED`
|
|
893
|
+
|
|
894
|
+
| Method | Description |
|
|
895
|
+
|---|---|
|
|
896
|
+
| `.new(red:, green:, blue:, frequency:, active_low:, balance:, pwm:, chip:, consumer:)` | Three channels as one device |
|
|
897
|
+
| `#color` / `#color=` | An r,g,b triple, or a name from `COLORS` |
|
|
898
|
+
| `#red` / `#green` / `#blue` (and `=`) | One channel at a time |
|
|
899
|
+
| `#balance` / `#balance=` | Per-channel scale under every level asked for |
|
|
900
|
+
| `#on` / `#off` / `#toggle` | White / dark / the complement of each channel |
|
|
901
|
+
| `#on?` | True when any channel is lit |
|
|
902
|
+
| `#channels` | The three `PWMLED`s, by colour |
|
|
903
|
+
| `#close` / `#closed?` | Stop all three, close an owned chip |
|
|
904
|
+
|
|
905
|
+
### `Rgpio::Servo`
|
|
906
|
+
|
|
907
|
+
| Method | Description |
|
|
908
|
+
|---|---|
|
|
909
|
+
| `.new(gpio, min_pulse_us:, max_pulse_us:, frequency:, min_angle:, max_angle:, initial_value:, pwm:, chip:, consumer:)` | Claim a line and centre the servo |
|
|
910
|
+
| `#value` / `#value=` | -1.0..1.0, or nil to detach |
|
|
911
|
+
| `#angle` / `#angle=` | Degrees between `min_angle` and `max_angle` |
|
|
912
|
+
| `#min` / `#mid` / `#max` | The ends of the travel and the centre |
|
|
913
|
+
| `#pulse_width_us` / `#pulse_width_us=` | The width being sent; assigning one calibrates by hand |
|
|
914
|
+
| `#detach` / `#attached?` | Stop the pulses so the horn goes limp / report whether they are running |
|
|
915
|
+
| `#close` / `#closed?` | Stop the channel, releasing it if the servo opened it |
|
|
916
|
+
|
|
917
|
+
### `Rgpio::HardwarePWM`
|
|
918
|
+
|
|
919
|
+
| Method | Description |
|
|
920
|
+
|---|---|
|
|
921
|
+
| `.new(gpio:, board:)` | Auto-detect chip/channel for a GPIO pin (board auto-detected) |
|
|
922
|
+
| `.new(chip:, channel:)` | Explicit chip/channel |
|
|
923
|
+
| `.open(...) { \|pwm\| }` | Block form; closes on exit |
|
|
924
|
+
| `.available_chips` | List sysfs PWM chips |
|
|
925
|
+
| `.detect_board` | Board family from the device-tree model (`:pi5` / `:pi4` / `:unknown`) |
|
|
926
|
+
| `#board` | Resolved board family |
|
|
927
|
+
| `#frequency=` / `#frequency` | Hz |
|
|
928
|
+
| `#duty_cycle=` / `#duty_ratio` | 0.0–1.0 ratio |
|
|
929
|
+
| `#pulse_width_us=` / `#pulse_width_us` | Microseconds |
|
|
930
|
+
| `#enable` / `#disable` | Start/stop PWM output |
|
|
931
|
+
| `#close` | Disable and unexport |
|
|
932
|
+
|
|
933
|
+
---
|
|
934
|
+
|
|
935
|
+
## Architecture
|
|
936
|
+
|
|
937
|
+
```
|
|
938
|
+
┌─────────────────────────────────────────────────────────┐
|
|
939
|
+
│ LED / Button / Motor / PWMLED / RGBLED / Servo │ device classes (gpiozero-style)
|
|
940
|
+
│ ADT7410 / ST7032 / MCP3208 │ (one object per part)
|
|
941
|
+
├─────────────────────────────────────────────────────────┤
|
|
942
|
+
│ Rgpio::Chip / LineRequest │ OOP wrappers (this gem)
|
|
943
|
+
├──────────────────┬──────────────────┬───────────────────┤
|
|
944
|
+
│ Native (fiddle) │ HardwarePWM │ I2C / SPI │ libgpiod.so / sysfs PWM / i2c-dev
|
|
945
|
+
│ SoftwarePWM │ │ │ (PWM timed in Ruby on any line)
|
|
946
|
+
└──────────────────┴──────────────────┴───────────────────┘
|
|
947
|
+
libgpiod v2 ABI Linux PWM sysfs /dev/i2c-N, /dev/spidev ioctl
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
- **Layer 1 (`Native`)** — raw `fiddle` declarations of the libgpiod C functions
|
|
951
|
+
- **Layer 2 (`Chip`, `LineRequest`, `HardwarePWM`, `SoftwarePWM`, `I2C`, `SPI`)** — Ruby-idiomatic wrappers
|
|
952
|
+
- **Layer 3 (`LED`, `Button`, `Motor`, `PWMLED`, `RGBLED`, `Servo`, `ADT7410`,
|
|
953
|
+
`ST7032`, `MCP3208`)** — one object per piece of hardware
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
## Project status
|
|
958
|
+
|
|
959
|
+
`0.1.0` is the first release. It is `0.x` in earnest: everything documented here
|
|
960
|
+
has been exercised on real hardware, but the API may still change before `1.0`.
|
|
961
|
+
|
|
962
|
+
- **Released changes:** [CHANGELOG.md](CHANGELOG.md)
|
|
963
|
+
- **Roadmap, planned APIs, and multi-board validation status:** [PLAN.md](PLAN.md)
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
## License
|
|
968
|
+
|
|
969
|
+
[MIT](LICENSE)
|