近一段时间遇到一个CEC功能失效的bug,即TV关机,DVD没有关机。因为CEC功能涉及 app–> framework --> hal --> kernel,任何一个环节出问题,都可能造成DVD关机失败,所以需要查看android源码来分析原因。第一次从framework 层到kernel一层一层看。感觉还是比较有意思的,在这里分享一下过程。
背景
CEC功能现在用的不多,大家听着玩一下就可以了。CEC概念 CEC是HDMI模块里面的一个功能。CEC标准功能里面有一条:TV设备关机的时候,会顺便把DVD关闭。在Android TV的实现原理是,HdmiControlService里面监听android的关机广播,然后发送CEC指令给到DVD设备,dvd设备关机。下面我们就直接开始从源码一层一层分析,看一下整个CEC关机的流程
framework层
首先我们来到HdmiControlService监听广播的地方
private class HdmiControlBroadcastReceiver extends BroadcastReceiver {
300 @ServiceThreadOnly
301 @Override
302 public void onReceive(Context context, Intent intent) {
... ...
322 case Intent.ACTION_SHUTDOWN:
323 if (isPowerOnOrTransient() && !isReboot) {
324 onStandby(STANDBY_SHUTDOWN);
325 }
326 break;
327 }
328 }
329
330 }
如上代码,监听到case Intent.ACTION_SHUTDOWN的广播,通过onStandby发送关机指令,然后我们看下onStandby的实现。
(不一定是在这里调用关机,详细分析可以查看这里)
protected void onStandby(final int standbyAction) {
... ...
3332
3333 final List<HdmiCecLocalDevice> devices = getAllLocalDevices();
3334
3335 if (!isStandbyMessageReceived() && !canGoToStandby()) {
3336 mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_STANDBY);
3337 for (HdmiCecLocalDevice device : devices) {
3338 device.onStandby(mStandbyMessageReceived, standbyAction);
3339 }
3340 return;
3341 }
3342 ...
3356 }
接着看HdmiCecLocalDevice中的onStandby函数实现
protected void onStandby(boolean initiatedByCec, int standbyAction) {}
啊哈,是个空实现,不要激动,我们拉到最上面看一下,HdmiCecLocalDevice的实现就明白了
abstract class HdmiCecLocalDevice {
...
}
final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
...
}
他是一个抽象类,因为我们是TV设备,所以我们需要在HdmiCecLocalDeviceTv.java中找对应的实现
64 protected void onStandby(boolean initiatedByCec, int standbyAction) {
... ...
1374 if (!initiatedByCec && sendStandbyOnSleep) {
1375 mService.sendCecCommand(
1376 HdmiCecMessageBuilder.buildStandby(
1377 getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST));
1378 }
1379 }
这里我们就可以看到,是要发送cec指令了,看样子是回到了HdmiControlService类了,接着往下面走
1238 void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
1239 assertRunOnServiceThread();
1240 if (command.getValidationResult() == HdmiCecMessageValidator.OK
1241 && verifyPhysicalAddresses(command)) {
1242 mCecController.sendCommand(command, callback);
1243 } else {
1244 HdmiLogger.error("Invalid message type:" + command);
1245 if (callback != null) {
1246 callback.onSendCompleted(SendMessageResult.FAIL);
1247 }
1248 }
1249 }
我们接着去HdmiCecController 中查看sendCommand的实现
651 void sendCommand(final HdmiCecMessage cecMessage,
652 final HdmiControlService.SendMessageCallback callback) {
.... ....
655 runOnIoThread(new Runnable() {
656 @Override
657 public void run() {
658 HdmiLogger.debug("[S]:" + cecMessage);
659 byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
660 int retransmissionCount = 0;
661 int errorCode = SendMessageResult.SUCCESS;
662 do {
663 errorCode = mNativeWrapperImpl.nativeSendCecCommand(
664 cecMessage.getSource(), cecMessage.getDestination(), body);
665 if (errorCode == SendMessageResult.SUCCESS) {
666 break;
667 }
668 } while (retransmissionCount++ < HdmiConfig.RETRANSMISSION_COUNT);
669
... ....
690 }
代码量有点大,但是我们主要关注nativeSendCecCommand函数即可,从函数的名字上面来看,应该是要到下一层了
errorCode = mNativeWrapperImpl.nativeSendCecCommand(cecMessage.getSource(), cecMessage.getDestination(), body);
接着来看nativeSendCecCommand的实现
private android.hardware.tv.cec.V1_1.IHdmiCec mHdmiCec;
public int nativeSendCecCommand(int srcAddress, int dstAddress, byte[] body) {
1056 CecMessage message = new CecMessage();
1057 message.initiator = srcAddress;
1058 message.destination = dstAddress;
1059 message.body = new ArrayList<>(body.length);
1060 for (byte b : body) {
1061 message.body.add(b);
1062 }
1063 try {
1064 return mHdmiCec.sendMessage(message);
1065 } catch (RemoteException e) {
1066 HdmiLogger.error("Failed to send CEC message : ", e);
1067 return SendMessageResult.FAIL;
1068 }
1069 }
链路好长啊,从这里看,我们应该是要达到hal层了
硬件抽象层【Hal层】
欢迎来到hal层,这里也是我收获最多的地方。我们接着看代码
309 Return<SendMessageResult> HdmiCec::sendMessage(const CecMessage& message) {
310 cec_message_t legacyMessage {
311 .initiator = static_cast<cec_logical_address_t>(message.initiator),
312 .destination = static_cast<cec_logical_address_t>(message.destination),
313 .length = message.body.size(),
314 };
315 for (size_t i = 0; i < message.body.size(); ++i) {
316 legacyMessage.body[i] = static_cast<unsigned char>(message.body[i]);
317 }
318 return static_cast<SendMessageResult>(mDevice->send_message(mDevice, &legacyMessage));
319 }
初看到我是一脸懵逼,还是请教了公司的同事,才大概知道hal层的一些规范。当然了,他也是发我了一些文档,不过文档内容质量很高,分享给大家 Android硬件抽象层(HAL)概要介绍和学习计划我们不着急接着追接口,先来学习一下基础知识。
1.为什么需要hal层
Android硬件抽象层(HAL)概要介绍和学习计划写的很清楚。大概总结一下就是,Android 是基于Linux的,但是Linux是需要开源的。各个厂家的一下硬件模块参数或算法是有一定商业价值,不想公开,因此就有了硬件抽象层的概念。厂家可以将重要的实现放到Hal层来实现,Kernel只需要提供对外的接口能力即可。当然这个东西也相当于一个投机取巧的办法,也是因为这个原因,Android被踢出了Linux内核主线代码树中。
2.Hal层代码流程
(参考:在Ubuntu上为Android增加硬件抽象层(HAL)模块访问Linux内核驱动程序)
①首先我们需要三个东西:定义模块ID、硬件模块结构体、硬件接口结构体。我们通过CEC的hal层实现来验证一下我们的结论。
代码量较大,我仅保留重要内容
#ifndef ANDROID_INCLUDE_HARDWARE_HDMI_CEC_H
18 #define ANDROID_INCLUDE_HARDWARE_HDMI_CEC_H
... ...
32
33 #define HDMI_CEC_HARDWARE_MODULE_ID "hdmi_cec" //模块ID
34 #define HDMI_CEC_HARDWARE_INTERFACE "hdmi_cec_hw_if"
//硬件模块结构体
typedef struct hdmi_cec_module {
288 /**
289 * Common methods of the HDMI CEC module. This *must* be the first member of
290 * hdmi_cec_module as users of this structure will cast a hw_module_t to hdmi_cec_module
291 * pointer in contexts where it's known the hw_module_t references a hdmi_cec_module.
292 */
293 struct hw_module_t common;
294 } hdmi_module_t;
...
... //硬件接口结构体
/*
297 * HDMI-CEC HAL interface definition.
298 */
299 typedef struct hdmi_cec_device {
300 /**
301 * Common methods of the HDMI CEC device. This *must* be the first member of
302 * hdmi_cec_device as users of this structure will cast a hw_device_t to hdmi_cec_device
303 * pointer in contexts where it's known the hw_device_t references a hdmi_cec_device.
304 */
305 struct hw_device_t common;
... ....
341 int (*get_physical_address)(const struct hdmi_cec_device* dev, uint16_t* addr);
342
343 /*
344 * (*send_message)() transmits HDMI-CEC message to other HDMI device.
345 *
346 * The method should be designed to return in a certain amount of time not
347 * hanging forever, which can happen if CEC signal line is pulled low for
348 * some reason. HAL implementation should take the situation into account
349 * so as not to wait forever for the message to get sent out.
350 *
351 * It should try retransmission at least once as specified in the standard.
352 *
353 * Returns error code. See HDMI_RESULT_SUCCESS, HDMI_RESULT_NACK, and
354 * HDMI_RESULT_BUSY.
355 */
356 int (*send_message)(const struct hdmi_cec_device* dev, const cec_message_t*);
... ....
413 } hdmi_cec_device_t;
②实现对应接口
既然我们现在已经在头文件中定义了对应的接口了,那下一步就需要实现对应接口。实现接口的时候,有什么流程呢?
我们根据hdmi_module_t前面的硬件模块结构体来看下
36 hdmi_module_t HAL_MODULE_INFO_SYM = {
37 common: {
38 tag: HARDWARE_MODULE_TAG,
39 version_major: 1,
40 version_minor: 0,
41 id: HDMI_CEC_HARDWARE_MODULE_ID,
42 name: "Rockchip hdmi cec module",
43 author: "Rockchip",
44 methods: &hdmi_cec_module_methods,
45 }
46 };
这里,实例变量名必须为HAL_MODULE_INFO_SYM,tag也必须为HARDWARE_MODULE_TAG,这是Android硬件抽象层规范规定的。
找到了对应实现hdmi_module_t 文件,我们就知道了,接口实现在hdmi_cec_device_open中
32 static struct hw_module_methods_t hdmi_cec_module_methods = {
33 open: hdmi_cec_device_open
34 };
我们接着看hdmi_cec_device_open函数
static int hdmi_cec_device_open(const struct hw_module_t* module, const char* name,
451 struct hw_device_t** device)
452 {
453 if (strcmp(name, HDMI_CEC_HARDWARE_INTERFACE))
454 return -EINVAL;
455
456 struct hdmi_cec_context_t *dev;
457 dev = (hdmi_cec_context_t*)malloc(sizeof(*dev));
458
459 /* initialize our state here */
460 memset(dev, 0, sizeof(*dev));
461
462 dev->enable = true;
463 dev->system_control = false;
464 dev->cec_init = false;
465 /* initialize the procs */
466 dev->device.common.tag = HARDWARE_DEVICE_TAG;
467 dev->device.common.version = HDMI_CEC_DEVICE_API_VERSION_1_0;
468 dev->device.common.module = const_cast<hw_module_t*>(module);
469 dev->device.common.close = hdmi_cec_device_close;
470
471 dev->device.add_logical_address = hdmi_cec_add_logical_address;
472 dev->device.clear_logical_address = hdmi_cec_clear_logical_address;
473 dev->device.get_physical_address = hdmi_cec_get_physical_address;
474 dev->device.send_message = hdmi_cec_send_message;
475 dev->device.register_event_callback = hdmi_cec_register_event_callback;
476 dev->device.get_version = hdmi_cec_get_version;
477 dev->device.get_vendor_id = hdmi_cec_get_vendor_id;
478 dev->device.get_port_info = hdmi_cec_get_port_info;
479 dev->device.set_option = hdmi_cec_set_option;
480 dev->device.set_audio_return_channel = hdmi_cec_set_audio_return_channel;
481 dev->device.is_connected = hdmi_cec_is_connected;
482 dev->phy_addr = 0;
483 dev->fd = open(HDMI_DEV_PATH,O_RDWR,0);
484 ALOGE(HDMI_DEV_PATH);
485 ALOGE("\n");
486 if (dev->fd < 0) {
487 ALOGE("%s open error!", __func__);
488 ALOGE("cec %s\n", strerror(errno));
489 }
490 ALOGI("%s dev->fd = %d", __func__, dev->fd);
491 property_set("vendor.sys.hdmicec.version",HDMI_CEC_HAL_VERSION);
492 *device = &dev->device.common;
493 init_uevent_thread(dev);
494
495 ALOGI("rockchip hdmi cec modules loaded");
496 return 0;
497 }
看到这里dev->device.send_message = hdmi_cec_send_message;
,是不是觉得特别眼熟,没错这个就是我们前面的
然后后面就是RK3588的具体实现了hdmi_cec_send_message,这个是各个厂家自己实现的内容,我将整个函数贴出来吐槽一下。实在是太丑了。整个函数长达100多行,且if 里面套if ,还接着套for循环,for循环里面又是各种if else。且变量定义的也没有一点标准,追到这里,实在头疼。大家可以不用看这段代码实现内容,仅仅是吐槽一下。
(实际我们出现没有关机的情况,是因为在整个函数里面的某个场景下,直接return了。不太适合我司的业务场景,向RK研发确认,可以删除。删除后确认可以解决问题)
static int hdmi_cec_send_message(const struct hdmi_cec_device* dev, const cec_message_t* message)
256 {
257 struct hdmi_cec_context_t* ctx = (struct hdmi_cec_context_t*)dev;
258 struct cec_msg cecframe;
259 int i, ret = 0;
260
261 if (!ctx->enable) {
262 ALOGE("%s cec disabled\n", __func__);
263 return -EPERM;
264 }
265
266 if (ctx->fd < 0) {
267 ALOGE("%s open error", __func__);
268 return -ENOENT;
269 }
270
271 memset(&cecframe, 0, sizeof(struct cec_msg));
272 if (message->initiator == message->destination) {
273 struct cec_log_addrs log_addr;
274
275 ret = ioctl(ctx->fd, CEC_ADAP_G_LOG_ADDRS, &log_addr);
276 if (ret) {
277 ALOGE("%s get logic address err ret:%d\n", __func__, ret);
278 return -EINVAL;
279 }
280
281 ALOGD("kernel logic addr:%02x, preferred logic addr:%02x",
282 log_addr.log_addr[0], message->initiator);
283 if (log_addr.log_addr[0] != CEC_LOG_ADDR_INVALID && log_addr.log_addr[0]) {
284 ALOGI("kernel logaddr is existing\n");
285 if (log_addr.log_addr[0] == message->initiator) {
286 ALOGI("kernel logaddr is preferred logaddr\n");
287 return HDMI_RESULT_NACK;
288 } else {
289 ALOGI("preferred log addr is not kernel log addr\n");
290 return HDMI_RESULT_SUCCESS;
291 }
292 } else {
293 ALOGI("kernel logaddr is not existing\n");
294 if(!set_kernel_logical_address(ctx, message->initiator)) {
295 for (i = 0; i < 5; i++) {
296 if (!ctx->phy_addr || ctx->phy_addr == 0xffff) {
297 ALOGE("phy addr not ready\n");
298 usleep(200000);
299 } else {
300 break;
301 }
302 }
303 }
304 if (i == 5) {
305 ALOGE("can't make kernel addr done\n");
306 return HDMI_RESULT_FAIL;
307 } else {
308
309 return HDMI_RESULT_NACK;
310 }
311 }
312 }
313
314 cecframe.msg[0] = (message->initiator << 4) | message->destination;
315 cecframe.len = message->length + 1;
316 cecframe.msg[1] = message->body[0];
317 ALOGI("send msg LEN:%d,opcode:%02x,addr:%02x\n",
318 cecframe.len ,cecframe.msg[1],cecframe.msg[0]);
319 if (cecframe.len > 16)
320 cecframe.len = 0;
321 for (ret = 0; ret < cecframe.len; ret++)
322 cecframe.msg[ret + 2] = message->body[ret + 1];
323 if (cecframe.msg[1] == 0x90)
324 cecframe.msg[2] = 0;
325
326 ret = ioctl(ctx->fd, CEC_TRANSMIT, &cecframe);
327
328 if (ret < 0) {
329 ALOGE("ioctl err:%d\n", ret);
330 return HDMI_RESULT_FAIL;
331 }
332 if (cecframe.tx_status & CEC_TX_STATUS_NACK) {
333 ALOGE("HDMI_RESULT_NACK\n");
334 return HDMI_RESULT_NACK;
335 }
336 else if (cecframe.tx_status & CEC_TX_STATUS_OK) {
337 ALOGE("HDMI_RESULT_SUCCESS\n");
338 return HDMI_RESULT_SUCCESS;
339 }
340 else {
341 ALOGE("HDMI_RESULT_BUSY\n");
342 return HDMI_RESULT_BUSY;
343 }
344 return HDMI_RESULT_FAIL;
345 }
看到整个接口中的ioctl函数我们自然的就想到了,要到达kernel层了。Kernal层的实现各个厂家实现不一致,且没有参考意义,就不很细的看了。基本逻辑都是遵循CEC标准发送指令。
Kernel 层
我们回到hdmi_cec_device_open函数,可以看到
#define HDMI_DEV_PATH "dev/cec0"
然后我们全局搜"dev/cec0",即可找到对应的kernel代码。
总结
最终总结时序图如下: