站在芯片原厂角度移植最新u-boot 2020.07到jz2440开发板(4)(全部驱动使用设备树与dm设备模型)

8. 编写串口驱动

终于开始进行驱动的移植与编写了!

提到驱动,在最近几年的u-boot版本中,uboot引入了驱动模型(driver model),那具体是什么呢?各位别急,我们之后的每一个驱动都将使用这种驱动模型进行编写,那现在先看比较简单的串口驱动,从实际的驱动中一步步的了解驱动模型dm。

8.1 驱动代码

首先在drivers/serial目录下新建一个文件serial_s3c2440.c,全部内容如下:

/* SPDX-License-Identifier: GPL-2.0+ */
/*
 * (C) Copyright 2020 Asymptote
 */

#include <common.h>
#include <dm.h>
#include <errno.h>
#include <fdtdec.h>
#include <linux/compiler.h>
#include <asm/io.h>
#include <serial.h>

DECLARE_GLOBAL_DATA_PTR;

struct s3c2440_serial {
	u32 ulcon;
	u32 ucon;
	u32 ufcon;
	u32 umcon;
	u32 utrstat;
	u32 uerstat;
	u32 ufstat;
	u32 umstat;
	u32 utxh;
	u32 urxh;
	u32 ubrdiv;
};

struct s3c2440_serial_priv {
	struct s3c2440_serial *reg;
};

int s3c2440_serial_setbrg(struct udevice *dev, int baudrate)
{
	struct s3c2440_serial_priv *priv = dev_get_priv(dev);
	u32 val, uclk;

	/* 使用pclk时钟,大小为50MHz */
	uclk = 50000000;

	val = uclk / baudrate;
	writel(val / 16 - 1, &priv->reg->ubrdiv);

	return 0;
}

static int s3c2440_serial_getc(struct udevice *dev)
{
	struct s3c2440_serial_priv *priv = dev_get_priv(dev);

	if (!(readl(&priv->reg->utrstat) & (1 << 0)))
		return -EAGAIN;

	return (int)(readb(&priv->reg->urxh) & 0xff);
}

static int s3c2440_serial_putc(struct udevice *dev, const char ch)
{
	struct s3c2440_serial_priv *priv = dev_get_priv(dev);

	if (!(readl(&priv->reg->utrstat) & (1 << 2)))
		return -EAGAIN;

	writeb(ch, &priv->reg->utxh);

	return 0;
}

static int s3c2440_serial_pending(struct udevice *dev, bool input)
{
	struct s3c2440_serial_priv *priv = dev_get_priv(dev);
	uint32_t utrstat;

	utrstat = readl(&priv->reg->utrstat);
	
	if (input)
		return (utrstat & (1 << 0));
	else
		return (utrstat & (1 << 2));
}

static const struct dm_serial_ops s3c2440_serial_ops = {
	.putc = s3c2440_serial_putc,
	.pending = s3c2440_serial_pending,
	.getc = s3c2440_serial_getc,
	.setbrg = s3c2440_serial_setbrg,
};

#define GPHCON  (*(volatile unsigned long *)0x56000070)
#define GPHUP	(*(volatile unsigned long *)0x56000078)

static int s3c2440_serial_probe(struct udevice *dev)
{
	struct s3c2440_serial_priv *priv = dev_get_priv(dev);

	priv->reg = (void *)dev_read_addr(dev);

 	/* GPH2,GPH3用作TXD0,RXD0 */
    GPHCON |= 0xa0;   
    /* GPH2,GPH3内部上拉 */
	GPHUP = 0x0c;     

    /* 8N1(8个数据位,无较验,1个停止位) */
	writel(0x03, &priv->reg->ulcon);
    /* 查询方式,UART时钟源为PCLK */
	writel(0x05, &priv->reg->ucon);
    /* 不使用FIFO */
	writel(0x00, &priv->reg->ufcon);
    /* 不使用流控 */
	writel(0x00, &priv->reg->umcon);

	return 0;
}

static const struct udevice_id s3c2440_serial_ids[] = {
	{ .compatible = "samsung,s3c2440-uart" },
	{ }
};

