|
| 1 | +# TAS5805M DAC Driver for Berry/Tasmota |
| 2 | +# Based on: https://github.com/sonocotta/esp32-tas5805m-dac |
| 3 | + |
| 4 | +class TAS5805M |
| 5 | + # Louder-ESP32 I2S DAC Driver for TAS5805M |
| 6 | + static i2c_addr = 0x2D # Default I2C address |
| 7 | + static pdn_pin = 33 # Power Down Pin (GPIO 33) |
| 8 | + static fault_pin = 34 # Fault Pin (GPIO 34, optional) |
| 9 | + var i2c |
| 10 | + |
| 11 | + # Register definitions |
| 12 | + static PAGE_REG = 0x00 |
| 13 | + static RESET_REG = 0x01 |
| 14 | + static DEVICE_CTRL_1 = 0x02 |
| 15 | + static DEVICE_CTRL_2 = 0x03 |
| 16 | + static SIG_CH_CTRL = 0x28 |
| 17 | + static SAP_CTRL1 = 0x33 |
| 18 | + static SAP_CTRL2 = 0x34 |
| 19 | + static SAP_CTRL3 = 0x35 |
| 20 | + static FS_MON = 0x37 |
| 21 | + static BCK_MON = 0x38 |
| 22 | + static CLKDET_STATUS = 0x39 |
| 23 | + static DIG_VOL_CTRL = 0x4C |
| 24 | + static DIG_VOL_CTRL2 = 0x4D |
| 25 | + static DIG_VOL_CTRL3 = 0x4E |
| 26 | + static AUTO_MUTE_CTRL = 0x50 |
| 27 | + static AUTO_MUTE_TIME = 0x51 |
| 28 | + static ANA_CTRL = 0x53 |
| 29 | + static AGAIN_CTRL = 0x54 |
| 30 | + static BQ_WR_CTRL1 = 0x5C |
| 31 | + static ADR_PIN_CTRL = 0x60 |
| 32 | + static ADR_PIN_CONFIG = 0x61 |
| 33 | + static DSP_MISC = 0x66 |
| 34 | + static DIE_ID = 0x67 |
| 35 | + static POWER_STATE = 0x68 |
| 36 | + static AUTOMUTE_STATE = 0x69 |
| 37 | + static PHASE_CTRL = 0x6A |
| 38 | + static SS_CTRL0 = 0x6B |
| 39 | + static SS_CTRL1 = 0x6C |
| 40 | + static SS_CTRL2 = 0x6D |
| 41 | + static SS_CTRL3 = 0x6E |
| 42 | + static SS_CTRL4 = 0x6F |
| 43 | + static CHAN_FAULT = 0x70 |
| 44 | + static GLOBAL_FAULT1 = 0x71 |
| 45 | + static GLOBAL_FAULT2 = 0x72 |
| 46 | + static OT_WARNING = 0x73 |
| 47 | + static PIN_CONTROL1 = 0x74 |
| 48 | + static PIN_CONTROL2 = 0x75 |
| 49 | + static MISC_CONTROL = 0x76 |
| 50 | + static FAULT_CLEAR = 0x78 |
| 51 | + |
| 52 | + static CFG_META_DELAY = 254 |
| 53 | + |
| 54 | + # Minimal register configuration sequence for TAS5805M |
| 55 | + # could be loaded from file or defined here |
| 56 | + # Use CFG_META_DELAY for delays (value = ms) |
| 57 | + static tas5805m_registers = bytes( |
| 58 | + # RESET |
| 59 | + "0000" |
| 60 | + "7f00" |
| 61 | + "0302" |
| 62 | + "0111" |
| 63 | + "0302" |
| 64 | + "fe0a" # CFG_META_DELAY |
| 65 | + "0300" |
| 66 | + "4601" |
| 67 | + "0302" |
| 68 | + "610b" |
| 69 | + "6001" |
| 70 | + "7d11" |
| 71 | + "7eff" |
| 72 | + "0001" |
| 73 | + "5105" |
| 74 | + # Register Tuning |
| 75 | + "0000" |
| 76 | + "7f00" |
| 77 | + "0200" |
| 78 | + "3000" |
| 79 | + "4c30" |
| 80 | + "5300" |
| 81 | + "5400" |
| 82 | + "0303" |
| 83 | + "7880") |
| 84 | + |
| 85 | + # Constructor |
| 86 | + def init() |
| 87 | + self.i2c = tasmota.wire_scan(self.i2c_addr) |
| 88 | + if self.i2c == nil |
| 89 | + log("TAS5805M: I2C device not found at address 0x{self.i2c_addr:02X}",1) |
| 90 | + return false |
| 91 | + end |
| 92 | + |
| 93 | + # Configure PDN pin as output |
| 94 | + gpio.pin_mode(self.pdn_pin, gpio.OUTPUT) |
| 95 | + |
| 96 | + # Configure Fault pin as input (if defined) |
| 97 | + if self.fault_pin >= 0 |
| 98 | + gpio.pin_mode(self.fault_pin, gpio.INPUT_PULLUP) |
| 99 | + end |
| 100 | + |
| 101 | + # Initialize chip |
| 102 | + log(f"TAS5805M: Initializing I2C device at address 0x{self.i2c_addr:02X}") |
| 103 | + self.hardware_reset() |
| 104 | + log("TAS5805M: Device reset complete") |
| 105 | + self.device_init() |
| 106 | + log("TAS5805M: Device initialized successfully") |
| 107 | + self.transmit_registers(self.tas5805m_registers, size(self.tas5805m_registers)) |
| 108 | + tasmota.add_driver(self) |
| 109 | + log("TAS5805M: Driver added successfully") |
| 110 | + self.add_commands() |
| 111 | + log("TAS5805M: Commands i2smute and i2svol added successfully") |
| 112 | + end |
| 113 | + |
| 114 | + |
| 115 | + def audio(cmd, idx, payload, raw) |
| 116 | + if cmd == "power" |
| 117 | + self.set_power(idx) |
| 118 | + log(f"TAS5805M: Power {idx}") |
| 119 | + end |
| 120 | + end |
| 121 | + |
| 122 | + # Hardware reset |
| 123 | + def hardware_reset() |
| 124 | + gpio.digital_write(self.pdn_pin, 0) |
| 125 | + tasmota.delay(20) |
| 126 | + gpio.digital_write(self.pdn_pin, 1) |
| 127 | + tasmota.delay(200) |
| 128 | + end |
| 129 | + |
| 130 | + # Write I2C register |
| 131 | + def write_register(reg, value) |
| 132 | + return self.i2c.write(self.i2c_addr, reg, value, 1) |
| 133 | + end |
| 134 | + |
| 135 | + # Read I2C register |
| 136 | + def read_register(reg) |
| 137 | + return self.i2c.read(self.i2c_addr, reg, 1) |
| 138 | + end |
| 139 | + |
| 140 | + def transmit_registers(conf_buf) |
| 141 | + var i = 0 |
| 142 | + var ret = true |
| 143 | + var sz = size(conf_buf) |
| 144 | + log("tas5805m_transmit_registers: enter",4) |
| 145 | + while i < sz |
| 146 | + var reg = conf_buf[i] |
| 147 | + var val = conf_buf[i + 1] |
| 148 | + if reg == self.CFG_META_DELAY |
| 149 | + # Delay in milliseconds |
| 150 | + tasmota.delay(val) |
| 151 | + else |
| 152 | + # Write register |
| 153 | + ret = self.write_register(reg, val) |
| 154 | + log(f"\t0x{reg:02X} <- 0x{val:02X}",4) |
| 155 | + end |
| 156 | + i += 2 |
| 157 | + end |
| 158 | + if !ret |
| 159 | + log("tas5805m_transmit_registers: Failed to load configuration to tas5805m") |
| 160 | + return false |
| 161 | + end |
| 162 | + log(f"tas5805m_transmit_registers: leave; wrote {sz} registers",4) |
| 163 | + return true |
| 164 | + end |
| 165 | + |
| 166 | + # Device initialization |
| 167 | + def device_init() |
| 168 | + # Perform reset |
| 169 | + self.write_register(self.RESET_REG, 0x01) |
| 170 | + tasmota.delay(100) |
| 171 | + |
| 172 | + # Configure device control |
| 173 | + self.write_register(self.DEVICE_CTRL_1, 0x02) |
| 174 | + self.write_register(self.DEVICE_CTRL_2, 0x03) |
| 175 | + |
| 176 | + # Signal channel control |
| 177 | + self.write_register(self.SIG_CH_CTRL, 0x00) |
| 178 | + |
| 179 | + # Configure SAP control |
| 180 | + self.write_register(self.SAP_CTRL1, 0x00) # I2S standard |
| 181 | + self.write_register(self.SAP_CTRL2, 0x10) # 16-bit |
| 182 | + self.write_register(self.SAP_CTRL3, 0x00) |
| 183 | + |
| 184 | + # Disable auto-mute |
| 185 | + self.write_register(self.AUTO_MUTE_CTRL, 0x00) |
| 186 | + |
| 187 | + # Analog control |
| 188 | + self.write_register(self.ANA_CTRL, 0x00) |
| 189 | + |
| 190 | + # Power-up |
| 191 | + self.write_register(self.DEVICE_CTRL_2, 0x03) |
| 192 | + |
| 193 | + tasmota.delay(100) |
| 194 | + end |
| 195 | + |
| 196 | + # Set volume (0-255) |
| 197 | + def set_volume(volume) |
| 198 | + if volume > 255 volume = 255 end |
| 199 | + if volume < 0 volume = 0 end |
| 200 | + |
| 201 | + # Convert volume to dB (-103.5dB to 24dB) |
| 202 | + # 0 = -103.5dB, 255 = 24dB |
| 203 | + var vol_db = volume |
| 204 | + |
| 205 | + self.write_register(self.DIG_VOL_CTRL, vol_db) |
| 206 | + self.write_register(self.DIG_VOL_CTRL2, vol_db) |
| 207 | + end |
| 208 | + |
| 209 | + # Read volume |
| 210 | + def get_volume() |
| 211 | + return self.read_register(self.DIG_VOL_CTRL) |
| 212 | + end |
| 213 | + |
| 214 | + # Set the volume as percentage [0..124], where 100 is 0 dB, 0 is mute |
| 215 | + def set_volume_pct(vol) |
| 216 | + # Clamp to allowed range |
| 217 | + if vol < 0 |
| 218 | + vol = 0 |
| 219 | + end |
| 220 | + if vol > 124 |
| 221 | + vol = 124 |
| 222 | + end |
| 223 | + |
| 224 | + # Convert to register value: 0 = mute, 100 = 0 dB, 124 = max |
| 225 | + # Formula: reg = vol == 0 ? 255 : (-2 * vol + 248) |
| 226 | + var reg_val = vol == 0 ? 255 : (-2 * vol + 248) |
| 227 | + self.set_volume(reg_val) |
| 228 | + return reg_val |
| 229 | + end |
| 230 | + |
| 231 | + # Get the volume as percentage [0..124], where 100 is 0 dB, 0 is mute |
| 232 | + def get_volume_pct() |
| 233 | + var reg_val = self.get_volume() |
| 234 | + # Formula: pct = reg_val >= 248 ? 0 : (248 - reg_val) / 2 |
| 235 | + if reg_val >= 248 |
| 236 | + return 0 |
| 237 | + else |
| 238 | + return (248 - reg_val) / 2 |
| 239 | + end |
| 240 | + end |
| 241 | + |
| 242 | + # Set mute |
| 243 | + def set_mute(mute) |
| 244 | + var ctrl = self.read_register(self.DEVICE_CTRL_2) |
| 245 | + if mute |
| 246 | + ctrl = ctrl | 0x08 # Set mute bit |
| 247 | + else |
| 248 | + ctrl = ctrl & 0xF7 # Clear mute bit |
| 249 | + end |
| 250 | + self.write_register(self.DEVICE_CTRL_2, ctrl) |
| 251 | + end |
| 252 | + |
| 253 | + # Read mute status |
| 254 | + def get_mute() |
| 255 | + var ctrl = self.read_register(self.DEVICE_CTRL_2) |
| 256 | + return (ctrl & 0x08) != 0 |
| 257 | + end |
| 258 | + |
| 259 | + # Set power state |
| 260 | + def set_power(power_on) |
| 261 | + if power_on |
| 262 | + self.write_register(self.DEVICE_CTRL_2, 0x03) # Power up |
| 263 | + else |
| 264 | + self.write_register(self.DEVICE_CTRL_2, 0x01) # Power down |
| 265 | + end |
| 266 | + end |
| 267 | + |
| 268 | + # Read power state |
| 269 | + def get_power_state() |
| 270 | + return self.read_register(self.POWER_STATE) |
| 271 | + end |
| 272 | + |
| 273 | + # Read sample rate |
| 274 | + def get_sample_rate() |
| 275 | + var fs_mon = self.read_register(self.FS_MON) |
| 276 | + var rates = [8000, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000] |
| 277 | + |
| 278 | + if fs_mon < size(rates) |
| 279 | + return rates[fs_mon] |
| 280 | + end |
| 281 | + return 0 |
| 282 | + end |
| 283 | + |
| 284 | + # Read BCK ratio |
| 285 | + def get_bck_ratio() |
| 286 | + return self.read_register(self.BCK_MON) |
| 287 | + end |
| 288 | + |
| 289 | + # Read fault status |
| 290 | + def get_fault_status() |
| 291 | + return { |
| 292 | + 'Channel': self.read_register(self.CHAN_FAULT), |
| 293 | + 'Global1': self.read_register(self.GLOBAL_FAULT1), |
| 294 | + 'Global2': self.read_register(self.GLOBAL_FAULT2) |
| 295 | + } |
| 296 | + end |
| 297 | + |
| 298 | + # Clear fault status |
| 299 | + def clear_fault() |
| 300 | + self.write_register(self.FAULT_CLEAR, 0x80) |
| 301 | + tasmota.delay(10) |
| 302 | + self.write_register(self.FAULT_CLEAR, 0x00) |
| 303 | + end |
| 304 | + |
| 305 | + # Set analog gain |
| 306 | + def set_analog_gain(gain) |
| 307 | + # Gain: 0 = 19.2dBV, 1 = 20.7dBV, 2 = 22.2dBV, 3 = 23.7dBV |
| 308 | + if gain > 3 gain = 3 end |
| 309 | + if gain < 0 gain = 0 end |
| 310 | + |
| 311 | + var ana_ctrl = self.read_register(self.ANA_CTRL) |
| 312 | + ana_ctrl = (ana_ctrl & 0xFC) | gain |
| 313 | + self.write_register(self.ANA_CTRL, ana_ctrl) |
| 314 | + end |
| 315 | + |
| 316 | + # Read analog gain |
| 317 | + def get_analog_gain() |
| 318 | + var ana_ctrl = self.read_register(self.ANA_CTRL) |
| 319 | + return ana_ctrl & 0x03 |
| 320 | + end |
| 321 | + |
| 322 | + # Read device ID |
| 323 | + def get_device_id() |
| 324 | + return self.read_register(self.DIE_ID) |
| 325 | + end |
| 326 | + |
| 327 | + # Read auto-mute state |
| 328 | + def get_auto_mute_state() |
| 329 | + return self.read_register(self.AUTOMUTE_STATE) |
| 330 | + end |
| 331 | + |
| 332 | + # Read fault pin status (if configured) |
| 333 | + def get_fault_pin() |
| 334 | + if self.fault_pin >= 0 |
| 335 | + return gpio.digital_read(self.fault_pin) |
| 336 | + end |
| 337 | + return nil |
| 338 | + end |
| 339 | + |
| 340 | + def status() |
| 341 | + var result = {"Status": { |
| 342 | + "DeviceID": self.get_device_id(), |
| 343 | + "PowerState": self.get_power_state(), |
| 344 | + "Volume": self.get_volume(), |
| 345 | + "Mute": self.get_mute(), |
| 346 | + "SampleRate": self.get_sample_rate(), |
| 347 | + "BCKRatio": self.get_bck_ratio(), |
| 348 | + "AnalogGain": self.get_analog_gain(), |
| 349 | + "AutoMuteState": self.get_auto_mute_state(), |
| 350 | + "FaultStatus": self.get_fault_status() |
| 351 | + }} |
| 352 | + if self.fault_pin >= 0 |
| 353 | + result["FaultPin"] = self.get_fault_pin() |
| 354 | + end |
| 355 | + log(f"TAS5805M = {result}") |
| 356 | + end |
| 357 | + |
| 358 | + def i2smute(cmd, idx, payload, raw) |
| 359 | + log(f"TAS5805M: i2smute command received with raw={raw}",3) |
| 360 | + if raw != nil |
| 361 | + self.set_mute(raw != 0) # idx is 0 for unmute |
| 362 | + end |
| 363 | + tasmota.resp_cmnd(format("{'i2smute':%i}}",self.get_mute())) |
| 364 | + end |
| 365 | + |
| 366 | + def i2svol(cmd, idx, payload, raw) |
| 367 | + if raw |
| 368 | + log(f"TAS5805M: set new volume: {raw}") |
| 369 | + self.set_volume_pct(raw) |
| 370 | + end |
| 371 | + tasmota.resp_cmnd(format("{'i2svol':%i}}",self.get_volume_pct())) |
| 372 | + end |
| 373 | + |
| 374 | + def add_commands() |
| 375 | + tasmota.add_cmd('i2smute', /c,i,p,r->self.i2smute(c,i,p,r)) |
| 376 | + tasmota.add_cmd('i2svol', /c,i,p,r->self.i2svol(c,i,p,r)) |
| 377 | + end |
| 378 | +end |
| 379 | + |
| 380 | +# Execute at Tasmota startup |
| 381 | +dac = TAS5805M() |
0 commit comments