简介:
本文主要介绍一种可以复现操作的方法。通过该方法我们可以复现我们前面做过的操作。我们主要的思路是先将我们先前的操作记录下来,然后再将我们记录的值上报,以实现操作的复现。
Linux内核:linux-2.6.22.6
所用开发板:JZ2440 V3(S3C2440A)
声明:
本文是看完韦东山老师的视频后所写的课程总结,同时文中也会加入一些我自己对这方面知识的理解。希望可以帮到你们。
输入模拟器想法:
1. 产品要经过测试才能发布,一般都是人工操作,比如手机触摸屏,遥控器。
2. 操作过程中发现错误,要再次复现找到规律从而修改程序。
3. 能否在驱动程序中将所有的操作记录下来,存为文件。当出现错误时,可以通过文件里的数据来复现输入过程。
程序写作思路:
使用输入子系统中的触摸屏程序来进行说明。因为触摸屏程序中有input_event函数。我们在使用input_event函数上报事件的同时使用我们前面写的myprintk函数来记录上报的内容。当记录完成后我们可以通过cat /proc/mymsg命令来查看我们所记录的内容。当需要复现的时候我们再通过input_event函数将记录的数据输出,做到操作的复现。
而关于触摸屏的程序可以参考:嵌入式Linux ———触摸屏驱动开发
而关于myprintk函数可以参考:嵌入式Linux——kmsg:分析/proc/kmsg文件以及写自己的/proc/mymsg
记录上报参数:
我们看input_event函数,来了解我们都要上报什么信息:
/**
* input_event() - 上报新的输入事件
* @dev: 产生事件的设备
* @type: 事件类型
* @code: 具体事件码
* @value: 事件值
*/
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)
{
······
switch (type) {
case EV_SYN:
switch (code) {
case SYN_CONFIG:
case SYN_REPORT:
}
break;
case EV_KEY:
break;
case EV_SW:
break;
case EV_ABS:
break;
case EV_REL:
break;
case EV_MSC:
break;
case EV_LED:
break;
case EV_SND:
break;
case EV_REP:
break;
case EV_FF:
break;
}
}
从上面看input_event函数的参数有:dev,type,code,val。因为我们知道要记录的主要是事件的值,所以发生事件的设备就不记录了,取而代之的是我们要记录上报这个事件的时间time。这里我们使用jiffies来记录时间。所以我们要记录的内容为time,type,code,val。
同时由于我们要使用myprintk函数来将上报内容记录到mymsg中,所以在这个程序里我们要声明myprintk函数:
extern int myprintk(const char *fmt,...);
下面我们就要写一个记录函数来将这些重要的信息记录下来了。
void report_to_printk(unsigned int time,unsigned int type,unsigned int code,int val)
{
myprintk("0x%08x 0x%08x 0x%08x %d\n",time,type,code,val);
}
而记录信息的格式为:
0x00076617 0x00000003 0x00000018 0
0x00076617 0x00000003 0x00000018 0
0x0007661b 0x00000003 0x00000018 0
0x0007661b 0x00000003 0x00000018 0
接下来我们就要到触摸屏程序中找上报事件了,我们找到上报事件后在其后加入report_to_printk函数,将上报的内容记录下来。例如:
input_report_abs(s3c_ts_dev,ABS_PRESSURE,0);
report_to_printk(jiffies,EV_ABS,ABS_PRESSURE,0);
input_report_key(s3c_ts_dev,BTN_TOUCH,0);
report_to_printk(jiffies,EV_KEY,BTN_TOUCH,0);
input_sync(s3c_ts_dev);
report_to_printk(jiffies,EV_SYN,SYN_REPORT,0);
input_report_abs(s3c_ts_dev,ABS_X,(x[0]+x[1]+x[2]+x[3])/4);
report_to_printk(jiffies,EV_ABS,ABS_X,(x[0]+x[1]+x[2]+x[3])/4);
input_report_abs(s3c_ts_dev,ABS_Y,(y[0]+y[1]+y[2]+y[3])/4);
report_to_printk(jiffies,EV_ABS,ABS_Y,(y[0]+y[1]+y[2]+y[3])/4);
input_report_abs(s3c_ts_dev,ABS_PRESSURE,1);
report_to_printk(jiffies,EV_ABS,ABS_PRESSURE,1);
input_report_key(s3c_ts_dev,BTN_TOUCH,1);
report_to_printk(jiffies,EV_KEY,BTN_TOUCH,1);
input_sync(s3c_ts_dev);
report_to_printk(jiffies,EV_SYN,SYN_REPORT,0);
修改完代码我们就可以装载这些驱动了。但是这里提醒一下,在装载驱动时,要先装载记录事件mymsg.ko的驱动,再装载触摸屏s3c_ts.ko的驱动。而如果先装载触摸屏的驱动就会出现:Unknown symbol myprintk 的错误提示而且不能装载这个驱动,这是因为我们在触摸屏中用到了myprintk函数,但是我们并没有定义它,所以我们要先装载记录事件mymsg.ko的驱动,再装载触摸屏s3c_ts.ko的驱动。
装载完两个驱动我们就可以调试通过cat /proc/mymsg命令查看记录信息,同时我们可以通过命令: cp /proc/mymsg /ts.txt 将记录的数据保存到一个文件中。
复现记录:
现在我们要完成另一个功能:从文件中得到记录的数据,并上报这些数据以实现复现功能。因此我们要在触摸屏程序中再添加一个字符驱动程序来完成上面的功能。这个字符驱动程序中有:
write函数:将文件中的数据写入驱动程序的buf中。
ioctl函数:启动复现功能,同时加入标记功能来记录重要信息的位置。
我先将驱动框架写出:
int auto_major = 0;
static struct class *cls;
static ssize_t replay_write (struct file *file, const char __user *buf, size_t size, loff_t *loff_t)
{
return 0;
}
static int replay_ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
return 0;
}
static struct file_operations replay_fops = {
.owner = THIS_MODULE,
.write = replay_write,
.ioctl = replay_ioctl,
};
入口函数:
auto_major = register_chrdev(0,"replay",&replay_fops);
cls = class_create(THIS_MODULE,"replay");
device_create(cls,NULL,MKDEV(auto_major,0),"replay");
出口函数:
device_destroy(cls,MKDEV(auto_major,0));
class_destroy(cls);
unregister_chrdev(auto_major,"replay");
而要完成其他函数之前,我们要先申请一块空间来存放写入驱动的数据,所以我们先申请空间:
static char *replay_buf;
入口函数:
replay_buf = kmalloc(1024*1024,GFP_KERNEL);
if(!replay_buf){
printk("can't alloc for mylog_buf\n");
return -EIO;
}
出口函数:
kfree(replay_buf);
写函数:将文件中的数据写入驱动程序的buf中。
static int replay_w = 0; /* 记录数据写入的位置 */
static int replay_r = 0; /* 记录数据读取的位置 */
static ssize_t replay_write (struct file *file, const char __user *buf, size_t size, loff_t *loff_t)
{
int err;
/* 检验是否超出replay_buf的范围,如果超出则提示错误并返回 */
if(replay_w + size > (1024*1024)){
printk(" replay_buf full! \n ");
return -EIO;
}
/* 将文件从用户空间读入,这里使用replay_w记录数据存放的位置 */
err = copy_from_user(replay_buf+replay_w,buf,size); /* 这里涉及copy_from_user的返回值,当完成操作时返回0,没有完成返回非0 */
if(err){
return -EIO;
}else{
replay_w += size;
}
return size;
}
ioctl函数:启动复现功能,同时加入标记功能来记录重要信息的位置。
static int replay_ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
char buf[100];
switch (cmd){
case REPLAY_MODE_ON: {
replay_timer.expires = jiffies + 1; /* 这里的超时时间为当前时间加5ms,所以我们一调用add_timer函数就可以进入处理函数了 */
del_timer(&replay_timer); /* 为了可以重复复现,在添加定时器之前要先删除定时器,防止发生错误 */
add_timer(&replay_timer);
break;
}
case REPLAY_MODE_TAG:{
copy_from_user(buf,(const void __user *)arg,100); /* 从用户空间获得标志位 */
buf[99] = '\0';
myprintk("%s\n",buf); /* 记录标志位信息 */
break;
}
}
return 0;
}
这里需要说明的一点是:由于我们在replay_buf中记录的时间是过去的时间,而我们要复现记录,要用到现在的时间。所以我们只能计算在记录中时间的间隔,使用过去的时间间隔加上现在的时间来复现过程。所以这里我们要用到定时器。而复现的过程也是在定时器的处理函数中完成的。所以上面ioctl函数使用了定时器的函数。
例如当记录的时间为100,100,102,1000时,我们的操作为:
1. 把时间为100的数据取出,然后上报
2. 判断是否还有时间为100的数据,如果有继续上报
3,如果没有时间为100 的数据,启动定时器,超时时间为jiffies+(102-100)
4,时间到,上报时间为102的数据,判断是否还有时间为100的数据,如果有继续上报,如果没有时间为100 的数据,启动定时器,超时时间为jiffies+(1000-102)
5,以此循环上报各个时间位的数据
所以我们关于定时器的程序为:
static struct timer_list replay_timer;
void replay_timer_func(unsigned long data)
{
/* 把replay_buf里的一些数据取出来上报
* 读出第1行数据, 确定time值, 上报第1行
* 继续读下1行数据, 如果它的time等于第1行的time, 上报
* 否则: mod_timer
*/
unsigned int time;
unsigned int type;
unsigned int code;
int val;
static unsigned int pre_time = 0;
static unsigned int pre_type = 0;
static unsigned int pre_code = 0;
static int pre_val = 0;
int ret = 0;
printk("replay_timer_func\n");
if (pre_time != 0)
{
/* 上报事件 */
input_event(s3c_ts_dev, pre_type, pre_code, pre_val);
}
while(1){
ret = read_one_line(line);
if(ret == 0){
printk("end of input replay\n");
del_timer(&replay_timer);
pre_time = pre_type = pre_code = 0;
pre_val = 0;
replay_r = replay_w = 0;
break;
}
/* 处理数据 */
time = 0;
type = 0;
code = 0;
val = 0;
printk("sscanf\n");
sscanf(line,"%x %x %x %d",&time,&type,&code,&val);
if (!time && !type && !code && !val)
continue;
else{
if((pre_time == 0) || (pre_time == time)){
/* 上报事件 */
input_event(s3c_ts_dev, type,code, val);
if(pre_time == 0){
pre_time = time;
}
}else{
/* 根据下一个要上报的数据的时间 mod_timer */
mod_timer(&replay_timer,jiffies+(time-pre_time));
pre_time = time;
pre_type = type;
pre_code = code;
pre_val = val;
break;
}
}
}
}
入口函数:
init_timer(&replay_timer);
replay_timer.function = replay_timer_func;
由我们的记录函数的格式为:myprintk("0x%08x 0x%08x 0x%08x %d\n",time,type,code,val);,可知我们的数据是一行一行记录的,所以我们也应该将数据一行一行的读出,而读一行数据的函数为:
static int read_one_line(char *line)
{
int i = 0;
/* 清除前导的空格,回车,换行 */
while(replay_r <= replay_w){
if((replay_buf[replay_r] == '\n') || (replay_buf[replay_r] == ' ') || (replay_buf[replay_r] == '\r') || (replay_buf[replay_r] == '\t')){
replay_r++;
}else{
break;
}
}
/* 读一行内容到line中 */
while(replay_r <= replay_w){
if((replay_buf[replay_r] == '\n') || (replay_buf[replay_r] == '\r'))
break;
else{
line[i] = replay_buf[replay_r];
replay_r++;
i++;
}
}
line[i] = '\0'; /* 在line的末尾加‘\0’表示字符串结尾 */
return i;
}
写到这里我们的驱动程序就写完了,下面我们写测试应用程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#define REPLAY_MODE_ON 1
#define REPLAY_MODE_TAG 2
/* Usage:
* ./input_replay write <file>
* ./input_replay replay
* ./input_repaly tag <string>
*/
void print_usage(char *file)
{
printf("Usage:\n");
printf("%s write <file>\n", file);
printf("%s replay\n", file);
printf("%s tag <string>\n", file);
}
int main(int argc,char **argv)
{
int fd;
int fd_data;
int cnt;
int buf[100];
if(argc != 3 && argc != 2){
print_usage(argv[0]);
return -1;
}
fd = open("/dev/replay",O_RDWR);
if(fd < 0){
printf("can't open /dev/replay\n");
return -1;
}
if(strcmp("replay",argv[1]) == 0){
ioctl(fd,REPLAY_MODE_ON);
}
else if(strcmp("write",argv[1]) == 0){
if(argc != 3){
print_usage(argv[0]);
return -1;
}
fd_data = open(argv[2],O_RDONLY);
if(fd_data < 0){
printf("can't open %s\n",argv[2]);
return -1;
}
while(1){
cnt = read(fd_data,buf,100);
if(cnt == 0){
printf("write ok!!!\n");
break;
//printf("user space %s\n",buf);
}else{
write(fd,buf,cnt);
}
}
}
else if(strcmp("tag",argv[1]) == 0){
if(argc != 3){
print_usage(argv[0]);
return -1;
}
ioctl(fd,REPLAY_MODE_TAG,argv[2]);
}
else{
print_usage(argv[0]);
return -1;
}
return 0;
}
对于上面的测试程序我想说的是open,read,write,ioctl函数的用法。
open函数:open(文件名/设备名,操作属性),返回值fd小于0表示不成功。
read函数:read(fd,buf,size),其中fd为open所传递的fd,buf表示将读到的内容存到buf中,而size表示读多少字节数据,返回值 表示读了多少数据。
write函数:write(fd,buf,size),其中fd为open所传递的fd,buf表示将buf中的内容传到内核的write函数中,而size表示传递多少字节数据。
ioctl函数: ioctl(fd,cmd,arg),其中fd为open所传递的fd,cmd表示传到内核的ioctl函数中cmd参数,arg表示传到内核的ioctl函数中arg参数。
测试:
写完测试程序我们就可以测试了,我们使用tslib-1.4来完成对触摸屏的操作。
1. 编译tslib-1.4:
tar xzf tslib-1.4.tar.gz
cd tslib
./autogen.sh
mkdir tmp
echo "ac_cv_func_malloc_0_nonnull=yes" >arm-linux.cache
./configure --host=arm-linux --cache-file=arm-linux.cache --prefix=$(pwd)/tmp
make
make install
2. 安装tslib-1.4:
cd tmp
cp * -rf /nfsroot
3. 使用tslib-1.4:
4.
修改 /etc/ts.conf第1行(去掉#号和第一个空格):
# module_raw input
改为:
module_raw input
5.设置环境变量
export TSLIB_TSDEVICE=/dev/event0
export TSLIB_CALIBFILE=/etc/pointercal
export TSLIB_CONFFILE=/etc/ts.conf
export TSLIB_PLUGINDIR=/lib/ts
export TSLIB_CONSOLEDEVICE=none
export TSLIB_FBDEVICE=/dev/fb0
6. 触摸屏校准 :ts_calibrate
7. 触摸屏测试: ts_test
下面正式测试我们所写的程序:
0. 使用没有触摸屏驱动的内核:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_notss ; bootm 30000000
1. 安装myprintk的驱动:insmod mymsg.ko
2. 安装触摸屏驱动:insmod s3c_ts_drv.ko
3. 安装LCD驱动:
insmod cfbcopyarea.ko && insmod cfbfillrect.ko && insmod cfbimgblt.ko
insmod lcd_drv.ko
4. 添加标志:表示标志位下面将是重要信息: ./input_replay tag jingxaingge
5. 在触摸屏上操作 : ts_test 并在触摸屏上操作(画画或者写字)
6. 将记录的信息保存到文件中:cp /proc/mymsg /ts.txt
7. 在文件中删除标志位和标志位之前的信息:vi /ts.txt ,然后dd掉不需要的信息
8. 将信息写入驱动:./input_replay write /ts.txt
9. 后台运行触摸屏:ts_test &
10. 回显痕迹: ./input_replay replay