【北京迅为】iTOP-4412全能版使用手册-第九十六章 嵌入式Linux驱动开发进阶(上)

iTOP-4412全能版采用四核Cortex-A9,主频为1.4GHz-1.6GHz,配备S5M8767 电源管理,集成USB HUB,选用高品质板对板连接器稳定可靠,大厂生产,做工精良。接口一应俱全,开发更简单,搭载全网通4G、支持WIFI、蓝牙、陀螺仪、CAN总线、RS485总线、500万摄像头等模块,稳定运行Android 4.0.3/Android 4.4操作,系统通用Linux-3.0.15+Qt操作系统(QT支持5.7版本),Ubuntu版本:12.04,接口智能分配 方便好用。

【交流群2】258811263(获取开源资料)

【迅为B站】北京迅为电子的个人空间-北京迅为电子个人主页-哔哩哔哩视频


第九部分 Linux内核开发

第九十六章 嵌入式Linux驱动开发进阶

96.1 Linux显卡驱动分析及不同屏幕的支持方法

本章介绍迅为 4412 开发板的显卡驱动以及不同屏幕分辨率等参数的修改方法,从而来支持不同规格和分辨率的屏幕。

iTOP-4412 开发板支持 4.3 寸、7 寸和 9.7 寸等 lcd 显示屏。其中 4.3 寸屏是用的 cpu 直接出来的 RGB 信号,7 寸屏和 9.7 寸屏是用的 LVDS 信号,硬件上使用了一个 RGB 转LVDS 的芯片实现的。

我们来看下显示驱动,显示驱动在内核源码的“drivers/video/samsung”目录下面,这个驱动是三星提供好的,这里只介绍一下需要修改的相关文件。

首先是关于屏幕的分辨率的修改,因为不同的屏幕分辨率,频率以及其他一些硬件参数是不同的,所以我们需要根据这些参数去配置 cpu 的显示控制器,关于这些参数是在“drivers/video/samsung/s3cfb_wa101s.c”这个文件,打开这个文件我们可以看到这个文件主要就是定义了一个类型是 s3cfb_lcd 的变量 wa101,屏幕的硬件参数(分辨率,时钟频率以及其它)就是保存在这个变量里面,现在我们来看下这个变量结构类型的定义:

struct s3cfb_lcd { 
int width;
int height; 
int bpp; 
int freq;
struct s3cfb_lcd_timing timing;
struct s3cfb_lcd_polarity polarity;
void (*init_ldi)(void); 
void (*deinit_ldi)(void);
};

其中的 width 和 height 指屏幕的分辨率,freq 是时钟频率,bpp 是数据位。timing 是屏幕的其他一些参数,timing 的类型定义如下:

struct s3cfb_lcd_timing { 
int h_fp;
int h_bp; 
int h_sw; 
int v_fp; 
int v_fpe; 
int v_bp; 
int v_bpe; 
int v_sw;
};

 这个结构代表屏幕的左间距,右间距,水平同步信号宽度,垂直同步信号的有效行数等屏幕的硬件参数,这些参数可以通过查看屏幕的数据手册获得。

下面是 polarity 变量,他的定义如下:

struct s3cfb_lcd_polarity { 
int rise_vclk;
int inv_hsync;
int inv_vsync;
int inv_vden;
};

这个变量代表时钟行场的极性。

通过修改这个文件里面的这些参数就可以设置 cpu 的显示控制器来支持我们使用的 lcd 屏幕了。

下面我们来看一下 lcd 的控制文件:arch/arm/mach-exynos/setup-fb-s5p.c 在这个文件的 s3cfb_cfg_gpio 函数完成 LCD 数据引脚初始化, 驱动能力设为最高S5P_GPIO_DRVSTR_LV4;管脚驱动能力,S5P_GPIO_DRVSTR_LV1-4 四个等级选择,并且设置 LVDS 芯片的使能引脚输出高:

void s3cfb_cfg_gpio(struct platform_device *pdev)
{
int err;

s3cfb_gpio_setup_24bpp(EXYNOS4_GPF0(0), 8, S3C_GPIO_SFN(2), S5P_GPIO_DRVSTR_LV4); 
s3cfb_gpio_setup_24bpp(EXYNOS4_GPF1(0), 8, S3C_GPIO_SFN(2), S5P_GPIO_DRVSTR_LV4); 
s3cfb_gpio_setup_24bpp(EXYNOS4_GPF2(0), 8, S3C_GPIO_SFN(2), S5P_GPIO_DRVSTR_LV4); 
s3cfb_gpio_setup_24bpp(EXYNOS4_GPF3(0), 4, S3C_GPIO_SFN(2), S5P_GPIO_DRVSTR_LV4); 
#if 1 // TC4

//LVDS_PWDN

err = gpio_request(EXYNOS4_GPL1(0), "GPL1_0"); 
if (err) {
printk(KERN_ERR "failed to request GPL1 for " "lcd power control\n");
return err;
}

gpio_direction_output(EXYNOS4_GPL1(0), 1); 
s3c_gpio_cfgpin(EXYNOS4_GPL1(0), S3C_GPIO_OUTPUT); 
gpio_free(EXYNOS4_GPL1(0));
#endif
}

然后是时钟控制函数,完成时钟的使能和关闭:

int s3cfb_clk_on(struct platform_device *pdev, struct clk **s3cfb_clk)
{
struct clk *sclk = NULL;
struct clk *mout_mpll = NULL; 
struct clk *lcd_clk = NULL; 
u32 rate = 0;
int ret = 0;

lcd_clk = clk_get(&pdev->dev, "lcd"); 
if (IS_ERR(lcd_clk)) {
dev_err(&pdev->dev, "failed to get operation clk for fimd\n"); 
goto err_clk0;
}
ret = clk_enable(lcd_clk); 
if (ret < 0) {
dev_err(&pdev->dev, "failed to clk_enable of lcd clk for fimd\n");
goto err_clk0;
}

clk_put(lcd_clk);
sclk = clk_get(&pdev->dev, "sclk_fimd");
if (IS_ERR(sclk)) {
dev_err(&pdev->dev, "failed to get sclk for fimd\n");
goto err_clk1;
}

if (soc_is_exynos4210())
mout_mpll = clk_get(&pdev->dev, "mout_mpll");
else
mout_mpll = clk_get(&pdev->dev, "mout_mpll_user");
if (IS_ERR(mout_mpll)) {
dev_err(&pdev->dev, "failed to get mout_mpll for fimd\n");
goto err_clk2;
}

ret = clk_set_parent(sclk, mout_mpll);
if (ret < 0) {
dev_err(&pdev->dev, "failed to clk_set_parent for fimd\n");
goto err_clk2;
}

ret = clk_set_rate(sclk, 800000000);
if (ret < 0) {
dev_err(&pdev->dev, "failed to clk_set_rate of sclk for fimd\n");
goto err_clk2;
}

dev_dbg(&pdev->dev, "set fimd sclk rate to %d\n", rate);
clk_put(mout_mpll);

ret = clk_enable(sclk);
if (ret < 0) {
dev_err(&pdev->dev, "failed to clk_enable of sclk for fimd\n");
goto err_clk2;
}

*s3cfb_clk = sclk;
return 0;
err_clk2:
clk_put(mout_mpll);
err_clk1:
clk_put(sclk);
err_clk0:
clk_put(lcd_clk);
return -EINVAL;
}

int s3cfb_clk_off(struct platform_device *pdev, struct clk **clk)
{
struct clk *lcd_clk = NULL;
lcd_clk = clk_get(&pdev->dev, "lcd");
if (IS_ERR(lcd_clk)) {
printk(KERN_ERR "failed to get ip clk for fimd0\n");
goto err_clk0;
}

clk_disable(lcd_clk);
clk_put(lcd_clk);
clk_disable(*clk);
clk_put(*clk);
*clk = NULL;
return 0;
err_clk0:
clk_put(lcd_clk);
return -EINVAL;
}

void s3cfb_get_clk_name(char *clk_name)
{
strcpy(clk_name, "sclk_fimd");
}

然后是 s3cfb_backlight_on 函数,这个是使能屏幕显示,s3cfb_backlight_off 关闭屏幕显示。

大家可以结合以上内容来修改参数,最终支持不同规格的屏幕。

96.2 修改驱动使显卡支持HDMI_1080P分辨率

本文档介绍 iTOP-4412 显卡驱动通过修改之后,能够支持 HDMI 的 1080P 分辨率输出。

请注意:本文档针对精英版和全能版,2018 年及以后迅为发布的内核源码,包含Android4.03 和 Android4.4、Ubuntu 和 QtE 系统。

本文档以精英版 Android4.0.3 系统的内核为例(4412 全能版和 4412 精英版的Android4.4 内核也是一样),打开内核“drivers/video/samsung/s3cfb_wa101s.c”显卡配置文件,如下图所示。

打开之后,搜“0x3”,可以看到条件语句“else if(0x3==type)”,这部分是将拨码开关设置为“0111”之后对应的显卡分辨率设置代码,将其改为如下图所示。 

上面代码只需要修改“else if(0x3 == type)”条件语句包含的语句,修改之后代码如下: 

//add by rty 20180719
#define HDMI_1080P_OUT #ifdef	HDMI_1080P_OUT
//HDMI 1080P
wa101.width = 1920;
wa101.height = 1080;
#else

//LCD 1024*600
wa101.width = 1024;
wa101.height = 600;
wa101.bpp =24;
wa101.freq = 70;//70;
#endif
//add end

 修改之后的分辨率就是 HDMI 的 1080P 输出,对应硬件上拨码开关为 0111。

如果想要恢复对金属框 7 寸屏幕和 10.1 寸屏幕的支持,则可以将上面修改之后的代码

“#define HDMI_1080P_OUT”屏蔽掉,这样就恢复原样,支持金属框 7 寸屏幕和 10.1 寸屏幕,HDMI 也就输出 1024*600 了。

修改完成之后,重新编译内核,烧写到开发板,HDMI 小口即可输出 1080P 的视频信号。

96.3 Linux触摸驱动源码分析

iTOP-4412 开发板 9.7 寸和 7 寸的屏幕使用的是电容触摸屏,触摸 IC 是 ft5406,对应的驱动文件是 drivers/input/touchscreen/ft5x06_ts.c,现在我们来分析下这个文件,首先是 ft5x0x_ts_init 函数,它是驱动加载首先执行的函数,代码如下:

static int	init ft5x0x_ts_init(void)
{
 int ret;

#if 1
 printk("==%s: reset==\n",	FUNCTION	);
 	ret = gpio_request(EXYNOS4_GPX0(3), "GPX0_3");
if (ret) {
 	 gpio_free(EXYNOS4_GPX0(3));
 	 ret = gpio_request(EXYNOS4_GPX0(3), "GPX0_3");
 	 if(ret)
 	 {
 	  printk("ft5xox: Failed to request GPX0_3 \n");
 	 }
 	}
   gpio_direction_output(EXYNOS4_GPX0(3), 0);
   mdelay(200);
   gpio_direction_output(EXYNOS4_GPX0(3), 1);

   s3c_gpio_cfgpin(EXYNOS4_GPX0(3), S3C_GPIO_OUTPUT);
   gpio_free(EXYNOS4_GPX0(3));
   msleep(10); #endif

   return i2c_add_driver(&ft5x0x_ts_driver);
}

在这个函数里面主要实现的功能是通过 cpu 的一个 GPIO 引脚给触摸芯片复位,然后就是向内核注册 i2c 驱动,接下来在系统在枚举设备的时候会执行到 ft5x0x_ts_probe 函数,这个函数是整个驱动的初始化函数,下面我们来分析下这个函数,首先是

