Skip to content

Commit 01fed47

Browse files
neildavisdeadprogram
authored andcommitted
Add 'irremote' package as a basic infra-red driver
Currently contains an IR Receiver device only. An IR Sender device will be added shortly Supports receiving only NEC (extended address) protocol including repeat codes so far
1 parent edfdd4e commit 01fed47

File tree

4 files changed

+287
-1
lines changed

4 files changed

+287
-1
lines changed

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,15 @@ endif
231231
@md5sum ./build/test.hex
232232
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go
233233
@md5sum ./build/test.uf2
234+
tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go
235+
@md5sum ./build/test.hex
234236

235237
DRIVERS = $(wildcard */)
236238
NOTESTS = build examples flash semihosting pcd8544 shiftregister st7789 microphone mcp3008 gps microbitmatrix \
237239
hcsr04 ssd1331 ws2812 thermistor apa102 easystepper ssd1351 ili9341 wifinina shifter hub75 \
238240
hd44780 buzzer ssd1306 espat l9110x st7735 bmi160 l293x keypad4x4 max72xx p1am tone tm1637 \
239241
pcf8563 mcp2515 servo sdcard rtl8720dn image cmd i2csoft hts221 lps22hb apds9960 axp192 xpt2046 \
240-
ft6336 sx126x ssd1289
242+
ft6336 sx126x ssd1289 irremote
241243
TESTS = $(filter-out $(addsuffix /%,$(NOTESTS)),$(DRIVERS))
242244