U_BOOT_DRIVER(serial_s3c2440) = {
	.name = "serial_s3c2440",
	.id	= UCLASS_SERIAL,
	.of_match = s3c2440_serial_ids,
	.probe = s3c2440_serial_probe,
	.ops = &s3c2440_serial_ops,
	.priv_auto_alloc_size = sizeof(struct s3c2440_serial_priv),
};

这个驱动非常简单,只有区区130行左右,但是简单归简单,麻雀虽小五脏俱全,要想学习u-boot的设备模型框架,这是一个非常好的例子。

首先说明一下,这个驱动是我借鉴韦东山老师的串口裸机驱动以及u-boot中的其他使用设备模型的串口驱动重新写的,算是做了一个组合者的作用。

另外,大家还记得之前编译的最后default_serial_console函数未定义的问题吗?default_serial_console函数属于u-boot旧的串口驱动框架,我们的驱动由于是遵循新的设备模型框架的,所以就可以不用理会这个问题了。

好,下面开始进行介绍。

与阅读linux驱动代码相同,我们从下往上看,首先是定义一个如下的结构体,

/* 可以类比为linux设备驱动模型中的platform_driver */
U_BOOT_DRIVER(serial_s3c2440) = {
	.name = "serial_s3c2440",				/* 该驱动的名称 */
	.id	= UCLASS_SERIAL,					/* 该驱动属于UCLASS_SERIAL这一类 */
	.of_match = s3c2440_serial_ids,			/* 用于与设备树中的节点匹配 */
	.probe = s3c2440_serial_probe,			/* 匹配成功后执行的探测函数 */
	.ops = &s3c2440_serial_ops,				/* 串口这一类驱动的操作函数集合 */
	.priv_auto_alloc_size = sizeof(struct s3c2440_serial_priv),		/* u-boot为该驱动分配的私有数据空间的大小 */
};

对于上面的内容,许多读者可能会感到很陌生,这里我想顺便说一下我自己的学习经验供大家借鉴。

当我移植进行到这里的时候看到u-boot有了这样一个新的设备模型框架,此时,我的做法并不是去网上查找这方面的内容,我直接找了几个同目录下使用该设备模型的串口驱动,经过对比研究,发现了它们的大致结构都是相同的(其实这里就是所谓的遵从同一框架),我就模仿着他们的代码框架,把相同的部分保留下来,其实剩下的不同的部分一般就是硬件相关的操作各种寄存器了,这样,我可以先不必去详细了解设备模型的框架原理,只关注于硬件操作,等之后有了一定的实践经验了,再回过头去详细了解设备模型的具体内容。这也符合人的认知过程,从感性到理性,而不是一上来就去研究设备模型的框架原理,把自己搞得很是难受,以至于根本没有信心去做接下来的事情了。

言归正传,这里我们可以先只了解每个变量的作用是什么,至于何时被调用之类的可以之后再去了解,免得给自己增加入门难度,打击自信心。

可以看到,每个变量的作用我已经在后面写了注释说明,其中比较重要的有三个变量:

  1. s3c2440_serial_ids

说到这个变量,不得不提到另一个除了设备模型外u-boot的新改变,那就是开始使用设备树。至于语法与linux的设备树完全相同,可以说就是借鉴linux而来。那这样的话,我们在arch/arm/dts目录下新建一个jz2440.dts的文件,

// SPDX-License-Identifier: GPL-2.0+
/*
 * Samsung's S3c2440-based JZ2440 board device tree source
 *
 * Copyright (c) 2020 Asymptote
 */

/dts-v1/;

#include "skeleton.dtsi"

/ {
	model = "JZ2440";
	compatible = "samsung,s3c2440";

    aliases {
		console = &serial0;
	};

	serial0: serial@50000000 {
		compatible = "samsung,s3c24x0-uart";
		reg = <0x50000000 0x100>;
	};
};

熟悉linux设备树的读者应该会感到很熟悉,这里我们:

  • 指定默认的串口设备为serial0

  • 添加串口设备的节点serial0,compatible对应s3c2440_serial_ids变量中的compatible成员,只要二者内容相同,u-boot设备模型框架便会为我们执行下面的s3c2440_serial_probe函数。