if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
 	err = -ENODEV;
 	goto exit_check_functionality_failed;
}

这个函数的作用是检查一下设备是不是 i2c 设备,然后是

ts = kzalloc(sizeof(*ts), GFP_KERNEL);
 if (!ts) {
 	err = -ENOMEM;
 	goto exit_alloc_data_failed;
}


 pdata = client->dev.platform_data;
 if (!pdata) {
 	dev_err(&client->dev, "failed to get platform data\n");
 	goto exit_no_pdata;
}


 ts->screen_max_x = pdata->screen_max_x;
 ts->screen_max_y = pdata->screen_max_y;
ts->pressure_max = pdata->pressure_max;

定义了一个 ft5x0x_ts_data 结构的变量 ts,并为其分配内存,并且给这个结构体赋值。然后是:

ts->gpio_irq = pdata->gpio_irq;
 if (ts->gpio_irq != -EINVAL) {
 	client->irq = gpio_to_irq(ts->gpio_irq);
 } else {
 	goto exit_no_pdata;
}
 if (pdata->irq_cfg) {
 	s3c_gpio_cfgpin(ts->gpio_irq, pdata->irq_cfg);
 	s3c_gpio_setpull(ts->gpio_irq, S3C_GPIO_PULL_NONE);
}


 ts->gpio_wakeup = pdata->gpio_wakeup;
 ts->gpio_reset = pdata->gpio_reset;

获取到触摸的中断引脚,并配置这个引脚为中断模式。接下来是:

INIT_WORK(&ts->work, ft5x0x_ts_pen_irq_work); 
this_client = client;
i2c_set_clientdata(client, ts);

ts->queue = create_singlethread_workqueue(dev_name(&client->dev)); 
if (!ts->queue) {
err = -ESRCH;
goto exit_create_singlethread;

}

创建了一个工作队列,用于读取触摸的数据,并上报给内核。接下来是:

input_dev = input_allocate_device();
 if (!input_dev) {
 	err = -ENOMEM;
 	dev_err(&client->dev, "failed to allocate input device\n");
 	goto exit_input_dev_alloc_failed;
}


 ts->input_dev = input_dev;

上面是分配了一个 input_dev 结构. 接下来是:

set_bit(EV_SYN, input_dev->evbit);
 set_bit(EV_ABS, input_dev->evbit);
 set_bit(EV_KEY, input_dev->evbit);


#ifdef CONFIG_FT5X0X_MULTITOUCH


 set_bit(ABS_MT_TRACKING_ID, input_dev->absbit);
 set_bit(ABS_MT_TOUCH_MAJOR, input_dev->absbit);
 set_bit(ABS_MT_WIDTH_MAJOR, input_dev->absbit);
 set_bit(ABS_MT_POSITION_X, input_dev->absbit);
 set_bit(ABS_MT_POSITION_Y, input_dev->absbit);
 input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, ts->screen_max_x, 0, 
 input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, ts->screen_max_y, 0, 0);
 input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, ts->pressure_max, 0, 0);
 input_set_abs_params(input_dev, ABS_MT_WIDTH_MAJOR, 0, 200, 0, 0);
 input_set_abs_params(input_dev, ABS_MT_TRACKING_ID, 0, FT5X0X_PT_MAX, 0, 0);


#else
 set_bit(ABS_X, input_dev->absbit);
 set_bit(ABS_Y, input_dev->absbit);
 set_bit(ABS_PRESSURE, input_dev->absbit);
 set_bit(BTN_TOUCH, input_dev->keybit);



 input_set_abs_params(input_dev, ABS_X, 0, ts->screen_max_x, 0, 0);
 input_set_abs_params(input_dev, ABS_Y, 0, ts->screen_max_y, 0, 0);


 input_set_abs_params(input_dev, ABS_PRESSURE, 0, ts->pressure_max, 0 , 0); #endif


 input_dev->name = FT5X0X_NAME;
 input_dev->id.bustype = BUS_I2C;
 input_dev->id.vendor = 0x12FA;
 input_dev->id.product = 0x2143;
 input_dev->id.version = 0x0100;
上面是设置 input_dev 的一些参数,用于通知输入子系统上报的数据类型以及方式。接下来:
err = ft5x0x_read_fw_ver(&val);
 if (err < 0) {
 	dev_err(&client->dev, "chip not found\n");
 	goto exit_irq_request_failed;
}
上面是读取触摸芯片的硬件信息。接下来是:
err = request_irq(client->irq, ft5x0x_ts_interrupt,
 	IRQ_TYPE_EDGE_FALLING /*IRQF_TRIGGER_FALLING*/, "ft5x0x_ts", ts);
 if (err < 0) {
 	dev_err(&client->dev, "Request IRQ %d failed, %d\n", client->irq, err);
 	goto exit_irq_request_failed;
}
 disable_irq(client->irq);
dev_info(&client->dev, "Firmware version 0x%02x\n", val); 

上面是向内核注册中断,然后先关闭中断。

接下来是:

#ifdef CONFIG_HAS_EARLYSUSPEND
 ts->early_suspend.level = EARLY_SUSPEND_LEVEL_DISABLE_FB + 1;
 ts->early_suspend.suspend = ft5x0x_ts_suspend;
 ts->early_suspend.resume = ft5x0x_ts_resume;
 register_early_suspend(&ts->early_suspend); #endif
 enable_irq(client->irq);
 //cym 4412_set_ctp(CTP_FT5X06);
 dev_info(&client->dev, "FocalTech ft5x0x TouchScreen initialized\n");
 return 0;

上面代码是如果内核支持休眠唤醒功能,就注册休眠唤醒处理函数,然后使能中断。

到这里驱动的初始化函数就完成了,现在我们再来回顾一下初始化函数完成的主要功能:首先是把连接触摸芯片的 GPIO 设置成中断模式,然后向系统注册一个输入设备,接着注册中断处理。

下面我们再来看看中断处理:

static irqreturn_t ft5x0x_ts_interrupt(int irq, void *dev_id) {
 struct ft5x0x_ts_data *ts = dev_id;
 //printk("%s(%d)\n",	FUNCTION	,	LINE	);
 disable_irq_nosync(this_client->irq);
 if (!work_pending(&ts->work)) {
 	queue_work(ts->queue, &ts->work);
}
 return IRQ_HANDLED;
}

中断处理函数里面会启动工作队列,然后就返回了,这样的好处就是中断的处理时间短,具体的处理交给工作队列去完成。

接着我们在来看看工作队列函数:

static void ft5x0x_ts_pen_irq_work(struct work_struct *work) {
 struct ft5x0x_ts_data *ts = container_of(work, struct ft5x0x_ts_data, work);
 if (!ft5x0x_read_data(ts)) {
 	ft5x0x_ts_report(ts);
}
 enable_irq(this_client->irq);
}

这个函数的作用是读取触摸的数据,然后上报给内核的输入子系统。

其中函数 ft5x0x_read_data(ts)就是读取触摸的数据,下面来看下这个函数: static int ft5x0x_read_data(struct ft5x0x_ts_data *ts) {
 struct ft5x0x_event *event = &ts->event;
 //u8 buf[32] = { 0 };
 u8 buf[64] = { 0 };
 int ret;

#ifdef CONFIG_FT5X0X_MULTITOUCH
 //ret = ft5x0x_i2c_rxdata(buf, 31);
 ret = ft5x0x_i2c_rxdata(buf, 63); #else
 ret = ft5x0x_i2c_rxdata(buf, 7); #endif
 if (ret < 0) {
 	printk("%s: read touch data failed, %d\n",	func	, ret);
 	return ret;
}
 memset(event, 0, sizeof(struct ft5x0x_event));

 //event->touch_point = buf[2] & 0x07;
 event->touch_point = buf[2] & 0x0F;
 if (!event->touch_point) {
 	ft5x0x_ts_release(ts);
 	return 1;
}
 //printk("point = %d\n", event->touch_point); #ifdef CONFIG_FT5X0X_MULTITOUCH
 switch (event->touch_point) {
 	case 10:
 	event->x[9] = (s16)(buf[57] & 0x0F)<<8 | (s16)buf[58];
 	event->y[9] = (s16)(buf[59] & 0x0F)<<8 | (s16)buf[60];
 	case 9:
 	event->x[8] = (s16)(buf[51] & 0x0F)<<8 | (s16)buf[52];
 	event->y[8] = (s16)(buf[53] & 0x0F)<<8 | (s16)buf[54];
 	//printk("x:%d, y:%d\n", event->x[3], event->y[3]);
 	event->x[2] = (s16)(buf[0x0f] & 0x0F)<<8 | (s16)buf[0x10];
 	event->y[2] = (s16)(buf[0x11] & 0x0F)<<8 | (s16)buf[0x12];
 	//printk("x:%d, y:%d\n", event->x[2], event->y[2]);
 	event->x[1] = (s16)(buf[0x09] & 0x0F)<<8 | (s16)buf[0x0a];
 	event->y[1] = (s16)(buf[0x0b] & 0x0F)<<8 | (s16)buf[0x0c];
 	//printk("x:%d, y:%d\n", event->x[1], event->y[1]);
 	case 1:
 	event->x[0] = (s16)(buf[0x03] & 0x0F)<<8 | (s16)buf[0x04];
 	event->y[0] = (s16)(buf[0x05] & 0x0F)<<8 | (s16)buf[0x06];
 	//printk("x:%d, y:%d\n", event->x[0], event->y[0]);
 	break;
 	default:
 	printk("%s: invalid touch data, %d\n",	func	, event->touch_point);
 	return -1;
}
#else
 if (event->touch_point == 1) {
 	event->x[0] = (s16)(buf[0x03] & 0x0F)<<8 | (s16)buf[0x04];
 	event->y[0] = (s16)(buf[0x05] & 0x0F)<<8 | (s16)buf[0x06];
}
#endif

 event->pressure = 200;
 return 0;
}

通过看这个函数的代码,我们可以知道 CPU 先通过 i2c 读出有几组数据(有几点按下) 然后在根据有几点按下,通过 i2c 把这几点的数据读上来,并保存到全局变量 ts->event 里面。 然后是 ft5x0x_ts_report 函数:

static void ft5x0x_ts_report(struct ft5x0x_ts_data *ts) {
 struct ft5x0x_event *event = &ts->event;
 int x, y;
 int i;
#ifdef CONFIG_FT5X0X_MULTITOUCH
 for (i = 0; i < event->touch_point; i++) {
 	event->x[i] = ts->screen_max_x - event->x[i];
 	//event->y[i] = ts->screen_max_y - event->y[i]; #ifdef CONFIG_PRODUCT_SHENDAO
 	event->y[i] = ts->screen_max_y - event->y[i]; #endif
 	if (swap_xy) {
 	x = event->y[i];
 	y = event->x[i];
 	} else {
 	x = event->x[i];
 	y = event->y[i];
 	}


 	if (scal_xy) {
 	x = (x * ts->screen_max_x) / TOUCH_MAX_X;
 	y = (y * ts->screen_max_y) / TOUCH_MAX_Y;
 	}
 	//printk("x = %d, y = %d\n", x, y);
 	input_report_abs(ts->input_dev, ABS_MT_POSITION_X, x);
 	input_report_abs(ts->input_dev, ABS_MT_POSITION_Y, y);
 	input_report_abs(ts->input_dev, ABS_MT_PRESSURE, event->pressure);
 	input_report_abs(ts->input_dev, ABS_MT_TOUCH_MAJOR, event->pressure);
 	input_report_abs(ts->input_dev, ABS_MT_TRACKING_ID, i);
 	input_mt_sync(ts->input_dev);
}
#else
 if (event->touch_point == 1) {
 	//event->x[0] = ts->screen_max_x - event->x[0];
 	//event->y[0] = ts->screen_max_y - event->y[0];
 	if (swap_xy) {
 	x = event->y[0];
 	y = event->x[0];
 	} else {
 	x = event->x[0];
 	y = event->y[0];
 	}
 	if (scal_xy) {
 	 x = (x * ts->screen_max_x) / TOUCH_MAX_X;
 	 y = (y * ts->screen_max_y) / TOUCH_MAX_Y;
 	}

 	input_report_abs(ts->input_dev, ABS_X, x);
 	input_report_abs(ts->input_dev, ABS_Y, y);
 	input_report_abs(ts->input_dev, ABS_PRESSURE, event->pressure);
}

 input_report_key(ts->input_dev, BTN_TOUCH, 1); #endif
 input_sync(ts->input_dev);
}

