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

由于论坛的篇幅限制我将此文章分为了四篇,本文为第四篇。 整篇文章遵循着以问题为导向的讲解方式,重实践,基本没有原理性的知识,并且从零开始进行移植。我相信,如果你能认真的跟着文章实际操作一把,以后不管遇到什么类型的芯片、什么类型的开发板,同样都能完成从零移植u-boot的工作。

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

   2. 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中获取到设备树中的关于该设备的所有信息,这里主要是寄存器首地址

      2. 初始化串口,需要说明的是:

  •            - 设置串口的gpio引脚时,这里是直接操作的寄存器。其实我已经实现了基于设备模型的pinctrl子系统驱动,但是这里为了简单起见,就直接操作寄存器了,后面我会讲解pinctrl子系统的驱动,之后我们再在设备树中利用pinctrl设置gpio为串口功能以及内部上拉功能

  •            - 设置串口的时钟时,也是直接操作的寄存器,我也已经实现了基于设备模型的clock子系统,同样为了简单起见,就直接操作寄存器了,后面讲解clock驱动后,再改回来

   3. 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 编译

好了,可以试着编译一把。

attachments-2020-06-yhdxqYIi5ee11d7e3dada.jpg

说实话,这个错误当时可让我很是捉急,主要它没有明显的提示信息,我当时差点都想看这个编译器的源码了。。。

后来,我将这个错误贴到网上,最后发现了有个国外网友也遇到类似的问题,最后的解决方法是添加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)
 {
 #defineWTCON0x53000000
 #defineWTCNT0x53000008
 
 /* 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)
 {
 #defineTCNTO40x51000040
 
 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是这样的:

attachments-2020-06-xIvoo0Ol5ee11da09b5d4.jpg

插上后是这样的:

attachments-2020-06-M2XXs2hw5ee11db16f227.jpg

也就是说多了一个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,方面之后的调试,然后执行之进行烧写,

attachments-2020-06-twyw8sE75ee11ddf288cb.jpg

一般速度在一点几M就是正常的,如果几百M的话就出现问题了,需要重新拔插读卡器再次进行烧写。

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

8.6 运行

串口终端我使用的是Ubuntu下的picocom,波特率设为之前在jz2440_defconfig中设置的115200,

attachments-2020-06-ss3nbT9w5ee11dec28cca.jpg

好了,现在将sdcard插到jz2440的sd插槽,上电——

attachments-2020-06-C7Sr5ts15ee11e239e2c9.jpg

yes!成功打印!

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

此时我们也可以看到,在我们没有人为添加任何命令的情况下,u-boot默认为我们加入了很多的命令,下图只截取了一部分:

正所谓万事开头难,我们现在已经完成了移植的开头工作,现在我们移植的u-boot仅仅只有打印的功能,比如,有的读者可能会问怎么没有倒计时的功能?这是因为我们还没有把这个功能加上,u-boot默认是不支持的,还有许多常用的功能都需要我们后面按需要进行添加。

8.7 写在后面

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

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


~END~



  • 发表于 2020-06-11 01:54
  • 阅读 ( 558 )
  • 分类:经验分享

0 条评论&回复

请先 登录 后评论
渐进
渐进

12 篇文章

作家榜 »

  1. 百问网-周老师 18 文章
  2. st_ashang 14 文章
  3. 渐进 12 文章
  4. zxq 11 文章
  5. helloworld 8 文章
  6. 谢工 5 文章
  7. Litchi_Zheng 5 文章
  8. 星星之火 5 文章