From: Peter Hinch Date: Tue, 10 Mar 2020 13:26:21 +0000 (+0000) Subject: Prior to merge. X-Git-Url: https://vault307.fbx.one/gitweb/micorpython_ir.git/commitdiff_plain/36140aa568d4a538adafb03e575b26de8a0784ef Prior to merge. --- diff --git a/README.md b/README.md index 3c78771..371615f 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,24 @@ This repo provides a driver to receive from IR (infra red) remote controls and a driver for IR "blaster" apps. The device drivers are nonblocking. They do not -require `uasyncio` but are compatible with it. +require `uasyncio` but are compatible with it, and are designed for standard +firmware builds. -The transmitter driver is specific to the Pyboard. The receiver is cross -platform and has been tested on Pyboard, ESP8266 and ESP32. See -[Receiver platforms](./README.md#42-receiver-platforms) for test results and -limitations. +The receiver is cross platform and has been tested on Pyboard, ESP8266 and +ESP32. The transmitter driver is compatible with Pyboard (1.x and D series) and +also (subject to limitations) with ESP32. For the transmitter a Pyboard is +recommended. + +The transmitter is documented [here](./TRANSMITTER.md) and the receiver is +[here](./RECEIVER.md). # 1. IR communication IR communication uses a carrier frequency to pulse the IR source. Modulation takes the form of OOK (on-off keying). There are multiple protocols and at -least three options for carrier frequency, namely 36KHz, 38KHz and 40KHz. +least three options for carrier frequency: 36, 38 and 40KHz. -The drivers support NEC and Sony protocols and two Philips protocols, namely +The drivers support NEC and Sony protocols plus two Philips protocols, namely RC-5 and RC-6 mode 0. In the case of the transmitter the carrier frequency is a runtime parameter: any value may be specified. The receiver uses a hardware demodulator which should be purchased for the correct frequency. The receiver @@ -25,7 +29,7 @@ agnostic. Examining waveforms from various remote controls it is evident that numerous protocols exist. Some are doubtless proprietary and undocumented. The supported protocols are those for which I managed to locate documentation. My preference -is for the NEC version. It has conservative timing and ample scope for error +is for the NEC version. It has conservative timing and good provision for error detection. RC-5 has limited error detection, and RC-6 mode 0 has rather fast timing. @@ -39,349 +43,20 @@ dependent. # 2. Hardware Requirements +These are discussed in detail in the relevant docs; the following provides an +overview. + The receiver is cross-platform. It requires an IR receiver chip to demodulate the carrier. The chip must be selected for the frequency in use by the remote. For 38KHz devices a receiver chip such as the Vishay TSOP4838 or the [adafruit one](https://www.adafruit.com/products/157) is required. This demodulates the 38KHz IR pulses and passes the demodulated pulse train to the -microcontroller. The tested chip returns a 0 level on carrier detect, but the -driver design ensures operation regardless of sense. +microcontroller. In my testing a 38KHz demodulator worked with 36KHz and 40KHz remotes, but this is obviously neither guaranteed nor optimal. -The pin used to connect the decoder chip to the target is arbitrary. The test -program assumes pin X3 on the Pyboard, pin 23 on ESP32 and pin 13 on ESP8266. -On the WeMos D1 Mini the equivalent pin is D7. - -The transmitter requires a Pyboard 1.x (not Lite) or a Pyboard D. Output is via -an IR LED which will normally need a transistor to provide sufficient current. -Typically these need 50-100mA of drive to achieve reasonable range and data -integrity. A suitable LED is [this one](https://www.adafruit.com/product/387). - -The transmitter test script assumes pin X1 for IR output. It can be changed, -but it must support Timer 2 channel 1. Pins for pushbutton inputs are -arbitrary: X3 and X4 are used. - -# 3. Installation - -On import, demos print an explanation of how to run them. - -## 3.1 Receiver - -This is a Python package. This minimises RAM usage: applications only import -the device driver for the protocol in use. - -Copy the following to the target filesystem: - 1. `ir_rx` Directory and contents. Contains the device drivers. - 2. `ir_rx_test.py` Demo of a receiver. - -There are no dependencies. - -The demo can be used to characterise IR remotes. It displays the codes returned -by each button. This can aid in the design of receiver applications. The demo -prints "running" every 5 seconds and reports any data received from the remote. - -## 3.2 Transmitter - -Copy the following files to the Pyboard filesystem: - 1. `ir_tx.py` The transmitter device driver. - 2. `ir_tx_test.py` Demo of a 2-button remote controller. - -The device driver has no dependencies. The test program requires `uasyncio` -from the official library and `aswitch.py` from -[this repo](https://github.com/peterhinch/micropython-async). - -# 4. Receiver - -This implements a class for each supported protocol. Applications should -instantiate the appropriate class with a callback. The callback will run -whenever an IR pulse train is received. Example running on a Pyboard: - -```python -import time -from machine import Pin -from pyb import LED -from ir_rx.nec import NEC_8 # NEC remote, 8 bit addresses - -red = LED(1) - -def callback(data, addr, ctrl): - if data < 0: # NEC protocol sends repeat codes. - print('Repeat code.') - else: - print('Data {:02x} Addr {:04x}'.format(data, addr)) - -ir = NEC_8(Pin('X3', Pin.IN), callback) -while True: - time.sleep_ms(500) - red.toggle() -``` - -#### Common to all classes - -Constructor: -Args: - 1. `pin` is a `machine.Pin` instance configured as an input, connected to the - IR decoder chip. - 2. `callback` is the user supplied callback. - 3. `*args` Any further args will be passed to the callback. - -The user callback takes the following args: - 1. `data` (`int`) Value from the remote. Normally in range 0-255. A value < 0 - signifies an NEC repeat code. - 2. `addr` (`int`) Address from the remote. - 3. `ctrl` (`int`) The meaning of this is protocol dependent: - NEC: 0 - Philips: this is toggled 1/0 on repeat button presses. If the button is held - down it is not toggled. The transmitter demo implements this behaviour. - Sony: 0 unless receiving a 20-bit stream, in which case it holds the extended - value. - 4. Any args passed to the constructor. - -Bound variable: - 1. `verbose=False` If `True` emits debug output. - -Method: - 1. `error_function` Arg: a function taking a single arg. If this is specified - it will be called if an error occurs. The value corresponds to the error code - (see below). - -A function is provided to print errors in human readable form. This may be -invoked as follows: - -```python -from ir_rx.print_error import print_error # Optional print of error codes -# Assume ir is an instance of an IR receiver class -ir.error_function(print_error) -``` - -#### NEC classes - -`NEC_8`, `NEC_16` - -```python -from ir_rx.nec import NEC_8 -``` - -Remotes using the NEC protocol can send 8 or 16 bit addresses. If the `NEC_16` -class receives an 8 bit address it will get a 16 bit value comprising the -address in bits 0-7 and its one's complement in bits 8-15. -The `NEC_8` class enables error checking for remotes that return an 8 bit -address: the complement is checked and the address returned as an 8-bit value. -A 16-bit address will result in an error. - -#### Sony classes - -`SONY_12`, `SONY_15`, `SONY_20` - -```python -from ir_rx.sony import SONY_15 -``` - -The SIRC protocol comes in 3 variants: 12, 15 and 20 bits. `SONY_20` handles -bitstreams from all three types of remote. Choosing a class matching the remote -improves the timing reducing the likelihood of errors when handling repeats: in -20-bit mode SIRC timing when a button is held down is tight. A worst-case 20 -bit block takes 39ms nominal, yet the repeat time is 45ms nominal. -A single physical remote can issue more than one type of bitstream. The Sony -remote tested issued both 12 bit and 15 bit streams. - -#### Philips classes - -`RC5_IR`, `RC6_M0` - -```python -from ir_rx.philips import RC5_IR -``` - -These support the RC-5 and RC-6 mode 0 protocols respectively. - -# 4.1 Errors - -IR reception is inevitably subject to errors, notably if the remote is operated -near the limit of its range, if it is not pointed at the receiver or if its -batteries are low. The user callback is not called when an error occurs. - -On ESP8266 and ESP32 there is a further source of errors. This results from the -large and variable interrupt latency of the device which can exceed the pulse -duration. This causes pulses to be missed or their timing measured incorrectly. -On ESP8266 some improvment may be achieved by running the chip at 160MHz. - -In general applications should provide user feedback of correct reception. -Users tend to press the key again if the expected action is absent. - -In debugging a callback can be specified for reporting errors. The value passed -to the error function are represented by constants indicating the cause of the -error. These are as follows: - -`BADSTART` A short (<= 4ms) start pulse was received. May occur due to IR -interference, e.g. from fluorescent lights. The TSOP4838 is prone to producing -200µs pulses on occasion, especially when using the ESP8266. -`BADBLOCK` A normal data block: too few edges received. Occurs on the ESP8266 -owing to high interrupt latency. -`BADREP` A repeat block: an incorrect number of edges were received. -`OVERRUN` A normal data block: too many edges received. -`BADDATA` Data did not match check byte. -`BADADDR` (`NEC_IR`) If `extended` is `False` the 8-bit address is checked -against the check byte. This code is returned on failure. - -# 4.2 Receiver platforms - -Currently the ESP8266 suffers from [this issue](https://github.com/micropython/micropython/issues/5714). -Testing was therefore done without WiFi connectivity. - -Philips protocols (especially RC-6) have tight timing constraints with short -pulses whose length must be determined with reasonable accuracy. The Sony 20 -bit protocol also has a timing issue in that the worst case bit pattern takes -39ms nominal, yet the repeat time is 45ms nominal. These issues can lead to -errors particularly on slower targets. As discussed above, errors are to be -expected. It is up to the user to decide if the error rate is acceptable. - -Reception was tested using Pyboard D SF2W, ESP8266 and ESP32 with signals from -remote controls (where available) and from the tranmitter in this repo. Issues -are listed below. - -NEC: No issues. -Sony 12 and 15 bit: No issues. -Sony 20 bit: On ESP32 some errors occurred when repeats occurred. -Philips RC-5: On ESP32 with one remote control many errors occurred, but paired -with the transmitter in this repo it worked. -Philips RC-6: No issues. Only tested against the transmitter in this repo. - -# 4.3 Principle of operation - -Protocol classes inherit from the abstract base class `IR_RX`. This uses a pin -interrupt to store in an array the start and end times of pulses (in μs). -Arrival of the first pulse triggers a software timer which runs for the -expected duration of an IR block (`tblock`). When it times out its callback -(`.decode`) decodes the data and calls the user callback. The use of a software -timer ensures that `.decode` and the user callback can allocate. - -The size of the array and the duration of the timer are protocol dependent and -are set by the subclasses. The `.decode` method is provided in the subclass. - -CPU times used by `.decode` (not including the user callback) were measured on -a Pyboard D SF2W at stock frequency. They were: NEC 1ms for normal data, 100μs -for a repeat code. Philips codes: RC-5 900μs, RC-6 mode 0 5.5ms. - -# 5 Transmitter - -This is specific to Pyboard D and Pyboard 1.x (not Lite). - -It implements a class for each supported protocol, namely `NEC`, `SONY`, `RC5` -and `RC6_M0`. The application instantiates the appropriate class and calls the -`transmit` method to send data. - -Constructor -All constructors take the following args: - 1. `pin` An initialised `pyb.Pin` instance supporting Timer 2 channel 1: `X1` - is employed by the test script. Must be connected to the IR diode as described - below. - 2. `freq=default` The carrier frequency in Hz. The default for NEC is 38000, - Sony is 40000 and Philips is 36000. - 3. `verbose=False` If `True` emits debug output. - -The `SONY` constructor is of form `pin, bits=12, freq=40000, verbose=False`. -The `bits` value may be 12, 15 or 20 to set SIRC variant in use. Other args are -as above. - -Method: - 1. `transmit(addr, data, toggle=0)` Integer args. `addr` and `data` are - normally 8-bit values and `toggle` is normally 0 or 1. - In the case of NEC, if an address < 256 is passed, normal mode is assumed and - the complementary value is appended. 16-bit values are transmitted as extended - addresses. - In the case of NEC the `toggle` value is ignored. For Philips protocols it - should be toggled each time a button is pressed, and retained if the button is - held down. The test program illustrates a way to do this. - `SONY` ignores `toggle` unless in 20-bit mode, in which case it is transmitted - as the `extended` value and can be any integer in range 0 to 255. - -The `transmit` method is synchronous with rapid return. Actual transmission -occurs as a background process, controlled by timers 2 and 5. Execution times -on a Pyboard 1.1 were 3.3ms for NEC, 1.5ms for RC5 and 2ms for RC6. - -# 5.1 Wiring - -I use the following circuit which delivers just under 40mA to the diode. R2 may -be reduced for higher current. -![Image](images/circuit.png) - -This alternative delivers a constant current of about 53mA if a higher voltage -than 5V is available. R4 determines the current value and may be reduced to -increase power. -![Image](images/circuit2.png) - -The transistor type is not critical. - -The driver assumes circuits as shown. Here the carrier "off" state is 0V, -which is the driver default. If using a circuit where "off" is required to be -3.3V, the constant `_SPACE` in `ir_tx.py` should be changed to 100. - -# 5.2 Principle of operation - -The classes inherit from the abstract base class `IR`. This has an array `.arr` -to contain the duration (in μs) of each carrier on or off period. The -`transmit` method calls a `tx` method of the subclass which populates this -array. On completion `transmit` appends a special `STOP` value and initiates -physical transmission which occurs in an interrupt context. - -This is performed by two hardware timers initiated in the constructor. Timer 2, -channel 1 is used to configure the output pin as a PWM channel. Its frequency -is set in the constructor. The OOK is performed by dynamically changing the -duty ratio using the timer channel's `pulse_width_percent` method: this varies -the pulse width from 0 to a duty ratio passed to the constructor. The NEC -protocol defaults to 50%, the Sony and Philips ones to 30%. - -The duty ratio is changed by the Timer 5 callback `._cb`. This retrieves the -next duration from the array. If it is not `STOP` it toggles the duty cycle -and re-initialises T5 for the new duration. - -The `IR.append` enables times to be added to the array, keeping track of the -notional carrier on/off state for biphase generation. The `IR.add` method -facilitates lengthening a pulse as required in the biphase sequences used in -Philips protocols. - -# 6. References - -[General information about IR](https://www.sbprojects.net/knowledge/ir/) - -The NEC protocol: -[altium](http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol) -[circuitvalley](http://www.circuitvalley.com/2013/09/nec-protocol-ir-infrared-remote-control.html) - -Philips protocols: -[RC5](https://en.wikipedia.org/wiki/RC-5) -[RC6](https://www.sbprojects.net/knowledge/ir/rc6.php) - -Sony protocol: -[SIRC](https://www.sbprojects.net/knowledge/ir/sirc.php) - -# Appendix 1 NEC Protocol description - -A normal burst comprises exactly 68 edges, the exception being a repeat code -which has 4. An incorrect number of edges is treated as an error. All bursts -begin with a 9ms pulse. In a normal code this is followed by a 4.5ms space; a -repeat code is identified by a 2.25ms space. A data burst lasts for 67.5ms. - -Data bits comprise a 562.5µs mark followed by a space whose length determines -the bit value. 562.5µs denotes 0 and 1.6875ms denotes 1. - -In 8 bit address mode the complement of the address and data values is sent to -provide error checking. This also ensures that the number of 1's and 0's in a -burst is constant, giving a constant burst length of 67.5ms. In extended -address mode this constancy is lost. The burst length can (by my calculations) -run to 76.5ms. - -A pin interrupt records the time of every state change (in µs). The first -interrupt in a burst sets an event, passing the time of the state change. A -coroutine waits on the event, yields for the duration of a data burst, then -decodes the stored data before calling the user-specified callback. - -Passing the time to the `Event` instance enables the coro to compensate for -any asyncio latency when setting its delay period. - -The algorithm promotes interrupt handler speed over RAM use: the 276 bytes used -for the data array could be reduced to 69 bytes by computing and saving deltas -in the interrupt service routine. +The transmitter requires a Pyboard 1.x (not Lite), a Pyboard D or an ESP32. +Output is via an IR LED which will need a transistor to provide sufficient +current. The ESP32 has significant limitations as a transmitter discussed +[here](./TRANSMITTER.md#52-esp32). diff --git a/RECEIVER.md b/RECEIVER.md new file mode 100644 index 0000000..db0f164 --- /dev/null +++ b/RECEIVER.md @@ -0,0 +1,264 @@ +# IR Receiver + +##### [Main README](./README.md#1-ir-communication) + +# 1. Hardware Requirements + +The receiver is cross-platform. It requires an IR receiver chip to demodulate +the carrier. The chip must be selected for the frequency in use by the remote. +For 38KHz devices a receiver chip such as the Vishay TSOP4838 or the +[adafruit one](https://www.adafruit.com/products/157) is required. This +demodulates the 38KHz IR pulses and passes the demodulated pulse train to the +microcontroller. The tested chip returns a 0 level on carrier detect, but the +driver design ensures operation regardless of sense. + +In my testing a 38KHz demodulator worked with 36KHz and 40KHz remotes, but this +is obviously neither guaranteed nor optimal. + +The pin used to connect the decoder chip to the target is arbitrary. The test +program assumes pin X3 on the Pyboard, pin 23 on ESP32 and pin 13 on ESP8266. +On the WeMos D1 Mini the equivalent pin is D7. + +A remote using the NEC protocol is [this one](https://www.adafruit.com/products/389). + +# 2. Installation and demo script + +The receiver is a Python package. This minimises RAM usage: applications only +import the device driver for the protocol in use. + + +Copy the following to the target filesystem: + 1. `ir_rx` Directory and contents. + +There are no dependencies. + +The demo can be used to characterise IR remotes. It displays the codes returned +by each button. This can aid in the design of receiver applications. The demo +prints "running" every 5 seconds and reports any data received from the remote. + +```python +from ir_rx.test import test +``` +Instructions will be displayed at the REPL. + +# 3. The driver + +This implements a class for each supported protocol. Each class is subclassed +from a common abstract base class in `__init__.py`. + +Applications should instantiate the appropriate class with a callback. The +callback will run whenever an IR pulse train is received. Example running on a +Pyboard: +```python +import time +from machine import Pin +from pyb import LED +from ir_rx.nec import NEC_8 # NEC remote, 8 bit addresses + +red = LED(1) + +def callback(data, addr, ctrl): + if data < 0: # NEC protocol sends repeat codes. + print('Repeat code.') + else: + print('Data {:02x} Addr {:04x}'.format(data, addr)) + +ir = NEC_8(Pin('X3', Pin.IN), callback) +while True: + time.sleep_ms(500) + red.toggle() +``` + +#### Common to all classes + +Constructor: +Args: + 1. `pin` is a `machine.Pin` instance configured as an input, connected to the + IR decoder chip. + 2. `callback` is the user supplied callback. + 3. `*args` Any further args will be passed to the callback. + +The user callback takes the following args: + 1. `data` (`int`) Value from the remote. Normally in range 0-255. A value < 0 + signifies an NEC repeat code. + 2. `addr` (`int`) Address from the remote. + 3. `ctrl` (`int`) The meaning of this is protocol dependent: + NEC: 0 + Philips: this is toggled 1/0 on repeat button presses. If the button is held + down it is not toggled. The transmitter demo implements this behaviour. + Sony: 0 unless receiving a 20-bit stream, in which case it holds the extended + value. + 4. Any args passed to the constructor. + +Bound variable: + 1. `verbose=False` If `True` emits debug output. + +Method: + 1. `error_function` Arg: a function taking a single `int` arg. If this is + specified it will be called if an error occurs. The value corresponds to the + error code (see below). Typical usage might be to provide some user feedback + of incorrect reception although beware of occasional triggers by external + events. In my testing the TSOP4838 produces 200µs pulses on occasion for no + obvious reason. See [section 4](./RECEIVER.md#4-errors). + +A function is provided to print errors in human readable form. This may be +invoked as follows: + +```python +from ir_rx.print_error import print_error # Optional print of error codes +# Assume ir is an instance of an IR receiver class +ir.error_function(print_error) +``` +Class variables: + 1. These are constants defining the NEC repeat code and the error codes sent + to the error function. They are discussed in [section 4](./RECEIVER.md#4-errors). + +#### NEC classes + +`NEC_8`, `NEC_16` + +Typical invocation: +```python +from ir_rx.nec import NEC_8 +``` + +Remotes using the NEC protocol can send 8 or 16 bit addresses. If the `NEC_16` +class receives an 8 bit address it will get a 16 bit value comprising the +address in bits 0-7 and its one's complement in bits 8-15. +The `NEC_8` class enables error checking for remotes that return an 8 bit +address: the complement is checked and the address returned as an 8-bit value. +A 16-bit address will result in an error. + +#### Sony classes + +`SONY_12`, `SONY_15`, `SONY_20` + +Typical invocation: +```python +from ir_rx.sony import SONY_15 +``` + +The SIRC protocol comes in 3 variants: 12, 15 and 20 bits. `SONY_20` handles +bitstreams from all three types of remote. Choosing a class matching the remote +improves the timing reducing the likelihood of errors when handling repeats: in +20-bit mode SIRC timing when a button is held down is tight. A worst-case 20 +bit block takes 39ms nominal, yet the repeat time is 45ms nominal. +A single physical remote can issue more than one type of bitstream. The Sony +remote tested issued both 12 bit and 15 bit streams. + +#### Philips classes + +`RC5_IR`, `RC6_M0` + +Typical invocation: +```python +from ir_rx.philips import RC5_IR +``` + +These support the RC-5 and RC-6 mode 0 protocols respectively. + +# 4. Errors + +IR reception is inevitably subject to errors, notably if the remote is operated +near the limit of its range, if it is not pointed at the receiver or if its +batteries are low. The user callback is not called when an error occurs. + +On ESP8266 and ESP32 there is a further source of errors. This results from the +large and variable interrupt latency of the device which can exceed the pulse +duration. This causes pulses to be missed or their timing measured incorrectly. +On ESP8266 some improvment may be achieved by running the chip at 160MHz. + +In general applications should provide user feedback of correct reception. +Users tend to press the key again if the expected action is absent. + +In debugging a callback can be specified for reporting errors. The value passed +to the error function are represented by constants indicating the cause of the +error. These are driver ABC class variables and are as follows: + +`BADSTART` A short (<= 4ms) start pulse was received. May occur due to IR +interference, e.g. from fluorescent lights. The TSOP4838 is prone to producing +200µs pulses on occasion, especially when using the ESP8266. +`BADBLOCK` A normal data block: too few edges received. Occurs on the ESP8266 +owing to high interrupt latency. +`BADREP` A repeat block: an incorrect number of edges were received. +`OVERRUN` A normal data block: too many edges received. +`BADDATA` Data did not match check byte. +`BADADDR` (`NEC_IR`) If `extended` is `False` the 8-bit address is checked +against the check byte. This code is returned on failure. + +# 5. Receiver platforms + +Currently the ESP8266 suffers from [this issue](https://github.com/micropython/micropython/issues/5714). +Testing was therefore done without WiFi connectivity. If the application uses +the WiFi regularly, or if an external process pings the board repeatedly, the +crash does not occur. + +Philips protocols (especially RC-6) have tight timing constraints with short +pulses whose length must be determined with reasonable accuracy. The Sony 20 +bit protocol also has a timing issue in that the worst case bit pattern takes +39ms nominal, yet the repeat time is 45ms nominal. These issues can lead to +errors particularly on slower targets. As discussed above, errors are to be +expected. It is up to the user to decide if the error rate is acceptable. + +Reception was tested using Pyboard D SF2W, ESP8266 and ESP32 with signals from +remote controls (where available) and from the tranmitter in this repo. Issues +are listed below. + +NEC: No issues. +Sony 12 and 15 bit: No issues. +Sony 20 bit: On ESP32 some errors occurred when repeats occurred. +Philips RC-5: On ESP32 with one remote control many errors occurred, but paired +with the transmitter in this repo it worked. +Philips RC-6: No issues. Only tested against the transmitter in this repo. + +# 6. Principle of operation + +Protocol classes inherit from the abstract base class `IR_RX`. This uses a pin +interrupt to store in an array the time (in μs) of each transition of the pulse +train from the receiver chip. Arrival of the first edge starts a software timer +which runs for the expected duration of an IR block (`tblock`). The use of a +software timer ensures that `.decode` and the user callback can allocate. + +When the timer times out its callback (`.decode`) decodes the data. `.decode` +is a method of the protocol specific subclass; on completion it calls the +`do_callback` method of the ABC. This resets the edge reception and calls +either the user callback or the error function (if provided). + +The size of the array and the duration of the timer are protocol dependent and +are set by the subclasses. The `.decode` method is provided in the subclass. + +CPU times used by `.decode` (not including the user callback) were measured on +a Pyboard D SF2W at stock frequency. They were: NEC 1ms for normal data, 100μs +for a repeat code. Philips codes: RC-5 900μs, RC-6 mode 0 5.5ms. + +# 7. References + +[General information about IR](https://www.sbprojects.net/knowledge/ir/) + +The NEC protocol: +[altium](http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol) +[circuitvalley](http://www.circuitvalley.com/2013/09/nec-protocol-ir-infrared-remote-control.html) + +Philips protocols: +[RC5](https://en.wikipedia.org/wiki/RC-5) +[RC5](https://www.sbprojects.net/knowledge/ir/rc5.php) +[RC6](https://www.sbprojects.net/knowledge/ir/rc6.php) + +Sony protocol: +[SIRC](https://www.sbprojects.net/knowledge/ir/sirc.php) + +# Appendix 1 NEC Protocol description + +A normal burst comprises exactly 68 edges, the exception being a repeat code +which has 4. An incorrect number of edges is treated as an error. All bursts +begin with a 9ms pulse. In a normal code this is followed by a 4.5ms space; a +repeat code is identified by a 2.25ms space. A data burst lasts for 67.5ms. + +Data bits comprise a 562.5µs mark followed by a space whose length determines +the bit value. 562.5µs denotes 0 and 1.6875ms denotes 1. + +In 8 bit address mode the complement of the address and data values is sent to +provide error checking. This also ensures that the number of 1's and 0's in a +burst is constant, giving a constant burst length of 67.5ms. In extended +address mode this constancy is lost. The burst length can (by my calculations) +run to 76.5ms. diff --git a/TRANSMITTER.md b/TRANSMITTER.md new file mode 100644 index 0000000..3478ba8 --- /dev/null +++ b/TRANSMITTER.md @@ -0,0 +1,193 @@ +# IR Transmitter + +##### [Main README](./README.md#1-ir-communication) + +# 1. Hardware Requirements + +The transmitter requires a Pyboard 1.x (not Lite), a Pyboard D or an ESP32. +Output is via an IR LED which will need a transistor to provide sufficient +current. Typically these need 50-100mA of drive to achieve reasonable range and +data integrity. A suitable 940nm LED is [this one](https://www.adafruit.com/product/387). + +On the Pyboard the transmitter test script assumes pin X1 for IR output. It can +be changed, but it must support Timer 2 channel 1. Pins for pushbutton inputs +are arbitrary: X3 and X4 are used. The driver uses timers 2 and 5. + +On ESP32 pin 23 is used for IR output and pins 18 and 19 for pushbuttons. The +ESP32 solution has limitations discussed in [section 5.2](./TRANSMITTER.md#52-esp32). + +## 1.1 Wiring + +I use the following circuit which delivers just under 40mA to the diode. R2 may +be reduced for higher current. +![Image](images/circuit.png) + +This alternative delivers a constant current of about 53mA if a higher voltage +than 5V is available. R4 determines the current value and may be reduced to +increase power. +![Image](images/circuit2.png) + +The transistor type is not critical. + +The driver assumes circuits as shown. Here the carrier "off" state is 0V, +which is the driver default. If using a circuit where "off" is required to be +3.3V, the constant `_SPACE` in `ir_tx.__init__.py` should be changed to 100. + +# 2. Installation + +The transmitter is a Python package. This minimises RAM usage: applications +only import the device driver for the protocol in use. + +Copy the following to the target filesystem: + 1. `ir_tx` Directory and contents. + +The device driver has no dependencies. + +The demo program requires `uasyncio` from the official library and `aswitch.py` +from [this repo](https://github.com/peterhinch/micropython-async). The demo is +of a 2-button remote controller with auto-repeat. It may be run by issuing: + +```python +from ir_tx.test import test +``` +Instructions will be displayed at the REPL. + +# 3. The driver + +This is specific to Pyboard D, Pyboard 1.x (not Lite) and ESP32. + +It implements a class for each supported protocol, namely `NEC`, `SONY_12`, +`SONY_15`, `SONY_20`, `RC5` and `RC6_M0`. Each class is subclassed from a +common abstract base class in `__init__.py`. The application instantiates the +appropriate class and calls the `transmit` method to send data. + +The ESP32 platform is marginal in this application because of imprecision in +its timing. The Philips protocols are unsupported as they require unachievable +levels of precision. Test results are discussed [here](./TRANSMITTER.md#52-esp32). + +Constructor +All constructors take the following args: + 1. `pin` An initialised `pyb.Pin` instance supporting Timer 2 channel 1: `X1` + is employed by the test script. Must be connected to the IR diode as described + below. + 2. `freq=default` The carrier frequency in Hz. The default for NEC is 38000, + Sony is 40000 and Philips is 36000. + 3. `verbose=False` If `True` emits debug output. + +Method: + 1. `transmit(addr, data, toggle=0)` Integer args. `addr` and `data` are + normally 8-bit values and `toggle` is normally 0 or 1; details are protocol + dependent and are described below. + +The `transmit` method is synchronous with rapid return. Actual transmission +occurs as a background process, on the Pyboard controlled by timers 2 and 5. +Execution times on a Pyboard 1.1 were 3.3ms for NEC, 1.5ms for RC5 and 2ms +for RC6. + +#### NEC class + +This has an additional method `.repeat` (no args). This causes a repeat code to +be transmitted. Should be called every 108ms if a button is held down. + +The NEC protocol accepts 8 or 16 bit addresses. In the former case, a 16 bit +value is transmitted comprising the 8 bit address and its one's complement, +enabling the receiver to perform a simple error check. The `NEC` class supports +these modes by checking the value of `addr` passed to `.transmit` and sending +the complement for 8 bit values. + +`toggle` is ignored. + +#### Sony classes + +The SIRC protocol supports three sizes, supported by the following classes: + 1. 12 bit (7 data, 5 address) `SONY_12` + 2. 15 bit (7 data, 8 address) `SONY_15` + 3. 20 bit (7 data, 5 addresss, 8 extended) `SONY_20` + +The `.transmit` method masks `addr` and `data` values to the widths listed +above. `toggle` is ignored except by `SONY_20` which treats it as the extended +value. + +#### Philips classes + +The RC-5 protocol supports a 5 bit address and 6 or 7 bit (RC5X) data. The +driver uses the appropriate mode depending on the `data` value provided. + +The RC-6 protocol accepts 8 bit address and data values. + +Both send a `toggle` bit which remains constant if a button is held down, but +changes when the button is released. The application should implement this +behaviour, setting the `toggle` arg of `.transmit` to 0 or 1 as required. + +# 4. Test results + +# 5. Principle of operation + +## 5.1 Pyboard + +The classes inherit from the abstract base class `IR`. This has an array `.arr` +to contain the duration (in μs) of each carrier on or off period. The +`transmit` method calls a `tx` method of the subclass which populates this +array. This is done by two methods of the base class, `.append` and `.add`. The +former takes a list of times (in μs) and appends them to the array. A bound +variable `.carrier` keeps track of the notional on/off state of the carrier: +this is required for bi-phase (manchester) codings. + +The `.add` method takes a single μs time value and adds it to the last value +in the array: this pulse lengthening is used in bi-phase encodings. + +On completion of the subclass `.tx`, `.transmit` appends a special `STOP` value +and initiates physical transmission which occurs in an interrupt context. + +This is performed by two hardware timers initiated in the constructor. Timer 2, +channel 1 is used to configure the output pin as a PWM channel. Its frequency +is set in the constructor. The OOK is performed by dynamically changing the +duty ratio using the timer channel's `pulse_width_percent` method: this varies +the pulse width from 0 to a duty ratio passed to the constructor. The NEC +protocol defaults to 50%, the Sony and Philips ones to 30%. + +The duty ratio is changed by the Timer 5 callback `._cb`. This retrieves the +next duration from the array. If it is not `STOP` it toggles the duty cycle +and re-initialises T5 for the new duration. + +## 5.2 ESP32 + +This is something of a hack because my drivers work with standard firmware. + +A much better solution will be possible when the `esp32.RMT` class supports the +`carrier` option. A fork supporting this is +[here](https://github.com/mattytrentini/micropython). You may want to adapt the +base class to use this fork: it should be easy and would produce a solution +capable of handling all protocols. + +A consequence of this hack is that timing is imprecise. In testing NEC +protocols were reliable. Sony delivered some erroneous bitsreams but may be +usable. Philips protocols require timing precision which is unachievable; these +are unsupported and an exception will be thrown on an attempt to instantiate a +Philips class on an ESP32. + +The ABC stores durations in Hz rather than in μs. This is because the `period` +arg of `Timer.init` expects an integer number of ms. Passing a `freq` value +enables slightly higher resolution timing. In practice timing lacks precision +with the code having a hack which subtracts a nominal amount from each value to +compensate for the typical level of overrun. + +The carrier is generated by PWM instance `.pwm` with its duty cycle controlled +by software timer `._tim` in a similar way to the Pyboard Timer 5 described +above. The ESP32 duty value is in range 0-1023 as against 0-100 on the Pyboard. + +# 6. References + +[General information about IR](https://www.sbprojects.net/knowledge/ir/) + +The NEC protocol: +[altium](http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol) +[circuitvalley](http://www.circuitvalley.com/2013/09/nec-protocol-ir-infrared-remote-control.html) + +Philips protocols: +[RC5](https://en.wikipedia.org/wiki/RC-5) +[RC5](https://www.sbprojects.net/knowledge/ir/rc5.php) +[RC6](https://www.sbprojects.net/knowledge/ir/rc6.php) + +Sony protocol: +[SIRC](https://www.sbprojects.net/knowledge/ir/sirc.php) diff --git a/ir_rx/test.py b/ir_rx/test.py index 354a656..112db2e 100644 --- a/ir_rx/test.py +++ b/ir_rx/test.py @@ -32,7 +32,7 @@ def cb(data, addr, ctrl): else: print('Data {:02x} Addr {:04x} Ctrl {:02x}'.format(data, addr, ctrl)) -def run(proto=0): +def test(proto=0): classes = (NEC_8, NEC_16, SONY_12, SONY_15, SONY_20, RC5_IR, RC6_M0) ir = classes[proto](p, cb) # Instantiate receiver ir.error_function(print_error) # Show debug information @@ -46,13 +46,13 @@ def run(proto=0): # **** DISPLAY GREETING **** s = '''Test for IR receiver. Run: from ir_rx import test -test.run() for NEC 8 bit protocol, -test.run(1) for NEC 16 bit, -test.run(2) for Sony SIRC 12 bit, -test.run(3) for Sony SIRC 15 bit, -test.run(4) for Sony SIRC 20 bit, -test.run(5) for Philips RC-5 protocol, -test.run(6) for RC6 mode 0. +test() for NEC 8 bit protocol, +test(1) for NEC 16 bit, +test(2) for Sony SIRC 12 bit, +test(3) for Sony SIRC 15 bit, +test(4) for Sony SIRC 20 bit, +test(5) for Philips RC-5 protocol, +test(6) for RC6 mode 0. Hit ctrl-c to stop, then ctrl-d to soft reset.''' diff --git a/ir_rx_test.py b/ir_rx_test.py deleted file mode 100644 index badd2b5..0000000 --- a/ir_rx_test.py +++ /dev/null @@ -1,59 +0,0 @@ -# ir_rx_test.py Test program for IR remote control decoder arem.py -# Supports Pyboard and ESP8266 - -# Author: Peter Hinch -# Copyright Peter Hinch 2020 Released under the MIT license - -# Run this to characterise a remote. - -from sys import platform -import time -import gc -from machine import Pin, freq -from ir_rx.print_error import print_error # Optional print of error codes -# Import all implemented classes -from ir_rx.nec import NEC_8, NEC_16 -from ir_rx.sony import SONY_12, SONY_15, SONY_20 -from ir_rx.philips import RC5_IR, RC6_M0 - -# Define pin according to platform -if platform == 'pyboard': - p = Pin('X3', Pin.IN) -elif platform == 'esp8266': - freq(160000000) - p = Pin(13, Pin.IN) -elif platform == 'esp32' or platform == 'esp32_LoBo': - p = Pin(23, Pin.IN) # was 27 - -# User callback -def cb(data, addr, ctrl): - if data < 0: # NEC protocol sends repeat codes. - print('Repeat code.') - else: - print('Data {:02x} Addr {:04x} Ctrl {:02x}'.format(data, addr, ctrl)) - -def test(proto=0): - classes = (NEC_8, NEC_16, SONY_12, SONY_15, SONY_20, RC5_IR, RC6_M0) - ir = classes[proto](p, cb) # Instantiate receiver - ir.error_function(print_error) # Show debug information - #ir.verbose = True - # A real application would do something here... - while True: - print('running') - time.sleep(5) - gc.collect() - -# **** DISPLAY GREETING **** -s = '''Test for IR receiver. Run: -from ir_rx_test import test -test() for NEC 8 bit protocol, -test(1) for NEC 16 bit, -test(2) for Sony SIRC 12 bit, -test(3) for Sony SIRC 15 bit, -test(4) for Sony SIRC 20 bit, -test(5) for Philips RC-5 protocol, -test(6) for RC6 mode 0. - -Hit ctrl-c to stop, then ctrl-d to soft reset.''' - -print(s) diff --git a/ir_tx.py b/ir_tx.py deleted file mode 100644 index 4a91336..0000000 --- a/ir_tx.py +++ /dev/null @@ -1,175 +0,0 @@ -# ir_tx.py Nonblocking IR blaster -# Runs on Pyboard D or Pyboard 1.x only (not Pyboard Lite) - -# Released under the MIT License (MIT). See LICENSE. - -# Copyright (c) 2020 Peter Hinch - -from pyb import Pin, Timer -from micropython import const -from array import array -import micropython - -# micropython.alloc_emergency_exception_buf(100) - -# Common -_SPACE = const(0) # Or 100. Depends on wiring: 0 assumes logic 0 turns IR off. -_STOP = const(0) # End of data -# NEC -_TBURST = const(563) -_T_ONE = const(1687) -# RC5 -_T_RC5 = const(889) # Time for pulse of carrier -# RC6_M0 -_T_RC6 = const(444) -_T2_RC6 = const(889) - -# IR abstract base class. Array holds periods in μs between toggling 36/38KHz -# carrier on or off. Physical transmission occurs in an ISR context controlled -# by timer 2 and timer 5. See README.md for details of operation. -class IR: - - def __init__(self, pin, freq, asize, duty, verbose): - tim = Timer(2, freq=freq) # Timer 2/pin produces 36/38KHz carrier - self._ch = tim.channel(1, Timer.PWM, pin=pin) - self._ch.pulse_width_percent(_SPACE) # Turn off IR LED - self._duty = duty if not _SPACE else (100 - duty) - self._tim = Timer(5) # Timer 5 controls carrier on/off times - self._tcb = self.cb # Pre-allocate - self.verbose = verbose - self.arr = array('H', 0 for _ in range(asize)) # on/off times (μs) - self.carrier = False # Notional carrier state while encoding biphase - self.aptr = 0 # Index into array - - # Before populating array, zero pointer, set notional carrier state (off). - def transmit(self, addr, data, toggle=0): # NEC: toggle is unused - self.aptr = 0 # Inital conditions for tx: index into array - self.carrier = False - self.tx(addr, data, toggle) - self.append(_STOP) - self.aptr = 0 # Reset pointer - self.cb(self._tim) # Initiate physical transmission. - - def cb(self, t): # T5 callback, generate a carrier mark or space - t.deinit() - p = self.aptr - v = self.arr[p] - if v == _STOP: - self._ch.pulse_width_percent(_SPACE) # Turn off IR LED. - return - self._ch.pulse_width_percent(_SPACE if p & 1 else self._duty) - self._tim.init(prescaler=84, period=v, callback=self._tcb) - self.aptr += 1 - - def append(self, *times): # Append one or more time peiods to .arr - for t in times: - self.arr[self.aptr] = t - self.aptr += 1 - self.carrier = not self.carrier # Keep track of carrier state - self.verbose and print('append', t, 'carrier', self.carrier) - - def add(self, t): # Increase last time value - self.verbose and print('add', t) - self.arr[self.aptr - 1] += t # Carrier unaffected - -# NEC protocol -class NEC(IR): - - def __init__(self, pin, freq=38000, verbose=False): # NEC specifies 38KHz - super().__init__(pin, freq, 68, 50, verbose) - - def _bit(self, b): - self.append(_TBURST, _T_ONE if b else _TBURST) - - def tx(self, addr, data, _): # Ignore toggle - self.append(9000, 4500) - if addr < 256: # Short address: append complement - addr |= ((addr ^ 0xff) << 8) - for _ in range(16): - self._bit(addr & 1) - addr >>= 1 - data |= ((data ^ 0xff) << 8) - for _ in range(16): - self._bit(data & 1) - data >>= 1 - self.append(_TBURST) - - def repeat(self): - self.aptr = 0 - self.append(9000, 2250, _TBURST, _STOP) - self.aptr = 0 # Reset pointer - self.cb(self._tim) # Initiate physical transmission. - -# NEC protocol -class SONY(IR): - - def __init__(self, pin, bits=12, freq=40000, verbose=False): # Sony specifies 40KHz - super().__init__(pin, freq, 3 + bits * 2, 30, verbose) - if bits not in (12, 15, 20): - raise ValueError('bits must be 12, 15 or 20.') - self.bits = bits - - def tx(self, addr, data, ext): - self.append(2400, 600) - bits = self.bits - v = data & 0x7f - if bits == 12: - v |= (addr & 0x1f) << 7 - elif bits == 15: - v |= (addr & 0xff) << 7 - else: - v |= (addr & 0x1f) << 7 - v |= (ext & 0xff) << 12 - for _ in range(bits): - self.append(1200 if v & 1 else 600, 600) - v >>= 1 - - -# Philips RC5 protocol -class RC5(IR): - - def __init__(self, pin, freq=36000, verbose=False): - super().__init__(pin, freq, 28, 30, verbose) - - def tx(self, addr, data, toggle): - d = (data & 0x3f) | ((addr & 0x1f) << 6) | ((data & 0x40) << 6) | ((toggle & 1) << 11) - self.verbose and print(bin(d)) - mask = 0x2000 - while mask: - if mask == 0x2000: - self.append(_T_RC5) - else: - bit = bool(d & mask) - if bit ^ self.carrier: - self.add(_T_RC5) - self.append(_T_RC5) - else: - self.append(_T_RC5, _T_RC5) - mask >>= 1 - -# Philips RC6 mode 0 protocol -class RC6_M0(IR): - - def __init__(self, pin, freq=36000, verbose=False): - super().__init__(pin, freq, 44, 30, verbose) - - def tx(self, addr, data, toggle): - # leader, 1, 0, 0, 0 - self.append(2666, _T2_RC6, _T_RC6, _T2_RC6, _T_RC6, _T_RC6, _T_RC6, _T_RC6, _T_RC6) - # Append a single bit of twice duration - if toggle: - self.add(_T2_RC6) - self.append(_T2_RC6) - else: - self.append(_T2_RC6, _T2_RC6) - d = (data & 0xff) | ((addr & 0xff) << 8) - mask = 0x8000 - self.verbose and print('toggle', toggle, self.carrier, bool(d & mask)) - while mask: - bit = bool(d & mask) - if bit ^ self.carrier: - self.append(_T_RC6, _T_RC6) - else: - self.add(_T_RC6) - self.append(_T_RC6) - mask >>= 1 diff --git a/ir_tx/__init__.py b/ir_tx/__init__.py new file mode 100644 index 0000000..ef52e84 --- /dev/null +++ b/ir_tx/__init__.py @@ -0,0 +1,113 @@ +# __init__.py Nonblocking IR blaster +# Runs on Pyboard D or Pyboard 1.x (not Pyboard Lite) and ESP32 + +# Released under the MIT License (MIT). See LICENSE. + +# Copyright (c) 2020 Peter Hinch +from sys import platform +ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' +if ESP32: + from machine import Pin, Timer, PWM, freq +else: + from pyb import Pin, Timer # Pyboard does not support machine.PWM + +from micropython import const +from array import array +import micropython + +# micropython.alloc_emergency_exception_buf(100) + +# ABC only +_SPACE = const(0) +# If the wiring is such that 3.3V turns the LED off, set _SPACE as follows +# On Pyboard 100, on ESP32 1023 +# Shared by NEC +STOP = const(0) # End of data + +# IR abstract base class. Array holds periods in μs between toggling 36/38KHz +# carrier on or off. Physical transmission occurs in an ISR context controlled +# by timer 2 and timer 5. See README.md for details of operation. +class IR: + + def __init__(self, pin, cfreq, asize, duty, verbose): + if ESP32: + freq(240000000) + self._pwm = PWM(pin) # Produces 36/38/40KHz carrier + self._pwm.deinit() + self._pwm.init(freq=cfreq, duty=_SPACE) + # ESP32: 0 <= duty <= 1023 + self._duty = round((duty if not _SPACE else (100 - duty)) * 10.23) + self._tim = Timer(-1) # Controls carrier on/off times + self._off = self.esp_off # Turn IR LED off + self._onoff = self.esp_onoff # Set IR LED state and refresh timer + else: # Pyboard + tim = Timer(2, freq=cfreq) # Timer 2/pin produces 36/38/40KHz carrier + self._ch = tim.channel(1, Timer.PWM, pin=pin) + self._ch.pulse_width_percent(_SPACE) # Turn off IR LED + # Pyboard: 0 <= pulse_width_percent <= 100 + self._duty = duty if not _SPACE else (100 - duty) + self._tim = Timer(5) # Timer 5 controls carrier on/off times + self._off = self.pb_off + self._onoff = self.pb_onoff + self._tcb = self.cb # Pre-allocate + self.verbose = verbose + self.arr = array('H', 0 for _ in range(asize)) # on/off times (μs) + self.carrier = False # Notional carrier state while encoding biphase + self.aptr = 0 # Index into array + + # Before populating array, zero pointer, set notional carrier state (off). + def transmit(self, addr, data, toggle=0): # NEC: toggle is unused + self.aptr = 0 # Inital conditions for tx: index into array + self.carrier = False + self.tx(addr, data, toggle) + self.append(STOP) + self.aptr = 0 # Reset pointer + self.cb(self._tim) # Initiate physical transmission. + + # Turn IR LED off (pyboard and ESP32 variants) + def pb_off(self): + self._ch.pulse_width_percent(_SPACE) + + def esp_off(self): + self._pwm.duty(_SPACE) + + # Turn IR LED on or off and re-initialise timer (pyboard and ESP32 variants) + @micropython.native + def pb_onoff(self, p, v): + self._ch.pulse_width_percent(_SPACE if p & 1 else self._duty) + self._tim.init(prescaler=84, period=v, callback=self._tcb) + + @micropython.native + def esp_onoff(self, p, v): + self._pwm.duty(_SPACE if p & 1 else self._duty) + self._tim.init(mode=Timer.ONE_SHOT, freq=v, callback=self.cb) + + def cb(self, t): # T5 callback, generate a carrier mark or space + t.deinit() + p = self.aptr + v = self.arr[p] + if v == STOP: + self._off() # Turn off IR LED. + return + self._onoff(p, v) + self.aptr += 1 + + def append(self, *times): # Append one or more time peiods to .arr + for t in times: + if ESP32 and t: + t -= 350 # ESP32 sluggishness + t = round(1_000_000 / t) # Store in Hz + self.arr[self.aptr] = t + self.aptr += 1 + self.carrier = not self.carrier # Keep track of carrier state + self.verbose and print('append', t, 'carrier', self.carrier) + + def add(self, t): # Increase last time value + assert t > 0 + self.verbose and print('add', t) + # .carrier unaffected + if ESP32: + t -= 350 + self.arr[self.aptr - 1] = round((self.arr[self.aptr - 1] / 1_000_000 + t) / 1_000_000) + else: + self.arr[self.aptr - 1] += t diff --git a/ir_tx/nec.py b/ir_tx/nec.py new file mode 100644 index 0000000..8cf39e7 --- /dev/null +++ b/ir_tx/nec.py @@ -0,0 +1,38 @@ +# nec.py Encoder for IR remote control using synchronous code +# NEC protocol. + +# Author: Peter Hinch +# Copyright Peter Hinch 2020 Released under the MIT license + +from micropython import const +from ir_tx import IR, STOP + +_TBURST = const(563) +_T_ONE = const(1687) + +class NEC(IR): + + def __init__(self, pin, freq=38000, verbose=False): # NEC specifies 38KHz + super().__init__(pin, freq, 68, 50, verbose) + + def _bit(self, b): + self.append(_TBURST, _T_ONE if b else _TBURST) + + def tx(self, addr, data, _): # Ignore toggle + self.append(9000, 4500) + if addr < 256: # Short address: append complement + addr |= ((addr ^ 0xff) << 8) + for _ in range(16): + self._bit(addr & 1) + addr >>= 1 + data |= ((data ^ 0xff) << 8) + for _ in range(16): + self._bit(data & 1) + data >>= 1 + self.append(_TBURST) + + def repeat(self): + self.aptr = 0 + self.append(9000, 2250, _TBURST, STOP) + self.aptr = 0 # Reset pointer + self.cb(self._tim) # Initiate physical transmission. diff --git a/ir_tx/philips.py b/ir_tx/philips.py new file mode 100644 index 0000000..b069361 --- /dev/null +++ b/ir_tx/philips.py @@ -0,0 +1,68 @@ +# philips.py Encoder for IR remote control using synchronous code +# RC-5 and RC-6 mode 0 protocols. + +# Author: Peter Hinch +# Copyright Peter Hinch 2020 Released under the MIT license + +from micropython import const +from sys import platform +ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' +from ir_tx import IR + +# Philips RC5 protocol +_T_RC5 = const(889) # Time for pulse of carrier +ermsg = 'ESP32 does not support Philips protocols' +class RC5(IR): + + def __init__(self, pin, freq=36000, verbose=False): + if ESP32: + raise RuntimeError(ermsg) + super().__init__(pin, freq, 28, 30, verbose) + + def tx(self, addr, data, toggle): + d = (data & 0x3f) | ((addr & 0x1f) << 6) | ((data & 0x40) << 6) | ((toggle & 1) << 11) + self.verbose and print(bin(d)) + mask = 0x2000 + while mask: + if mask == 0x2000: + self.append(_T_RC5) + else: + bit = bool(d & mask) + if bit ^ self.carrier: + self.add(_T_RC5) + self.append(_T_RC5) + else: + self.append(_T_RC5, _T_RC5) + mask >>= 1 + +# Philips RC6 mode 0 protocol +_T_RC6 = const(444) +_T2_RC6 = const(889) + +class RC6_M0(IR): + + def __init__(self, pin, freq=36000, verbose=False): + if ESP32: + raise RuntimeError(ermsg) + super().__init__(pin, freq, 44, 30, verbose) + + def tx(self, addr, data, toggle): + # leader, 1, 0, 0, 0 + self.append(2666, _T2_RC6, _T_RC6, _T2_RC6, _T_RC6, _T_RC6, _T_RC6, _T_RC6, _T_RC6) + # Append a single bit of twice duration + if toggle: + self.add(_T2_RC6) + self.append(_T2_RC6) + else: + self.append(_T2_RC6, _T2_RC6) + d = (data & 0xff) | ((addr & 0xff) << 8) + mask = 0x8000 + self.verbose and print('toggle', toggle, self.carrier, bool(d & mask)) + while mask: + bit = bool(d & mask) + if bit ^ self.carrier: + self.append(_T_RC6, _T_RC6) + else: + self.add(_T_RC6) + self.append(_T_RC6) + mask >>= 1 diff --git a/ir_tx/sony.py b/ir_tx/sony.py new file mode 100644 index 0000000..86a23dd --- /dev/null +++ b/ir_tx/sony.py @@ -0,0 +1,45 @@ +# sony.py Encoder for IR remote control using synchronous code +# Sony SIRC protocol. + +# Author: Peter Hinch +# Copyright Peter Hinch 2020 Released under the MIT license + +from micropython import const +from ir_tx import IR + +class SONY_ABC(IR): + + def __init__(self, pin, bits, freq, verbose): + super().__init__(pin, freq, 3 + bits * 2, 30, verbose) + if bits not in (12, 15, 20): + raise ValueError('bits must be 12, 15 or 20.') + self.bits = bits + + def tx(self, addr, data, ext): + self.append(2400, 600) + bits = self.bits + v = data & 0x7f + if bits == 12: + v |= (addr & 0x1f) << 7 + elif bits == 15: + v |= (addr & 0xff) << 7 + else: + v |= (addr & 0x1f) << 7 + v |= (ext & 0xff) << 12 + for _ in range(bits): + self.append(1200 if v & 1 else 600, 600) + v >>= 1 + +# Sony specifies 40KHz +class SONY_12(SONY_ABC): + def __init__(self, pin, freq=40000, verbose=False): + super().__init__(pin, 12, freq, verbose) + +class SONY_15(SONY_ABC): + def __init__(self, pin, freq=40000, verbose=False): + super().__init__(pin, 15, freq, verbose) + +class SONY_20(SONY_ABC): + def __init__(self, pin, freq=40000, verbose=False): + super().__init__(pin, 20, freq, verbose) + diff --git a/ir_tx_test.py b/ir_tx/test.py similarity index 59% rename from ir_tx_test.py rename to ir_tx/test.py index ad55a22..bb54329 100644 --- a/ir_tx_test.py +++ b/ir_tx/test.py @@ -5,11 +5,18 @@ # Copyright (c) 2020 Peter Hinch # Implements a 2-button remote control on a Pyboard with auto repeat. - -from pyb import Pin, LED +from sys import platform +ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' +if ESP32: + from machine import Pin +else: + from pyb import Pin, LED import uasyncio as asyncio from aswitch import Switch, Delay_ms -from ir_tx import NEC, SONY, RC5, RC6_M0 +# Import all implemented classes +from ir_tx.nec import NEC +from ir_tx.sony import SONY_12, SONY_15, SONY_20 +from ir_tx.philips import RC5, RC6_M0 loop = asyncio.get_event_loop() @@ -48,40 +55,50 @@ async def main(proto): # Test uses a 38KHz carrier. Some Philips systems use 36KHz. # If button is held down normal behaviour is to retransmit # but most NEC models send a REPEAT code - rep_code = False # Rbutton constructor requires False for RC-X. NEC protocol only. - pin = Pin('X1') - if not proto: - irb = NEC(pin) # Default NEC freq == 38KHz - # Option to send REPEAT code. Most remotes do this. - rep_code = True - elif proto < 4: - bits = (12, 15, 20)[proto - 1] - irb = SONY(pin, bits, 38000) # My decoder chip is 38KHz - elif proto == 5: - irb = RC5(pin, 38000) # My decoder chip is 38KHz - elif proto == 6: - irb = RC6_M0(pin, 38000) + rep_code = proto == 0 # Rbutton constructor requires False for RC-X. NEC protocol only. + pin = Pin(23, Pin.OUT) if ESP32 else Pin('X1') + classes = (NEC, SONY_12, SONY_15, SONY_20, RC5, RC6_M0) + irb = classes[proto](pin, 38000) # My decoder chip is 38KHz b = [] # Rbutton instances - b.append(Rbutton(irb, Pin('X3', Pin.IN, Pin.PULL_UP), 0x1, 0x7, rep_code)) - b.append(Rbutton(irb, Pin('X4', Pin.IN, Pin.PULL_UP), 0x10, 0xb, rep_code)) - led = LED(1) - while True: - await asyncio.sleep_ms(500) # Obligatory flashing LED. - led.toggle() + px3 = Pin(18, Pin.IN, Pin.PULL_UP) if ESP32 else Pin('X3', Pin.IN, Pin.PULL_UP) + px4 = Pin(19, Pin.IN, Pin.PULL_UP) if ESP32 else Pin('X4', Pin.IN, Pin.PULL_UP) + b.append(Rbutton(irb, px3, 0x1, 0x7, rep_code)) + b.append(Rbutton(irb, px4, 0x10, 0xb, rep_code)) + if ESP32: + while True: + print('Running') + await asyncio.sleep(5) + else: + led = LED(1) + while True: + await asyncio.sleep_ms(500) # Obligatory flashing LED. + led.toggle() s = '''Test for IR transmitter. Run: from ir_tx_test import test test() for NEC protocol test(1) for Sony SIRC 12 bit test(2) for Sony SIRC 15 bit -test(3) for Sony SIRC 20 bit +test(3) for Sony SIRC 20 bit''' +spb = ''' test(5) for Philips RC-5 protocol test(6) for Philips RC-6 mode 0. -Ground X3 to send addr 1 data 7 -Ground X4 to send addr 0x10 data 0x0b.''' +IR LED on pin X1 +Ground pin X3 to send addr 1 data 7 +Ground pin X4 to send addr 0x10 data 0x0b.''' +sesp = ''' + +IR LED on pin 23 +Ground pin 18 to send addr 1 data 7 +Ground pin 19 to send addr 0x10 data 0x0b.''' +if ESP32: + s = ''.join((s, sesp)) +else: + s = ''.join((s, spb)) print(s) + def test(proto=0): loop.run_until_complete(main(proto))