该函数的作用就是把触摸的数据传递到内核的输入子系统,我们可以在这个函数里面加条打印信息,把触摸的坐标打印出来,以方便调试,例如上面代码里面红色的语句就是打印坐标的,把这个语句前面的“//”去掉,当我们点触摸屏的时候就会在串口看到坐标数据的输 出,有的时候屏幕的坐标和触摸的坐标对不上,导致触摸不准,我们就可以在这个函数里对读上来的坐标进行转换以达到和屏幕坐标一致。

关于触摸的驱动大体主要就是完成初始化,在初始化的时候向内核注册输入设备,向内核注册中断,然后就是中断服务了。中断服务里面完成触摸数据的读取,上报触摸数据给内核的输入子系统。

至于其他的 i2c 的触摸驱动,大体框架也是这个流程,大家通过上面的学习主要是掌握触摸驱动的整个流程和框架,至于代码里面的具体细节,可以通过添加打印信息来调试。

96.4 配置Linux内核支持AV-IN功能

迅为的 AV-IN 摄像头模块可以直接连接普通的模拟摄像头,这种摄像头应用十分广泛, 平时所看到的监控用的摄像头大多属于此类,具体使用方法也很简单,一方面把 AV-IN 摄像头模块连接开发板,然后把模块的另一端连接模拟摄像头即可。迅为的 AV-IN 模块如下图所示:

开发板提供的内核已经包含了 AV-IN 的驱动,不过使用的时候需要配置一下,具体步骤如下:

在内核目录下执行命令“make menuconfig”,如下图:

将会打开内核的配置界面,如下图: 

然后选择“Device Drivers”选项,进入“Device Drivers”配置界面,如下图: 

然后选择“Multimedia support”选项,进入“Multimedia support”配置界面,如下图: 

然后选择“Video capture adapters”选项,进入“Video capture adapters”配置界面,如下图:

然后选择“Encoders, decoders, sensors and other helper chips”, 进入“Encoders, decoders, sensors and other helper chips”配置界面,如下图: 

然后选择“Texas Instruments TVP5150 video decoder”选项,如下图: 

然后返回到“Video capture adapters”配置界面,如下图: 

然后找到“OmniVision OV5640 sensor support”选项,取消掉“OmniVision OV5640 sensor support”的配置,如下图: 

然后依次选择“Exit”退出配置界面,如下图: 

然后在上图选择“Yes”,并按回车,退出配置界面。最后在串口输入”make“开始编译内核,如下图: 

编译完成后,把生成的 zImage 烧写到 iTOP-4412 开发板就可以支持 AVIN 的摄像头了。

96.5 移植声卡tinyplay功能

本教程适合 Linux 最小系统以及 Qt 系统。

96.5.1 编译

将我们提供的压缩包中的文件夹”tinyalsa”拷贝到 Ubunut 的工作目录,在网盘资料“iTOP4412开发板资料汇总(不含光盘内容)\iTOP-4412开发板Linux内核开发\iTOP-4412-linux系统-声卡测试tinyplay.zip”目录下。如图

 

使用 makefile,在当前目录下输入命令“make”,自动生成 4 个可执行文件 

96.5.2 测试

本次测试是在 4412 上进行,耳机,麦克风的连接如图

将 4 个可执行文件和测试音乐 1.wav 文件拷贝到 U 盘,插入到运行 qt 系统的 4412 开发板上,挂载 U 盘。以下步骤通过串口进行操作。

mknod /dev/sda1 b 8 1

mount /dev/sda1 /mnt/disk

然后将 U 盘中的四个可执行文件拷贝到‘/bin’目录下“cp /mnt/disk/tiny* /bin

1、输入 tinymix 可以看到全部设置。如下图所示

 

操作系统默认使用耳机输出。用户在超级终端中输入以下命令即可实现内放和外放的切换。

(1)使用耳机输出,现在程序默认使用的耳机,使用耳机输入,在串口依次以下命令:

tinymix 4 127

tinymix 5 1

tinymix 39 1

tinymix 42 1

这些命令的具体意义用户可以根据 tinymix 命令的输出进行查看,例如命令“tinymix 4 127”是对耳机音量(Headphone Playback Volume)设置为 127(取值 0-127),如图

值 5  Headphone Playback ZC Switch 表示开关耳机左右声道切换 

值 39 Right Output Mixer PCM Playback Switch 表示开关右声道

值 42 Left Output Mixer PCM Playback Switch 表示开关左声道

(2)使用外置的喇叭,在串口依次输入以下命令:

tinymix 6 127 设置外放声音(最大为 127)

tinymix 7 1 开启左右声道切换功能(不开启则无法切换外放声道):

tinymix 39 1 右声道播放喇叭开启

tinymix 42 1 左声道播放喇叭开启

播放歌曲测试,测试歌曲需要 wav 格式文件。输入”tinyplay 1.wav

可以看到正在播放,并且耳机中可以听到声音。想要使用右声道外放,则可以使用以下命令:

tinymix 6 127

tinymix 7 1

tinymix 39 1

tinymix 42 0

想要使用左声道外放,则可以使用以下命令:tinymix 6 127

tinymix 7 1

tinymix 39 0

tinymix 42 1

2、录音测试,系统默认开启录音功能,不需 tinymix 另外设置。输 入 “tinycap 2.wav”

 

可以看到正在录音,录音结束通过 ctrl+c 强行退出即可,之后在当前目录下可以看到 2.wav 音频文件。到此 tinyalsa 移植完成。

96.6 中断方式实现按键控制 LED

在驱动实验教程里我们实现了注册中断以及触发中断的操作,本节我们练习一下通过Linux 中断处理函数来实现按键控制 led 操作。在之前的章节我们学习了 led 驱动的编写,在掌握了 led 驱动的编写以后,如果要实现按键控制 led 的功能,大家可能会想到可以在 led 的

驱动里面使用轮询的方式一直查询按键的状态,如果有按键按下就设置 led 的状态。通过这种方式可以实现按键控制 led 的功能,但是通过这样的方式有一个缺点就是 led 驱动会占用cpu,这样 cpu 的利用率就大大降低了,所以我们可以通过中断的方式来实现。这样 cpu 就可以去做其他的事情了,当有按键中断触发的时候才会去设置 led。

ARM 架构 linux 内核中,有 5 种常见的异常,其中中断异常是其一,Linux 内核将所有中断统一编号,使用一个 irq_desc 结构体来描述这些中断,里面记录了中断名称、中断状态、中断标记、并提供了中断的底层硬件访问函数(如:清除、屏蔽、使能中断),提供了这个中断的处理函数入口,通过它还可以调用用户注册的的中断处理函数。linux 内核的中断体系已经很完善了,驱动工程师需要做的就是调用 request_irq 函数向内核注册中断处理函数, 下面我们来看看 request_irq 函数的定义:

static inline int must_check request_irq(
unsigned int irq, 
irq_handler_t handler, 
unsigned long flags, 
const char *name, 
void *dev
)

第一个参数 irq:中断号,与平台架构相关;

第二个参数 handler:用户中断处理函数;

第三个参数 flags:中断标记

第四个参数 devname:中断名字,可以通过 cat /proc/interrupts 查看;

第五个参数 dev_id:在 free_irq 中有用,也用做区分中断处理函数

有注册就得对应着有注销,驱动的注销函数是 free_irq,其定义如下:

void free_irq(unsigned int irq, void *dev_id)

第一个参数 irq:中断号,与 request_irq 中的 irq 一致,用于定位 action 链表;

第二个参数 dev_id:用于在 action 链表中找到要卸载的表项;同一个中断的不同中断处理函数必须使用不通的 dev_id 来区分,这就要求在注册中断共享时参数 dev_id 必须唯一。

下面我们来看一下中断按键的驱动,代码如下:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h>
#include <mach/regs-gpio.h>
#include <asm/io.h>
#include <linux/regulator/consumer.h>
#include <linux/delay.h>

#define IRQ_DEBUG
#ifdef IRQ_DEBUG
#define DPRINTK(x...) 
printk("IRQ_CTL DEBUG:" x)
#else
#define DPRINTK(x...)
#endif

#define DRIVER_NAME "irq_test"

static int led_gpios[] = {
 	EXYNOS4_GPL2(0),
 	EXYNOS4_GPK1(1),
};

#if 1
static irqreturn_t eint9_interrupt(int irq, void *dev_id) {
 	if(gpio_get_value(led_gpios[0]))
 		gpio_set_value(led_gpios[0], 0);
 	else
 		gpio_set_value(led_gpios[0], 1);
 	return IRQ_HANDLED;
}

static irqreturn_t eint10_interrupt(int irq, void *dev_id) {
 	if(gpio_get_value(led_gpios[1]))

 		gpio_set_value(led_gpios[1], 0);
 	else
 		gpio_set_value(led_gpios[1], 1);
 	return IRQ_HANDLED;
}

#endif
static int irq_probe(struct platform_device *pdev)
{
 	int ret, i;
 	char *banner = "irq_test Initialize\n";

 	printk(banner);
 	for(i=0; i<LED_NUM; i++)
 	{
 		ret = gpio_request(led_gpios[i], "LED");
 		if (ret) {
 			printk("%s: request GPIO %d for LED failed, ret = %d\n", DRIVER_NAME,led_gpios[i], ret);
 			return ret;
 		}
 		s3c_gpio_cfgpin(led_gpios[i], S3C_GPIO_OUTPUT);
 		gpio_set_value(led_gpios[i], 0);
 	}

 	ret = gpio_request(EXYNOS4_GPX1(1), "EINT9");
 	if (ret) {
 		printk("%s: request GPIO %d for EINT9 failed, ret = %d\n", DRIVER_NAME,EXYNOS4_GPX1(1), ret);
 		return ret;
 	}

 	s3c_gpio_cfgpin(EXYNOS4_GPX1(1), S3C_GPIO_SFN(0xF));
 	s3c_gpio_setpull(EXYNOS4_GPX1(1), S3C_GPIO_PULL_UP);
 	gpio_free(EXYNOS4_GPX1(1));
 	ret = gpio_request(EXYNOS4_GPX1(2), "EINT10");
 	if (ret) {
 		printk("%s: request GPIO %d for EINT10 failed, ret = %d\n", DRIVER_NAME,
 		EXYNOS4_GPX1(2), ret);
 		return ret;
 	}

 	s3c_gpio_cfgpin(EXYNOS4_GPX1(2), S3C_GPIO_SFN(0xF));
 	s3c_gpio_setpull(EXYNOS4_GPX1(2), S3C_GPIO_PULL_UP);
 	gpio_free(EXYNOS4_GPX1(2));

#if 1
 	ret = request_irq(IRQ_EINT(9), eint9_interrupt,
 	IRQ_TYPE_EDGE_FALLING /*IRQF_TRIGGER_FALLING*/, "eint9", pdev);
 	if (ret < 0) {
 		printk("Request IRQ %d failed, %d\n", IRQ_EINT(9), ret);
 		goto exit;
 	}

 	ret = request_irq(IRQ_EINT(10), eint10_interrupt,
 	IRQ_TYPE_EDGE_FALLING /*IRQF_TRIGGER_FALLING*/, "eint10", pdev);
 	if (ret < 0) {
 		printk("Request IRQ %d failed, %d\n", IRQ_EINT(10), ret);
 		goto exit;
 	}

#endif
 	return 0;

exit:
 	return ret;
}


static int irq_remove (struct platform_device *pdev)
{
 	return 0;
}


static int irq_suspend (struct platform_device *pdev, pm_message_t state)
{
 	DPRINTK("irq suspend:power off!\n");
 	return 0;
}


static int irq_resume (struct platform_device *pdev)
{
 	DPRINTK("irq resume:power on!\n");
 	return 0;
}


static struct platform_driver irq_driver = {
 		.probe = irq_probe,
 		.remove = irq_remove,
 		.suspend = irq_suspend,
 		.resume = irq_resume,
 		.driver = {
 			.name = DRIVER_NAME,
 			.owner = THIS_MODULE,
 		},
};

从这个结构里我们可以看到驱动的探测函数是 irq_probe,注销函数是 irq_remove,休眠调用的函数是 irq_suspend,唤醒调用的函数是 irq_resume,驱动的名字是DRIVER_NAME,DRIVER_NAME 是个宏定义,如下:

#define DRIVER_NAME "irq_test"

接下来我们看看驱动的探测函数 irq_probe,这个函数也是这个驱动里面最主要的函数, 这个函数一开始掉欧勇 printk 打印一条信息,接着是一个 for 循环,初始化 led 的 GPIO,如下:

for(i=0; i<LED_NUM; i++)
{
 	ret = gpio_request(led_gpios[i], "LED");
 	if (ret) {
 		printk("%s: request GPIO %d for LED failed, ret = %d\n", DRIVER_NAME,led_gpios[i], ret);
return ret;
 	}
 	s3c_gpio_cfgpin(led_gpios[i], S3C_GPIO_OUTPUT);
 	gpio_set_value(led_gpios[i], 0);
}

然后是初始化两个按键的 gpio,设置为中断模式,如下所示:

ret = gpio_request(EXYNOS4_GPX1(1), "EINT9");
if (ret) {
 printk("%s: request GPIO %d for EINT9 failed, ret = %d\n", DRIVER_NAME,EXYNOS4_GPX1(1), ret);
 return ret;
}

s3c_gpio_cfgpin(EXYNOS4_GPX1(1), S3C_GPIO_SFN(0xF));
s3c_gpio_setpull(EXYNOS4_GPX1(1), S3C_GPIO_PULL_UP);
gpio_free(EXYNOS4_GPX1(1));

ret = gpio_request(EXYNOS4_GPX1(2), "EINT10");
if (ret) {
  printk("%s: request GPIO %d for EINT10 failed, ret = %d\n", DRIVER_NAME,
  EXYNOS4_GPX1(2), ret);
 return ret;
}

s3c_gpio_cfgpin(EXYNOS4_GPX1(2), S3C_GPIO_SFN(0xF));
s3c_gpio_setpull(EXYNOS4_GPX1(2), S3C_GPIO_PULL_UP);
gpio_free(EXYNOS4_GPX1(2));

接着调用中断注册函数 request_irq 向内核注册中断处理函数,如下:

ret=request_irq(IRQ_EINT(9), eint9_interrupt,IRQ_TYPE_EDGE_FALLING, "eint9", pdev);
if (ret < 0) {
  printk("Request IRQ %d failed, %d\n", IRQ_EINT(9), ret);
  goto exit;
}
ret= request_irq(IRQ_EINT(10), eint10_interrupt,IRQ_TYPE_EDGE_FALLING, "eint10", pdev);
if (ret < 0) {
  printk("Request IRQ %d failed, %d\n", IRQ_EINT(10), ret);
  goto exit;
}

上面的中断注册函数分别注册了两个中断处理函数 eint9_interrupt 和eint10_interrupt。

eint9_interrupt 的定义如下:

static irqreturn_t eint9_interrupt(int irq, void *dev_id) {
printk("%s(%d)\n", FUNCTION , LINE );
 	if(gpio_get_value(led_gpios[0]))
 		gpio_set_value(led_gpios[0], 0);
 	else
 		gpio_set_value(led_gpios[0], 1);
 	return IRQ_HANDLED;
}

这个函数首先会打印一句信息,然后是获取 led 的状态,把状态取反。eint10_interrupt 的定义如下:

static irqreturn_t eint10_interrupt(int irq, void *dev_id) {
printk("%s(%d)\n", FUNCTION , LINE );
 	if(gpio_get_value(led_gpios[1]))
 		gpio_set_value(led_gpios[1], 0);
else
 		gpio_set_value(led_gpios[1], 1);
 	return IRQ_HANDLED;
}

这个函数和 eint9_interrupt 的功能类似。

其他的函数就和前面讲的 led 驱动里面的函数基本一样了,唯一的区别是我们在注销函数irq_remove 里面使用了 free_irq 函数来注销之前初测的中断处理函数。

把这个驱动放到内核的 driver/char 目录下面,如下图所示:

然后打开 driver/char 目录下面的 Makefile,添加“obj-y += itop4412_irq.o”,如下图所示: 

然后打开 arch/arm/mach-exynos/mack-itop4412.c 文件,找到“struct platform_device s3c_device_buzzer_ctl”,如下图所示: 

然后在它的下面添加下面的信息: 

struct platform_device s3c_device_irq_test = {
 	.name	= "irq_test",
.id	= -1
};

 如下图所示:

然后找到“&s3c_device_buzzer_ctl,”这一行,如下图所示:

在这一行的下面添加“&s3c_device_irq_test,”,如下图所示: 

然后保存并退出。因为本章实验使用到了 led 和按键,所以我们要把内核里面的 led 驱动和按键的驱动去掉,在内核目录下使用 make menuconfig 命令打开内核配置界面,如下图所示: 

进入到“Device Drivers”,如下图所示: 

然后进入到“Keyboards”界面,如下图所示: 

然后取消掉“GPIO Buttons”,如下图所示: 

然后返回到“Device Drivers”界面,如下图所示: 

然后选择“Character devices”,进入 Character devices 界面,如下图所示: 

然后取消“Enable LEDS config”,如下图所示: 

然后保存并退出内核配置界面,使用 make 命令编译内核,如下图所示: 

编译完成后,如下图所示: 

然后把编译生成的 zImage 烧写到 iTOP-4412 开发板上,烧写完成后启动开发板,系统起来以后,我们可以按开发板上的 BACK 或 HOME 按键,来看下运行结果。

当我们按下 BACK 按键时,可以看到 led 会点亮,在次按下,led 就会熄灭,同事串口会打印信息,如下图所示:

 

我们按下 HOME 键,可以看到另外一个 led 会点亮,再次按下,led 就会熄灭,同时串口会打印信息,如下图所示: 

至此,Linux 下中断驱动我们就已经完成了,最终实现了按键控制 LED 的操作。

96.7 Linux 输入子系统之按键分析

前面的教程中,介绍过通过按键和上层通信的方式:轮询读,poll 机制,异步通信机制等等。在 Linux 中,有一套标准的处理用户输入的子系统叫输入子系统,输入子系统通常用来实现用户的输入,如下图所示(下面这段取自网络),这是输入子系统的简易模型。在网盘资料“iTOP4412开发板资料汇总(不含光盘内容)\iTOP-4412开发板Linux内核开发\iTOP-4412-驱动-输入子系统之按键.zip”目录下。

主要分为三个层次:设备驱动层、核心层、事件层。

设备驱动层:主要实现对具体硬件的驱动以后获得硬件产生的数据,并且把硬件设备产生的数据封装成事件的形式提交给核心层。这些是通过申请一些 io、中断等资源完成的来读写硬件设备的,用核心层定义的规范来注册到核心层的。

核心层:核心层处于事件处理层和设备驱动层的中间,它的作用就是承上启下的作用。设备驱动层调用核心层提供的接口,把具体数据提交给核心层,核心层就自动把数据交给事件处理层。

事件处理层:这里就是对提交的数据做相应的处理了,同时这里也是用户的编程接口。 到这里,大家有没有一个疑惑,我们写一个简单的按键驱动程序,还要遵循这么多东西,

我们直接定义一个字符设备写不就得了吗?的确如果仅仅是写一个按键程序,其实怎么样写都一样,但是如果要写的是触摸屏、电脑的键盘以及鼠标等,那在驱动中要做的事就很多了,而且你写的驱动,在应用层调用时,没有一个统一的标准,那就必须做一个文档介绍给应用层提供的接口了,而且不见得能写的比较好,再一个 Android、X windows、qt 等众多应用对于linux 系统中键盘、鼠标、触摸屏等输入设备的支持都通过、或越来越倾向于标准的 input 输入子系统。如果按照 linux 提供的 input 子系统的规范来完成驱动,在设备驱动中仅仅需要把硬件产生的数据提交给核心层,而核心层就会自动提交给事件处理层,在事件处理层中已经完成了对字符设备的文件操作接口,同时也完成了所提交数据的处理,这样写设备驱动的工作量就不是很大了,而且写的驱动能更规范、更稳定,这就是咱们要研究 input 子系统的原因。当然老话告诉我们,什么都是一把双刃剑,要按照 input 子系统写驱动,你首先得了解 input 子系统,这就为写设备驱动提高了门槛,而且如果要灵活应用,还得进一步研究 input 子系统。

本文档将设计一个输入子系统的驱动例程,使用 HOME 按键,在 HOME 按键按下之后,向输入子系统中传输字母“l”。通过例程来学习输入子系统驱动的框架和用法。

96.7.1 输入子系统驱动的实现步骤

输入子系统驱动一般分为以下几个步骤:

第一步:定义 input_dev 结构体,向内核申请一个 input_dev 事件,标准内核函数接口input_allocate_device(该函数返回输入子系统事件)和 input_free_device(释放申请的输入子系统事件)。

第二步:配置输入子系统的某一类“事件类型”,例如:按键事件,鼠标事件,触摸事件等等。在 input.h 头文件中,定义了 10 多种事件类型,这里定义按键事件。

第三步:配置能产生这类事件中的那些具体操作,例如:按键按键事件中,可以输入键盘a,键盘 b 等,我们例程中定义产生 l 按键值。

第四步:注册输入子系统,标准内核函数接口 input_register_device 和input_unregister_device。

下一节结合具体的代码来分析这几个步骤。

96.7.2 驱动代码分析

驱动例程源码 keys_input_test.c 在和文档一起的压缩包中。代码如下,其中的注释已经写得很详细了。

#include <linux/init.h> 
#include <linux/module.h>
#include <linux/kernel.h> 
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h>
#include <mach/regs-gpio.h>
#include <asm/io.h>
#include <linux/regulator/consumer.h>
#include <linux/delay.h>
#include <linux/input.h>


#define MY_KEY_HOME	EXYNOS4_GPX1(1)
#define MY_KEY_BACK	EXYNOS4_GPX1(2)
#define MY_KEY_SLEEP	EXYNOS4_GPX3(3)
#define MY_KEY_VOL_U	EXYNOS4_GPX2(1)
#define MY_KEY_VOL_D	EXYNOS4_GPX2(0)