同时还需要在arch/arm/dts/Makefile文件中加入编译信息,这样我们的dts文件才会被编译,

......

dtb-$(CONFIG_TARGET_GURNARD) += at91sam9g45-gurnard.dtb

dtb-$(CONFIG_TARGET_JZ2440) += jz2440.dtb

dtb-$(CONFIG_S5PC100) += s5pc1xx-smdkc100.dtb

......

最后,还需要在jz2440_defconfig中加入设备树相关的配置信息,

# Architecture and machine
CONFIG_ARM=y
CONFIG_TARGET_JZ2440=y

# Link address
CONFIG_SYS_TEXT_BASE=0x33f00000

# Device tree
CONFIG_OF_CONTROL=y
CONFIG_OF_SEPARATE=y
CONFIG_DEFAULT_DEVICE_TREE="jz2440"
  • CONFIG_OF_CONTROL表示使用设备树;
  • CONFIG_OF_SEPARATE表示由dts编译成的dtb文件追加到u-boot.bin的最后面;
  • CONFIG_DEFAULT_DEVICE_TREE表示使用的默认dts文件名称前缀,因为可能对于有的单板会有好几个dts文件同时被编译,那这里便是指定默认使用哪一个;

关于更具体的设备树信息,可以参考如下博客,我当时就是参考的这个博客,在此也十分感谢这位博主:

https://blog.csdn.net/ooonebook/article/details/53206623

  1. s3c2440_serial_probe

只要与设备树中的对应节点匹配成功,此函数便会被调用。一般在这个函数中都是完成从设备树中获取设备信息,初始化硬件等等。

这里我们完成的功能主要有两点:

  1. 从设备树中获取串口的寄存器首地址,并存到设备私有结构体中,需要说明的是:
  • 该函数的参数struct udevice *dev:与linux的平台总线驱动模型中的xxx_probe函数的参数struct platform_device *pdev功能类似,同样是u-boot初始化的时候解析设备树,当匹配后就会将设备树中对应的设备信息转换成struct udevice结构体,并作为probe函数的参数最后再调用probe函数(这里就是我们赋值的s3c2440_serial_probe函数)。所以,我们可以从struct udevice *dev中获取到设备树中的关于该设备的所有信息,这里主要是寄存器首地址
  1. 初始化串口,需要说明的是:
  • 设置串口的gpio引脚时,这里是直接操作的寄存器。其实我已经实现了基于设备模型的pinctrl子系统驱动,但是这里为了简单起见,就直接操作寄存器了,后面我会讲解pinctrl子系统的驱动,之后我们再在设备树中利用pinctrl设置gpio为串口功能以及内部上拉功能
  • 设置串口的时钟时,也是直接操作的寄存器,我也已经实现了基于设备模型的clock子系统,同样为了简单起见,就直接操作寄存器了,后面讲解clock驱动后,再改回来
  1. s3c2440_serial_ops

这应该是该驱动的核心,主要实现了串口驱动常见的几个函数:

  • s3c2440_serial_putc:向pc串口终端打印一个字符
  • s3c2440_serial_getc:从pc串口终端获取一个字符
  • s3c2440_serial_pending:返回pc串口终端的输入状态给soc芯片
  • s3c2440_serial_setbrg:设置串口的波特率

这应该是再熟悉不过的几个串口驱动函数了,这里只说明一点,就是在设置波特率的时候,这里是直接指定给到串口的时钟频率为50MHz,如果要是正规来说,还是要使用clock子系统来进行设置,就像初始化时候一样,为了简化就直接指定了,后面讲解clock驱动后,同样会改回来。

另外,关于每个函数的参数struct udevice *dev与s3c2440_serial_probe函数的参数相同,我们在s3c2440_serial_probe函数中获取到寄存器的首地址,在ops的这几个函数中可以直接使用这个值。

8.2 修改Kconfig以及Makefile

最后,我们还需要修改Kconfig以及Makefile。

  • 在drivers/serial目录下的Kconfig中添加关于s3c2440串口驱动的配置信息,
......

config MTK_SERIAL
	bool "MediaTek High-speed UART support"
	depends on DM_SERIAL
	help
	  Select this to enable UART support for MediaTek High-speed UART
	  devices. This driver uses driver model and requires a device
	  tree binding to operate.
	  The High-speed UART is compatible with the ns16550a UART and have
	  its own high-speed registers.

