]> vault307.fbx.one Git - micorpython_ir.git/commitdiff
RMT tested.
authorPeter Hinch <peter@hinch.me.uk>
Wed, 11 Mar 2020 11:39:58 +0000 (11:39 +0000)
committerPeter Hinch <peter@hinch.me.uk>
Wed, 11 Mar 2020 11:39:58 +0000 (11:39 +0000)
TRANSMITTER.md
images/circuits.fzz
images/gate.png [new file with mode: 0644]
ir_rx/test.py
ir_tx/__init__.py
ir_tx/nec.py
ir_tx/philips.py
ir_tx/test.py

index 88c3ed794b8b53e973565784d0398e3a348aa279..8a8331414ee1d726e1ddb87e1ec0b40da2ff1211 100644 (file)
@@ -5,7 +5,7 @@
 # 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
+Output is via an IR LED which needs a simple circuit 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).
 
@@ -13,10 +13,10 @@ 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).
+On ESP32 the demo uses pins 21 and 23 for IR output and pins 18 and 19 for
+pushbuttons. These pins may be changed.
 
-## 1.1 Wiring
+## 1.1 Pyboard Wiring
 
 I use the following circuit which delivers just under 40mA to the diode. R2 may
 be reduced for higher current.  
@@ -33,20 +33,40 @@ 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
+## 1.2 ESP32 Wiring
 
-The transmitter is a Python package. This minimises RAM usage: applications
-only import the device driver for the protocol in use.
+The ESP32 RMT device does not currently support the carrier option. A simple
+hardware gate is required to turn the IR LED on when both the carrier pin and
+the RMT pin are high. A suitable circuit is below.  
+![Image](images/gate.png)
 
-Copy the following to the target filesystem:
- 1. `ir_tx` Directory and contents.
+The transistor type is not critical. A gate could be built with two similarly
+connected N-channel MOSFETS. The 1KΩ resistors would not be required. The
+MOSFETS would require a low RDSon at Vgs == 3.3V. A ZVN4210A seems suitable
+but I haven't tried it.
+
+# 2. Dependencies and installation
+
+## 2.1 Dependencies
 
 The device driver has no dependencies.
 
+On ESP32 a firmware version >= V1.12 is required. The Loboris port is not
+supported owing to the need for the RMT device.
+
 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:
+from [this repo](https://github.com/peterhinch/micropython-async).
+
+## 2.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 demo is of a 2-button remote controller with auto-repeat. It may be run by
+issuing:
 ```python
 from ir_tx.test import test
 ```
@@ -61,10 +81,6 @@ It implements a class for each supported protocol, namely `NEC`, `SONY_12`,
 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).
-
 #### Common to all classes
 
 Constructor args:  
@@ -112,9 +128,6 @@ value.
 
 #### Philips classes
 
-These are only supported on Pyboard hosts. An `RuntimeError` will be thrown on
-an attempt to instantiate a Philips class on an ESP32.
-
 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.
 
@@ -124,17 +137,15 @@ 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
+# 4. Principle of operation
 
-## 5.1 Pyboard
+## 4.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
+former takes a list of times (in ) 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.
 
@@ -155,32 +166,17 @@ 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.
+## 4.2 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 output continuously at the specified duty ratio. A pulse train
+generated by the RMT instance drives a hardware gate such that the IR LED is
+lit only when both carrier and RMT are high.
 
-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.
+The carrier is generated by PWM instance `.pwm` running continuously. The ABC
+constructor converts the 0-100 duty ratio specified by the subclass to the
+0-1023 range used by ESP32.
 