//定义一个输入子系统结构体,它是输入子系统的核心,后面所有工作都是围绕这个结构体还做的!
//第一步:定义之后,向内核申请一个 input_dev 事件,标准内核函数接口 input_allocate_device 和input_free_device//第二步:配置输入子系统的某一类“事件类型”,例如:按键事件,鼠标事件,触摸事件等等
//第三步:配置能产生这类事件中的那些具体操作,例如:按键按键事件中,可以输入键盘 a,键盘 b 等
//第四步:注册输入子系统,标准内核函数接口 input_register_device 和 input_unregister_device

static struct input_dev *buttons_dev;

static irqreturn_t ehome_interrupt(int irq, void *dev_id)
{
//printk("%s(%d)\n", FUNCTION , LINE );
//char *ps = dev_id;
//printk("%s\n",ps);
//input_event(buttons_dev, EV_KEY, KEY_L, 1);
//input_sync(buttons_dev);
//最后一个参数: 0-松开, 非 0-按下

input_report_key(buttons_dev, KEY_L, 1);
//通知上层,本次事件结束
input_sync(buttons_dev);
input_report_key(buttons_dev, KEY_L, 0);
//通知上层,本次事件结束
input_sync(buttons_dev);
return IRQ_HANDLED;
}

static void exit keys_input_exit(void)
{
printk("%s(%d)\n", FUNCTION , LINE );
gpio_free(MY_KEY_HOME);
free_irq(gpio_to_irq(MY_KEY_HOME),(void *)"my_key_home");

//释放申请的 input_dev 事件以及释放输入子系统
input_unregister_device(buttons_dev);
input_free_device(buttons_dev);
}


static int init keys_input_init(void)
{
int ret;
printk("%s(%d)\n", FUNCTION , LINE );
//第一步:定义之后,向内核申请一个 input_dev 事件,标准内核函数接口 input_allocate_device 和
//input_free_device
buttons_dev = input_allocate_device();

//第二步:配置输入子系统的某一类“事件类型”,例如:按键事件,鼠标事件,触摸事件等等
//在 input.h 头文件中,定义了 10 多种事件类型,这里定义按键事件

set_bit(EV_KEY, buttons_dev->evbit);

//set_bit(EV_REP, buttons_dev->evbit); //重复扫描
//第三步:配置能产生这类事件中的那些具体操作,例如:按键按键事件中,可以输入键盘 a,键盘 b 	等
//这里定义产生 l 按键值

set_bit(KEY_L, buttons_dev->keybit);

//第四步:注册输入子系统,标准内核函数接口 input_register_device 和 input_unregister_device
ret = input_register_device(buttons_dev);
if(ret <0){
printk("%s(%d)\n", FUNCTION , LINE );
goto exit;
}

gpio_request(MY_KEY_HOME, "my_key_home");
s3c_gpio_cfgpin(MY_KEY_HOME, S3C_GPIO_OUTPUT);
gpio_set_value(MY_KEY_HOME, 0);
gpio_free(MY_KEY_HOME);
ret = request_irq(gpio_to_irq(MY_KEY_HOME),ehome_interrupt,IRQ_TYPE_EDGE_FALLING,"my_key_home", (void *)"my_key_home");
if(ret<0){
printk("Request IRQ %s failed, %d\n","key_home" , ret);
goto exit;
}

return 0;
exit:
return ret;
}

module_init(keys_input_init);
module_exit(keys_input_exit);
MODULE_LICENSE("Dual BSD/GPL");

注释写得已经很详细,下面分析重要的部分。

#define MY_KEY_HOME EXYNOS4_GPX1(1)

以上代码,定义 5 个按键的宏,方便操作,例程中只使用了其中一个 MY_KEY_HOME。

static struct input_dev *buttons_dev;

以上代码,定义输入子系统的事件“buttons_dev”,后面的代码几乎全部是围绕这个事件来操作。

buttons_dev = input_allocate_device();

以上代码,向内核申请一个输入子系统的事件, input_allocate_device()函数返回一个输入子系统的事件。

set_bit(EV_KEY, buttons_dev->evbit);

以上代码,将输入子系统事件的类型设置为按键事件。申请输入子系统事件之后,需要配置输入子系统的某一类“事件类型”,例如:按键事件,鼠标事件,触摸事件等等。在input.h 头文件中,定义了 10 多种事件类型,我们这里定义的是按键事件。

set_bit(KEY_L, buttons_dev->keybit);

以上代码,定义按键事件中允许产生输入 l 按键值事件的操作。配置能产生这类事件中的那些具体操作,例如:按键按键事件中,可以输入键盘值 a,键盘值 b 等。

ret = input_register_device(buttons_dev);

以上代码,注册输入子系统,标准内核函数接口注册和撤销:input_register_device 和input_unregister_device。

接着看一下,中断处理函数 ehome_interrupt 部分,如下图所示。

//最后一个参数: 0-松开, 非 0-按下

input_report_key(buttons_dev, KEY_L, 1);

//通知上层,本次事件结束

input_sync(buttons_dev);

input_report_key(buttons_dev, KEY_L, 0);

//通知上层,本次事件结束

input_sync(buttons_dev);

中断产生之后,需要向系统上报产生具体哪个事件,使用 input_report_key 可以上报按键事件。这里需要注意一下,在网上的例程中,有很多例程中,需要使用定时器来防抖, 4412 是不需要在代码中进行按键防抖的,因为 4412 芯片内部可以实现硬件防抖。所以,在上面代码中,直接上报按键事件两次。input_report_key 的第三个参数,如果为 0,则是松开按键 l 事件;如果为非 0,则是按下按键 l 事件。input_sync 函数则是通知系统,本次上报事件结束。

96.7.3 测试

注册按键的输入子系统之后,会产生“/dev/input/event*”节点。 在加载驱动之前,使用命令

ls /dev/input/event*”,如下图所示。

 

加载驱动之后,如下图所示,可以看到产生了新的“/dev/input/event1”节点。 

接着需要使用到一个工具“hexdump”,这个工具一般 linux 系统都自带,如下图所示,使用命令“hexdump /dev/input/event1”,然后按一下开发板上“HOME”键。 

接着需要使用到一个工具“hexdump”,这个工具一般 linux 系统都自带,如下图所示,使用命令“hexdump /dev/input/event1”,然后按一下开发板上“HOME”键。 

如上图所示,可以看到有九列数据,比较重要的是 6,7,8 列。第 6 列表示上报的事件,第 7 列表示上报的键值,第 8 列表示按键事件是按下还是释放。

先看第一行的 6,7,8 列,分别是 0001,0026,0001,

第一行第 6 列-0001,如下图所示,是和 EV_KEY 宏的值对应。

 

第一行第 7 列-0026,如下图所示,对应宏 KEY_L,宏定义的值是 38,是个 10 进制数, 将其转换为 16 进制数就是 0x26。 

第一行第 7 列-0001,可以看到按键产生之后,第一个上报事件是有按键按下。 

如上图所示,代码“input_sync(buttons_dev);”,接着通知上层本次事件结束,所以第二行 6,7,8 列都是 0。

接着上报按键事件,有按键松开,所以第三行的 6,7 列和第一行相同,第 8 列就是 0 了, 表示有按键释放。

第四行和第二行一样,表示事件终止。到此,按键的输入子系统分析完毕。

96.8 看门狗(watchdog)配置及测试

本文档介绍 iTOP-4412 看门狗测试例程,iTOP-4412 开发板的看门狗驱动需要修改和配置使用,uboot 源码也需要修改,然后才能使用测试例程。在网盘资料的“iTOP4412开发板资料汇总(不含光盘内容)\iTOP-4412开发板Linux内核开发\iTOP-4412-驱动-看门狗watchdog以及Linux-c测试例程_V1.2.zip”目录下。

具体步骤如下:

96.8.1 内核修改

iTOP-4412 开发板内核源码中的看门狗没有配置时钟,需要配置时钟,然后驱动源码也需要修改。

内核源码增加看门狗时钟

在内核源码中,使用命令“vim arch/arm/mach-exynos/clock-exynos4.c”打开时钟配置文件。在数组中“static struct clk exynos4_init_clocks[]”中添加如下图所示代码。

上图中增加的代码如下所示: 

{
.name = “watchdog”
.parent = &exynos4_clk_pclk_acp,
.enable = exynos4_clk_ip_perir_ctrl,
.ctrlbit =  (1 << 14),
}

驱动程序

在内核源码中,看门狗驱动源码是“drivers/watchdog/s3c2410_wdt.c”文件。

使用压缩包中的“s3c2410_wdt.c”将内核源码中的文件“s3c2410_wdt.c”替换。

如果编译过内核源码,可以将 s3c2410_wdt.o 文件删除,再重新编译内核源码,至此内核修改全部完成。

内核编译

在内核目录下使用 make menuconfig 命令打开内核配置界面,进入 “Device Drivers --->”→“Watchdog Timer Support --->”→“watchdog”,如下图所示,配置上宏 定义“s3c2410-wdt”。

编译内核。使用命令“ls /dev”,可以看到新生成了设备节点 watchdog。 

96.8.2 U-boot源码修改

在内核中开启看门狗之后,需要将 uboot 源码中的看门狗关闭掉。进入 uboot 源码目录,如下图所示。

 

使用命令“vim cpu/arm_cortexa9/start.S”,搜索“emmc441_boot:”关键词,在“emmc441_boot:”之后添加代码 

#add by cym 20181009
ldr r0, =0x10060000
ldr r1, =0x8021
str r1, [r0]
#add end

 修改完之后如下图所示。

重新编译 uboot 生成新的镜像,烧写到开发板。

96.8.3 测试

更新 uboot 和内核镜像之后,就可以使用测试例程了。测试例程文件为“topeet_watchdogtest.c”。

在虚拟机 Ubuntu 下,编译测试例程“topeet_watchdogtest.c”,使用命令“arm- none-linux-gnueabi-gcc -o topeet_watchdogtest topeet_watchdogtest.c -static”编译, 如下图所示,编译得到测试程序 topeet_watchdogtest。

 

将编译得出的测试程序“topeet_watchdogtest”拷贝到开发板。 测试程序第一个参数为看门狗设备节点,第二个参数为看门狗重启时间。 

例如:./topeet_watchdogtest /dev/watchdog 5 

程序执行之后,如下图所示,程序会先启动看门狗,然后喂狗,最后停止喂狗,开发板重启。 

96.9 温度传感器(DS18B20)开发教程

本文档主要介绍 DS18B20 温度传感器在 iTOP-4412 开发板上的使用过程。主要介绍硬件连接、内核配置以及驱动和应用程序的编译运行测试。在网盘资料的“iTOP4412开发板资料汇总(不含光盘内容)\iTOP-4412开发板Linux内核开发\iTOP-4412-驱动-DS18B20温度传感器.zip”目录下。

96.9.1 硬件连接

DS18B20 引脚定义:

① DQ 为数字信号输入/输出端(电压范围:3.0~5.5V);

② GND 为电源地;

③ VDD 为外接供电电源输入端(在寄生电源接线方式时接地)。

由于 iTOP-4412 开发板的引脚输出电压是 1.8V,因此需要接一个电平转换模块。电平转换模块如下图所示。

想实现 1.8v 到 3.3 的电平转换,可以按照如下描述进行连接。

① VCCA 接 1.8V 电源

② VCCB 接 3.3V 电源

③ GND 接电源负极,两个电源共地。

④ Ax 输入 1.8VTTL 电平,Bx 将输出 3.3V 传感器模块 TTL 电平。