config S3C2440_SERIAL
	bool "Samsung s3c2440 UART support"
	depends on DM_SERIAL
	help
	  This driver supports the Samsung s3c2440 UART. If unsure say N.

config MPC8XX_CONS
	bool "Console driver for MPC8XX"
	depends on MPC8xx
	default y

......
  • 在drivers/serial目录下的Makefile中添加关于s3c2440串口驱动的编译信息,
......

obj-$(CONFIG_MTK_SERIAL) += serial_mtk.o
obj-$(CONFIG_SIFIVE_SERIAL) += serial_sifive.o
obj-$(CONFIG_S3C2440_SERIAL) += serial_s3c2440.o

......

8.3 修改defconfig

我们还需要修改jz2440_defconfig:

  • 由于这是第一个设备模型驱动,需要配置相关宏定义
  • 添加关于串口驱动的宏定义,这里把波特率设为115200
# Architecture and machine
CONFIG_ARM=y
CONFIG_TARGET_JZ2440=y

# Link address
CONFIG_SYS_TEXT_BASE=0x33f00000

# Device tree
CONFIG_OF_CONTROL=y
CONFIG_OF_SEPARATE=y
CONFIG_DEFAULT_DEVICE_TREE="jz2440"

# Device Model
CONFIG_DM=y

# Serial driver
CONFIG_BAUDRATE=115200
CONFIG_DM_SERIAL=y
CONFIG_S3C2440_SERIAL=y

8.4 编译

好了,可以试着编译一把。
在这里插入图片描述说实话,这个错误当时可让我很是捉急,主要它没有明显的提示信息,我当时差点都想看这个编译器的源码了。。。

后来,我将这个错误贴到网上,最后发现了有个国外网友也遇到类似的问题,最后的解决方法是添加get_timer这个函数。

我结合之前有未定义的错误时也出现了这个断言失败的错误,感觉很大概率是由于get_timer这个函数未定义造成的,于是找到u-boot 2016版本中关于s3c2440的get_timer函数,最后放到了jz2440.c文件中,

// SPDX-License-Identifier: GPL-2.0+
/*
 *  Copyright (C) 2008-2009 Samsung Electronics
 *  Minkyu Kang <mk7.kang@samsung.com>
 *  Kyungmin Park <kyungmin.park@samsung.com>
 */

#include <common.h>
#include <init.h>
#include <asm/io.h>
#include <asm/mach-types.h>

DECLARE_GLOBAL_DATA_PTR;

int board_init(void)
{
	return 0;
}

int print_cpuinfo(void)
{
	printf("hello, u-boot!\n");
	
	return 0;
}

/*
 * This function is derived from PowerPC code (timebase clock frequency).
 * On ARM it returns the number of timer ticks per second.
 */
ulong get_tbclk(void)
{
	return CONFIG_SYS_HZ;
}

/*
 * reset the cpu by setting up the watchdog timer and let him time out
 */
void reset_cpu(ulong ignored)
{
#define	WTCON		0x53000000
#define	WTCNT		0x53000008

	/* Disable watchdog */
	writel(0x0000, WTCON);

	/* Initialize watchdog timer count register */
	writel(0x0001, WTCNT);

	/* Enable watchdog timer; assert reset at timer timeout */
	writel(0x0021, WTCON);

	while (1)
		/* loop forever and wait for reset to happen */;

	/*NOTREACHED*/
}

int dram_init(void)
{
	gd->ram_size = get_ram_size((long *)PHYS_SDRAM_1, PHYS_SDRAM_1_SIZE);

	return 0;
}

int dram_init_banksize(void)
{
	gd->bd->bi_dram[0].start = PHYS_SDRAM_1;
	gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;

	return 0;
}


/*
 * This function is derived from PowerPC code (read timebase as long long).
 * On ARM it just returns the timer value.
 */