-# 6. References
+# 5. References
 
 [General information about IR](https://www.sbprojects.net/knowledge/ir/)
 
index 9ad4333f0e9e99873c79b027a9a509527c339da7..ad48376633883df0873fd8ea95d6eae757485228 100644 (file)
Binary files a/images/circuits.fzz and b/images/circuits.fzz differ
diff --git a/images/gate.png b/images/gate.png
new file mode 100644 (file)
index 0000000..31b11f2
Binary files /dev/null and b/images/gate.png differ
index c3c71602845986bc372b309c68b2c5e9fa746fc6..112db2ee9cc3291fe07a6d1b12981ea536fb81c5 100644 (file)
@@ -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
index ef52e84a3cccc8126a56d8968dc7dd9aa68216ba..6fe771048d3d535c921d6aec0185580b70609f0f 100644 (file)
@@ -5,9 +5,10 @@
 
 # Copyright (c) 2020 Peter Hinch
 from sys import platform
-ESP32 = platform == 'esp32' or platform == 'esp32_LoBo'
+ESP32 = platform == 'esp32'  # Loboris not supported owing to RMT
 if ESP32:
-    from machine import Pin, Timer, PWM, freq
+    from machine import Pin, PWM
+    from esp32 import RMT
 else:
     from pyb import Pin, Timer  # Pyboard does not support machine.PWM
 
@@ -17,10 +18,12 @@ import micropython
 
 # micropython.alloc_emergency_exception_buf(100)
 
-# ABC only
+# ABC and Pyboard only: ESP32 ignores this value.
+# Duty ratio in carrier off state: if driver is such that 3.3V turns the LED
+# off, set _SPACE = 100
 _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
+# On ESP32 gate hardware design is led_on = rmt and carrier
+
 # Shared by NEC
 STOP = const(0)  # End of data
 
@@ -31,15 +34,11 @@ 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 = PWM(pin[0])  # Continuous 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
+            self._pwm.init(freq=cfreq, duty=round(duty * 10.23))
+            self._rmt = RMT(0, pin=pin[1], clock_div=80)  # 1μs resolution
         else:  # Pyboard
             tim = Timer(2, freq=cfreq)  # Timer 2/pin produces 36/38/40KHz carrier
             self._ch = tim.channel(1, Timer.PWM, pin=pin)
@@ -47,67 +46,51 @@ class IR:
             # 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._tcb = self._cb  # Pre-allocate
+        self._arr = array('H', 0 for _ in range(asize))  # on/off times (μs)
+        self._mva = memoryview(self._arr)
+        # Subclass interface
         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
+    def _cb(self, t):  # T5 callback, generate a carrier mark or space
         t.deinit()
         p = self.aptr
-        v = self.arr[p]
+        v = self._arr[p]
         if v == STOP:
-            self._off()  # Turn off IR LED.
+            self._ch.pulse_width_percent(_SPACE)  # Turn off IR LED.
             return
-        self._onoff(p, v)
+        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
+    # Public interface
+    # 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)  # Subclass populates ._arr
+        self.trigger()  # Initiate transmission
+
+    # Subclass interface
+    def trigger(self):  # Used by NEC to initiate a repeat frame
+        if ESP32:
+            self._rmt.write_pulses(tuple(self._mva[0 : self.aptr]), start = 1)
+        else:
+            self.append(STOP)
+            self.aptr = 0  # Reset pointer
+            self._cb(self._tim)  # Initiate physical transmission.
+
+    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._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
+    def add(self, t):  # Increase last time value (for biphase)
         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
+        self._arr[self.aptr - 1] += t
index 8cf39e70ca79b462cf5d5438959627ef40e0d9b8..1e02c9f9068acb74b442dfd2f85e2399ce47fac6 100644 (file)
@@ -33,6 +33,5 @@ class NEC(IR):
 
     def repeat(self):
         self.aptr = 0
-        self.append(9000, 2250, _TBURST, STOP)
-        self.aptr = 0  # Reset pointer
-        self.cb(self._tim)  # Initiate physical transmission.
+        self.append(9000, 2250, _TBURST)
+        self.trigger()  # Initiate physical transmission.
index b0693611a8d2ab6c8e63301b293e3fc284a41348..a37dd5b282217c0ed5bce634af432905ba95544f 100644 (file)
@@ -6,7 +6,6 @@
 
 from micropython import const
 from sys import platform
-ESP32 = platform == 'esp32' or platform == 'esp32_LoBo'
 from ir_tx import IR
 
 # Philips RC5 protocol
@@ -15,8 +14,6 @@ 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):
@@ -42,8 +39,6 @@ _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):
index bb54329affaf379001b6876ab6eccf6da660abd0..b175dcd2ef8fec1a87be167140448debd5df45b4 100644 (file)
@@ -6,7 +6,7 @@
 
 # Implements a 2-button remote control on a Pyboard with auto repeat.
 from sys import platform
-ESP32 = platform == 'esp32' or platform == 'esp32_LoBo'
+ESP32 = platform == 'esp32'
 if ESP32:
     from machine import Pin
 else:
@@ -56,7 +56,10 @@ async def main(proto):
     # If button is held down normal behaviour is to retransmit
     # but most NEC models send a REPEAT code
     rep_code = proto == 0  # Rbutton constructor requires False for RC-X. NEC protocol only.
-    pin = Pin(23, Pin.OUT) if ESP32 else Pin('X1')
+    if ESP32:  # Pins for IR LED gate
+        pin = (Pin(23, Pin.OUT, value = 0), Pin(21, Pin.OUT, value = 0))
+    else:
+        pin = Pin('X1')
     classes = (NEC, SONY_12, SONY_15, SONY_20, RC5, RC6_M0)
     irb = classes[proto](pin, 38000)  # My decoder chip is 38KHz
 
@@ -75,30 +78,30 @@ async def main(proto):
             await asyncio.sleep_ms(500)  # Obligatory flashing LED.
             led.toggle()
 
+# Greeting strings. Common:
 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'''
-spb = '''
-test(5) for Philips RC-5 protocol
-test(6) for Philips RC-6 mode 0.
+test(3) for Sony SIRC 20 bit
+test(4) for Philips RC-5 protocol
+test(5) for Philips RC-6 mode 0.
+'''
 
+# Pyboard:
+spb = '''
 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
+# ESP32
+sesp = '''
+IR LED gate on pins 23, 21
 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)
 
+print(''.join((s, sesp)) if ESP32 else ''.join((s, spb)))
 
 def test(proto=0):
     loop.run_until_complete(main(proto))