所以,现在要在 iTOP-4412 开发板上找到一个 1.8V 电源引脚、两个 3.3V 电源引脚、三个共地引脚以及一个 1.8V TTL 输入(针对电平转换模块来说)引脚(和驱动程序中对应)。

查看 iTOP-4412 底板原理图,找到如下所示接口。可以被我们使用。J40 接口(WIFI 接口)。

 

 J40 接口我们使用 4 号共地引脚,9 号共地引脚,12 号 1.8V 电源引脚,19 号 3.3V 电源引脚。连接情况为:

4号和 9 号引脚接电平转换模块的 GND。12 号接电平转换模块的 VCCA、19 号引脚接电平转换的 VCCB。

J38 接口(GPIO 接口)。

J38 接口我们使用 4 号共地引脚,11 号 3.3V 电源引脚,13 号 1.8V TTL 输入引脚。

连接情况为:4 号引脚接 DS18B20 模块的 GND,11 号接 DS18B20 模块的 VDD。13 号引脚接电平转换模块的 A3 引脚,然后 B3 引脚引出接到 DS18B20 模块的 DQ 引脚。

96.9.2 配置平台文件

在内核源码目录,使用“vi arch/arm/mach-exynos/mach-itop4412.c”命令打开平台文件。搜索关键词“struct platform_device s3c_device_buzzer_ctl”找到 buzzer 配置。然后在它的上面添加如下信息。

#ifdef CONFIG_DS18B20_CTL
struct platform_device s3c_device_ds18b20_ctl = {
.name	= "ds18b20",
.id = -1,
};
#endif

如下图。

 保存,退出。

再次使用“vi arch/arm/mach-exynos/mach-itop4412.c”命令打开平台文件。搜索关键词“&s3c_device_buzzer_ctl”,在这一行上面添加:

#ifdef CONFIG_DS18B20_CTL
&s3c_device_ds18b20_ctl,
#endif

如下图。

保存,退出。

使用“vi drivers/char/Kconfig ”命令打开 Kconfig 配置文件。搜索关键词“BUZZER_CTL”,在该段的上面添加如下信息 

config DS18B20_CTL

bool "Enable DS18B20 config" default y
help

Enable DS18B20 config

如下图。

保存,退出。

在内核源码目录使用“cp config_for_linux_scp_elite .config”命令配置缺省信息。(用户要根据自己板子修改配置命令)

在内核目录下使用“make menuconfig”命令打开内核配置界面,如下图。

进入到 Device Drivers --->Character devices 目录。如下图。 

 可以看到 DS18B20 设备已经被选中。

然后保存退出内核配置界面。使用“make zImage”命令编译内核。如下图。

编译完成后如下图。 

然后把编译生成的 zImage (在 arch/arm/boot 目录下)烧写到 iTOP-4412 开发板上,烧写完成后启动开发板。

开发板启动之后,使用命令“ls /sys/devices/platform/”可以查看到新注册的ds18b20 设备,如下图所示。

设备注册完成。

96.9.3 驱动应用程序

驱动程序:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/input.h>
#include <linux/errno.h>
#include <asm/uaccess.h>
#include <linux/sched.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <asm/irq.h>
#include <linux/gpio.h>
#include <plat/gpio-cfg.h>
#include <mach/gpio.h>
#include <mach/gpio-exynos4.h>

#define DS18B20_DEBUG

#ifdef DS18B20_DEBUG
#define DPRINTK(x...)  printk("DS18B20_CTL DEBUG:" x)
#else
#define DPRINTK(x...)

#endif


#define DRIVER_NAME "ds18b20"
#define DEVICE_NAME "ds18b20"
#define DS18B20_DQ	EXYNOS4_GPA0(7) //J38 接口 13 号引脚
#define OUTPUT	S3C_GPIO_OUTPUT
#define INPUT	S3C_GPIO_INPUT

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("TOPEET");


unsigned char init_ds(void)//ds18b20 复位,返回 0 成功,返回 1 失败
{
unsigned char ret = 1;
int i =0;
s3c_gpio_cfgpin(DS18B20_DQ,OUTPUT);
s3c_gpio_setpull(DS18B20_DQ, S3C_GPIO_PULL_DOWN);
gpio_direction_output(DS18B20_DQ,0);
udelay(250);
gpio_direction_output(DS18B20_DQ,0);//发送复位脉冲
udelay(500);//延时( >480us )
gpio_direction_output(DS18B20_DQ,1);//拉高数据线

//用返回的值来判断初始化有没有成功,18B20 存在的话=0,否则=1
s3c_gpio_cfgpin(DS18B20_DQ, INPUT); 

while((ret==1)&&(i<10)){
ret = gpio_get_value(DS18B20_DQ);
udelay(10);
i++;
}

if(ret==0){
return 0;
}
else{
return -1;
}
}


void write_byte(char data)//向 18b20 写一个字节
{	
//数据线从高电平拉至低电平,产生写起始信号。15us 之内将所需写的位送到数据线上,
int i = 0;

s3c_gpio_cfgpin(DS18B20_DQ,OUTPUT);
s3c_gpio_setpull(DS18B20_DQ, S3C_GPIO_PULL_UP);
for(i=0;i<8;i++)
{
gpio_direction_output(DS18B20_DQ,0);
udelay(10);
gpio_direction_output(DS18B20_DQ,1);
gpio_direction_output(DS18B20_DQ, data&0x01);

udelay(40);
gpio_direction_output(DS18B20_DQ,1);
udelay(2);
data >>= 1;
}
}

unsigned char read_byte(void)//从 18b20 读一个字节
{	
//主机数据线先从高拉至低电平 1us 以上,再使数据线升为高电平,从而产生读信号

unsigned char i;

unsigned char data=0;

s3c_gpio_cfgpin(DS18B20_DQ,OUTPUT);

for(i=0;i<8;i++)
{
data >>= 1;
gpio_direction_output(DS18B20_DQ,0);
udelay(1);
gpio_direction_output(DS18B20_DQ,1);
s3c_gpio_cfgpin(DS18B20_DQ,INPUT);
udelay(10);
if(gpio_get_value(DS18B20_DQ))
data |= 0x80;

udelay(50);
s3c_gpio_cfgpin(DS18B20_DQ,OUTPUT);
gpio_direction_output(DS18B20_DQ,0);
gpio_direction_output(DS18B20_DQ,1);
}

return data;
}


static ssize_t ds18b20_ctl_read(struct file *files, unsigned int *buffer, size_t count, loff_t *ppos)
{
unsigned int tmp;
unsigned int ret;
unsigned int th,tl;
th=tl=0;
init_ds( );
udelay(500);
write_byte(0xcc);//跳过读序号列号的操作
write_byte(0x44); //启动温度转换
init_ds( );
udelay(500);
write_byte(0xcc);	//跳过读序号列号的操作
write_byte(0xbe);	//准备读温度
tl= read_byte( );	//读出温度的低位 LSB
th= read_byte( );	//读出温度的高位 MSB
th<<=8;
tl|=th;	//获取温度
tmp=tl;
ret=copy_to_user(buffer, &tmp, sizeof(tmp));
if(ret>0)
{
return 0;
}
return -1;
}


static int ds18b20_ctl_close(struct inode *inode, struct file *file){
printk(" %s	! ! !\n", FUNCTION );
DPRINTK("Device Closed Success!\n");
return 0;
}


static int ds18b20_ctl_open(struct inode *inode, struct file *file){
printk(" %s	! ! !\n", FUNCTION );
DPRINTK("Device Opened Success!\n");
return nonseekable_open(inode,file);
}


int ds18b20_pm(bool enable)
{
int ret = 0;
printk(" %s	! ! !\n", FUNCTION );
printk("debug: DS18B20 PM return %d\r\n" , ret);
return ret;
};
static struct file_operations ds18b20_ctl_ops = {
.owner = THIS_MODULE,
.open = ds18b20_ctl_open,
.release = ds18b20_ctl_close,
.read	= ds18b20_ctl_read,
};


static	struct miscdevice ds18b20_ctl_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &ds18b20_ctl_ops,
};


static int ds18b20_ctl_probe(struct platform_device *pdv){
int ret;
char *banner = "ds18b20 Initialize\n";
printk(banner);
printk(" %s	! ! !\n", FUNCTION );
ret = gpio_request(DS18B20_DQ,"GPA0_7");
if(ret){
DPRINTK( "gpio_request DS18B20_DQ failed!\n");
return ret;
}

ret=init_ds();
if(ret==0){
DPRINTK( "DS18B20 initialized ok!\n");
}
else
DPRINTK( "DS18B20 initialized fail!\n");
s3c_gpio_cfgpin(DS18B20_DQ,OUTPUT);
gpio_direction_output(DS18B20_DQ, 1);
ret = misc_register(&ds18b20_ctl_dev);
if(ret<0)
{
printk("leds:register device failed!\n");
goto exit;
}
return 0;

exit:
misc_deregister(&ds18b20_ctl_dev);
return ret;
}


static int ds18b20_ctl_remove(struct platform_device *pdv){
printk(" %s	! ! !\n", FUNCTION );
printk(KERN_EMERG "\tremove\n");
gpio_free(DS18B20_DQ);
misc_deregister(&ds18b20_ctl_dev);
return 0;
}


static void ds18b20_ctl_shutdown(struct platform_device *pdv){
printk(" %s	! ! !\n", FUNCTION );
}


static int ds18b20_ctl_suspend(struct platform_device *pdv,pm_message_t pmt){
printk(" %s	! ! !\n", FUNCTION );
DPRINTK("ds18b20 suspend:power off!\n");
return 0;
}

static int ds18b20_ctl_resume(struct platform_device *pdv){
printk(" %s	! ! !\n", FUNCTION );
DPRINTK("ds18b20 resume:power on!\n");
return 0;
}


struct platform_driver ds18b20_ctl_driver = {
.probe = ds18b20_ctl_probe,
.remove = ds18b20_ctl_remove,
.shutdown = ds18b20_ctl_shutdown,
.suspend = ds18b20_ctl_suspend,
.resume = ds18b20_ctl_resume,
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
}
};

static int ds18b20_ctl_init(void)
{
int DriverState;
printk(" %s	! ! !\n", FUNCTION );
DriverState = platform_driver_register(&ds18b20_ctl_driver); 
printk(KERN_EMERG "\tDriverState is %d\n",DriverState); 
return 0;
}

static void ds18b20_ctl_exit(void)
{
printk(" %s	! ! !\n", FUNCTION ); 
platform_driver_unregister(&ds18b20_ctl_driver);
}

module_init(ds18b20_ctl_init); 
module_exit(ds18b20_ctl_exit);

应用程序:

#include <stdio.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <unistd.h> 
#include <stdlib.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <termios.h>

#define K 0.0625

int main(int argc , char **argv){

int fd,i=5;
char count = 5;
unsigned int tmp;
float res=0;
char *hello_node = "/dev/ds18b20";
if((fd = open(hello_node,O_RDWR|O_NOCTTY))<0){
printf("APP open %s failed",hello_node);
}
else{
printf("open ds18b20 success \n" );
}

while(i--){
read(fd, &tmp , sizeof(tmp));
printf("read");
sleep(1);
}
res=tmp*K;
printf("the currently temperature is %f\n",res); 
close(fd);
return 0;
}

96.9.4 驱动程序编译

把驱动程序“itop4412-ds18b20.c”和 Makefile 文件上传到同一目录,执行“make” 命令。如下图。

通过 U 盘挂载、tftp 或者 nfs 功能把“itop4412-ds18b20.ko”文件上传到开发板。

