Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions examples/irremote/sender/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"machine"
"time"

"tinygo.org/x/drivers/irremote"
)

var (
pinIROut = machine.GPIO17
pwmIROut = machine.PWM0
ir irremote.SenderDevice
)

const (
irAddress = 0x00
irCmdPwr = 0x45
)

func main() {
ir = irremote.NewSender(irremote.SenderConfig{
Pin: pinIROut,
PWM: pwmIROut,
DutyCycle: 33,
})
if err := ir.Configure(); err != nil {
println(err.Error())
return
}
for {
// Send one frame, then hold the button down for half a second.
ir.SendNEC(irAddress, irCmdPwr, true)
time.Sleep(time.Millisecond * 500)
ir.StopNECRepeats()
time.Sleep(time.Second * 2)
}
}
61 changes: 61 additions & 0 deletions irremote/irprotocol/proto_nec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package irprotocol // import "tinygo.org/x/drivers/irremote/irprotocol"

import "time"

// NEC protocol reference
// https://www.sbprojects.net/knowledge/ir/nec.php

// NEC timings.
const (
NECCarrierHz = 38000

NECUnit = time.Nanosecond * 562_500
NECLeadMark = NECUnit * 16
NECLeadSpace = NECUnit * 8
NECRepeatSpace = NECUnit * 4
NECBitMark = NECUnit
NECZeroSpace = NECUnit
NECOneSpace = NECUnit * 3
NECTrailMark = NECUnit

// NECRepeatPeriod is the time from the start of one frame to the start of
// the next repeat frame while a button stays down.
NECRepeatPeriod = NECUnit * 192
)

// SplitRawNECData breaks a raw NEC code into its parts and reports whether the
// command and its inverse agree.
func SplitRawNECData(data uint32) (valid bool, address uint16, command byte) {
addrLow := byte(data)
addrHigh := byte(data >> 8)
command = byte(data >> 16)
invCmd := byte(data >> 24)
address = MakeNECAddress(addrLow, addrHigh)
valid = command == ^invCmd
return
}

// MakeRawNECData assembles a raw NEC code from an address and a command.
func MakeRawNECData(address uint16, command byte) uint32 {
addrLow, addrHigh := SplitNECAddress(address)
return uint32(^command)<<24 | uint32(command)<<16 | uint32(addrHigh)<<8 | uint32(addrLow)
}

// SplitNECAddress splits an NEC address into low and high bytes.
func SplitNECAddress(address uint16) (addrLow, addrHigh byte) {
addrLow = byte(address)
addrHigh = byte(address >> 8)
if addrHigh == 0 {
addrHigh = ^addrLow
}
return addrLow, addrHigh
}

// MakeNECAddress assembles an NEC address from low and high bytes. A high byte
// that is the inverse of the low byte is the 8 bit form, not an extended address.
func MakeNECAddress(addrLow, addrHigh byte) uint16 {
if addrHigh == ^addrLow {
return uint16(addrLow)
}
return uint16(addrHigh)<<8 | uint16(addrLow)
}
99 changes: 99 additions & 0 deletions irremote/irprotocol/proto_nec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package irprotocol

import (
"fmt"
"testing"

qt "github.com/frankban/quicktest"
)

type necTestData struct {
Code uint32
Address uint16
Command byte
}

func decodeTests(t *testing.T, tests []necTestData, expectedValid bool) {
c := qt.New(t)

for _, data := range tests {
name := fmt.Sprintf("Decode:Code:%08x Addr:%04x Cmd:%02x",
data.Code, data.Address, data.Command)
c.Run(name, func(c *qt.C) {
valid, addr, cmd := SplitRawNECData(data.Code)
c.Assert(valid, qt.Equals, expectedValid)
if valid {
c.Assert(addr, qt.Equals, data.Address)
c.Assert(cmd, qt.Equals, data.Command)
}
})
}
}

func encodeTests(t *testing.T, tests []necTestData) {
c := qt.New(t)

for _, data := range tests {
name := fmt.Sprintf("Encode:Code:%08x Addr:%04x Cmd:%02x",
data.Code, data.Address, data.Command)
c.Run(name, func(c *qt.C) {
code := MakeRawNECData(data.Address, data.Command)
c.Assert(code, qt.Equals, data.Code)
})
}
}