243245
unit-test:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ The following 78 devices are supported.
8787
| [software I2C driver](https://www.ti.com/lit/an/slva704/slva704.pdf) | GPIO |
8888
| [ILI9341 TFT color display](https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf) | SPI |
8989
| [INA260 Volt/Amp/Power meter](https://www.ti.com/lit/ds/symlink/ina260.pdf) | I2C |
90+
| [Infrared remote control](https://en.wikipedia.org/wiki/Consumer_IR) | GPIO |
9091
| [IS31FL3731 matrix LED driver](https://www.lumissil.com/assets/pdf/core/IS31FL3731_DS.pdf) | I2C |
9192
| [4x4 Membrane Keypad](https://cdn.sparkfun.com/assets/f/f/a/5/0/DS-16038.pdf) | GPIO |
9293
| [L293x motor driver](https://www.ti.com/lit/ds/symlink/l293d.pdf) | GPIO/PWM |

examples/irremote/main.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package main
2+
3+
import (
4+
"machine"
5+
"time"
6+
7+
"tinygo.org/x/drivers/irremote"
8+
)
9+
10+
var irCmdButtons = map[uint16]string{
11+
0xA2: "POWER",
12+
0xE2: "FUNC/STOP",
13+
0x62: "VOL+",
14+
0x22: "FAST BACK",
15+
0x02: "PAUSE",
16+
0xC2: "FAST FORWARD",
17+
0xE0: "DOWN",
18+
0xA8: "VOL-",
19+
0x90: "UP",
20+
0x98: "EQ",
21+
0xB0: "ST/REPT",
22+
0x68: "0",
23+
0x30: "1",
24+
0x18: "2",
25+
0x7A: "3",
26+
0x10: "4",
27+
0x38: "5",
28+
0x5A: "6",
29+
0x42: "7",
30+
0x4A: "8",
31+
0x52: "9",
32+
}
33+
34+
var (
35+
pinIRIn = machine.GP26
36+
ir irremote.ReceiverDevice
37+
)
38+
39+
func setupPins() {
40+
ir = irremote.NewReceiver(pinIRIn)
41+
ir.Configure()
42+
}
43+
44+
func irCallback(data irremote.Data) {
45+
msg := "Command: " + irCmdButtons[data.Command]
46+
if data.Flags&irremote.DataFlagIsRepeat != 0 {
47+
msg = msg + " (REPEAT)"
48+
}
49+
println(msg)
50+
}
51+
52+
func main() {
53+
setupPins()
54+
ir.SetCommandHandler(irCallback)
55+
for {
56+
time.Sleep(time.Millisecond * 10)
57+
}
58+
}

irremote/receiver.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package irremote // import "tinygo.org/x/drivers/irremote"
2+
3+
import (
4+
"machine"
5+
"time"
6+
)
7+
8+
// NEC protocol references
9+
// https://www.sbprojects.net/knowledge/ir/nec.php
10+
// https://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol
11+
// https://simple-circuit.com/arduino-nec-remote-control-decoder/
12+
13+
// Data encapsulates the data received by the ReceiverDevice.
14+
type Data struct {
15+
// Code is the raw IR data received.
16+
Code uint32
17+
// Address is the decoded address from the IR data received.
18+
Address uint16
19+
// Command is the decoded command from the IR data recieved
20+
Command uint16
21+
// Flags provides additional information about the IR data received. See DataFlags
22+
Flags DataFlags
23+
}
24+
25+
// DataFlags provides bitwise flags representing various information about recieved IR data.
26+
type DataFlags uint16
27+
28+
// Valid values for DataFlags
29+
const (
30+
// DataFlagIsRepeat set indicates that the IR data is a repeat commmand
31+
DataFlagIsRepeat DataFlags = 1 << iota
32+
)
33+
34+
// CommandHandler defines the callback function used to provide IR data received by the ReceiverDevice.
35+
type CommandHandler func(data Data)
36+
37+
// nec_ir_state represents the various internal states used to decode the NEC IR protocol commands
38+
type nec_ir_state uint8
39+
40+
// Valid values for nec_ir_state
41+
const (
42+
lead_pulse_start nec_ir_state = iota // Start receiving IR data, beginning of 9ms lead pulse
43+
lead_space_start // End of 9ms lead pulse, start of 4.5ms space
44+
lead_space_end // End of 4.5ms space, start of 562µs pulse
45+
bit_read_start // End of 562µs pulse, start of 562µs or 1687µs space
46+
bit_read_end // End of 562µs or 1687µs space
47+
trail_pulse_end // End of 562µs trailing pulse
48+
)
49+
50+
// ReceiverDevice is the device for receiving IR commands
51+
type ReceiverDevice struct {
52+
pin machine.Pin // IR input pin.
53+
ch CommandHandler // client callback function
54+
necState nec_ir_state // internal state machine
55+
data Data // decoded data for client
56+
lastTime time.Time // used to track states
57+
bitIndex int // tracks which bit (0-31) of necCode is being read
58+
}
59+
60+
// NewReceiver returns a new IR receiver device
61+
func NewReceiver(pin machine.Pin) ReceiverDevice {
62+
return ReceiverDevice{pin: pin}
63+
}
64+
65+
// Configure configures the input pin for the IR receiver device
66+
func (ir *ReceiverDevice) Configure() {
67+
// The IR receiver sends logic HIGH when NOT receiving IR, and logic LOW when receiving IR
68+
ir.pin.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
69+
}
70+
71+
// SetCommandHandler is used to start or stop receiving IR commands via a callback function (pass nil to stop)
72+
func (ir *ReceiverDevice) SetCommandHandler(ch CommandHandler) {
73+
ir.ch = ch
74+
ir.resetStateMachine()
75+
if ch != nil {
76+
// Start monitoring IR output pin for changes
77+
ir.pin.SetInterrupt(machine.PinFalling|machine.PinRising, ir.pinChange)
78+
} else {
79+
// Stop monitoring IR output pin for changes
80+
ir.pin.SetInterrupt(0, nil)
81+
}
82+
}
83+
84+
// Internal helper function to reset state machine on protocol failure
85+
func (ir *ReceiverDevice) resetStateMachine() {
86+
ir.data = Data{}
87+
ir.bitIndex = 0
88+
ir.necState = lead_pulse_start
89+
}
90+
91+
// Internal pin rising/falling edge interrupt handler
92+
func (ir *ReceiverDevice) pinChange(pin machine.Pin) {
93+
/* Currently TinyGo is sending machine.NoPin (0xff) for all pins, at least on RP2040
94+
if pin != ir.pin {
95+
return // This is not the pin you're looking for
96+
}
97+
*/
98+
now := time.Now()
99+
duration := now.Sub(ir.lastTime)
100+
ir.lastTime = now
101+
switch ir.necState {
102+
case lead_pulse_start:
103+
if !ir.pin.Get() {
104+
// IR is 'on' (pin is pulled high and sent low when IR is received)
105+
ir.necState = lead_space_start // move to next state
106+
}
107+
case lead_space_start:
108+
if duration > time.Microsecond*9500 || duration < time.Microsecond*8500 {
109+
// Invalid interval for 9ms lead pulse. Reset
110+
ir.resetStateMachine()
111+
} else {
112+
// 9ms lead pulse detected, move to next state
113+
ir.necState = lead_space_end
114+
}
115+
case lead_space_end:
116+
if duration > time.Microsecond*5000 || duration < time.Microsecond*1750 {
117+
// Invalid interval for 4.5ms lead space OR 2.25ms repeat space. Reset
118+
ir.resetStateMachine()
119+
} else {
120+
// 4.5ms lead space OR 2.25ms repeat space detected
121+
if duration > time.Microsecond*3000 {
122+
// 4.5ms lead space detected, new code incoming, move to next state
123+
ir.resetStateMachine()
124+
ir.necState = bit_read_start
125+
} else {
126+
// 2.25ms repeat space detected.
127+
if ir.data.Code != 0 {
128+
// Valid repeat code. Invoke client callback with repeat flag set
129+
ir.data.Flags |= DataFlagIsRepeat
130+
if ir.ch != nil {
131+
ir.ch(ir.data)
132+
}
133+
ir.necState = lead_pulse_start
134+
} else {
135+
// ir.data is not in a valid state for a repeat. Reset
136+
ir.resetStateMachine()
137+
}
138+
}
139+
}
140+
case bit_read_start:
141+
if duration > time.Microsecond*700 || duration < time.Microsecond*400 {
142+
// Invalid interval for 562.5µs pulse. Reset
143+
ir.resetStateMachine()
144+
} else {
145+
// 562.5µs pulse detected, move to next state
146+
ir.necState = bit_read_end
147+
}
148+
case bit_read_end:
149+
if duration > time.Microsecond*1800 || duration < time.Microsecond*400 {
150+
// Invalid interval for 562.5µs space OR 1687.5µs space. Reset
151+
ir.resetStateMachine()
152+
} else {
153+
// 562.5µs OR 1687.5µs space detected
154+
mask := uint32((1 << (31 - ir.bitIndex)))
155+
if duration > time.Microsecond*1000 {
156+
// 1687.5µs space detected (logic 1) - Set bit
157+
ir.data.Code |= mask
158+
} else {
159+
// 562.5µs space detected (logic 0) - Clear bit
160+
ir.data.Code &^= mask
161+
}
162+
163+
ir.bitIndex++
164+
if ir.bitIndex > 31 {
165+
// We've read all bits for this code, move to next state
166+
ir.necState = trail_pulse_end
167+
} else {
168+
// Read next bit
169+
ir.necState = bit_read_start
170+
}
171+
}
172+
case trail_pulse_end:
173+
if duration > time.Microsecond*700 || duration < time.Microsecond*400 {
174+
// Invalid interval for trailing 562.5µs pulse. Reset
175+
ir.resetStateMachine()
176+
} else {
177+
// 562.5µs trailing pulse detected. Decode & validate data
178+
err := ir.decode()
179+
if err == irDecodeErrorNone {
180+
// Valid data, invoke client callback
181+
if ir.ch != nil {
182+
ir.ch(ir.data)
183+
}
184+
// around we go again. Note: we don't resetStateMachine() since repeat codes are now possible
185+
ir.necState = lead_pulse_start
186+
} else {
187+
ir.resetStateMachine()
188+
}
189+
}
190+
}
191+
}
192+
193+
// Error type for NEC format decoding
194+
type irDecodeError int
195+
196+
// Valid values for irDecodeError
197+
const (
198+
irDecodeErrorNone irDecodeError = iota // no error occurred
199+
irDecodeErrorInverseCheckFail // validation of inverse cmd does not match cmd
200+
)
201+
202+
func (ir *ReceiverDevice) decode() irDecodeError {
203+
// Decode cmd and inverse cmd and perform validation check
204+
cmd := uint8((ir.data.Code & 0xff00) >> 8)
205+
invCmd := uint8(ir.data.Code & 0xff)
206+
if cmd != ^invCmd {
207+
// Validation failure. cmd and inverse cmd do not match
208+
return irDecodeErrorInverseCheckFail
209+
}
210+
// cmd validation pass, decode address
211+
ir.data.Command = uint16(cmd)
212+
addrLow := uint8((ir.data.Code & 0xff000000) >> 24)
213+
addrHigh := uint8((ir.data.Code & 0x00ff0000) >> 16)
214+
if addrHigh == ^addrLow {
215+
// addrHigh is inverse of addrLow. This is not a valid 16-bit address in extended NEC coding
216+
// since it is indistinguishable from 8-bit address with inverse validation. Use the 8-bit address
217+
ir.data.Address = uint16(addrLow)
218+
} else {
219+
// 16-bit extended NEC address
220+
ir.data.Address = (uint16(addrHigh) << 8) | uint16(addrLow)
221+
}
222+
// Clear repeat flag
223+
ir.data.Flags &^= DataFlagIsRepeat
224+
return irDecodeErrorNone
225+
}

0 commit comments

Comments
 (0)