96.9.5 应用程序编译

把应用程序“test-itop4412-ds18b20.c”上传到 ubuntu 系统。使用“ arm-none- linux-gnueabi-gcc -o test-itop4412-ds18b20 test-itop4412-ds18b20.c -static”命令来静态编译应用程序。如下图。

 

通过 U 盘挂载、tftp 或者 nfs 功能把“test-itop4412-ds18b20”文件上传到开发板。如下图。 

96.9.6 运行测试

使用“insmod itop4412-ds18b20.ko”命令来加载驱动程序。如下图。

 由上图可知。进入 probe,并且 ds18b20 初始化成功。

使用“ ./test-itop4412-ds18b20”运行应用程序,提示权限不够。使用“chmod 777 test-itop4412-ds18b20”命令修改权限后,继续运行应用程序,如下图。

程序运行大约 5、6 秒之后,自动停止,并返回温度值。由上图可知当前室内温度为22.68℃。

使用“rmmod itop4412-ds18b20”命令卸载驱动。如下图。

由上图可知卸载成功,测试完成。

96.10 存储芯片 AT24C08 驱动开发教程

本章主要介绍 i2c 设备——AT24C08 的开发教程。在网盘资料的“iTOP4412开发板资料汇总(不含光盘内容)\iTOP-4412开发板Linux内核开发\iTOP-4412-i2c-AT24C08存储例程.zip”目录下。

96.10.1 硬件连接

AT24C08 提供 8192 位的串行电可擦写可编程只读存储器(EEPROM),组织形式为 1024 字×8 位字长。是一种串口通信标准 I2C 总线标准。

AT24C08 存储模块的地线(4 号引脚)接 j38 接口的 4 号引脚,AT24C08 存储模块的电源(8 号引脚)接 j38 接口的 11 号引脚,SDA 接 j27 接口的 2 号引脚,SCL 接 j27 接口的 4 号引脚。

96.10.2 平台文件注册

在内核源码目录使用“vim arch/arm/mach-exynos/mach-itop4412.c”命令打开平台文件。搜索关键词“i2c_devs7[]”并注释掉“i2c_devs7[]”结构体的内容。内容修改为:

/* I2C7 */

//static struct i2c_board_info i2c_devs7[] initdata = {


//#if defined(CONFIG_CPU_TYPE_SCP_ELITE) || defined(CONFIG_CPU_TYPE_POP_ELITE) || defined(CONFIG_CPU_TYPE_POP2G_ELITE)
/* add by cym 20130417 for TSC2007 TouchScreen */

//#ifdef CONFIG_TOUCHSCREEN_TSC2007

/*

{
I2C_BOARD_INFO("tsc2007", 0x48),
.type= "tsc2007",
.platform_data = &tsc2007_info,
.irq = IRQ_EINT(0),
}

#endif
*/
/* end add */
//#endif
//};

如下图。

保存退出。

再次打开平台文件,搜索关键词“i2c_devs7[]”并在上面添加如下内容。

#include <linux/i2c/at24.h>

static struct at24_platform_data at24c02 = {
.byte_len	= SZ_2K / 8,
.page_size	= 8,
.flags = 0,
};
;
/* I2C7 */
static struct i2c_board_info i2c_devs7[] initdata = {
{
I2C_BOARD_INFO("at24", 0x50),	
.platform_data = &at24c02, /
},
};

如下图。

保存退出。

96.10.3 配置编译内核

在内核源码目录使用“cp config_for_linux_scp_elite .config”配置缺省文件。然后使用“make menuconfig”命令打开 menuconfig 配置窗口。

进入到:Device Drivers ---> Misc devices ---> EEPROM support ---> 目录, 选中“I2C EEPROMs from most vendors”和“Old I2C EEPROM reader ”选项,如下图。

 配置完成后,保存退出。

然后使用编译命令“make zImage”编译内核。编译完成后在内核源码下的“arch/arm/boot”目录找到新生成的 zImage,烧写到开发板。

96.10.4 应用测试程序

#include <stdio.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/ioctl.h> 
#include <stdlib.h> 
#include <fcntl.h> 
#include <sys/io.h>

int main(int argc, char **argv)
{
int ret, fd, i, j;
char read_data[256];
char write_data[256];
char offset;

fd = open("/sys/devices/platform/s3c2440-i2c.7/i2c-7/7-0050/eeprom", O_RDWR);
if(fd < 0){
printf("Open at24c02 fail\n");
return -1;
}


ret = read(fd, &offset, 1);
if(ret < 0){
printf("Read error\n");
return -1;
}else if(ret < 1){
perror("Incomplete read\n");
printf("%d\n", ret);
return -1;
}


for(i = 0; i < 256; i++)
write_data[i] = offset+ 1 + i;
lseek(fd, 0 , SEEK_SET);	//It's a must, or something wierd will happen

ret = write(fd, write_data, 256);
if(ret < 0){
printf("Write error\n");
return -1;
}

lseek(fd, 0 , SEEK_SET);	//It's a must, or something wierd will happen

ret = read(fd, read_data, 256);
if(ret < 0){
printf("Read error\n");
return -1;
}else if(ret < 256){
perror("Incomplete read\n");
printf("%d\n", ret);
return -1;
}


for(i = 0; i < 256; i++){
if(i %16 == 0)
printf("\n");
printf(" %03d ", read_data[i]);
}
printf("\n");
}

 96.10.5 编译运行测试

把应用程序“test_at24c08.c”上传到 ubuntu 系统。使用“arm-none-linux-gnueabi- gcc -o test_at24c08 test_at24c08.c -static”命令来静态编译应用程序。如下图。

 通过 U 盘挂载、tftp 或者 nfs 功能把“test_at24c08”文件上传到开发板。如下图。

 使用“./test_at24c08”命令,运行应用程序。发现权限不允许,修改权限后,再次运行。如下图。 

由上图可知,成功读取到 AT24C08 模块中的信号。测试完成。

96.11 USB转串口ch34x 移植教程

在压缩包中有 ch34x 的驱动源码文件“ch34x.c”,以及编译好的驱动文件“ch34x.ko”, 以及编译脚本 Makefile,在网盘资料的“”iTOP4412开发板资料汇总(不含光盘内容)\iTOP-4412开发板Linux内核开发\iTOP-4412开发板-驱动-usb转串口ch34x移植使用文档_V1.0.zip”目录下。

请注意,Makefile 文件是 4412 的,如果使用其它型号,请注意参考对应的独立文档来修改。

在开发板的 USB 端口上,接上 ch34x 的 usb 转串口模块,可以看到如下提示。下面只提示有检测到设备,但是没有对应驱动。

 

拔掉串口模块,将编译生成的或者压缩包中的“ch34x.ko”拷贝到开发板上,使用命令“insmod ch34x.ko”加载驱动,如下图所示,然后接上 ch34x 的 usb 转串口模块,可以看到有提示“ttyUSB0”。 

如下图所示,可以看到“/dev”目录下有设备设备节点“/dev/ttyUSB0”。 

串口的测试方法可以参考串口例程。

96.12 USB-WIFI 驱动移植和测试

配套资料在网盘资料的“”iTOP4412开发板资料汇总(不含光盘内容)\iTOP-4412开发板Linux内核开发\iTOP-4412-驱动-usb文档11-usbWIFI驱动移植.zip目录下。

USB 的 WIFI 移植,主要分为以下几个步骤:

  1. 找到 WIFI 的驱动源码;
  2. 找到产品识别码(Product ID)和供应商 ID(Vendor ID)将其添加到 USB 的 WIFI 源码中;
  3. 设置编译器,内核路径,将源码编译通过;
  4. 移植 OpenSSL 以及 libnl

Openssl 是为网络通信提供安全及数据完整性的一种安全协议,囊括了主要的密码算法、常用的密钥和证书封装管理功能以及 SSL 协议,并提供了丰富的应用程序供测试或其它目的使用。SSL 是 Secure Socket Layer(安全套接层)的缩写,可以在 Internet 上提供秘密性传输。官方源码下载网站 http://www.openssl.org/source/

  1. 移植 wpa_supplicant

upplicant 是恳求者的意思,是 wpa 的发起者,是发送认证请求的设备(手机),它的编译依赖于 libopenSSL

  1. 测试

一般情况下,USB 的 WIFI 驱动源码除了 ID 部分,其它都不需要修改,wpa 工具有可能因为版本问题需要尝试一到两个版本。移植的 USBwifi 是 360 二代 wifi,请大家在购买硬件的时候要注意硬件版本。

96.12.1 驱动源码下载

360WiFi2 代使用的是 MT7601U 芯片,要移植首先要找到厂商提供的驱动源码,这里我们去 www.mediatek.com 官网搜索 MT7601,可以得到相关的驱动源码,如下图所示。

下载得到“DPO_MT7601U_LinuxSTA_3.0.0.4_20130913.tar.gz”,该驱动在压缩包中也有提供,如下图所示。 

如上图所示,在 wpa_lib 文件夹是两个 wpa 工具的支持库,所有文件的用法在本文档后面部分都有详细描述。

将驱动源码拷贝到工作目录,如下图。

 解压完成得到驱动的代码,如下图所示。

96.12.2 驱动的 ID 修改

为了让驱动识别 USB 设备,需要得到 360WiFi 的 PID 和 VID,即产品识别码(Product ID)和供应商 ID(Vendor ID)。先将 360WiFi 插到 Ubuntu 上,运行命令“lsusb”来得到,或者查看厂商的设备手册也可以得到,运行 lsusb 如下图所示。

可以看到 ID 一栏后面得到 VID 为 148f,PID 为 760b。

进入驱动源码的目录“/DPO_MT7601U_LinuxSTA_3.0.0.4_20130913”,打开文件“common/rtusb_dev_id.c”,在第 42 行下面加上 360WiFi 的 ID 号

{USB_DEVICE(0x148f,0x760b)}, /* 360 Wifi */

如下图:

96.12.3 驱动移植和编译

驱动源码目录“/DPO_MT7601U_LinuxSTA_3.0.0.4_20130913”下的 Makefile 编译文件需要修改。其实,在任何一个大点的驱动或者应用,都会提供 Makefile 文件,一般都需要修改编译器、目标平台和目标内核等部分。

打开 Makefile 文件,在第 30 行,可以看到默认平台为 PC。

将 PC 注释掉,然后在这里定义一个自己的平台 4412,如下图所示。 

然后针对自定义平台,在第 105 行开始,添加交叉编译链和内核路径。红色部分要和自己 Ubuntu 中的内核源码目录对应,而且内核源码一定要先编译通过,这一点在前面都强调过的。

ifeq ($(PLATFORM),4412)

LINUX_SRC = /home/frao/Sourcecode/kernel/iTop4412_Kernel_3.0 

CROSS_COMPILE = arm-none-linux-gnueabi-

endif

这里的 LINUX_SRC 为要运行平台的内核源码,此时为 4412 的内核源码位置。CROSS_COMPILE 为要使用的交叉编译器,可以是绝对路径或者环境变量。添加完成如下图所示,Makefile 文件就修改完成。

 

接着修改配置文件“os/linux/config.mk”,从 854 行起,添加以下内容。

ifeq ($(PLATFORM),4412)

EXTRA_CFLAGS := $(WFLAGS)

endif

 如果在 android 平台使用,则应确保该文件中下面两项为 y。

在“include/rtmp_def.h”的第 1601 行,该预编译定义了分别在 Android 和嵌入式Linux 平台上的设备名称,即在 Android 平台显示 wlan 设备名,在 Linux 显示 ra 设备名,如下图所示,由于一般在 Android 下的 HAL 和脚本中的 wifi 设备结点名称用的 wlan0,嵌入Linux 设备中 wifi 设备结点名称为 ra0,所以这里我们保持不变。 

