diff --git a/Makefile b/Makefile index 262f5e7..59cbce9 100644 --- a/Makefile +++ b/Makefile @@ -231,13 +231,15 @@ endif @md5sum ./build/test.hex tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go @md5sum ./build/test.uf2 + tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go + @md5sum ./build/test.hex DRIVERS = $(wildcard */) NOTESTS = build examples flash semihosting pcd8544 shiftregister st7789 microphone mcp3008 gps microbitmatrix \ hcsr04 ssd1331 ws2812 thermistor apa102 easystepper ssd1351 ili9341 wifinina shifter hub75 \ hd44780 buzzer ssd1306 espat l9110x st7735 bmi160 l293x keypad4x4 max72xx p1am tone tm1637 \ pcf8563 mcp2515 servo sdcard rtl8720dn image cmd i2csoft hts221 lps22hb apds9960 axp192 xpt2046 \ - ft6336 sx126x ssd1289 + ft6336 sx126x ssd1289 irremote TESTS = $(filter-out $(addsuffix /%,$(NOTESTS)),$(DRIVERS)) unit-test: diff --git a/README.md b/README.md index 8fc3696..9b4641d 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ The following 78 devices are supported. | [software I2C driver](https://www.ti.com/lit/an/slva704/slva704.pdf) | GPIO | | [ILI9341 TFT color display](https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf) | SPI | | [INA260 Volt/Amp/Power meter](https://www.ti.com/lit/ds/symlink/ina260.pdf) | I2C | +| [Infrared remote control](https://en.wikipedia.org/wiki/Consumer_IR) | GPIO | | [IS31FL3731 matrix LED driver](https://www.lumissil.com/assets/pdf/core/IS31FL3731_DS.pdf) | I2C | | [4x4 Membrane Keypad](https://cdn.sparkfun.com/assets/f/f/a/5/0/DS-16038.pdf) | GPIO | | [L293x motor driver](https://www.ti.com/lit/ds/symlink/l293d.pdf) | GPIO/PWM | diff --git a/examples/irremote/main.go b/examples/irremote/main.go new file mode 100644 index 0000000..38940e4 --- /dev/null +++ b/examples/irremote/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/irremote" +) + +var irCmdButtons = map[uint16]string{ + 0xA2: "POWER", + 0xE2: "FUNC/STOP", + 0x62: "VOL+", + 0x22: "FAST BACK", + 0x02: "PAUSE", + 0xC2: "FAST FORWARD", + 0xE0: "DOWN", + 0xA8: "VOL-", + 0x90: "UP", + 0x98: "EQ", + 0xB0: "ST/REPT", + 0x68: "0", + 0x30: "1", + 0x18: "2", + 0x7A: "3", + 0x10: "4", + 0x38: "5", + 0x5A: "6", + 0x42: "7", + 0x4A: "8", + 0x52: "9", +} + +var ( + pinIRIn = machine.GP26 + ir irremote.ReceiverDevice +) + +func setupPins() { + ir = irremote.NewReceiver(pinIRIn) + ir.Configure() +} + +func irCallback(data irremote.Data) { + msg := "Command: " + irCmdButtons[data.Command] + if data.Flags&irremote.DataFlagIsRepeat != 0 { + msg = msg + " (REPEAT)" + } + println(msg) +} + +func main() { + setupPins() + ir.SetCommandHandler(irCallback) + for { + time.Sleep(time.Millisecond * 10) + } +} diff --git a/irremote/receiver.go b/irremote/receiver.go new file mode 100644 index 0000000..580d2f9 --- /dev/null +++ b/irremote/receiver.go @@ -0,0 +1,225 @@ +package irremote // import "tinygo.org/x/drivers/irremote" + +import ( + "machine" + "time" +) + +// NEC protocol references +// https://www.sbprojects.net/knowledge/ir/nec.php +// https://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol +// https://simple-circuit.com/arduino-nec-remote-control-decoder/ + +// Data encapsulates the data received by the ReceiverDevice. +type Data struct { + // Code is the raw IR data received. + Code uint32 + // Address is the decoded address from the IR data received. + Address uint16 + // Command is the decoded command from the IR data recieved + Command uint16 + // Flags provides additional information about the IR data received. See DataFlags + Flags DataFlags +} + +// DataFlags provides bitwise flags representing various information about recieved IR data. +type DataFlags uint16 + +// Valid values for DataFlags +const ( + // DataFlagIsRepeat set indicates that the IR data is a repeat commmand + DataFlagIsRepeat DataFlags = 1 << iota +) + +// CommandHandler defines the callback function used to provide IR data received by the ReceiverDevice. +type CommandHandler func(data Data) + +// nec_ir_state represents the various internal states used to decode the NEC IR protocol commands +type nec_ir_state uint8 + +// Valid values for nec_ir_state +const ( + lead_pulse_start nec_ir_state = iota // Start receiving IR data, beginning of 9ms lead pulse + lead_space_start // End of 9ms lead pulse, start of 4.5ms space + lead_space_end // End of 4.5ms space, start of 562µs pulse + bit_read_start // End of 562µs pulse, start of 562µs or 1687µs space + bit_read_end // End of 562µs or 1687µs space + trail_pulse_end // End of 562µs trailing pulse +) + +// ReceiverDevice is the device for receiving IR commands +type ReceiverDevice struct { + pin machine.Pin // IR input pin. + ch CommandHandler // client callback function + necState nec_ir_state // internal state machine + data Data // decoded data for client + lastTime time.Time // used to track states + bitIndex int // tracks which bit (0-31) of necCode is being read +} + +// NewReceiver returns a new IR receiver device +func NewReceiver(pin machine.Pin) ReceiverDevice { + return ReceiverDevice{pin: pin} +} + +// Configure configures the input pin for the IR receiver device +func (ir *ReceiverDevice) Configure() { + // The IR receiver sends logic HIGH when NOT receiving IR, and logic LOW when receiving IR + ir.pin.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) +} + +// SetCommandHandler is used to start or stop receiving IR commands via a callback function (pass nil to stop) +func (ir *ReceiverDevice) SetCommandHandler(ch CommandHandler) { + ir.ch = ch + ir.resetStateMachine() + if ch != nil { + // Start monitoring IR output pin for changes + ir.pin.SetInterrupt(machine.PinFalling|machine.PinRising, ir.pinChange) + } else { + // Stop monitoring IR output pin for changes + ir.pin.SetInterrupt(0, nil) + } +} + +// Internal helper function to reset state machine on protocol failure +func (ir *ReceiverDevice) resetStateMachine() { + ir.data = Data{} + ir.bitIndex = 0 + ir.necState = lead_pulse_start +} + +// Internal pin rising/falling edge interrupt handler +func (ir *ReceiverDevice) pinChange(pin machine.Pin) { + /* Currently TinyGo is sending machine.NoPin (0xff) for all pins, at least on RP2040 + if pin != ir.pin { + return // This is not the pin you're looking for + } + */ + now := time.Now() + duration := now.Sub(ir.lastTime) + ir.lastTime = now + switch ir.necState { + case lead_pulse_start: + if !ir.pin.Get() { + // IR is 'on' (pin is pulled high and sent low when IR is received) + ir.necState = lead_space_start // move to next state + } + case lead_space_start: + if duration > time.Microsecond*9500 || duration < time.Microsecond*8500 { + // Invalid interval for 9ms lead pulse. Reset + ir.resetStateMachine() + } else { + // 9ms lead pulse detected, move to next state + ir.necState = lead_space_end + } + case lead_space_end: + if duration > time.Microsecond*5000 || duration < time.Microsecond*1750 { + // Invalid interval for 4.5ms lead space OR 2.25ms repeat space. Reset + ir.resetStateMachine() + } else { + // 4.5ms lead space OR 2.25ms repeat space detected + if duration > time.Microsecond*3000 { + // 4.5ms lead space detected, new code incoming, move to next state + ir.resetStateMachine() + ir.necState = bit_read_start + } else { + // 2.25ms repeat space detected. + if ir.data.Code != 0 { + // Valid repeat code. Invoke client callback with repeat flag set + ir.data.Flags |= DataFlagIsRepeat + if ir.ch != nil { + ir.ch(ir.data) + } + ir.necState = lead_pulse_start + } else { + // ir.data is not in a valid state for a repeat. Reset + ir.resetStateMachine() + } + } + } + case bit_read_start: + if duration > time.Microsecond*700 || duration < time.Microsecond*400 { + // Invalid interval for 562.5µs pulse. Reset + ir.resetStateMachine() + } else { + // 562.5µs pulse detected, move to next state + ir.necState = bit_read_end + } + case bit_read_end: + if duration > time.Microsecond*1800 || duration < time.Microsecond*400 { + // Invalid interval for 562.5µs space OR 1687.5µs space. Reset + ir.resetStateMachine() + } else { + // 562.5µs OR 1687.5µs space detected + mask := uint32((1 << (31 - ir.bitIndex))) + if duration > time.Microsecond*1000 { + // 1687.5µs space detected (logic 1) - Set bit + ir.data.Code |= mask + } else { + // 562.5µs space detected (logic 0) - Clear bit + ir.data.Code &^= mask + } + + ir.bitIndex++ + if ir.bitIndex > 31 { + // We've read all bits for this code, move to next state + ir.necState = trail_pulse_end + } else { + // Read next bit + ir.necState = bit_read_start + } + } + case trail_pulse_end: + if duration > time.Microsecond*700 || duration < time.Microsecond*400 { + // Invalid interval for trailing 562.5µs pulse. Reset + ir.resetStateMachine() + } else { + // 562.5µs trailing pulse detected. Decode & validate data + err := ir.decode() + if err == irDecodeErrorNone { + // Valid data, invoke client callback + if ir.ch != nil { + ir.ch(ir.data) + } + // around we go again. Note: we don't resetStateMachine() since repeat codes are now possible + ir.necState = lead_pulse_start + } else { + ir.resetStateMachine() + } + } + } +} + +// Error type for NEC format decoding +type irDecodeError int + +// Valid values for irDecodeError +const ( + irDecodeErrorNone irDecodeError = iota // no error occurred + irDecodeErrorInverseCheckFail // validation of inverse cmd does not match cmd +) + +func (ir *ReceiverDevice) decode() irDecodeError { + // Decode cmd and inverse cmd and perform validation check + cmd := uint8((ir.data.Code & 0xff00) >> 8) + invCmd := uint8(ir.data.Code & 0xff) + if cmd != ^invCmd { + // Validation failure. cmd and inverse cmd do not match + return irDecodeErrorInverseCheckFail + } + // cmd validation pass, decode address + ir.data.Command = uint16(cmd) + addrLow := uint8((ir.data.Code & 0xff000000) >> 24) + addrHigh := uint8((ir.data.Code & 0x00ff0000) >> 16) + if addrHigh == ^addrLow { + // addrHigh is inverse of addrLow. This is not a valid 16-bit address in extended NEC coding + // since it is indistinguishable from 8-bit address with inverse validation. Use the 8-bit address + ir.data.Address = uint16(addrLow) + } else { + // 16-bit extended NEC address + ir.data.Address = (uint16(addrHigh) << 8) | uint16(addrLow) + } + // Clear repeat flag + ir.data.Flags &^= DataFlagIsRepeat + return irDecodeErrorNone +}