最近LZ接公司安排任务,移植一款CAN总线设备Mcp2515。由于在前次任务中有SPI经验,所以在接受任务是主要关注此设备采用SPI接口。所以一直没有关注CAN相关的知识,后续过程中遇到了不少麻烦,走了一些弯路。特把此次移植过程记录整理一下。
CAN总线是一种在汽车上广泛采用的总线协议,被设计作为汽车环境中的微控制器通讯。LZ理论知识有限,网上抄一句介绍的吧。如下:CAN(Controller Area Network)总线,即控制器局域网总线,是一种有效支持分布式控制或实时控制的串行通信网络。由于其高性能、高可靠性、及独特的设计和适宜的价格而广泛应用于工业现场控制、智能楼宇、医疗器械、交通工具以及传感器等领域,并已被公认为几种最有前途的现场总线之一。CAN总线规范已经被国际标准化组织制订为国际标准ISO11898,并得到了众多半导体器件厂商的支持。
我们的产品是作为CAN控制端,使用在一个电动汽车公司的汽车上的。该公司提供了单片机的终端设备进行通信调试。所以LZ这次任务还要写测试APK。这也是LZ第一次从APK到驱动的一次经验。使一次从上到下的经验,所以值得总结一下下。
此次调试是在EXYNOS4412三星四核CPU上,采用的是Android4.0.4文件系统和linux3.0.15的内核。是俺们公司的主打平台哦!吼吼!
调试开始了!
这是开发板的CAN小板图,从图上可知LZ需要连接的是VDD,CS,CLK,SI,SO,INT,RESET,VDD5.0几个pin。其中CS,CLK,SI,SO是SPI总线的常用的接口,是LZ比较熟悉的。其中SI连接MOSI控制气的发送口,SO连接MISO控制气的接收口。其他几个pin也是很容易明白的。INT中断pin,reset复位pin。在此LZ遗漏了一个严重的问题,导致后来驱动移植一直没有反应。就是LZ忽略了datasheet中的电源信息:
-工作电压范围2.7V至5.5V
- 5mA典型工作电流
Mcp2515的工作电压是2.7V至5V,这是适应了采用3.3V工作电压的CPU。但是LZ的4412的CPU采用的是1.8V。所以这里需要将上边的IOpin用1.8V转3.3V的电压转换IC进行转换。LZ后来在驱动调试中多次查问题无果后,重新仔细阅读了datasheet后才发现此问题。所以楼主在CPU与CAN设备之间添加了MAX3390E转换IC进行电压转换。
二.驱动移植
Mcp2515有标准的驱动,可以从网上找到下载,linux的内核里边也有默认的驱动。所以LZ只要配置好内核,添加设备端的配置信息就好了。以下是我的配置信息:
#ifdef CONFIG_CAN_MCP251X
static struct s3c64xx_spi_csinfo spi0_mcp251x_csi[] = { //spi总线CS片选pin配置
[0] = {
.line =EXYNOS4_GPB(1),
.set_level= gpio_set_value,
// .fb_delay =0x2,
},
};
static struct spi_board_info spi_mcp251x_board_info[] __initdata ={ //mcp251x设备信息
{
.modalias = "mcp2515",
.max_speed_hz = 6500000, //spi最大速率,配置为6500000
.bus_num = 0,
.chip_select = 0,
.mode = SPI_MODE_0, //采用的是SPI0
.controller_data= &spi0_mcp251x_csi[0],
}
};
#endif
static void __init smdk4x12_machine_init(void) //内核的机器初始部分
{
struct device *spi_mcp251x_dev = &exynos_device_spi0.dev; //设备指针指向SPI0
……
……
#ifdef CONFIG_CAN_MCP251X
sclk =clk_get(spi_mcp251x_dev, "dout_spi0"); //spi总线时钟CLK配置
if (IS_ERR(sclk))
dev_err(spi_mcp251x_dev,"failed to get sclk for SPI-0\n");
prnt =clk_get(spi_mcp251x_dev, "mout_mpll_user");
if (IS_ERR(prnt))
dev_err(spi_mcp251x_dev,"failed to get prnt\n");
if(clk_set_parent(sclk, prnt))
printk(KERN_ERR"Unable to set parent %s of clock %s.\n",
prnt->name,sclk->name);
clk_set_rate(sclk,100 * 1000 * 1000);
clk_put(sclk);
clk_put(prnt);
if (!gpio_request(EXYNOS4_GPB(1),"SPI_CS0")) {
gpio_direction_output(EXYNOS4_GPB(1),0);
s3c_gpio_cfgpin(EXYNOS4_GPB(1),S3C_GPIO_SFN(1));
s3c_gpio_setpull(EXYNOS4_GPB(1),S3C_GPIO_PULL_UP);
exynos_spi_set_info(0,EXYNOS_SPI_SRCCLK_SCLK,
ARRAY_SIZE(spi0_mcp251x_csi));
}
spi_register_board_info(spi_mcp251x_board_info,ARRAY_SIZE(spi_mcp251x_board_info)); //注册SPI设备函数
#endif
……
……
}
static struct platform_device *smdk4x12_devices[] __initdata = {
……
……
#ifdef CONFIG_CAN_MCP251X //注册SPI0设备
&exynos_device_spi0,
#endif
……
……
}
这里需要注意的是,在设备注册中不能有于此相冲突的其它设备的注册。LZ把没有用到的原有注册都注释掉了。不过还得感谢这些注册设备给我了很好的结构参考。接下来就是menuconfig的配置了。关于这一点网上有很多介绍。LZ参考了网友的信息:
1 [*]Networking support->
2 <*>CAN bus subsystem support->
3 <*>Raw CAN Protocal
4 <*>Broadcast Manage CAN Protocal
5 CAN Device Drivers->
6 <*>Platform CAN driver with Netlink support
7 [*]CAN bit-timing calculation
8 <*>Microchip MCP251x SPI CAN controllers
9
10 Device drivers->
11 [*]SPI support ->
12<*> Samsung S3C64XX series type SPI
完成以上配置后,编译内核,启动。
[ 5.585238] CAN devicedriver interface
[ 5.648409]mcp251x_power_enable power on reset
[ 5.662537] mcp251xspi0.0: probed
内核显示mcp251x 模块probe成功。LZ用示波器测量CAN设备上的时钟源晶振,显示为16MHZ,再在终端敲netcfg命令(没权限的话先su一下)。显示:
lo UP 127.0.0.1/8 0x0000004900:00:00:00:00:00
can0 UP 0.0.0.0/0 0x000000c100:00:00:00:00:00
于是LZ知道设备出来了。驱动移植至此完成。(*^__^*) 嘻嘻……
三.通信测试
CAN总线就两条连线,CANH和CANL,通常电压值为CAN_H = 3.5V 和CAN_L= 1.5V。在连接好调试板和我们的控制设备后,开始调试。由于LZ对CAN完全是空白。所以LZ只能广泛查阅网上资料。感谢网友们的积极贡献文档。LZ收获颇丰,不仅查到了测试工具以及相当多的介绍文档,还得到了一份上层的APK代码(这是老大帮忙弄到的)。于是LZ开整。
首先,测试socket CAN需要两个测试工具。iproute2和canutils。Iproute这个工具在我们的代码里边已经有一份。然后canutils在我得到的APK源码里边也有。下边是两个工具的下载地址:
下载iproute2的最新源码 http://www.kernel.org/pub/linux/utils/net/iproute2/。
下载canutils的最新源码 http://www.pengutronix.de/software/socket-can/download/canutils。
另外,因为canutils编译需要libsocketcan库的支持,需要下载libsocketcan。下载libsocketcan的最新源码http://www.pengutronix.de/software/libsocketcan/download/。
首先,使用ip命令。这是网上得到的命令:
ifconfig can0 down //关闭can0,以便配置
ip link set can0 up type can bitrate 250000 //设置can0波特率
ip -details link show can0 //显示can0信息
但是楼主发现,版本库里边的ip命令根本不支持can相关的命令。于是楼主参照上边的介绍,又下载了一份iproute2-2.6.39.tar.gz。参照网上编译过程:
(1) 解压iproute2-3.6.0.tar.xz,修改Makefile第33行。
33 #CC = gcc
34 CC = arm-none-linux-gnueabi-gcc
(2) 因为我们只需要iprout2的ip命令,所以修改Makefile的第42行。
42 #SUBDIRS=lib ip tc bridge misc netem genl man
43 SUBDIRS=lib ip
修改完成执行make命令,生成ip命令。但是LZ发现还是不支持CAN类型。LZ就郁闷了。对照版本库里边,是一样的效果。但是代码中明明有CAN相关的代码,没有调用到嘛! LZ此时错误的放弃了这个工具。然后LZ把apk中的canutils相关代码移到系统中编译。生成了cansend和candump,两个文件。参考命令为:
cansend can0 -e 0x81 0x00 0x00 0x00 0x40 0x55
candump can0
LZ发现,也没什么效果。用示波器测量CAN总线上的信号,发现对方已经不停地发送信号过来了,但是这边却还是安安静静,很害羞滴不肯回应。LZ此时有些乱了阵脚,又是查证电路问题,又是下载新的canutils-4.0.6.tar.bz2来进行编译。编译canutils-4.0.6发现少了很多头文件,一直不能编译成功。LZ这时在两头来来回回,没有收获。后老大说,还是要先从工具着手,先把ip命令不支持的问题解决。于是,LZ在仔细阅读ip相关的代码后发现,之所以IP命令不支持CAN设备,是因为CAN相关的代码是以so库的形式调用的,但是代码根本没有编译成库,所以根本没有调用。于是问题就比较清晰了。只需要把link_can做成so动态库,并修改好ip中调用库的获取路径,就能够调用的到了,同时也学习到了怎样写编译成动态库的Android.mk编译文件的相关知识。
于是IP命令成功调用到了驱动层的mcp251x.c文件中。Cansend和candump命令也终于调用到了驱动里边去了。但是却只有第一次调用到,后边就中途退出了。楼主遇到了一个likely和ulikely的问题。关于这个问题,LZ查了一些资料和解释,费了一些时间理解,才明白了它的功能。程序跑到协议层自动退出,说明上层调用或设置了错误的参数。lZ还没大胆到去怀疑协议层出来问题。所以,翻出了对方的板子的单片机的代码来阅读了一番,查阅相关信息。发现,对方CAN的传输速率为125000。于是配置了CAN bus的速率为125000。命令如下:
iplink set can0 up type can bitrate 1250000
于是,中断跑出来了,说明和设备端沟通上了,通信成功。此时应该开始测试收发数据了。candump命令比较简单,只要收数据就好了,所以楼主在测试candump的时候成功接收到了数据。但是cansend命令参照网友的介绍进行使用就没有成功。LZ查阅发送的另一端设备代码,并参考cansend help。发现,cansend的一个参数可能要添加。就是设备ID,cansend的i选项。接收端的设备ID为8。于是cansend命令如下:
cansend can0 -i 8 -e 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88
于是接收端屏上反应出数据有改变,通信成功。
Cansend帮助文档如下:
shell@android :/ # cansend
Usage: cansend [<can-interface>] [Options] <can-msg>
<can-msg> can consist of up to 8 bytes given as a spaceseparated list
Options:
-i, --identifier=ID CAN Identifier (default = 1)
-r --rtr send remote request
-e --extended send extended frame
-f, --family=FAMILY Protocol family (default PF_CAN = 29)
-t, --type=TYPE Socket type, see man 2 socket (defaultSOCK_RAW = 3)
-p, --protocol=PROTO CAN protocol (default CAN_RAW = 1)
-l send message infinitetimes
--loop=COUNT send message COUNT times
-v, --verbose be verbose
-h, --help this help
--version print version information and exit
四.写测试apk
最后就是要写测试apk了,毕竟人家用户是不可能在终端上使用的嘛。LZ手上有一份apk测试代码的示例。由于没有太多写apk的经验,也问同事要了一些参考资料。
LZ发现,写apk有两种,一种是系统相关的apk,一种是完全独立的apk。我写的是系统相关的。因为我是在系统里边添加代码进行编译。独立apk是用ndk进行编译。
我在学习示例代码后理解,要调用到驱动层。上层代码可以分层四个部分。Package,service,jni,lib库。Package和service之间用一个aidl文件连接。而jni是service和库之间的连接。Package层主要做了两个按键和一个textView文本显示框。采用后台进程的方式candump数据并显示到textView中。后台进程用的是AsyncTask方式。其中碰到了好多问题,都是由于对JAVA太不熟悉的缘故,常常用C的方式理解。然后是service,由于功能非常简单,所以都没什么操作。最主要就是把函数接口调用下去就行。它的调用是在frameworks/base/services/java/com/android/server/SystemServer.java中添加为默认启动的服务,则可以在开机后默认启动此服务,代码如下。
//yym add 20130329 str line:481
try {
Slog.i(TAG,"Flexcan Service");
ServiceManager.addService("flexcan", new FlexcanService());
} catch(Throwable e) {
Slog.e(TAG,"Failure starting Flexcan Service", e);
}
//yym add 20130329 end
第三个部分是jni。JNINativeMethod方式。系统的jni需要在frameworks/base/services/jni/onload.cpp中添加注册,楼主照样添加了:
intregister_android_server_FlexcanService(JNIEnv* env);
register_android_server_FlexcanService(env);
JNI层的代码调用service层的功能时有一个专门的调用方式,LZ在使用时可能是字符敲错了。移植失败。这里记录下来。
jclassframe_cls = env->FindClass("com/android/server/Frame");
if(frame_cls== NULL) {
LOGE("FlexcanJNI: find class FlexcanService error!!");
returnNULL;
} //首先获取类的源
1.调用处理单个数据的函数。
jmethodIDsetDlc = env->GetMethodID(frame_cls,
"setDlc","(I)V"); //映射类的方法函数过来
if( setDlc== NULL) {
LOGE("FlexcanJNI: setDlc error!!");
returnNULL;
}
jobjectmyFrame = frame;
if(myFrame==NULL) {
LOGE("FlexcanJNI: frame NULL error!!");
returnNULL;
}
env->CallVoidMethod(myFrame,setID,can_id); //调用方法函数,并传递参数。
2.调用处理buffer多个数据的函数
jmethodID setBuf= env->GetMethodID(frame_cls,
"setBuf","([I)V");
if(setBuf==NULL){
LOGE("FlexcanJNI: setBuf error!!");
returnNULL;
} //映射类的方法函数过来
jobjectmyFrame = frame;
if(myFrame==NULL) {
LOGE("FlexcanJNI: frame NULL error!!");
returnNULL;
}
jintArrayarr;
arr =env->NewIntArray(8); //new一个数组
if(arr ==NULL) {
LOGE("FlexcanJNI: arr init error!!");
returnNULL;
}
env->SetIntArrayRegion(arr,0,8,data); //将数据传到数组里边去
env->CallVoidMethod(myFrame,setBuf, arr); //调用方法函数,并传递参数。
env->DeleteLocalRef(arr); //销毁数组
最后,JNI就调用到了lib层里边了。这里也是主要功能处理的地方。从上边我用终端命令IP,cansend,candump进行通信的过程来看,过程如下:
1. ip link set can0 up type canbitrate 125000
2. ip link set can0 up type can
3. cansend can0 -i 8 -e 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88
4. candump can0
所以,我只要将上述命令的处理过程从原函数代码中移植过来就可以了。但是LZ发现,设置bitrate这个步骤可以再mcp2515的驱动中更加简便的设置,由于我们的设备不需要兼容其它别的设备,bitrate还不需要更改,所以就取巧了一下,将bitrate在驱动中直接默认设置好了。免去了上层的设置。然后把其它三个功能移植分别移植到了can_init,can_native_dump,can_native_send三个函数中。支持从上到下的功能基本调通。测试的apk基本写成。功能完善还要到后边确定这个case可以开始时再进行。
LZ此 次移植,波折还是有那么几个,但也都终于跳出来了。发现,之所以有那么多问题和波折,主要是对架构不是很理解,思路不清晰。所以很容易在出问题的时候乱了 阵脚。其次,细心的查阅资料,从中获取需要的信息也是非常重要的。看文档不仔细的后果那是,哎,绕了好多圈啊,说多了都是泪啊!不过终于完成了!吼吼。俺 要做下个任务去了。