// Copyright 2020 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. //! `max98390d` module implements the required initialization workflows for sound //! cards that use max98390d smart amp. //! It currently supports boot time calibration for max98390d. #![deny(missing_docs)] mod settings; use std::time::Duration; use std::{fs, path::Path}; use cros_alsa::{Card, IntControl, SwitchControl}; use dsm::{CalibData, Error, Result, SpeakerStatus, TempConverter, ZeroPlayer, DSM}; use crate::Amp; use settings::{AmpCalibSettings, DeviceSettings}; /// Amp volume mode emulation used by set_volume(). #[derive(PartialEq, Clone, Copy)] enum VolumeMode { /// Low mode protects the speaker by limiting its output volume if the /// calibration has not been completed successfully. Low = 138, /// High mode removes the speaker output volume limitation after /// having successfully completed the calibration. High = 148, } /// It implements the Max98390 functions of boot time calibration. #[derive(Debug)] pub struct Max98390 { card: Card, setting: AmpCalibSettings, } impl Amp for Max98390 { /// Performs max98390d boot time calibration. /// /// # Errors /// /// If the amplifier fails to complete the calibration. fn boot_time_calibration(&mut self) -> Result<()> { if !Path::new(&self.setting.dsm_param).exists() { return Err(Error::MissingDSMParam); } let mut dsm = DSM::new( &self.card.name(), self.setting.num_channels(), Self::rdc_to_ohm, Self::TEMP_UPPER_LIMIT_CELSIUS, Self::TEMP_LOWER_LIMIT_CELSIUS, ); dsm.set_temp_converter(TempConverter::new( Self::dsm_unit_to_celsius, Self::celsius_to_dsm_unit, )); self.set_volume(VolumeMode::Low)?; let calib = match dsm.check_speaker_over_heated_workflow()? { SpeakerStatus::Hot(previous_calib) => previous_calib, SpeakerStatus::Cold => self .do_calibration()? .iter() .enumerate() .map(|(ch, calib_data)| dsm.decide_calibration_value_workflow(ch, *calib_data)) .collect::>>()?, }; self.apply_calibration_value(calib)?; self.set_volume(VolumeMode::High)?; Ok(()) } } impl Max98390 { const TEMP_UPPER_LIMIT_CELSIUS: f32 = 40.0; const TEMP_LOWER_LIMIT_CELSIUS: f32 = 0.0; const RDC_CALIB_WARM_UP_TIME: Duration = Duration::from_millis(300); /// Creates an `Max98390`. /// # Arguments /// /// * `card_name` - card name. /// * `config_path` - config file path. /// /// # Results /// /// * `Max98390` - It implements the Max98390 functions of boot time calibration. /// /// # Errors /// /// * If `Card` creation from sound card name fails. pub fn new(card_name: &str, config_path: &Path) -> Result { let conf = fs::read_to_string(config_path) .map_err(|e| Error::FileIOFailed(config_path.to_path_buf(), e))?; let settings = DeviceSettings::from_yaml_str(&conf)?; Ok(Self { card: Card::new(card_name)?, setting: settings.amp_calibrations, }) } /// Sets the card volume control to given VolumeMode. fn set_volume(&mut self, mode: VolumeMode) -> Result<()> { for control in &self.setting.controls { self.card .control_by_name::(&control.volume_ctrl)? .set(mode as i32)?; } Ok(()) } /// Applies the calibration value to the amp. fn apply_calibration_value(&mut self, calib: Vec) -> Result<()> { for (ch, &CalibData { rdc, temp }) in calib.iter().enumerate() { self.card .control_by_name::(&self.setting.controls[ch].rdc_ctrl)? .set(rdc)?; self.card .control_by_name::(&self.setting.controls[ch].temp_ctrl)? .set(Self::celsius_to_dsm_unit(temp))?; } Ok(()) } /// Triggers the amplifier calibration and reads the calibrated rdc and ambient_temp value /// from the mixer control. /// To get accurate calibration results, the main thread calibrates the amplifier while /// the `zero_player` starts another thread to play zeros to the speakers. fn do_calibration(&mut self) -> Result> { let mut zero_player: ZeroPlayer = Default::default(); zero_player.start(Self::RDC_CALIB_WARM_UP_TIME)?; // Playback of zeros is started for Self::RDC_CALIB_WARM_UP_TIME, and the main thread // can start the calibration. let setting = &self.setting; let card = &mut self.card; let calib = setting .controls .iter() .map(|control| { card.control_by_name::(&control.calib_ctrl)? .on()?; let rdc = card .control_by_name::(&control.rdc_ctrl)? .get()?; let temp = card .control_by_name::(&control.temp_ctrl)? .get()?; card.control_by_name::(&control.calib_ctrl)? .off()?; Ok(CalibData { rdc, temp: Self::dsm_unit_to_celsius(temp), }) }) .collect::>>()?; zero_player.stop()?; Ok(calib) } /// Converts the ambient temperature from celsius to the DSM unit. #[inline] fn celsius_to_dsm_unit(celsius: f32) -> i32 { (celsius * ((1 << 12) as f32) / 100.0) as i32 } /// Converts the ambient temperature from DSM unit to celsius. #[inline] fn dsm_unit_to_celsius(temp: i32) -> f32 { temp as f32 * 100.0 / (1 << 12) as f32 } /// Converts the calibrated value to real DC resistance in ohm unit. #[inline] fn rdc_to_ohm(x: i32) -> f32 { 3.66 * (1 << 20) as f32 / x as f32 } } #[cfg(test)] mod tests { use super::*; #[test] fn celsius_to_dsm_unit() { assert_eq!( Max98390::celsius_to_dsm_unit(Max98390::TEMP_UPPER_LIMIT_CELSIUS), 1638 ); assert_eq!( Max98390::celsius_to_dsm_unit(Max98390::TEMP_LOWER_LIMIT_CELSIUS), 0 ); } #[test] fn dsm_unit_to_celsius() { assert_eq!( Max98390::dsm_unit_to_celsius(1638).round(), Max98390::TEMP_UPPER_LIMIT_CELSIUS ); assert_eq!( Max98390::dsm_unit_to_celsius(0), Max98390::TEMP_LOWER_LIMIT_CELSIUS ); } #[test] fn rdc_to_ohm() { assert_eq!(Max98390::rdc_to_ohm(1123160), 3.416956); assert_eq!(Max98390::rdc_to_ohm(1157049), 3.3168762); } }