JACK SENSE(插孔检测)是HDA(High Definition Audio)音频子系统中的核心机制,用于实时检测音频插孔上设备的插拔状态。当用户插入或拔出耳机、麦克风等设备时,系统需要:
整个流程从硬件中断开始,经过内核多层级处理,最终到达用户空间,涉及HDA Controller、HDA Codec、ALSA Core三个主要层次。
HDA(High Definition Audio)采用Controller-Codec架构:
在HDA规范中,每个Pin Complex Widget可以支持Presence Detection(存在检测)。当插孔状态变化时:
关键前提:Pin Widget必须通过AC_VERB_SET_UNSOLICITED_ENABLEVerb启用Unsolicited Response上报功能,并分配一个tag值用于标识事件来源。
硬件层
插拔设备 → JSENSE管脚电平变化 → Codec发送Unsolicited Response↓Controller中断
azx_interrupt→ snd_hdac_bus_update_rirb↓事件入队
snd_hdac_bus_queue_event → 写入unsol_queue → schedule_work↓工作队列
snd_hdac_bus_process_unsol_events → 查找codec → drv->unsol_event↓Codec驱动层
hda_codec_unsol_event → ops->unsol_event即 snd_hda_jack_unsol_event↓JACK核心层
提取tag → 查找jack_tbl → 标记dirty → call_jack_callback↓回调执行
hp_automute / line_automute / mic_autoswitch↓状态上报
snd_hda_jack_report_sync() → snd_jack_report() → input子系统 + ALSA kcontrol
当HDA Controller接收到Codec的响应后,触发硬件中断。中断处理函数读取中断状态寄存器和RIRB状态寄存器:
irqreturn_t azx_interrupt(int irq, void *dev_id){ ...do { status = azx_readl(chip, INTSTS); // 读取中断状态if (status == 0 || status == 0xffffffff)break;if (snd_hdac_bus_handle_stream_irq(bus, status, stream_update)) active = true; status = azx_readb(chip, RIRBSTS); // 读取RIRB状态if (status & RIRB_INT_MASK) { azx_writeb(chip, RIRBSTS, RIRB_INT_MASK); // 清除中断if (status & RIRB_INT_RESPONSE) {snd_hdac_bus_update_rirb(bus); // 处理RIRB响应 } } } while (active && ++repeat < 10);return IRQ_RETVAL(handled);}要点:中断处理函数在中断上下文中执行,只做最基础的寄存器读取和RIRB处理,将耗时的回调处理延迟到工作队列中异步执行。
RIRB(Response Input Ring Buffer)是HDA Controller维护的环形缓冲区,存储Codec的响应。每个RIRB条目为8字节:低32位为响应数据res,高32位为扩展信息res_ex。
#define AZX_RIRB_EX_UNSOL_EV (1<<4)voidsnd_hdac_bus_update_rirb(struct hdac_bus *bus){ ...while (bus->rirb.rp != wp) { ... res_ex = le32_to_cpu(bus->rirb.buf[rp + 1]); res = le32_to_cpu(bus->rirb.buf[rp]); addr = res_ex & 0xf; // 低4位为Codec地址if (res_ex & AZX_RIRB_EX_UNSOL_EV) // bit4: 主动事件标志snd_hdac_bus_queue_event(bus, res, res_ex);else if (bus->rirb.cmds[addr]) { bus->rirb.res[addr] = res; // 普通命令响应 bus->rirb.cmds[addr]--; } }}res_ex & AZX_RIRB_EX_UNSOL_EV用于区分Unsolicited Response(主动上报事件)和普通命令响应。JACK检测事件走的是Unsolicited Response路径。
voidsnd_hdac_bus_queue_event(struct hdac_bus *bus, u32 res, u32 res_ex){ ... wp = (bus->unsol_wp + 1) % HDA_UNSOL_QUEUE_SIZE; bus->unsol_wp = wp; wp <<= 1; bus->unsol_queue[wp] = res; // 写入响应数据 bus->unsol_queue[wp + 1] = res_ex; // 写入扩展信息 schedule_work(&bus->unsol_work); // 调度工作队列}事件被写入unsol_queue环形缓冲区后,通过schedule_work()调度工作队列异步处理。这个工作队列在snd_hdac_bus_init()中初始化:
INIT_WORK(&bus->unsol_work, snd_hdac_bus_process_unsol_events);工作队列在进程上下文中执行,从环形缓冲区逐个取出Unsolicited Event,根据Codec地址找到对应的Codec驱动,调用其unsol_event回调:
static voidsnd_hdac_bus_process_unsol_events(struct work_struct *work){ struct hdac_bus *bus = container_of(work, struct hdac_bus, unsol_work); ... spin_lock_irq(&bus->reg_lock);while (bus->unsol_rp != bus->unsol_wp) { ... res = bus->unsol_queue[rp]; caddr = bus->unsol_queue[rp + 1];if (!(caddr & (1 << 4))) // 非unsol事件,跳过continue; codec = bus->caddr_tbl[caddr & 0x0f]; // 通过Codec地址查找 ... spin_unlock_irq(&bus->reg_lock);if (drv->unsol_event) drv->unsol_event(codec, res); // 调用Codec驱动回调 spin_lock_irq(&bus->reg_lock); } spin_unlock_irq(&bus->reg_lock);}设计要点:中断上下文只做最小工作(读取RIRB、入队),耗时的回调处理在workqueue的进程上下文中完成。这遵循了Linux内核中断处理的"顶半部/底半部"设计原则。
hdac_driver.unsol_event的实现在Codec驱动注册时被设置:
int__hda_codec_driver_register(struct hda_codec_driver *drv, ...){ ... drv->core.unsol_event = hda_codec_unsol_event; // 注册回调return driver_register(&drv->core.driver);}该回调将事件从hdac_device层转换到hda_codec_ops层:
static voidhda_codec_unsol_event(struct hdac_device *dev, unsigned int ev){ struct hda_codec *codec = container_of(dev, struct hda_codec, core); ...if (codec->card->shutdown || codec->bus->shutdown)return;if (codec->core.dev.power.power_state.event != PM_EVENT_ON)return;if (driver->ops->unsol_event) driver->ops->unsol_event(codec, ev); // 调用具体Codec的ops}各Codec驱动(Realtek、Conexant、Generic等)都在其hda_codec_ops中注册.unsol_event = snd_hda_jack_unsol_event:
static const struct hda_codec_ops generic_codec_ops = { ... .unsol_event = snd_hda_jack_unsol_event, // 统一入口 ...};统一入口:几乎所有HDA Codec驱动都将snd_hda_jack_unsol_event 作为unsol_event的处理入口,这体现了HDA JACK检测框架的通用性设计。
这是JACK检测的核心处理函数,负责从Unsolicited Response中提取tag,查找对应的jack表项,标记脏数据,执行回调链,最后同步上报状态:
voidsnd_hda_jack_unsol_event(struct hda_codec *codec, unsigned int res){ struct hda_jack_tbl *event;int tag = (res & AC_UNSOL_RES_TAG) >> AC_UNSOL_RES_TAG_SHIFT; event = snd_hda_jack_tbl_get_from_tag(codec, tag, 0);if (!event)return; event->jack_dirty = 1; // 标记需要更新call_jack_callback(codec, res, event); // 执行回调链snd_hda_jack_report_sync(codec); // 同步上报状态}每个jack表项维护一个回调链表,当事件发生时依次执行所有注册的回调:
static voidcall_jack_callback(struct hda_codec *codec, unsigned int res, struct hda_jack_tbl *jack){ struct hda_jack_callback *cb;for (cb = jack->callback; cb; cb = cb->next) { cb->jack = jack; cb->unsol_res = res; cb->func(codec, cb); // 执行回调函数 }if (jack->gated_jack) { // 处理gated jack ... }}在状态上报前,需要通过jack_detect_update()读取Pin Widget的实际检测状态:
#define get_jack_plug_state(sense) !!(sense & AC_PINSENSE_PRESENCE)static voidjack_detect_update(struct hda_codec *codec, struct hda_jack_tbl *jack){if (!jack->jack_dirty)return;if (jack->phantom_jack) jack->pin_sense = AC_PINSENSE_PRESENCE; // 虚拟jack始终在位else jack->pin_sense = read_pin_sense(codec, jack->nid, jack->dev_id);// gating jack: 如果gating jack未插入,则gated jack无效if (jack->gating_jack && !snd_hda_jack_detect_mst(codec, jack->gating_jack, jack->dev_id)) jack->pin_sense &= ~AC_PINSENSE_PRESENCE; jack->jack_dirty = 0; ...}该函数通过发送HDA Verb读取Pin Widget的检测状态:
static u32read_pin_sense(struct hda_codec *codec, hda_nid_t nid, int dev_id){ u32 pincap, val;if (!codec->no_trigger_sense) { pincap = snd_hda_query_pin_caps(codec, nid);if (pincap & AC_PINCAP_TRIG_REQ) // 需要触发? snd_hda_codec_read(codec, nid, 0, AC_VERB_SET_PIN_SENSE, 0); // 先触发 } val = snd_hda_codec_read(codec, nid, 0, AC_VERB_GET_PIN_SENSE, dev_id); // 读取状态if (codec->inv_jack_detect) val ^= AC_PINSENSE_PRESENCE; // 反转检测逻辑 ...return val;}JACK检测的回调函数主要实现三大自动切换功能:
当检测到耳机插入时,自动静音扬声器;拔出时恢复扬声器输出:
voidsnd_hda_gen_hp_automute(struct hda_codec *codec, struct hda_jack_callback *jack){ ... spec->hp_jack_present = detect_jacks(codec, num_pins, pins);if (!spec->detect_hp || (!spec->automute_speaker && !spec->automute_lo))return;call_update_outputs(codec); // 更新输出路径}voidsnd_hda_gen_line_automute(struct hda_codec *codec, struct hda_jack_callback *jack){ ... spec->line_jack_present = detect_jacks(...);if (!spec->automute_speaker || !spec->detect_lo)return; call_update_outputs(codec);}当检测到外接麦克风插入时,自动将输入源切换到外接麦克风;拔出时切回内置麦克风:
voidsnd_hda_gen_mic_autoswitch(struct hda_codec *codec, struct hda_jack_callback *jack){ ...// 优先级从高到低检查,找到第一个在位的micfor (i = spec->am_num_entries - 1; i > 0; i--) {if (snd_hda_jack_detect_state(codec, pin) == HDA_JACK_PRESENT) {mux_select(codec, 0, spec->am_entry[i].idx);return; } } mux_select(codec, 0, spec->am_entry[0].idx); // 默认选择内置mic}voidsnd_hda_jack_report_sync(struct hda_codec *codec){ ...// 第一步:更新所有jack的检测状态for (i = 0; i < codec->jacktbl.used; i++, jack++)if (jack->nid) jack_detect_update(codec, jack);// 第二步:上报状态变化for (i = 0; i < codec->jacktbl.used; i++, jack++)if (jack->nid) { ...snd_jack_report(jack->jack, state); // 上报 }}该函数通过两条路径向用户空间报告JACK状态:
voidsnd_jack_report(struct snd_jack *jack, int status){ ...// 路径1:更新ALSA kcontrol状态 list_for_each_entry(jack_kctl, &jack->kctl_list, list) snd_kctl_jack_report(jack->card, jack_kctl->kctl, status & jack_kctl->mask_bits);// 路径2:通过input子系统上报for (i = 0; i < ARRAY_SIZE(jack_switch_types); i++) {if (jack->type & testbit) input_report_switch(idev, jack_switch_types[i], status & testbit); } input_sync(idev); // 同步input事件}双路径上报:JACK状态通过两条路径到达用户空间:1.ALSA kcontrol:用户空间可通过/dev/snd/controlc0的ioctl读取jack状态2.Linux input子系统:用户空间PulseAudio通过/dev/input/eventX监听SW_HEADPHONE_INSERT等switch事件
该函数是JACK检测的注册入口,完成两件事:创建jack表项并注册回调,通过Verb启用Pin的Unsolicited Response上报:
struct hda_jack_callback *snd_hda_jack_detect_enable_callback_mst(struct hda_codec *codec, hda_nid_t nid,int dev_id, hda_jack_callback_fn func){ ... jack = snd_hda_jack_tbl_new(codec, nid, dev_id); // 创建/获取jack表项if (func && !callback) { callback->func = func; callback->next = jack->callback; // 头插法加入回调链 jack->callback = callback; }if (jack->jack_detect)return callback; // 已注册过 jack->jack_detect = 1;if (codec->jackpoll_interval > 0)return callback; // 轮询模式不需要unsol// 发送Verb启用Unsolicited Response snd_hda_codec_write_cache(codec, nid, 0, AC_VERB_SET_UNSOLICITED_ENABLE, AC_USRSP_EN | jack->tag);return callback;}Codec初始化snd_hda_gen_init()会调用check_auto_mute_availability(),为每个可检测的HP Pin和Line-out Pin注册自动静音回调:
static intcheck_auto_mute_availability(struct hda_codec *codec){ ...// 为每个HP pin注册 call_hp_automute 回调for (i = 0; i < cfg->hp_outs; i++) {if (!is_jack_detectable(codec, nid))continue; snd_hda_jack_detect_enable_callback(codec, nid, call_hp_automute); spec->detect_hp = 1; }// 为每个Line-out pin注册 call_line_automute 回调for (i = 0; i < cfg->line_outs; i++) {if (!is_jack_detectable(codec, nid))continue; snd_hda_jack_detect_enable_callback(codec, nid, call_line_automute); spec->detect_lo = 1; } ...}static boolauto_mic_check_imux(struct hda_codec *codec){ ...// 第一个pin不需要jack检测(默认输入源)// 从第二个pin开始注册 call_mic_autoswitch 回调for (i = 1; i < spec->am_num_entries; i++) snd_hda_jack_detect_enable_callback(codec, spec->am_entry[i].pin,call_mic_autoswitch);return true;}并非所有Pin都支持JACK检测,需要满足以下条件:
boolis_jack_detectable(struct hda_codec *codec, hda_nid_t nid){if (codec->no_jack_detect) // 全局禁用return false;if (!(snd_hda_query_pin_caps(codec, nid) & AC_PINCAP_PRES_DETECT))return false; // 不支持Presence Detectif (get_defcfg_misc(snd_hda_codec_get_pincfg(codec, nid)) & AC_DEFCFG_MISC_NO_PRESENCE)return false; // Pin配置标记为无检测if (!(get_wcaps(codec, nid) & AC_WCAP_UNSOL_CAP) && !codec->jackpoll_interval)return false; // 不支持Unsolicited且无轮询return true;}当Codec不支持Unsolicited Response,或jackpoll_interval被设置为非零值时,系统使用轮询模式检测JACK状态:
static voidhda_jackpoll_work(struct work_struct *work){ ...if (!codec->jackpoll_interval)return; snd_hda_jack_set_dirty_all(codec); // 标记所有jack为dirty snd_hda_jack_poll_all(codec); // 轮询所有jack schedule_delayed_work(&codec->jackpoll_work, codec->jackpoll_interval); // 重新调度}snd_hda_jack_poll_all()的实现:
voidsnd_hda_jack_poll_all(struct hda_codec *codec){ ...for (i = 0; i < codec->jacktbl.used; i++, jack++) { old_sense = get_jack_plug_state(jack->pin_sense); jack_detect_update(codec, jack);if (old_sense == get_jack_plug_state(jack->pin_sense))continue; // 状态未变,跳过 changes = 1; call_jack_callback(codec, 0, jack); }if (changes) snd_hda_jack_report_sync(codec);}注意:参考文档指出,在实际运行中jackpoll_interval通常为0,因此轮询模式不会实际执行。JACK检测主要依赖Unsolicited Response中断方式。轮询模式主要用于不支持Unsolicited Response的Codec或调试场景。
插拔耳机时,可通过ftrace观察到Unsolicited Event:
root@E490-2:/sys/kernel/debug/tracing# cat trace<idle>-0 [007] d.h. 60822.084078: hda_unsol_event: [0000:00:1f.3:0] res=0x04000000, res_ex=0x00000010<idle>-0 [007] d.h. 60822.624109: hda_unsol_event: [0000:00:1f.3:0] res=0x08000000, res_ex=0x00000010根据HDA规范(7.3.3.14 Unsolicited Response),res字段的格式为:
Tag值与Pin NID的对应关系可通过/proc/asound/card0/codec#0查看:
Node 0x16 [Pin Complex] wcaps 0x400581: Stereo Pincap 0x0001001c: OUT HP EAPD Detect Pin Default 0x03211040: [Jack] HP Out at Ext Left Unsolicited: tag=01, enabled=1 // tag=1 对应 0x16 (耳机)Node 0x19 [Pin Complex] wcaps 0x40048b: Stereo Amp-In Pincap 0x00001324: IN Detect Pin Default 0x03a11030: [Jack] Mic at Ext Left Unsolicited: tag=02, enabled=1 // tag=2 对应 0x19 (麦克风)结论:插拔耳机孔时,JSENSE管脚产生中断,Codec向Controller发送Node 0x16(耳机)和0x19(麦克风)的Unsolicited Response事件。tag=1对应耳机,tag=2对应麦克风。
tag在snd_hda_jack_tbl_new()中分配,值为jack表项在数组中的索引:
static struct hda_jack_tbl *snd_hda_jack_tbl_new(struct hda_codec *codec, hda_nid_t nid, int dev_id){ ... jack = snd_array_new(&codec->jacktbl); jack->nid = nid; jack->jack_dirty = 1;if (existing_nid_jack) { jack->tag = existing_nid_jack->tag; } else { jack->tag = codec->jacktbl.used; // tag = 数组索引 }return jack;}struct hda_jack_tbl { hda_nid_t nid; // Pin Widget的NIDint dev_id; // 设备ID(DP MST用) unsigned char tag; // Unsolicited Response的tag struct hda_jack_callback *callback; // 回调链表头 unsigned int pin_sense; // 缓存的Pin Sense值 unsigned int jack_detect:1; // 是否已启用JACK检测 unsigned int jack_dirty:1; // 是否需要更新 unsigned int phantom_jack:1;// 虚拟jack(固定在位) unsigned int block_report:1;// 是否阻止上报 hda_nid_t gating_jack; // gating jack的NID hda_nid_t gated_jack; // gated jack的NID hda_nid_t key_report_jack; // 按键事件上报的jack NIDint type; // JACK类型(HEADPHONE/MICROPHONE等)int button_state; // 按键状态 struct snd_jack *jack; // ALSA jack对象};struct hda_jack_callback { hda_nid_t nid; // 关联的Pin NIDint dev_id; // 设备ID hda_jack_callback_fn func; // 回调函数指针 unsigned int private_data; // 私有数据 unsigned int unsol_res; // Unsolicited Response数据 struct hda_jack_tbl *jack; // 关联的jack表项 struct hda_jack_callback *next; // 链表下一个节点};struct snd_jack { struct list_head kctl_list; // kcontrol链表 struct snd_card *card; // 所属声卡 const char *id; // jack标识 struct input_dev *input_dev;// input设备(向用户空间报告)int type; // jack类型位掩码int hw_status_cache; // 硬件状态缓存void *private_data; // 私有数据(指向hda_jack_tbl)void (*private_free)(struct snd_jack *); // 释放回调};struct hdac_bus { ... u32 unsol_queue[HDA_UNSOL_QUEUE_SIZE * 2]; // 环形缓冲区 unsigned int unsol_rp, unsol_wp; // 读写指针 struct work_struct unsol_work; // 工作队列项 ...};飞腾FT2000平台搭配Conexant CX11880/SN6140 Codec的机器,系统reboot后插入4段耳机,系统无法正确识别CTIA/OMTP类型,导致耳机麦克风不工作。冷启动不受影响,仅重启后出现。
受影响硬件:飞腾FT2000 + CX11880/SN6140 Codec,涉及BXC T350、NZ269等机型。
CX11880/SN6140的CTIA/OMTP检测依赖两个vendor寄存器:0x4f0(耳机检测使能)和0x3b0(DFET OFF电压和micbias电阻)。reboot过程中Codec不断电但vendor寄存器恢复默认值,DFET OFF电压从-0.8V回到-1.2V,micbias电阻从2.0K回到2.2K,导致CTIA/OMTP检测阈值偏移。
static voidcx_fixup_headset_recog(struct hda_codec *codec){ unsigned int mic_persent; snd_hda_codec_write(codec, 0x1c, 0, 0x4f0, 0x087); // 使能耳机检测 snd_hda_codec_write(codec, 0x1c, 0, 0x3b0, 0xe10); // DFET=-0.8V, micbias=2.0K mic_persent = snd_hda_codec_read(codec, 0x19, 0, AC_VERB_GET_PIN_SENSE, 0x0);if (mic_persent & AC_PINSENSE_PRESENCE) snd_hda_codec_write(codec, 0x19, 0, AC_VERB_SET_PIN_WIDGET_CONTROL, 0x24);else snd_hda_codec_write(codec, 0x19, 0, AC_VERB_SET_PIN_WIDGET_CONTROL, 0x20);}T350机型因硬件时序问题需延迟1500ms执行:
if (codec->core.subsystem_id == SUBSYS_ID_T350) schedule_delayed_work(&spec->fixup_hp_recog_work, msecs_to_jiffies(1500));else cx_fixup_headset_recog(codec);修复原理:通过vendor verb重新配置DFET电压和micbias电阻,使CTIA/OMTP检测阈值回到正确范围。VREF根据mic在位状态动态设置。
飞腾平台+CX11880/SN6140,插入4段耳机后开启录音,耳机麦克风无声音。拔出耳机后内置麦克风正常。涉及BXC NZ307C、NZ291B、NZ300、MF292B等多款机型。
两个相互关联的问题:
cx_update_headset_mic_vref()在unsol回调中根据capture_pcm_state决定VREF值——录音未启动时VREF=0x20(无偏置),mic无供电而静音。修复一:CTIA/OMTP检测完成后,根据录音状态动态切换VREF:
if (val & 0x800) { // CTIAif (spec->capture_pcm_state == PCM_ACT_PREPARED) snd_hda_codec_write(codec, 0x19, 0, 0x707, 0x24); // VREF+INelse snd_hda_codec_write(codec, 0x19, 0, 0x707, 0x20); // IN only}修复二:录音PCM prepare时先静音ADC,延迟500ms后恢复增益:
static intcxt_capture_pcm_prepare(...){// 先静音ADC(设置bit7) snd_hda_codec_write(codec, nid, 0, (0x370 | idx), (value | 0x80));// 延迟500ms后恢复 queue_work(spec->unmute_queue, &spec->unmute_work);}static voiddo_unmute(struct work_struct *work){ msleep(500);if ((value & ~0x80) == 0x0) snd_hda_codec_write(codec, nid, 0, (0x370 | idx), spec->init_cap_val);}修复三:mic pin (0x19)不支持unsol event,通过gating_jack与HP pin (0x16)绑定:
if (action == HDA_FIXUP_ACT_PROBE) snd_hda_jack_set_gating_jack(codec, 0x19, 0x16);飞腾平台+CX11880/SN6140,S3恢复后首次播放音频时扬声器/耳机出现"pop"爆音,之后播放正常。
S3恢复时cx_resume()调用cx_auto_init()重新初始化Codec,DAC输出路径在初始化过程中被使能,但此时DAC输出电压尚未稳定,导致pop音。
Resume时先静音输出,首次播放时延迟取消静音:
static intcx_resume(struct hda_codec *codec){if (is_s3_resume(codec) || is_s4_resume(codec)) cx_auto_init(codec);if (spec->is_cx11880_sn6140) { spec->resume_work = true; mute_output_buff(codec, 0x16, true); // 静音HP mute_output_buff(codec, 0x17, true); // 静音Speaker }}voidcxt_playback_hook(...){case HDA_GEN_PCM_ACT_PREPARE:if (spec->resume_work) { schedule_delayed_work(&spec->resume_unmute_work, msecs_to_jiffies(100)); spec->resume_work = false; }}static voidresume_unmute_worker(struct work_struct *work){ mute_output_buff(codec, 0x16, false); mute_output_buff(codec, 0x17, false);}飞腾FT2000平台+Realtek ALC269系列Codec,S3恢复后音频功能完全异常:耳机插入无反应、扬声器/耳机切换不正常、麦克风不工作。此Bug是飞腾HDA控制器特有的,Intel平台不受影响。
Intel HDA控制器S3期间不复位Codec,寄存器保持不变。但飞腾FT2000控制器在S3过程中执行azx_enter_link_reset(),导致Codec被硬件复位,Pin Config Default和Processing Coefficient寄存器全部丢失。Pin Config丢失后is_jack_detectable()判断错误,jack检测被错误禁用。
Suspend时保存所有Pin Config和Coef,Resume时检测并恢复:
static intalc269_verbs_save(struct hda_codec *codec){for (i = 0; i < nodes; i++, nid++) {if (wid_type == AC_WID_PIN) {// 保存Pin Config Default为4条SET_CONFIG_DEFAULT_BYTES verb snd_hdac_read(&codec->core, nid, AC_VERB_GET_CONFIG_DEFAULT, 0, &val); *verb0 = snd_hdac_make_cmd(&codec->core, nid, AC_VERB_SET_CONFIG_DEFAULT_BYTES_0, (val >> 0) & 0xff); ... }if (wid_caps & AC_WCAP_PROC_WID) {// 保存所有Coef寄存器for (j = 0; j < ncoeff; j++) { ... } } }}static intalc269_resume(struct hda_codec *codec){if (is_ft2000_pc()) {if (alc269_restore_verbs) // 模块参数强制恢复 alc269_verbs_restore(codec);else if (alc269_if_restore(codec)) // 检测pin config是否为出厂默认值 alc269_verbs_restore(codec);else// 配置未丢失,释放保存数据 snd_array_free(&codec->saved_verbs); }}智能恢复:alc269_if_restore()比较当前pin config与ALC269VC/VB出厂默认值,若全部匹配则说明S3恢复后配置丢失,需要恢复。模块参数alc269_restore_verbs可强制恢复。
某些主板上ALC662/ALC861的JACK检测完全不可靠——插拔耳机时系统时而检测到时而检测不到,或始终报告"已插入"。导致扬声器/耳机反复切换或始终静音。
受影响硬件:ALC662, 1043:8469、ALC861, 1462:7254。这些主板的音频插孔物理上未连接JACK SENSE信号线,或信号被错误拉高/拉低。
硬件层面JACK SENSE信号不可靠,Pin Sense寄存器的PRESENCE位始终为1或随机跳变。软件无法修复硬件信号,只能完全禁用JACK检测。
static voidalc_fixup_no_jack_detect(struct hda_codec *codec,const struct hda_fixup *fix, int action){if (action == HDA_FIXUP_ACT_PRE_PROBE) codec->no_jack_detect = 1;}// PCI Quirk匹配SND_PCI_QUIRK(0x1462, 0x7254, "HP DX2200", ALC861_FIXUP_NO_JACK_DETECT);SND_PCI_QUIRK(0x1043, 0x8469, "ASUS mobo", ALC662_FIXUP_NO_JACK_DETECT);no_jack_detect=1使is_jack_detectable()直接返回false,所有pin都不注册jack检测回调。耳机自动静音、mic自动切换等功能全部失效,用户需通过alsamixer手动控制。这是"宁可不检测也不能误检测"的保守策略。
Realtek ALC255/ALC256等Codec的4段耳机combo jack,插入CTIA耳机后麦克风不工作,插入OMTP耳机后声音失真或有底噪,拔出后内置麦克风不恢复。
jack由状态机alc_update_headset_mode()驱动,有4种模式:UNPLUGGED、HEADSET、MIC、HEADPHONE。CTIA/OMTP检测需800ms等待硬件完成,VREF需随模式切换动态配置。若检测未完成就切换模式,或VREF时序不对,就会导致上述问题。
CTIA/OMTP检测:通过coef寄存器配置检测参数,等待800ms后读取结果:
static voidalc_determine_headset_type(struct hda_codec *codec){case 0x10ec0255: alc_update_coef_idx(codec, 0x45, 0x3f<<10, 0x34<<10); alc_update_coef_idx(codec, 0x49, 3<<8, 2<<8); msleep(800); val = alc_read_coef_idx(codec, 0x46); is_ctia = (val & 0x00f0) == 0x00f0;break;}模式切换:根据HP jack状态和输入源确定模式,通过coef统一管理VREF和信号路径:
static voidalc_update_headset_mode(struct hda_codec *codec){if (!snd_hda_jack_detect(codec, hp_pin)) new_headset_mode = ALC_HEADSET_MODE_UNPLUGGED;else if (mux_pin == spec->headset_mic_pin) new_headset_mode = ALC_HEADSET_MODE_HEADSET;else if (mux_pin == spec->headphone_mic_pin) new_headset_mode = ALC_HEADSET_MODE_MIC;else new_headset_mode = ALC_HEADSET_MODE_HEADPHONE;switch (new_headset_mode) {case ALC_HEADSET_MODE_HEADSET:if (spec->current_headset_type == ALC_HEADSET_TYPE_UNKNOWN) alc_determine_headset_type(codec);if (spec->current_headset_type == ALC_HEADSET_TYPE_CTIA) alc_headset_mode_ctia(codec);else alc_headset_mode_omtp(codec);break;case ALC_HEADSET_MODE_MIC: alc_headset_mode_mic_in(codec, hp_pin, mic_pin);break; }}设计对比:Realtek通过coef寄存器统一管理CTIA/OMTP检测和VREF控制,状态机确保模式切换原子性;Conexant CX11880则在unsol回调中直接操作pin control,更容易出现竞争条件。但两者都面临msleep()在回调中阻塞的问题。
HDA JACK检测是一个从硬件到软件、从内核到用户空间的完整链路,核心设计思想可以归纳为以下几点:
1. 中断驱动的异步架构:JACK检测采用"中断顶半部 + 工作队列底半部"的经典模式。硬件中断只做RIRB读取和事件入队,耗时的回调处理延迟到进程上下文执行,保证了中断处理的实时性和系统的响应能力。
2. tag机制解耦硬件与软件:Unsolicited Response中的tag字段将Codec物理Pin与内核jack表项一一映射,使得事件分发无需关心底层硬件细节,各Codec驱动通过统一的snd_hda_jack_unsol_events入口处理,实现框架通用性。
3. 回调链支持灵活扩展:每个jack表项维护一个回调链表,支持注册多个回调函数。gating/gated jack机制解决了某些Pin不支持独立检测的问题(如mic pin依赖hp pin的事件触发),体现了软件对硬件局限的优雅补偿。
4. 双路径上报确保兼容性:JACK状态同时通过ALSA kcontrol和Linux input子系统两条路径上报,前者供alsa-lib等传统音频库使用,后者供PulseAudio/PipeWire等现代音频服务使用,确保了用户空间不同层次的需求都能被满足。
5. 案例中的共性规律:从6个实际Bug案例可以看出,JACK检测问题主要集中在三个领域:一是S3/S4恢复后寄存器状态丢失(飞腾平台尤为突出),二是4段耳机CTIA/OMTP类型检测的时序和VREF控制,三是硬件信号不可靠时的降级策略。这些问题的修复往往需要在Codec驱动的unsol回调、PCM hook、resume回调等多个位置协同处理,并大量使用schedule_delayed_work()来规避时序竞争。