arendst / Tasmota

Alternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at
https://tasmota.github.io/docs
GNU General Public License v3.0
21.73k stars 4.72k forks source link

Berry add gpio.set_freq() #21375

Closed Staars closed 2 months ago

Staars commented 2 months ago

Description:

Adds function to set the PWM frequency to Berry's gpio module. Adds ~70 bytes of flash.

Usage:

gpio.set_pwm_freq(pin,frequency)

Can be used to play sounds via a cheap mainboard speaker to signal status, failure and so on.

Complex driver example to play RTTTL ringtones in the comments.

Checklist:

NOTE: The code change must pass CI tests. Your PR cannot be merged unless tests pass

Staars commented 2 months ago

RTTTL play in Berry with hardcoded pin setting for the Ulanzi pixel clock:

#-
 - RTTTL driver in Berry using gpio module
-#

class RTTTL
  # static song ="HauntedHouse: d=4,o=5,b=108: 2a4, 2e, 2d#, 2b4, 2a4, 2c, 2d, 2a#4, 2e., e, 1f4, 1a4, 1d#, 2e., d, 2c., b4, 1a4, 1p, 2a4, 2e, 2d#, 2b4, 2a4, 2c, 2d, 2a#4, 2e., e, 1f4, 1a4, 1d#, 2e., d, 2c., b4, 1a4"
  static song ="dkong:d=4,o=5,b=160:2c,8d.,d#.,c.,16b,16c6,16b,16c6,16b,16c6,16b,16c6,16b,16c6,16b,16c6,16b,2c6"
  # static song = "TakeOnMe:d=4,o=4,b=160:8f#5,8f#5,8f#5,8d5,8p,8b,8p,8e5,8p,8e5,8p,8e5,8g#5,8g#5,8a5,8b5,8a5,8a5,8a5,8e5,8p,8d5,8p,8f#5,8p,8f#5,8p,8f#5,8e5,8e5,8f#5,8e5,8f#5,8f#5,8f#5,8d5,8p,8b,8p,8e5,8p,8e5,8p,8e5,8g#5,8g#5,8a5,8b5,8a5,8a5,8a5,8e5,8p,8d5,8p,8f#5,8p,8f#5,8p,8f#5,8e5,8e5"
  var name
  var duration, octave, bpm, loop_ticks
  var track, pos, remaining
  var note_val, note_octave, note_left

  def init()
    import string
    import gpio
    import math

    if global.rtttl_pin == nil # never call this twice after boot
      gpio.pin_mode(15,gpio.OUTPUT) # PWM buzzer pin of the Ulanzi clock = 15
      global.rtttl_pin = true
    end
    var parts = string.split(self.song,":")
    self.name = parts[0]
    print("Loading song:",self.name)
    var header = parts[1]
    var pos = string.find(header,"d")
    if pos>-1 self.duration = int(header[pos+2]) end
    pos = string.find(header,"o")
    if pos>-1 self.octave = int(header[pos+2]) end
    pos = string.find(header,"b")
    if pos>-1 self.bpm = int(header[pos+2..]) end # a bit unsafe
    self.track = string.split(parts[2],",")
    var loop_ticks = (self.bpm/60.0)/self.duration/0.05 # still unsure about
    self.loop_ticks = loop_ticks
    if loop_ticks%8 > 0
      self.loop_ticks = (math.ceil(loop_ticks/8))*8
    end
    print("Note duration:",self.duration,"Octave:",self.octave,"BPM:",self.bpm,"->loops per note:",self.loop_ticks)
    self.pos = 0
    self.note_left = 0
    print("Track length:",size(self.track))
    tasmota.add_driver(self)
  end

  def note_to_int(note)
    # from esp32-hal-led.c - but we return no sharp and no flat
    #   C        C#       D        Eb       E        F       F#        G       G#        A       Bb        B
    if
      note == "c" return 0
    elif
      note == "d" return 2
    elif
      note == "e" return 4
    elif
      note == "f" return 5
    elif
      note == "g" return 7
    elif
      note == "a" return 9
    else # note == "b" 
      return 11
    end
  end

  def note_to_freq(note, octave)
    #               C        C#       D        Eb       E        F       F#        G       G#        A       Bb        B  from esp32-hal-led.c
    var freqs = [4186,    4435,    4699,    4978,    5274,    5588,    5920,    6272,    6645,    7040,    7459,    7902]    
    if (octave > 8 || note > 11)
        return 0
    end
    return  freqs[note] / (1 << (8-octave))
  end

  def is_number(char)
    import string
    if string.byte(char) > 0x29 && string.byte(char) < 0x3a
      return true
    end
    return false
  end

  def parse_note(note)
    var pos = 0
    var length = size(note)
    self.note_left = self.loop_ticks / self.duration # default
    self.note_octave = self.octave # default

    while note[pos] == " " pos+= 1 end # skip white space

    if self.is_number(note[pos])
      self.note_left = self.loop_ticks / int(note[pos])
      print("new length:",self.note_left)
      pos += 1
    end
    if self.is_number(note[pos])
      if note[pos] == '6'
        self.note_left = self.loop_ticks / 16 # this can only be the 6 of 16
      else
        self.note_left = self.loop_ticks / 32 # 32 is the fastest we support
      end
      print("new length:",self.note_left)
      pos += 1
    end

    self.note_val = self.note_to_int(note[pos]) # maybe the only value
    pos += 1
    if pos == length return end

    if note[pos] == "#" # sharp?
      self.note_val += 1
      if self.note_val > 11
        print("Invalid RTTTL !!")
        self.note_val = 11
      end
      pos+= 1 
      if pos == length return end
    end

    if note[pos] == "." # punctuated note length
       self.note_left = self.note_left + (self.note_left/2)
       print("punctuated length:",self.note_left)
       pos+= 1
       if pos == length return end
    end

    if self.is_number(note[pos])
      self.note_octave = int(note[pos])
      pos += 1
    end
  end

  def every_50ms()
    import gpio
    if self.note_left > 0
      self.note_left -= 1 # note still active
      return
    else
      self.pos += 1 # next note
    end
    if self.pos > size(self.track) - 1
      print("Song finished!")
      gpio.set_pwm(15,0) # stop
      tasmota.remove_driver(self)
      return
    end
    print("Will parse:", self.track[self.pos])
    self.parse_note(self.track[self.pos])
    print("Note, Octave, Length:",self.note_val,self.note_octave, self.note_left)
    var freq = self.note_to_freq(self.note_val,self.note_octave - 1)
    gpio.set_pwm_freq(15,freq) # stop
    gpio.set_pwm(15,30) # pretty low volume
  end

end

return RTTTL()

I can add a more refined example to the repo later.

sfromis commented 2 months ago

Nice feature :smile: Tested it with a speaker on a digital pin, and two sliders.

If I try to set the frequency lower than supported, I get this log message:

PWM: ledc_timer_config 3 failed ret=-1

I was hoping for gpio.set_pwm_freq to return a value about the failure, but alas...

Staars commented 2 months ago

I can understand the value of returning an error code, but as analogWriteFreq does only return void too, it is not super easy/super short to implement here. Maybe the whole code part (PWM related with Tasmotas extra feature phase) will be refactored some day and then we should not throw away the error codes. It is definitely worth it to do some work here in the future.