unsigned long long get_ticks(void)
{
#define	TCNTO4		0x51000040

	ulong now = readl(TCNTO4) & 0xffff;

	if (gd->arch.lastinc >= now) {
		/* normal mode */
		gd->arch.tbl += gd->arch.lastinc - now;
	} else {
		/* we have an overflow ... */
		gd->arch.tbl += gd->arch.lastinc + gd->arch.tbu - now;
	}
	gd->arch.lastinc = now;

	return gd->arch.tbl;
}

ulong get_timer_masked(void)
{
	ulong tmr = get_ticks();

	return tmr / (gd->arch.timer_rate_hz / CONFIG_SYS_HZ);
}

/*
 * timer without interrupts
 */
ulong get_timer(ulong base)
{
	return get_timer_masked() - base;
}

再次编译,
在这里插入图片描述成功了!看来真是get_timer函数未定义的问题,但让人比较郁闷的是为何编译器它不提示呢?算了,这个问题已经超出我的能力范围了,既然已经解决了就不纠结了。

8.5 烧写

8.5.1 烧写spl.bin到nand flash

我们先利用openjtag将spl.bin烧写到nandflash的0地址处,这里相信大家都已经很熟练了,就不截图示范了。

8.5.2 烧写u-boot.bin到sdcard

这里需要用到一张sdcard(前面也说过,由于spl的sdcard裸机驱动有问题,现在只能支持4g及以下的卡)以及一个读卡器,将sdcard插到读卡器并将读卡器查到pc的usb端口后,在pc端的/dev目录下会出现一个sdx的设备节点,这里的x表示未知的意思,一般是最后一个,比如,我没有插读卡器的时候,/dev目录下sdx是这样的:
在这里插入图片描述
插上后是这样的:
在这里插入图片描述
也就是说多了一个sdb,至于后面的sdb1,sdb2是我自己为后面放linux的uImage以及根文件系统分的两个区,这个大家可以不用分区,只要有sdx(我这里是sdb)就可以。

接着,执行下面的命令烧写u-boot.bin到sdcard:

sleep 2
ls /dev/sd*
sudo dd if=/home/zhangxu/study/s3c2440/u-boot/u-boot.bin of=/dev/sdb bs=512 seek=4
sync

前面两条命令主要是因为有时候烧写会出现错误,所以我先延时了2秒钟,然后又列出了/dev/sd*,以防写错设备。

第三条命令就是烧写命令了,不熟悉dd命令的读者可以自行去网上查阅资料,这里我就不详细介绍了,大概说一下,这条命令的功能是:

  • 将/home/zhangxu/study/s3c2440/u-boot/u-boot.bin写到/dev/sdb(代表我的sdcard)
  • 写入的块大小为512字节(与spl中sdcard驱动程序中读写的块大小相同)
  • 空过前面4个块(也就是从sdcard绝对地址的第512 * 4 = 2048字节 = 2K字节处开始写入)

将以上命令写成一个脚本copy2sdcard.sh,方便之后的调试,然后执行之进行烧写,
在这里插入图片描述
一般速度在一点几M就是正常的,如果几百M的话就出现问题了,需要重新拔插读卡器再次进行烧写。

好了,烧写完毕,激动人心的时刻来临了。

8.6 运行

串口终端我使用的是Ubuntu下的picocom,波特率设为之前在jz2440_defconfig中设置的115200,
在这里插入图片描述好了,现在将sdcard插到jz2440的sd插槽,上电——
在这里插入图片描述yes!成功打印!

最上面的两行信息是我在spl中打印的调试信息,u-boot打印的信息从第三行开始。

此时我们也可以看到,在我们没有人为添加任何命令的情况下,u-boot默认为我们加入了很多的命令,下图只截取了一部分:
在这里插入图片描述正所谓万事开头难,我们现在已经完成了移植的开头工作,现在我们移植的u-boot仅仅只有打印的功能,比如,有的读者可能会问怎么没有倒计时的功能?这是因为我们还没有把这个功能加上,u-boot默认是不支持的,还有许多常用的功能都需要我们后面按需要进行添加。

8.7 写在后面

经过这个串口驱动,我想大家应该对u-boot的设备模型有了感性的认识,接下来,如果想上升到理性认识,想深入了解设备模型,推荐下面这篇博客:

https://blog.csdn.net/ooonebook/article/details/53234020

-END-

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值