1.总结
从事嵌入式行业多年,虽然因为工作原因接触过嵌入式Linux,也参与过相关产品的底层和应用功能开发,但对于嵌入式Linux的内核,驱动,以及上层开发,仍然停留在初级的水平,没有过系统深入的去总结整理,随着工作年限的递增,越来越感受到这种浮躁感带来的技术面瓶颈。既然发现了问题,自然就要去解决,回想起我踏入嵌入式行业来的经历,正是对STM32芯片以及网络部分的学习总结笔记支撑我走到如今的地步,那么沉淀下来,从嵌入式Linux入门开始整理,层层深入,对嵌入式Linux进行系统的总结也是最符合我目前现状的解决办法,这也是我下定决心放弃日常娱乐,开始本系列的由来。
嵌入式Linux的掌握学习是很复杂的过程,从最基础的Linux安装,shell指令的学习和应用,交叉编译环境搭建,C语言开发,Linux内核接口,Linux系统接口,在掌握了前面所有知识后,才只是完成了产品开发的基础构建,这些知识不仅对于学习是难点,对于已经掌握的人来用文字描述清楚,特别是系统/软件版本引发的编译,调试问题,如果没有总结和整理,这部分经验是文字很难描述的,嵌入式Linux是一门应用开发技术,多练多总结才能积累足够的知识。另外如果遇到问题,不要着急,要善于使用搜索引擎,嵌入式Linux开发遇到的问题基本都能找到答案,但找到解决方法只是目的之一,如何从这些方法中总结经验,也是学习中的重要部分,这部分对于开发者更加重要,切记!这是我做嵌入式软件开发来最重要的经验。按照正常的预期流程,嵌入式Linux的学习应该是讲如何注册字符型设备,然后按照从易到难的顺序在掌握中断和时钟,文件系统,块设备,I2C驱动,LCD驱动,摄像头驱动,网络设备驱动,设备树,然后在讲述涉及上层的QT界面,远程访问的网络socket(B/S, C/S框架),以及应用端的Android平台开发,多线程,多进程同步等知识,这也是大部分开发板的例程方案,可从我经验来看,如果按照上面的流程是可以覆盖嵌入式Linux的主要工作需求的(可能部分知识是溢出的)。但是对于开发产品来说,这些只是基础的技术,而不是应用的产品方案,事实上,对于刚入门的来说,如何从学习思维转变为工程师开发思维这部分更加重要,从更高维的角度了解嵌入式Linux开发,这也是本系列的目的。我们先制定一个产品目标(可能不符合现有的产品模型),所有学习都围绕着此产品来开发。这个系列将不仅仅讲述学习嵌入式,而且也讲述我根据工作积累的开发经验,如何完成项目,也方便未踏入行业的人员什么是嵌入式软件开发。
题目1:基于串口(RS485/RS232)的局域网管理设备
系统架构
硬件说明
正点原子的I.MX6U-ALPHA开发平台,256MB(DDR3)+256MB/512MB(NAND)核心板。涉及硬件 RS232,GPIO,I2C,SPI, ADC, DAC
学习笔记章节
嵌入式Linux学习笔记(二) 交叉编译环境和Linux系统编译、下载
嵌入式Linux学习笔记(三) 字符型设备驱动--LED的驱动开发
嵌入式Linux学习笔记(五) 通讯协议制定和下位机代码实现
嵌入式Linux学习笔记(六) 上位机QT界面实现和通讯实现代码路径
详细代码见:https://github.com/zc110747/remote_manage
软件说明
1.上位机软件支持串口通讯,双机通讯需要制定协议(可使用自定义协议或者Modbus),支持界面化管理(目前定义使用QT开发, 与后续的完善计划有关)2.支持文件传输,文件传输支持断点重传(传输后文件位于指定文件夹,初步定义为/usr/download)
3.能够查询内部的一些数据,除显示已经列出状态外,支持后期扩展查询其他状态
任务分解
1. uboot,内核和文件系统的编译,下载和调试,并集成ssh方便传输应用文件调试
2. 分模块完成驱动的开发调试,不过为了方便测试及后期集成,需要同步完成串口驱动,串口通讯协议定义及上位机的软件框架
3. 后期的综合性功能调试和应用开发(如协议扩展问题,状态查询到界面显示,考虑到协议数据的复用, 后期该数据可能用于网页界面的状态显示或者QT界面的控制)
参考资料
1. 宋宝华《Linux设备驱动开发详解 -- 基于最新的Linux4.0内核》第四章 Linux内核模块
内核模块初探
本节作为整个系列的起点,重点当然是上面的项目规划和任务分解,不过为了让文章更丰富,我们可以初步体验下Linux下的应用和编程,下面代码将执行在Ubuntu系统,PC端,事实上PC端的Ubuntu可以验证很多实现,如加载驱动和设备,实现QT界面,进行网络通讯的应用端测试,所以一定不要忽略这个优势,本小节的代码都是在PC端测试完成,用于体验内核模块开发的特征。作为内核模块,可以通关Kernel编译时加入到内核中,也可以通过insmod/rmmod动态的加载到系统中,为了满足Linux系统的访问,内核模块就需要实现接口用于Linux访问,开发者只要按照规则用C语言实现这些需要的接口,在按照一定的规则编译后,就可以使用lsmod/rmmod来加载和移除自定义的模块,这套规则就是我们掌握内核模块需要学习的知识,按照功能分为以下接口:
必须模块
模块加载函数:module_init(func)
模块卸载函数: module_exit(func)
模块许可声明:MODULE_LICENSE("xxx") 支持的许可有: "GPL", "GPL V2", "GPL and additional right", "Dual BSCD/GPL", "DUAL MPL/GPL", "Proprietary"
可选模块
模块参数 -- 模块加载时传递变量 module_param(name, charp, S_IRUGO);
模块导出符号 --用于将符号导出,用于其它内核模块使用。
EXPORT_SYSMBOL(func)/EXPORT_SYSMBOL_GPL(func)
注意:Linux内核2.6增加了函数校验机制,后续模块需要引入时要在Module.symvers下添加导入函数内核的路径和symbol。
模块作者 -- MODULE_AUTHOR("xxx")
模块描述 -- MODULE_DESCRIPTION("xxx")
模块版本 -- MODULE_VERSION("xxx")
模块别名 -- MODULE_ALIAS("xxx")
模块设备表 -- MODULE_DEVICE_TABLE, 对于USB或者PCI设备需要支持,表示支持的设备,这部分比较复杂,这里就不在多说,后续如果用到,在详细去说明。
在了解上述模块的基础上,就可以实现如下的模块代码:
1 //hello.ko 2 #include <linux/init.h> 3 #include <linux/module.h> 4 5 6 //extern int add_integar(int a, int b); 7 static char *buf = "driver"; 8 module_param(buf, charp, S_IRUGO); //模块参数 9 10 static int __init hello_init(void) 11 { 12 int dat = 3; //int dat = add_integar(5, 6); 13 printk(KERN_WARNING "hello world enter, %s, %d ", buf, dat); 14 return 0; 15 } 16 module_init(hello_init); //模块加载函数 17 18 static void __exit hello_exit(void) 19 { 20 printk(KERN_WARNING "hello world exit "); 21 } 22 module_exit(hello_exit); //模块卸载函数 23 24 MODULE_AUTHOR("ZC"); //模块作者 25 MODULE_LICENSE("GPL v2"); //模块许可协议 26 MODULE_DESCRIPTION("a simple hello module"); //模块许描述 27 MODULE_ALIAS("a simplest module"); //模块别名
使用Makefile文件如下:
1 ifeq ($(KERNELRELEASE),) 2 KDIR := /lib/modules/$(shell uname -r)/build 3 PWD := $(shell pwd) 4 modules: 5 $(MAKE) -C $(KDIR) M=$(PWD) modules 6 modules_install: 7 $(MAKE) -C $(KDIR) M=$(PWD) modules_install 8 clean: 9 rm -rf *.o *.ko .depend *.mod.o *.mod.c modules.* 10 .PHONY:modules modules_install clean 11 else 12 obj-m :=hello.o 13 endif
保存后,使用Make即可编译,如果遇到编译错误,请先查看文章最后的备注,未包含问题请搜索或者留言,编译结果如图所示。
之后执行指令modinfo hello.ko即可查看当前的模块信息。
如果无法查看信息,可通过dmesg查看加载信息。
内核模块的跨模块调用
上一节可以解决我们遇到的大部分内核实现问题,但某些时候我们可能需要一些公共内核模块,提供接口给大部分模块使用,这就涉及到内核模块的跨模块调用。
对于跨核模块调用的实现,对于调用的模块,主要包含2步:
1、在代码实现中添加extern int add_integar(int a, int b);
2、在编译环境下修改Module.symvers, 添加被链接模块的地址,函数校验值(可通过查看被链接模块编译环境下的Module.symvers内复制即可)
对于被链接的模块,代码实现如下:
1 //math.ko 2 #include <linux/init.h> 3 #include <linux/module.h> 4 5 static int __init math_init(void) 6 { 7 printk(KERN_WARNING "math enter "); 8 return 0; 9 } 10 module_init(math_init); 11 12 static void __exit math_exit(void) 13 { 14 printk(KERN_WARNING "math exit "); 15 } 16 module_exit(math_exit); 17 18 int add_integar(int a, int b) 19 { 20 return a+b; 21 } 22 EXPORT_SYMBOL(add_integar); 23 24 int sub_integar(int a, int b) 25 { 26 return a-b; 27 } 28 EXPORT_SYMBOL(sub_integar); 29 30 MODULE_LICENSE("GPL V2");
编译Makefile同上,需要将obj-m :=hello.o修改为obj-m :=math.o
执行make编译完成该文件,并通过insmod加载完模块后,可通过
grep integar /proc/kallsyms 查看加载在内核中的符号,状态如下:
然后加载insmod hello.ko, 即可跨文件调用该接口。如此,便初步完成对Linux内核模块的学习。
备注
1.内核编译名称必须为Makefile,否则编译会出错
make[2]: *** No rule to make target `/usr/kernel/hello/Makefile'. Stop.
make[1]: *** [_module_/usr/kernel/hello] Error 2
make[1]: Leaving directory `/usr/src/linux-headers-3.5.0-23-generic'
2.Makefile的内容,如果编译多个文件obj-m :=hello.o test.o
3.Makefile中,指令必须以Tab对齐,否则编译会异常。
4.printk不打印,一般来说输出的KERNEL_INFO为超过最大输出值,可直接通过dmesg,在系统信息内查看。
5.内核跨文件访问接口
除EXPORT_SYSMBOL外,在编译时Module.symvers需要包含对应函数的校验值,路径
0x13db98c9 sub_integar /usr/kernel/math/math EXPORT_SYMBOL
0xe1626dee add_integar /usr/kernel/math/math EXPORT_SYMBOL
否则编译时报警告
WARNING: "add_integar" [/usr/kernel/hello/hello.ko] undefined!
安装模块时出错
[ 9091.025357] hello: no symbol version for add_integar
[ 9091.025360] hello: Unknown symbol add_integar (err -22)