CEC Framework层关机流程细节
目录
一、前言
我们在探索RK3588 framework层到Hal层CEC关机流程实现大概梳理了Framework层到kernel层CEC的关机流程。但是前段时间又遇到TV关机,CEC设备没有关机问题,在排查过程中,发现前面对于Framework层的认识有点错误, 在这里更正一下。
二、问题现象
我司产品有三路HDMI IN端子,在HDMI1端子整机关机,DVD不会关机。但是HDMI2、HDMI3会正常关机。首先我们用逻辑分析仪,量了整机关机,HDMI CEC引脚的波形,发现没有CEC的波形。到这里,我首先怀疑是硬件问题, 因为我们的三路HDMI是通过HDMI swith芯片在SOC上面拓展出来的,硬件可能漏接了一路HDMI1。但是发现在HDMI1通道,CEC的其他功能是正常的,排出了硬件问题的可能,只能从软件出发,好好分析代码了。
三、解决思路
1.是否可能是硬件问题
HDMI1异常,HDMI2、HDMI3正常,说明硬件是正常的。DVD也是正常的
2.确认DVD是否收到关机信号
使用逻辑分析仪,量了整机关机,HDMI CEC引脚的波形,发现没有CEC的波形。说明DVD未收到关机指令
3.确认整机是否发送关机信号
综合前两个实验,我们已经确认是软件层面的问题了。只能通过阅读Android系统源码来确认问题
四、代码流程分析
1.关机流程回顾
我们这里讲了,CEC的关机第一步是HdmiControlService监听到关机广播后,进行关机, 如下
private class HdmiControlBroadcastReceiver extends BroadcastReceiver {
@ServiceThreadOnly
@Override
public void onReceive(Context context, Intent intent) {
assertRunOnServiceThread();
boolean isReboot = SystemProperties.get(SHUTDOWN_ACTION_PROPERTY).contains("1");
switch (intent.getAction()) {
···
case Intent.ACTION_SHUTDOWN:
if (isPowerOnOrTransient() && !isReboot) {
onStandby(STANDBY_SHUTDOWN);
}
break;
}
}
}
@ServiceThreadOnly
@VisibleForTesting
protected void onStandby(final int standbyAction) {
mWakeUpMessageReceived = false;
assertRunOnServiceThread();
mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY,
false);
invokeVendorCommandListenersOnControlStateChanged(false,
HdmiControlManager.CONTROL_STATE_CHANGED_REASON_STANDBY);
final List<HdmiCecLocalDevice> devices = getAllLocalDevices();
if (!isStandbyMessageReceived() && !canGoToStandby()) {
mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_STANDBY);
for (HdmiCecLocalDevice device : devices) {
device.onStandby(mStandbyMessageReceived, standbyAction);
}
return;
}
disableDevices(new PendingActionClearedCallback() {
@Override
public void onCleared(HdmiCecLocalDevice device) {
Slog.v(TAG, "On standby-action cleared:" + device.mDeviceType);
devices.remove(device);
if (devices.isEmpty()) {
onPendingActionsCleared(standbyAction);
// We will not clear local devices here, since some OEM/SOC will keep passing
// the received packets until the application processor enters to the sleep
// actually.
}
}
});
}
前面一直想当然的以为调用关机的是在
for (HdmiCecLocalDevice device : devices) {
device.onStandby(mStandbyMessageReceived, standbyAction);
}
但是实际调试过程中发现,if (!isStandbyMessageReceived() && !canGoToStandby())
是未满足的, 最终的调用入口为
disableDevices(new PendingActionClearedCallback() {
@Override
public void onCleared(HdmiCecLocalDevice device) {
Slog.v(TAG, "On standby-action cleared:" + device.mDeviceType);
devices.remove(device);
if (devices.isEmpty()) {
onPendingActionsCleared(standbyAction);
// We will not clear local devices here, since some OEM/SOC will keep passing
// the received packets until the application processor enters to the sleep
// actually.
}
}
});
在HDMI1通道下PendingActionClearedCallback
的回调,是未被触发的, 其他通道正常触发。接着看触发回调之后的逻辑
/**
* Normally called after all devices have cleared their pending actions, to execute the final
* phase of the standby flow.
*
* This can also be called during wakeup, when pending actions are cleared after failing to be
* cleared during standby. In this case, it does not execute the standby flow.
*/
@ServiceThreadOnly
private void onPendingActionsCleared(int standbyAction) {
assertRunOnServiceThread();
Slog.v(TAG, "onPendingActionsCleared");
if (mPowerStatusController.isPowerStatusTransientToStandby()) {
mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_STANDBY);
for (HdmiCecLocalDevice device : mHdmiCecNetwork.getLocalDeviceList()) {
//最终在这进行关机
device.onStandby(mStandbyMessageReceived, standbyAction);
}
if (!isAudioSystemDevice()) {
mCecController.setOption(OptionKey.SYSTEM_CEC_CONTROL, false);
mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, DISABLED);
}
}
// Always reset this flag to set up for the next standby
mStandbyMessageReceived = false;
}
最终在这里device.onStandby(mStandbyMessageReceived, standbyAction);
进行关机流程。因此我们目标便是是要正常触发PendingActionClearedCallback
回调函数, 即可正常关机。我们先不纠结为什么条件未满足,我们先来看下,disableDevices
这个函数需要如何触发回调函数
2.详细流程分析
首先我们来看disableDevices
的实现, 他接受一个回调的参数PendingActionClearedCallback
private void disableDevices(PendingActionClearedCallback callback)
PendingActionClearedCallback
的实现如下,看注释是所有action清除的时候调用onCleared
/**
* A callback interface to get notified when all pending action is cleared. It can be called
* when timeout happened.
*/
interface PendingActionClearedCallback {
void onCleared(HdmiCecLocalDevice device);
}
接下来继续看disableDevices
的实现,
private void disableDevices(PendingActionClearedCallback callback) {
if (mCecController != null) {
for (HdmiCecLocalDevice device : mHdmiCecNetwork.getLocalDeviceList()) {
device.disableDevice(mStandbyMessageReceived, callback);
}
}
mMhlController.clearAllLocalDevices();
}
遍历HdmiCecNetwork
中的所有device,然后调用disableDevice
/**
* Disable device. {@code callback} is used to get notified when all pending actions are
* completed or timeout is issued.
*
* @param initiatedByCec true if this sequence is initiated by the reception the CEC messages
* like <Standby>
* @param originalCallback callback interface to get notified when all pending actions are
* cleared
*/
protected void disableDevice(
boolean initiatedByCec, final PendingActionClearedCallback originalCallback) {
removeAction(AbsoluteVolumeAudioStatusAction.class);
removeAction(SetAudioVolumeLevelDiscoveryAction.class);
mPendingActionClearedCallback =
new PendingActionClearedCallback() {
@Override
public void onCleared(HdmiCecLocalDevice device) {
mHandler.removeMessages(MSG_DISABLE_DEVICE_TIMEOUT);
originalCallback.onCleared(device);
}
};
mHandler.sendMessageDelayed(
Message.obtain(mHandler, MSG_DISABLE_DEVICE_TIMEOUT), DEVICE_CLEANUP_TIMEOUT);
}
从这里可以看到,如果想要触发disableDevices(PendingActionClearedCallback callback)
中的回调函数,首先要出发mPendingActionClearedCallback
, 然后调用 originalCallback.onCleared(device)
才会触发最开始的回调函数。那么mPendingActionClearedCallback
需要如何触发呢?
disableDevice
函数体中removeAction
的实现如下:
@ServiceThreadOnly
void removeAction(final HdmiCecFeatureAction action) {
assertRunOnServiceThread();
action.finish(false);
mActions.remove(action);
checkIfPendingActionsCleared();
}
protected void checkIfPendingActionsCleared() {
if (mActions.isEmpty() && mPendingActionClearedCallback != null) {
PendingActionClearedCallback callback = mPendingActionClearedCallback;
// To prevent from calling the callback again during handling the callback itself.
mPendingActionClearedCallback = null;
callback.onCleared(this);
}
}
可以发现,在checkIfPendingActionsCleared
中,只要if (mActions.isEmpty() && mPendingActionClearedCallback != null)
条件满足,就会触发mPendingActionClearedCallback
回调,
mPendingActionClearedCallback =
new PendingActionClearedCallback() {
@Override
public void onCleared(HdmiCecLocalDevice device) {
mHandler.removeMessages(MSG_DISABLE_DEVICE_TIMEOUT);
originalCallback.onCleared(device);
}
};
然后在mPendingActionClearedCallback
中, originalCallback.onCleared(device)
中,触发HdmiControlService.java
中disableDevices(new PendingActionClearedCallback() {
处的回调,实现关机.
到这里,我们基本可以明确,因为mPendingActionClearedCallback
在这个流程中一定不为null, 所有需要mActions.isEmpty
为空是TV关机,DVD关机流程的关键.
然后在checkIfPendingActionsCleared
中加了打印,果然在HDMI1通道下,mActions.isEmpty
不为空,还存在一个Action
后面加打印确认在HDMI1通道下,关机的时候,还存在一个RoutingControlAction
未被清除。
五、解决措施
既然确认是RoutingControlAction
未被清除, 我们在中添加这一行removeAction(RoutingControlAction.class);
代码,最终问题解决
protected void disableDevice(
boolean initiatedByCec, final PendingActionClearedCallback originalCallback) {
removeAction(AbsoluteVolumeAudioStatusAction.class);
removeAction(SetAudioVolumeLevelDiscoveryAction.class);
//在检测之前,移除RoutingControlAction,
removeAction(RoutingControlAction.class);
···
}
六、反思
1.RoutingControlAction是什么?
RoutingControl表示在CEC链路上,不是CEC设备直通TV设备, 中间有其他设备的时候,会启动RoutingControlAction来转发控制命令
2.为什么仅仅HDMI1通道异常,其他通道正常?
硬件设计的原因,使用的主芯片仅一路HDMI IN, 我司使用了HDMI switch芯片来拓展,在HDMI1链路中间多加了一个芯片
3.在disableDevice中移除RoutingControlAction,是否会影响CEC功能?
不会,disableDevice的调用时机仅为关机前。同步也找芯片厂商确认,谷歌在hdmi多次拓展的场景下兼容不太好,可以修改此处。