问题描述:
Modem已经注册成功了,但是android系统显示仍然是无网状态。
背景知识:
Android为了屏蔽各Modem厂商之间的差异,并未直接使用AT指令,而是定义了通用接口——RIL命令来控制Modem(当然,RIL命令与AT指令之间存在某种不固定的映射关系,因厂商而异)。各Modem厂商则负责提供对应于RIL命令的Vender RIL。RIL是Modem的HAL(硬件抽象),是android系统与Modem之间的通信桥梁,为AP提供了控制Modem的无线接口,RIL实现了Android平台与Modem间的分离。
Android的通信栈共由4层构成:
1、应用程序——利用通信框架提供的API访问Modem;
2、通信框架——为上层提供API,负责将上层请求转成RIL指令,通过UNIX SOCKET(该socket可在/dev/socket/目录下找到,名为rild)方式传递到RIL守护进程(通常双卡系统会有两个rild进程,可通过“ps | grep rild”命令查看),并处理来自RIL进程的回复,将结果传递给上层;
3、RIL层————RIL守护进程利用Vender RIL控制Modem;
4、Vender RIL——是实际控制Modem的软件,由厂商提供;实现了android定义的标准Modem控制接口。它是*.so形式的共享库。
问题确认:
1、Radio日志包含了RIL层与Modem之间的交互信息(RIL指令),以及android通讯框架层的所有相关日志信息;Modem日志记录了其与基站的所有交互(鉴权)过程。因此分析通讯相关问题,Radio日志和Modem日志都是关键资料。
2、通过获取并分析Radio和Modem日志,确认是两边状态未同步所致(Modem日志明确显示已经通过鉴权注册成功)。
探索思路:
首先我们认定这是android系统通信框架的BUG,所以希望通过直接与Modem通信,而绕开android系统的通信框架。 理论上,的确是可以绕过android系统的通讯框架,而直接与RIL层通信,并通过RIL层发指令对Modem进行控制的; 但是首先,需要你伪装成Radio进程,其次一旦你的程序与RIL守护进程建立连接,那么android系统的通讯框架(包括上层Phone应用)将会与RIL失去连接,以致于功能失效; 因此,你的程序务必功能足够完善,能够模拟android通讯框架的绝大部分关键功能,其次你的程序还需要足够健壮,保证不会导致系统Crash。 顺着这个思路,实现的难度之大、开发调试的周期之长,可想而知。并且,有点“重复造轮子”的意味。因此,调试android系统框架层、及RIL层代码成为最(甚至是唯一)可行的方向。
问题解决:
由于笔者所参与开发的android设备,采用了虚拟sim卡(软sim)技术,所以发起扫卡和注册的时间并不固定。 通常的android设备只会在开机,或者打开飞行模式再关闭时,触发扫卡和注册,但采用虚拟sim卡技术的android设备,可能在任意时间点触发。基于这个差异,笔者发现了android通信框架与Modem状态未同步问题的必现场景——在开机等待了一定时间(超过1分半钟)后,再发起注册。
而后经过不懈努力,终于找到了AP侧与BP侧状态不同步的蛛丝马迹——有一个状态取值on和off的变量radio(可能象征了设备的无线射频是否开启),决定框架层是否会调用“> OPERATOR”等这些RIL指令去查询Modem的实际注册状态。如果radio的状态为off,框架层的确是不会发起注册结果相关的任何查询的,而在AP和BP采用异步通信的场景下,不查询意味着即使Modem注册成功,android系统也是不会知道的。
而正好发现,笔者开发的设备在开机等待了一定时间后radio的状态就off,且不会再自己on了。于是抓住radio状态这个关键点做文章,最终把这个问题成功修复了。
问题总结:这是android系统的一个BUG(或者说android系统并未考虑到虚拟sim卡这种应用场景)
前段时间再为我们公司的模块产品sim5210写linux下的驱动, sim5210使用的是qualcomm的6280芯片, 该平台提供了USB功能, 并再USB之上提供了modem, diag, nmea等设备接口, 即再usb之上我们的模块实现了modem功能, diag(诊断)功能, nmea(gps)功能, 由于在linux下有现成的标准CDC modem的驱动, 所以我们以此为参考实现了我们的非标准modem, 和用于收发at command的diag的驱动. 下面我们重点总结一下modem驱动的编写方法, diag类似.
我们的modem驱动是tty+usb的层次关系, 即上层跟用户交互的接口使用tty驱动类型, 初始化时先注册一个设备文件, 以后用户使用时直接通过这个设备文件与设备打交道, 当用户的操作传递到我们的tty驱动层后, 在我们的驱动中在把这些操作(read, write)转化为对usb设备的操作, 最后通过usb总线与usb设备交互, 整个驱动结构如下:
下面详细讲解这个驱动的整个流程:
1 通过module_init() 和 module_exit()注册我们驱动的init和exit函数, init函数将会在驱动加载时被调用, exit函数将会在驱动卸载时被调用.
2 我们的init函数是
static int __init qcmdm_init()
{
/*1 调用alloc_tty_driver()分配一个tty_driver对象: qcmdm_tty_driver */
/*2 初始化qcmdm_tty_driver 的相关field*/
/*3 调用tty_set_operations 为qcmdm_tty_driver 设好操作函数表, 即tty驱动的函数表*/
/*4 调用tty_register_driver()把qcmdm_tty_driver注册到系统中去*/
/*调用usb_register()注册我们的usb驱动对象 qcmdm_usb_driver(静态分配并初始化好的)*/
return 0;
}
通过这一步, 我们注册了usb驱动对象, 和tty驱动对象, 但这时设备文件还没创建好,因此这个tty驱动还没法用, 当modem查上后linux内核会调用我们先前注册的usb驱动对象中的probe函数(整个调用流程可以参考我写的另一篇文章<
我们的exit函数是:
static void __exit qcmdm_exit()
{
/*1 调用tty_unregister_driver() 来卸载qcmdm_tty_driver*/
/*2 调用put_tty_driver() 来释放qcmdm_tty_driver占用的内存*/
/*3 调用 usb_deregister()来下载qcmdm_usb_driver*/
}
3 我们qcmdm_usb_driver的probe函数:
static int qcmdm_probe(struct usb_interface *intf, /*匹配的接口*/
const struct usb_device_id *id /*驱动支持的设备表项*/)
{
/*
1 根据设备的配置判断这个interface是否就是我们驱动支持的接口,
一般到了这步说明这个接口匹配了我们的device_id列表(这是我们驱动
提供的用于判断一个接口是否是我们驱动支持的这么一个列表), 但由于我们
的设备是个多接口的设备,所以这里还要进一步判断是否匹配,
*/
/*
2 检测设备的其他信息是否和我们的真是设备的信息匹配(如端点数量,端点类型等)
*/
/*
* 3 一般每个驱动都会定义一个记录我们设备信息的私有数据结构,我的驱动中定义的是
* strcut qcmdm, 这里我们分配一个qcmdm的对象mdm,并初始化好它的各个字段(如保存各个端点信息等)
*/
/*4 我们在mdm中保存了一个读和写的urb列表, 这里我们为每个urb分配内存(usb_alloc_urb), 并为每个每个urb分配DMA缓冲(usb_buffer_alloc). 并初始化好每个urb(usb_fill_xxx_urb())*/
/*5 调用tty_register_device() 来把qcmdm_tty_driver和这个interface绑定起来,并创建设备文件, 以后对这个设备文件的操作就会映射到qcmdm_tty_driver的操作函数.*/
}
此外,我们还可以在probe中保存额外的一些信息, 如调用usb_set_intfdata(intf, mdm)来将mdm保存在intf的相关字段中, 这样在以后使用中可以直接通过usb_get_intfdata()来获取mdm指针.
4我们qcmdm_usb_driver的disconnect函数(在设备拔出时调用): 这个函数主要做一些和probe相反的工作
static void qcmdm_disconnect(struct usb_interface *intf)
{
/*1 删除urb (usb_kill_urb())*/
/*2 reset 读写list*/
/*3 释放相关资源, 如urb memory, usb dma memory等*/
/*4 调用tty_unregister_device()来释放驱动和设备的绑定,并删除设备文件*/
/*5 其他资源的释放或reset*/
}
总之在probe中申请了什么资源应该在disconnect中释放掉, 因为下次设备连上后,又会调用probe, 此时又会去分配资源.
5 打开函数: 这个函数是tty driver的一部分, 当打开我们创建的设备文件时会调用这个函数, 它的工作主要也是做一些初始化工作, 并调用usb_submit_urb()提交一个中断类型的urb(因为我们的设备上有个interrupt的端点, 用于设备上报一些自身状态的改变信息, 所以这里我们应该准备好接收这些状态信息)
6 write函数: 这也是tty driver的一部分, 在我们的驱动中保存了一个用于写usb的缓冲链表, 每次要写时就去获取一个空闲表项, 并把端点,数据等信息填充号这个表项中的urb, 特别的一般我们还会填充一个我们自己的用于完成后的回调函数. 然后调用usb_submit_urb提交这个写操作, 在usb写完或错误后会调用我们的回调函数(注意这是运行在中断上下文的), 这样我们就知道成功与否.
7 read函数: 这也是tty driver的一部分跟write相似,也有一个用于读的链表, 每个读操作也是先填充urb, 然后在提交,在完成后调用我们的回调函数, 此时我们得到了数据, 可以通过tty_insert_flip_string()和tty_flip_buffer_push()把数据提交到tty核心,在由tty核心交给用户. 由于在usb核心收到数据后应该快速的返回给用户, 因此在读的部分, 我们使用了tasklet, 以加快数据读出,又可避免直接在中断上下文读数据.
8 其他函数相对简单,在此不作介绍