// TestRawNECDataNonExtendedAddr encodes and decodes codes with 8 bit addresses.
func TestRawNECDataNonExtendedAddr(t *testing.T) {
tests := []necTestData{
{Code: 0xFF00FF00, Address: 0x0000, Command: 0x00},
{Code: 0x00FFFF00, Address: 0x0000, Command: 0xFF},
{Code: 0xFF0000FF, Address: 0x00FF, Command: 0x00},
{Code: 0x00FF00FF, Address: 0x00FF, Command: 0xFF},
{Code: 0xFF00DF20, Address: 0x0020, Command: 0x00},
{Code: 0xFF0020DF, Address: 0x00DF, Command: 0x00},
{Code: 0xDF20FF00, Address: 0x0000, Command: 0x20},
{Code: 0x20DFFF00, Address: 0x0000, Command: 0xDF},
}
decodeTests(t, tests, true)
encodeTests(t, tests)
}

// TestRawNECDataExtendedAddr encodes and decodes codes with 16 bit extended
// addresses.
func TestRawNECDataExtendedAddr(t *testing.T) {
tests := []necTestData{
{Code: 0xFF000100, Address: 0x0100, Command: 0x00},
{Code: 0xFF00FE00, Address: 0xFE00, Command: 0x00},
{Code: 0xFF00F00D, Address: 0xF00D, Command: 0x00},
}
decodeTests(t, tests, true)
encodeTests(t, tests)
}

// TestSplitRawNECDataInvalidCommand checks that a command that does not match
// its inverse fails validation.
func TestSplitRawNECDataInvalidCommand(t *testing.T) {
decodeTests(t,
[]necTestData{
// One wrong bit in each position of the inverse command.
{Code: 0x01FFFF00, Address: 0x0000, Command: 0xFF},
{Code: 0x02FFFF00, Address: 0x0000, Command: 0xFF},
{Code: 0x04FFFF00, Address: 0x0000, Command: 0xFF},
{Code: 0x08FFFF00, Address: 0x0000, Command: 0xFF},
{Code: 0x10FFFF00, Address: 0x0000, Command: 0xFF},
{Code: 0x20FFFF00, Address: 0x0000, Command: 0xFF},
{Code: 0x40FFFF00, Address: 0x0000, Command: 0xFF},
{Code: 0x80FFFF00, Address: 0x0000, Command: 0xFF},
// One wrong bit in each position of the command.
{Code: 0xFF01FF00, Address: 0x0000, Command: 0xFF},
{Code: 0xFF02FF00, Address: 0x0000, Command: 0xFF},
{Code: 0xFF04FF00, Address: 0x0000, Command: 0xFF},
{Code: 0xFF08FF00, Address: 0x0000, Command: 0xFF},
{Code: 0xFF10FF00, Address: 0x0000, Command: 0xFF},
{Code: 0xFF20FF00, Address: 0x0000, Command: 0xFF},
{Code: 0xFF40FF00, Address: 0x0000, Command: 0xFF},
{Code: 0xFF80FF00, Address: 0x0000, Command: 0xFF},
},
false)
}
21 changes: 5 additions & 16 deletions irremote/receiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package irremote // import "tinygo.org/x/drivers/irremote"
import (
"machine"
"time"

"tinygo.org/x/drivers/irremote/irprotocol"
)

// NEC protocol references
Expand Down Expand Up @@ -200,25 +202,12 @@ const (
)

