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有了这样一个新的设备模型框架,此时,我的做法并不是去网上查找这方面的内容,我直接找了几个同目录下使用该设备模型的串口驱动,经过对比研究,发现了它们的大致结构都是相同的(其实这里就是所谓的遵从同一框架),我就模仿着他们的代码框架,把相同的部分保留下来,其实剩下的不同的部分一般就是硬件相关的操作各种寄存器了,这样,我可以先不必去详细了解设备模型的框架原理,只关注于硬件操作,等之后有了一定的实践经验了,再回过头去详细了解设备模型的具体内容。这也符合人的认知过程,从感性到理性,而不是一上来就去研究设备模型的框架原理,把自己搞得很是难受,以至于根本没有信心去做接下来的事情了。
言归正传,这里我们可以先只了解每个变量的作用是什么,至于何时被调用之类的可以之后再去了解,免得给自己增加入门难度,打击自信心。
可以看到,每个变量的作用我已经在后面写了注释说明,其中比较重要的有三个变量:
- 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
- s3c2440_serial_probe
只要与设备树中的对应节点匹配成功,此函数便会被调用。一般在这个函数中都是完成从设备树中获取设备信息,初始化硬件等等。
这里我们完成的功能主要有两点:
- 从设备树中获取串口的寄存器首地址,并存到设备私有结构体中,需要说明的是:
- 该函数的参数struct udevice *dev:与linux的平台总线驱动模型中的xxx_probe函数的参数struct platform_device *pdev功能类似,同样是u-boot初始化的时候解析设备树,当匹配后就会将设备树中对应的设备信息转换成struct udevice结构体,并作为probe函数的参数最后再调用probe函数(这里就是我们赋值的s3c2440_serial_probe函数)。所以,我们可以从struct udevice *dev中获取到设备树中的关于该设备的所有信息,这里主要是寄存器首地址
- 初始化串口,需要说明的是:
- 设置串口的gpio引脚时,这里是直接操作的寄存器。其实我已经实现了基于设备模型的pinctrl子系统驱动,但是这里为了简单起见,就直接操作寄存器了,后面我会讲解pinctrl子系统的驱动,之后我们再在设备树中利用pinctrl设置gpio为串口功能以及内部上拉功能
- 设置串口的时钟时,也是直接操作的寄存器,我也已经实现了基于设备模型的clock子系统,同样为了简单起见,就直接操作寄存器了,后面讲解clock驱动后,再改回来
- 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-