在 DPO_MT7601U_LinuxSTA_3.0.0.4_20130913 目录下执行“make”命令。编译成功后如下图所示。 

按照上图中提示,编译生成驱动模块 mt7601Usta.ko,后面可以动态加载到内核中。

源码目录下的 RT2870STA.dat 需要拷贝到开发板的“/etc/Wireless/RT2870STA/”下, 这个目录默认没有,需要手动创建 。

 

至此,驱动编译完成。

96.12.4 编译器设置

wpa 是一款开源工具,移植这个工具需要做以下工作:

  1. 编译器环境设置;
  2. OpenSSL 库的移植;
  3. libnl 库的移植;
  4. 移植 wpa_supplicant 工具。

需要用到的库和工具的源码在 wpa_supplicant.zip 压缩包中,解压之后得到hostap.tar.gz、libnl-1.1.4.tar.gz 和 openssl-1.1.0g.tar.gz 三个源码。

 在进行编译之前要先修改编译器为 4.3.2 版本(编译器版本和编译文件系统的编译器保持一致),如何设置编译器参见手册的 “Qt/E4.7.1 编译器的安装”部分的介绍。另外为了避免使用环境变量设置编译器而可能出现的问题,文档中大部分编译是使用编译器的绝对路径,用户也应先找到自己编译器的绝对路径待用。下图是本次编译使用的编译器以及编译器压缩包。

由上图可知该编译器的绝对路径为“/usr/local/arm/4.3.2/bin/ arm-none-linux- gnueabi-gcc”。

用户需要将提供的源码压缩包拷贝到 Ubuntu 的工作目录,分别解压 ,如下图所示。

96.12.5 移植OpenSSL

解压 OpenSSL 压缩包“openssl-1.1.0g.tar.gz”。进入目录 openssl-1.1.0g,内容如下图所示。

执行下面指令,做相应配置。

./config no-asm shared --prefix=$(pwd)/ install

执行完成后如下图所示。

 

no-asm:是在交叉编译过程中不使用汇编代码代码加速编译过程,原因是它的汇编代码是不支持 arm 格式。

shared:生成动态连接库。

prefix :指定 make install 后生成目录的路径,不修改此项则默认为 OPENSSLDIR 目录(/usr/local/ssl)。

使用命令“vim Makefile”打开 Makefile,搜索 CFLAG,定位到下图中所示位置。

 

删除上图中红框中的“-m64”,完成后 CFLAG 应如下图所示。 

执行以下命令,编译 OpenSSL 库,注意这里使用的是交叉编译器的绝对路径。

make CROSS_COMPILE=/usr/local/arm/4.3.2/bin/arm-none-linux-gnueabi-

编译完成后如下图所示。

执行下面命令,将编译好的库文件拷贝到第一步指定的目录。

make install

如下图所示在当前目录下的 install 目录下生成了头文件和库文件:

在编译 wpa 工具的时候需要用到该“ install/include”下的头文件和“ install/lib” 下的库,在编译 wpa 的时候,会提醒大家设置这个路径。

而在 wpa 工具运行时需要用到“ install/lib”下的库,所以也要将 lib 下所有文件拷贝到开发板/lib 文件夹中。

96.12.6 移植 libnl

解压 libnl 压缩包“libnl-1.1.4.tar.gz”。

libnl 是为了方便应用程序使用 netlink 接口而开发的一个库。这个库为原始 netlink 消息传递以及不同的 netlink family 专用接口提供了一个统一的接口。进入目录“libnl-1.1.4/”,如下图所示。

执行下面的指令,配置编译架构。

./configure --prefix=$(pwd)/ install --enable-shared --enable-static

其中--prefix=$(pwd)/ install 指定了编译出来的库存放的路径,一般将其放在当前目录下的 install 目录下,执行结果如下图所示。

 

执行下面的命令,编译库。

make CC=/usr/local/arm/4.3.2/bin/arm-none-linux-gnueabi-gcc

完成后如下图所示。

使用命令“make install”, 将编译好的库文件拷贝到指定目录“ install”下。在当前目录下的 install 目录下生成了头文件和库文件 ,如下图所示。 

在编译 wpa 工具的时候需要用到“ install/include”下的头文件和“ install/lib”下的库,我们在编译 wpa 的时候,会提醒大家设置这个路径。

而在 wpa 程序运行时需要用到“ install/lib”下的库,所以同样要将 lib 下所有文件拷贝到开发板/lib 文件夹中。

96.12.7 移植 wpa_supplicant

wpa_supplicant 是作为 hostap 的一部分,它的源码在 hostap 目录中。

解压 wpa 压缩包“hostap.tar.gz”。

使用命令“cd hostap/wpa_supplicant/”进入 wpa_supplicant 目录,如下图所示。

 

使用命令“cp defconfig .config”复制一份默认的配置文件。然后使用命令“vim Makefile”修改 Makefile,如下图所示。 

ifndef CC

CC=gcc

endif

修改为如下所示,注意下面的黑体部分,这里是设置库 libnl 和 openssl 的头文件和库文件,用户一定要根据实际路径来设置,否则将会无法编译。

CFLAGS += -I../../libnl-1.1.4/ install/include/

CFLAGS += -I../../openssl-1.1.0g/ install/include/

LIBS += -L../../libnl-1.1.4/   install/lib/

LIBS += -L../../openssl-1.1.0g/ install/lib/

#ifndef CC

CC=/usr/local/arm/4.3.2/bin/arm-none-linux-gnueabi-gcc

#endif

注意,CC 路径为自己环境中的交叉工具链路径。修改完成后 Makefile 如下图所示。

 

接下来使用命令“make”编译,结果如下图所示。 

此时在当前目录下出现了“wpa_supplicant”工具,如下图所示。 

将编译好的 wpa_supplicant 工具以及 wpa_supplicant 拷贝到开发板上的“/usr/sbin” 目录下。

至此,wpa 部分完成。

96.12.8 测试

在测试前,需要拷贝的内容如下:

拷贝“RT2870STA.dat”到开发板中的“/etc/Wireless/RT2870STA/”目录下;作者编译好的文件,在压缩包中也有提供。

拷贝 libnl 和 openssl 编译出来的“ install/lib”目录下的库文件到开发板的/lib 目录下。作者提供编译好的两个库,都在压缩包中的 wpa_lib 目录下。

拷贝 wpa_supplicant 工具以及 wpa_passphrase 到开发板的“/usr/sbin”目录下;作者编译好的两个工具在压缩包中也有提供。

作者编译好的驱动文件“mt7601Usta.ko”在压缩包中也有提供。

将提供的“default.script”拷贝到开发板的“/usr/share/udhcpc”目录下,这个目录默认不存在,需要手动创建,拷贝完如图所示。注意:如果是 qt 系统则不需要拷贝,如果是根文件系统则需要拷贝这个文件。

 

接着我们可以按照文档中的步骤来操作测试。

将 360Wifi 插到开发板的 USB 接口上,可以在端口上看到如下打印信息。

 

拷贝驱动程序到开发,加载驱动程序“mt7601Usta.ko”,如下图。 

配置 ra0 网络,输入“ifconfig ra0 up”,如下图所示,因为作者前面没有将 ra 修改为wlan,所以这里使用命令“ifconfig ra0 up”,如果修改了,则需要使用“ifconfig wlan0 up”命令。 

使用命令“wpa_passphrase 帐号 密码 > /etc/ wpa_supplicant.conf”配置 WiFi, 如下图所示。 

执行命令“wpa_supplicant -B -i ra0 -D wext -c /etc/wpa_supplicant.conf”。 

执行命令“udhcpc -i ra0”获取动态 IP 以及网关 DNS。 

到此,就可以使用 360WiFi 上网了,测试完毕,如下图所示。 

96.12.9 小结

大家看了 USB WIFI 驱动的移植,可能会感到很困惑,为什么前面的 USB 学习文档介绍了那么多,移植驱动的时候反而就是这么简单的几个步骤。实际上,USB 驱动框架,是非常复杂的,但是核心的内部驱动部分,甚至外部驱动一般也不需要我们从头开始写的,我们只需要会‘移植’过来就可以了,希望通过本文档能够感受到什么叫“移植”。

大家可能还有疑惑,这些驱动是谁写的?首先是 USB 内部驱动,它们是 Linus 同学带着一帮全世界最聪明最厉害的程序员,免费做的 Linux 核心部分代码,并且免费开源给大家用; 另外一部分是外部驱动,例如这个 360WIFI 的芯片,它的驱动当然是 MTK(芯片厂商)的工程师来做的,他们也是在前人的基础上一步一步完成的,也不会一蹴而就的从零开始写一个驱动,而且他们一般是一个团队,专门做 USB 部分的代码。假如大家将来有机会进入芯片厂商工作,只需要有良好的基本功就没什么问题的。

大家可能还有疑惑,怎么连 datasheet 都不提供,寄存器怎么配置呢?其实像这样大的

驱动,寄存器配置都会提供一个脚本或者一个二进制文件(叫芯片固件应该更合适),有的会提供专门的小程序将配置文件在驱动加载前就烧写到芯片中,类似 MT6620。如果要修改一些参数,直接通过上层工具就可以实现。

96.13 关闭调试串口以及修改串口权限的方法

本节我们介绍一下 iTOP-4412 的串口设置。

首先 iTOP-4412 可以支持四路串口,分别编号为 0,1,2,3,他们在内核里面的设备节点分别是:/dev/ttySAC0,/dev/ttySAC1,/dev/ttySAC2,/dev/ttySAC3。

iTOP-4412 使用串口 2 作为调试串口,也就是/dev/ttySAC2,如果我们想把串口 2 也作为普通串口来使用,需要修改下内核的配置,重新编译下内核,具体修改方法如下:

首先在内核源码目录下执行命令”make menuconfig“打开内核配置界面,如下图所示:

然后进入到 Boot options 界面,如下图所示: 

然后选择“(console=ttySAC2,115200) Default kernel command string”,如下图所示: 

然后进入到”Default kernel command string”配置界面,如下图所示:

然后把里面的“console=ttySAC2,115200”改成“console=NULL,115200”,如下图所示: 

然后保存并退出 menuconfig 界面,回到内核源码目录下,如下图所示: 

然后输入“make”命令,开始编译内核,如下图所示: 

编译完成后会在“arch/arm/boot”下会生成“zImage”文件,如下图所示: 

最好把生成的”zImage“烧写到开发板上就可以使用串口 2(/dev/ttySAC2)了。

我们在 android 下操作串口有时会遇到没有权限的问题,这就需要修改下 android 的启动脚本,在里面修改下串口的权限。

具体修改方法是:

在 android 源码目录下输入 “vi device/samsung/smdk4x12/conf/init.smdk4x12.rc”,如下图所示:

在里面找到修改权限的地方,如下图所示: 

 

上面的“chmod 777 xxxx”就是修改设备节点的权限,比如我们现在想修改串口 0(/dev/ttySAC0)的权限,那我们在这下面输入“chmod 777 /dev/ttySAC0”就可以了, 如下图所示: 

 

其他几个串口的修改方法也是这样的,修改完以后,保存并退出,回到 android 源码的目录下面,如下图所示: 

 

然后输入“./build_android.sh ”开始编译 android,如下图所示: 

 

编译完成后,把生成的“ramdisk-uboot.img”和“system.img”烧到开发板里面,重新启动 android,就可以看到串口 0(/dev/ttySAC0)的权限修改了。  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值