func (ir *ReceiverDevice) decode() irDecodeError {
// Decode cmd and inverse cmd and perform validation check
cmd := uint8((ir.data.Code & 0x00ff0000) >> 16)
invCmd := uint8((ir.data.Code & 0xff000000) >> 24)
if cmd != ^invCmd {
// Validation failure. cmd and inverse cmd do not match
valid, addr, cmd := irprotocol.SplitRawNECData(ir.data.Code)
if !valid {
return irDecodeErrorInverseCheckFail
}
// cmd validation pass, decode address
ir.data.Address = addr
ir.data.Command = uint16(cmd)
addrLow := uint8(ir.data.Code & 0xff)
addrHigh := uint8((ir.data.Code & 0xff00) >> 8)
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
Expand Down
152 changes: 152 additions & 0 deletions irremote/sender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package irremote // import "tinygo.org/x/drivers/irremote"

import (
"errors"
"machine"
"sync"
"time"

"tinygo.org/x/drivers/irremote/irprotocol"
)

// ErrNoPWM is returned when the SenderConfig has no PWM.
var ErrNoPWM = errors.New("irremote: no PWM")

// PWM is the interface necessary for driving the IR carrier.
type PWM interface {
Configure(config machine.PWMConfig) error
Channel(pin machine.Pin) (channel uint8, err error)
Top() uint32
Set(channel uint8, value uint32)
}

// SenderConfig is used to configure the SenderDevice.
type SenderConfig struct {
Pin machine.Pin
PWM PWM
// DutyCycle is the carrier duty in percent. Values outside 1 to 100 give 33.
DutyCycle uint8
}

// SenderDevice is the device for sending IR commands. Timing busy waits on
// time.Now, so accuracy follows the clock resolution.
// https://github.com/tinygo-org/drivers/issues/668
type SenderDevice struct {
pwm PWM
pin machine.Pin
dutyPct uint8
channel uint8
duty uint32
mu sync.Mutex
gen uint64
}

// NewSender returns a new IR sender device.
func NewSender(config SenderConfig) SenderDevice {
return SenderDevice{pwm: config.PWM, pin: config.Pin, dutyPct: config.DutyCycle}
}

// Configure sets the PWM to the 38kHz carrier and leaves it off.
func (ir *SenderDevice) Configure() error {
ir.mu.Lock()
defer ir.mu.Unlock()
if ir.pwm == nil {
return ErrNoPWM
}
err := ir.pwm.Configure(machine.PWMConfig{Period: uint64(time.Second / irprotocol.NECCarrierHz)})
if err != nil {
return err
}
ch, err := ir.pwm.Channel(ir.pin)
if err != nil {
return err
}
ir.channel = ch
pct := ir.dutyPct
if pct < 1 || pct > 100 {
pct = 33
}
// Multiply first, so a small PWM top does not truncate the duty to zero.
ir.duty = uint32(uint64(ir.pwm.Top()) * uint64(pct) / 100)
ir.pwm.Set(ir.channel, 0)
return nil
}

// SendNEC sends one NEC frame. With autoRepeat set, repeat frames go out
// until StopNECRepeats is called.
func (ir *SenderDevice) SendNEC(address uint16, command uint16, autoRepeat bool) {
ir.mu.Lock()
defer ir.mu.Unlock()
ir.gen++
start := time.Now()
ir.sendNECRawCode(irprotocol.MakeRawNECData(address, byte(command)))
if !autoRepeat {
return
}
gen := ir.gen
go func(next time.Time) {
for {
time.Sleep(time.Until(next))
ir.mu.Lock()
if ir.gen != gen {
ir.mu.Unlock()
return
}
next = time.Now().Add(irprotocol.NECRepeatPeriod)
ir.sendNECRepeat()
ir.mu.Unlock()
}
}(start.Add(irprotocol.NECRepeatPeriod))
}

// StopNECRepeats ends the repeat frames started by SendNEC.
func (ir *SenderDevice) StopNECRepeats() {
ir.mu.Lock()
defer ir.mu.Unlock()
ir.gen++
}

// SendNECRawCode sends one NEC frame from a 32 bit code, least significant bit
// first.
func (ir *SenderDevice) SendNECRawCode(code uint32) {
ir.mu.Lock()
defer ir.mu.Unlock()
ir.gen++
ir.sendNECRawCode(code)
}

func (ir *SenderDevice) sendNECRawCode(code uint32) {
ir.mark(irprotocol.NECLeadMark)
ir.space(irprotocol.NECLeadSpace)
for i := 0; i < 32; i++ {
ir.mark(irprotocol.NECBitMark)
if code&(1<<uint(i)) != 0 {
ir.space(irprotocol.NECOneSpace)
} else {
ir.space(irprotocol.NECZeroSpace)
}
}
ir.mark(irprotocol.NECTrailMark)
}

func (ir *SenderDevice) sendNECRepeat() {
ir.mark(irprotocol.NECLeadMark)
ir.space(irprotocol.NECRepeatSpace)
ir.mark(irprotocol.NECTrailMark)
}

func (ir *SenderDevice) mark(d time.Duration) {
ir.pwm.Set(ir.channel, ir.duty)
wait(d)
ir.pwm.Set(ir.channel, 0)
}

func (ir *SenderDevice) space(d time.Duration) {
wait(d)
}

func wait(d time.Duration) {
end := time.Now().Add(d)
for time.Now().Before(end) {
}
}
1 change: 1 addition & 0 deletions smoketest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/sx
tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/sx127x/lora_rxtx/
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go
tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go
tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/sender/main.go
tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/uc8151/main.go
tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/waveshare-epd/epd2in9v2/main.go
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd30/main.go
Expand Down
Loading